算法导论 总结索引 | 第二部分 第六章:堆排序

本文详细介绍了排序算法如插入排序、归并排序、堆排序和快速排序,强调了它们的时间复杂度、原址性以及在构建最大堆和优先级队列中的应用。讨论了如何维护堆的性质以及堆排序的HEAPSORT过程。
摘要由CSDN通过智能技术生成

第二部分:排序和排序统计量(96)

1、每个记录 包含一个关键字,就是 排序问题中要重排的值。记录的剩余部分 由卫星数据 组成,通常 与关键字是一同存取的
关注排序问题,通常假定 输入只是由数组成

2、插入排序 最坏情况下 可以在Θ(n2)时间内 将n个数排好序。对于 小规模的输入,插入排序 是一种非常快的原址排序(输入数组中 仅有常数个元素 需要在排序过程中 存储在数组外) 算法

归并排序 有更好的渐近运行时间Θ(nlgn),但它所使用的 MERGE过程并不是原址的

第六章介绍 堆排序,是一种O(n lgn)时间的 原址排序算法。堆 还可以实现 优先级队列

第七章介绍 快速排序,是一种 原址排序算法,最坏情况运行时间为Θ(n2),它的期望运行时间为Θ(n lgn),在实际应用中 通常比堆排序快。与插入排序 类似,快速排序的代码很紧凑,因此 运行时间中 隐含的常数系数很小。快速排序 是排序大数组的最常用算法

3、插入排序、归并排序、堆排序 及 快速排序 都是比较排序算法:通过对元素进行 比较操作来确定 输入数组的有序次序。第八章 介绍了决策树模型,可用来 研究比较排序算法的性能局限。使用 决策树模型,可以证明 任意比较排序算法 排序n个元素的最坏情况运行时间 的下界为Ω(n lgn),证明 堆排序和归并排序 是渐近最优的 比较排序算法

通过 比较操作之外的方法 来获得输入序列 有序次序的信息,可能 打破Ω(n lgn)的下界。计数排序 假定输入元素的值 均在集合{0, 1, … , k} 内,可以在Θ(k + n)的时间内 将n个数排好序
基数排序 扩展计数排序的适用范围。如果 有n个整数 要进行排序,每个整数有d位数字,并且 每个数字可能取k个值,基数排序 就可以在Θ(d(n + k))时间内 完成排序工作。当d是常数且 k = O(n)时,基数排序的运行时间 就是线性的
桶排序算法,需要 了解输入数组中 数据的概率分布。对于 半开区间[0, 1) 内服从均匀分布的n个实数,桶排序的平均情况运行时间 为O(n)

算法最坏情况运行时间平均情况/期望运行时间
插入排序Θ(n2)Θ(n2)
归并排序Θ(n lgn)Θ(n lgn)
堆排序O(n lgn)——
快速排序Θ(n2)Θ(n lgn) (期望)
计数排序Θ(k + n)Θ(k + n)
基数排序Θ(d(n + k))Θ(d(n + k))
桶排序Θ(n2)Θ(n) (评价情况)

与 归并排序一样,堆排序的时间复杂度是 O(n lgn)。与 插入排序相同,堆排序 同样具有 空间原址性

1、堆

1、(二叉)堆 是一个数组,可以被看成 一个近似的完全二叉树。除最底层外,该树是完全满的,从左向右填充
虽然 A[1…A.length] 可能都存有 数据,但只有 A[1…A.heap-size] 中存放的是 堆的有效元素,这里 0 <= A.heap-size <= A.length
它的父节点、左孩子 和 右孩子的下标:
堆父节点、左右孩子
连线是 父子关系,父节点 总在 孩子节点的左边

通过将i的值 左移一位,LEFT过程 在一条指令内计算出2i。在RIGHT过程中 通过将i的值 左移一位 并在低位加1,快速计算得到 2i + 1。PARENT过程中,可以通过把i的值 右移1位 计算得到⌊i/2⌋

2、二叉堆 可以分为两种形式:最大堆 和 最小堆。在 最大堆中,最大堆性质 是指除了根以外的 所有节点i都要满足
最大堆性质
堆中的最大元素 存放在 根节点中

