Last Updated: 2023-11-02 12:04:35 Thursday
-- TOC --
应用开发一般不会直接撸二叉树,Binary Tree,各种库和底层接口已经把它封装好了,但这个东西却是很多系统性能的关键,必须要深刻理解。Heap相对比较简单,很多时候需要我们自己直接手搓heap。
下面是关于二叉树的相关术语。
定义:Binary Tree要么是empty,或者是一个Node,这个Node的左右两边包含value和reference,其reference指向的也是一颗Binary Tree,即子树,subtree。
Root
Node is the top node,树根Parent
Node,父节点Child
Node,子结点Leaf
Node, which is node with no children,叶子节点Internal
Node, which is node with at least one children,中间节点Edge
, lines between any two node,边Level
, start from zero,层Depth
of node, the number of node to the root,深Height
of node, the max number of node to a leaf node,高,root的height值最高,Height of Tree, is the height of root node计算node height时,要么数node,要么数edge,根据场景看那种方便,height要么最小为1,要么最小为0。
BTree不是Binary Tree,它是另一个概念!
All internal nodes have 2 children, and all leaves have the same depth. 所有中间节点都有2个子结点,所有叶子节点的深度相同。
Perfect Binary Tree总是完美的三角形!
Every node has 0 or 2 children. 每个节点都有0或2个子结点。
Perfect Binary Tree is Full Binary Tree, but not vice versa. 完美二叉树是满二叉树,但反过来不成立。
Every level is completely filled maybe except the bottom. Nodes in the bottom level are packed towards the left side. 除了最后一层,其它层的节点是满的,而且最后一层的节点全部靠向左侧。
Perfect Binary Tree is Complete Binary Tree, but not vice versa. 完美二叉树是完全二叉树,但反过来不成立。Full Binary Tree may not be Complete Binary Tree. 满二叉树不一定是完全二叉树。
完全二叉树有重要特性:
parent(idx) = (idx-1) // 2
,idx>0left(idx) = idx*2 + 1
right(idx) = idx*2 + 2
len(array) // 2
len(array) & 1
,奇数有右child,此时它还是一颗full binary tree,偶数就没有右child。这里的Heap可不是进程虚拟空间中的那个heap....
Heap是由Complete Binary Tree构建的数据结构,有两种类型的Heap,如下:
max-heap
,in any given subtree,the max value must be found in its root,大根堆。min-heap
,in any given subtree,the min value must be found in its root,小根堆。对Heap的几个基本操作:
以上任何操作执行之后,都必须保证heap结构的特性不变(invariant):
观察到Heap结构的存储是一个array,但此array并非完全有序。Heap结构并非要求所有元素有序,仅仅是要求parent与children的关系。所谓堆排序算法,Heapsort算法,就是利用Heap结构,对一组数据,先执行add,然后执行pop,得到的序列就是排好序的了。
推荐:内部排序算法总结
与普通Queue不一样的地方,在于pop(dequeue)出来的总是具有最大的(或最小的)优先级的元素。实现Priority Queue的方式很多,但性能最好的,是基于Heap的实现。排序算法怎么也要\(N\cdot\log{N}\),而Heap这种结构,不追求完全有序,它可以在add和pop的时候,做到\(\log{N}\)。
FIFO,fair enough!
unfair!
下面是一个较通用的Heap参考实现和测试,如果保持默认的K,它就是个max heap:
class Heap:
def __init__(self, key=lambda x:x):
self.data = []
self.K = key
@staticmethod
def _parent(idx):
assert idx > 0
return (idx-1) // 2 # same as >>1, not int()
@staticmethod
def _left(idx):
return idx*2 + 1
@staticmethod
def _right(idx):
return idx*2 + 2
def heapify(self, data):
self.data = data
for i in reversed(range(len(data)//2)):
self._trickle_down(i)
def _trickle_down(self, idx=0):
""" trickle down start at idx """
while idx < len(self.data):
left, right = Heap._left(idx), Heap._right(idx)
tidx = idx
if left<len(self.data) and self.K(self.data[tidx])<self.K(self.data[left]):
tidx = left
if right<len(self.data) and self.K(self.data[tidx])<self.K(self.data[right]):
tidx = right
if tidx == idx:
return
self.data[idx], self.data[tidx] = self.data[tidx], self.data[idx]
idx = tidx
def add(self, x):
""" bubble up """
idx = len(self.data)
self.data.append(x)
while idx > 0:
parent = Heap._parent(idx)
if self.K(self.data[idx]) > self.K(self.data[parent]):
self.data[idx], self.data[parent] = self.data[parent], self.data[idx]
idx = parent
continue
return
def peek(self):
ret = None
if self.data:
ret = self.data[0]
return ret
def pop(self):
ret = None
if self.data:
ret = self.data[0]
self.data[0] = self.data[-1]
del self.data[-1]
self._trickle_down()
return ret
def __bool__(self):
return len(self.data) > 0
def __len__(self):
return len(self.data)
def __repr__(self):
return repr(self.data)
import random
h = Heap()
a = [i for i in range(16)]
random.shuffle(a)
for i in a:
h.add(i)
print(h)
for i in range(len(a)+1):
print(h.peek())
assert h.peek() == h.pop()
h = Heap(lambda x:-x)
a = [i for i in range(16)]
random.shuffle(a)
for i in a:
h.add(i)
print(h)
for i in range(len(a)+1):
print(h.peek())
assert h.peek() == h.pop()
按字符串长度排序的Heapsort
h = Heap(lambda x:len(x))
a = [str(i)*i for i in range(9)]
random.shuffle(a)
for i in a:
h.add(i)
b = [h.pop() for _ in range(len(h))]
print(b)
heapify接口
给上面的实现,增加了一个heapify接口,如上!将一个任意的list,直接原地转换成heap结构。下面是测试代码,check_heap
是个通用的heap结构checker!
def heap_check(data, K=lambda x:x):
if (size:=len(data)) == 0:
return True
idx = (size//2) - 1
i2 = idx * 2
if size & 1:
t = data[i2+2] if K(data[i2+1])<K(data[i2+2]) else data[i2+1]
if K(data[idx]) < K(t):
return False
else:
if K(data[idx]) < K(data[i2+1]):
return False
for i in range(idx-1,-1,-1):
i2 = i * 2
t = data[i2+2] if K(data[i2+1])<K(data[i2+2]) else data[i2+1]
if K(data[i]) < K(t):
return False
return True
import random
for n in range(2000):
a = [i for i in range(n)]
random.shuffle(a)
h = Heap()
h.heapify(a)
assert len(h) == n
assert heap_check(h.data)
K = lambda x: -x
for n in range(2000):
a = [i for i in range(n)]
random.shuffle(a)
h = Heap(K)
h.heapify(a)
assert len(h) == n
assert heap_check(h.data, K)
对Queue的一点思考
除了priority queue的存储只能基于array,一般的queue和stack都可以采用linked-list方式存储。
接下来学习:BSTree和AVLTree
https://docs.python.org/3/library/heapq.html
这个模块的源码,非常值得学习!
这里面有两个接口有点意思:nlargest
和nsmallest
,分别搜索一个iterable中最大或最小的n个元素。这两个接口背后的原理,就是heap,对任意iterable执行heapify后,每次pop,都是\(O(\log{N})\)。这两个接口在n比较小的时候使用,如果n太大,直接sorted排序然后slicing,如果n==1,用max或min。
我在LeetCode第23题(合并K个有序单链表)中应用了heap数据结构,但实现与本文代码稍有不同,更加高效优雅...C和C++版本的heap实现也在此题内,供参考。
本文链接:https://cs.pynote.net/ag/202306271/
-- EOF --
-- MORE --