二项堆和二叉堆都是是为了优先队列的功能。
1 结构
一个二项堆由一组二项树组成。
二项树的特性是,其中一棵树的根是另一棵树的最左孩子。画成图就是这样
二项树的性质,要牢记啊:
1 拥有
2
k
2^k
2k个节点
2 树的高度为k
3 在深处d处恰有
(
d
k
)
(^k_d)
(dk)个节点。这是数学里组合符号,代表从k中选d个的组合数量,哈哈,学过排列组合就知道了。
4 根的度数为k,子女的度数(高度)依次递减。
那为什么叫二项树。这里面有数学之美。
可以看到这两部分完美对称。
每一个都完美对称,可以一直二分下去。
从上面的完美对称性质可以看到。节点的个数只能是2的n次方。
但是实际应用中,节点的数量不可能这么完美啊。怎么办呢?这就需要一组二项树。比如13个节点,可以表示为8+4+1.这其实就是二进制计算。13转为二进制是1101.
13个元素的二项堆如下图
二项堆的性质:
1 每棵树都是小顶堆
2 按二进制编码决定树的数量。
3 按树的高度在链表上排序
每个二项树的节点有五个属性:
如图所示:
这五个属性是
1 p
2 key
3 degree
4 child
5 sibling
所以二项堆只有两个类。一个是堆类,只包含了一个head节点。一个是节点类。
2 合并两个二项堆
二项堆的算法里面最核心的算法,比较难的算法。这个过程分为两步,第一步是合并(MERGE),第二步是进行连接(UNION)。
合并两个链表的过程是新建一个二项堆h,每次从两个二项堆链上最左侧取最小高度的节点,拼接到h上。
合并链表之后要对链表上同高度的节点继续进行连接。
这时候涉及四种场景:
1. 当前指针和下一个节点等级不相等
2. 当前指针和下下个指针高度相等
3. 当前指针key<=下一个指针的key
4. 排除以上所有情况
代码比较繁琐,这就和红黑树代码一样,各种case,各种if/else,代码很丑,但是没有办法通过一个优雅的判断去实现。当然这比红黑树代码的场景还是少一点的。
Case 2、3、4都是等级/高度相等的。但是稍有不同
Case 2是三个树连续高度相等,但是当前指针x在第一个。这个时候如果合并,前面的树高度就比后面的高了,不符合二项树的特征了。所以直接把x指针移动一格。转为case 3或case 4的场景。
Case 3肯定是和下一棵树高度相等,也可能是三棵树高度相等,但是x指向中间那棵。 Case 3和case 4的区别在于当前节点比下一个节点key值小。
Case 4 是在于当前节点比下一个节点key值大,其他的和case 3一样。
所以case 3 和case 4是需要合并节点的。
图的话,是这样的
可以看到case 2为什么跳过去呢?因为直接转变为case3 或case4了。
两个同高度的节点合成一个节点的过程叫做链接(LINK)。
3 插入
插入操作是临时创建一个只有一个节点的二项堆,然后调用两个堆的合并方法。
4 移除最小值
步骤如下:
1 找到最小值所在的链表节点x
2 删除这个节点x
3 反向添加x的children为新的二项树,反向的原因是因为二项树的高度是递减的,而链表的高度是递增的
4 合并UNION新旧二项堆
5 减小键值
下图是减小键值为7的过程。
减小键值的过程比较简单,是一个数据结构不变,不断向上交换的过程。
6 移除任意值
分为两步:
1 替换为负无穷
2 调用移除最小值方法
代码如下:
7 python代码实现
所有代码git地址
https://e.coding.net/buildt/python-study/heaps.git
完整代码,节点类:
# _*_ coding:utf-8 _*_
class BinomialNode:
def __init__(self, value):
self.__p = None
self.__degree = 0
self.__key = value
self.__child = None
self.__sibling = None
self.__prev = None
@property
def p(self):
return self.__p
@p.setter
def p(self, p):
self.__p = p
@property
def degree(self):
return self.__degree
@degree.setter
def degree(self, degree):
self.__degree = degree
@property
def key(self):
return self.__key
@key.setter
def key(self, key):
self.__key = key
@property
def child(self):
return self.__child
@child.setter
def child(self, child):
self.__child = child
@property
def sibling(self):
return self.__sibling
@sibling.setter
def sibling(self, sibling):
self.__sibling = sibling
if sibling is not None:
sibling.__prev = self
@property
def prev(self):
return self.__prev
@prev.setter
def prev(self, prev):
self.__prev = prev
# 两棵树的相加
def __add__(self, other):
if self.degree != other.degree:
raise RuntimeError('两棵高度不一样的树不能相加')
left, right = (self, other) if self.__key > other.__key else (other, self)
left.p = right
left.sibling = right.child
right.child = left
right.degree = right.degree + 1
return right
def children(self):
x = self.child
if x is None:
return []
result = [x]
while x.sibling is not None:
result.append(x.sibling)
x = x.sibling
return result
def decrease_key(self, key):
if key > self.__key:
raise RuntimeError('不能增加键值')
self.__key = key
y = self
z = y.p
while z is not None and y.key < z.key:
# 交换值
y.key, z.key = z.key, y.key
y = z
z = z.p
def __str__(self):
return str(self.__key)
堆类:
# _*_ coding:utf-8 _*_
# 一个二项堆底层是二项树的链表
from com.youngthing.heaps.binomial_node import BinomialNode
class BinomialHeap:
def __init__(self, *args):
if args is None or len(args) == 0:
self.__head = None
else:
value = args[0]
self.__head = BinomialNode(value)
# 查找最小值,只需要在根部的链表查询就可以了
def min_key(self):
min = self.min_node()
return min.key
def min_list_node(self):
point = self.__head
min = self.__head
while point is not None:
if point.key < min.key:
min = point
point = point.sibling
return min
def min_node(self):
return self.min_list_node().key
# 二项堆最重要的操作就是合并,合并的重要方向是合并同样degree的。
def __add__(self, other):
h = self.merge(other)
if h is None:
return h
x = h.__head
while x.sibling is not None:
# case 1
if x.degree != x.sibling.degree:
x = x.sibling
# case 2
elif x.sibling.sibling is not None and x.sibling.sibling.degree == x.degree:
x = x.sibling
elif x.key <= x.sibling.key:
# 让下一个消失,直接连上下下个
x_next = x.sibling
x.sibling = x_next.sibling
x_next.sibling = None
x_next.prev = None
# 和下一个相加
x = x + x_next
else:
# 当前节点是要“消失”在链上的
# 这里做个防空判断
x_prev = x.prev
x_sibling = x.sibling
x.prev = None
x.sibling = None
x = x + x_sibling
if x_prev is None:
h.__head = x_sibling
h.__head.prev = None
else:
x_prev.sibling = x_sibling
return h
def insert(self, value):
h = BinomialHeap(value)
self.__head = (self + h).__head
def extra_min(self):
if self.__head is None:
raise RuntimeError('二项堆为空')
# 如果只有一个,那么直接删除
if self.__head.sibling is None and self.__head.degree == 0:
x = self.__head
self.__head = None
return x
x:BinomialNode = self.min_list_node()
# 链上删除掉x
if x.prev is not None:
x.prev.sibling = x.sibling
else:
self.__head = x.sibling
if x.sibling is not None:
x.sibling.prev = None
# 清空属性
x.sibling = None
x.prev = None
# 逆向添加x所有的children
children = x.children()
h = BinomialHeap()
for i in range(len(children) - 1, -1, -1):
k = children[i]
k.sibling = None
k.prev = None
k.p = None
h.append(k)
self.__head = (self + h).__head
return x.key
# 合并
def merge(self, other):
# 首先新建一个新的堆
h = BinomialHeap()
# 然后通过链表将两个链表连起来
# 具体算法就是循环遍历两个链表,然后串起来
# 双指针定义
p1: BinomialNode = self.__head
p2: BinomialNode = other.__head
while p1 is not None and p2 is not None:
if p1.degree < p2.degree:
x = p1.sibling
p1.sibling = None
h.append(p1)
p1 = x
elif p1.degree == p2.degree:
x = p1.sibling
y = p2.sibling
p1.sibling = None
p2.sibling = None
h.append(p1).append(p2)
p1 = x
p2 = y
else:
x = p2.sibling
p2.sibling = None
h.append(p2)
p2 = x
# 然后连不空的
if p1 is not None:
h.append(p1)
elif p2 is not None:
h.append(p2)
# 最后合并
return h
def append(self, node: BinomialNode):
head = self.__head
if head is None:
self.__head = node
return self
# 只要不成环就行
last = head
while last.sibling is not None:
last = last.sibling
last.sibling = node
return self
def append_key(self, key):
node = BinomialNode(key)
return self.append(node)
def delete(self, key):
key.decrease_key(float('-inf'))
self.extra_min()
def __str__(self):
is_first = True
result_str = ''
x = self.__head
while x is not None:
if is_first:
is_first = False
else:
result_str += ' -> '
result_str += str(x.key)
x = x.sibling
return result_str