![e4ecc4508c70bb586d84d8fda9e226e3.png](https://i-blog.csdnimg.cn/blog_migrate/b1a0379cdec6d2e0c58fb5510b378737.jpeg)
通常在开始学编程的时候,你会接触一些常用数据结构。
到最后一般会学到哈希表。对于修读计算机科学学位的朋友,你通常要上专门的数据结构课,从了解有关链表、队列和栈的各种知识。这些统称为线性数据结构,因为依逻辑次序从头排到尾。
当你开始进入下一阶段,学习树和图结构时,事情就会显得比处理线性数据结构复杂很多。这促使我们专门写一篇文章来探讨“树”这种特定的数据结构帮大家答疑解惑。
本文内容包括:
- 树的定义
- 树的结构
- 工作原理
- 代码实现
现在就开始学习吧 :)
树的定义
通常对编程新手来说,线性数据结构比树和图要更好理解。我们此处所说的树,即是以层次化方式组织和存放数据的特定数据结构。
实例解析
为了理解“层次化”的意思,我们以家谱为例:里面有祖父母、父母、孩子、兄弟姐妹。这就是用层次化的模式来构建家谱。
![c292eb48316469b81a7ec5bdcf145166.png](https://i-blog.csdnimg.cn/blog_migrate/3e9d722f9390188e7a65bbd823b34996.jpeg)
上图就是我的家谱。Tossico,Akikazu,Hitomi和Takemi作为我的祖父母和外祖父母处于最顶层。Toshiaki 和 Juliana是我父母。TK,Yuji,Bruno 和 Kaio 则是我和我的的兄弟姐妹们。
![29f700f442a158b368ae31c64164928a.png](https://i-blog.csdnimg.cn/blog_migrate/815a9d39b2dc06507f8a68339c92f1d6.jpeg)
公司组织也是类似的层次化结构
![12566cb19d2e64e4b585e21783e435bc.png](https://i-blog.csdnimg.cn/blog_migrate/94d7700795f9de639fd9aba75bb819aa.jpeg)
HTML的文档模型对象 (DOM) 就是一棵树最顶层 HTML 标签连接到 head 标签和 body 标签。二者又有对应的子标签,比如 head 含有 meta 和 title 标签,body 含有与可视化内容相关的 h1, a, li等标签。名词定义
树(tree):是以边(edge)相连的结点(node)的集合,每个结点存储对应的值(value/data),当存在子结点时与之相连。
![09ffaebfd01f03c5ad96966e52da6516.png](https://i-blog.csdnimg.cn/blog_migrate/62985c355f4a4f9e31a4bc33d3fb8287.jpeg)
根结点(root):是树的首个结点,在相连两结点中更接近根结点的成为父结点(parent node),相应的另一个结点称为子结点(parent node)。
![cd478d697062887a3b8824a3849782fd.png](https://i-blog.csdnimg.cn/blog_migrate/76db022ccaee9ad46f42e99d1e05bbca.jpeg)
边(edge):所有结点都由边相连,用于标识结点间的关系。边是树中很重要的一个概念,因为我们用它来确定节点之间的关系。
![f30c4fed9b2ddf7bb7145769524b602e.png](https://i-blog.csdnimg.cn/blog_migrate/af2e42e5abd3bda581d06a5f516cd84e.jpeg)
叶子结点(Leaves):是树的末端结点,他们没有子结点,就像真实的树那样 ,由根开始,伸展枝干,到叶为止。
![e682adb11f50e1ae842663f00273437e.png](https://i-blog.csdnimg.cn/blog_migrate/275fcd625c91c99b2bb7b4b30203fb41.jpeg)
树高(height)与结点深度(depth)也是很重要的概念。树高:是由根结点出发,到子结点的最长路径长度。结点深度:是指对应结点到根结点路径长度。
二叉树
现在来探讨一种特殊的树结构-二叉树(binary tree),它每个节点最多有两个子结点,亦称左孩子和右孩子。
在计算机科学中,二叉树是一种“树”数据结构,树上的每个节点最多有两个孩子,分别为左孩和右孩。——维基百科
来看一个二叉树的实例。
![1f1ebbd568d238990f419f46bb10a04e.png](https://i-blog.csdnimg.cn/blog_migrate/2247fbc8c7d03180c4e9a66c764d337f.jpeg)
动手写二叉树
首先明确我们要实现的对象是一个结点集合,每个结点有三个属性:值(value), 左孩子(left_child)和右孩子(right_child)。
写出来会是这个样子:
![10b615f9fd466d8e33e3e7f18398b822.png](https://i-blog.csdnimg.cn/blog_migrate/081127943cdf73e6e96e283ff41b800f.jpeg)
我们写了一个BinaryTree类,在初始化实际对象的时候传入对应值,并在此时还没有子结点的情况下将左右孩子设为空。
为什么要这么做呢?
因为当我们创建节点的时候,它还没有孩子,我们只有节点数据。
让我们测试一下:
![30ec640803fbaa5272ecde64bbb4028c.png](https://i-blog.csdnimg.cn/blog_migrate/9d2133679234c774877c8e42967c9435.jpeg)
下面到了插入结点的操作:在树还没有对应子结点时新建结点,并赋值给现有结点对应变量。否则,新建结点连接并替换掉现有位置子结点。
画出来是这个样子:
![1f1ebbd568d238990f419f46bb10a04e.png](https://i-blog.csdnimg.cn/blog_migrate/2247fbc8c7d03180c4e9a66c764d337f.jpeg)
相应代码(左右相同):
![08ad8a234c4e8a5ac242ec860c0277e7.png](https://i-blog.csdnimg.cn/blog_migrate/1b51c32e1157268ea83344b1a07221f9.jpeg)
为了进一步测试,让我们构建一个更复杂一些的树:
![format,png](https://i-blog.csdnimg.cn/blog_migrate/c36c1216ed08be389e3fcb7c8821e343.png)
这棵树共有六个结点,其中结点b没有左孩子。对应初始化并插入结点的代码如下:
![27c1173c35a0534d1272f3a33965f070.png](https://i-blog.csdnimg.cn/blog_migrate/95bc9f3623d8c7e3b26c3265de6a2960.jpeg)
下一步让我们看看如何对树进行遍历。
一般来讲我们有两种遍历方式:深度优先遍历(DFS)和 广度优先遍历(BFS),前者沿着特定路径遍历到根结点再转换临近路径继续遍历,后者逐层遍历整个树结构。来看具体的例子:
深度优先遍历 (DFS)
DFS会沿特定路径遍历到叶子结点再回溯 (backtracking)进入临近路径继续遍历。以下面的树结构为例:
![e033ec4fdafdfcd581c429013518571c.png](https://i-blog.csdnimg.cn/blog_migrate/b1465b88d88af7957b8fee5e89329c7c.jpeg)
具体来讲,我们会先访问根结点1再访问其左孩子2,接着是2的左孩子3,到达叶子结点回溯一步,访问2的右孩子4,进一步回溯,继续顺序访问5,6和7。在输出遍历结果时,据父结点值相对子结点输出顺序的不同,深度优先遍历又可细分为先序、中序和后序遍历三种情况。
先序遍历
即直接按照我们对结点的访问顺序输出遍历结果即实现,父结点值被最先输出。代码:
![7325f0106d05c9342bb6d57ca3b35747.png](https://i-blog.csdnimg.cn/blog_migrate/71e2e75b849ace2a19d8adfab390d905.jpeg)
中序遍历
![e033ec4fdafdfcd581c429013518571c.png](https://i-blog.csdnimg.cn/blog_migrate/b1465b88d88af7957b8fee5e89329c7c.jpeg)
中序遍历输出结果为:3–2–4–1–6–5–7。
左孩子值最先输出,然后是父结点,最后是右孩子。对应代码如下:
![958a186c82ce80fdaeb0d91ece24e878.png](https://i-blog.csdnimg.cn/blog_migrate/361289b9078a49cfbc7bc0a616be2346.jpeg)
后序遍历
![e033ec4fdafdfcd581c429013518571c.png](https://i-blog.csdnimg.cn/blog_migrate/b1465b88d88af7957b8fee5e89329c7c.jpeg)
后序遍历输出结果为:3–4–2–6–7–5–1.
左右孩子值依次输出,最后是父结点,对应代码如下:
![a475165842e84093d3ecdca109096c0e.png](https://i-blog.csdnimg.cn/blog_migrate/cc5220dff0662a2e420d18befdfbf6cd.jpeg)
广度优先搜索 (BFS)
BFS:按照结点深度逐层遍历树结构。
![e543e91c80335eed6d212bf2f34ab9f4.png](https://i-blog.csdnimg.cn/blog_migrate/bd783628e689b0d41a825e01e8d33f31.jpeg)
再拿上面的图来实际解释这种方法:
![e033ec4fdafdfcd581c429013518571c.png](https://i-blog.csdnimg.cn/blog_migrate/b1465b88d88af7957b8fee5e89329c7c.jpeg)
逐层每层从左到右进行遍历,对应遍历结果为:1–2–5–3–4–6–7。对应代码如下:
![191b184d893a8cf86f31b55fc3645959.png](https://i-blog.csdnimg.cn/blog_migrate/2c521fdaf99e155e65042653850c409f.jpeg)
你应该已经注意到了,我们要借助先进先出(FIFO)的队列(queue)结构完成操作,具体的出入队列顺序如下图所示:
![format,png](https://i-blog.csdnimg.cn/blog_migrate/29ba6ccc1f35575c74411ae0f0764408.png)
二叉搜索树
二叉搜索树又名有序二叉树,结点元素按固定次序排布,使得我们可以在进行查找等操作时使用二分搜索提高效率。——维基百科
它最明显的特征是父结点值大于左子树任意结点值,小于右子树任意结点值。
![59e2a6698db9736e21b011a1dd442739.png](https://i-blog.csdnimg.cn/blog_migrate/d9cd7d842c1fa213f5156673ce042c56.jpeg)
上图以三个二叉树为例,哪个才是正确的呢?
- A 左右子树需要进行交换。
- B 满足条件,是二叉搜索树。
- C 值为4的结点需要移至3的右孩子处
接下来进行二叉搜索插入、结点检索、结点删除以及平衡的解释。
插入
假设以这种顺序插入结点: 50, 76, 21, 4, 32, 100, 64, 52。50会是我们初始的根结点。
![d5b6bf2b9be191b7185ff298ff0f5c7a.png](https://i-blog.csdnimg.cn/blog_migrate/3c32c5a26c875e74ea7355dc422284dd.jpeg)
再依次进行如下操作:
- 76 大于50,置于右边
- 21 小于 50, 置于左边
- 4 置于21左边
最终一气呵成我们会得到下面这棵树:
![aba7301b2006460731aeecabc9c16d47.png](https://i-blog.csdnimg.cn/blog_migrate/58870ae5829dc5b8ee64d4892c88270d.jpeg)
发现规律了么?像开车一样,从根结点驶入,待插入值大于当前结点值向右开否则向左开知道找到空位停车入库。(嘀嘀嘀,老司机)
代码实现也很简单:
![328e632fac7ec8d4e68cc6f9f264790a.png](https://i-blog.csdnimg.cn/blog_migrate/14e1ce0e83cce31b19c01324743b4f43.jpeg)
这个算法最牛逼的地方在于他的递归部分,你知道是哪几行吗?
结点检索
其实结合我们的插入操作,检索的方法就显而易见,依旧从根结点开始,小于对应结点值左转,大于右转,等于报告找到,走到叶子结点都没找到 gg,就报没有该元素。例如我们想知道下图中有没有52这个值:
![25718a50467caa9de280abd916a0f68a.png](https://i-blog.csdnimg.cn/blog_migrate/90855e2cfc80c355ea18d01f5bc40094.jpeg)
代码如下:
![4927d059513e812948b8bfe39e09b150.png](https://i-blog.csdnimg.cn/blog_migrate/d1a043f3834a12e931c9163db93d67a0.jpeg)
删除: 移除并重构
删除操作要更复杂一些,因为要处理三种不同情况:
- 情景 #1:叶子结点
![7eec6b4fd763ab2cba3c56d2f09b48c4.png](https://i-blog.csdnimg.cn/blog_migrate/543b475bead5761f4c20f5295ffeb1b8.jpeg)
是最简单的情况,直接删除就好.
- 情景 #2:只有左孩子或右孩子
![2fc2533a3d17683af13a5a984338cdf4.png](https://i-blog.csdnimg.cn/blog_migrate/48d4aafe67df8bab2e296af7ed5875a7.jpeg)
该情景等价于链表上的结点删除,把当前结点删除并让其子结点替换自己原来位置。
- 情景 #3:同时具有左右孩子的结点
![c70927314dc687e0c2272de802284634.png](https://i-blog.csdnimg.cn/blog_migrate/a74606b4d8e333756c2321d187d74b5b.jpeg)
找到该结点右子树中最小值所在的结点,剔除要删除的当前结点并把最小值结点提升到空缺位置。
一些别的骚操作
清零:将三个属性全部置None即可。
![55de0983fde30eadfa10b39f25e3283f.png](https://i-blog.csdnimg.cn/blog_migrate/19e363382c6b1005fd05b6769688a3fb.jpeg)
找到最小值:从根节点开始,一直左转,直到找不到任何结点为止,此时我们就找到了最小值。
![b92d7e4263ea8186b479f2310302d1c0.png](https://i-blog.csdnimg.cn/blog_migrate/54774edde9d73fd0e4bbb2e21283185a.jpeg)
恭喜你学完本篇内容!数据结构中的树的内容大致如此,赶紧收藏起来吧
投稿:黄猛击
参考: https:// medium.freecodecamp.org /all-you-need-to-know-about-tree-data-structures-bceacb85490c
知乎机构号:来自硅谷的终身学习平台——优达学城(http://Udacity.com),专注于技能提升和求职法则,让你在家能追随 Google、Facebook、IBM 等行业大佬,从零开始掌握数据分析、机器学习、深度学习、人工智能、无人驾驶等前沿技术,激发未来无限可能!
优达学城cn.udacity.com![6f51c5221371a15e199b424fa135a61e.png](https://i-blog.csdnimg.cn/blog_migrate/0990b5f886c729a7533d1dc7413da175.png)
知乎专栏:优达技术流,每天分享来自行业大牛、工程师必读的技术干货
优达技术流zhuanlan.zhihu.com![5d13decfe711cff775ddbbc2df4d42b4.png](https://i-blog.csdnimg.cn/blog_migrate/174a2b7e53137628f7e2376cda8bf447.jpeg)