算法——堆和堆排序介绍
一、什么是堆?
堆:一种特殊的完全二叉树结构。
大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大;
小根堆:一棵完全二叉树,满足任一节点都比其他孩子节点小。
二、堆的向下调整性质
假设:节点的左右子树都是堆,但自身不是堆。
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] """