快速排序实现
2014-08-30 21:01
197 查看
作为统治世界的算法之一,快速排序(Quick Sort)在很多场合下都能发挥其强大的力量。数据量在百万级别的数据量对快速排序来说是小case. 该算法最早是由图灵奖获得者Tony Hoare设计出来的,他在形式化方法理论以及ALGOL60编程语言的发明中都有卓越的贡献。可以认为是冒泡排序的升级,它们都属于交换排序类。即通过不断的比较和移动交换来实现排序,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录移到后面,较小的移到前面,从而减少了总的比较次数和移动交换次数。
在实现快速排序的过程中,我们一般需要关注2点,一个是枢纽元的选择(即pivot), 还有很重要的一点是两个工作指针的初始位置以及他们的运动方向。
看了很多的实现,pivot一般来说有4种选择:
1、头元素 Hoare版本的做法,《数据结构与算法分析》中不推荐将第一个元素作为枢纽元,因为在输入是预排序或反序时,会产生糟糕的分割。 其实尾元素道理是一样的。
2、尾元素 Lomuto版本的做法
3、中间元素(包括3数中值分割法(median of three), 即取3个关键字先进行排序,将中间数作为pivot, 一般取左中右3个数,也可以随机选取。)
4、随机选择 排除随机数生成的代价外,是一种不错的选择
快排一般来说有以下两个版本,
一、先看看Hoare最原始的版本,pivot为首元素,其工作指针分别在一头一尾,这完全就一稍微快一点的冒泡嘛= =:
具体实现如下:
二、Nico Lomuto也提出了一个版本,pivot为尾元素,工作指针都从左向右一个方向运动,个人觉得这个更好理解:
分析如下,摘自算法导论:
两种实现方式对比的话, Lomuto版本更加简单易实现,但不适用于库函数的实现,因为它使用了更多的交换次数。更加细节参见:StackExchange
下面是Lomuto版本的实现代码:
下面讨论一下变种版本:
三、一种Hoare变种实现,pivot为首元素,工作指针从两边向中间移动,也比较好理解
从上面的实现可以看出,快速排序用到的都是递归实现,这在数据量较大的情况下容易出现堆栈溢出,我在一次测试中使用10万的数据量出现了溢出(有点疑惑不过程序确认崩溃了),
所以当你的需求是比较大的数据量时,非递归的实现就会派上用场了。这里看到有人用STL stack实现发现速度明显比较慢(快速排序算法的几种实现的性能对比:递归实现和非递归实现),所以还是建议自己实现堆栈比较靠谱:)
那么单链表可以使用快速排序吗?答案是可以,实现思路是使用两个链表,一个保存比key小的值,另一个保存比key大的,最后把两个链表再连接起来。这样通过调整指针的指向即可实现排序效果。
总结:快速排序是一种平均复杂度在O(nlogn)的算法,最坏在已有序的情况下为O(n²),是不稳定的排序算法。
这里有一篇快排细节优化的文章,对pivot的选取以及存在相同元素的情况做了详述。
在实现快速排序的过程中,我们一般需要关注2点,一个是枢纽元的选择(即pivot), 还有很重要的一点是两个工作指针的初始位置以及他们的运动方向。
看了很多的实现,pivot一般来说有4种选择:
1、头元素 Hoare版本的做法,《数据结构与算法分析》中不推荐将第一个元素作为枢纽元,因为在输入是预排序或反序时,会产生糟糕的分割。 其实尾元素道理是一样的。
2、尾元素 Lomuto版本的做法
3、中间元素(包括3数中值分割法(median of three), 即取3个关键字先进行排序,将中间数作为pivot, 一般取左中右3个数,也可以随机选取。)
4、随机选择 排除随机数生成的代价外,是一种不错的选择
快排一般来说有以下两个版本,
一、先看看Hoare最原始的版本,pivot为首元素,其工作指针分别在一头一尾,这完全就一稍微快一点的冒泡嘛= =:
具体实现如下:
#include <stdio.h> void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; } void printArray(int a[], int num) { for (int i = 0; i < num; i++) { printf("%d ", a[i]); } printf("\n"); } int HoarePartition(int a[], int p, int r) { int key = a[p], i = p - 1, j = r + 1; // fprintf(fp, "key = %d, i = %d, j = %d\n", key, i, j); while (1) { do { j--; }while (a[j] > key); do { i++; }while (a[i] < key); if (i < j) { swap(&a[i], &a[j]); } else { return j; } } } void QuickSort(int a[], int start, int end) { int q; // fprintf(fp, "new sort: start = %d, end = %d\n", start, end); if (end <= start) return; q = HoarePartition(fp, a, start, end); QuickSort(fp, a, start, q); QuickSort(fp, a, q + 1, end); } int main() { int a[] = {3, 4, 12, 56, 0, 6, 9, 10, 6, 23}; printf(fp, "init array: \n"); printArray(fp, a, 10); QuickSort(fp, a, 0, 9); printf(fp, "sorted: \n"); printArray(fp, a, 10); fclose(fp); return 0; }
二、Nico Lomuto也提出了一个版本,pivot为尾元素,工作指针都从左向右一个方向运动,个人觉得这个更好理解:
Lomuto-Partition(A, p, r) x = A[r] i = p - 1 for j = p to r - 1 if A[j] <= x i = i + 1 swap( A[i], A[j] ) swap( A[i+1], A[r] ) return i + 1 QUICKSORT(A, p, r) if p < r then q ← Lomuto-Partition(A, p, r) QUICKSORT(A, p, q - 1) QUICKSORT(A, q + 1, r)
分析如下,摘自算法导论:
两种实现方式对比的话, Lomuto版本更加简单易实现,但不适用于库函数的实现,因为它使用了更多的交换次数。更加细节参见:StackExchange
下面是Lomuto版本的实现代码:
#include <stdio.h> void printArray(int a[], int num) { for (int i = 0; i < num; i++) { printf("%d ", a[i]); } printf("\n"); } void swap(int *a, int *b) { int t; t = *a; *a = *b; *b = t; } int Partition(int data[], int p, int r) { int key = data[r]; int i = p - 1; for (int j = p; j < r; j++) { if (data[j] <= key) { i++; swap(&data[i], &data[j]); } } swap(&data[i+1], &data[r]); return i + 1; } void QuickSort(int data[], int start, int end) { if (start < end) { int q = Partition(data, start, end); QuickSort(data, start, q - 1); QuickSort(data, q + 1, end); } } int main() { int a[] = {3, 32, 13, 8, 9, 9, 12, 33, 41}; printf("init array:\n"); printArray(a, 9); QuickSort(a, 0, 8); printf("sorted:\n"); printArray(a, 9); }
下面讨论一下变种版本:
三、一种Hoare变种实现,pivot为首元素,工作指针从两边向中间移动,也比较好理解
#include <stdio.h> void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; } void printArray(int a[], int num) { for (int i = 0; i < num; i++) { printf("%d ", a[i]); } printf("\n"); } int HoarePartition(int a[], int p, int r) { int key = a[p], i = p, j = r; // fprintf(fp, "key = %d, i = %d, j = %d\n", key, i, j); while (i < j) { while (a[j] >= key && i < j) j--; a[i] = a[j]; while (a[i] <= key && i < j) i++; a[j] = a[i]; } a[i] = key; return i; } void QuickSort(int a[], int start, int end) { int q; // fprintf(fp, "new sort: start = %d, end = %d\n", start, end); if (end <= start) return; q = HoarePartition(a, start, end); QuickSort(a, start, q - 1); QuickSort(a, q + 1, end); } int main() { int a[] = {3, 4, 12, 56, 0, 6, 9, 10, 9, 23}; // FILE *fp = fopen("log", "w+"); printf("init array: \n"); printArray(a, 10); printf("sorted: \n"); QuickSort(a, 0, 9); printArray(a, 10); fclose(fp); return 0; }
从上面的实现可以看出,快速排序用到的都是递归实现,这在数据量较大的情况下容易出现堆栈溢出,我在一次测试中使用10万的数据量出现了溢出(有点疑惑不过程序确认崩溃了),
所以当你的需求是比较大的数据量时,非递归的实现就会派上用场了。这里看到有人用STL stack实现发现速度明显比较慢(快速排序算法的几种实现的性能对比:递归实现和非递归实现),所以还是建议自己实现堆栈比较靠谱:)
#include <stdio.h> void swap(int *a, int *b) { int temp; temp = *a; *a = *b; *b = temp; } int partition(int *a, int start, int end) { int key = a[end]; int i = start - 1; int j = start; for ( ; j < end; j++) { if (a[j] < key) { i++; swap(&a[i], &a[j]); } } swap(&a[++i], &a[j]); return i; } void QuickSort(int *a, int size) { int stack[100]; int top = -1; int start, end; stack[++top] = 0; stack[++top] = size - 1; while (top > 0) { end = stack[top--]; start = stack[top--]; int i = partition(a, start, end); if (start < i - 1) { stack[++top] = start; stack[++top] = i - 1; } if (i + 1 < end) { stack[++top] = i + 1; stack[++top] = end; } } } void printArray(int *a, int num) { for (int i = 0; i < num; i++) { printf("%d ", a[i]); } printf("\n"); } int main() { int a[] = {3, 45, 15, 6, 9, 15, 90, 17, 28, 10}; printf("init array:\n"); printArray(a, 10); QuickSort(a, 10); printf("after sorted:\n"); printArray(a, 10); return 0; }
那么单链表可以使用快速排序吗?答案是可以,实现思路是使用两个链表,一个保存比key小的值,另一个保存比key大的,最后把两个链表再连接起来。这样通过调整指针的指向即可实现排序效果。
#include <stdio.h> #include <time.h> typedef struct tagLinkNode { int value; struct tagLinkNode *next; }LinkNode; void QuickSort(LinkNode** head, LinkNode** end) { LinkNode *head1, *head2, *end1, *end2; head1 = head2 = end1 = end2 = NULL; if (*head == NULL || *end == NULL) { // printf("head or end is null\n"); return; } // printf("head=%d, end=%d\n", (*head)->value, (*end)->value); LinkNode *p, *pre1, *pre2; p = pre1 = pre2 = NULL; int key = (*head)->value; // printf("key is %d\n", key); // divide head node p = (*head)->next; (*head)->next = NULL; while (p != NULL) { // value less than key if (p->value < key) { // printf("less than key!\n"); if (!head1) { head1 = p; pre1 = p; } else { pre1->next = p; pre1 = p; } p = p->next; pre1->next = NULL; } // value larger than key else { // printf("larger than key!\n"); if (!head2) { head2 = p; pre2 = p; } else { pre2->next = p; pre2 = p; } p = p->next; pre2->next = NULL; } } // printf("merge it\n"); end1 = pre1; end2 = pre2; // recuring QuickSort(&head1, &end1); QuickSort(&head2, &end2); // printf("after recuring\n"); /* conjection 2 List */ // if 2 List all exist if (head1 && head2) { end1->next = *head; (*head)->next = head2; *head = head1; *end = end2; } // only left list exist else if (head1) { end1->next = *head; *end = *head; *head = head1; } // only right list exist else if (head2) { (*head)->next = head2; *end = end2; } } void addList(LinkNode **head, LinkNode *node) { node->value = rand() % 50 + 1; node->next = (*head)->next; (*head)->next = node; printf("%d ", node->value); } LinkNode* getListFirst(LinkNode *head) { return head->next; } LinkNode* getListLast(LinkNode *head) { LinkNode *p = head; while (p->next != NULL) { p = p->next; } return p; } void printList(LinkNode *head) { LinkNode *p = head->next; while (p != NULL) { printf("%d ", p->value); p = p->next; } } int main() { srand(time(NULL)); int i; LinkNode linkArray[10]; LinkNode *listHead; listHead = (LinkNode *) malloc(sizeof(LinkNode)); if (NULL == listHead) { printf("listHead malloc failed\n"); return -1; } listHead->value = 10; listHead->next = NULL; printf("init array: \n"); for (i = 0; i < 10; i++) { addList(&listHead, &linkArray[i]); } printf("\n"); LinkNode *first, *end; first = getListFirst(listHead); end = getListLast(listHead); QuickSort(&first, &end); listHead->next = first; // important printf("after sorted: \n"); printList(listHead); printf("\n"); }
总结:快速排序是一种平均复杂度在O(nlogn)的算法,最坏在已有序的情况下为O(n²),是不稳定的排序算法。
这里有一篇快排细节优化的文章,对pivot的选取以及存在相同元素的情况做了详述。
相关文章推荐
- 原创:快速排序的实现
- 快速排序的C#实现以及,算法导论上之后一个习题的思考
- List 采用delegate快速实现排序、查找等操作
- 快速排序分析与C语言实现
- Java下实现快速排序
- 递归实现快速排序
- 快速排序方法Java实现与分析
- 快速排序的JAVA实现
- C语言实现 排序源程序(包括直接插入、希尔、冒泡、快速、简单选择、堆排序)
- 快速排序Java实现
- 分治法:用C#实现快速排序
- 插入排序,合并排序,堆排序,快速排序,计数排序的实现(算法导论)
- vb应用--快速排序-法实现二维数组的指定列排序
- List<T>采用delegate快速实现排序、查找等操作
- 排序算法复习(Java实现)(二): 插入,冒泡,选择,Shell,快速排序
- 快速排序的两种实现
- 快速排序及代码实现
- java实现的冒泡、选择、快速排序
- 排序算法复习(Java实现):插入,冒泡,选择,Shell,快速排序, 归并排序,堆排序,桶式排序,基数排序
- 快速排序---c#实现