В этой статье мы расскажем вам про одну известную перестановку элементов массива, которая сама по себе редко упоминается и известна в контексте одного из самых популярных алгоритмов — алгоритма быстрого преобразования Фурье.
В прикладной математике, бит-реверсивная перестановка (или перестановка с обращением битов, bit-reversal permutation) является перестановкой элементов последовательности из N элементов, где N = 2k (т.е само число N является степенью двойки). Она осуществляется путем присвоения элементам последовательности индексов от 0 до N-1 и затем инвертирования порядка битов их двоичного представления, где каждое число представлено k битами, дополненными нулями при необходимости. Также, данная перестановка является инволюцией, т.е повторное ее применение к набору данных восстанавливает исходный порядок элементов, что является очень ценным свойством бит-реверсной перестановки.
Бит-реверсная перестановка применяется в нескольких быстрых алгоритмах, поскольку вычисление перстановки осуществляется за линейное время и требует только простых вычислений на индексах элементов последовательности. Такая перестановка применяется в быстром преобразовании Фурье (первый этап — это такая перестановка, последний этап точно такая же перестановка для возвращения последовательности в исходный порядок) и при генерации последовательностей с низким расхождением.
Как это работает ?
Предположим, у нас есть массив элементов (к примеру, чисел от 0 до N-1) и его размер является степенью двойки, как было указано ранее. Тогда бит-реверсивная перестановка для некоторых N выглядит следующим образом:
k | N = 2k | Массив после перестановки |
---|---|---|
0 | 1 | 0 |
1 | 2 | 0 1 |
2 | 4 | 0 2 1 3 |
3 | 8 | 0 4 2 6 1 5 3 7 |
4 | 16 | 0 8 4 12 2 10 6 14 1 9 5 13 3 11 7 15 |
В таблице представлены примеры для некоторых последовательно идущих N и таблица может быть продолжена дальше и для остальных степеней двойки.
Рассмотрим последовательность из восьми чисел от 0 до 7 — [0, 1, 2, 3, 4, 5, 6, 7]. Их индексы представляют собой двоичные числа 000, 001, 010, 011, 100, 101, 110 и 111, которые при обращении порядка битов превращаются в 000, 100, 010, 110, 001, 101, 011 и 111. Таким образом, число 0 на позиции 000 отображается в ту же позицию (000), цифра 1 на позиции 001 отображается в пятую позицию (т.е в позицию с номером 100) и т.д., что дает новую последовательность — [0, 4, 2, 6, 1, 5, 3, 7] . Повторное применение той же перестановки к этой новой последовательности возвращает нам исходную последовательность.
Также, данная перестановка обладает еще одним интересным свойством: она может быть получена из перестановки с меньшим размером. Для получения бит-реверсивной перестановки размером 2*N из перестановки размером N элементов требуется взять результат перестановки, удвоить каждый его индекс (обозначим как a), а затем опять взять результат и прибавить к каждому индексу 1 (обозначим как b), и объединить оба результата через конкатенацию: a ~ b. К примеру, упомянутую выше перестановку для 8 элементов можно получить из перестановки для 4 элементов: перестановка четырех элементов — это массив [0, 2, 1, 3] и если удвоить каждый индекс (получим часть a — [0,4, 2, 6]) и также к каждому индексу добавить по 1 (получим часть b — [1, 5, 3, 7]), то совмещение этих последовательностей в одну даст искомую последовательность из 8 элементов.
Реализация бит-реверсной перестановки в D сводится к нескольким функциям: функция для инвертирования порядка битов (делается на базе побитовых операций), функция для определения количества переставляемых битов в некотором числе и функция выполняющая перестановку. С учетом предыдущих описаний, реализация выглядит так:
/// Перестановка битов в числе uint reverseBits(uint value, uint numberBitsToReverse) { uint reversed; for (uint i = 0; i < numberBitsToReverse; i++) { reversed <<= 1; reversed |= value & 1; value >>= 1; } return reversed; } /// Количество лидирующих нулей uint countLeadingZeros(T)(T x) { if (x == 0) { return x.sizeof * 8; } int count = 0; int numBits = x.sizeof * 8; while (x > 0) { x >>= 1; // Сдвигаем число вправо на один бит count++; } return numBits - count; } /// Бит-реверсная перестановка массива void bitReversePermutation(T)(ref T[] array) { auto length = cast(int) array.length; //int log2_Length = 31 - iLog2(length); int log2_Length = 31 - countLeadingZeros(length); for (uint i = 0; i < length; i++) { int j = reverseBits(i, log2_Length); if (j > i) { T temp = array[i]; array[i] = array[j]; array[j] = temp; } } }
Данный код реализует три указанные функции, которые работают следующим образом:
- reverseBits. Эта функция переворачивает биты в заданном числе. Она принимает два аргумента: value — число, которое нужно перевернуть, и numberBitsToReverse — количество битов, которые требуется перевернуть. Она выполняет цикл, в котором сдвигает reversed влево на 1 бит, затем устанавливает младший бит в value в качестве младшего бита reversed, и сдвигает value вправо на 1 бит. Таким образом получается число с перевернутым порядком следования битов.
- countLeadingZeros. Эта функция подсчитывает количество лидирующих нулей в заданном числе x. Если x равно нулю, то возвращается количество битов в типе, хранящем x. В противном случае, выполняется цикл, в котором x сдвигается вправо на 1 бит, и счетчик count увеличивается на 1. После окончания цикла, возвращается разница между количеством битов в типе значения x и значением count.
- bitReversePermutation. Эта функция выполняет бит-реверсивную перестановку элементов массива array. Она сначала определяет количество битов log2_Length, которые требуется перевернуть в индексе элемента массива. Затем она выполняет цикл от 0 до length — 1 и использует функцию reverseBits для получения перевернутого индекса j. Если j больше i, то значения элементов массива array[i] и array[j] меняются местами.
Реализация алгоритма бит-реверсивной перестановки простая, но неочеивдная. Кроме того, мы полагаем, что вполне можно построить алгоритм и на основе сборке перестановки из перестановок меньшей длины, выполнив перестановку рекурсивно. Такая возможность следует из свойств бит-реверсивной перестановки, но мы не уверены в ее эффективности и потому не сделали ее реализацию, оставив ее как упражнение для энтузиастов.
Примеров использования исключительно самой перестановки мы не обнаружили, но она входит в структуру некоторых алгоритмов, среди которых алгоритм Кули-Тьюки, алгоритмы генерации последовательности Ван Дер Корпута и некоторые оценочные механизмы в распределеных вычислениях. Поскольку, бит-реверсивная перестановка входит в структуру известных процедур, то мы и решили кратко рассказать о ней, особенно с учетом того, что в русскоязычных источниках она практически не освещается (даже в википедии статьи нет). А для тех кто, хочет узнать о ней боольше мы прилагаем скромный набор ссылок ниже: