Разбор задач
3адача1
Напишите функцию, которая подсчитывает количество единичных битов в данном четырехбайтовом целом числе. Функция должна быть оптимизирована по скорости и использовать возможно меньше памяти.
Решение:
int bitCounter1(int N) {
int count = 0;
for (int i = 0; i < 32; i++) {
count += N & 1;
N = N >> 1;
}
return count;
}
В этой функции используется count += N & 1, а не count += N % 2 потому что операция N & 1 возвращает
0 или 1, тогда как N % 2 может вернуть -1, в зависимости от знака N.
int bitCounter2(int N) {
int count = 0;
while (N != 0) {
count += N & 1;
N = N >>> 1;
}
}
В этой функции, в отличие от предыдущей, используется >>>, так как при сдвиге >> в начало числа добавляются биты, соответствующие знаку
числа, а при сдвиге >>> всегда добавляются нули. Соответственно, в BitCounter1 не важно какие биты записываются в начало числа,
так как используется цикл for, выполняющий конечное число действий. А в BitCounter2 используется цикл while,
для выхода из которого нужно получить 0, и, следовательно, необходимо чтобы в начало числа записывались нули.
В предыдущей функции проверяются все биты.
Попробуем написать функцию, которая за один шаг уменьшала бы количество единичных битов в числе на 1. Для этого рассмотрим выражение число (N - 1).
Это число, отличается от N тем, что вместо последнего единичного бита у него 0, а все последующие биты равны 1. Тогда N & (N - 1) — это
число, у которого число единичных битов меньше на 1.
| N | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
| N - 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 1 |
| N & (N - 1) | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
Воспользовавшись выражением N = N & (N - 1) получим функцию:
int bitCounter 3(int N) {
int count = 0;
while (N != 0) {
count ++;
N = N & (N - 1)
}
}
Еще один способ
Рассмотрим что делают с числом N следующие операции: N = (N & 0x55555555) + ((N >>> 1) & 0x55555555).
Число 0x55555555 = (01010101010101010101010101010101)
Соответственно (N & 0x55555555) обнуляет нечетные биты числа, а ((N >>> 1) & 0x55555555) — четные. Результатом сложения
полученных чисел будет такое число, в каждой паре бит которого записано количество единиц в этой паре.
| N | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
| N & 0x55555555 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
| (N>>1) & 0x55555555 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
| (N & 0x55555555) + ((N>>1) & 0x55555555) | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 |
Теперь воспользуемся числом 0x33333333 = (00110011001100110011001100110011) , чтобы записать в каждую четверку бит количество единиц в ней,
числом 0x0F0F0F0F = (00001111000011110000111100001111) для записи количества единичных бит в группу из 8 бит,
числом 0x00FF00FF = (00000000111111110000000011111111) для записи количества единичных бит в группу из 16 бит и
числом 0x0000FFFF = (00000000000000001111111111111111)
для записи количества единичных бит в группу из 32 бит, то есть в само число.
int bitCounter4 (int N) {
N = (N & 0x55555555) + ((N >>> 1) & 0x55555555);
N = (N & 0x33333333) + ((N >>> 2) & 0x33333333);
N = (N & 0x0F0F0F0F) + ((N >>> 4) & 0x0F0F0F0F);
N = (N & 0x00FF00FF) + ((N >>> 4) & 0x00FF00FF);
N = (N & 0x0000FFFF) + ((N >>> 4) & 0x0000FFFF);
return N;
}
Задача 2
Напишите функцию, которая подсчитывает количество единичных байтов в данном очень длинном(скажем, из миллиарда элементов) массиве байтов. Функция должна быть оптимизирована по скорости. Использование функций, аналогичных задаче 1, для подсчета битов в каждом элементе не эффективно. Для повышения эффективности заведем вспомогательный массив или воспользуемся оператором switch.
Решение 1Заведем массив int table[256], значения которого равны количеству единиц в двоичной записи числа типа byte. Нумерация
массива начинается с 0, а числа типа byte принимают значения от -128 до 127. Заполнять массив будем следующим образом: первому элементу
массива будет соответствовать количество единиц, использующихся в записи числа -128, второму элементу — количество единиц, использующихся при записи
числа -127 и так далее. Соответственно, по индексу index + 128 мы сможем получить количество единиц в числе index. Тогда для
подсчета битов необходимо достаточно бежать по исходному массиву и складывать значения массива table[index + 128].
int table[256];
int bitCounter1(byte[] array) {
int count = 0;
for (int i = 0; i < N; i++) {
count += table[array[i] + 128];
}
return count;
}
Решение 2
В качестве другого решения рассмотрим функцию, которая возвращает количество единичных бит в числе с помощью оператора switch().
int oneByteBitCounter(byte value) {
switch(value) {
case 0: return 0;
case 1: return 1;
case 2: return 1;
case 3: return 0;
case 4: return 1;
...
}
}
int bitCounter2(byte[] array) {
int count - 0;
for (int i = 0; i < N; i++) {
count += oneByteBitCounter(array[i]);
}
return count;
}
Оба эти решения очень похожи, но в Java быстрее будет работать 2 вариант, так как при каждом обращении к массиву, Java проверяет, не выходим ли мы за границы. В С,С++ например, работать будет одинаково.
Стандартные структуры данных.
Изучение языка начинается с переменных. Когда переменных не хватает, то используют массивы. Простой массив — проще, чем структура данных. Но массив не всегда удобен, потому что, например, в процессе работы нельзя изменить его размер.
1.Массив переменной длины (линейные структура данных)
Для начала, рассмотрим какие операции можно сделать с обычным массивом:
- создать (инициализировать) массив (A = new int[10])
- получить значение элемента массива по индексу (int k = A[i])
- записать значения элемента массива по индексу (A[i] = 8)
- получить длины массива (int length = A.length)
У массива переменной длины больше возможностей:
- Создание (инициализация) массива нулевой длины
- Добавление нового элемента в конец массива
- Получение длины массива
- Получение значения элемента массива по индексу
- Изменение элемента массива по индексу
- Добавление элемента в массив в указанное место по индексу
- Удаление элемента из массива по индексу
Реализация:
Обычный массив int[200] выглядит следующим образом. Это 800 байт, расположенных в памяти подряд. При работе с таким массивом могут возникнуть как минимум 2 проблемы:
- столько свободной памяти может не оказаться
- в процессе работы может понадобиться массив большей длины. Поскольку длину массива не увеличить, придется завести еще один массив.
Рассмотрим реализации массива переменной длины.
1.СписокКаждый элемент списка представляет собой пару: значение элемента, например int, и указатель на следующий элемент списка, или указатель на null, если данный элемент является последним в списке. Таким образом, список представляет собой цепочку элементов. При такой реализации не будет проблем с выделением памяти, так как создать элемент можно в любом месте памяти, а о положении этого элемента можно будет узнать с помощью указателя на него.
Отступление:
Введем понятие O(n).
f(n) = O(g(n)), если существуют m, M такие что для любого n m * g(n) < f(n) < M * g(n).
f1 = O(lg(n)) и f2 = O(ln(n)) — одно и тоже. Их отношение константа.
Заметим, что O(n) не всегда быстрее O(n2). 109*n при малых n больше, чем 10-9*n2.
O(1) — константное время
O(n) — линейное время
O(n2) — квадратичное время
Посмотрим на время работы операций для стандартной реализации массива переменной длины:
- Получение длины массива — O(n), в стандартных реализациях длина хранится отдельно
- Получение значения элемента по индексу — для получения значения необходимо пробежаться по списку до элемента index, следовательно — O(index)
- Изменение значения элемента по индексу — для изменения значения элемента необходимо пробежаться по списку до элемента inldex и лишь потом изменить его, следовательно — O(index)
- Добавление элемента в массив — если добавляем элемент в конец массива и не храним указатель на конец списка, то O(N), если храним — то O(1). Если добавляем в начало — O(1).
- Вставка элемента в массив по индексу — O(index)
- Удаление элемента из массива по индексу — O(index)
Рассмотрим реализацию массива переменной длины в Java 5.0.
В Java существует встроенный тип LinkedList<Integer>. <Integer> означает, что в данном массиве будут храниться элементы типа int. Так же можно использовать <Byte> — для элементов типа byte, <Char> — для элементов типа char.Для использование встроенного типа необходимо до объявления класса написать import java.util.LinkedList.
Рассмотрим несколько операций LinkedList.
import java.util.LinkedList;
LinkedList<Integer> list;
list = new LinkedList<Integer>(); // создание(инициализация) массива
list.add(25); // добавление элемента в конец массива
list.size(); // получение длины массива
list.isEmpty(); // проверка массива на наличие в нем элементов
list.get(index); // получение значения элемента массива по индексу
list.set(index, value); // изменение элемента массива по индексу
list.add(index, value); // добавление элемента в массив по индексу
list.remove(index); // удаление элемент из массива по индексу
Замечание: В Java предусмотрены некоторые дополнительные возможности для работы с типом LinkedList. Часто возникают ситуации, когда необходимо пробежаться по всему списку, например, необходимо посчитать сумму элементов списка. Если для этого использовать функцию get(ndex), то время работы такого цикла будет квадратичным.
| пример плохого кода: | В Java можно: |
|---|---|
|
|
| работает за O(n2) | работает за O(n). |
