深入浅出数据结构C语言版(22)——排序决策树与桶式排序

  在(17)中我们对排序算法进行了简单的分析,并得出了两个结论:

  1.只进行相邻元素交换的排序算法时间复杂度为O(N2)

  2.要想时间复杂度低于O(N2),算法必须进行远距离的元素交换

  

  而今天,我们将对排序算法进行进一步的分析,这一次的分析将针对“使用比较进行排序”的排序算法,到目前为止我们所讨论过的所有排序算法都在此范畴内。所谓“使用比较进行排序”,就是指这个算法实现排序靠的就是让元素互相比较,比如插入排序的元素与前一个元素比较,若反序则交换位置,再比如快速排序小于枢纽的元素分为一组,大于枢纽的元素分为另一组。它们都是依靠“比较”来完成排序工作。

  要对使用比较进行排序的算法进行分析,我们首先要引入一个概念:决策树。

  决策树就是这样的二叉树:树的根结点表示“元素的所有可能顺序”,树的每一条边表示“一种可能的比较”,一条边连接的孩子结点则是“父结点经过该边所代表的比较后剩余的可能顺序”。这样的解释很难理解,但有图搭配就可以好很多:

  

  上图是一棵三元素排序决策树,根结点处表示所有可能的顺序,而从根延伸下来的两条边分别表示了两种“决策”,或者说“比较”,经过该“决策”后就可以得出剩余的可能情况,比如根结点的左孩子是经历决策“a<b”后剩余的可能。显然,叶子代表只剩一种可能顺序。

  注意,决策树并没有代表任何排序算法,即没有哪个排序算法是这样工作的。但是决策树可以给我们这样一个信息:通过比较来排序的算法,本质上就是沿着决策树从根到某个叶子的路径比较下去。

  因此,分析这条“路径”平均经过多少条边,就相当于分析使用比较的排序算法平均需要多少次比较。这也是本次分析与(17)的不同之处,在(17)中我们的分析针对的是排序算法的“交换”次数,这次我们分析的是“比较”次数,而比较次数显然更为关键,因为不论元素是否远距离交换,比较总是存在的。

  要分析使用比较进行排序的算法平均进行几次比较,我们就必须知晓以下定理。

  定理1:深度为d的二叉树,最多拥有2d个叶子

  证明很简单:二叉树的深度d即二叉树中深度最大的叶子的深度d,若存在某个叶子深度不是d,则可以在该叶子下添加两个孩子而不改变树的深度,因此深度为d的二叉树要有最多的叶子则必为满二叉树,此时有叶子2d个(深度为d的层最多有2d个结点)

  定理2:有y个叶子的二叉树,深度至少为[logy](底数默认为2)

  证明:由定理1可以直接推出。

  这个证明可能有点难懂,我们可以触类旁通一下:假如1元钱最多可以买5个糖,那么5个糖最少需要多少钱?答案是1元,恰好是反函数的关系。类似的,深度为x的二叉树最多有y个叶子,那么有y个叶子的二叉树最少有多少深度?答案就是x了。

  定理3:N元素排序的决策树有N!个叶子结点

  证明:N元素排序的可能顺序共有N!个,而决策树的叶子就是表示“仅剩的可能性”即某一种可能顺序,所以N元素排序的决策树共有N!个叶子

  定理4:使用元素比较的排序算法至少需要O(logN!)次比较

  证明:由定理2可知,有y个叶子的决策树,深度至少为[logy],而N元素排序决策树叶子数量必为N!,所以N元素排序决策树深度至少为[logN!],也即N元素排序决策树的任一叶子深度至少为[logN!],而叶子的深度就表示了从根到该叶子的路径上经过的边的数量,也就是“比较”的次数,因此定理4成立。

  定理5:使用元素比较的排序算法至少需要Ω(N*logN)次比较

  证明:根据定理4进行继续计算:

  logN!=log(N*(N-1)*(N-2)*……*2*1)

    =logN+log(N-1)+log(N-1)+……+log2+log1

    >=logN+log(N-1)+……log(N/2)

    >=(N/2)*log(N/2)=(N/2)*log(N*1/2)=(N/2)*logN+(N/2)*log(1/2)

    >=(N/2)*logN-N/2

    =Ω(N*logN)

  

  定理5就是我们这次分析的最终结果,并且我们可以将定理5进行一个推广:假设存在X种可能情形,确定具体情形的方法是不断地问“是或否”型的问题,那么累计需要问的次数至少是[logX]。

  那么根据定理5,堆排序、合并排序和快速排序是否已经代表了排序的最快境界呢?不是的,因为定理5依然是有“限定”的,那就是通过比较进行排序的算法才符合,也就是说不是通过比较来完成排序的话,是可能突破这个界限的。

  不通过比较来完成排序,是个什么样子?我们这里可以举一个简单的例子:桶式排序。其时间复杂度是O(N)。

  现实生活中桶式排序的思想是不少见的,举个例子感受一下:

  假设我们有很多硬币,一分、二分、五分、一角、五角和一元都有,现在我们想要将它们按从小到大排好序,该怎么做?手工模拟任意排序算法都可以完成这项工作,但没有人会这么傻。大部分人的做法都是:准备6个“桶”,分别存放这6种硬币,一分的扔进一分桶,一元的扔进一元桶,所有硬币扔进桶里了,再按顺序从桶里倒出来,排序就完成了。

  将上述思想转换到计算机中就是这样:假设我们的元素都是自然数,且一定小于MAX,那我们只要准备MAX个空桶,即定义一个整形数组bucket[MAX],并将其全部初始化为0。然后遍历所有元素,若元素为i,则令bucket[i]加1,最后统计数组bucket的情况,就可以得出元素的顺序:

