数据结构 | 堆

115 阅读6分钟

  • 小顶堆:任意节点的值 < 其子节点的值
  • 大顶堆:任意节点的值 > 其子节点的值

作为一个完全二叉树特例:

  • 最底层节点靠左填充,其他层的节点都被填满
  • 二叉树的根节点称为“堆顶”,底层靠最右的节点称为“堆低”

常用操作

大多数编程语言提供的是优先队列这是一种抽象数据结构定义为具有优先级排序的队列

实际上,堆通常用于实现优先队列

  • 大顶堆相当于元素按从小到大的顺序出队的优先队列

从使用角度来看,可以将“优先队列”和“堆”看作等价的数据结构

方法名描述时间复杂度
push()元素入堆O(logn)O(logn)
pop()堆定元素出堆O(logn)O(logn)
peek()访问堆顶元素(大顶堆最大值/小顶堆最小值)O(1)O(1)
size()获取堆的元素数量O(1)O(1)
isEmpty()判断堆是否为空O(1)O(1)

体验堆可以选择编程语言提高的堆类(或优先队列类)

大小堆的转换:

  • 类似排序的升序降序,使用额外的flag或者修改Comparator

Python的heapq模块默认实现小顶堆,将元素取负后再入堆,这样就可以将大小关系颠倒,实现大顶堆

import heapq
# 初始化小顶堆
min_heap, flag = [], 1
# 初始化大顶堆
max_heap, flag = [], -1

# 元素入堆
heapq.heappush(max_heap, flag * 1)
heapq.heappush(max_heap, flag * 3)
heapq.heappush(max_heap, flag * 2)
heapq.heappush(max_heap, flag * 5)
heapq.heappush(max_heap, flag * 4)

# 获取堆顶元素
peek = flag * max_heap[0]

# 堆顶元素出堆出堆元素形成从大到小的序列
for i in range(len(max_heap)):
    print(flag * heapq.heappop(max_heap))

# 获取堆大小
print(len(max_heap))

# 判断是否为空
print(not max_heap)

# 输入列表并建立堆
min_heap = [1, 3, 2, 5, 4]
heapq.heapify(min_heap)
5
4
3
2
1
0
True

堆的实现

例子实现大顶堆,实现小顶堆只需要所有大小逻辑逆转即可

堆的存储与表示

由于堆作为完全二叉树的特例,而完全二叉树又十分适合使用数组表示,所以我们可以采用数组存储堆

  • 数组表示二叉树
  • 元素代表节点值
  • 索引代表节点再二叉树中的位置
  • 节点指针通过索引映射公式实现
# 索引映射公式
def left(self, i):
    return 2 * i + 1
def right(self, i):
    return 2 * i + 2
def parent(self, i):
    return (i - 1) // 2

访问堆顶元素的实现

堆顶元素即使二叉树的根节点,列表的首个元素

# 访问堆顶元素
def peek(self):
    return self.max_heap[0]

元素入堆

添加元素时,先将元素添加至堆底,之后再与堆中其他元素比较大小

添加了新值,由于值可能大于堆中其他元素,堆成立条件被破坏,需要修复插入节点到根节点路径上的各个节点,这个操作叫堆化

  • 从入堆节点开始,从底至顶执行堆化
  • 比较插入节点与父节点的值,若插入节点更大,交换他们,否者结束
  • 重复执行第二步,修复堆中各个节点,直到超过根节点和遇到无需交换节点时结束

若总节点数为n,则树的高度为lognlogn。所以堆化操作最多循环O(logn)O(logn)次,元素入堆的时间复杂度为O(logn)O(logn)

def push(self, val):
    """ 元素入堆"""
    # 添加节点
    self.max_heap.append(val)
    # 堆化
    self.sift_up(self.size() - 1)

def sift_up(self, i):
    """从节点i开始,从底至顶堆化"""
    while True:
        # 获取节点i的父节点
        p = self.parent(i)
        # 但超过根节点或无需交换时结束堆化
        if p < 0 or self.max_heap[i] <= self.max_heap[p]:
            break
        # 交换两节点
        self.swap(i, p)
        # 循环堆化
        i = p

堆顶元素出堆

