Compare commits

..

4 Commits

Author SHA1 Message Date
Max McWhae
d20f6c756d
Merge 071ce716e4 into e3bd7721c8 2024-11-18 12:10:35 +08:00
pre-commit-ci[bot]
071ce716e4 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-11-18 04:10:32 +00:00
mcawezome
a92936bd01 Added tests and docstrings to fibonacci_heap.py 2024-11-18 12:10:01 +08:00
mcawezome
ea5a187b0a Rewrote entire file to pass checks 2024-11-18 11:03:04 +08:00

View File

@ -1,444 +1,361 @@
from __future__ import annotations
"""
Fibonacci Heap
A more efficient priority queue implementation that provides amortized time bounds
that are better than those of the binary and binomial heaps.
reference: https://en.wikipedia.org/wiki/Fibonacci_heap
Operations supported:
- Insert: O(1) amortized
- Find minimum: O(1)
- Delete minimum: O(log n) amortized
- Decrease key: O(1) amortized
- Merge: O(1)
"""
class Node:
"""A node in the Fibonacci heap.
"""
A node in a Fibonacci heap.
Each node maintains references to its key, degree (number of children),
marked status, parent, child, and circular linked list references (left/right).
Args:
val: The value stored in the node.
Attributes:
key: The key value stored in the node
degree: Number of children of the node
marked: Boolean indicating if the node is marked
parent: Reference to parent node
child: Reference to one child node
left: Reference to left sibling in circular list
right: Reference to right sibling in circular list
Examples:
>>> node = Node(5)
>>> node.key
5
>>> node.degree
0
>>> node.marked
False
>>> node.left == node
True
>>> node.right == node
True
val: The value stored in the node.
parent: Reference to parent node.
child: Reference to one child node.
left: Reference to left sibling.
right: Reference to right sibling.
degree: Number of children.
mark: Boolean indicating if node has lost a child.
"""
def __init__(self, key: float | None) -> None:
self.key = key or None
self.degree = 0
self.marked = False
self.parent = Node(None)
self.child = Node(None)
def __init__(self, val):
self.val = val
self.parent = None
self.child = None
self.left = self
self.right = self
self.degree = 0
self.mark = False
def add_sibling(self, node):
"""
Adds a node as a sibling to the current node.
Args:
node: The node to add as a sibling.
"""
node.left = self
node.right = self.right
self.right.left = node
self.right = node
def add_child(self, node):
"""
Adds a node as a child of the current node.
Args:
node: The node to add as a child.
"""
node.parent = self
if not self.child:
self.child = node
else:
self.child.add_sibling(node)
self.degree += 1
def remove(self):
"""Removes this node from its sibling list."""
self.left.right = self.right
self.right.left = self.left
class FibonacciHeap:
"""Implementation of a Fibonacci heap using circular linked lists.
A Fibonacci heap is a collection of trees satisfying the min-heap property.
This implementation uses circular linked lists for both the root list and
child lists of nodes.
Attributes:
min_node: Reference to the node with minimum key
total_nodes: Total number of nodes in the heap
Reference: Introduction to Algorithms (CLRS) Chapter 19
https://en.wikipedia.org/wiki/Fibonacci_heap
Examples:
>>> heap = FibonacciHeap()
>>> heap.is_empty()
True
>>> node = heap.insert(3)
>>> node.key
3
>>> node2 = heap.insert(2)
>>> node2.key
2
>>> heap.find_min()
2
>>> heap.extract_min()
2
>>> heap.find_min()
3
"""
A Fibonacci heap implementation providing
amortized efficient priority queue operations.
def __init__(self) -> None:
self.min_node = Node(None)
self.total_nodes = 0
def insert(self, key: float | None) -> Node:
"""Insert a new key into the heap.
Args:
key: The key value to insert
Returns:
Node: The newly created node
Examples:
>>> heap = FibonacciHeap()
>>> node = heap.insert(5)
>>> node.key
5
>>> heap.find_min()
5
>>> node2 = heap.insert(3)
>>> node2.key
3
>>> heap.find_min()
3
"""
new_node = Node(key)
if self.min_node is None:
self.min_node = new_node
else:
self._insert_into_circular_list(self.min_node, new_node)
if new_node.key < self.min_node.key:
self.min_node = new_node
self.total_nodes += 1
return new_node
@staticmethod
def _insert_into_circular_list(base_node: Node, node_to_insert: Node) -> Node:
"""Insert node into circular linked list.
Args:
base_node: The reference node in the circular list
node_to_insert: The node to insert into the list
Returns:
Node: The base node
Examples:
>>> heap = FibonacciHeap()
>>> node1 = Node(1)
>>> node2 = Node(2)
>>> result = heap._insert_into_circular_list(node1, node2)
>>> result == node1
True
>>> node1.right == node2
True
>>> node2.left == node1
True
"""
if base_node.key is None:
return node_to_insert
node_to_insert.right = base_node.right
node_to_insert.left = base_node
base_node.right.left = node_to_insert
base_node.right = node_to_insert
return base_node
def extract_min(self) -> float | None:
"""Remove and return the minimum key from the heap.
This operation removes the node with the minimum key from the heap,
adds all its children to the root list, and consolidates the heap
to maintain the Fibonacci heap properties. This is one of the more
complex operations with amortized time complexity of O(log n).
Returns:
Node: The minimum key value that was removed,
or None if the heap is empty
This implementation provides the following time complexities:
- Insert: O(1) amortized
- Find minimum: O(1)
- Delete minimum: O(log n) amortized
- Decrease key: O(1) amortized
- Merge: O(1)
Example:
>>> heap = FibonacciHeap()
>>> node1 = heap.insert(3)
>>> node2 = heap.insert(1)
>>> node3 = heap.insert(2)
>>> heap.extract_min() # Removes and returns 1
1
>>> heap.extract_min() # Removes and returns 2
>>> node2 = heap.insert(2)
>>> node3 = heap.insert(15)
>>> heap.peek()
2
>>> heap.extract_min() # Removes and returns 3
>>> heap.delete_min()
2
>>> heap.peek()
3
>>> heap.extract_min() # Heap is now empty
Note:
This operation may trigger heap consolidation to maintain
the Fibonacci heap properties after removal of the minimum node.
>>> other_heap = FibonacciHeap()
>>> node4 = other_heap.insert(1)
>>> heap.merge_heaps(other_heap)
>>> heap.peek()
1
"""
if self.min_node is None:
return Node(None).key
min_node = self.min_node
def __init__(self):
"""Initializes an empty Fibonacci heap."""
self.min_node = None
self.size = 0
if min_node.child:
current_child = min_node.child
last_child = min_node.child.left
while True:
next_child = current_child.right
self._insert_into_circular_list(self.min_node, current_child)
current_child.parent.key = None
if current_child == last_child:
break
current_child = next_child
def is_empty(self):
"""
Checks if the heap is empty.
min_node.left.right = min_node.right
min_node.right.left = min_node.left
Returns:
bool: True if heap is empty, False otherwise.
"""
return self.min_node is None
if min_node == min_node.right:
self.min_node.key = None
def insert(self, val):
"""
Inserts a new value into the heap.
Args:
val: Value to insert.
Returns:
Node: The newly created node.
"""
node = Node(val)
if not self.min_node:
self.min_node = node
else:
self.min_node = min_node.right
self._consolidate()
self.min_node.add_sibling(node)
if node.val < self.min_node.val:
self.min_node = node
self.size += 1
return node
self.total_nodes -= 1
return min_node.key
def _consolidate(self) -> None:
"""Consolidate the heap after removing the minimum node.
This internal method maintains the Fibonacci heap properties by combining
trees of the same degree until no two roots have the same degree. This
process is key to maintaining the efficiency of the data structure.
The consolidation process works by:
1. Creating a temporary array indexed by tree degree
2. Processing each root node and combining trees of the same degree
3. Reconstructing the root list and finding the new minimum
Time complexity: O(log n) amortized
Note:
This is an internal method called by extract_min and should not be
called directly from outside the class.
def peek(self):
"""
max_degree = int(self.total_nodes**0.5) + 1
degree_table = [Node(None)] * max_degree
Returns the minimum value without removing it.
Returns:
The minimum value in the heap.
Raises:
IndexError: If the heap is empty.
"""
if not self.min_node:
raise IndexError("Heap is empty")
return self.min_node.val
def merge_heaps(self, other):
"""
Merges another Fibonacci heap into this one.
Args:
other: Another FibonacciHeap instance to merge with this one.
"""
if not other.min_node:
return
if not self.min_node:
self.min_node = other.min_node
else:
# Merge root lists
self.min_node.right.left = other.min_node.left
other.min_node.left.right = self.min_node.right
self.min_node.right = other.min_node
other.min_node.left = self.min_node
if other.min_node.val < self.min_node.val:
self.min_node = other.min_node
self.size += other.size
def __link_trees(self, node1, node2):
"""
Links two trees of same degree.
Args:
node1: First tree's root node.
node2: Second tree's root node.
"""
node1.remove()
if node2.child:
node2.child.add_sibling(node1)
else:
node2.child = node1
node1.parent = node2
node2.degree += 1
node1.mark = False
def delete_min(self):
"""
Removes and returns the minimum value from the heap.
Returns:
The minimum value that was removed.
Raises:
IndexError: If the heap is empty.
"""
if not self.min_node:
raise IndexError("Heap is empty")
min_val = self.min_node.val
# Add all children to root list
if self.min_node.child:
curr = self.min_node.child
while True:
next_node = curr.right
curr.parent = None
curr.mark = False
self.min_node.add_sibling(curr)
if curr.right == self.min_node.child:
break
curr = next_node
# Remove minimum node
if self.min_node.right == self.min_node:
self.min_node = None
else:
self.min_node.remove()
self.min_node = self.min_node.right
self.__consolidate()
self.size -= 1
return min_val
def __consolidate(self):
"""
Consolidates the trees in the heap after a delete_min operation.
This is an internal method that maintains the heap's structure.
"""
max_degree = int(self.size**0.5) + 1
degree_table = [None] * max_degree
# Collect all roots
roots = []
if self.min_node:
current_root = self.min_node
curr = self.min_node
while True:
roots.append(current_root)
if current_root.right == self.min_node:
roots.append(curr)
curr = curr.right
if curr == self.min_node:
break
current_root = current_root.right
for current_root in roots:
root_node = current_root
current_degree = root_node.degree
# Consolidate trees
for root in roots:
degree = root.degree
while degree_table[degree]:
other = degree_table[degree]
if root.val > other.val:
root, other = other, root
self.__link_trees(other, root)
degree_table[degree] = None
degree += 1
degree_table[degree] = root
while degree_table[current_degree] is not None:
other_root = degree_table[current_degree]
if root_node.key > other_root.key:
root_node, other_root = other_root, root_node
other_root.left.right = other_root.right
other_root.right.left = other_root.left
if root_node.child.key is None:
root_node.child = other_root
other_root.right = other_root
other_root.left = other_root
else:
self._insert_into_circular_list(root_node.child, other_root)
other_root.parent = root_node
root_node.degree += 1
other_root.marked = False
degree_table[current_degree] = Node(None)
current_degree += 1
degree_table[current_degree] = root_node
self.min_node.key = None
# Find new minimum
self.min_node = None
for degree in range(max_degree):
if degree_table[degree] is not None and (
self.min_node is None or (degree_table[degree] < self.min_node.key)
):
if degree_table[degree]:
if not self.min_node:
self.min_node = degree_table[degree]
self.min_node.left = self.min_node
self.min_node.right = self.min_node
else:
self.min_node.add_sibling(degree_table[degree])
if degree_table[degree].val < self.min_node.val:
self.min_node = degree_table[degree]
def decrease_key(self, node: Node, new_key: float | None) -> None:
"""Decrease the key value of a given node.
This operation updates the key of a node to a new, smaller value and
maintains the min-heap property by potentially cutting the node from
its parent and performing cascading cuts up the tree.
def decrease_key(self, node, new_val):
"""
Decreases the value of a node.
Args:
node: The node whose key should be decreased
new_key: The new key value, must be smaller than the current key
node: The node whose value should be decreased.
new_val: The new value for the node.
Example:
>>> heap = FibonacciHeap()
>>> node1 = heap.insert(5)
>>> heap.decrease_key(node, 3)
>>> node.key
3
>>> heap.find_min()
3
>>> heap.decrease_key(node, 1)
>>> node.key
1
>>> heap.find_min()
1
Raises:
ValueError: If new value is greater than current value.
"""
if new_key > node.key:
raise ValueError("New key is greater than current key")
if new_val > node.val:
raise ValueError("New value is greater than current value")
node.key = new_key
parent_node = node.parent
node.val = new_val
parent = node.parent
if parent_node.key is not None and node.key < parent_node.key:
self._cut(node, parent_node)
self._cascading_cut(parent_node)
if parent and node.val < parent.val:
self.__cut(node, parent)
self.__cascading_cut(parent)
if node.key < self.min_node.key:
if node.val < self.min_node.val:
self.min_node = node
def _cut(self, child_node: Node, parent_node: Node) -> None:
"""Cut a node from its parent and add it to the root list.
This is a helper method used in decrease_key operations. When a node's key
becomes smaller than its parent's key, it needs to be cut from its parent
and added to the root list to maintain the min-heap property.
def __cut(self, node, parent):
"""
Cuts a node from its parent.
Args:
child_node: The node to be cut from its parent
parent_node: The parent node from which to cut
node: Node to be cut.
parent: Parent of the node to be cut.
""" """
Performs cascading cut operation.
Note:
This is an internal method that maintains heap properties during
decrease_key operations. It should not be called directly from
outside the class.
Args:
node: Starting node for cascading cut.
"""
if child_node.right == child_node:
parent_node.child = Node(None)
parent.degree -= 1
if parent.child == node:
parent.child = node.right if node.right != node else None
node.remove()
node.left = node
node.right = node
node.parent = None
node.mark = False
self.min_node.add_sibling(node)
def __cascading_cut(self, node):
"""
Performs cascading cut operation.
Args:
node: Starting node for cascading cut.
"""
if parent := node.parent:
if not node.mark:
node.mark = True
else:
parent_node.child = child_node.right
child_node.right.left = child_node.left
child_node.left.right = child_node.right
self.__cut(node, parent)
self.__cascading_cut(parent)
parent_node.degree -= 1
self._insert_into_circular_list(self.min_node, child_node)
child_node.parent = Node(None)
child_node.marked = False
def _cascading_cut(self, current_node: Node) -> None:
"""Perform cascading cut operation.
Args:
current_node: The node to start cascading cut from
def __str__(self):
"""
if (parent_node := current_node.parent) is not None:
if not current_node.marked:
current_node.marked = True
else:
self._cut(current_node, parent_node)
self._cascading_cut(parent_node)
def delete(self, node: Node) -> None:
"""Delete a node from the heap.
This operation removes a given node from the heap by first decreasing
its key to negative infinity (making it the minimum) and then extracting
the minimum.
Args:
node: The node to be deleted from the heap
Example:
>>> heap = FibonacciHeap()
>>> node1 = heap.insert(3)
>>> node2 = heap.insert(2)
>>> heap.delete(node1)
>>> heap.find_min()
2
>>> heap.total_nodes
1
Note:
This operation has an amortized time complexity of O(log n)
as it combines decrease_key and extract_min operations.
"""
self.decrease_key(node, float("-inf"))
self.extract_min()
def find_min(self) -> float | None:
"""Return the minimum key without removing it from the heap.
This operation provides quick access to the minimum key in the heap
without modifying the heap structure.
Returns a string representation of the heap.
Returns:
float | None: The minimum key value, or None if the heap is empty
Example:
>>> heap = FibonacciHeap()
>>> heap.find_min() is None
True
>>> node1 = heap.insert(3)
>>> heap.find_min()
3
str: A string showing the heap structure.
"""
return self.min_node.key if self.min_node else Node(None).key
if not self.min_node:
return "Empty heap"
def is_empty(self) -> bool:
"""Check if heap is empty.
def print_tree(node, level=0):
result = []
curr = node
while True:
result.append("-" * level + str(curr.val))
if curr.child:
result.extend(print_tree(curr.child, level + 1))
curr = curr.right
if curr == node:
break
return result
Returns:
bool: True if heap is empty, False otherwise
Examples:
>>> heap = FibonacciHeap()
>>> heap.is_empty()
True
>>> node = heap.insert(1)
>>> heap.is_empty()
False
"""
return self.min_node.key is None
def merge(self, other_heap: FibonacciHeap) -> None:
"""Merge another Fibonacci heap into this one.
This operation combines two Fibonacci heaps by concatenating their
root lists and updating the minimum pointer if necessary. The other
heap is effectively consumed in this process.
Args:
other_heap: Another FibonacciHeap instance to merge into this one
Example:
>>> heap1 = FibonacciHeap()
>>> node1 = heap1.insert(3)
>>> heap2 = FibonacciHeap()
>>> node2 = heap2.insert(2)
>>> heap1.merge(heap2)
>>> heap1.find_min()
2
>>> heap1.total_nodes
2
"""
if other_heap.min_node.key is None:
return
if self.min_node.key is None:
self.min_node = other_heap.min_node
else:
self.min_node.right.left = other_heap.min_node.left
other_heap.min_node.left.right = self.min_node.right
self.min_node.right = other_heap.min_node
other_heap.min_node.left = self.min_node
if other_heap.min_node.key < self.min_node.key:
self.min_node = other_heap.min_node
self.total_nodes += other_heap.total_nodes
return "\n".join(print_tree(self.min_node))
if __name__ == "__main__":