Expanding on rcgldr's answer, I wrote a simplistic1 implementation of Quick Sort on linked lists using the first element as pivot (which behaves pathologically bad on sorted lists) and ran a benchmark on lists with pseudo-random data.
I implemented Quick Sort using recursion but taking care of avoiding a stack overflow on pathological cases by recursing only on the smaller half.
I also implemented the proposed alternative with an auxiliary array of pointers to the nodes.
Here is the code:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef struct NODE {
struct NODE *next;
int data;
} NODE;
/* merge two already sorted lists */
/* compare uses pSrc2 < pSrc1 to follow the STL rule */
/* of only using < and not <= */
NODE *MergeLists(NODE *pSrc1, NODE *pSrc2) {
NODE *pDst = NULL; /* destination head ptr */
NODE **ppDst = &pDst; /* ptr to head or prev->next */
for (;;) {
if (pSrc2->data < pSrc1->data) { /* if src2 < src1 */
*ppDst = pSrc2;
pSrc2 = *(ppDst = &(pSrc2->next));
if (pSrc2 == NULL) {
*ppDst = pSrc1;
break;
}
} else { /* src1 <= src2 */
*ppDst = pSrc1;
pSrc1 = *(ppDst = &(pSrc1->next));
if (pSrc1 == NULL) {
*ppDst = pSrc2;
break;
}
}
}
return pDst;
}
/* sort a list using array of pointers to list */
NODE *MergeSort(NODE *pNode) {
#define NUMLISTS 32 /* number of lists */
NODE *aList[NUMLISTS]; /* array of lists */
/* aList[i] == NULL or ptr to list with 2^i nodes */
int i, n = 0;
while (pNode != NULL) {
NODE *pNext = pNode->next;
pNode->next = NULL;
for (i = 0; i < n && aList[i] != NULL; i++) {
pNode = MergeLists(aList[i], pNode);
aList[i] = NULL;
}
if (i == NUMLISTS) /* don't go beyond end of array */
i--;
else
if (i == n) /* extend array */
n++;
aList[i] = pNode;
pNode = pNext;
}
for (i = 0; i < n; i++) {
if (!pNode)
pNode = aList[i];
else if (aList[i])
pNode = MergeLists(aList[i], pNode);
}
return pNode;
}
void QuickSortRec(NODE **pStart, NODE *pList, NODE *stop) {
NODE *pivot, *left, *right;
NODE **ppivot, **pleft, **pright;
int data, nleft, nright;
while (pList != stop && pList->next != stop) {
data = pList->data; // use the first node as pivot
pivot = pList;
ppivot = &pList->next;
pleft = &left;
pright = &right;
nleft = nright = 0;
while ((pList = pList->next) != stop) {
if (data == pList->data) {
*ppivot = pList;
ppivot = &pList->next;
} else
if (data > pList->data) {
nleft++;
*pleft = pList;
pleft = &pList->next;
} else {
nright++;
*pright = pList;
pright = &pList->next;
}
}
*pleft = pivot;
*pright = stop;
*ppivot = right;
if (nleft >= nright) { // recurse on the smaller part
if (nright > 1)
QuickSortRec(ppivot, right, stop);
pList = left;
stop = pivot;
} else {
if (nleft > 1)
QuickSortRec(pStart, left, pivot);
pStart = ppivot;
pList = right;
}
}
*pStart = pList;
}
NODE *QuickSort(NODE *pList) {
QuickSortRec(&pList, pList, NULL);
return pList;
}
int NodeCmp(const void *a, const void *b) {
NODE *aa = *(NODE * const *)a;
NODE *bb = *(NODE * const *)b;
return (aa->data > bb->data) - (aa->data < bb->data);
}
NODE *QuickSortA(NODE *pList) {
NODE *pNode;
NODE **pArray;
int i, len;
/* compute the length of the list */
for (pNode = pList, len = 0; pNode; pNode = pNode->next)
len++;
if (len > 1) {
/* allocate an array of NODE pointers */
if ((pArray = malloc(len * sizeof(NODE *))) == NULL) {
QuickSortRec(&pList, pList, NULL);
return pList;
}
/* initialize the array from the list */
for (pNode = pList, i = 0; pNode; pNode = pNode->next)
pArray[i++] = pNode;
qsort(pArray, len, sizeof(*pArray), NodeCmp);
for (i = 0; i < len - 1; i++)
pArray[i]->next = pArray[i + 1];
pArray[i]->next = NULL;
pList = pArray[0];
free(pArray);
}
return pList;
}
int isSorted(NODE *pList) {
if (pList) {
int data = pList->data;
while ((pList = pList->next) != NULL) {
if (data > pList->data)
return 0;
data = pList->data;
}
}
return 1;
}
void test(int count) {
NODE *pMem1, *pMem2, *pMem3;
NODE *pList1, *pList2, *pList3;
int i;
time_t t1, t2, t3;
/* create linear lists of nodes with pseudo-random data */
srand(clock());
if (count == 0
|| (pMem1 = malloc(count * sizeof(NODE))) == NULL
|| (pMem2 = malloc(count * sizeof(NODE))) == NULL
|| (pMem3 = malloc(count * sizeof(NODE))) == NULL)
return;
for (i = 0; i < count; i++) {
int data = rand();
pMem1[i].data = data;
pMem1[i].next = &pMem1[i + 1];
pMem2[i].data = data;
pMem2[i].next = &pMem2[i + 1];
pMem3[i].data = data;
pMem3[i].next = &pMem3[i + 1];
}
pMem1[count - 1].next = NULL;
pMem2[count - 1].next = NULL;
pMem3[count - 1].next = NULL;
t1 = clock();
pList1 = MergeSort(pMem1);
t1 = clock() - t1;
t2 = clock();
pList2 = QuickSort(pMem2);
t2 = clock() - t2;
t3 = clock();
pList3 = QuickSortA(pMem3);
t3 = clock() - t3;
printf("%10d", count);
if (isSorted(pList1))
printf(" %10.3fms", t1 * 1000.0 / CLOCKS_PER_SEC);
else
printf(" failed");
if (isSorted(pList2))
printf(" %10.3fms", t2 * 1000.0 / CLOCKS_PER_SEC);
else
printf(" failed");
if (isSorted(pList3))
printf(" %10.3fms", t3 * 1000.0 / CLOCKS_PER_SEC);
else
printf(" failed");
printf("\n");
free(pMem1);
free(pMem2);
}
int main(int argc, char **argv) {
int i;
printf(" N MergeSort QuickSort QuickSortA\n");
if (argc > 1) {
for (i = 1; i < argc; i++)
test(strtol(argv[1], NULL, 0));
} else {
for (i = 10; i < 23; i++)
test(1 << i);
}
return 0;
}
Here is the benchmark on lists with geometrically increasing lengths, showing N log(N) times:
N MergeSort QuickSort QuickSortA
1024 0.052ms 0.057ms 0.105ms
2048 0.110ms 0.114ms 0.190ms
4096 0.283ms 0.313ms 0.468ms
8192 0.639ms 0.834ms 1.022ms
16384 1.233ms 1.491ms 1.930ms
32768 2.702ms 3.786ms 4.392ms
65536 8.267ms 10.442ms 13.993ms
131072 23.461ms 34.229ms 27.278ms
262144 51.593ms 71.619ms 51.663ms
524288 114.656ms 240.946ms 120.556ms
1048576 284.717ms 535.906ms 279.828ms
2097152 707.635ms 1465.617ms 636.149ms
4194304 1778.418ms 3508.703ms 1424.820ms
QuickSort()
is approximately half as fast as MergeSort()
on these datasets, but would behave much worse on partially ordered sets and other pathological cases, whereas MergeSort
has a regular time complexity that does not depend on the dataset and performs a stable sort. QuickSortA()
performs marginally better than MergeSort()
for large datasets on my system, but performance will depend on the actual implementation of qsort
, which does not necessarily use a Quick Sort algorithm.
MergeSort()
does not allocate any extra memory and performs a stable sort, which makes it a clear winner to sort lists.
1) well, not so simplistic after all, but the choice of pivot is too simple