Python数据结构应用——树
Python数据结构应用——树
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。
-
数据结构中的树的结点和机器学习中决策树的结点有一个很大的不同就是,数据结构中的树的每个叶结点都是独立的。
-
树的高度(Height)指叶结点的最大层树(不包含根结点)
一、树的建立
树可以这样定义:一棵树由一系列结点和一系列连接结点的边组成
树也可以这样定义: 一棵树有根和其他子树组成,这些子树也是树
在python,使用的定义都是后者。
1.1.list of lists
对于一个list:['q',[],[]]
,代表的是一棵树(子树),’q’
是根结点,[],[]
分别左右两个子结点。所以,有一个左结点为'a'
的树可以写为['q',['a',[],[]],[]]
my_tree = ['a', ['b', ['d',[],[]], ['e',[],[]] ], ['c',['f',[],[]], []] ]
在这种定义的情况下,根结点为my_tree[0]
,左结点为my_tree[1]
,右结点为my_tree[2]
def binary_tree(r):
return [r, [], []]
def insert_left(root, new_branch):
t = root.pop(1) # The left child position
if len(t) > 1: # if not empty
# The origin left child turn to be the left child of new_branch
root.insert(1, [new_branch, t, []])
else:
root.insert(1, [new_branch, [], []])
return root
def insert_right(root, new_branch):
t = root.pop(2)
if len(t) > 1:
root.insert(2, [new_branch, [], t])
else:
root.insert(2, [new_branch, [], []])
return root
def get_root_val(root):
return root[0]
def set_root_val(root, new_val):
root[0] = new_val
def get_left_child(root):
return root[1]
def get_right_child(root):
return root[2]
x = binary_tree('a')
insert_left(x,'b')
insert_right(x,'c')
insert_right(get_right_child(x), 'd') # important!
insert_left(get_right_child(get_right_child(x)), 'e')
['d', ['e', [], []], []]
1.2 Nodes and References
这种思想主要是构建一个类,并采用嵌套的方法,生成左右结点(子树)
class BinaryTree:
def __init__(self, root):
self.key = root
self.left_child = None
self.right_child = None
def insert_left(self, new_node):
if self.left_child == None:
self.left_child = BinaryTree(new_node)
else:
t = BinaryTree(new_node)
t.left_child = self.left_child
self.left_child = t
def insert_right(self, new_node):
if self.right_child == None:
self.right_child = BinaryTree(new_node)
else:
t = BinaryTree(new_node)
t.right_child = self.right_child
self.right_child = t
def get_right_child(self):
return self.right_child
def get_left_child(self):
return self.left_child
def set_root_val(self,obj):
self.key = obj
def get_root_val(self):
return self.key
x = BinaryTree('a')
x.insert_left('b')
x.insert_right('c')
x.get_right_child().insert_right('f')
x
<__main__.BinaryTree at 0x105930278>
这两种构造方法都没有办法返回到父结点,具体见 三、解析树
二、基于二叉堆的优先队列
之前的队列(queue)都是先进先出的数据结构。然而在优先队列中,在队列里的数据都有一个优先级标签。 在优先级队列中,队列的项的逻辑顺序由他们的优先级决定。最高优先级在队列的前面,最低优先级在项的后面。
实现优先级队列的经典方法是使用成为二叉堆的数据结构,它可以在O(log(n))O(log(n))时间中排队和取出队列
PS:二叉堆(binary heap)和stack的那个堆不是同一回事,这个堆(heap)在结构上是一棵完全二叉树,在实现上使用的数组list,采用数组的index值表示父结点及子结点。二叉堆有两个常见的变体:最小堆(其中最小的键总是在前面)和最大堆。
由于二叉堆要是一个完全二叉树,所以为了保证log(n)的时间量,必须保证树是平衡的。
如图是一个二叉堆,堆的结构类似树,实现方式由list实现。list的第一个元素是0,至于为什么要这样呢?因为树上的每一个元素都有对应的index,要满足左右结点分别是2*p
&2*p+1
,如果索引index是从0开始,则不满足。
二叉堆的属性是一个很重要的东西,二叉堆的所有操作基本都基于它的属性:每个结点的父结点都比它本身要小 (最小堆)
class BinHeap:
def __init__(self):
self.heap_list = [0]
self.current_size = 0
2.1 插入(insert)操作
在二叉堆中插入一个项,过程是:将这个要插入的项置于二叉堆list的最后一项,由于二叉堆要保持它的属性(每个结点的父结点都比它本身要小),这个项与其父结点进行比较,小的在上,大的在下进行可能的交换,这种比较要一直持续到根结点。
这种交换由于是将待插入的项从下往上交换,所以叫做percolate_up
def perc_up(self, i):
while i//2 > 0:
if self.heap_list[i] < self.heap_list[i//2]:
self.heap_list[i],self.heap_list[i//2]=self.heap_list[i//2],self.heap_list[i]
i = i//2
def insert(self, k):
self.heap_list.append(k)
self.current_size += 1
self.perc_up(self, self.current_size)
2.2 删除(出队)操作
由于我们研究的是最小堆,所以出队操作是移出值最小的那个结点,即根结点。
具体操作:移除根结点后,将list的最后一个项放置在根结点的位置上,然后根据二叉堆的属性进行percolate_down
操作(即根结点的项依次向下进行比较)
def perc_down(self, i):
while (i * 2) <= self.current_size:
mc = self.min_child(i)
if self.heap_list[i] > self.heap_list[mc]:
self.heap_list[i],self.heap_list[mc]=self.heap_list[mc],self.heap_list[i]
i = mc
# 返回左右结点小的那一个
def min_child(self, i):
if i*2+1 > self.current_size:
return i*2
else:
if self.heap_list[i*2] < self.heap_list[i*2+1]:
return i*2
else:
return i*2+1
def del_min(self):
# The first element is 0
ret_val = self.heap_list[1]
self.heap_list[1] = self.heap_list[self.current_size]
self.current_size = self.current_size - 1
self.heap_list.pop()
self.perc_down(1)
return ret_val
2.3 建立二叉堆
两种想法:
1.可以将list直接进行排序,最低花费O(nlogn)O(nlogn),得出的list肯定是个二叉堆
2.对于一个list的每一个项从左往右进行percolate_down
操作,时间复杂度O(n)O(n)
def build_heap(self, a_list):
i = len(a_list)//2
self.current_size = len(a_list)
self.heap_list = [0] + a_list[:]
while(i > 0):
self.perc_down(i)
i = i - 1
三、解析树(parse tree)
把解析表达式表示成树的形式,如:((7+3)∗(5−2))((7+3)∗(5−2))可以表示成
解析树的形成可以定义为以下四个规则:
1.如果遇到(
,意味着有一个新的表达式,则创建一棵新子树(添加一个新子结点,且再创建这个新结点的左孩子,将处理目标移到这个左孩子上)
2.如果遇到[+,-,*,/]
操作符(operators),将此操作符置于现结点,同时创建一个右孩子,将处理目标移到这个右孩子上
3.如果遇到一个number,将这个number置于到现结点,将处理目标返回到父结点
4.如果遇到)
,将处理目标返回到父结点。
def build_parse_tree(fp_exp):
fp_list = fp_exp.split()
p_stack = Stack()
e_tree = BinaryTree('')
p_stack.push(e_tree)
current_tree = e_tree
for i in fp_list:
if i == '(':
current_tree.insert_left('')
p_stack.push(current_tree)
current_tree = current_tree.get_left_child()
elif i not in ['+','-','*','/',')']:
current_tree.set_root_val(int(i))
parent = p_stack.pop()
current_tree = parent
elif i in ['+','-','*','/']:
current_tree.set_root_val(i)
current_tree.insert_right('')
p_stack.push(current_tree)
current_tree = current_tree.get_right_child()
elif i == ')':
current_tree = p_stack.pop()
else:
raise ValueError
return e_tree
由于之间构造的二叉树类无法访问其父结点,所以这里要新建一个堆在储存当前的父结点。
在遇到(
和operaters的这两个操作都需要将当前操作结点移到左右孩子上,所以这两个操作需要将父结点入堆。
四、树的遍历
三种遍历方式:先序遍历(preorder,中左右), 中序遍历(inorder,左中右), 后序遍历(postorder,左右中),他们的不同在于每个结点访问的顺序。
先,中,后都是对于根结点来说的。
在python中两种方法进行遍历:
1.外界函数
2.类中函数
事实证明外界函数处理遍历更加有效率,因为在大多数情况下,遍历都会跟别的操作混合在一起,所以外界函数修改更方便。
def preorder(tree):
if tree:
print(tree.get_root_val())
preorder(tree.get_left_child())
preorder(tree.get_right_child())
def preorder(self): # 类中函数
print(self.key)
if self.left_child:
self.left.preorder()
if self.right_child:
self.right.preorder()
def postorder(tree):
if tree:
postorder(tree.get_left_child())
postorder(tree.get_right_child())
print(tree.get_root_val)
def inorder(tree):
if tree:
inorder(tree.get_left_child())
print(tree.get_root_val)
inorder(tree.get_right_child())
4.1 解析树evalute
import operator
def postorder_eval(tree):
opers = {'+':operator.add,'-':operator.sub,'*':operator.mul,
'/':operator.truediv}
res1 = None
res2 = None
if tree:
res1 = postorder_eval(tree.get_left_child())
res2 = postorder_eval(tree.get_right_child())
if res1 and res2:
return opers[tree.get_root_val()](res1,res2)
else:
return tree.get_root_val()
五、二叉搜索树(BST)
二叉搜索树的实现主要依赖于它的性质(BST property):parent的key值比左结点大,比右结点小。
class TreeNode:
def __init__(self, key, val, left=None, right=None, parent=None):
self.key = key
self.payload = val
self.left_child = left
self.right_child = right
self.parent = parent
def has_left_child(self):
return self.left_child
def has_right_child(self):
return self.right_child
def is_left_child(self):
return self.parent and self.parent.left_child == self
def is_right_child(self):
return self.parent and self.parent.right_child == self
def is_root(self):
return not self.parent
def is_leaf(self):
return not (self.right_child or self.left_child)
def has_any_children(self):
return self.right_child or self.left_child
def has_both_children(self):
return self.right_child and self.left_child
def replace_node_data(self, key, value, lc, rc):
self.key = key
self.payload = value
self.left_child = lc
self.right_child = rc
if self.has_left_child():
self.left_child.parent = self
if self.has_right_child():
self.right_child.parent = self
BinarySearchTree 这个类很复杂:
class BinarySearchTree:
def __init__(self):
self.root = None
self.size = 0
def length(self):
return self.size
def __len__(self):
return self.size
def __iter__(self):
return self.root.__iter__()
下面的put()
函数在BinarySearchTree
中,他的主要功能是插入一个新node。如果树没有根结点,即将待插入结点为根结点,如果树有根结点,即执行该类的私有函数_put()
def put(self, key, val):
if self.root:
self._put(key, val, self.root)
else:
self.root = TreeNode(key ,val)
self.size = self.size + 1
def _put(self, key, val, current_node):
if key < current_node.key:
if current_node.has_left_child():
self._put(key, val, current_node.left_child)
else:
current_node.left.child = TreeNode(key, val, parent=current_node)
else:
if current_node.has_right_child():
self._put(key, val, current_node.right_child)
else:
current_node.right_child = TreeNode(key, val, parent=current_node)
下面是python的魔术方法,能轻易的进行赋值操作:(下列函数生成后即可以执行 my_zip_tree[32]=='hello'
这种赋值操作)
def __setitem__(self, k, v):
self.put(k, v)
get()
操作是put()
的反操作,用来找到结点
def get(self, key):
if self.root:
res = self._get(key, self.root)
if res:
return res.payload
else:
return None
else:
return None
def _get(self, key, current_node):
if not current_node:
return None
elif current_node.key == key:
return current_node
elif key < current_node.key:
return self._get(key, current_node.left_child)
else:
return self._get(key, current_node.right_child)
def __getitem(self, key):
return self.get(key)
使用get()函数,我们还可以创建in
操作,即创建__contains__
函数:
def __contains__(self, key):
if self._get(key, self.root):
return True
else:
return False
if 'Northfield' in my_zip_tree:
print("oom ya ya")
删除结点操作是二叉搜索树中最复杂的部分,主要过程分为两步:首先,使用get()操作找到要删除的node;然后,删除node且保证其他子树连接仍然保持二叉树属性。
def delete(self, key):
if self.size > 1:
node_to_remove = self._get(key, self.root)
if node_to_remove:
self.remove(node_to_remove)
self.size = self.size - 1
else:
raise KeyError('Error, key not in tree')
elif self.size == 1 and self.root.key == key:
self.root = None
self.size = self.size - 1
else:
raise KeyError('Error, key not in tree')
def __delitem__(self, key):
self.delete(key)
remove()
操作分为三种情况:
第一种:删除的结点没有孩子,这种情况很直接,直接删除就ok
if current_node.is_leaf():
if current_node == current_node.parent.left_child:
current_node.parent.left_child = None
else:
current_node.parent.right_child = None
第二种:删除的结点有一个孩子(这里只考虑这个孩子是左孩子的情况,右孩子的情况是对称的)
1.如果要删除的结点是左孩子,那么我们只要更新它的左孩子与他的父结点对接即可
2.如果要删除的结点是右孩子,那么我们只要更新它的右孩子与他的父结点对接即可
3.如果删除的结点没有父母,那么他肯定是根结点
else: # this node has one child
if current_node.has_left_child():
if current_node.is_left_child():
current_node.left_child.parent = current_node.parent
current_node.parent.left_child = current_node.left_child
elif current_node.is_right_child():
current_node.left_child.parent = current_node.parent
current_node.parent.right_child = current_node.left_child
else:
current_node.replace_node_data(current_node.left_child.key,
current_node.left_child.payload,
current_node.left_child.left_child,
current_node.left_child.right_child)
else:
if current_node.is_left_child():
current_node.right_child.parent = current_node.parent
current_node.parent.left_child = current_node.right_child
elif current_node.is_right_child():
current_node.right_child.parent = current_node.parent
current_node.parent.right_child = current_node.right_child
else:
current_node.replace_node_data(current_node.right_child.key,
current_node.right_child.payload,
current_node.right_child.left_child,
current_node.right_child.right_child)
第三种:删除的结点有两个孩子。这时,并不能直接用他的孩子替代,这个时候需要找一个继任者(successor),这个继任者满足两个条件(1.在删除结点中的子孙中第一个比他大的结点。2.这个继任者最多有一个孩子),继任者接替删除结点的位置。
elif current_node.has_both_children(): #interior
succ = current_node.find_successor()
succ.splice_out()
current_node.key = succ.key
current_node.payload = succ.payload
def find_successor(self):
succ = None
if self.has_right_child():
succ = self.right_child.find_min()
else:
if self.parent:
if self.is_left_child():
succ = self.parent
else:
self.parent.right_child = None
succ = self.parent.find_successor()
self.parent.right_child = self
return succ
- Reference:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!