概念
二叉树的种类
主要分为 满二叉树 和 完全二叉树
满二叉树: 如果一棵树上只有度为0 和 度为2 的结点,并且度为0的结点都在同一层上,那么这颗树就叫做满二叉树
完全二叉树: 如果一棵树上除了最底层结点没有填满,其余层的结点数都达到了最大值,且最底层结点都聚集于该层最左边的若干位置,那么这颗树叫做完全二叉树,堆就是一个完全二叉树。
二叉搜索树: 之前介绍的树都是没有数值的,而二叉搜索树是一个有数值的树,且是有序树
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
平衡二叉搜索树: 又称AVL(Adelson-Velsky and Landis)树,他是一个空树,或者他的左右子树高度差的绝对值不超过1,并且左右两个子树都是平衡二叉树。
二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。
那么链式存储方式就用指针, 顺序存储的方式就是用数组
二叉树的遍历方式
二叉树的主要两种遍历方式:
- 深度优先遍历:先往深处走,遇到叶子结点在往回走
- 广度优先遍历:一层一层遍历
前中后序遍历: 这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。
看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式
- 前序遍历:中左右
- 中序遍历:左中右
- 后序遍历:左右中
刷题
递归遍历
- 二叉树的前序遍历
golang函数的参数是值拷贝的,所以在递归中将result变量作为递归函数的参数导入,result变量不会被修改,所以使用指针的参数传递。
或者利用无名函数,直接访问preoderTraversal内的result变量,这个变量也可以定义在函数返回的变量里
- 二叉树的后序遍历
- 二叉树的中序遍历
二叉树的迭代遍历
- 二叉树的前序遍历
这道题主要是将上面的递归遍历变成利用栈这个数据结构来做迭代遍历,golang没有现成的栈数据结构库,这里利用container/list双向队列来模拟栈的操作。
上篇文章说过,递归操作在计算机中也是使用栈来存储实现的。主要思路:
- 前序遍历:中左右
- 中序遍历:左中右
- 后序遍历:左右中
这里的中就是一个结点,左右分别代表左右子树
- 前序遍历: 将root压入栈,取栈顶(root)作为结果的第一个,分别压入右孩子和左孩子(栈的取值是相反的),取栈顶(root左孩子)作为加入result切片中,压入右孩子和左孩子(如果孩子为空就不加),直到栈为空,得到result切片
- 二叉树的中序遍历
- 中序遍历:从root开始遍历每一个节点的左孩子,并将左孩子节点压入栈中,直到没有左孩子,取栈顶加入result切片中,并检测该节点是否存在右孩子,如果有,压入栈中,并继续遍历新节点的左孩子,压入栈中(和刚才一样),如果没有,继续取栈顶,直到栈为空。
这里很巧妙的把遍历 左孩子 的操作写到了一起,值得再看看。
- 二叉树的后序遍历
- 后序遍历:利用空节点作为标记,按顺序将节点,空节点,右孩子,左孩子压入栈中,空节点作为标记,遇到空节点不遍历下一个元素,直接将val加入result切片中。遍历从栈中取出节点,直到栈为空。
还有一种写法:就是利用前序遍历,前序遍历为中 左 右, 后序遍历可以是 中 右 左,然后翻转。
二叉树的统一迭代法
和之前的后序遍历一样,使用一个空指针,作为已经遍历的节点的标记,遇到空指针,取出两个栈顶,把第二个作为result的元素记录,直到栈为空。
前序遍历这样写有点脱裤子放屁了。
中序遍历,发现这样写的好处就和标题一样,统一写法,一种思路,三种方式都能做出来,牛!
二叉树的层序遍历
- 二叉树的层序遍历
第一种方法: 利用先入先出队列
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
第二种方法:使用递归的方法
每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
本题中,递归函数的参数是树的深度(就是result的第一层数组的下标)x int, 和 node *treeNode, 没有返回值。result作为递归函数外的变量来调用。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
本题中,终止条件就是node == nil, 直接返回
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
本地中,单层逻辑是将遍历的node节点的Val加入到指定result[x]切片的后面,如果没有result[x],新建一个。 递归left 和 right两个节点。
- 二叉树的层序遍历II
先入先出队列,这道题其实可以把102倒转过来就是107题目的结果了
递归
199.二叉树的右视图
这道题用队列最方便,存储空间比递归法使用更小,不需要全部遍历
- 二叉树的层平均值
依然队列
429.N叉树的层序遍历
依然队列
- 在每个树行中找到最大值
依然队列
- 填充每个节点的下一个右侧节点指针
使用队列
- 填充每个节点的下一个右侧节点指针
用切片代替container/list的双向队列,速度更快
- 二叉树的最大深度
切片实现队列
- 二叉树的最小深度
我使用递归实现,这里有一个注意的地方,是root节点到叶子节点的最小距离,叶子节点的判断条件是left和right节点都是nil。 使用队列也可以轻松实现
总结
[强制类型转换]一般用于对已知类型(非any类型)进行高低精度之间的转换,如从 int 转换为 int64, 或者 int转换为 uint , float32转换为 float64 这类的转换, 这类型的转换较为简单,
语法: 类型( 变量名 )
类型断言一个专门用于any类型(也就是 interface{}空接口类型,他们是一个东西)的[数据类型转换]的
语法: 变量名.(类型)