堆算法

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 最后一块石头的重量

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 xy,且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
}

完整代码:堆算法

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值