We start here by defining a class BH of binomial heaps. Binomial heaps being a heap of binary trees, a class BT of binary trees is also defined.
All functions produced in the exercise are enlisted within the class definition but repeated later on the fly.
As a consequence of these properties, $B_k$ has depth $k$ and size $2^k$, so letting $n=2^k$, $B_k$ has $\log n$ depth. Besides, since $B_k$ has up $k$ children, for $n=2^k$, $B_k$ has no more than $\log n$ children: by construction, this holds true for every subtree of $B_k$ (being itself a $B_{k'}$ for some $k'<k$).
import numpy as np
import sys
class BH: # binomial heap
def __init__(self,elements=[]):
self.trees = []
for e in elements:
self.insert(e)
def insert(self,e):
if len(self.trees) == 0:
self.trees = [BT(e)]
else:
self.fuse(BH([e]))
def fuse(self,bh): # fuse in another BH
union_trees = self.trees + bh.trees
union_trees.sort(key=lambda x : x.size) # sort the trees by size
tree_pairs=[ [j,j+1] for j in range(len(union_trees))] # sliding window of all pairs (j,j+1) to compare trees pairwise
i=0
while i < len(tree_pairs)-1:
if union_trees[tree_pairs[i][0]].size == union_trees[tree_pairs[i][1]].size:
union_trees[tree_pairs[i][0]]=union_trees[tree_pairs[i][0]].fuse(union_trees[tree_pairs[i][1]]) # fusing trees of equal size
union_trees[tree_pairs[i][0]].size *= 2
union_trees.pop(tree_pairs[i][1]) # one tree is gone, reducing union_trees length
tree_pairs.pop(-1)
else: # trees are of different size: move to next pair
i+=1
self.trees = union_trees
def minimum(self):
return min([t.value for t in self.trees])
def remove_minimum(self):
min_value = self.minimum()
for index,t in enumerate(self.trees):
if t.value == min_value:
del self.trees[index]
bh = BH()
bh.trees = t.children
self.fuse(bh)
def find_element(self,value):
for t in self.trees:
foundVal,path=t.find_element(value)
if foundVal:
return path
return []
def reduce_value(self,value,delta):
path = self.find_element(value)
path[-1].value -= delta
new_value = path[-1].value
i=1
while i<len(path) and path[-1-i].value>path[-1-i+1].value:
path[-1-i+1].value = path[-1-i].value
path[-1-i].value = new_value
i+=1
def remove_element(self,value):
self.reduce_value(value,value+sys.maxsize//2)
self.remove_minimum()
def print_heap(self):
for index,t in enumerate(self.trees):
print("BTree",index)
t.print_tree()
print("\n")
return
class BT: # binomial tree
def __init__(self,value): #,children=[],size=1):
self.value = value
self.size = 1
self.children = [] #children
def fuse(self,bt):
if self.value < bt.value:
self.children.insert(0,bt)
return self
else:
bt.children.insert(0,self)
return bt
self.size += bt.size
def find_element(self,value):
if self.value == value:
return True,[self]
else:
foundVal = False
i=0
while (not foundVal and i<len(self.children) and self.value<value):
foundVal,path=self.children[i].find_element(value)
i+=1
if foundVal:
return True,[self]+path
return False,[]
def print_tree(self,level=0):
print(" |---" * level,self.value)
for child in self.children:
if child == None:
print(" |---" * (level+1),"*")
else:
child.print_tree(level+1)
treeSize = 20
elements = np.random.randint(0,100,treeSize)
mybh=BH(elements)
mybh.print_heap()
print("Removing minimum, i.e.,",mybh.minimum(),":\n")
mybh.remove_minimum()
mybh.print_heap()
Since BH's are a stack of BT's which are "min-sorted", the minimum in a BH is one of the roots of its BT's, so no more than $\log n$ of them. Hence a complexity of $O(\log n)$ to find the minimum in a BH.
# At BH level
def minimum(self):
return min([t.value for t in self.trees])
mybh.minimum()
The union of two BHs is in general not a BH as no two BTs of the same size can be found in a BH: by the union operation, two BTs of the same size could be found.
The idea in the fusion of two BTs is to ensure that no two trees in the heaps are of the same size. If it's the case, the trees are fused (in a tree twice as large), and so on over all trees (one must be careful that the newly created tree could be of the same size as other trees). Specifically the algorithm runs as follows:
This result is then used to add up a new node to a BH: by merging the current BH with the BH with the new node as singleton tree node.
# At BH level
def fuse(self,bh): # fuse in another BH
union_trees = self.trees + bh.trees
union_trees.sort(key=lambda x : x.size) # sort the trees by size
tree_pairs=[ [j,j+1] for j in range(len(union_trees))] # sliding window of all pairs (j,j+1) to compare trees pairwise
i=0
while i < len(tree_pairs)-1:
if union_trees[tree_pairs[i][0]].size == union_trees[tree_pairs[i][1]].size:
union_trees[tree_pairs[i][0]]=union_trees[tree_pairs[i][0]].fuse(union_trees[tree_pairs[i][1]]) # fusing trees of equal size
union_trees[tree_pairs[i][0]].size *= 2
union_trees.pop(tree_pairs[i][1]) # one tree is gone, reduncing union_trees length
tree_pairs.pop(-1)
else: # trees are of different size: move to next pair
i+=1
self.trees = union_trees
# At BT level
def fuse(self,bt):
if self.value < bt.value:
self.children.insert(0,bt)
return self
else:
bt.children.insert(0,self)
return bt
self.size += bt.size
# At BH level: insert a new node
def insert(self,e):
if len(self.trees) == 0:
self.trees = [BT(e)]
else:
self.fuse(BH([e]))
To remove the smallest element of a BH, it suffices to find this smallest element (through the function minimum()), which needs to be the root of a BT of the heap, then break the corresponding tree into its children and fuse them back with the original heap.
The complexity of minimum() is $O(\log n)$. The fusion of the children into the BH both come at cost $O(\log n)$. The total resulting complexity is then $$O(\log n)$$
# At BH level
def remove_minimum(self):
min_value = self.minimum()
for index,t in enumerate(self.trees):
if t.value == min_value:
del self.trees[index]
bh = BH()
bh.trees = t.children
self.fuse(bh)
treeSize = 20
elements = np.random.randint(0,100,treeSize)
mybh=BH(elements)
print("Original BH:\n")
mybh.print_heap()
mybh.remove_minimum()
print("After removal of smallest element:\n")
mybh.print_heap()
To find an element in a heap, we must scan over all BTs and go down successively into each of them until the value met is larger than the sought for element (and of course, stop whenever the element is found). On the way, we can store the path to the found node as a list (or return [] if not found). By default, we will return the path to the first found element (no scan of further trees is done after the element is first found).
The complexity in the worst case is $\log n$ (number of BTs) times $\log n$ (depth of each BT), so $$O( (\log n)^2 )$$
Function: find_element# At BH level
def find_element(self,value):
for t in self.trees:
foundVal,path=t.find_element(value)
if foundVal:
return path
return [] # default return if element not found
# At BT level, we return both the indicator on finding the node and the path to it
def find_element(self,value):
if self.value == value:
return True,[self]
else:
foundVal = False
i=0
while (not foundVal and i<len(self.children) and self.value<value): # go down so long that self.value<value
foundVal,path=self.children[i].find_element(value)
i+=1
if foundVal:
return True,[self]+path # create the path
return False,[]
index = np.random.randint(0,len(elements))
a=mybh.find_element(elements[index])
print("Searching for value:",elements[index])
print("Path to value:",[a[i].value for i in range(len(a))])
Reducing an element consists in finding the element to be reduced (using the routine find_element) and then switch between the node and its parent so long that the parent has larger value than the node, up until reaching the root.
# At BH level
def reduce_value(self,value,delta):
path = self.find_element(value)
path[-1].value -= delta
new_value = path[-1].value
i=1
while i<len(path) and path[-1-i].value>path[-1-i+1].value:
path[-1-i+1].value = path[-1-i].value
path[-1-i].value = new_value
i+=1
treeSize = 20
elements = np.random.randint(0,100,treeSize)
mybh=BH(elements)
print("Original BH:\n")
mybh.print_heap()
delta=10
index = np.random.randint(0,len(elements))
mybh.reduce_value(elements[index],delta)
print("Reducing",elements[index],"by",delta,":\n")
mybh.print_heap()
To remove an element from the heap, a fun "trick" is used:
Again, the complexity is equivalent to the complexity of 1. + 2. + 3., all in $O(\log n)$, so a total complexity of $O(\log n)$.
# At BH level
def remove_element(self,value):
self.reduce_value(value,value+sys.maxsize//2) ## division by 2 to avoid calculus overflow
self.remove_minimum()
treeSize = 20
elements = np.random.randint(0,100,treeSize)
mybh=BH(elements)
print("Original BH\n")
mybh.print_heap()
index = np.random.randint(0,len(elements))
mybh.remove_element(elements[index])
print("After removal of",elements[index],"\n")
mybh.print_heap()