最小堆的组织方式 正好相反:除了根以外的 所有节点i都有
最小堆性质
3、在 堆排序算法 中,使用的 最大堆;最小堆 通常用于 构造优先级队列

4、把堆的高度 定义为 根节点的高度。既然 一个包含n个元素的队 可以看做一棵完全二叉树,堆的高度是 Θ(lgn)。堆结构上的 一些基本操作的运行时间 至多与树的高度 成正比(时间复杂度为 O(lgn))

MAX-HEAPIFY过程:时间复杂度O(lg n),维护最大堆性质的关键
BUILD-MAX-HEAP过程:线性时间复杂度,从 无序的输入数据中 构造一个最大堆
HEAPSORT过程:其时间复杂度 O(n lgn),对一个数组 进行原址排序
MAX-HEAP-INSERT、HEAP-EXTRACT-MAX、HEAP-INCREASE-KEY和HEAP-MAXIMUM过程:时间复杂度为 O(lgn),功能是 实现一个优先队列

5、考虑下标为⌊n/2⌋+1的结点的左孩子下标
练习

2、维护堆的性质(100)

1、MAX-HEAPIFY的输入为 一个数组A 和 下标i,调用的时候,假定根节点为LEFT(i)和RIGHT(i)的二叉树 都是最大堆(根节点换了,左右节点仍然符合 最大堆的性质),但这时A[i]有可能小于 其孩子。MAX-HEAPIFY通过让A[i]的值 在最大堆中 逐级下降,从而 使得下标i为根节点的子树 重新遵循最大堆的性质

最大值只可能是 左右孩子和根节点中,因为左右孩子是满足 最大堆的条件的

MAX-HEAPIFY(A, i)
	l = LEFT(i)
	r = RIGHT(i)
	if l <= A.heap-size and A[l] > A[i]
		largest = l
	else largest = i
	if r <= A.heap-size and A[r] > A[largest]
		largest = r
	if largest != i
		exchange A[i] with A[largest]
		MAX-HEAPIFY(A, largest)

从A[i]、A[LEFT(i)]和A[RIGHT(i)]中选出最大的,并将其 下标存储在 largest中。如果A[i]是最大的,程序结束。最大元素是i的某个孩子节点,则 交换A[i]和A[largest]的值,从而使i及其孩子都满足 最大堆的性质。交换后,下标为largest的结点的值 是原来的A[i],以该节点为根的子树 又可能违反最大堆的性质(因为值减小了),因此 需要对该子树递归调用 MAX-HEAPIFY(过程如2中图 印刷部分

2、对于 一棵以i为根节点、大小为n的子树,MAX-HEAPIFY的时间代价包括:调整A[i]、A[LEFT(i)] 和 A[RIGHT(i)] 的关系 的时间代价Θ(1),加上 在一棵以i的一个孩子 为根节点的子树上运行 MAX-HEAPIFY的时间代价
每个孩子的子树的大小 至多为 2n/3(最坏情况发生在 树的最底层恰好半满的时候)计算过程见下图红笔部分

MAX-HEAPIFY过程 与 递归计算
MAX-HEAPIFY过程与递归计算
递推公式 和 主定理计算过程
递推公式和主定理计算过程
MAX-HEAPIFY时间复杂度 的计算
MAX-HEAPIFY时间复杂度的计算
3、用循环控制结构取代递归,重写MAX-HEAPIFY代码

MAX-HEAPIFY(A, i)
    while i ≤ A.heap-size/2 (循环部分,到叶子节点层(heap-size/2)为止)
        l = LEFT(i)
        r = RIGHT(i)
        if A[l] > A[i]
            largest = l
        else largest = i
        if A[r] > A[largest]
            largest = r
        if largest != i
            exchange A[i] with A[largest]
            i = largest (更新i)
        else break

4、
4

3、建堆

1、用 自底向上的方法 利用过程MAX-HEAPIFY把一个大小为 n = A.length 的数组 A[1…n]转化为最大堆
每个叶子节点 都可以看成 只包含一个元素的堆。过程 BUILD-MAX-HEAP 对树中的 除叶节点之外的结点 都调用一次 MAX-HEAPIFY
建堆过程
为了证明 BUILD-MAX-HEAP 的正确性,使用如下循环不变量:证明见 102
在第2-3行中 每一次for循环开始,结点 i+1,i+2,…,n 都是一个 最大堆的根节点

2、BUILD-MAX-HEAP 的时间复杂度:
BUILD-MAX-HEAP 的时间复杂度
公式 A.8 计算过程
在这里插入图片描述
在这里插入图片描述

构建堆的过程:
构建堆的过程
证明 高度为h的堆最多包含 ⌈n/2k+1⌉个高度为h的结点 来自 第六章 堆排序 答案
证明过程

4、堆排序算法

1、初始时候,堆排序算法 利用BUILD-MAX-HEAP将输入数组 A[1…n] 建成最大堆。数组的最大元素 总在根节点 A[1]中,通过把它与A[n] 进行互换,可以把 元素放到正确的位置上。如果 从堆中去掉结点n(减少A.heap-size来实现),剩余的节点中,原来根的孩子结点 仍然是 最大堆,而新换到根节点的可能会违背性质,调用 MAX-HEAPIFY(A, 1),从而在 A[1…n-1]上构造 一个新的最大堆。一直重复,直到 堆的大小从 n-1降到2
算法描述
过程如下
过程1过程2

2、时间复杂度:HEAPSORT过程的时间复杂度是 O(n lgn),每次调用BUILD-MAX-HEAP的时间复杂度是O(n),而n - 1次调用 MAX-HEAPIFY,每次的时间为 O(lgn)

3、证明HEAPSORT的正确性 的循环不变量:在算法2-5行 for循环每次迭代开始时,子数组 A[1…i] 是一个包含了数组 A[1…n] 中第i小元素的最大堆,而子数组 A[i+1…n]包含了数组A[1…n]中已排序的 n-i 个最大元素

初始化:在第一次循环迭代之前,i=n,子数组A[1…n]是一个包含了数组A[1…n]中第n小元素的最大堆,而子数组A[n+1…n]包含了数组A[1…n]中已排序的0个最大元素。

保持:因为每次循环都把根结点从子数组A[1…i]中取出加入到子数组A[i+1…n]中,而且MAX-HEAPIFY维护了子数组A[1…i]是一个最大堆的性质,所以子数组A[1…i]是一个包含了数组A[1…n]中第i小元素的最大堆,而子数组A[i+1…n]包含了数组A[1…n]中已排序的n-i个最大元素。

终止:过程终止时,i=1,子数组A[1]包含了数组A[1…n]中的最小元素,而子数组A[2…n]包含了数组A[1…n]中已排序的n-1个最大元素,所以数组A[1…n]已经全部有序

4、对于一个按升序或降序排列的包含n个元素的有序数组A来说,HEAPSORT的时间复杂度都是O(nlgn)

倒序时,虽然 在建堆时,MAX-HEAPIFY过程中 不会进行再调用MAX-HEAPIFY,但是建堆过程中仍需要 遍历数组只是 不需要调整了,还是O(n);在维护堆的时候还是 需要n-1次交换并 每次调整堆的操作 把换上来的小根结点沉下去(O(lgn)),这里的代价是不变的,只要建堆建好了就有的代价,也是O(nlgn)

5、优先队列(105)

1、第七章介绍的 快速排序 性能优于 堆排序

2、优先级队列有两种形式:最大优先队列 和 最小优先队列

3、优先队列 是一种用来维护 由一组元素构成的集合S的数据结构,其中的每一个元素 都有一个相关的值,称为 关键字。一个最大优先队列 支持以下操作:
INSERT(S, x):把元素x插入集合S中。这一操作 等价于 S=S∪{x}
MAXIMUM(S):返回S中具有 最大关键字的元素
EXTRACT-MAX(S):去掉并返回S中具有 最大关键字的元素
INCREASE-KEY(S, x, k):将元素x的关键字值 增加到k,这里假设k的值 不小于x的原关键字值

1)过程HEAP–MAXIMUM可以在Θ(1)时间内 实现MAXIMUM操作

HEAP-MAXIMUM(A)
	return A[1]

2)过程 HEAP-EXTRACT-MAX 实现 EXTRACT-MAX 操作。它与 HEAPSORT 过程的 for循环体部分 相似

HEAP-EXTRACT-MAX(A)
	if A.heap-size < 1
		error "heap underflow"
	max = A[1]
	A[1] = A[A.heap-size]
	A.heap-size = A.heap-size - 1
	MAX-HEAPIFY(A, 1)
	return max

HEAP-EXTRACT-MAX 的时间复杂度为 O(lgn)。时间复杂度为 O(lgn)的MAX-HEAPIFY之外,其他操作 都是常数阶的

3)HEAP-INCREASE-KEY能够实现 INCREASE-KEY操作。需要将元素A[i]的关键字更新为新值,因为增大关键字A[i]可能违反最大堆的性质,采用了类似 INSERT-SORT中插入循环的方式,从当前结点到 根结点的路径上,为 新增的关键字 寻找恰当的插入位置
在 HEAP-INCREASE-KEY的过程中,当前元素 会不断地与其父结点 进行比较,如果 当前元素的关键字较大,则 当前元素与 其父结点 进行交换。这一过程不断重复,直到当前元素的关键字 小于其父结点时终止

HEAP-INCREASE-KEY(A, i, key)
	if key < A[i]
		error "new key is smaller than current key"
	A[i] = key
	while i > 1 and A[PARENT(i)] < A[i]
		exchange A[i] with A[PARENT(i)]
		i = PARENT(i)

HEAP-INCREASE-KEY的时间复杂度时 O(lgn)(因为关键字更新的结点 到根结点的路径长度为O(lgn))
HEAP-INCREASE-KEY操作过程
4)MAX-HEAP-INSERT能够实现 INSERT操作。它的输入是要被插入到 最大堆A中的新元素的关键字。MAX-HEAP-INSERT首先通过增加一个关键字为负无穷 的叶结点来扩展最大堆,然后调用 HEAP-INCREASE-KEY 作为新结点 设置对应的关键字,同时 保持最大堆的性质

MAX-HEAP-INSERT(A, key)
	A.heap-size = A.heap-size + 1
	A[A.heap-size] = -∞
	HEAP-INCREASE-KEY(A, A.heap-size, key)

MAX-HEAP-INSERT的运行时间为 O(lgn)

在一个包含n个元素的堆中,所有优先级队列的操作 都可以在O(lgn)时间内完成

4、最大优先级队列 应用在 共享计算机系统的作业调度:最大优先队列 记录将要执行的 各个作业 以及 它们之间的相对优先级。当一个作业完成 或者 被中断后,调度器 调用EXTRACT-MAX从所有的等待作业中,选出 具有最高优先级的作业 来执行。在任何时候,调度器 可以调用INSERT把一个新作业 加入到 队列中来

5、在用堆来实现 优先队列时,需要在堆中的 每个元素里 存储对应对象的句柄,其准确含义 依赖于具体程序
在重新确定堆元素位置时,也需要更新相应应用程序对象中的 数组下标

6、在HEAP-INCREASE-KEY的第5行的交换操作中,利用INSERTION-SORT内循环部分的思想,只用一次赋值就完成这一交换操作

HEAP-INCREASE-KEY(A, i, key)
    if key < A[i]
        error "new key is smaller than current key"
    while i > 1 and A[PARENT(i)] < key
        A[i] = A[PARENT(i)]
        i = PARENT(i)
    A[i] = key    

// 原来的代码
A[i] = key
while i > 1 and A[PARENT(i)] < A[i]
	exchange A[i] with A[PARENT(i)]
	i = PARENT(i)    

7、一个时间复杂度为O(nlgk)的算法,它能够将k个有序链表合并为一个有序链表,这里n是所有输入链表包含的总的元素个数

MERGE-ORDER-LIST(A, n, k)
    let R be a new min-heap
    i = 0
    for j = 1 to n
        i += 1
        i %= k
        if A[i].length ≥ 1
            MIN-HEAP-INSERT(R, A[i][1])
            A[i].REMOVE-HEAD()

不用堆直接 选出每个链表的表头元素中最小的 插入新链表 的代价是Θ(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值