二叉树、图

二叉树

一个普通的链表里,每一个结点会包含一个连接自身和另一结点的链。树也是基于结点的数据结构,但树里面的每个结点,可以含有多个链分别指向其他多个结点。

以下是一棵典型的树。

谈论树的时候,我们会用到以下术语。

 最上面的那一结点(此例中的“j”)被称为

 此例中,“j”是“m”和“b”的父结点,反过来,“m”和“b”是“j”的子结点。“m” 又是“q”和“z”的父结点,“q”和“z”是“m”的子结点。

 树可以分层。此例中的树有 3 层。 

 二叉树遵守以下规则:

 每个结点的子结点数量可为 0、1、2。

 如果有两个子结点,则其中一个子结点的值必须小于父结点,另一个子结点的值必须大于父结点。

以下是一个二叉树的例子,其中结点的值是数字。

注意,小于父结点的子结点用左箭头来表示,大于父结点的子结点则用右箭头来表示。

尽管下图是一棵树,但它不是二叉树。 

原因:它的两个子结点的值都小于父结点。

以 Python 来实现一个树结点:

class TreeNode: 
 def __init__(self,val,left=None,right=None): 
 self.value = val 
 self.leftChild = left 
 self.rightChild = right 

然后就可以用它来构建一棵简单的树了。

node = TreeNode(1) 
node2 = TreeNode(10) 
root = TreeNode(5, node, node2) 

因为二叉树具有这样独特的结构,所以我们能在其中非常快速地进行查找操作

查找

二叉树的查找算法先从根结点开始。

(1) 检视该结点的值。

(2) 如果正是所要找的值,太好了!

(3) 如果要找的值小于当前结点的值,则在该结点的左子树查找。

(4) 如果要找的值大于当前结点的值,则在该结点的右子树查找。

以下是用 Python 写的递归式查找。 

def search(value, node): 
 # 基准情形:如果 node 不存在
 # 或者 node 的值符合
 if node is None or node.value == value: 
 return node 
 # 如果 value 小于当前结点,那就从左子结点处查找
 elif value < node.value: 
 return search(value, node.leftChild) 
 # 如果 value 大于当前结点,那就从右子结点处查找
 else: # value > node.value 
 return search(value, node.rightChild)

 插入

def insert(value, node): 
 if value < node.value: 
 # 如果左子结点不存在,则将新值作为左子结点
 if node.leftChild is None: 
 node.leftChild = TreeNode(value) 
 else: 
 insert(value, node.leftChild) 
 elif value > node.value: 
 # 如果右子结点不存在,则将新值作为右子结点
 if node.rightChild is None: 
 node.rightChild = TreeNode(value) 
 else: 
 insert(value, node.rightChild) 

在完全失衡的最坏情况下,二叉树的查找需要 O(N)。在理想平衡的最好情况下,则是 O(log N)。 在数据随机插入的一般情况下,因为树也大致平衡,所以查询效率也大约是 O(log N)。

删除

如图:如果删掉 10 的话,就会导致 11 的那个结点从树上脱离。当然这是不允许的,否则这个 11 就永远都找不到了。好在我们还有解决办法:将 11 放到之前 10 所在的位置。

删除操作遵循以下规则:

 如果要删除的结点没有子结点,那直接删掉它就好。

 如果要删除的结点有一个子结点,那删掉它之后,还要将子结点填到被删除结点的位置上。

要删除带有两个子结点的结点是最复杂的。比如说现在要删除 56。 

那 52 和 61 要怎么处理呢?显然不能将它们都放到 56 原本的位置上,还需要第三条规则。

 如果要删除的结点有两个子结点,则将该结点替换成其后继结点。一个结点的后继结点, 就是所有比被删除结点大的子结点中,最小的那个。 

再来看一个更复杂的删除,这次我们删除根结点。

 

现在需要找后继结点来填补根的位置。 首先,访问右子结点,然后一路往左下方向移步,直至没有左子结点的结点上。 

这就找出后继结点 52 了,接着我们将其填到被删除结点的位置上。

删除完成!

然而,还有一种情况,那就是后继结点带有右子结点。让我们回到根被删除之前的状态,并且给 52 加上一个右子结点。 

如此一来,就不能只将后继结点 52 移到根那里了,因为这样会使其子结点 55 悬空。

于是, 我们再加一条关于删除的规则。

 如果后继结点带有右子结点,则在后继结点填补被删除结点以后,用此右子结点替代后继结点的父节点的左子结点。 