堆顶元素是二叉树的根节点, 即为列表首元素,若直接删除首元素,二叉树中所有节点都会发生变化, 后续堆化修复会十分困难,所以采用一下步骤:

  • 交换堆顶元素与堆底元素(交换根节点与最右叶节点)
  • 交换完成后,将堆底从列表中删除(实际上删除的是原来堆顶元素)
  • 从根节点开始,从顶至底执行堆化
    • 将根节点的值与其两个子节点值比较,将最大的子节点与根节点交换
    • 循环该步骤,直到超过叶节点或无需交换的节点时结束
def pop(self):
    """元素出堆"""
    if self.is_empty():
        raise IndexError('堆为空')
    # 交换根节点与最右叶节点
    self.swap(0, size() - 1)
    # 删除节点
    val = self.max_heap.pop()
    # 从顶至底堆化
    self.sift.down(heap, 0)
    # 返回堆顶元素
    return val

def sift_down(self, i):
    """从节点i开始,从顶至底堆化"""
    while True:
        l, r, ma = self.left(i), self.right(i), i
        if l < self.size() and self.max_heap[l] > self.max_heap[ma]:
            ma = l
        if r < self.size() and self.max_heap[r] > self.max_heap[ma]:
            ma = r
        # 若节点i最大或者索引越界则无需继续堆化
        if ma == i:
            break
        # 交换两节点
        self.swap(i, ma)
        # 循环向下堆化
        i = ma

常见应用

  • 优先队列:通常作为优先队列的首选数据结构,入队和出队时间复杂度为O(logn)O(logn)建堆操作为O(n)O(n)这些操作都十分高效
  • 堆排序建立一个堆,不断的出堆操作,得到有序的数据
  • 获取最大的k个元素:经典算法题,典型应用,比如获取热度前十的XXX
class MyHeap:
    def __init__(self):
        self.max_heap = []

    # 索引映射公式
    def left(self, i):
        return 2 * i + 1

    def right(self, i):
        return 2 * i + 2

    def parent(self, i):
        return (i - 1) // 2

    def size(self):
        return len(self.max_heap)

    def is_empty(self):
        return len(self.max_heap) == 0

    # Swap method
    def swap(self, i, j):
        self.max_heap[i], self.max_heap[j] = self.max_heap[j], self.max_heap[i]

    # 访问堆顶元素
    def peek(self):
        if self.is_empty():
            raise IndexError('堆为空')
        return self.max_heap[0]

    def push(self, val):
        """元素入堆"""
        # 添加节点
        self.max_heap.append(val)
        # 堆化
        self.sift_up(self.size() - 1)

    def sift_up(self, i):
        """从节点i开始,从底至顶堆化"""
        while i > 0:
            # 获取节点i的父节点
            p = self.parent(i)
            # 但超过根节点或无需交换时结束堆化
            if self.max_heap[i] <= self.max_heap[p]:
                break
            # 交换两节点
            self.swap(i, p)
            # 循环堆化
            i = p

    def pop(self):
        """元素出堆"""
        if self.is_empty():
            raise IndexError('堆为空')
        # 交换根节点与最右叶节点
        self.swap(0, self.size() - 1)
        # 删除节点
        val = self.max_heap.pop()
        # 从顶至底堆化
        self.sift_down(0)
        # 返回堆顶元素
        return val

    def sift_down(self, i):
        """从节点i开始,从顶至底堆化"""
        while True:
            l, r = self.left(i), self.right(i)
            ma = i
            if l < self.size() and self.max_heap[l] > self.max_heap[ma]:
                ma = l
            if r < self.size() and self.max_heap[r] > self.max_heap[ma]:
                ma = r
            # 若节点i最大或者索引越界则无需继续堆化
            if ma == i:
                break
            # 交换两节点
            self.swap(i, ma)
            # 循环向下堆化
            i = ma

heap = MyHeap()
heap.push(15)
heap.push(10)
heap.push(20)
heap.push(17)

print(heap.peek())  # 20

print(heap.pop())   # 20
print(heap.pop())   # 17
print(heap.pop())   # 15
print(heap.pop())   # 10

# 堆空异常
# print(heap.pop())

# 测试堆
elements = [3, 1, 6, 5, 2, 4]
for elem in elements:
    heap.push(elem)

# 堆应该维护max-heap属性
print([heap.pop() for _ in range(heap.size())])  # [6, 5, 4, 3, 2, 1]
20
20
17
15
10
[6, 5, 4, 3, 2, 1]