Разбор задач

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.Массив переменной длины (линейные структура данных)

Для начала, рассмотрим какие операции можно сделать с обычным массивом:

У массива переменной длины больше возможностей:

Реализация:

Обычный массив 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) — квадратичное время

Посмотрим на время работы операций для стандартной реализации массива переменной длины:

Рассмотрим реализацию массива переменной длины в 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 можно:
 
  for (int i = 0; i < list.size; i++){
      sum += list.get(i);
  }

 for (int i : list) {
      sum += i;
  }
работает за O(n2) работает за O(n).