//size为数组src的大小,也即元素个数
void BucketSort(unsigned int *src,unsigned int size)
{
    //MAX为宏,表示src中元素不会大于等于的值
    unsigned int bucket[MAX] = { 0 };

    //将元素们“扔进桶里”
    for (unsigned int i = 0;i < size;++i)
        ++bucket[src[i]];

    //将桶里的元素“倒出来”
    unsigned int j = 0;
    for (unsigned int i = 0;i < MAX;++i)
        for (unsigned int x = 0;x < bucket[i];++x)
            src[j++] = bucket[i];
}

  显然,桶式排序的局限性在于要求元素必须是自然数,必须存在上限且上限不可过分大,因为元素的上限决定了桶的数量,而桶的数量并不是想要多少有多少,比如我的电脑就不支持分配一个大小为INT_MAX的数组。

  桶式排序还有一种变种,只需要10个桶即可,感兴趣的可以去搜索“桶式排序”或“基数排序”,此处不做介绍。

  本篇博文就是有关排序的最后一篇博文了,下一篇博文开始,我将会介绍图论算法,并不难,至少理解起来是不难。

时间: 2024-11-05 18:19:46

深入浅出数据结构C语言版(22)——排序决策树与桶式排序的相关文章

深入浅出数据结构C语言版(8)——后缀表达式、栈与四则运算计算器

在深入浅出数据结构(7)的末尾,我们提到了栈可以用于实现计算器,并且我们给出了存储表达式的数据结构(结构体及该结构体组成的数组),如下: //SIZE用于多个场合,如栈的大小.表达式数组的大小 #define SIZE 1000 //表达式的单个元素所使用的结构体 typedef struct elem { int num = 0; //若元素存储操作数则num为该操作数 char oper = '='; //若元素存储操作符则oper为该操作符 bool IsNum = false; //用于

深入浅出数据结构C语言版(4)——表与链表

在我们谈论本文具体内容之前,我们首先要说明一些事情.在现实生活中我们所说的"表"往往是二维的,比如课程表,就有行和列,成绩表也是有行和列.但是在数据结构,或者说我们本文讨论的范围内,我们所说的"表"是一维的,即所有"元素"都是前后排列的.就我个人而言,这样的"表"用"队列"来形容比较恰当.但是,数据结构中"队列"这个名词是被一种特殊的"表"给占用了的,所以我们没法再用

深入浅出数据结构C语言版(9)——多重表(广义表)

在深入浅出数据结构系列前面的文章中,我们一直在讨论的表其实是"线性表",其形式如下: 由a1,a2,a3,--a(n-1)个元素组成的序列,其中每一个元素ai(0<i<n)都是一个"原子","原子"的意思就是说元素本身是一个个体,所有元素都是相同的结构. 但是在我们常见的某些应用,比如Excel的表格中,我们发现表并不一定是线性表,Excel中的表就明显是二维的结构 那么在数据结构中,我们会使用这种广义上的表吗?答案是会,我们也会.或

