二叉堆与斐波那契堆:优先队列实现的两种选择

493 阅读8分钟

二叉堆与斐波那契堆:优先队列实现的两种选择

在计算机科学中,优先队列是一种常见的数据结构,它支持按优先级高低访问元素。二叉堆和斐波那契堆是两种经典的优先队列实现方式,每种方式在不同的场景下都有其独特的优势和应用。

二叉堆

image-20240716010550260

二叉堆是一种特殊的二叉树,满足以下两个特性:

  1. 结构特性:二叉堆是一棵完全二叉树(即除了最后一层外,每一层都是满的,且最后一层从左到右填充)。
  2. 堆序性质:对于大顶堆(或小顶堆),任意节点的值都大于等于(或小于等于)其子节点的值。

实现二叉堆

我们可以使用数组来实现二叉堆,其中父节点和子节点之间的关系通过数组索引可以方便地计算得出。下面是一个简单的示例代码:

class BinaryHeap:
    def __init__(self):
        self.heap = []
​
    def parent(self, i):
        return (i - 1) // 2
​
    def left_child(self, i):
        return 2 * i + 1
​
    def right_child(self, i):
        return 2 * i + 2
​
    def insert(self, val):
        self.heap.append(val)
        self.heapify_up(len(self.heap) - 1)
​
    def heapify_up(self, i):
        while i > 0 and self.heap[i] > self.heap[self.parent(i)]:
            self.heap[i], self.heap[self.parent(i)] = self.heap[self.parent(i)], self.heap[i]
            i = self.parent(i)
​
    def extract_max(self):
        if len(self.heap) == 0:
            return None
        max_val = self.heap[0]
        self.heap[0] = self.heap[-1]
        del self.heap[-1]
        self.heapify_down(0)
        return max_val
​
    def heapify_down(self, i):
        while True:
            max_index = i
            left = self.left_child(i)
            right = self.right_child(i)
            if left < len(self.heap) and self.heap[left] > self.heap[max_index]:
                max_index = left
            if right < len(self.heap) and self.heap[right] > self.heap[max_index]:
                max_index = right
            if max_index == i:
                break
            self.heap[i], self.heap[max_index] = self.heap[max_index], self.heap[i]
            i = max_index

斐波那契堆

斐波那契堆是一种多叉树结构,具有以下特点:

  • 每个节点可能有多个子节点。
  • 通过合并树的方式来支持高效的插入和删除操作,使得其在某些操作上比二叉堆更加高效。

image-20240716010626235

实现斐波那契堆

斐波那契堆的实现比较复杂,包括节点合并、最小节点移除等操作。下面是一个简化的示例代码:

class FibonacciHeapNode:
    def __init__(self, val):
        self.val = val
        self.degree = 0
        self.child = None
        self.parent = None
        self.marked = False
        self.left = self
        self.right = self
​
class FibonacciHeap:
    def __init__(self):
        self.min_node = None
        self.count = 0
​
    def insert(self, val):
        new_node = FibonacciHeapNode(val)
        if self.min_node is None:
            self.min_node = new_node
        else:
            new_node.left = self.min_node
            new_node.right = self.min_node.right
            self.min_node.right.left = new_node
            self.min_node.right = new_node
            if val < self.min_node.val:
                self.min_node = new_node
        self.count += 1
        return new_node
​
    def extract_min(self):
        z = self.min_node
        if z is not None:
            if z.child is not None:
                children = [x for x in z.child]
                for child in children:
                    self.min_node.right.left = child
                    child.right = self.min_node.right
                    child.left = self.min_node
                    self.min_node.right = child
                    if child.val < self.min_node.val:
                        self.min_node = child
            z.left.right = z.right
            z.right.left = z.left
            if z == z.right:
                self.min_node = None
            else:
                self.min_node = z.right
                self.consolidate()
            self.count -= 1
        return z

操作复杂度比较

在实际应用中,选择二叉堆还是斐波那契堆取决于对操作时间复杂度的需求。以下是各种操作的平均时间复杂度比较:

操作二叉堆斐波那契堆
插入O(log n)O(1)
提取最大/最小O(log n)O(log n) amortized
删除节点O(log n)O(log n) amortized
合并堆O(n)O(1)

二叉堆的特点和适用场景

image-20240716010652598

  • 结构简单:由于二叉堆是一棵完全二叉树,实现相对简单,适合实现简单的优先队列。
  • 常见用途:在需要快速插入和提取最大(或最小)值的场景下,二叉堆表现良好,如任务调度、Dijkstra算法等。

斐波那契堆的特点和适用场景

  • 高效的插入和合并操作:斐波那契堆通过延迟合并树来实现高效的插入和删除操作,对于频繁合并和分离堆的场景更为适用。
  • 复杂实现:斐波那契堆的实现复杂,需要额外的指针结构来支持多个子节点,但在某些应用中能够显著提升性能。

斐波那契堆的实现代码

下面给出斐波那契堆的完整实现代码,包括节点类 FibonacciHeapNode 和堆类 FibonacciHeap 的实现。

class FibonacciHeapNode:
    def __init__(self, val):
        self.val = val
        self.degree = 0
        self.child = None
        self.parent = None
        self.marked = False
        self.left = self
        self.right = self
​
class FibonacciHeap:
    def __init__(self):
        self.min_node = None
        self.count = 0
​
    def insert(self, val):
        new_node = FibonacciHeapNode(val)
        if self.min_node is None:
            self.min_node = new_node
        else:
            new_node.left = self.min_node
            new_node.right = self.min_node.right
            self.min_node.right.left = new_node
            self.min_node.right = new_node
            if val < self.min_node.val:
                self.min_node = new_node
        self.count += 1
        return new_node
