Ось нерекурсивне місце в алгоритмі лінійного часу для переплетення двох половин масиву без додаткового зберігання.
Загальна ідея проста: пройдіть першу половину масиву зліва направо, поміняючи правильні значення на свої місця. По мірі того, як ще використовуються ліві значення, поміняються місцями, звільненими від правильних значень. Єдина хитрість - з'ясувати, як їх знову витягнути.
Почнемо з масиву розміром N, розділеного на 2 майже рівні половини.
[ left_items | right_items ]
Коли ми його обробляємо, це стає
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]
Простір підкачки збільшується за такою схемою: А) збільшуйте простір, видаляючи сусідній правий елемент і замінюючи новий елемент зліва; Б) обміняйте найстаріший предмет лівим на новий. Якщо ліві елементи пронумеровані 1..N, виглядає такий візерунок
step swapspace index changed
1 A: 1 0
2 B: 2 0
3 A: 2 3 1
4 B: 4 3 0
5 A: 4 3 5 2
6 B: 4 6 5 1
7 A: 4 6 5 7 3
...
Послідовність зміни індексу - саме OEIS A025480 , яку можна обчислити простим процесом. Це дозволяє знайти місце підкачки, враховуючи лише кількість доданих до цього часу елементів, що також є індексом поточного елемента, що розміщується.
Це вся інформація, яка нам потрібна для заповнення 1-ї половини послідовності у лінійний час.
Коли ми дістанемося до середини, масив матиме три частини:
[ placed_items | swapped_left_items | remaining_right_items]
Якщо ми можемо розкрутити замінені елементи, ми зменшили проблему до половини розміру і можемо повторити.
Щоб розшифрувати простір swap, ми використовуємо таке властивість: Послідовність, побудована за допомогою N
чергування операцій додавання та swap_oldest, буде містити N/2
елементи, за якими визначається їх вік A025480(N/2)..A025480(N-1)
. (Ціле ділення, менші значення старіші).
Наприклад, якщо ліва половина спочатку містила значення 1..19, то простір підміни містив би [16, 12, 10, 14, 18, 11, 13, 15, 17, 19]
. A025480 (9..18) - [2, 5, 1, 6, 3, 7, 0, 8, 4, 9]
це саме перелік індексів елементів від найдавніших до найновіших.
Таким чином , ми можемо розшифровувати наш простір підкачки, просуваючи через нього і обмінювати S[i]
з S[ A(N/2 + i)]
. Це також лінійний час.
Залишається ускладненням в тому, що з часом ви досягнете позиції, коли правильне значення повинно бути нижчим показником, але воно вже замінено. Знайти нове місце легко: просто зробіть обчислення індексу ще раз, щоб виявити, на який пункт був замінений. Можливо, буде потрібно виконати ланцюг кілька кроків, поки ви не знайдете незамінене місце.
На цьому етапі ми об'єднали половину масиву і підтримували порядок незанурених частин в іншій половині, точно N/2 + N/4
розміняючи місцями. Ми можемо продовжити через решту масиву загальну кількість N + N/4 + N/8 + ....
свопів, що суворо менше 3N/2
.
Як обчислити A025480:
Це визначено в OEIS як a(2n) = n, a(2n+1) = a(n).
альтернативна рецептура a(n) = isEven(n)? n/2 : a((n-1)/2)
. Це призводить до простого алгоритму з використанням побітових операцій:
index_t a025480(index_t n){
while (n&1) n=n>>1;
return n>>1;
}
Це амортизована операція O (1) над усіма можливими значеннями для N. (1/2 потрібно 1 зміна, 1/4 потрібно 2, 1/8 потрібно 3, ...) . Існує ще більш швидкий метод, який використовує невелику таблицю пошуку, щоб знайти положення найменш значущого нульового біта.
Враховуючи це, ось реалізація в C:
static inline index_t larger_half(index_t sz) {return sz - (sz / 2); }
static inline bool is_even(index_t i) { return ((i & 1) ^ 1); }
index_t unshuffle_item(index_t j, index_t sz)
{
index_t i = j;
do {
i = a025480(sz / 2 + i);
}
while (i < j);
return i;
}
void interleave(value_t a[], index_t n_items)
{
index_t i = 0;
index_t midpt = larger_half(n_items);
while (i < n_items - 1) {
//for out-shuffle, the left item is at an even index
if (is_even(i)) { i++; }
index_t base = i;
//emplace left half.
for (; i < midpt; i++) {
index_t j = a025480(i - base);
SWAP(a + i, a + midpt + j);
}
//unscramble swapped items
index_t swap_ct = larger_half(i - base);
for (index_t j = 0; j + 1 < swap_ct ; j++) {
index_t k = unshuffle_item(j, i - base);
if (j != k) {
SWAP(a + midpt + j, a + midpt + k);
}
}
midpt += swap_ct;
}
}
Це має бути досить сприятливим для кешу алгоритмом, оскільки до 2 із 3-х локальних даних можна отримувати доступ послідовно, а кількість оброблюваних даних суворо зменшується. Цей метод може бути перетворений з поза-перетасовки к-у випадковому порядку шляхом заперечення is_even
тест на початку циклу.