算法导论 总结索引 | 第二部分 第八章:线性时间排序

1、介绍了几种能在 O(n lgn)时间内排序n个数的算法。归并排序和堆排序达到了最坏情况下的上界;快速排序在平均情况下达到该上界

2、比较排序:在排序的最终结果中,各元素的次序 依赖于 它们之间的比较

任何比较排序 在最坏情况下 都要经过 Ω(n lgn)次比较,归并排序和堆排序是渐近最优的

3、本节 三种线性时间复杂度的排序算法:计数排序、基数排序 和 桶排序。因为 这些排序是用 运算 而不是 比较来确定排序顺序的,下界 Ω(n lgn) 不适用

1、排序算法的下界(122)

1.1 决策树模型

比较排序 可以被抽象成 一棵决策树。决策树 是一棵完全二叉树
决策树模型
对一个正确的比较排序算法来说,n个元素的 n!种可能的排列 都应该出现在决策树的叶结点上,而且,每一个叶结点都必须是可以 从根结点经由某条路径到达的,该路径 对应于比较排序的一次实际执行过程

1.2 最坏情况的下界

1、在决策树中,从根结点到任意一个可达叶结点之间 最长简单路径的长度,表示的是 对应的排序算法中最坏情况下的 比较次数。因此,一个比较算法中的最环情况比较次数 就等于其决策树的高度

2、在最坏情况下,任何比较排序算法 都需要做 Ω(n lgn) 次比较

证明:一棵高度为h、具有l个 可达叶结点的决策树,对n个元素做 比较排序
因为 输入数据的 n! 种可能的排列 都是叶子节点,所以有 n! <= l。由于 在一棵高为h的二叉树中,叶结点的数目 不多于 2h
证明过程1
对该式 两边取对数
证明过程2
3、在一棵比较n个元素的排序算法的决策树中,一个叶结点可能的最小深度是n

2、计数排序

1、计数排序 假设个人元素中的 每一个都是在0到k区间内的一个整数,其中 k为某个整数,当 k = O(n) 时,排序的运行时间为 Θ(n)

2、计数排序的基本思想是:对每一个输入元素x,确定 小于x的元素个数。利用这一信息,就可以 直接把工放到它在输出数组中的位置上了

假设输入 是一个数组A[1…n],A.length = n。我们还需要两个数组:B[1…n] 存放排序的输出,C[0…k] 提供临时存储空间

COUNTING-SORT(A, B, k)
let C[0..k] be a new array
for i = 0 to k
	C[i] = 0
	
// C存放A数组中 值等于下标i的 有多少个
for j = 1 to k
	C[A[j]]++

// C存放A数组中 值小于或等于 下标i的 有多少个
for i = 1 to k
	C[i] = C[i] + C[i - 1]

// 处理重复元素
for j = A.length downto 1    // 如果改成 1 to A.length ,正确但不稳定了
	B[C[A[j]]] = A[j]
	C[A[j]]--

图示
最后一个for循环,把每个元素A[j]放到 它在输出数组B中的 正确位置上
每将 一个值 A[j] 放入数组B中 以后,都要将 C[A[j]]的值 减1。当遇见下一个值 等于A[j] 的输入元素时,该元素可以 直接被放到 输出数组中A[j]前一个位置上

3、计数排序 时间代价:第一个for循环所花时间 Θ(k),第二个for循环所花时间 Θ(n),第三个 Θ(k),第四个 Θ(n)。总时间代价 Θ(k + n)

下界 优于上节所证明的O(n lgn),因为 它并不是一个比较排序算法

4、计数排序的一个重要性质就是它是稳定的:具有相同值的元素 在输出数组中的相对次序与 它们在输人数组中的相对次序 相同

证明:因为 提供临时存储空间的数组C中 存储的是每类元素在排好序的 最终数组中的最大下标,所以在COUNTING-SORT的最后一个循环中,同一类元素 在存放排序输出的数组B中,是 从大下标往小下标填入的
又因为 最后一个循环是逆序遍历输入数组A,即 从大下标往小下标遍历输入数组A,与 存放排序输出的数组B中填入元素的方式相同,所以 在输入数组A中相同的多个元素,有更大下标的元素 在存放排序输出的数组B中 也有更大的下标

