树堆逃生记
一、动图演示
二、思路分析
1. 相关概念
堆是具有以下性质的完全二叉树:
-
每个结点的值都大于或等于其左右孩子结点的值,称为【大顶堆】;
-
每个结点的值都小于或等于其左右孩子结点的值,称为【小顶堆】。
如下图:
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中,就是下面这个样子。
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:大顶堆:
小顶堆:
2. 基本思想
返回顶部
【1】 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根结点。
【2】 将其与末尾元素进行交换,此时末尾就为最大值。
【3】 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。
【4】 如此反复执行,便能得到一个有序序列了。
3. 步骤
【步骤一】 构造初始堆
将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
a. 假设给定无序序列结构如下
b. 此时我们从最后一个非叶子结点开始,【从右至左,从下至上】进行调整。
(叶结点自然不用调整,第一个非叶子结点
,也就是下面的6结点)
【局部代码对应演示一】
start = 1
end = 4
root = start
while True:
child = 2 * root + 1 # 第一轮循环:child = 3;第二轮循环:child = 9
if child > end: break # 第一轮循环:不执行;第二轮循环:执行,【退出循环】
if child + 1 <= end and arr[child] < arr[child + 1]: # 第一轮循环:成立,执行
child += 1 # 【让子元素中较大值去与根元素比较】,第一轮循环:child = 4
if lt[root] < lt[child]: # 第一轮循环:成立,执行
lt[root], lt[child] = lt[child], lt[root] # 第一轮循环:6和9换位
root = child # 第一轮循环:root = 4
else: break # 【不需要换位,退出循环】,第一轮循环:不执行
c. 找到第二个非叶结点4,由于[4, 9, 8]中9元素最大,4和9交换。【见下面代码第一轮循环】
【局部代码对应演示二】
start = 0
end = 4
root = start
while True:
child = 2 * root + 1 # 第一轮循环:child = 1;第二轮循环:child = 3;第三轮循环:child = 9
if child > end: break # 第一轮循环:不执行;第二轮循环:不执行;第三轮循环:执行,【退出循环】
if child + 1 <= end and arr[child] < arr[child + 1]: # 第一轮循环:不执行;第二轮循环:执行
child += 1 # 【让子元素中较大值去与根元素比较】,第一轮循环:child = 1;第二轮循环:child = 4
if lt[root] < lt[child]: # 第一轮循环:成立,执行;第二轮循环:执行
lt[root], lt[child] = lt[child], lt[root] # 第一轮循环:4和9换位;第二轮循环:4和6换位
root = child # 【从上至下,调整交换导致的子根混乱】,第一轮循环:root = 1;第二轮循环:root = 4
else: break # 【不需要换位,退出循环】,第一轮循环:不执行;第二轮循环:不执行
d. 这时,交换导致了子根[4, 5, 6]结构混乱,继续调整,[4, 5, 6]中6最大,交换4和6。【见上面代码第二轮循环】
此时,我们就将一个无序序列构造成了一个大顶堆。
【说明】:高度 ,调整次数最多 , 实际换了3次【确定最高(2层)的0号元素,用了2次;接下来,确定第一个有子结点**(1层)的1号元素,用1次**】。
【步骤二】 将堆顶元素与末尾元素进行交换,使末尾元素最大。
返回顶部
将堆顶元素9和末尾元素4进行交换
【步骤三】 继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。
a. 重新调整结构,使其继续满足堆定义
【此时采用的是从上至下,选择最大分支8结点,其它分支已经满足堆定义】
【局部代码对应演示三】
start = 0
end = 3
root = start
while True:
child = 2 * root + 1 # 第一轮循环:child = 1;第二轮循环:child = 5
if child > end: break # 第一轮循环:不执行;第二轮循环:执行,【退出循环】
if child + 1 <= end and arr[child] < arr[child + 1]: # 第一轮循环:成立,执行
child += 1 # 【让子元素中较大值去与根元素比较】,第一轮循环:child = 2
if lt[root] < lt[child]: # 第一轮循环:成立,执行
lt[root], lt[child] = lt[child], lt[root] # 第一轮循环:4和8换位
root = child # 【从上至下,调整交换导致的子根混乱】,第一轮循环:root = 2
else: break # 【不需要换位,退出循环】,第一轮循环:不执行
【说明】:重建堆时,只需要进行最多
次交换,实际进行1次。
b.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
【步骤四】 继续进行调整,交换,如此反复进行
返回顶部
【局部代码对应演示四】
start = 0
end = 2
root = start
while True:
child = 2 * root + 1 # 第一轮循环:child = 1;第二轮循环:child = 3
if child > end: break # 第一轮循环:不执行;第二轮循环:执行,【退出循环】
if child + 1 <= end and arr[child] < arr[child + 1]: # 第一轮循环:不执行
child += 1 # 【让子元素中较大值去与根元素比较】,第一轮循环:child = 1
if lt[root] < lt[child]: # 第一轮循环:成立,执行
lt[root], lt[child] = lt[child], lt[root] # 第一轮循环:5和6换位
root = child # 【从上至下,调整交换导致的子根混乱】,第一轮循环:root = 1
else: break # 【不需要换位,退出循环】,第一轮循环:不执行
最终使得整个序列有序。
三、python代码实现
返回顶部
【代码参考自暗焰之珩的博客《Python实现堆排序》】
def big_endian(lt, start, end):
root = start
while True:
child = root * 2 + 1
if child > end:
break
if child + 1 <= end and lt[child] < lt[child + 1]: # 有两个子元素,且后一个子元素更大【反例是只有一个子元素,或第一个子元素更大】
child += 1 # 【让子元素中较大值去与根元素比较】
if lt[root] < lt[child]:
lt[root], lt[child] = lt[child], lt[root]
root = child # 从上至下,调整交换导致的子根混乱
else: # 不需要换位,退出循环
break
def heap_sort(lt):
first=len(lt) // 2 - 1
for start in range (first, -1, -1): # 构建初始堆【范围包括最顶端元素】
big_endian(lt, start, len(lt)-1)
for end in range (len(lt) - 1, 0, -1): # 【范围从最后一个元素到第二个元素,不包括第一个元素,共交换n - 1次】
lt[0], lt[end] = lt[end], lt[0] # 最大元素与末尾元素互换
big_endian(lt, 0, end - 1) # 只剩下end - 1个待排序元素
def main():
lt = [9, 6, 8, 5, 4]
print ('堆排序前:',lt)
heap_sort(lt)
print ('堆排序后',lt)
if __name__ == "__main__":
main()
四、复杂度分析
【时间复杂度】:
构建初始堆的时间复杂度
【推导过程】
-
建立堆时,我们先将n个数据顺序读入到数组中,接着从下向上进行调整。
- 从第一个有子结点的结点开始考虑。由于这个结点最多只有左右两个子结点,因此,这两个子结点可以分别看成是两个最大堆。
- 将根结点从根调整到应该在的位置,最多需要进行【1次交换】位置。这样就形成了一个比之前的子结点堆高度高一(高度为2)的新堆。
- 同样的,与这个结点在同一层上,并在这个结点之前的结点,也最多进行【1次交换】位置。
-
第一层调整完之后,进行上一层的调整。
- 此时,对新一层的每一个结点,其子结点都是堆。
- 同样对这个结点向下调整。最多进行【2次交换】位置。
-
递推得到,对于高度为i的结点,最多进行【i次交换】位置。
【分析】
设堆的高度为h,因堆是完全二叉树,其高度 。
【确定上层元素后,交换可能导致子根混乱,可以这样看:】
【最顶端(只有 个元素) 进行 h 次交换确定位置】
【其子根(有 个元素) 进行 h-1 次交换确定位置】
【……】
【第一个有子结点的层(高度为1,有 个元素) 进行 1 次交换确定位置】【计算】
显然,建立堆时一共的交换次数为所有结点的交换次数之和。高度为i的结点数为 , 因此堆的调整次数为: ,sum乘2,错位相减:
可得 。所以,建立堆的时间复杂度(最坏情况时)为O(n)。
堆排序的时间复杂度
堆排序是一种选择排序,整体主要由【构建初始堆】+【交换堆顶元素和末尾元素,并重建堆】两部分组成。
- 构建初始堆的复杂度为O(n);
- 在交换并重建堆的过程中,需交换n-1次【确定 n-1个元素后,最后一个元素自动确定】;
- 而重建堆的过程中【每次重建最多比较互换 次,比构建初始堆简单】,根据完全二叉树的性质, 逐步递减,近似为 。
- ,所以堆排序时间复杂度最好和最坏情况下都是 级。
【空间复杂度】:
堆排序不要任何辅助数组,只需要一个辅助变量,所占空间是常数,与n无关,所以空间复杂度为O(1)。
堆排序库应用示例:
返回顶部
100w个数中找出最大的100个数。
import heapq # 引入堆模块
import random # 产生随机数
test_list = [] # 测试列表
for i in range(1000000): # 产生100w个数,每个数在【0, 1000w】之间
test_list.append(random.random() * 100000000)
heapq.nlargest(100, test_list) # 求100w个数最大的100个数
总结
堆排序的基本思路:
- 将无序序列构建成一个堆,根据升序(或降序)需求选择大顶堆(或小顶堆);
- 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
- 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素;
- 反复执行“调整+交换”步骤,直到整个序列有序。
堆排序复杂度:
- 时间复杂度:
- 空间复杂度:O(1)
欢迎关注,敬请点赞!
返回顶部