以下为二叉树的删除算法的所有规则。

 如果要删除的结点没有子结点,那直接删掉它就好。

 如果要删除的结点有一个子结点,那删掉它之后,还要将子结点填到被删除结点的位置上。

 如果要删除的结点有两个子结点,则将该结点替换成其后继结点。一个结点的后继结点, 就是所有比被删除结点大的子结点中,最小的那个。

 如果后继结点带有右子结点,则在后继结点填补被删除结点以后,用此右子结点替代后继结点的父节点的左子结点。

以下是用 Python 写的二叉树递归式删除算法。 

def delete(valueToDelete, node): 
 # 当前位置的上一层无子结点,已到达树的底层,即基准情形
 if node is None: 
 return None
# 如果要删除的值小于(或大于)当前结点,
 # 则以左子树(或右子树)为参数,递归调用本方法,
 # 然后将当前结点的左链(或右链)指向返回的结点
 elif valueToDelete < node.value: 
 node.leftChild = delete(valueToDelete, node.leftChild) 
 # 将当前结点(及其子树,如果存在的话)返回,
 # 作为其父结点的新左子结点(或新右子结点)
 return node 
 elif valueToDelete > node.value: 
 node.rightChild = delete(valueToDelete, node.rightChild) 
 return node 
 # 如果要删除的正是当前结点
 elif valueToDelete == node.value: 
 # 如果当前结点没有左子结点,
 # 则以右子结点(及其子树,如果存在的话)替换当前结点成为当前结点之父结点的新子结点
 if node.leftChild is None: 
 return node.rightChild 
 # 如果当前结点没有左子结点,也没有右子结点,那这里就是返回 None 
 elif node.rightChild is None: 
 return node.leftChild 
 # 如果当前结点有两个子结点,则用 lift 函数(见下方)来做删除,
 # 它会使当前结点的值变成其后继结点的值
 else: 
 node.rightChild = lift(node.rightChild, node) 
 return node 
def lift(node, nodeToDelete): 
 # 如果此函数的当前结点有左子结点,
 # 则递归调用本函数,从左子树找出后继结点
 if node.leftChild: 
 node.leftChild = lift(node.leftChild, nodeToDelete) 
 return node 
 # 如果此函数的当前结点无左子结点,
 # 则代表当前结点是后继结点,于是将其值设置为被删除结点的新值
 else: 
 nodeToDelete.value = node.value 
 # 用后继结点的右子结点替代后继结点的父节点的左子结点
 return node.rightChild 

跟查找和插入一样,平均情况下二叉树的删除效率也是 O(log N)。因为删除包括一次查找, 以及少量额外的步骤去处理悬空的子结点。有序数组的删除则由于需要左移元素去填补被删除元 素产生的空隙,最终导致 O(N)的时间复杂度。 

总结

二叉树是一种强大的基于结点的数据结构,它既能维持元素的顺序,又能快速地查找、插入 和删除。尽管比它的近亲链表更为复杂,但它更有用。

值得一提的是,树形的数据结构除了二叉树以外还有很多种,包括堆、B 树、红黑树、2-3-4 树等。它们也各有自己适用的场景。

图是一种善于处理关系型数据的数据结构,使用它可以很轻松地表示数据之间是如何关联的。

广度优先搜索

图有两种经典的遍历方式:广度优先搜索和深度优先搜索。

我们在 Person 类里加上 display_network 方法,以广度优先搜索的方式展示一个人的关 系网里所有的名字。

class Person 
 attr_accessor :name, :friends, :visited 
 def initialize(name) 
 @name = name 
 @friends = [] 
 @visited = false
 end 
 def add_friend(friend) 
 @friends << friend 
 end 
 def display_network 
 # 记下每个访问过的人,以便算法完结后能重置他们的 visited 属性为 false 
 to_reset = [self] 
 # 创建一个开始就含有根顶点的队列
 queue = [self] 
 self.visited = true
 while queue.any? 
 # 设出队的顶点为当前顶点
 current_vertex = queue.shift 
 puts current_vertex.name 
 # 将当前顶点的所有未访问的邻接点加入队列
 current_vertex.friends.each do |friend| 
 if !friend.visited
to_reset << friend 
 queue << friend 
 friend.visited = true 
 end 
 end 
 end 
 # 算法完结时,将访问过的结点的 visited 属性重置为 false 
 to_reset.each do |node| 
 node.visited = false
 end 
 end 
end

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值