一、什么是堆?
堆:一种特殊的完全二叉树结构。
大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大;
小根堆:一棵完全二叉树,满足任一节点都比其他孩子节点小。
二、堆的向下调整性质
假设:节点的左右子树都是堆,但自身不是堆。
1、图示向下调整过程
由于左右子树都是大根堆,但是2并不比其孩子节点大,因此2不称职,需要更换新的领导
2也不够资格做8、5的父节点,继续下移,8提上来做父节点:
2也不够资格做6、4的父节点,将6提上来做父节点,2放到6原来的位置,成为叶子节点:
2、堆向下调整总结
当根节点的左右子树都是堆时(根节点不满足堆的性质),可以通过一次向下的调整来将其变换成一个堆。
三、堆排序
1、堆排序过程
1、建立堆
2、得到堆顶元素为最大元素
3、去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序。
4、堆顶元素为第二大元素。
5、重复步骤3,知道堆变空
2、堆排序过程——挨个出数图示
(1)如下图所示为一个堆,9为堆顶元素,也是堆的最大元素
(2)去除堆顶元素9,将堆最后元素3放到堆顶
(3)此时满足了向下调整的条件,用向下调整以保证仍为一个堆(完全二叉树)
(4)此时堆顶元素8是第二大元素,再次去除堆顶元素8,再次将3提到堆顶。
(5)再次满足向下调整的条件,做向下调整,依此类推。
3、堆排序过程——构造堆图示
如上图所示的二叉树不符合堆的结构特征,由于向下调整的性质,构造堆首先要让下级先有序。
(1)如果有很多层怎么看?看最后一个非叶子节点!对子树做一次调整
(2)再看前一个非叶子节点,该子树符合堆的结构特点因此不做调整
(3)再看前一个非叶子节点,该子树不符合堆结构,进行子树调整
(4)再观察前一个非叶子节点,以整体作为子树调整
(5)到这一步之后就又开始了向下调整,堆也就构造完成了
四、堆排序代码实现
在实际实现中为了最大节省空间和时间,并不会重新生成一个空间存放堆顶元素。而是将堆顶元素(9)和最后一个元素(3)进行交换。并标记9这个元素不在堆内,只是占用了一个位置,标记元素(4)是堆的最后一个元素。
1、向下调整函数的实现
def sift(li, low, high):
"""
向下调整函数
:param li:列表
:param low:堆的根节点位置
:param high:堆的最后一个元素的位置
:return:
"""
i = low # 父节点位置(编号下标)最开始指向根节点(0)
j = 2 * i + 1 # 子节点位置(左孩子节点编号下标为2i+1)
tmp = li[low] # 把堆顶存起来
while j<= high: # 只要j位置有值就一直循环(保证不越界)
if j<= high and li[j+1] > li[j]: # 如果右孩子存在并且大于左孩子
j = j + 1 # 将j指向右孩子
if li[j] > tmp: # 如果下标j节点元素大于堆顶元素
li[i] = li[j] # 将j位置上的数写到i位置(空位置)上
i = j # 再往下看一层
j = 2 * i +1 # j指向下一层的左子孩子
else: # 如果tmp更大,将tmp放到i的位置上
li[i] = tmp # 循环跳出条件一:tmp放到了某一个父节点位置上
break
else: # 循环跳出条件二:j>high ,此时i已经指向了叶子节点,i不存在子节点了
li[i] = tmp # 将tmp放在叶子节点上
2、使用sift函数实现堆排序
def heap_sort(li):
n = len(li)
"""建堆"""
for i in range((n-2)//2, -1, -1): # i从n-2整除2开始倒着遍历到0,一个一个子树调整
# i表示建堆的时候调整的部分根的下标。
sift(li, i, n-1)
"""挨个出数"""
for i in range(n-1, -1, -1): # i从n-1开始一直到零
# i指向当前堆的最后一个元素
li[0], li[i] = li[i], li[0] # 堆顶(li[0])和最后一个元素(li[i])交换位置
sift(li, 0, i-1) # i-1是新的high,堆中最后一个元素
五、堆排序时间复杂度
首先sift函数最多是走一个树的高度层(走左边右边就不用考虑),因此它的时间复杂度是logn。
由此可见heap_sort是2个nlogn,因此堆排序的时间复杂度是nlogn级别。
六、python堆排序内置模块(heapq)
import heapq # q——》queue优先队列
import random
li = list(range(10))
random.shuffle(li)
print(li)
heapq.heapify(li) # 建堆
print(li)
n = len(li)
for i in range(n):
print(heapq.heappop(li), end=',') # 每次弹出最小元素
"""
[3, 4, 7, 6, 2, 5, 1, 0, 8, 9]
[0, 2, 1, 4, 3, 5, 7, 6, 8, 9]
0,1,2,3,4,5,6,7,8,9,
"""
七、topk问题(堆应用)
1、什么是topk问题?
现在有n个数,设计算法得到前k大的数。(k<n)
常用于实现网站热搜榜等。
2、解决思路
(1)排序后切片:O(nlogn)
(2)排序LowB三人组:O(kn)
(3)堆排序的思路:O(nlogk)
取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数(最小的数)。
依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行依次调整。
遍历列表所有元素后,倒序弹出堆顶。
3、堆排序思路图解
比如要从以下这十个数中取前五大的数:
先取前五个数建立一个小根堆:
现在堆顶1就是小根堆中第五大的数,下一个数是0,比1还要小,直接排除。
再下一个数是7,7比1大,因此7把1换掉:
小根堆向下调整:
接着看2,2比3小,直接排除,4比3大替换3,5比4大替换4.均不需要做向下调整:
这样就得到了前5大的数。它还是需要遍历所有的数来判断每个数是否进堆(O(n)),同时堆的大小是k,因此调整的复杂度是O(logk)。所以总的时间复杂度是O(nlogk)
4、基于堆排序的topk代码实现
def sift(li, low, high):
"""
向下调整函数 (小根堆)
:param li:列表
:param low:堆的根节点位置
:param high:堆的最后一个元素的位置
:return:
"""
i = low # 父节点位置(编号下标)最开始指向根节点(0)
j = 2 * i + 1 # 子节点位置(左孩子节点编号下标为2i+1)
tmp = li[low] # 把堆顶存起来
while j<= high: # 只要j位置有值就一直循环(保证不越界)
# if j+1 <= high and li[j+1] > li[j]: # 如果右孩子存在并且大于左孩子
if j + 1 <= high and li[j + 1] < li[j]: # 取两个孩子里小的那个
j = j + 1 # 将j指向右孩子
# if li[j] > tmp: # 如果下标j节点元素大于堆顶元素
if li[j] < tmp: # 只要小于省长就放过来,满足父亲比孩子小
li[i] = li[j] # 将j位置上的数写到i位置(空位置)上
i = j # 再往下看一层
j = 2 * i +1 # j指向下一层的左子孩子
else: # 如果tmp更大,将tmp放到i的位置上
li[i] = tmp # 循环跳出条件一:tmp放到了某一个父节点位置上
break
else: # 循环跳出条件二:j>high ,此时i已经指向了叶子节点,i不存在子节点了
li[i] = tmp # 将tmp放在叶子节点上
def topk(li, k):
heap = li[0:k]
for i in range((k-2)//2, -1, -1): # i从k-2整除2开始倒着遍历到-1
sift(heap, i, k-1)
# 1.建堆
for i in range(k, len(li)-1):
if li[i] > heap[0]:
heap[0] = li[i] # 用li[i]覆盖heap[0]的值
sift(heap, 0, k-1) # 将小根堆做一次调整
# 2.遍历heap
for i in range(k-1, -1, -1): # i从k-1开始一直到零
# i指向当前堆的最后一个元素
heap[0], heap[i] = heap[i], heap[0] # 堆顶(li[0])和最后一个元素(li[i])交换位置
sift(heap, 0, i-1) # i-1是新的high,堆中最后一个元素
# 3.出数
return heap
li = list(range(100))
import random
random.shuffle(li)
print(li)
print(topk(li, 5))
"""
[28, 82, 65, 98, 54, 47, 79, 46, 19, 85, 26, 52, 69, 97, 91, 36, 81, 58, 87, 50, 24, 3, 17, 35, 39, 94, 11, 90, 74, 48, 68, 8, 7, 77, 57, 6, 44, 40, 14, 86, 23, 30, 45, 89, 31, 96, 9, 93, 84, 20, 15, 22, 67, 34, 66, 71, 59, 73, 41, 92, 63, 55, 12, 10, 99, 21, 49, 2, 4, 29, 0, 70, 51, 32, 27, 64, 76, 38, 53, 56, 61, 5, 62, 13, 78, 25, 18, 88, 16, 60, 83, 72, 43, 33, 80, 75, 1, 37, 95, 42]
[99, 98, 97, 96, 95]
"""