Appearance
第17讲:树结构及经典考题剖析
本课时我们主要讲解树结构,以及剖析树结构相关的考题。
树结构定义
首先,我们先来看下什么是树结构,树结构的定义是如果有一种数据,它既有自己的核心字段,同时又有指向它的若干个子节点,其中子节点下面又可以嵌套更多的子节点,这样便形成了一定的层级关系,这种结构看起来像是一颗翻转过来的树,所以被称为树结构。
如图所示,在树结构里,A 节点包含了 B 节点和 C 节点,B 节点下面又包含了三个其他的子节点,数据一层一层往下扩展。
在树结构里还有一种特例,就是二叉树。二叉树是指所有的节点最多只能有两个子节点,这种树结构我们便称之为二叉树。二叉树是面试中经常会被问到的考题,但是对普通的树形结构的处理反而是我们测试中经常会用到的。所以二叉树的处理和普通树结构的处理,你都需要掌握。
那么普通的树结构在什么地方会用到呢?比如我们打开一个网站,这个网站有很多页面,这些页面又都是层级逻辑关系,也就是进入一层界面处理一层界面的操作逻辑,然后又进入另外一个界面,所有的界面的层级便构成了一个复杂的树结构。产品经理在设计 App 或网站的时候,也是一个层级结构。除此之外,还有很多我们测试中用到的结构其实也都是树结构。
二叉树
讲完了树结构,我们来了解到底什么是二叉树。
树结构其实是由一个核心的数据和一堆指向节点的链接组成。二叉树则是树结构的一种特例,二叉树每个节点下最多只能有两个子节点,所以我们可以使用 left、right 进行表示。如果有更多的子树,你还可以使用 self.children,把所有子节点放到 children 中进行描述。
class BTree:
def __init__(self,data=None):
self.data = data
self.left = None
self.right = None
# slef.children =[]
本课时我们就先以二叉树的例子来给你讲解树结构,以及如何做相关的常见的数据处理。
了解了二叉树的基本定义,接下来我们从零开始创建一个二叉树。
让我们回到 IDE,创建一个 BTree 的类,和链表类似,我们先创建出一个树结构。树结构有一个核心的数据,那这个数据从哪里来呢?当然是需要我们传入进来,所以我们定义一个参数,这里为了简单方便直接给定一个默认值。
二叉树最多有两个节点,所以我们分别使用 left 及 right 作为代表。如果还有更多的子节点则使用 children,在 children 中其实使用了一个结构,如果它里面有节点,我们就可以往里面插入更多的节点,所以说不同的树结构是非常相似的。
class TestBTee:
def test_create(self)
我们先创建一个测试用例。在这里构建一个二叉树描述我们今天的 demo 数据。
class TestBTee:
"""
0
1 2
3 4 | 5 6
none none | none none | none none | 7 none|
"""
def test_create(self):
我们先来描述下我们想要的二叉树结构。0 下面有 1、2 两个节点,1 下面又分 3、4 两个节点,2 下面又分 5、6 两个节点,其中 6下面只有一个节点 7。
好,那么这个结构相信你已经理解了,接下来我们去创建这样的一个结构。
def test_create(self):
tree=BTree(0)
tree.left=BTree(1)
tree.right=BTree(2)
tree.left.left=BTree(3)
tree.left.right=BTree(4)
tree.right.left=BTree(5)
tree.right.right=BTree(6)
tree.right.right.left=BTree(7)
首先,我们使用一个 tree 变量,让其等于 BTree(0),你可以从底层开始进行创建,也可以从上层开始创建,我们就先从上层开始构建,过程参考代码。
当然这样创建树形结构打的字比较多,你也可以自己写一个构建函数来模拟这个操作,我这里就先临时这么写,简单构建一个小的二叉树,它一共有 4 层。
def travel(self):
print(self.data)
print(self.left)
print(self.right)
构建完二叉树后,我们第一个操作就是打印这个树形结构,我们使用 travel,那么 travel 怎么对树进行遍历呢?我们先写一个最简单的方法,首先打印根节点,然后再打印它的左节点和右节点。
但是这样打印,我们只能打印出来一层,可是下面还有很多层,它是一个层级关系,每个子节点又是一个新的树。所以你觉不觉得处理起来像递归用法呢?接下来我们就写一个递归的算法遍历树形结构,我们把它改造成一种递归。
def travel(subtree):
print(subtree.data)
print(subtree.left)
print(subtree.right)
首先,self 不可能一直都是根节点,它是需要再找到子树的,所以我们将 self 改为 subTree。但是这样只是完成了第一步,它还可以支持传参。这样就完成了一个递归思路,通过这个办法,我们可以先打印核心的根节点,然后再打印 left 和 right,而在打印 left 的时候,left 可能又有新的子节点,所以又把它传递一次递归,这样就打印了左节点的数据,接着再打印左节点下面的左节点。
def travel(self,subtree):
if subtree is None:
return
print(subtree.data)
self.travel(subtree.left)
那么,什么时候终止呢?这里我们需要添加一个递归的终止条件,这个终止条件就是 if subtree is none,如果是 none 的话,就直接退出;如果不是的话,就直接打印数据。
tree.travel(tree)
我们写完这个算法之后就需要检查数据到底对不对,所以这里写一个 tree.travel,因为它是参数化的,所以需要传参,这里传入的是二叉树自身。
这个函数看起来有点啰嗦,我们可以再写一个新的函数,把整个调用封装起来,把它改造成内部的函数。
@classmethod
def travel(cls,subtree):
if subtree is None:
return
print(subtree.data)
cls.travel(subtree.left)
cls.travel(subtree.right)
classTestBTee:
"""
0
1 2
3 4 | 5 6
none none | none none | none none | 7 none|
"""
def test_create(self):
tree=BTree(0)
tree.left=BTree(1)
tree.right=BTree(2)
tree.left.left=BTree(3)
tree.left.right=BTree(4)
tree.right.left=BTree(5)
tree.right.right=BTree(6)
tree.right.right.left=BTree(7)
BTree.travel(tree)
或者,你也可以在这儿加一个 @classmethod,把 self 换成 cls,这样效果也是一样的。这样就可以使用 BTree 方法打印了。
整个遍历的过程需要我们注意,它根据根节点打印的先后顺序,分为前序、中序和后序。前序指的是核心数据节点先打印出来,而核心数据如果放在左右节点的中间就叫作中序,其次是后序。
我们来看下打印的结果值,因为是按照前序遍历打印数据,所以第一个打印的是根节点 0,其次是左节点 1,然后是左节点的左节点 3,以此类推,结果按照0、 1、2、4、5、6、7 全部打印出来。
@classmethod
def travel(cls,subtree):
if subtree is None:
return
cls.travel(subtree.left)
print(subtree.data)
cls.travle(subtree.right)
如果现在我们把这个节点放到中间呢?再次执行你会注意到这个时候变成了 3、1、4、0、5、2、7、6。这是因为我们此时是按照中序在遍历。
打印的过程中先去寻找左节点,第一个左节点是 3,3 打印完是 1,然后再寻找右节点,接着是 4,最后的结果便是 3、1、4。4 打印完后是 0,以此类推,你可以课后自己推导结果,这里就不再一一介绍了。
树形结构考题
一个简单的树的遍历,差不多就是这样一个写法,接下来我们剖析一个经典的树结构面试考题。
怎样获取一个树结构的最大深度?其实我们在一些测试工作中有时候也经常会用到这个算法。比如计算某个控件在当前界面中它属于多大的一个深度。接下来我们看下具体怎么解答这个问题。
首先,我们写一个叫作 max depth 的函数,然后我们用递归算法来计算深度。这个深度是这样子的,首先如果它即有左节点,又有右节点,那么每进去一层深度其实是要加 1 的。最后完成之后,我们计算加 1 的值最后能加到多少?你可以看到它的算法本质和遍历是一样的。我们看下它的写法是什么?
def max_depth(self,subtree,depth):
if subtree is None:
return depth
self.max_depth(subtree.left,depth+1)
self.max_depth(subtree.right,depth+1)
return max_left if max_left > max_right else max_right
我们梳理下思路,首先是通过递归,因为每一次递归都会查找一个子树,所以我们需要有个树的节点传递给函数。left 或 right 就是这时被传入函数的,在传入的过程中,因为深度加 1 了,所以 depth 也需要加 1。然后通过一个终止条件,判断返回的时机,如果 left 或 right 为空就直接返回,如果不为空就进行上面说的逻辑处理,直到找到最后一层子树并返回结果。
def setup(self):
self.tree=BTree(0)
self.tree.left=BTree(1)
self.tree.right=BTree(2)
self.tree.left.left=BTree(3)
self.tree.left.right=BTree(4)
self.tree.right.left=BTree(5)
self.tree.right.right=BTree(6)
self.tree.right.right.left=BTree(7)
然后,我们验证下这个 case。
def test_travel(self):
BTree.travel(self.tree)
def test_max_depth(self):
print(self.tree.max_depth(self.tree,0))
我们再创建一个测试用例和树来测试它的深度,首先 self.tree 前面已经创建好了,这个 tree 的 max_depth 也需要传入一个子树,这里传入 self.tree。下面是获取深度的逻辑,一开始的初始值是 0,每进一层它就加 1,最后打印下最后的值是多少。
def test_max_depth(self):
assert self.tree.max_depth(self.tree,0) == 4
self.tree.right.right.left.left = BTree(8)
assert self.tree.max_depth(self.tree,0) == 5
我看深度好像是 4,那么接下来我们开始写断言来证明它的值到底是不是 4。
所以通过这个办法,我们可以看到,我们就写出来一个关于 BTree 二叉树的一个经典的遍历算法,当然你觉得这个调用特别烦琐的话,你还可以创建一个新的函数,比如说:
def get_max_depth(self):
self.max_depth(self,0)
这样使得调用变得更简单。
本课时的内容就讲到这里了,其实核心就是一个树形结构,它的遍历跟链表很相似,子节点跟父节点是类似的算法处理,所以我们可以使用递归算法计算树形的深度。