二叉树
一个普通的链表里,每一个结点会包含一个连接自身和另一结点的链。树也是基于结点的数据结构,但树里面的每个结点,可以含有多个链分别指向其他多个结点。
以下是一棵典型的树。
谈论树的时候,我们会用到以下术语。
最上面的那一结点(此例中的“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