# 什么是堆
如果有一个关键码的集合 K={k_0,k_1,k_2,…,k_{n-1}}
,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足 ki<=k_{2i+1}
且 ki<=k_{2i+2}
(或满足 ki>=k_{2i+1}且ki>=k_{2i+2}
),其中 i=0,1,2,…
则称该集合为堆。
- 小堆:对于一棵完全二叉树,满足任一节点都比其孩子节点小,也就是将根结点为最小元素的堆叫做小堆,也叫最小堆或小根堆。
- 大堆:对于一棵完全二叉树,满足任一节点都比其孩子节点大,也就是将根结点为最大元素的堆叫做大堆,也叫最大堆或大根堆。
# 堆的性质
- 堆中某个结点的值总是不大于或不小于其父结点的值。
- 堆总是一棵完全二叉树。
# 堆的向下调整
假设:节点的左右子树都是堆(大 / 小),但是其自身不是堆,可以通过一次向下的调整来将其变换成一个堆。以调整至大堆为例
- 从根结点处开始,选出左右孩子中值较大的孩子。
- 让较大的孩子与其父亲进行比较。
- 若较大的孩子比父亲大,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。
- 若较大的孩子比父亲小,则不需处理了,调整完成,整个树已经是大堆了。
def sift(li,low,high): | |
''' | |
:param li:列表 | |
:param low:堆的根节点位置 | |
:param high:堆的最后一个元素的位置 | |
:return: | |
''' | |
i = low #父节点 | |
j=2*i+1 #左孩子 | |
tmp=li[low] #堆顶存储 | |
while j<=high: #只要左孩子没越界 | |
if j+1<=high and li[j+1]>li[j]: #如果有右孩子,并且右孩子比较大,别忘了前提需要保证 j+1 位置不越界 | |
j=j+1 #j 指向右孩子位置 | |
if li[j]>tmp: | |
li[i]=li[j] | |
i=j #往下看一层 | |
j=2*i+1 | |
else: #如果 tmp 更大,把 tmp 放到 i 的位置上 | |
li[i]=tmp | |
break | |
else: #别忘了考虑,如果当堆顶元素的左孩子已经越界,直接将其存入 li [i] | |
li[i]=tmp |
# 全过程
- 建立堆,得到堆顶元素,为最大元素
def sift(li,low,high): | |
''' | |
:param li:列表 | |
:param low:堆的根节点位置 | |
:param high:堆的最后一个元素的位置 | |
:return: | |
''' | |
i = low #父节点 | |
j=2*i+1 #左孩子 | |
tmp=li[low] #堆顶存储 | |
while j<=high: #只要左孩子没越界 | |
if j+1<=high and li[j+1]>li[j]: #如果有右孩子,并且右孩子比较大,别忘了还要保证 j+1 位置不越界 | |
j=j+1 #j 指向右孩子位置 | |
if li[j]>tmp: | |
li[i]=li[j] | |
i=j #往下看一层 | |
j=2*i+1 | |
else: # 不管怎么样都得把这个空补上,直接脱到最后执行 | |
break | |
li[i]=tmp | |
def heapCreate(li): | |
n=len(li) | |
for i in range((n-2)//2,-1,-1): #i 表示建堆的时候调整的部分根下标 | |
sift(li,i,n-1) | |
#堆建立完成 | |
# 验证 | |
import random | |
li=[i for i in range(16)] | |
random.shuffle(li) | |
print(li) | |
heapCreate(li) | |
print(li) |
[9, 13, 15, 6, 14, 8, 12, 7, 11, 1, 2, 3, 4, 10, 5, 0] | |
[15, 14, 12, 11, 13, 8, 10, 7, 6, 1, 2, 3, 4, 9, 5, 0] |
- 去掉堆顶,将堆最后一个元素放到堆顶,实质上交换了堆顶元素和堆尾元素,但是需要注意交换后的堆尾元素并不是原来的堆顶元素,而应当是原堆尾元素的左边紧挨着的那一个元素,所以接下来的调整传入的堆尾索引也就是
high
为i-1
,接着可通过一次调整重新使得堆有序 - 堆顶元素为第二大元素
- 重复步骤 2,直到堆空
def sift(li,low,high): | |
''' | |
:param li:列表 | |
:param low:堆的根节点位置 | |
:param high:堆的最后一个元素的位置 | |
:return: | |
''' | |
i = low #父节点 | |
j=2*i+1 #左孩子 | |
tmp=li[low] #堆顶存储 | |
while j<=high: #只要左孩子没越界 | |
if j+1<=high and li[j+1]>li[j]: #如果有右孩子,并且右孩子比较大,别忘了还要保证 j+1 位置不越界 | |
j=j+1 #j 指向右孩子位置 | |
if li[j]>tmp: | |
li[i]=li[j] | |
i=j #往下看一层 | |
j=2*i+1 | |
else: # 不管怎么样都得把这个空补上,直接脱到最后执行 | |
break | |
li[i]=tmp | |
def heapSort(li): | |
n=len(li) | |
for i in range((n-2)//2,-1,-1): #i 表示建堆的时候调整的部分根下标 | |
sift(li,i,n-1) | |
#堆建立完成 | |
for i in range(n-1,-1,-1): #i 指向当前堆的最后一个元素 | |
li[0],li[i]=li[i],li[0] #先交换堆顶元素和堆尾元素 | |
sift(li,0,i-1) #i-1 为新的 high | |
# 验证 | |
import random | |
li=[i for i in range(16)] | |
random.shuffle(li) | |
print(li) | |
heapSort(li) | |
print(li) |
[7, 5, 11, 12, 14, 0, 15, 4, 13, 9, 6, 8, 10, 2, 1, 3] | |
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] |
# 时间复杂度
分为两部分,第一部分向下调整
def sift(li,low,high): | |
i = low | |
j=2*i+1 | |
tmp=li[low] | |
while j<=high: | |
if j+1<=high and li[j+1]>li[j]: | |
j=j+1 | |
if li[j]>tmp: | |
li[i]=li[j] | |
i=j | |
j=2*i+1 | |
else: | |
break | |
li[i]=tmp |
这一部分,相当于走了一遍完全二叉树的深度,设元素个数为 n
,深度即为 log(n+1)
相当于 logn
(根据深度为 h
,结点数最多为 2^h-1
)
def heapSort(li): | |
n=len(li) | |
for i in range((n-2)//2,-1,-1): | |
sift(li,i,n-1) | |
#堆建立完成 | |
for i in range(n-1,-1,-1): | |
li[0],li[i]=li[i],li[0] | |
sift(li,0,i-1) |
第二部分为建堆,逐个出数,外层循环部分均为,而内层为调整函数,所以总的时间复杂度为
# C++ 版本
#define _CRT_SECURE_NO_WARNINGS | |
#include <iostream> | |
#include <algorithm> | |
#include <vector> | |
using namespace std; | |
void max_heapify(vector<int>& nums, int start, int end) | |
{ | |
int dad = start; | |
int son = dad * 2 + 1; | |
while (son <= end) | |
{ | |
if (son + 1 <= end && nums[son] < nums[son + 1]) | |
{ | |
son++; | |
} | |
if (nums[dad] > nums[son]) | |
{ | |
return; | |
} | |
else | |
{ | |
swap(nums[dad], nums[son]); | |
dad = son; | |
son = dad * 2 + 1; | |
} | |
} | |
} | |
void heap_sort(vector<int>& nums, int len) | |
{ | |
for (int i = len / 2 - 1; i >= 0; i--) | |
{ | |
max_heapify(nums, i, len - 1); | |
} | |
for (int i = len - 1; i > 0; i--) | |
{ | |
swap(nums[0], nums[i]); | |
max_heapify(nums, 0, i - 1); | |
} | |
} | |
int main() { | |
int num; | |
vector<int> int_array; | |
while (cin >> num) { | |
int_array.push_back(num); | |
if (cin.get() == '\n')break; | |
} | |
int len = int_array.size(); | |
heap_sort(int_array, len); | |
for (int n : int_array)cout << n << " "; | |
return 0; | |
} |
# topk 问题
现在有 n 个数,设计算法得到前 k 大的数(k<n)
解决思路
排序后切片 O (nlogn)
冒泡、插入、选择排序 O (kn)
堆排序 O (klogn)
取列表前 k 个元素建立一个小根堆,堆顶就是目前第 k 大的数;依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整;遍历列表所有元素后,倒序弹出堆顶。
比如 6 8 1 9 3 0 7 2 4 5
拿出前五个数,建立一个小根堆
剩下 0 7 2 4 5
0<1
0
舍 1
不动
剩下 7 2 4 5
7>1
1
舍 7
上
剩下 2 4 5
7>3
小根堆调整 7、3
对调
剩下 2 4 5
2<3
2
舍 3
不动
剩下 4 5
4>3
3
舍 4
上
剩下 5
5>4
4
舍 5
上
具体怎么做?
基本就是小根堆版本
def sift(li,low,high): | |
''' | |
:param li:列表 | |
:param low:堆的根节点位置 | |
:param high:堆的最后一个元素的位置 | |
:return: | |
''' | |
i = low #父节点 | |
j=2*i+1 #左孩子 | |
tmp=li[low] #堆顶存储 | |
while j<=high: #只要左孩子没越界 | |
if j+1<=high and li[j+1]<li[j]: #如果有右孩子,并且右孩子比较小,别忘了还要保证 j+1 位置不越界 | |
j=j+1 #j 指向右孩子位置 | |
if li[j]<tmp: | |
li[i]=li[j] | |
i=j #往下看一层 | |
j=2*i+1 | |
else: # 不管怎么样都得把这个空补上,直接脱到最后执行 | |
break | |
li[i]=tmp | |
def topk(li,k): | |
heap=li[0:k] #取前 k 个数 | |
for i in range((k-2)//2,-1,-1): | |
sift(heap,i,k-1) | |
#1. 小根堆建立完成 | |
for i in range(k,len(li)): | |
if li[i]>heap[0]: | |
heap[0]=li[i] | |
sift(heap,0,k-1) | |
#2. 遍历列表里剩下的所有元素完成 | |
for i in range(k-1,-1,-1): #i 指向当前堆的最后一个元素 | |
heap[0],heap[i]=heap[i],heap[0] #先交换堆顶元素和堆尾元素 | |
sift(heap,0,i-1) #i-1 为新的 high | |
#3. 挨个出数完成 | |
return heap | |
li=[6,8,1,9,3,0,7,2,4,5] | |
print(li) | |
print(topk(li,5)) |