Как работают битовые манипуляции в этом коде сортировки битов?

Джон Бентли в колонке 1 своей книги "Жемчужины программирования" представляет методику сортировки последовательности ненулевых натуральных чисел с использованием битовых векторов.

Я взял программу bitsort.c из здесь и вставил ее ниже:

/* Copyright (C) 1999 Lucent Technologies */
/* From 'Programming Pearls' by Jon Bentley */

/* bitsort.c -- bitmap sort from Column 1
 *   Sort distinct integers in the range [0..N-1]
 */

#include <stdio.h>

#define BITSPERWORD 32
#define SHIFT 5
#define MASK 0x1F
#define N 10000000

int a[1 + N/BITSPERWORD];

void set(int i) 
{
    int sh = i>>SHIFT;
    a[i>>SHIFT] |=  (1<<(i & MASK)); 
}
void clr(int i) { a[i>>SHIFT] &= ~(1<<(i & MASK)); }
int  test(int i){ return a[i>>SHIFT] &   (1<<(i & MASK)); }

int main()
{   int i;
for (i = 0; i < N; i++)
    clr(i);

    /*Replace above 2 lines with below 3 for word-parallel init
    int top = 1 + N/BITSPERWORD;
    for (i = 0; i < top; i++)
    a[i] = 0;
    */

while (scanf("%d", &i) != EOF)
    set(i);
for (i = 0; i < N; i++)
        if (test(i))
    printf("%d\n", i);
return 0;
}

Я понимаю, что делают функции clr, set и test, и объясню их ниже: (пожалуйста, исправьте меня, если я ошибаюсь).

  • clr очищает i-й бит
  • set устанавливает i-й бит
  • test возвращает значение в i-м бите

Теперь я не понимаю, как функции делают то, что они делают. Я не могу понять все битовые манипуляции, происходящие в этих трех функциях.

Ответ 1

Первые 3 константы взаимосвязаны. BITSPERWORD - 32. Это вы хотите установить на основе вашей архитектуры компилятора+. SHIFT равен 5, потому что 2 ^ 5 = 32. Наконец, MASK - это 0x1F, который равен 11111 в двоичном (т.е.: все 5 бит установлены). Эквивалентно, MASK = BITSPERWORD - 1.

Битрейт концептуально представляет собой только массив бит. Эта реализация фактически использует массив ints и предполагает 32 бита на int. Поэтому, когда мы хотим установить, очистить или проверить (прочитать) немного, нам нужно выяснить две вещи:

  • который int (из массива) находится в
  • какой из этих битов int мы говорим о

Поскольку мы принимаем 32 бита на int, мы можем просто делить на 32 (и обрезать), чтобы получить требуемый индекс массива. Разделение на 32 (BITSPERWORD) совпадает с сдвигом вправо на 5 (SHIFT). Итак, о чем бит [i → SHIFT]. Вы также можете написать это как [i/BITSPERWORD] (и на самом деле вы, вероятно, получите тот же или очень похожий код, если ваш компилятор имеет разумный оптимизатор).

Теперь, когда мы знаем, какой элемент мы хотим, нам нужно выяснить, какой бит. В самом деле, мы хотим остаться. Мы могли бы сделать это с помощью i% BITSPERWORD, но оказывается, что я & MASK эквивалентен. Это потому, что BITSPERWORD - это сила 2 (2 ^ 5 в этом случае), а MASK - это всего лишь 5 бит.

Ответ 2

В основном оптимизирован вид корзины:

  • зарезервировать бит массива длины n  биты.
  • очистить бит массива (сначала в основном).
  • читайте элементы поодиночке (все они должны быть разными).
    • установите i-й бит в битовом массиве, если прочитанным номером является i.
  • итерация битового массива.
    • если бит установлен, затем распечатайте позицию.

Или, другими словами (для N < 10 и сортировать 3 числа 4, 6, 2) 0

начните с пустого 10-битного массива (ака одно целое)

0000000000

прочитайте 4 и установите бит в массиве.

0000100000

прочитайте 6 и установите бит в массиве

0000101000

прочитайте 2 и установите бит в массиве