3、基数排序

1、基数排序 是一种用在卡片排序机上的算法

2、从直观上来看,可能会觉得 应该按最高有效位 进行排序,然后 对得到的每个容器 递归地进行排序,最后 再把所有结果合并起来,遗憾的是,为了排序 一个容器中的卡片,10个容器中的9个 都必须先放在一边,这一过程 产生了许多要保存的临时卡片

基数排序是 先按最低有效位进行排序 来解决卡片排序问题的,然后算法 将所有卡片合并成一叠
其中0号客器中的卡片都在1号容器中的卡片之前,而1号容器中的卡片 又在2号容器中的卡片前面,依此类推。之后,用同样的方法 按次低有效位 对所有的卡片进行排序,并把 排好的卡片 再次合并成 一叠

为了确保基数排序的正确性,一位数 排序算法必须是稳定的

3、基数排序 伪代码

// 获取数组中的最大值
function getMax(arr[], n) 
    max = arr[0]
    for i = 1 to n-1
        if arr[i] > max
            max = arr[i]
    return max

// 使用计数排序对数组按照指定位进行排序
function countSort(arr[], n, exp) 
    output[n]  // 存储排序后的输出数组
    count[10]  // 存储每个数字的出现次数,0到9
    for i = 0 to 9
        count[i] = 0
    
    // 统计每个数字出现的次数
    for i = 0 to n-1
        index = (arr[i] / exp) % 10
        count[index]++
    
    // 将count[i]更新为该数字在输出数组中的最后一个位置的索引
    for i = 1 to 9
        count[i] += count[i-1]
    
    // 构建输出数组
    for i = n-1 downto 0
        index = (arr[i]/exp) % 10
        output[count[index]-1] = arr[i]
        count[index]--
    
    // 将排序后的数组复制给原数组
    for i = 0 to n-1
        arr[i] = output[i]

// 使用基数排序对数组进行排序
function radixSort(arr[], n) 
    max = getMax(arr, n)
    
    // 对每一位数字进行计数排序
    for exp = 1 to max
        countSort(arr, n, exp)

基数排序 首先获取数组中的最大值,然后 针对每一位数字进行计数排序(从个位开始到最高位)

index = (arr[i]/exp) % 10 是计算基数排序中 某个数字 在某一位上的值(也就是它的余数)的表达式。这里 arr[i] 是待排序数组中的某个元素,exp 是基数排序 当前处理的位数(比如个位、十位、百位等),因为基数排序 是从低位到高位进行排序的,所以这里 exp 表示当前位的权重。10 表示 我们是按照十进制进行排序
所以 (arr[i] / exp) 这一部分先将待排序元素的值 按照当前位的权重进行了缩放,然后 % 10 会取得这个数 在当前位上的值(也就是取余数)

举个例子,如果 arr[i] 是 123,exp 是 10,那么 (arr[i]/exp) 就是 12,% 10 就是 2,这就是 123 在十位上的值

4、给定 n个d位数,其中 每一个数位有k个可能的取值,如果 RADIX-SORT使用的稳定排序方法 耗时 Θ(n + k) ,那么 他就可以在 Θ(d(n + k)) 时间内 将这些数排好序

当 每位数字 都在0到k - 1区间内(有k个可能的取值),且k的值 不太大的时候,计数排序 是个好的选择。对n个d位数来说,每一轮排序 耗时 Θ(n + k)。共有d轮,因此 基数排序的总时间为 Θ(d(n + k))

5、给定一个 b位数 和 任何正整数 r <= b,如果 RADIX-SORT 使用的 稳定排序算法 对数据取值区间是 0到k的输入 进行排序耗时 Θ(n + k) ,那么 它就可以在 Θ((b/r)(n + 2r)) 时间内 将这些数 排好序

证明:对于一个值 r<=b,每个关键字 可以看做 d = ⌈b/r⌉个r位数。(例如,可以将 一个32位的数字 看做是4个8位的数,于是有 b=32,r=8,k=2r-1=255 和 d=b/r=4)
所选择的r(r<=b)值能够 最小化表达式(b/r)(n + 2n)
详见P126

