Побитовые операции — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Подсчет количества единичных битов)
(Источники информации)
Строка 208: Строка 208:
 
* [https://graphics.stanford.edu/~seander/bithacks.html Bit Twiddling Hacks by Sean Eron Anderson]
 
* [https://graphics.stanford.edu/~seander/bithacks.html Bit Twiddling Hacks by Sean Eron Anderson]
 
* [https://habrahabr.ru/post/93172/ Habrahabr {{---}} Алгоритмы поиска старшего бита]
 
* [https://habrahabr.ru/post/93172/ Habrahabr {{---}} Алгоритмы поиска старшего бита]
 
+
* [https://yesteapea.wordpress.com/2013/03/03/counting-the-number-of-set-bits-in-an-integer/ STP's blog {{---}} Counting the number of set bits in an integer]
 
[[Категория: Дискретная математика и алгоритмы]]
 
[[Категория: Дискретная математика и алгоритмы]]
  
 
[[Категория: Булевы функции ]]
 
[[Категория: Булевы функции ]]

Версия 22:01, 20 марта 2016

Побитовые операции (англ. bitwise operations) — операции, производимые над цепочками битов. Выделяют два типа побитовых операций: логические операции и побитовые сдвиги.

Принцип работы

Логические побитовые операции

Битовые операторы И [math](AND,\ \&)[/math], ИЛИ [math](OR,\ \mid)[/math], НЕ [math](NOT,\ \sim)[/math] и исключающее ИЛИ [math](XOR,\ $\textasciicircum$,\ \oplus)[/math] используют те же таблицы истинности, что и их логические эквиваленты.

Побитовое И

Побитовое И используется для выключения битов. Любой бит, установленный в [math]0[/math], вызывает установку соответствующего бита результата также в [math]0[/math].

&
11001010
11100010
11000010

Побитовое ИЛИ

Побитовое ИЛИ используется для включения битов. Любой бит, установленный в [math]1[/math], вызывает установку соответствующего бита результата также в [math]1[/math].

|
11001010
11100010
11101010

Побитовое НЕ

Побитовое НЕ инвертирует состояние каждого бита исходной переменной.

~
11001010
00110101

Побитовое исключающее ИЛИ

Исключающее ИЛИ устанавливает значение бита результата в [math]1[/math], если значения в соответствующих битах исходных переменных различны.

^
11001010
11100010
00101000

Побитовые сдвиги

Операторы сдвига [math]\texttt{\lt \lt }[/math] и [math]\texttt{\gt \gt }[/math] сдвигают биты в переменной влево или вправо на указанное число. При этом на освободившиеся позиции устанавливаются нули (кроме сдвига вправо отрицательного числа, в этом случае на свободные позиции устанавливаются единицы, так как числа представляются в двоичном дополнительном коде и необходимо поддерживать знаковый бит).

Сдвиг влево может применяться для умножения числа на два, сдвиг вправо — для деления.

x = 7         // 00000111 (7)
x = x >> 1    // 00000011 (3)
x = x << 1    // 00000110 (6)
x = x << 5    // 11000000 (-64)
x = x >> 2    // 11110000 (-16)

В языке программирования Java существует также оператор беззнакового битового сдвига вправо [math]\texttt{\gt \gt \gt }[/math]. При использовании этого оператора на освободившиеся позиции всегда устанавливаются нули.

x = 7          // 00000111 (7)
x = x << 5     // 11100000 (-32)
x = x >>> 2    // 00111000 (56)

Применение

Сложные операции

Определение знака числа

Пусть дано число [math]x[/math]. Поскольку при сдвиге вправо на освобождающиеся позиции устанавливается бит знака, знак числа [math]x[/math] можно определить, выполнив сдвиг вправо на всю длину переменной:

// n — разрядность числа

if x != 0
    mask = 1
else
    mask = 0

sign = mask | (x >> (n - 1))    // результатом будет -1, 0, или +1 
                                                     // для отрицательного, равного нулю и положительного числа x соответственно

Используя побитовые операции можно также узнать, различны ли знаки двух переменных [math]x[/math] и [math]y[/math]. Если числа имеют различный знак, то результат операции XOR, произведенной над их знаковыми битами, будет единицей. Поэтому неравенство [math]((x \oplus y) \lt 0[/math] будет верно в том случае, если числа [math]x[/math] и [math]y[/math] разного знака.

Вычисление модуля числа без использования условного оператора

Пусть дано число [math]x[/math]. Если [math]x[/math] положительно, то [math]mask = 0[/math], и [math](x + mask) \oplus mask = x[/math]. В случае, если [math]x[/math] отрицательно, [math]mask = -1[/math]. Тогда получается, что мы работаем с числом [math]x[/math] так, как будто оно представлено в коде со сдвигом с тем отличием, что у нас знаковый бит принимает значение [math]1[/math] для отрицательных чисел, а [math]0[/math] — для положительных.

// n — разрядность числа
mask = x >> n - 1

abs = (x + mask) [math]\oplus[/math] mask
// другой способ сделать то же самое:
abs = (x [math]\oplus[/math] mask) - mask

Нахождение минимума и максимума из двух чисел без использования условного оператора

Этот способ корректен только если можно утверждать, что величина [math](x - y)[/math] лежит между граничными значениями типа int.

Пусть даны числа [math]x[/math] и [math]y[/math]. Тогда, если [math]x \lt y[/math], то [math]((x - y) \texttt{\gt \gt } (n - 1)) = -1[/math] а если [math]x \geqslant y[/math], то [math]((x - y) \texttt{\gt \gt } (n - 1)) = 0[/math]. Выражение [math]((x - y) \& ((x - y) \texttt{\gt \gt } (n - 1))[/math] принимает значение [math]0[/math], если [math]x \geqslant y[/math] и [math](x - y)[/math], если [math]x \lt y[/math].

// n — разрядность чисел
min = y + ((x - y) & ((x - y) >> (n - 1)))
max = x - ((x - y) & ((x - y) >> (n - 1)))

Проверка на то, является ли число степенью двойки

Пусть дано число [math]x[/math]. Тогда, если результатом выражения [math](x\ \&\&\ !(x\ \&\ (x - 1)))[/math] является единица, то число [math]x[/math] — степень двойки.

Правая часть выражения [math](!(x\ \&\ (x - 1)))[/math] будет равна единице только если число [math]x[/math] равно [math]0[/math] или является степенью двойки. Если число [math]x[/math] является степенью двойки, то в двоичной системе счисления оно представляется следующим образом: [math]1\underbrace{0\dots0}_{n}[/math], где [math]n[/math] — показатель степени. Соответственно, выражение [math](x - 1)[/math] будет иметь вид [math]\underbrace{1\dots1}_{n}[/math], и [math]x\ \&\ (x - 1)[/math] равно [math]0[/math].

Операция логического И в данном выражении отсекает тот случай, когда [math](x = 0)[/math] и не является степенью двойки, но при этом правая часть [math](!(x\ \&\ (x - 1)))[/math] равна единице.

Нахождение младшего единичного бита

Пусть дано число [math]x[/math] и необходимо узнать его младший единичный бит.

Применим к числу [math]x[/math] побитовое отрицание, чтобы инвертировать значения всех его бит, а затем прибавим к полученному числу единицу. У результата первая часть (до младшего единичного бита) не совпадает с исходным числом [math]x[/math], а вторая часть совпадает. Применив побитовое И к этим двум числам, получим степень двойки, соответствующую младшему единичному биту исходного числа [math](x\ \&\ (\sim x + 1))[/math].

К такому же результату можно прийти, если сначала отнять от числа [math]x[/math] единицу, чтобы обнулить его младший единичный бит, а все последующие разряды обратить в [math]1[/math], затем инвертировать результат и применить побитовое И с исходным числом [math](x\ \&\ \sim (x - 1))[/math].

Нахождение старшего единичного бита

Пусть дано число [math]x[/math] и необходимо узнать его старший единичный бит.

Рассмотрим некоторое число, представим его как [math]0\dots01b \dots b[/math], где [math]b[/math] — любое значение бита. Тогда, если совершить битовый сдвиг этого числа вправо на [math]1[/math] и произвести побитовое ИЛИ результата сдвига и исходного числа, мы получим результат [math]0\dots011b \dots b[/math]. Если мы повторим эту последовательность действий над полученным числом, но устроим сдвиг на [math]2[/math], то получим [math]0\dots01111b \dots b[/math]. При каждой следующей операции будем увеличивать модуль сдвига до следующей степени двойки. После некоторого количества таких операций (зависит от разрядности числа) мы получим число вида [math]0\dots01\dots1[/math]. Тогда результатом выполнения действий [math]x - (x \texttt{ \gt \gt }1)[/math] будет число, состоящее только из старшего бита исходного числа.

int power = 1
for i = 1..[math]\log_2{n}[/math]:    // n — разрядность числа
    x |= x >> power
    power <<= 1
result = x - (x >> 1)

Подсчет количества единичных битов

Для подсчета количества единичных битов в числе [math]x[/math] можно воспользоваться следующим алгоритмом:

// Пример приведен для 16-ти битных чисел. 
// Для чисел большей разрядности необходимо использовать соответствующие константы.
x = x - ((x >>> 1) & 0x5555)
x = (x & 0x3333) + ((x >>> 2) & 0x3333)
x = (x + (x >>> 4)) & 0x0F0F
answer = (x * 0x0101) >>> 8

Поскольку [math]5555_{16}[/math] равно [math]01010101 01010101_{2}[/math], результатом операции [math]x\ \&\ 5555_{16}[/math] является число, в котором все нечетные биты соответствуют нечетным битам числа [math]x[/math]. Аналогично, результатом операции [math](x\ \texttt{\gt \gt \gt }\ 1)\ \&\ 5555_{16}[/math] является число, в котором все нечетные биты соответствуют четным битам [math]x[/math]. Четные биты результата в обоих случаях равны нулю.

Мысленно разобьем двоичную запись нашего числа [math]x[/math] на группы по [math]2[/math] бита. Результатом операции [math]x\ \&\ 5555_{16} + (x\ \texttt{\gt \gt \gt }\ 1)\ \&\ 5555_{16}[/math] будет такое число, что если разбить его двоичную запись на группы по два бита, значение каждой группы соответствует количеству единичных битов в соответствующей паре битов числа [math]x[/math].

Аналогично, число [math]3333_{16}[/math] равно [math]00110011 00110011_{2}[/math] и операция [math]x = (x\ \&\ 3333_{16}) + (x\ \texttt{\gt \gt \gt }\ 2\ \&\ 3333_{16})[/math], примененная к результату, полученному на первом этапе, выполняет подсчет количества единичных битов в блоках по [math]4[/math]. В свою очередь, число [math]\texttt{0F0F}_{16}[/math] равно [math]00001111 00001111_{2}[/math] и операция [math]x = (x\ \&\ \texttt{0F0F}_{16}) + (x\ \texttt{\gt \gt \gt }\ 4\ \&\ \texttt{0F0F}_{16})[/math] позволяет подсчитать число единичных бит в блоках по [math]8[/math].

Теперь необходимо просуммировать числа, записанные в блоках по [math]8[/math] битов, чтобы получить искомую величину. Это можно сделать, домножив результат на [math]0101_{16}[/math] [math](1 00000001_{2})[/math]. Ответ на задачу будет находиться в первых восьми битах произведения. Выполнив сдвиг вправо на [math]8[/math] (для шестнадцатибитных чисел), мы получим долгожданный ответ.

Подведем итог:

x = (x & 0x5555) + ((x >>> 1) & 0x5555)
x = (x & 0x3333) + ((x >>> 2) & 0x3333)
x = (x & 0x0F0F) + ((x >>> 4) & 0x0F0F)
answer = (x * 0x0101) >>> 8

Заметим, что операция [math]x\ \&\ 55_{16} + (x\ \texttt{\gt \gt \gt }\ 1)\ \&\ 55_{16}[/math] равносильна операции [math]x - (x\ \texttt{\gt \gt \gt }\ 1)\ \&\ 55_{16}[/math], в чем легко убедиться, рассмотрев все числа из двух бит.

В свою очередь, операцию [math](x\ \&\ \texttt{0F0F}_{16}) + ((x\ \texttt{\gt \gt \gt }\ 4)\ \&\ \texttt{0F0F}_{16})[/math] можно заменить на [math](x + (x\ \texttt{\gt \gt \gt }\ 4))\ \&\ \texttt{0F0F}_{16}[/math]. Эта замена не повлияет на результат, так как максимальное значение в любой группе из четырех битов данного числа равно четырем, то есть требует только трех битов для записи, и выполнение суммирования не повлечет за собой переполнения и выхода за пределы четверок.

Таким образом, мы получили код, приведенный в начале раздела.

Циклический сдвиг влево

Пусть дано число [math]x[/math] и надо совершить циклический сдвиг его битов влево. Желаемый результат можно получить, если объединить числа, полученные при выполнении обычного битового сдвига влево на необходимую величину и вправо на разность между разрядностью числа и величиной сдвига. Таким образом, мы сможем поменять местами начальную и конечную части числа.

int head, tail
head = x << a          // x — изменяемое число
                       // a — число позиций, на которое хотим выполнить сдвиг
if a != 0:
    tail = x >> (n - a)    // n — разрядность числа x
else:
    tail = 0
result = head | tail

Разворот битов

Чтобы получить биты числа [math]x[/math], записанные в обратном порядке, будем снимать значение младшего бита, умножать его на старшую незаписанную степень двойки и сдвигать число вправо на единицу для обработки следующих бит.

result = 0
position = 1 << (n - 1)    // n — разрядность числа x
while x != 0:
    result += (x & 1) * position
    position >>= 1
    x >>= 1

Применение для решения задач

Работа с битовыми масками

Для работы с подмножествами удобно использовать битовые маски. Применяя побитовые операции легко сделать следующее: найти дополнение [math](\sim mask)[/math], пересечение [math](mask_1\ \&\ mask_2)[/math], объединение [math](mask_1 \mid mask_2)[/math] множеств, установить бит по номеру [math](mask \mid (1\ \texttt{\lt \lt }\ x))[/math], снять бит по номеру [math](mask\ \&\ \sim(1\ \texttt{\lt \lt }\ x))[/math].

Битовые маски используются, например, при решении некоторых задач[1] динамического программирования.

Алгоритм Флойда

Основная статья: Алгоритм Флойда

Дерево Фенвика

Основная статья: Дерево Фенвика

Примечания

Источники информации