1 基本概念
1.1 定义
堆是一种树状数据结构,它满足如下性质:
- 堆序性:任一节点值均小于(或大于)它的所有后代节点值,最小值节点(或最大值节点)在堆的根上。
- 结构性:堆总是一棵完全树,即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。
1 11
/ \ / \
2 3 9 10
/ \ / \ / \ / \
4 5 6 7 5 6 7 8
/ \ / \ / \ / \
8 9 10 11 1 2 3 4
1.2 小根堆与大根堆
- 小根堆:节点值小于后代节点值的堆,也叫最小堆
- 大根堆:节点值大于后代节点值的堆,也叫最大堆
1.3 二叉堆
二叉堆是一种特殊的堆,即每个节点的子节点不超过2个。
1.4 堆的存储
一般使用线性数据结构(如数组)存储堆:
- 根节点存储在第
0
个位置 - 节点
i
的左孩子存储在2*i+1
的位置 - 节点
i
的右孩子存储在2*i+2
的位置
2 堆的常用操作
2.1 初始化
#define HEAP_MAX_SIZE 1000
typedef struct {
int data[HEAP_MAX_SIZE];
int size;
int min; // 1: min heap, 0: max heap
} Heap;
void InitHeap(Heap *heap, int min)
{
if (heap == NULL)
return;
memset(heap, 0, sizeof(*heap));
heap->min = min;
}
其中的min
取值1表示初始化一个小根堆,取值0表示初始化一个大根堆。
2.2 添加元素
添加元素时,新元素被添加到数组末尾,但是添加元素后,堆的性质可能会被破坏,需要向上调整
堆结果。
70
/ \
60 12
/ \ / \
40 30 8 10
例如,要在如上大根堆内插入元素65
,首先在数组尾部插入新元素
70
/ \
60 12
/ \ / \
40 30 8 10
/
65
此时堆的结构被破坏,需要从下往上进行调整,首先用65与其父节点比较,65比40大,因此交换二者位置:
70
/ \
60 12
/ \ / \
65 30 8 10
/
40
继续用65与其父节点比较,65大于60,继续交换
70
/ \
65 12
/ \ / \
60 30 8 10
/
40
继续比较,由于65小于父节点70,已经满足大根堆的性质,因此调整结束。
代码:
int AdjustUp(Heap *heap, int idx)
{
if (heap == NULL || idx >= heap->size)
return -1;
while (idx > 0) {
int father = (idx - 1) / 2;
if (!ReversedWithFather(heap, idx))
break;
Swap(heap, idx, father);
idx = father;
}
return 0;
}
int AddHeap(Heap *heap, int val)
{
if (IsFull(heap)) {
return -1;
}
heap->data[heap->size] = val;
return AdjustUp(heap, heap->size++);
}
其中的ReversedWithFather
定义如下,用于判断指定节点是否大于父节点(小根堆),或者小于(大根堆)父节点,即节点与其父节点的值不满足堆序性。
int ReversedWithFather(Heap *heap, int idx)
{
if (heap == NULL || idx >= heap->size || idx <= 0)
return 0;
int parent = (idx - 1) / 2;
if (heap->min)
return heap->data[idx] < heap->data[parent];
return heap->data[idx] > heap->data[parent];
}
2.3 删除元素
堆删除元素都是从根节点删除,然后把数组的最后一个元素移动到根节点的位置,并向下调整
堆结构,直至重新符合堆序性。例如,我们要删除如下大根堆的根节点:
70
/ \
65 12
/ \ / \
60 30 8 10
/
40
删除根节点70,并将40移动到根节点:
40
/ \
65 12
/ \ / \
60 30 8 10
此时堆的性质被破坏,我们从根节点开始向下调整,调整规则是:获取待调整节点的两个子节点,并取其中的最大值节点(小根堆是取最小值节点),将其与父节点交换。以上图为例,40的两个子节点为65和12,其中65更大,因此将40与65交换位置:
65
/ \
40 12
/ \ / \
60 30 8 10
此时40成为待调整节点,继续上一过程,即交换40与60:
65
/ \
60 12
/ \ / \
40 30 8 10
此时,待调整节点为40,由于40已经没有子节点,调整结束,调整后的二叉树符合大根堆的性质。
代码:
int AdjustDown(Heap *heap, int idx)
{
if (IsEmpty(heap) || idx < 0 || idx >= heap->size) {
return -1;
}
while (idx < heap->size) {
int left = 2 * idx + 1;
int right = 2 * idx + 2;
int cmpChild = GetCmpChild(heap, idx);
if (cmpChild < 0)
break;
if (!ReversedWithFather(heap, cmpChild))
break;
Swap(heap, idx, cmpChild);
idx = cmpChild;
}
return 0;
}
int DelHeap(Heap *heap, int *val)
{
if (IsEmpty(heap)) {
return -1;
}
*val = heap->data[0];
heap->data[0] = heap->data[--heap->size];
return AdjustDown(heap, 0);
}
其中的GetCmpChild
由于获取待交换的子节点(大根堆是值更大的子节点,小根堆则是值更小的子节点)。
int GetCmpChild(Heap *heap, int idx)
{
if (heap == NULL || idx < 0 || idx >= heap->size)
return -1;
int left = 2 * idx + 1;
int right = 2 * idx + 2;
if (left >= heap->size)
return -1;
if (right >= heap->size)
return left;
if (heap->min)
return heap->data[left] < heap->data[right] ? left : right;
return heap->data[left] > heap->data[right] ? left : right;
}
2.4 Go语言实现
go语言的container/heap
包实现了堆,定义如下:
type Interface interface {
sort.Interface
Push(x interface{}) // add x as element Len()
Pop() interface{} // remove and return element Len() - 1.
}
func Init(h Interface) {
// heapify
n := h.Len()
for i := n/2 - 1; i >= 0; i-- {
down(h, i, n)
}
}
其中的sort.Interface
定义如下:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
因此用户一共需要实现5个方法:
- Len:获取数据元素个数
- Less:判断两个元素的大小,注意这里是实现大根堆/小根堆的决定条件
- Swap:交换两个元素
- Push:在线性存储结构的最后添加一个元素
- Pop:从线性存储结构内删除最后一个元素
我们再看一下添加元素和删除元素的操作:
func Push(h Interface, x interface{}) {
h.Push(x)
up(h, h.Len()-1)
}
func Pop(h Interface) interface{} {
n := h.Len() - 1
h.Swap(0, n)
down(h, 0, n)
return h.Pop()
}
func up(h Interface, j int) {
for {
i := (j - 1) / 2 // parent
if i == j || !h.Less(j, i) {
break
}
h.Swap(i, j)
j = i
}
}
func down(h Interface, i0, n int) bool {
i := i0
for {
j1 := 2*i + 1
if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
break
}
j := j1 // left child
if j2 := j1 + 1; j2 < n && h.Less(j2, j1) {
j = j2 // = 2*i + 2 // right child
}
if !h.Less(j, i) {
break
}
h.Swap(i, j)
i = j
}
return i > i0
}
注意,这里的Push/Pop是堆的插入/删除元素的操作,其中包含了堆结构的调整。用户需要实现的仅仅是添加/删除最后一个元素
的操作。
3 LeetCode真题
3.1 最后一块石头的重量
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x
和y
,且x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回0
。
示例:
输入:[2,7,4,1,8,1]
输出:1
解释:
先选出 7 和 8,得到 1,所以数组转换为 [2,4,1,1,1],
再选出 2 和 4,得到 2,所以数组转换为 [2,1,1,1],
接着是 2 和 1,得到 1,所以数组转换为 [1,1,1],
最后选出 1 和 1,得到 0,最终数组转换为 [1],这就是最后剩下那块石头的重量。
思路:
- 将所有石头构建为一个大根堆
- 每次删除两个元素(最大的两个石头),并比较它们的大小
- 如果相等,则继续下一轮
- 否则,将二者差值的绝对值重新添加到大根堆
- 直到堆内元素都被删除,或仅剩下一个元素为止
int lastStoneWeight(int* stones, int stonesSize){
Heap maxHeap;
if (ConstructHeap(stones, stonesSize, 0, &maxHeap) < 0)
return 0;
while (maxHeap.size > 1) {
int stone1, stone2;
DelHeap(&maxHeap, &stone1);
DelHeap(&maxHeap, &stone2);
if (stone1 == stone2)
continue;
int stone = stone1 - stone2;
if (stone < 0)
stone = -stone;
AddHeap(&maxHeap, stone);
}
if (maxHeap.size == 0)
return 0;
int stone;
DelHeap(&maxHeap, &stone);
return stone;
}
3.2 合并K个排序链表
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:
输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6
思路:
使用go语言实现一个堆:
type ListNodeHeap struct {
nodes []*ListNode
}
func (h *ListNodeHeap) Len() int {
return len(h.nodes)
}
func (h *ListNodeHeap) Less(i, j int) bool {
return h.nodes[i].Val < h.nodes[j].Val
}
func (h *ListNodeHeap) Swap(i, j int) {
h.nodes[i], h.nodes[j] = h.nodes[j], h.nodes[i]
}
func (h *ListNodeHeap) Push(x interface{}) {
h.nodes = append(h.nodes, x.(*ListNode))
}
func (h *ListNodeHeap) Pop() interface{} {
old := h.nodes
n := len(old)
x := old[n-1]
h.nodes = old[0 : n-1]
return x
}
- 首先将每个链表的第一个节点加入一个小根堆
- 从堆内删除一个最小节点,并将其后继节点加入堆,由于链表本身是有序的,因此如果一个节点是最小节点,则它的后继节点是较小节点的概率是比较高的
- 重复如上操作,直到堆的元素均被删除
func mergeKLists(lists []*ListNode) *ListNode {
if lists == nil || len(lists) <= 0 {
return nil
}
listNodeHeap := &ListNodeHeap{}
heap.Init(listNodeHeap)
k := len(lists)
for i := 0; i < k; i++ {
if lists[i] == nil {
continue
}
heap.Push(listNodeHeap, lists[i])
}
res := ListNode{}
p := &res
for len(listNodeHeap.nodes) > 0 {
node := heap.Pop(listNodeHeap).(*ListNode)
p.Next = &ListNode{node.Val, nil}
p = p.Next
node = node.Next
if node != nil {
heap.Push(listNodeHeap, node)
}
}
return res.Next
}
完整代码:堆算法