6、基数排序 是否比 基于比较的排序算法(如快速排序)更好?
如果 b = O(lg n),而且 选择r≈lgn,基数排序的运行时间为 Θ(n)。这一结果 看上去比快速排序的 期望运行时间代价 Θ(n lgn) 更好一些。但是 隐藏在Θ后面的 常数项是不同的。在处理n个关键字时,尽管 基数排序执行的循环轮数 会比快速排序要少,但是 每一轮 它所耗费的时间 要长得多

哪一个排序方法 更适合 更依赖于 具体实现 和 底层硬件的特性(例如,快速排序 通常可以比 基数排序 更有效地使用 硬件的缓存),以及 输入数据的特征
利用 计数排序作为 中间稳定排序的 基数排序 不是原址排序,而很多 比较排序是原址排序。当主存比较宝贵时,更倾向于 使用快速排序 这样的原址排序算法

7、稳定的排序算法:插入排序、归并排序;不稳定的排序算法:堆排序、快速排序。能使任何排序算法都稳定的方法:给每个元素新增一个数据项存放元素在输入数组中的下标,并在最后根据此数据项对相同的元素进行最终排序。此方法的额外时间开销是Θ(n lgn),额外空间开销是 Θ(n)

8、在O(n)时间内,对0到n3-1区间内的n个整数进行排序:

把这n个整数全部转换成n进制;
用基数排序 对这些n进制的n个整数进行排序
在 0 到 n3-1 区间内的 n个整数转换成n进制后 最多有3位,根据4,可以在Θ(3(n+n)) = Θ(n) 时间内 将这些数排好序

4、桶排序(127)

1、桶排序 假设输入数据 服从均匀分布,平均情况下 它的时间代价为O(n)。计数排序 假设输入数据 都属于 一个小区间内的整数,而 桶排序 则假设 输入是由 一个随机过程产生,该过程 将元素 均匀、独立地分布在 [0, 1) 区间上

桶排序将 [0, 1) 区间划分为 n个相同大小的子区间,称为 桶。将n个输入数 分别放在各个桶中
先对 每个桶中的数 进行排序,然后 遍历 每个桶,按照次序 把各个桶中的元素 列出来即可

2、需要一个临时数组 B[0…n - 1] 来存放链表(即桶)
图示
伪代码
3、验证算法正确性:看两个元素 A[i]和A[j],假设 A[i] <= A[j]。由于 ⌊nA[i]⌋ <= ⌊nA[j]⌋,元素A[i]或者与A[j]被放入 同一个桶中,或者 被放入 一个下标更小的桶中。如果 A[i] 和 A[j] 在同一个桶中,则 7-8行的for循环会将其正确排序,如果 在不同的桶中,第9行会 将其正确排序。所以 桶排序正确

4、运行时间:在最坏情况下,除 第8行外,其他各行代价是 O(n),再加上 第8行中n次插入排序 调用所花费时间
现在来分析 桶排序的运行时间。假设 ni 是表示桶B[i] 中元素个数的随机变量。因为 插入排序的时间代价 是平方阶的,所以 桶排序的时间代价是
桶排序时间代价
通过 对输人数据 取期望,我们可以计算 期望的运行时间。对上式 两边取 期望
期望运行时间
其中,期望运行时间计算 证明见 P128
所以 桶排序的期望运行时间为:
桶排序期望运行时间
即使 输入数据 不服从均匀分布,桶排序 也仍然可以线性时间内完成,只要 输入数据 满足下列性质:所有桶的大小的平方和 与总的元素数 呈线性关系

5、桶排序的最坏情况运行时间:在最坏情况下,桶排序的n个输入数 都被放到1个桶中,在第8行对此桶中的元素 用插入排序进行排序的时间代价是 O(n2)。除第8行以外,所有其他各行时间代价都是 O(n)。因此,桶排序在最坏情况下运行时间是 O(n2)

通过把桶排序第8行中对各个桶中的元素进行排序的排序算法改成归并排序或者堆排序,可以使其在保持平均情况为线性时间代价的同时,最坏情况下时间代价为 O(n lgn)

  • 27
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值