堆与堆排序
堆的定义
- 堆是一个完全二叉树:除了最后一层外,其他层几点必须是满的,且最后一层节点从左向右逐步填充
- 堆中每一个父节点的值必须大于等于(或小于等于)其两子树节点的值:大于等于是大顶堆,小于等于是小顶堆
堆的实现
完全二叉树使用使用数组来存储,所以堆也是基于数组来实现的,堆最重要的是下面两个基础操作:
- 新增(插入)新元素
- 删除堆顶元素
下面详细说明下其操作方法:
新增元素
新增元素的算法如下:
- 1.将新增的元素放到堆的最后,也就是数组的最后,但此时可能导致不符合堆的特性了,所以需要第二步的自下向上堆化
- 2.堆化,这里采用自下向上的堆化,即沿着节点的路径,与其父节点进行比较,如果不满足父子节点关系则交换,再继续比较;无法交换则堆化完成
下图是一个大顶堆的堆化过程,首先将新元素22插入尾部,然后进行堆化
删除堆顶元素
删除堆顶元素的算法如下:
- 1.交换堆顶元素和堆尾元素的值,这样堆就减小,堆顶元素也就删除了,但此时可能导致不符合堆的特性,所有需要第二步的从上向下的堆化
- 2.堆化,这里采用自上向下的堆化,沿着节点的路径,与其子节点进行比较,如果不满足父子节点关系则交换,重复这个过程,知道满足父子节点关系为止
下图是一个大顶堆的删除过程过程
大顶堆的实现
这里使用Python3简单模拟实现了一个大顶堆,小顶堆类似,也就是比较不同,这里就不重复实现了
class MaxHeap:
def __init__(self, size: int):
"""
堆的初始化,设置堆的大小
:param size: 堆的大小
"""
# 这里有个小技巧,数组下标不从0开始,而从1开始,这样父子节点的下标获取和计算方便,所有这里数组空间为size+1
self.data = [None] * (size + 1)
self.size = size
self.used = 0
print("init heap:", self.data)
def push(self, value: int) -> None:
"""
堆的插入操作
:return:
"""
if self.used == self.size:
self.pop()
self.used += 1
print("insert:", self.used, self.data, end="==>")
self.data[self.used] = value
self._shitUp()
print(self.data)
def pop(self) -> None:
"""
堆的删除操作
:return:
"""
if self.used == 0:
return
self.data[1] = self.data[self.used]
print("pop:", self.data, end="==>")
self.used -= 1
self._shitDown()
print(self.data)
def _shitUp(self):
"""
自下向上的堆化
与父节点进行比较,大于则交换
:return:
"""
child = self.used
while child // 2 > 0 and self.data[child] > self.data[child//2]:
self.data[child], self.data[child//2] = self.data[child//2], self.data[child]
child = child // 2
def _shitDown(self):
"""
自上向下的堆化
这里是与最大值的子节点进行交换,则用了一个maxPos保存最大值的位置
如果是当前父节点的位置,则堆化结束
不是则交换父子节点,继续循环
:return:
"""
parent = 1
while True:
maxPos = parent
if parent * 2 <= self.used and self.data[parent*2] > self.data[parent]:
maxPos = parent * 2
if parent * 2 + 1 <= self.used and self.data[parent*2+1] > self.data[maxPos]:
maxPos = parent*2+1
if maxPos == parent:
break
self.data[maxPos], self.data[parent] = self.data[parent], self.data[maxPos]
parent = maxPos
def toList(self):
return self.data
if __name__ == "__main__":
maxHeap = MaxHeap(2)
maxHeap.push(1)
maxHeap.push(2)
maxHeap.push(3)
print(maxHeap.toList())
init heap: [None, None, None]
insert: 1 [None, 1, None]
insert: 2 [None, 2, 1]
pop: [None, 1, 1]
insert: 2 [None, 3, 1]
[None, 3, 1]
LeetCode 347:前 K 个高频元素的使用自写堆
如何需要去跑测试的话,需要把代码里面的print打印的去掉,不然会超出输出限制。
LeetCode里面有一个是使用堆来解的,答案里面有是使用语言实现,这里就自己实现一个小顶堆来尝试。小顶堆做的唯一修改就是加入一个字典,进行比较时进行转换再比较。大致的代码和思路在下面的代码中。
"""
347. 前 K 个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
你可以按任意顺序返回答案。
解题思路:
使用小顶堆实现,因为要返回前最大K个数,小顶堆就可以保存着K个数,而堆顶是最小数,剩下的都是大于它的
1.先使用hashmap统计保存数字的出现次数
2.使用k个数据初始化小顶堆
3.比堆顶大的就插入,小于就说明前K大的数没它的份
统计N,遍历N,大顶堆操作logK,则最大时间复杂度O(N)
自己实现个堆来尝试尝试
内置的跑了60ms,自写的56,感觉差不多
"""
import collections
from typing import List
import heapq
class SolutionP:
def topKFrequent(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: List[int]
"""
count = collections.Counter(nums)
return heapq.nlargest(k, count.keys(), key=count.get)
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
count = {}
for num in nums:
count[num] = count.get(num, 0) + 1
print(count)
# 使用K个数初始化堆
minHeap = MinHeap(k, count)
keys = list(count.keys())
print(keys)
for i in range(0, k):
minHeap.push(keys[i])
# 大于堆顶才插入,小于的就说明前K个数没有它的份
for i in range(k, len(keys)):
if count[keys[i]] > count[minHeap.getMinest()]:
minHeap.push(keys[i])
return minHeap.toList()
class MinHeap:
def __init__(self, size: int, myDict: dict):
"""
堆的初始化,设置堆的大小
:param size: 堆的大小
"""
# 这里有个小技巧,数组下标不从0开始,而从1开始,这样父子节点的下标获取和计算方便,所有这里数组空间为size+1
self.data = [None] * (size + 1)
self.size = size
self.used = 0
self.myDict = myDict
print("init heap:", self.data)
def push(self, value: int) -> None:
"""
堆的插入操作
:return:
"""
print("insert value:", value, end=" ")
if self.used == self.size:
self.pop()
self.used += 1
print("insert:", self.used, value, self.data, end="==>")
self.data[self.used] = value
self._shitUp()
print(self.data)
def pop(self) -> None:
"""
堆的删除操作
:return:
"""
if self.used == 0:
return
self.data[1] = self.data[self.used]
print("pop:", self.data, end="==>")
self.used -= 1
self._shitDown()
print(self.data)
def _shitUp(self):
"""
自下向上的堆化
与父节点进行比较,大于则交换
:return:
"""
child = self.used
while child // 2 > 0 and self.myDict[self.data[child]] < self.myDict[self.data[child // 2]]:
self.data[child], self.data[child // 2] = self.data[child // 2], self.data[child]
child = child // 2
def _shitDown(self):
"""
自上向下的堆化
这里是与最大值的子节点进行交换,则用了一个maxPos保存最大值的位置
如果是当前父节点的位置,则堆化结束
不是则交换父子节点,继续循环
:return:
"""
parent = 1
while True:
minPos = parent
if parent * 2 <= self.used and self.myDict[self.data[parent * 2]] < self.myDict[self.data[parent]]:
minPos = parent * 2
if parent * 2 + 1 <= self.used and self.myDict[self.data[parent * 2 + 1]] < self.myDict[self.data[minPos]]:
minPos = parent * 2 + 1
if minPos == parent:
break
self.data[minPos], self.data[parent] = self.data[parent], self.data[minPos]
parent = minPos
def getMinest(self) -> int:
"""
返回堆顶元素值
:return:
"""
return self.data[1]
def toList(self):
return self.data[1:]
if __name__ == "__main__":
s = Solution()
print(s.topKFrequent(nums=[1, 1, 1, 2, 2, 3], k=2))
# [-3,-4,0,1,4,9]
print(s.topKFrequent(nums=[6, 0, 1, 4, 9, 7, -3, 1, -4, -8, 4, -7, -3, 3, 2, -3, 9, 5, -4, 0], k=6))