深入浅出数据结构C语言版(17)——有关排序算法的分析

这一篇博文我们将讨论一些与排序算法有关的定理,这些定理将解释插入排序博文中提出的疑问(为什么冒泡排序与插入排序总是执行同样数量的交换操作,而选择排序不一定),同时为讲述高级排序算法做铺垫(高级排序为什么会更快). 在讨论相关定理之前,我们必须先掌握一个与顺序有关的概念:逆序数. 所谓逆序数,就是"逆序组合的个数",假设我们希望的顺序为从小到大(反之同理): 设有元素互异数列X0,X1,X2--Xn-1,(元素互异即数列中任取两数均不相等)从中任取两数作为组合(Xa,Xb),若a<

深入浅出数据结构C语言版(21)——合并排序

在讲解合并排序之前,我们先来想一想这样一个问题如何解决: 有两个数组A和B,它们都已各自按照从小到大的顺序排好了数据,现在我们要把它们合并为一个数组C,且要求C也是按从小到大的顺序排好,请问该怎么做? 这个问题非常容易解决,我们将A.B和C都视为队列,然后不断比较A和B的首部,取出其中更小的数据出队 http://pic.cnhubei.com/space.php?uid=4593&do=album&id=1092952http://pic.cnhubei.com/space.php?ui

深入浅出数据结构C语言版(5)——链表的操作

上一次我们从什么是表一直讲到了链表该怎么实现的想法上:http://www.cnblogs.com/mm93/p/6574912.html 而这一次我们就要实现所说的承诺,即实现链表应有的操作(至于游标数组--我决定还是给它单独写个博文比较好~). 那么,我们的过程应该是怎么样的呢?首先当然是分析需要什么操作,然后再逐一思考该如何实现,最后再以代码的形式写出来. 不难发现,我们希望链表能支持的(基础,可以由此延伸)操作就是: 1.给出第n个元素 2.在第n个元素的后面插入一个元素(包含在最后一个

深入浅出数据结构C语言版(19)——堆排序

在介绍优先队列的博文中,我们提到了数据结构二叉堆,并且说明了二叉堆的一个特殊用途--排序,同时给出了其时间复杂度O(N*logN).这个时间界是目前我们看到最好的(使用Sedgewick序列的希尔排序时间复杂度为O(N4/3),下图为两者函数图像对比,但是注意,这并不是希尔排序与堆排序的对比,只是两个大O阶函数的对比).这篇博文,我们就是要细化用二叉堆进行排序的想法,实现堆排序. 在介绍优先队列的博文中(http://www.cnblogs.com/mm93/p/7481782.html)所提到

深入浅出数据结构C语言版(15)——优先队列(堆)

在普通队列中,元素出队的顺序是由元素入队时间决定的,也就是谁先入队,谁先出队.但是有时候我们希望有这样的一个队列:谁先入队不重要,重要的是谁的"优先级高",优先级越高越先出队.这样的数据结构我们称之为优先队列(priority queue),其常用于一些特殊应用,比如操作系统控制进程的调度程序. 那么,优先队列该如何实现呢?我们可以很快给出三种解决方案. 1.使用链表,插入操作选择直接插入到表头,时间复杂度为O(1),出队操作则遍历整个表,找到优先级最高者,返回并删除该结点,时间复杂度

深入浅出数据结构C语言版(12)——从二分查找到二叉树

在很多有关数据结构和算法的书籍或文章中,作者往往是介绍完了什么是树后就直入主题的谈什么是二叉树balabala的.但我今天决定不按这个套路来.我个人觉得,一个东西或者说一种技术存在总该有一定的道理,不是能解决某个问题,就是能改善解决某个问题的效率.如果能够先了解到存在的问题以及已存在的解决办法的不足,那么学习新的知识就更容易接受,也更容易理解. 万幸的是,二叉树的讲解是可以按照上述顺序来进行的.那么,今天在我们讨论二叉树之前,我们先来讨论一种情形.一种操作:假设现在有一个数组,数组中的数据按照某