​
    def extract_min(self):
        z = self.min_node
        if z is not None:
            if z.child is not None:
                children = [x for x in z.child]
                for child in children:
                    self.min_node.right.left = child
                    child.right = self.min_node.right
                    child.left = self.min_node
                    self.min_node.right = child
                    if child.val < self.min_node.val:
                        self.min_node = child
            z.left.right = z.right
            z.right.left = z.left
            if z == z.right:
                self.min_node = None
            else:
                self.min_node = z.right
                self.consolidate()
            self.count -= 1
        return z
​
    def consolidate(self):
        degree_table = {}
        to_visit = [x for x in self.min_node]
        for w in to_visit:
            x = w
            d = x.degree
            while d in degree_table:
                y = degree_table[d]
                if x.val > y.val:
                    x, y = y, x
                self.fib_heap_link(y, x)
                degree_table.pop(d)
                d += 1
            degree_table[d] = x
        self.min_node = None
        for d, w in degree_table.items():
            if self.min_node is None:
                self.min_node = w
            else:
                w.left.right = w.right
                w.right.left = w.left
                w.left = self.min_node
                w.right = self.min_node.right
                self.min_node.right.left = w
                self.min_node.right = w
                if w.val < self.min_node.val:
                    self.min_node = w
​
    def fib_heap_link(self, y, x):
        y.left.right = y.right
        y.right.left = y.left
        y.parent = x
        if x.child is None:
            x.child = y
            y.right = y
            y.left = y
        else:
            y.left = x.child
            y.right = x.child.right
            x.child.right.left = y
            x.child.right = y
        x.degree += 1
        y.marked = False# 示例用法
if __name__ == "__main__":
    fib_heap = FibonacciHeap()
    fib_heap.insert(5)
    fib_heap.insert(10)
    fib_heap.insert(3)
    print(fib_heap.extract_min().val)  # 输出:3
    print(fib_heap.extract_min().val)  # 输出:5

斐波那契堆的使用示例

上述代码展示了如何使用斐波那契堆实现插入和提取最小节点的操作。斐波那契堆通过延迟合并和复杂的节点链接方式,在某些操作上表现出更好的性能。

image-20240716010711312

实际应用举例

  1. 任务调度:二叉堆适合用于任务调度,通过维护任务的优先级可以快速提取最高优先级的任务。
  2. 最短路径算法:斐波那契堆在Dijkstra算法中的应用,通过高效的提取最小节点操作,加速了路径搜索过程。

斐波那契堆的额外特性

除了基本的优先队列操作外,斐波那契堆还具有以下额外的特性:

  1. 懒惰合并:斐波那契堆延迟合并操作,即在删除最小节点后不立即合并树,而是通过将树标记为“被切断”来稍后进行合并,从而提高了性能。
  2. 减小操作:斐波那契堆支持将某个节点的值减小后重新调整堆结构,这在某些算法中是非常有用的,如最小生成树算法中的Prim算法。

实现注意点

image-20240716010720798

斐波那契堆的实现需要考虑到节点之间复杂的指针关系和合并操作。下面是一些实现斐波那契堆时需要注意的关键点:

  • 节点的度数管理:每个节点需要维护其子节点的度数,并根据需要合并具有相同度数的树。
  • 最小节点的维护:斐波那契堆通过一个指针来维护堆中最小节点,确保在常数时间内获取最小节点。

二叉堆的稳定性和易用性

尽管斐波那契堆在某些操作上性能优越,但二叉堆因其稳定的性能和相对简单的实现而在实际中更为常见和易用。在不需要频繁合并堆或减小节点值的情况下,二叉堆提供了一个稳定且高效的选择。

应用案例

  1. 操作系统调度:二叉堆常用于操作系统中的进程调度,通过维护进程的优先级来决定下一个执行的任务。
  2. 事件驱动系统:斐波那契堆在事件驱动系统中的定时器管理中,通过快速的插入和删除操作来管理定时事件。

总结

二叉堆和斐波那契堆是两种常见的优先队列实现方式,每种方式都有其独特的优势和适用场景。以下是它们的主要特点和适用情况的总结:

二叉堆

  • 特点

    • 结构简单,基于完全二叉树。
    • 插入和删除操作的时间复杂度为 O(log n)。
    • 可以实现最大堆和最小堆。
  • 适用场景

    • 需要简单且稳定的优先队列实现。
    • 在任务调度、Dijkstra算法等需要快速插入和提取最大(或最小)值的场景中表现良好。

斐波那契堆

  • 特点

    • 结构复杂,支持延迟合并和减小操作。
    • 插入和删除操作的平摊时间复杂度较低,为 O(1) 和 O(log n) amortized。
    • 对于频繁合并和分离堆的场景具有较好的性能优势。
  • 适用场景

    • 需要高效的插入、删除和合并操作的应用,如定时器管理、Prim算法等。
    • 在需要动态调整优先级和支持减小操作的算法中表现出色。

image-20240716010741929

性能比较

  • 插入操作:二叉堆的时间复杂度为 O(log n),斐波那契堆的时间复杂度为 O(1)。
  • 删除操作:二叉堆和斐波那契堆的平摊时间复杂度均为 O(log n) 或 O(log n) amortized。
  • 空间复杂度:二叉堆和斐波那契堆的空间复杂度均为 O(n)。

结论

根据具体的应用场景和性能需求,选择合适的优先队列实现方式可以显著提升算法的效率和性能。二叉堆适合简单且稳定的场景,而斐波那契堆则在需要高效动态调整和频繁合并堆的应用中更为合适。

通过深入理解和实践这两种堆的实现,可以更好地应用于解决实际问题中的优先队列需求,从而优化算法的性能和效率。

希望本文能够帮助你更清晰地理解和选择二叉堆和斐波那契堆在优先队列实现中的应用和优化策略。