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

一般来说,我们讨论排序都是针对数组结构。数组的特点是:可以很方便地进行随机访问,但是增删元素比较耗时。因此,针对数组的排序,通常会避免元素的增删操作,改为元素交换。同时,常采用二分的方法实现高效排序算法。
链表与数组相反,随机访问非常耗时,但增删元素很简单。因此链表的排序和数组也会有所不同。
这篇博客针对数组和链表的不同,分析了常用排序算法——快速排序在数组和链表中的实现。

注:我们规定排序的语义如下:
sort(begin, end)表示对[begin, end)之间的元素排序,包含begin,但不包含end

1 数组的快速排序

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

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

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

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
此处headtail不存储数据,是为了在对链表进行操作时无需考虑边界情形。

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

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 小结

  1. 在数组和链表的排序中,需要充分考虑到数组和链表在随机访问和增删方面的复杂度,合理编写代码。
  2. 以上数组快速排序算法是不稳定的,而链表快速排序算法是稳定的。但一般来说链表快速排序的效率低于数组,这是因为链表的结构更为复杂,且无法有效利用程序局部性。
  3. 数组和链表的快速排序平均时间复杂度均为O(nlogn),平均空间复杂度为O(logn)
  4. 以上程序以首节点为切分点,在已排序数组或链表中会遇到最坏复杂度,最坏时间复杂度为O(n2),最坏空间复杂度为O(n)
  5. 值得注意的是,数组可以采用随机选择切分节点的方式使最坏情况随机化,但链表没有随机访问特性,因此无法高效地随机选择切分节点。也就是说,链表的快速排序算法是有漏洞的。
发布了10 篇原创文章 · 获赞 1 · 访问量 1万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览