线性时间排序: 三种非基于比较的内部排序算法

来源:互联网 发布:java运维实施岗位职责 编辑:程序博客网 时间:2024/05/18 03:31

未完待续。。。。。。

一、基于比较的排序算法,在最坏情况下,最少都需要用O(nlgn)次比较。

下面来证明一下。

不妨我们假设 排序都是从小到大进行排序。

说明用1、2、3来表示三个元素,用π(1)、π(2)、π(3)来表示该元素对应的值。用尖括号〈1,3,2〉表示π(1) ≤ π(3) ≤ π(2)

对于n个元素而言,由于写算法不知道 未来每个待排元素的大小,所以要使排序算法能正确地工作,其必要条件就是:n个元素的 n! 种排列中的每一种都能够作为输出结果。

从本质上来讲,比较排序可以被抽象地视为 决策树。由于比较的结果只有两种:1.小于等于 2.大于。所以该决策树是一棵二叉树。同时,一棵决策树 表示某种排序算法作用于给定输入所做的所有的可能比较。这里程序的控制结构、数据移动等体力活都被忽略了。我们用图片来形象地说明一下。


上图中,决策树的所有叶子结点都是一种可能的排序结果。再次强调一遍,用1、2、3来表示第1个元素、第2个元素、第3个元素,而元素的大小用π(1)、π(2)、π(3)来表示。

从根结点(即起始输入)到一个叶子结点,该条路径对应于比较排序算法的一次实际执行过程。这条路径的长度,就表示对应的排序算法中最坏情况下的比较次数。这个最坏情况下的比较次数 正是 树的高度。于是证明的题目就变成了 求具有n! 个叶子结点的二叉树的最小高度。

由于这是计算理论上该二叉树的最低高度,所以肯定是满二叉树。高度为h的二叉树的叶子结点的数目记为L,所以有 n! ≤ L ≤ 2h  对该式两边同时取对数得:

h ≥ lg(n!) = O(nlgn)

这样,我们从理论上证明了凡是基于比较的排序算法,其最坏情况下的最少时间复杂度一定不小于O(nlgn)。注意,这里的对数都是以2为底的。

二、计数排序(Counting Sort)

先说明一下,这种排序算法本身不会被独立使用,通常是作为基数排序的子过程来调用的。

计数排序假设n个输入元素中的每一个都是介于0到k-1之间的整数。所以说,使用该算法时,必须知道待排序的整数的可能的最大值

计数排序的基本思想就是对每一个输入的元素x,确定出小于等于x的元素的个数。这样,就可以把x直接放到它的最终位置上了。当然,我们是先将x直接放到一个缓冲数组中。这个缓冲数组和原数组具有同样的大小。

举个例子,比如,有17个元素小于等于x,那么x就属于第17个位置。当有几个元素相同时,这个方案要略做修改。因为不能把它们都放在同一位置上。

我们假定输入数组为A[0...n-1],那么输入数组的长度为n。为完成此算法,我们还需要两个数组:存放排序结果的B[0...n-1],以及提供临时存储区的C[0...k]。k为所有待排序整数中的可能最大值。

直接用C语言实现之,如下所示:

void CountingSort(int A[], int B[], int n, int k){int* C = new int[k+1]; //因为还有0值要存,共有k+1个桶//清空记数for(int i = 0; i <= k; i++)C[i] = 0;//统计原数组中每个值出现的次数for(int j = 1; j < n; j++)C[A[j]]++;//统计小于等于每个值对应的个数for(int i = 0; i < k; i++)C[i] += C[i-1];//将每个值 直接输出到缓冲数组中的最终位置上//从后向前输出,保证排序的稳定性for(int j = n - 1; j >=0; j--){B[C[A[j]]] = A[j];C[A[j]]--;}}

下面我们来分析一下时空复杂度。

从上述代码分析,时间复杂度为O(k+n+k+n) = O(k+n),空间复杂度也是O(k+n)。其中k是待排序整数的取值范围内的最大值,n为待排序整数的个数。

我们可以看到,上述代码中根本没有出现输入元素之间的比较。因为它不是基于比较的排序算法,所以它的时间复杂度才可能小于O(nlogn)。说排序算法的时间下界为O(nlogn),是针对于基于比较的排序算法的。

计数排序说白了,是用输入元素自身的实际值来确定它在数组中的最终位置的。

同时,从上面的代码看出,只有输出时是从后向前输出的,才能保证计数排序的稳定性:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的次序相同。也就是说,两个关键字相同的数据,在输入数组中先出现的,在输出数组中也位于前面。稳定性只有在存在卫星数据时才显得比较重要。

再强调一点,如果在后面要讲的基数排序中使用的计数排序子过程不是稳定的话,那么基数排序也就废了。

在介绍基数排序之前,先来说明一下为什么计数排序不好。

比如我们用计数排序来对这三个数进行排序:1,100,9999999。这时,n=3, k = 107。这时,虽然只是对3个数字进行排序,但是时间复杂度却是千万量级的,太慢了。用最水的冒泡排序、鸡尾酒排序都比它快的多。而基数排序,正好利用了计数排序的优点,同时避免了计数排序的缺点。

三、基数排序(radix sort)

变量说明:n个数字,每个数字都有d位,每位上的数字取值范围都是0到k-1

先来说明一下什么是基数。

基数,radix,指的是在某一记数系统中所有的单个数字位。比如在十进制表示中,基数指的就是0到9这十个数字,而在十二进制中,基数指的就是0...9、A、B这十二个数字。

知道了什么是基数,下面开始介绍基数排序(radix sort)。

提到基数排序,很多人马上会提到LSB和MSB。下面先来讲讲什么是LSB和MSB。

MSB是Most Significant Bit的缩写,意为 最高有效位。在二进制数中,MSB是最高加权位。与十进制数字中最左边的一位类似。同理,LSB是Least Significant Bit的缩写,意为 最低有效位。通常,MSB位于二进制数的最左侧,LSB位于二进制数的最右侧。

举个通俗的例子,当比较两个十进制数字大小的时候,是MSB,即最高位起决定性作用。91肯定比19要大。但是当判断一个十进制数字是否为偶数时,是LSB,即92肯定是偶数,尽管十位数字是9,它也是偶数。

基数排序中的LSB和MSB有相似的意思。LSB就是从低位开始,从低到高,逐位数字进行计数排序。MSB则正好相反。

虽然书上全都是用的LSB基数排序,但是MSB基数排序也是可以实现的。如果数字从高位开始排序,意味着有n位,就得有10^n个桶。因为你先排最高位,然后对于这个最高位又要分出十个桶排下一位。比如现在有13,23,12,22,11这五个数。你先为高位排序,就相当于把十位为1的分在一个桶1里(13,12,11),十位为2的分在一个桶2(22,23)里。然后在桶1和桶2之中剩下的元素排序((11),(12),(13))和((22),(23))。这样如果有很多位数,桶就很多。但是从最低位开始排就只需要10个桶,每移动一位,就用针对那一位排序(把元素扔进桶里)。所以不会占用大量的桶。同时,从高位开始排序,就要分段,每排完一位,把分不出大小的几个当成一段,一段段的排,不能让排完的数据跨段移动,保证这一段的数都比下一段小,排到最后每段就只有一个数了。这样就完全没有利用到每次调用计数排序都是稳定排序这一点,所以低位排序优于高位排序。

下面介绍LSB基数排序。

本质上还是计数排序,不过每次比较的都是一位数字,可能取值为0到9这十个数字。所以k = 10。也就是说需要10个桶(其实说成是栈更好)来存放数据。

对于每一位数字的排序,时间复杂度为O(n+k),设每个元素都有d位数字,则总共的时间复杂度为O(d(n+k))。空间复杂度仍为O(n+k)。


0 0
原创粉丝点击