0010101000

итерация массива и печать каждой позиции, в которой биты установлены на один.

2, 4, 6

отсортирован.

Ответ 3

Начиная с set():
Правый сдвиг 5 совпадает с делением на 32. Он делает это, чтобы найти, в каком int бит находится. MASK - это 0x1f или 31. Инициация с адресом дает бит индекс внутри int. Это то же самое, что и остаток деления адреса на 32.
Смещение 1, оставшееся от битового индекса ( "1 < < (i и MASK)" ), приводит к целому числу, которое имеет только 1 бит в заданной позиции.
ORing устанавливает бит.
Строка "int sh = я → SHIFT;" это потерянная линия, потому что они не использовали sh снова под ней, а вместо этого просто повторяли "i → SHIFT"

clr() в основном то же самое, что и set, за исключением того, что вместо ORing с 1 < < (i и MASK), чтобы установить бит, это AND с инверсией для очистки бит. test() AND с 1 < (i и MASK) для проверки бит.

Битовый набор также удалит дубликаты из списка, поскольку он будет считать только до 1 на целое число. Сорт, который использует целые числа вместо битов для подсчета более одного из них, называется сортировкой по основанию.

Ответ 4

Бит-магия используется как специальная схема адресации, которая хорошо работает с размерами строк, которые имеют две степени.

Если вы попытаетесь понять это (обратите внимание: я скорее использую биты за строку, чем бит-за-слово, поскольку мы говорим о битовой матрице здесь):

// supposing an int of 1 bit would exist...
int1 bits[BITSPERROW * N]; // an array of N x BITSPERROW elements

// set bit at x,y:
int linear_address = y*BITSPERWORD + x;
bits + linear_address = 1; // or 0
// 0 1 2 3 4 5 6 7 8 9 10 11 ... 31
// . . . . . . . . . .  .  .       .  
// . . . . X . . . . .  .  .       .  -> x = 4, y = 1 => i = (1*32 + 4)

Утверждение linear_address = y*BITSPERWORD + x также означает, что x = linear_address % BITSPERWORD и y = linear_address / BITSPERWORD.

Когда вы оптимизируете это в памяти, используя 1 слово из 32 бит на строку, вы получаете тот факт, что бит в столбце x можно установить с помощью

int bitrow = 0;
bitrow |= 1 << (x);

Теперь, когда мы перебираем биты, у нас есть линейный адрес, но нужно найти соответствующее слово.

int column = linear_address % BITSPERROW;
int bit_mask =  1 << column; // meaning for the xth column, 
                             // you take 1 and shift that bit x times
int row    = linear_address / BITSPERROW;

Итак, чтобы установить i-й бит, вы можете сделать это:

bits[ i%BITSPERROW ] |= 1 << (linear_address / BITSPERROW );

Дополнительный getcha заключается в том, что оператор modulo может быть заменен логическим И, а оператор/может быть заменен сдвигом, если второй операнд является степенью двух.

a % BITSPERROW == a & ( BITSPERROW - 1 ) == a & MASK
a / BITSPERROW == a >> ( log2(BITSPERROW) ) == a & SHIFT

Это в конечном счете сводится к очень плотной, но трудно понятной для бит-хук-агностической нотации

a[ i >> SHIFT ] |= ( 1 << (i&MASK) );

Но я не вижу алгоритма, работающего, например. 40 бит на слово.

Ответ 5

Цитируя выдержки из оригинальной статьи Bentleys в DDJ, это то, что делает код на высоком уровне:

/* phase 1: initialize set to empty */

for (i = 0; i < n; i++)

    bit[i] = 0

/* phase 2: insert present elements */

for each i in the input file

    bit[i] = 1

/* phase 3: write sorted output */

for (i = 0; i < n; i++)

    if bit[i] == 1

        write i on the output file

Ответ 6

Несколько сомнений: 1. Почему это необходимо для 32-битного? 2. Можем ли мы сделать это на Java, создав HashMap с ключами от 0000000 до 9999999  и значения 0 или 1 в зависимости от наличия/отсутствия бит? Каковы последствия  для такой программы?