#第六章
##堆
###6.1
什么是堆?
堆和二叉树挺像的,但堆的特点是:
只保证上双亲结点比都子节点 大/小,不保证两个子节点中,大/小的一定在左/右。例如:左子树 或 右子树 的话,是保证子结点的顺序的。
这样的好处是,减少对子节点顺序的排序时间。
什么是最大堆?
从上向下看,所有的双亲结点都比子结点大。官方说法:除了根以外的所有结点 i 都要满足:A[Parent[i]] >= A[i]
最小堆就是,从上向下看,所有的双亲结点都比子结点小。
特点:
最大复杂度:O( n l g n nlgn nlgn)
最小复杂度 :todo
排序空间使用:原址
使用场景:最大优先级 或 最小优先级
###6.2 维护堆的性质
####1,MAX-HEAPIFY(A, i)
它是用于维护最大堆性质的重要过程。功能是,在数组A
中,把下标i
所表示的元素“向下”放到合适的位置。前提是,下标i
下面的堆已经是最大/小堆。
下面是程序和图例:
MAX-HEAPIFY(A, i)的时间复杂度
T(n) <= T(2n/3) + Θ ( 1 ) \Theta(1) Θ(1)
(i 为根结点,n为子树大小)
(每个孩子的子树的大小,最多为 2n/3(不太明白如何证明))
根据主定理,上面的递归式的解为 T(n) = O(lgn)。也就是说,对于一个树高为 h 的结点来说,时间复杂度是 O(h)。
###6.3 建堆
当一个数组不是堆的时候,我们可以使用“自底向上”的方法,把一个大小为 n=A.length 的数组 A[1…n] 转换为最大堆。方法如下:
注意:建堆的时候,对 [1… n/2] 这么些元素进行建堆,顺序是从 [n/2 … 1]。因为 [n/2 + 1 … n] 的元素都是叶子节点,我们要调整的是双亲节点。(如何证明呢?)
例如(下图):
一共有10个结点,我们对 5(n/2) 到 1 的结点进行建堆就可以了。以倒序的方式进行建堆的话,每个双亲节点的“子结点们”都是已经排好的堆。但双亲结点可能是还没有排好,这时候把双亲节点和它的直接子结点进行重排就好了。
举例说明:当 4 和 5 号结点都排序完后,要对 2 号结点进行排序的话,只要把 2 和 4、5 进行排序就行了,因为 4、5 已经排序完了。
###6.3 堆排序
堆排序就是,把一个堆里的元素按大小顺序进行排序。
排序方法:
时间复杂度为:O(nlgn)
(每次 MAX-HEAPIFY 的时间复杂度为 O(lgn),一共要进行 n 次,所以为O(nlgn))
###6.5 优先队列
堆的一个常见应用,就是作为优先队列。我们下面来看一下最大优先队列
的例子。
优先队列(priority queue)是一种用来维护由一组元素构成的集合 S 的数据结构,其中每个元素都有一个相关的值,称为“Key”,一个最大优先队列
支持以下操作:
下面来看一下如何实现这些操作。
1,HEAP-MAXIMUM(A)
这个方法还是挺简单的,只是返回根元素。时间复杂度为 Θ ( 1 ) \Theta(1) Θ(1)
2,HEAP-EXTRACT-MAX(A)
这个方法的具体内容为:
这个方法的时间复杂度为:O(lgn)
3,HEAP-INCREASE-KEY(A, i, key)
感觉这个方法和 MAX-HEAPIFY 方法有点像,这个方法是插入一个新结点,代替原来位置上的结点,然后向上进行维护堆的正确性(MAX-HEAPIFY的话,是向下进行维护堆的正确性)。
程序如下:
这个方法的时间复杂度为:O(lgn)
4,MAX-HEAP-INSERT(A, key)
最后介绍插入功能。这个功能是利用了上面的 HEAP-INCREASE-KEY(A, i, key) 功能:
时间复杂度为:O(lgn)
###思考题
6-1 (用插入的方法建堆)我们可以通过反复调用MAX-HEAP-INSERT实现向一个堆中插入元素,考虑BUILD-MAX-HEAP的如下实现方式:
BULID-MAX-HEAP’(A)
A.heap-size=1
for i=2 to A.length
MAX-HEAP-INSERT(A,A[i])
a.当输入数据相同的时候,BULID-MAX-HEAP和BULID-MAX-HEAP’生成的堆是否总是一样?如果是,请证明;否则,请举一个反例。
b.证明:在最坏情况下,调用BULID-MAX-HEAP’建立一个包含n个元素的堆的时间复杂度是Θ(nlgn).
答:
a. 反例为:[100, 80, 50, 90, 95]
因为 BULID-MAX-HEAP 是从最后一个双亲结点开始进行建堆排序,双新节点 < 两个子结点的话,把双亲结点和最大的子结点进行交换就可以了;而BULID-MAX-HEAP’ 是从根结点后的第1个结点进行排序,每个双亲结点要和每个子结点进行比较和交换,如果双新节点 < 两个子结点的话,要把双亲结点和第1个子结点进行比较和交换,然后再拿交换完的双亲结点,再和第2个子节点进行比较和交换.
###总结
堆有很多方法,这些方法都在什么情况下使用呢?
#第七章 快速排序
###7.1 快速排序描述
快速排序最环情况的时间复杂度为 Θ ( n 2 ) \Theta(n^2) Θ(n2),但通常是实际排序应用中最好的选择,因为它的平均性能非常好:它的期望时间复杂度是 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn),而且 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn)中隐含的常数因子非常小。另外,它还能进行原址排序,甚至在虚拟环境中也能很好的工作。
特点:
最大复杂度:O( n 2 n^2 n2)
平均复杂度 : Θ ( n l g n ) \Theta(nlgn) Θ(nlgn)
最小复杂度 :todo
排序空间使用:原址
使用场景:todo
快速排序的主要思想:
取出要排序的数组的最后1个元素,当成一个基准值。然后从数组的第1个元素开始,和基准值比较,最终把数据分成两组:
数组 <= 基准值
和数组 > 基准值
。但这两部分数组中的数据不是有序的,然后再用递归方法对两部分数组重复使用上面的方法。
快速排序的程序中,主要是对指针的调整,所以包含了4个指针:
小于等于基准值的数组
和 大于基准值的数组
位置指针。i 左边是小于等于基准值
的元素,右边是大于基准值
的元素。(a) 在初始情况下,还没有区分任何大于/小于基准值的元素,所以 i 在起始元素之前。p 指向起始元素,所以指向 2。j 是指向未处理元素,起始元素就没有被处理,所以指向 2。r 指向最后一个元素,所以 r 指向 4。
(b) 开始拿第 j 个元素进行和基准值(最后一个元素的值)进行比较(2 和 4 比较),如果小于等于基准值,就移动指针 i ,向右移动一个位置。关于 j ,每处理一个元素就向后移动一个位置,所以 j 也向后移动一个位置。
© 再拿第 j 个元素进行和基准值比较(8 和 4),如果大于基准值,i 不进行移动,因为 i 右边是大于基准值的元素
。然后 j 继续移动。
(d) 接着是 7 和 4 比较,因为大于基准值,所以 i 也不移动,j 移动。
(e) 1 和 4 比较,因为 1 小于 4 ,所以要放到 i 的左边。于是,拿 A[i + 1] 和 A[j] 进行交换。
(f)(g)(i) 和上面一样
(h) 最后结束循环后,把 A[i+1] 和 A[r] 元素进行交换。因为 i 右边的元素都大于基准值,而基准值在 i 右边,所以要把基准值的位置换一下。
####练习
7.1-2 当数组A[p…r]中的元素都相同时,PARTITION返回的q值是什么?修改PARTITION,使得当数组A[p…r]中所有元素的值都相同时,q=(p+r)/2.
当元素相同时,q=i+1=r-1+1=r.
修改后的函数q=(p+r)/2.
###7.2 快速排序的性能
快速排序时间复杂度依赖于“划分是否平衡”,而平衡又依赖于用于划分的元素。如果划分平衡,快速排序接近于插入排序;如果不平衡,接近于插入排序。
####最坏情况
两个子问题分别包含 n-1 个元素和 0 个元素时,快速排序就发生最坏情况了。假设算法的每一次递归都发出了这种不平衡的划分,划分操作的时间复杂度是 Θ ( n ) \Theta(n) Θ(n)。由于对一个大小为 0 的数组进行递归调用会直接返回,所以T(0) = Θ ( 1 ) \Theta(1) Θ(1),于是算法运行时间的递归式可以表示为:
T ( n ) = T ( n − 1 ) + T ( 0 ) + Θ ( n ) = O ( n 2 ) T(n) = T(n-1) + T(0) + \Theta(n) = O(n^2) T(n)=T(n−1)+T(0)+Θ(n)=O(n2)
注意:为什么问题会分成 n-1 和 0 个元素呢?个人理解是,和 PARTITION 方法返回值有关。因为 PARTITION 返回的是要递归的值,如果数组是一个有序数组(假设从小到大),PARTITION 返回的结果是 0 ,那么
####最好情况
PARTITION 得到的两个子问题的规模都不大于 n/2 (因为每排序一次,递归排序都时都少一个数,所以两边不大于 n/2)。递归时间为:
T ( n ) = 2 T ( n / 2 ) + Θ ( n ) = O ( n l g n ) T(n) = 2T(n/2) + \Theta(n) = O(nlgn) T(n)=2T(n/2)+Θ(n)=O(nlgn)
####平均情况
一般算法我们可能关心的是最坏情况,但快速排序的平均时间更接近于最好情况。假如:把递归时的划分成10份,最坏情况是每次都是(10:0)划分,这种机率是 1/10,所以一般把接近最好情况。
####对于平时情况的进一步观察
对于一个随机输入的数组,在每一层上都有同样的划分是不太可能的,有一些可能会平衡(好的情况),另外一些不太平衡(坏的情况)。如果“好的情况”和“坏的情况”是交替出现的话,假设先出现坏(0, n),再出现好的话( (n-1)/2 - 1, (n-1)/2 ),会产生3个子数组 0、(n-1)/2 - 1、(n-1)/2,它的时间复杂度为: Θ ( n ) + Θ ( n − 1 ) = Θ ( n ) \Theta(n) + \Theta(n-1) = \Theta(n) Θ(n)+Θ(n−1)=Θ(n)。坏的情况 Θ ( n − 1 ) \Theta(n-1) Θ(n−1),可以被吸收到好的情况的划分代价 Θ ( n ) \Theta(n) Θ(n)中去,而得到的划分结果也是好的。
因此,当好和差的划分交替出现时,快速排序的时间复杂度与全是好的划分时一样,仍然是 O(nlgn)。区别只是 O 符号中了从的常数因子要略大一些。
####练习
7.2-2当数组A的所有元素都具有相同值时,QUICKSORT的时间复杂度是什么?
当数组A所有元素相同时,QUICKSORT中的划分时极为不平衡的,n-1:0的划分,T(n)=T(n-1)+Θ(n)解这个递归式T(n)=Θ(n^2)
7.2-3 证明:当数组A包含的元素不同,并且是按降序排列的时候,QUICKSORT的时间复杂度为Θ(n^2)
按照降序排序时,在QUICKSORT中的划分时极为不平衡的,n-1:0的划分,所以其时间复杂度为T(n)=T(n-1)+Θ(n)解这个递归式 T(n)=T(n)=Θ(n^2)
7.2-4 银行一半会按照交易时间来记录某一账户的交易情况。但是,很多人却喜欢收到银行对账单是按照支票号码的顺序来排列的。这是因为,人们通常 都是按照支票号码的顺序来开出支票的,而商人也通常都是根据支票编号的顺序兑付支票。这一问题时按照交易时间排序的序列转换成按支票号排序的 序列,它实质上是一个对几乎有序的输入序列进行排序的问题。请证明:在这个问题上,INSERTION-SORT的性能往往要优于QUICKSORT?
重要前提:“它实质上是一个对几乎有序的输入序列进行排序”
插入排序在基本有序的情况下,基本无需移动任何元素来插入,所以只有外层循环了n次,所以时间复杂度为O(n)。 快速排序在基本有序的情况下,在划分数组时,划分得非常不平衡,那么其时间复杂度是O(nlgn),而达到完全有序时,时间复杂度达到O(n^2)。对于几乎有序的数组来说,基本上能达到插入排序的最小复杂度,所以插入排序更好。
###7.3 快速排序的随机化版本
在上面讨论平均情况时候,我们的前提假设是:输出数据的所有排列都是等概率的。但在实际工程中,这个假设不会总是成立,所以我们可以通过在算法中引入“随机性”,从而便利算法对于所有输入都能获得较好的性能。(很多人都选择随机化版本的快速排序作为大数据输入情况下的排序算法)
原来版本算法是,把最后一个元素作为基准值;随机版本是,随机找一个元素作为基准值,然后把这个值和最后一个元素互换,这样程序改动最小。
####练习
7.3-1 为什么我们分析随机化算法的期望运行时间,而不是其最坏运行时间呢?
随机化算法不能改变最坏情况下得运行时间,但是能降低最坏情况发生的概率。
7.3-2在RANDOMIZED-QUICKSORT的运行过程中,在最坏情况下,随机数生成器RANDOM被调用了多少次?在最好情况下呢?
最好情况是均匀划分,其时间复杂度 T(n)=2T(n/2)+1 =>主定理case1得T(n)=Θ(n)
最坏情况是分成不平衡的划分,其时间复杂度 T(n)=T(n-1)+T(0)+1 各式相加得=>T(n)=Θ(n)
###总结
1,虽然快速排序的最大复杂度为 O ( n 2 ) O(n^2) O(n2),但因为 7.2(好坏相间) 和 7.3(随机防止最坏) 的原因,快速排序的时间复杂度基本上是平均复杂度 O ( n l g n ) O(nlgn) O(nlgn)。
2,为什么快速排序的平均复杂度 和 堆排序的最高复杂度一样(O(nlgn)),还说快速排序比堆排序更快呢?
因为堆排序的常数更大。可以参考:
#第八章
第八章以上的算法,都有一个特点:在排序的最终结果中,各元素的次序依赖于它们之间的比较。我们把这类排序算法称为“比较排序”。
对于任何比较排序,在最坏情况下,都要经过 Ω ( n l g n ) \Omega(nlgn) Ω(nlgn) 次比较。因此,归并排序和堆排序是渐近最优的,并且任何已知的比较排序最多就是在常数因子上优于它们。
下面要介绍的排序算法的时间复杂度是“线性的”。因此,下界 Ω ( n l g n ) \Omega(nlgn) Ω(nlgn) 对它们不适用。
##8.2 计数排序
计数排序不是通过比较,是通过数的不断存储来实现排序的。
特点:
最大复杂度:O( k + n k+n k+n)
最小复杂度 :无(因为是线性的,所有数据的复杂度都是一样的)
排序空间使用:非原址
使用条件:一般用在数据为 0 到 k 区间内的 n 多整数
使用场景:todo
是否稳定:是
其它:具有相同值的元素,在“输出数组”中的位置 和 “输入数组”的位置相同,这也是数据的一种稳定性。这种稳定性,只有当进行排序的数组,还带有卫星数据时才比较重要。
主要思想:
通过 3 个数组来排序:
(a) 首先轮循 数组A,把 数组A 中“每个元素”的“个数”放到 数组C 中。注意 数组A 中的每个元素都是 0 ~ k 之间的一个整数。
(b) 然后把 数组C 中的元素从 1 位开始,进行 C[i] = C[i] + C[i-1],i = 1 to k。
为什么是 1 to k 呢?因为 数组A 中的每个元素都是 0 ~ k ,所以数的种类是 k 种,我们要对这 k 种数时行排序,所以循环 k 次。
这个循环的目的是,让 数组C 的每一位,都记录小于等于 i 的数的个数。例如图 b,C[1] = C[0] + C[1] (2+2)= 2,C[2] = C[1] + C[2] (2+2)= 4 以此类推。
© 从 j = A.length to 1 进行循环,把 A[j] 的保存的要排序的元素取出来(例如:A[8] = 3)。再利用这个 A[j] 从 数组C 中找到相对应的位置所保存的元素(例如:C[3] = 7),C[j] 元素的值,保存的是 A[j] 这个值前面有多少个元素。有多少个元素,代表当前这个 A[j] 应该排在第多少位。(例如:C[3] 的值为 7,那么 3(A[j]) 这个元素前面有 6 个元素,它应该排在第 7 位)。
然后,再把 数组C 对应元素值 - 1。因为元素有重复的,第1个 3 排在第 7 位,第2个 3 应该排在第 6 位了。
计数排序稳定性很重要的另一个原因是:计数排序经常会被用作“排序”算法的一个子过程。
##8.3 基数排序
基数排序(英语:Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。有时候会用基数排序来对“多关键字”的记录进行排序,例如:年月日等。
特点:
最大复杂度: O ( d ( k + n ) ) O( d(k+n) ) O(d(k+n))
最小复杂度 :无(因为是线性的,所有数据的复杂度都是一样的)
排序空间使用:非原址
使用条件:一般用在数据为 0 到 k 区间内的 n 多整数
使用场景:todo
是否稳定:根据里面的子方法,来决定是否是稳定的
第2行可以使用“计数排序”实现。
##8.4 桶排序
桶排序 (bucket sort) 假设输入数据服从均匀分布,平均情况它的时间代价为 O(n)。与计数排序类似,因为对输入作了假设,桶排序的速度也是很快。具体来说,计数排序假设输入数据都属于“一个小区间内的整数”,而桶排序则假设输入是一个随机过程产生,该过程将元素均匀、独立地分布在 [0, 1) 区间上。
特点:
最大复杂度: O ( n 2 ) O(n^2) O(n2)
最小复杂度 : O ( n ) O(n) O(n)
排序空间使用:非原址
使用条件:数据元素均匀、独立地分布在 [0, 1) 区间上。
使用场景:todo
是否稳定:todo
主要思想:
有点和 java 的 HashMap 实现有点像,将 [0, 1) 区间划分为 n 个相同大小的子区间,称为“桶”。然后,
根据“划分规则”,把数据放到各个桶里。
例如:分成 10 个桶,每个数都取小数点后第 1 位(0.79 的话,取 0.7 ),把数放到小数点后第 1 位所对应的桶中(放到第 7 个桶中)。每个桶都是一个类似链表的结构,用来存放多个数。最后,从0号桶开始把每个桶里的链表拿出来,按顺序输出。
关于时间复杂度:
最大复杂度: O ( n 2 ) O(n^2) O(n2)。如果不能均匀分布,且所有数据都在一桶中,且数据都是倒序方式排序。如果要正序输出的话,在每次向桶里放数据时,执行插入算法时间复杂度是最高( O ( n 2 ) O(n^2) O(n2)),所以最高复杂度是 O ( n 2 ) O(n^2) O(n2)。
最小复杂度 : O ( n ) O(n) O(n)。如果(能均匀分布,且每个桶只放一个数据) 或者 (数据都是正序方式排序,放一个桶里的话,时间复杂度都是 O ( n ) O(n) O(n)。
#第九章 中位数和顺序统计量
什么是中位数?
对于一组有限个数的数据来说,它们的中位数是这样的一种数:这群数据里的一半的数据比它大,而另外一半数据比它小。
什么是顺序统计量?
在统计学中,样本的第 k顺序统计量(英语:Order Statistics)即它从小到大排列时的第 k个值。
这章介绍的是取得“顺序统计量”的算法。我们可以用堆排序和归并排序对数据进行排序,然后在输出中第 k 个元素就可以了。但是本意要介绍一些更快的算法。
##9.1 最小值和最大值
###在一个有 n 个元素的集合中,需要做多少次才能确定最小元素呢?
n -1 次比较。依次遍历每个元素,并记录最小元素。
###同时找到最小和最大值
因为找到最小值需要 n-1 次,所以“独立地”找出最小值和最大值,则需要 2n-2 次比较。
事实上,只需要 3[n/2] 下界 次比较,就可以同时找到最小和最大值。我们并不是将每一个输入元素与当前的最小值和最大值进行比较,这样代价是“每个元素需要2次比较”。可以成对处理元素,将一对输入元素相互进行比较,,然后把较小的元素与最小值进行比较,较大的与最大值进行比较,这样每两个元素只需要比较 3 次。
##9.2 期望为线性时间的选择算法
什么选择问题?
选择问题就像顺序统计量一样,从小到大的排列中找到第 K 个值。一般选择问题看起来比找最小值这样的简单问题更难。但令人惊奇的是,这两个问题的渐近运行时间却是相同的: Θ ( n ) \Theta(n) Θ(n)。
下面介绍选择问题的分治算法。RANDOMIZED-SELECT 算法是以快速排序算法为模型,但与快速排序不同的是:快速排序分递归处理划分的两边,而 RANDOMIZED-SELECT 只处理划分的一边就够了。(因为,RANDOMIZED-SELECT 找的是 K 值,在快速排序第一次执行后,就可以知道这个 K 值是比快速排序的“基准值”大还是小。如果大的话,就去大的那边进行不断递归就可以找到了,小的那边根本不需要处理。)
假设输入数据都是“互异”的。快速排序的期望时间是: Θ ( n l g n ) \Theta(nlgn) Θ(nlgn),而RANDOMIZED-SELECT的期望时间为 Θ ( n ) \Theta(n) Θ(n)。
特点:
最大复杂度: Θ ( n 2 ) \Theta(n^2) Θ(n2) (快速排序的最大复杂度)
平均复杂度 : Θ ( n ) \Theta(n) Θ(n)
排序空间使用:原址
使用条件:一般用在数据为 0 到 k 区间内的 n 多整数,并且是互异的。
使用场景:todo
是否稳定:todo
注意,在最后的 RANDOMIZED-SELECT(A, q+1, r, i - k)中,为什么是 i-k 呢?如果是有 10 个数,我们想找到第 7 大的数,那么 7 应该是后半段里面。假设 k 是 5 的话,那么第 7 大的数,在后半段的里的相对位置应该是第 2 位(7 -5),所以这里应该是 i-k。
###9.3 最坏时间为线性时间的选择算法
主要思想:
把 n 个数,按一定规模 Q 分成几组,最后一组的个数可以不为 Q,但前面的组的个数必须为 Q 个数。例如:n 为 28,Q 为 5 的话,就是 5 个数一组,分成 6 组。前 5 组每组个数为 5,最后一组个数为 3。如图:
对每组数进行“插入排序”,然后找出每组的中位数。这样,每组数都是排好顺序的。
对第2步找出的 n/Q 个中位数,进行递归调用 SELECT 以找出这几个“中位数”当中的“中位数”。这样,这 6 组数就有顺序了。图1 的空心结点就是每组的中位数,这些中位数的箭头方向,也是表示中位数之间的大小,最左边的数最小。
利用修改过的 PARTITION 版本,按“中位数”当中的中位数 x 进行划分,分成两部分“小于等于x”部分 和 “大于 x 部分”
如果 i = k,返回 x。如果 i < k,则在低区调用 select 来找出第 i 小的元素。如果 i > k,则在高区递归查找第 i-k 小的元素。(这里看的不是很明白)
简单的说,这种算法在最坏情况下,也会把时间复杂度缩减为原来的 3/4。什么呢?
通过上面的 123 可以判断,图1 阴影部分(右下),是“一定大于 x ”的部分,左上部分是“一定小于 x ”的部分。其它两部分(右上和左下)不确定是大于 x 还是 小于 x。所以,如果要找小于 x 部分的话,要在 左上 + 左下 + 右上 这 3 部分来找,右下因为是一定大于 x 部分,所以就不用找了。所以,时间复杂度为原来的 3/4。
在第8章中,我们知道在比较模型中,即使在平均情况下,排序仍然需要 Ω ( n l g n ) \Omega(nlgn) Ω(nlgn)时间。而且,第8章的线性时间排序算法在输入上作了一些假设。相反,本章中的线性时间选择算法不需要任何关于输入的假设。它们不受限于 Ω ( n l g n ) \Omega(nlgn) Ω(nlgn)的下界约束,因为他们没有使用排序就解决了选择问题。因此,在本意引言部分介绍的排序和索引方法,不是解决选择问题的渐近高效方法。
疑问:
1,第8章的假设是什么?这章的问题没有假设吗?n 个数,并且是互异的,这算不算假设?
2,“他们没有使用排序就解决了选择问题”,在 RANDOMIZED-SELECT 算法里面,没有用到像“插入排序”那样的“全排序”,但还是用了快速排序那样的半排序算法。
3,第8章和第9章都是线性的,有什么联系吗?是不是都能作同一件事?比如第K大数