More efficient way
Your current algorithm runs in \$O(N^2)\$ time. At the time I started thinking about this, I did not read Jerry Coffin's link that demonstrated an \$O(N)\$ solution, so I came up with my own \$O(N \log N)\$ solution based on a divide and conquer algorithm. Basically, the way it works is to reverse the second half, and then call a function that interleaves the first half with the second half. This interleaving function does the following:
- Divide the array into quarters
- Swap the middle two quarters
- Recurse on the left half and the right half
To demonstrate how this works, consider this array of size 8:
0 1 2 3 4 5 6 7
Swapping the middle quarters (2 3
) with (4 5
):
0 1 4 5 2 3 6 7
Recurse on left half 0 1 4 5
and right half 2 3 6 7
:
0 1 4 5 -> 0 4 1 5 (swap 1 and 4)
2 3 6 7 -> 2 6 3 7 (swap 3 and 6)
Final:
0 4 1 5 2 6 3 7
There is a slight complication if the length does not divide in half evenly. This complication is explained in the comments of the code.
The code
#include <stdio.h>
#include <stdlib.h>
static void weaveHalves(int *array, int start, int len);
void printArray(int *array, int len)
{
for (int i=0; i < len; ++i)
printf("%d ", array[i]);
printf("\n");
}
// Takes an array, and interleaves its first half with the reverse of the
// second half:
//
// 0 1 2 3 4 5 6 7 (First half = 0 1 2 3) (Rev 2nd half = 7 6 5 4)
// 0 7 1 6 2 5 3 4
void weave(int *array, int start, int len)
{
int halfLen = len >> 1;
int mid = start + halfLen;
int end = len - 1;
// First step: reverse the second half.
while (mid < end) {
int tmp = array[mid];
array[mid++] = array[end];
array[end--] = tmp;
}
// Next step, weave the first half with the second half. If the length
// of the array is odd, only weave len-1 entries because the last entry
// is in the correct place already.
weaveHalves(array, start, len & ~1);
}
// Takes an array, and interleaves its first half with its second half:
//
// 0 1 2 3 4 5 6 7 (First half = 0 1 2 3) (Second half = 4 5 6 7)
// 0 4 1 5 2 6 3 7
//
// Note: len must always be an even number.
static void weaveHalves(int *array, int start, int len)
{
int halfLen = len >> 1;
int mid = start + halfLen;
int quarterLen = halfLen >> 1;
if (len <= 2)
return;
if ((halfLen & 1) == 0) {
// If the half itself is even, then we can just swap the middle
// quarters, since the left quarter is the same size as the right
// quarter:
//
// 0 1 2 3 4 5 6 7 (Left to swap: 2 3) (Right to swap: 4 5)
// 0 1 4 5 2 3 6 7 (After swapping)
int leftIndex = start + quarterLen;
int rightIndex = mid;
for (int i = 0; i < quarterLen; i++) {
int tmp = array[leftIndex];
array[leftIndex++] = array[rightIndex];
array[rightIndex++] = tmp;
}
// Recurse on halves.
weaveHalves(array, start, halfLen);
weaveHalves(array, mid, halfLen);
} else {
// If the half is not even, then the left quarter will be one larger
// than the right quarter, and we need to do a more clever swapping:
//
// 0 1 2 3 4 5 6 7 8 9 (Left to swap: 2 3 4) (Right to swap: 5 6)
// 0 1 5 6 2 3 4 7 8 9 (After swapping)
//
// To do the swapping, we first save the end of the left half, which
// leaves equal length sides to move:
//
// 0 1 2 3 - 5 6 7 8 9 (save = 4) (Left to move: 2 3) (Right: 5 6)
//
// Then we swap in the hole (-) with the first left entry:
//
// 0 1 - 3 2 5 6 7 8 9
//
// Then we swap the hole with the first right entry:
//
// 0 1 5 3 2 - 6 7 8 9
//
// Then we continue to swap the hole with left and right entries:
//
// 0 1 5 - 2 3 6 7 8 9
// 0 1 5 6 2 3 - 7 8 9
//
// Finally we fill in the saved entry and we are done:
//
// 0 1 5 6 2 3 4 7 8 9
//
int leftIndex = start + quarterLen;
int rightIndex = mid;
int endOfLeft = array[rightIndex-1];
for (int i = 0; i < quarterLen; i++) {
// Note: the hole is always at rightIndex-1 here.
array[rightIndex-1] = array[leftIndex];
array[leftIndex++] = array[rightIndex++];
}
array[rightIndex-1] = endOfLeft;
// Recurse on halves, which are not the same size.
weaveHalves(array, start, halfLen - 1);
weaveHalves(array, mid-1, halfLen + 1);
}
}
int main(int argc, char *argv[])
{
int size;
int *array;
if (argc < 2)
size = 10;
else
size = atoi(argv[1]);
array = calloc(size, sizeof(*array));
for (int i=0; i < size; ++i)
array[i] = i;
// printArray(array, size);
weave(array, 0, size);
// printArray(array, size);
}
The linear time solution
I implemented the \$O(N)\$ solution linked by Jerry Coffin to see whether it was measurably better than my solution. Here is the code for it:
#include <stdio.h>
#include <stdlib.h>
static void inPlaceShuffle(int *array, int len);
static inline void cyclePermute(int *array, int start, int len);
static inline void reverseArray(int *array, int start, int end);
void printArray(int *array, int len)
{
for (int i=0; i < len; ++i)
printf("%d ", array[i]);
printf("\n");
}
// Takes an array, and interleaves its first half with the reverse of the
// second half:
//
// 0 1 2 3 4 5 6 7 (First half = 0 1 2 3) (Rev 2nd half = 7 6 5 4)
// 0 7 1 6 2 5 3 4
void weave(int *array, int start, int len)
{
int halfLen = len >> 1;
int mid = start + halfLen;
int end = len - 1;
// First step: reverse the second half.
while (mid < end) {
int tmp = array[mid];
array[mid++] = array[end];
array[end--] = tmp;
}
// Second step, swap the first and second half, because the shuffler
// makes the second half go first.
for (int i=0, mid=start + halfLen; i < halfLen; i++) {
int tmp = array[start];
array[start++] = array[mid];
array[mid++] = tmp;
}
// Next step, weave the second half with the second half. If the length
// of the array is odd, only weave len-1 entries because the last entry
// is in the correct place already.
inPlaceShuffle(array, len & ~1);
}
// Takes an array, and interleaves its second half with its first half:
//
// 0 1 2 3 4 5 6 7 (First half = 0 1 2 3) (Second half = 4 5 6 7)
// 4 0 5 1 6 2 7 3
//
// Note: len must always be an even number.
//
// Solution taken from this article: http://arxiv.org/pdf/0805.1598v1.pdf
static void inPlaceShuffle(int *array, int len)
{
int k = 0;
int m = 0;
int n = len >> 1;
if (len < 2)
return;
if (len == 2) {
int tmp = array[0];
array[0] = array[1];
array[1] = tmp;
return;
}
// Step 1: Find k such that 3^k <= len < 3^(k+1)
// Then set m = (3^k - 1) / 2
for (m = 1; m <= len; k++, m *= 3);
k--;
m = ((m / 3) - 1) >> 1;
// Step 2: Do a cyclic right shift of A[m, ..., n+m-1] by a distance m
// where n is len/2.
reverseArray(array, m, n+m-1);
reverseArray(array, m, m+m-1);
reverseArray(array, m+m, n+m-1);
// Step 3: For i = 0 .. k-1, do a cyclic permutation starting at 3^i-1
for (int i = 0, start = 1, mlen = m+m; i < k; i++, start *= 3) {
cyclePermute(array, start-1, mlen);
}
// Step 4: Recurse on A[2m, ..., 2n-1]
inPlaceShuffle(array + m + m, len - m - m);
}
// Reverse array from index start to index end inclusive.
static inline void reverseArray(int *array, int start, int end)
{
while (start < end) {
int tmp = array[start];
array[start++] = array[end];
array[end--] = tmp;
}
}
// Starting from index start, permutes the elements in a cycle, where the
// new permutation is the shuffled permutation.
static inline void cyclePermute(int *array, int start, int len)
{
int halfLen = len >> 1;
int i = start;
int hold = array[i];
int next = halfLen + (i >> 1);
while (next != start) {
array[i] = array[next];
i = next;
// array[i] in the final array is:
//
// array[halfLen + i/2] if i is even
// array[(i-1)/2] if i is odd
if (i & 1) {
next = (i >> 1);
} else {
next = halfLen + (i >> 1);
}
} while (next != start);
array[i] = hold;
}
int main(int argc, char *argv[])
{
int size;
int *array;
if (argc < 2)
size = 10;
else
size = atoi(argv[1]);
array = calloc(size, sizeof(*array));
for (int i=0; i < size; ++i)
array[i] = i;
//printArray(array, size);
weave(array, 0, size);
//printArray(array, size);
}
Results
I tested the original code and my version on an array of 200000 elements. Then I tested my version versus the linear time version on an array of 200000000 elements.
On 200000 elements:
OP code: 7.58 sec
Mine : 0.01 sec
Linear : 0.01 sec
On 200000000 elements:
Mine : 1.84 sec
Linear : 3.74 sec
My theory for why my solution is faster than the linear time solution is that the linear time solution's main step is a cycle permute, where it moves each item into place, leaving a hole, then filling the hole from the next correct place according to the shuffle. This part accesses the array in a non-linear order, jumping around the array, and therefore causes cache misses once the array grows bigger than the cache.
My solution swaps blocks at a time, but does so in a linear fashion. Therefore, the memory accesses are very cache friendly and shouldn't cause nearly as many cache misses. So even though my version is theoretically slower, it is practically better.