您的位置:首页 > 其它

array和list的排序算法对比(一):快速排序

2016-03-08 23:41 309 查看
一般来说,我们讨论排序都是针对数组结构。数组的特点是:可以很方便地进行随机访问,但是增删元素比较耗时。因此,针对数组的排序,通常会避免元素的增删操作,改为元素交换。同时,常采用二分的方法实现高效排序算法。

链表与数组相反,随机访问非常耗时,但增删元素很简单。因此链表的排序和数组也会有所不同。

这篇博客针对数组和链表的不同,分析了常用排序算法——快速排序在数组和链表中的实现。

注:我们规定排序的语义如下:

sort(begin, end)
表示对
[begin, end)
之间的元素排序,包含begin,但不包含end

1 数组的快速排序

本文待排序的int数组及排序函数如下:

int num
;
quick_sort(int *begin, int *end); //begin和end为指向数组元素的指针


快速排序的思路是:以某个元素为切分点,把小于它的元素全部交换到前面,大于它的元素交换到后面,使切分点成为已排序元素,再对切分点前后进一步排序。

以首元素为切分点,代码如下:

void quick_sort(int *begin, int *end){
/* quick sort */
if(begin == end) return;
int *tmp = begin + 1; //扫描位置
int *partition = begin + 1; //大于等于begin的第一个元素
while(tmp != end){
if(*tmp < *begin){
//当前位置小于切分元素,则应将其与partition处的元素交换,并将partition后移一位
swap(*partition++, *tmp++);
}else{
tmp++;
}
}
partition--;
swap(*partition, *begin); //begin放在切分点位置
quick_sort(begin, partition);
quick_sort(partition + 1, end);
}


快速排序的关键在于切分数组。上述代码中把数组分成4个部分:

1. begin元素

2. (begin, partition)为小于begin的元素

3. [partition, tmp)为大于等于begin的元素,

4. [tmp, end)为未排序元素



while循环用于处理tmp处元素,扩展第1和第2部分,缩减第3部分。处理方法是:若tmp小于begin元素,则tmp与partition处元素交换,再各自加1;若tmp大于等于begin元素,直接将tmp自加即可。

当tmp移动到end的时候,数组就只剩下前3个部分。然后,partition自减1,即为小于切分元素的最后一个元素。将partition元素和begin处元素交换,则数组的3部分如下:

1. [begin, partition)小于切分元素

2. partition处为切分元素

3. (partition, end)大于等于切分元素

至此切分数组工作完成,即可进行下一步的递归。

2 链表的快速排序

这里以一个简单的双向链表作为示例:

class Node{
int value;
Node *next;
Node *prev;
};


此外,设链表的首节点为
head
,尾节点为
tail
,均不存储实际数据,即,如果一个链表有三个元素1,2,3,则链表的结构应为:

head <-> Node(1) <-> Node(2) <-> Node(3) <-> tail


此处
head
tail
不存储数据,是为了在对链表进行操作时无需考虑边界情形。


对照数组排序的思路,可以写出链表的快速排序:

void quick_sort(Node *begin, Node *end){
Node *tmp = begin->next;
Node *tmp_head = begin->prev; //begin的前一个元素(head的存在使得begin前必然有元素)
while(tmp != end){
if(tmp->value < begin->value){
Node *s = tmp;
tmp = tmp->next; //先移动指针,再操作s处元素
//把s从链表中取出来
s->prev->next = s->next;
s->next->prev = s->prev;
//把s插入到begin前面(即begin与其前一个元素之间,由于begin前至少还有head,所以这个语义是成立的)
begin->prev->next = s;
s->prev = begin->prev;
s->next = begin;
begin->prev = s;
}else{
tmp = tmp->next;
}
}
quick_sort(tmp_head->next, begin);
quick_sort(begin->next, end);
}


链表的快速排序从总体上和数组的排序比较接近,也需要切分链表,但切分的方式则略有不同。

链表被分为以下4部分:

1. [tmp_head->next, begin)小于切分元素

2. begin处为切分元素

3. [begin->next, tmp)大于等于切分元素

4. [tmp, end)尚未处理



与数组快速排序相同,链表排序也需要借助while循环后移tmp节点,并扩展1和3部分。

通过对比可以发现,链表快速排序与数组快速排序的不同点为:

1. 数组排序中,需要不断修正partition的位置,而链表排序中,切分点始终在begin处。

2. 数组排序中,为避免增删元素,需通过交换将小于切分元素的节点向前移动。而链表排序中,只需要将小于begin的节点从原位置删除,再插入到begin正前方即可。

3. 由于没有交换操作,链表的快速排序结构更加清晰。同时,链表的快速排序也是稳定排序

3 小结

在数组和链表的排序中,需要充分考虑到数组和链表在随机访问和增删方面的复杂度,合理编写代码。

以上数组快速排序算法是不稳定的,而链表快速排序算法是稳定的。但一般来说链表快速排序的效率低于数组,这是因为链表的结构更为复杂,且无法有效利用程序局部性。

数组和链表的快速排序平均时间复杂度均为O(nlogn)O(nlogn),平均空间复杂度为O(logn)O(logn)。

以上程序以首节点为切分点,在已排序数组或链表中会遇到最坏复杂度,最坏时间复杂度为O(n2)O(n^2),最坏空间复杂度为O(n)O(n)。

值得注意的是,数组可以采用随机选择切分节点的方式使最坏情况随机化,但链表没有随机访问特性,因此无法高效地随机选择切分节点。也就是说,链表的快速排序算法是有漏洞的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: