Here is a non-recursive in-place in linear time algorithm to interleave two halves of an array with no extra storage.
The general idea is simple: Walk through the first half of the array from left to right, swapping the correct values into place. As you advance, the yet to be used left values get swapped into the space vacated by the right values. The only trick is figuring out how to pull them out again.
We start with an array of size N split into 2 nearly equal halves.
[ left_items | right_items ]
As we process it, it becomes
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]
The swap space grows with the following pattern:
A) grow the space by removing the adjacent right item and swapping in a new item from the left; B) swap the oldest item with a new item from the left. If the left items are numbered 1..N, this pattern looks like
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
...
The sequence of which index changed is exactly OEIS A025480, which can be calculated with a simple process. This allows the swap location to be found given only the number of items added so far, which is also the index of the current item being placed.
That's all the info we need to populate the 1st half of the sequence in linear time.
When we get to the midpoint, the array will have three parts:
[ placed_items | swapped_left_items | remaining_right_items]
If we can unscramble the swapped items, we have reduced the problem to half the size, and can repeat.
To unscramble the swap space, we use the following property:
A sequence built by N
alternating append and swap_oldest operations will contain N/2
items where their ages are given by A025480(N/2)..A025480(N-1)
. (Integer division, smaller values are older).
For example, if the left half originally held the values 1..19, then the swap space would contain [16, 12, 10, 14, 18, 11, 13, 15, 17, 19]
. A025480(9..18) is [2, 5, 1, 6, 3, 7, 0, 8, 4, 9]
, which is exactly the list of indexes of the items from oldest to newest.
So we can unscramble our swap space by advancing through it and swapping S[i]
with S[ A(N/2 + i)]
. This is also linear time.
The remaining complication is that eventually you will reach a position where the correct value should be at a lower index, but it has already been swapped out. It is easy to find the new location: just do the index calculation again to discover where the item was swapped to. It may be necessary to follow the chain a few steps until you find an unswapped location.
At this point, we have merged half the array, and maintained the order of the unmerged parts in the other half, with exactly N/2 + N/4
swaps. We can continue through the rest of the array for a total of N + N/4 + N/8 + ....
swaps which is strictly less than 3N/2
.
How to calculate A025480:
This is defined in OEIS as a(2n) = n, a(2n+1) = a(n).
An alternate formulation isa(n) = isEven(n)? n/2 : a((n-1)/2)
. This leads to a simple algorithm using bitwise operations:
index_t a025480(index_t n){
while (n&1) n=n>>1;
return n>>1;
}
This is an amortized O(1) operation over all possible values for N. (1/2 need 1 shift, 1/4 need 2, 1/8 need 3, ...). There is an even faster method which uses a small lookup table to find the position of the least significant zero bit.
Given that, here's an implementation in 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;
}
}
This should be a fairly cache-friendly algorithm, since 2 of the 3 data locations are accessed sequentially and the amount of data being processed is strictly decreasing.
This method can be turned from an out-shuffle to an in-shuffle by negating the is_even
test at the start of the loop.