您的位置:首页 > 其它

快速排序实现

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为首元素,其工作指针分别在一头一尾,这完全就一稍微快一点的冒泡嘛= =:





具体实现如下:

#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的选取以及存在相同元素的情况做了详述。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: