堆排序及python中heapq堆详解
一、知识点
堆的结构可以分为大根堆和小根堆,是一个完全二叉树,而堆排序是根据堆的这种数据结构设计的一种排序。
1. 大根堆和小根堆
性质:每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆;每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆。如下图
我们对上面的图中每个数都进行了标记,上面的结构映射成数组就变成了下面这个样子
还有一个基本概念:查找数组中某个数的父结点和左右孩子结点,比如已知索引为i的数,那么
1.父结点索引:(i-1)/2(这里计算机中的除以2,省略掉小数)
2.左孩子索引:2*i+1
3.右孩子索引:2*i+2
所以上面两个数组可以脑补成堆结构,因为他们满足堆的定义性质:
大根堆:arr(i)>arr(2i+1) && arr(i)>arr(2i+2)
小根堆:arr(i)<arr(2i+1) && arr(i)<arr(2i+2)
二、堆排序基本步骤
基本思想:
1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
2.1 构造堆
将无序数组构造成一个大根堆(升序用大根堆,降序就用小根堆)
假设存在以下数组
主要思路:第一次保证0~0位置大根堆结构(废话),
第二次保证0~1位置大根堆结构,
第三次保证0~2位置大根堆结构...
直到保证0~n-1位置大根堆结构
(每次新插入的数据都与其父结点进行比较,如果插入的数比父结点大,则与父结点交换,否则一直向上交换,直到小于等于父结点,或者来到了顶端)
插入6的时候,6大于他的父结点3,即arr(1)>arr(0),则交换;此时,保证了0~1位置是大根堆结构,如下图:
(待交换的数为蓝色,交换后的数为绿色)
插入8的时候,8大于其父结点6,即arr(2)>arr(0),则交换;此时,保证了0~2位置是大根堆结构,如下图
插入5的时候,5大于其父结点3,则交换,交换之后,5又发现比8小,所以不交换;此时,保证了0~3位置大根堆结构,如下图
插入7的时候,7大于其父结点5,则交换,交换之后,7又发现比8小,所以不交换;此时整个数组已经是大根堆结构
2.2 固定最大值再构造堆
此时,我们已经得到一个大根堆,下面将顶端的数与最后一位数交换,然后将剩余的数再构造成一个大根堆
(黑色的为固定好的数字,不再参与排序)
此时最大数8已经来到末尾,则固定不动,后面只需要对顶端的数据进行操作即可,拿顶端的数与其左右孩子较大的数进行比较,如果顶端的数大于其左右孩子较大的数,则停止,如果顶端的数小于其左右孩子较大的数,则交换,然后继续与下面的孩子进行比较
下图中,5的左右孩子中,左孩子7比右孩子6大,则5与7进行比较,发现5<7,则交换;交换后,发现5已经大于他的左孩子,说明剩余的数已经构成大根堆,后面就是重复固定最大值,然后构造大根堆
如下图:顶端数7与末尾数3进行交换,固定好7,
剩余的数开始构造大根堆 ,然后顶端数与末尾数交换,固定最大值再构造大根堆,重复执行上面的操作,最终会得到有序数组
三、大根堆和堆排序python实现
堆的特点就是FIFO(first in first out)先进先出
堆在接收数据的时候先接收的数据会被先弹出。
栈的特性正好与堆相反,是属于FILO(first in/last out)先进后出的类型。
大根堆调整(max_heapify):
将堆的末端子结点作调整,使得子结点永远小于父结点。这是核心步骤,在建堆和堆排序都会用到。
比较根结点和与其所对应的孩子结点的值,当i根结点的值比左孩子结点的值要小的时候,就把根结点和左孩子结点所对应的值交换,同理,就把根结点和右孩子结点所对应的值交换。
然后再调用堆调整这个过程,可见这是一个递归的过程。
# 大根堆
def max_heapify(arr, heapSize, root):
# 先记最大的数索引为root,给定某个结点下标root
largest = root
# 左孩子下标
left = 2 * root + 1 # left = 2*root + 1
# 右孩子下标
right = 2 * root + 2 # right = 2*root + 2
# 当根结点root的值比左孩子结点的值小时
if left < heapSize and arr[root] < arr[left]:
# largest记录左孩子的下标值
largest = left
# # 当根结点root的值比右孩子结点的值小时
if right < heapSize and arr[largest] < arr[right]:
# largest记录右孩子的下标值
largest = right
# 如果此时largest记录的下标不为root,则把根结点root和孩子结点所对应的值交换
if largest != root:
arr[root], arr[largest] = arr[largest], arr[root] # 交换
# 进行递归调用
max_heapify(arr, heapSize, largest)
# 堆排序
def heapSort(arr):
n = len(arr)
# 建立大根堆,自底向上建堆,-1代表递减,从n到-1进行遍历
for i in range(n, -1, -1):
max_heapify(arr, n, i)
# 进行堆排序,一个个交换元素
# -1代表递减,从n-1到0进行遍历
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i] # 交换
max_heapify(arr, i, 0)
arr = [12, 11, 13, 5, 6, 7]
heapSort(arr)
n = len(arr)
print("排序后:")
print(arr) # [5, 6, 7, 11, 12, 13]
四、python中heapq堆详解
python中heapq自带的为小根堆
import math
import random
import heapq
from io import StringIO
# 显示树结构的函数
def show_tree(tree, total_width=36, fill=' '):
output = StringIO()
last_row = -1
for i, n in enumerate(tree):
if i:
row = int(math.floor(math.log(i+1, 2)))
else:
row = 0
if row != last_row:
output.write('\n')
columns = 2**row
col_width = int(math.floor((total_width * 1.0) / columns))
output.write(str(n).center(col_width, fill))
last_row = row
print(output.getvalue())
print('-' * total_width)
print
return
# 显示树结构
data = random.sample(range(1,8), 7)
print('data: ', data)
show_tree(data)
"""
输出:
data: [4, 6, 3, 1, 2, 7, 5]
4
6 3
1 2 7 5
------------------------------------
"""
# 1.heappush插入元素到堆中
data = random.sample(range(1,8), 7)
print('data: ', data)
heap = []
for i in data:
print('add %3d:' % i)
heapq.heappush(heap, i)
show_tree(heap)
"""
输出:
data: [2, 1, 7, 3, 4, 5, 6]
add 2:
2
------------------------------------
add 1:
1
2
------------------------------------
add 7:
1
2 7
------------------------------------
add 3:
1
2 7
3
------------------------------------
add 4:
1
2 7
3 4
------------------------------------
add 5:
1
2 5
3 4 7
------------------------------------
add 6:
1
2 5
3 4 7 6
------------------------------------
"""
# 2.heapq.heapify(list),将list类型转化为heap, 在线性时间内, 重新排列列表。
print('data: ', data)
heapq.heapify(data)
print('data: ', data)
show_tree(data)
"""
输出:
data: [2, 1, 7, 3, 4, 5, 6]
data: [1, 2, 5, 3, 4, 7, 6]
1
2 5
3 4 7 6
------------------------------------
"""
# 3.heappop弹出堆顶的数(删除堆中最小的元素)
data = random.sample(range(1,8), 7)
print('data: ', data)
show_tree(data)
heap = []
while data:
i = heapq.heappop(data)
print('pop %3d:' % i)
show_tree(data)
heap.append(i)
"""
输出:
data: [1, 5, 4, 2, 7, 6, 3]
1
5 4
2 7 6 3
------------------------------------
pop 1:
3
5 4
2 7 6
------------------------------------
pop 3:
4
5 6
2 7
------------------------------------
pop 4:
5
2 6
7
------------------------------------
pop 5:
2
7 6
------------------------------------
pop 2:
6
7
------------------------------------
pop 6:
7
------------------------------------
pop 7:
------------------------------------
"""
# 4.heapq.heapreplace(iterable, n),现有元素并将其替换为一个新值。
data = random.sample(range(1, 8), 7)
print('data: ', data)
heapq.heapify(data)
show_tree(data)
for n in [8, 9, 10]:
smallest = heapq.heapreplace(data, n)
print('replace %2d with %2d:' % (smallest, n))
show_tree(data)
"""
输出:
data: [7, 3, 5, 2, 4, 1, 6]
1
2 5
3 4 7 6
------------------------------------
replace 1 with 8:
2
3 5
8 4 7 6
------------------------------------
replace 2 with 9:
3
4 5
8 9 7 6
------------------------------------
replace 3 with 10:
4
8 5
10 9 7 6
------------------------------------
"""
# 5.heapq.nlargest(n, iterable) 和 heapq.nsmallest(n, iterable),返回列表中的n个最大值和最小值
data = range(1, 6)
l = heapq.nlargest(3, data)
print(l) # [5, 4, 3]
s = heapq.nsmallest(3, data)
print(s) # [1, 2, 3]