二叉树的理论基础
二叉树的种类
满二叉树
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
深度为k,有2^k-1个节点的二叉树
完全二叉树
完全二叉树的定义如下:
在完全二叉树中,
- 除了最底层节点可能没填满外,其余每层节点数都达到最大值,
- 并且最下面一层的节点都集中在该层最左边的若干位置。
- 若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
可以发现第三幅图中的二叉树并不满足完全二叉树中的条件二,因此并不是完全二叉树。
二叉搜索树
二叉搜索树是有数值的了,二叉搜索树是一个有序树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
平衡二叉搜索树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:
- 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,
- 并且左右两个子树都是一棵平衡二叉树。
最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。
二叉树的储存方式
二叉树可以链式存储,也可以顺序存储。
链式储存
链式存储方式就用指针, 通过指针把分布在散落在各个地址的节点串联一起。
顺序储存
顺序存储的方式就是用数组。
顺序存储的元素在内存是连续分布的,
用数组来存储二叉树如何遍历的呢?
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
总结:
用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
但是要了解,用数组依然可以表示二叉树。
二叉树的遍历方式
二叉树主要有两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历。
那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:
- 深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历
- 层次遍历(迭代法)
在深度优先遍历中:
前中后,其实指的就是中间节点的遍历顺序,前中后序指的就是中间节点的位置。
- 前序遍历:中左右「中间节点在前」
- 中序遍历:左中右「中间节点在中」
- 后序遍历:左右中「中间节点在后」
且都是先遍历左边节点再遍历右边节点。
做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。
- 而栈其实就是递归的一种是实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。
广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
链式储存二叉树构造写法
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
二叉树的递归遍历
递归算法
递归的三要素:
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
二叉树前序遍历
public List<Integer> PreOrder(TreeNode root){
List<Integer> result = new ArrayList<>();
PreOderHelper(root, result);
return result;
}
private void PreOderHelper(TreeNode root, List<Integer> result) {
//终止条件
if(root == null){
return;
}
//单步操作
result.add(root.val); //根节点
//递归 - 先左节点,后右节点
PreOderHelper(root.left, result); //左节点
PreOderHelper(root.right, result); //右节点
}
二叉树中序遍历
public List<Integer> InOrder(TreeNode root){
List<Integer> result = new ArrayList<>();
InOderHelper(root, result);
return result;
}
private void InOderHelper(TreeNode root, List<Integer> result) {
//终止条件
if(root == null){
return;
}
//递归 - 先左节点,后右节点
InOderHelper(root.left, result); //左节点
result.add(root.val); /根节点
InOderHelper(root.right, result);//右节点
}
二叉树后序遍历
public List<Integer> PostOrder(TreeNode root){
List<Integer> result = new ArrayList<>();
PostOderHelper(root, result);
return result;
}
private void PostOderHelper(TreeNode root, List<Integer> result) {
//终止条件
if(root == null){
return;
}
//递归 - 先左节点,后右节点
PostOderHelper(root.left, result); //左节点
PostOderHelper(root.right, result);//右节点
result.add(root.val); //根节点
}
二叉树的迭代遍历
二叉树的前序遍历
前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,因此访问的元素和要处理的元素顺序是一致的,都是中间节点。
因此这里如果采用栈来进行遍历的话,只需要按照 中间节点 - 右节点 - 左节点 这个顺序,将二叉树的元素放入栈中就可以;注意栈中存放的是节点,而不是节点的值
为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
因为处理的节点就是中间节点,先访问的节点也是中间节点;
因此先出栈的节点就是中间节点。
如何移动指针呢?这里没有采用虚拟指针 - 而是把出栈的节点当作这个循环中需要遍历的小三角。
循环的思路:
- 初始化栈 - 将根节点放入栈
- 循环开始:将栈顶元素弹出栈 --- 处理节点
- 把栈顶元素的值加入list
- 查找栈顶元素的右节点,有则压入栈
- 查找栈顶元素的左节点,有则压入栈
- 直到栈为空,循环结束。
代码实现:
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if(root == null) return result;
LinkedList<TreeNode> stack = new LinkedList<>();
stack.push(root);//初始化栈 - 把根节点放入
while(!stack.isEmpty()){
TreeNode node = stack.pop();//弹出栈顶节点
result.add(node.val);//把节点值放入list
if(node.right != null) stack.push(node.right);//把右节点压入栈
if(node.left != null) stack.push(node.left);//把左节点压入栈
}
return result;
}
二叉树的中序遍历
中序遍历的时候,中序遍历是左中右,先访问的是二叉树顶部的节点,直到到达树左面的最底部,再开始处理节点,处理顺序和访问顺序是不一致的。
因此这里需要将处理节点和访问节点分开。
在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
循环思路其实和递归思路就非常像了:
- 判断节点是否为null,如果不是null,则将节点压入栈,然后把指针指向节点的左孩子
- 如果节点为null,则说明已经走到最底部,可以开始处理节点了 - 将栈顶元素弹出,把栈顶元素值放入list,然后把指针指向栈顶元素的右孩子。
- 一直到栈中没有元素且节点为null,循环结束
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if(root == null) return result;
LinkedList<TreeNode> stack = new LinkedList<>();
while(!stack.isEmpty() || root != null){
if(root != null){
stack.push(root);//栈中压入节点
root = root.left;//指针向左孩子移动
}else{//处理节点
root = stack.pop();
result.add(root.val);
root = root.right;//指针向右孩子移动
}
}
return result;
}
二叉树的后序遍历
后序遍历根据先前对前序遍历的处理可以得到一个很巧妙的解决思路:
先序遍历是中左右,后续遍历是左右中
只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中
实现代码:
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if(root == null) return result;
LinkedList<TreeNode> stack = new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if(node.left != null) stack.push(node.left);
if(node.right != null) stack.push(node.right);
}
Collections.reverse(result);//集合翻转
return result;
}
参考资料:代码随想录
ps: 统一的遍历方式就先放一放。
二叉树的层序遍历
与二叉树层序遍历相关的力扣题:
- 102.二叉树的层序遍历
- 107.二叉树的层次遍历II
- 199.二叉树的右视图
- 637.二叉树的层平均值
- 429.N叉树的层序遍历
- 515.在每个树行中找最大值
- 116.填充每个节点的下一个右侧节点指针
- 117.填充每个节点的下一个右侧节点指针II
- 104.二叉树的最大深度
- 111.二叉树的最小深度
102.二叉树的层序遍历
题目:力扣
层序遍历也就是广度遍历 - 肯定要用到队列
循环的思路:
- 初始化队列 - 将根节点放入队列中
- 循环开始:
- 若队列不为空,则先判断当前队列的size作为层循环的次数
- 然后弹出队头元素,放入list
- 若元素有左孩子,则放入队列;元素有右孩子则放入队列
- 层循环结束
- 一直到队列长度为0,整个循环结束
代码实现:
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if(root == null) return result;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int size = queue.size();//计算单层node的个数
List<Integer> list = new ArrayList<>();//使用list储存单层node
for(int i=0; i<size; i++){
TreeNode node = queue.poll();
list.add(node.val);
//储存下一层node
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
result.add(list);//将单层node的list放入result
}
Collections.reverse(result);//集合翻转API
return result;
}
107.二叉树层序遍历二
题目:力扣
这道题和上一道题的区别是,这里要求从底层往上输出二叉树的遍历顺序。
其实可以用上一道题的思路找到集合后再翻转集合就可。
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if(root == null) return result;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int size = queue.size();//计算单层node的个数
List<Integer> list = new ArrayList<>();//使用list储存单层node
for(int i=0; i<size; i++){
TreeNode node = queue.poll();
list.add(node.val);
//储存下一层node
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
result.add(list);//将单层node的list放入result
}
Collections.reverse(result);//集合翻转API
return result;
}
如果不用API进行翻转集合:
List<List<Integer>> result = new ArrayList<>();
for (int i = list.size() - 1; i >= 0; i-- ) {
result.add(list.get(i));
}
return result;
199.二叉树的右视图
题目:力扣
经过层序遍历,可以发现,结果集中的每层的list最后的元素,也就是每一层最右侧的元素。
因此还是采用层序遍历的思路,但是最后结果中只需要存入每层最后的元素。
public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
if(root == null) return result;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int size = queue.size();
result.add(queue.peekLast().val);//将该层最后的元素放入结果
for(int i=0; i<size; i++){
TreeNode node = queue.poll();
//储存下一层的元素
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
}
return result;
}
637.二叉树的平均值
题目:力扣
同样这道题也是通过层序遍历然后获得单层的平均值。
这里需要注意的是,在储存每层的总数sum的时候不要使用int整型来储存,可能会存在数太大溢出的情况,可以考虑用long或者double来储存sum。
public List<Double> averageOfLevels(TreeNode root) {
List<Double> result = new ArrayList<>();
if(root == null) return result;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
double sum = 0;
for(int i=0; i<size; i++){
TreeNode node = queue.poll();
sum += (double) node.val;
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
result.add(sum/size);
}
return result;
}
429.N叉树的遍历
题目:力扣
同样适用层序遍历的方式,将下一层的node存入二叉树的地方并不是光存入左右节点。
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
if(root == null) return result;
LinkedList<Node> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int size = queue.size();//计算单层node的个数
List<Integer> list = new ArrayList<>();//使用list储存单层node
for(int i=0; i<size; i++){
Node node = queue.poll();
list.add(node.val);
//储存下一层node
List<Node> childrens = node.children;
for(Node children: childrens){
queue.add(children);
}
}
result.add(list);//将单层node的list放入result
}
return result;
}
515.每个树行中的最大值
题目:力扣
同样是层序遍历,在储存下一层元素的时候顺便更新这层元素的最大值。
注意初始化 单层最大值不能用0,而是需要用Integer.MIN_VALUE,因为二叉树中的值可能存在负数。
public List<Integer> largestValues(TreeNode root) {
List<Integer> result = new ArrayList<>();
if(root == null) return result;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int size = queue.size();
int maxL = Integer.MIN_VALUE;
for(int i=0; i<size; i++){
TreeNode node = queue.poll();
maxL = Math.max(maxL, node.val);
//储存下一层的元素
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
result.add(maxL);
}
return result;
}
116.填充每一个节点的下一个右侧指针
这道题也可以采用递归,判断中间节点是否存在左右孩子,有的话就让左孩子的下一个指针指向右孩子。
public Node connect(Node root) {
//判断这个节点是否为空,或者这个节点的左子节点是否为空 - 因为是一个完美二叉树,所以每个节点肯定会有两个子节点,如果没有,则说明下面是没有节点的,可以直接返回了
if(root == null || root.left == null){
return root;
}
//这个root的左子节点的next节点是root的右子节点
root.left.next = root.right;
//并且如果这个root存在next节点
//那么这个root的右子节点的next节点是root的next节点的左子节点
if(root.next != null){
root.right.next = root.next.left;
}
//同样操作root的左子节点,以及右子节点
connect(root.left);
connect(root.right);
return root;
}
而当采用层序遍历,就是把存入到队列中的元素弹出然后将其指针指向下一个元素即可。
这里需要注意的是需要先把每一层的头节点先提出来,此时不要忘记将头节点的左右孩子放入队列中。
public Node connect(Node root) {
if(root == null || root.left == null && root.right == null) return root;
LinkedList<Node> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int size = queue.size();//计算单层node的个数
Node cur = queue.poll();
//储存下一层node
if(cur.left != null) queue.add(cur.left);
if(cur.right != null) queue.add(cur.right);
for(int i=1; i<size; i++){
Node node = queue.poll();
//储存下一层node
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
//连接右节点
cur.next = node;
cur = node;
}
}
return root;
}
117.填充每个节点的下一个右侧节点指针II
题目:力扣
这道题如果用递归就会比116题更加麻烦,因为需要判断节点的右节点是哪个,因为不是完全二叉树。因此需要先用递归寻找下一个右节点,然后先连接右边的节点,再连接左边的节点。
public Node connect(Node root) {
if(root == null || (root.left == null && root.right == null)){
return root;
}
if(root.left != null && root.right != null){
root.left.next = root.right;
}
if(root.left != null && root.right == null){
root.left.next = getNext(root.next);
}
if(root.right != null){ //这里左侧也可能有点的
root.right.next = getNext(root.next);
}
//需要先遍历右边,因为如果先遍历左边,那么会有左边还没有和右边建立起联系的情况,那么左边有些节点就会找不到next节点,存在错误
connect(root.right);
connect(root.left);
return root;
}
public Node getNext(Node root){
if(root == null){
return null;
}
if(root.left != null){
return root.left;
}
if(root.right != null){
return root.right;
}
//这个地方需要注意一下,如果是特别长的分支,那么可能有子节点的在最左边和最右边,中间隔了好几个节点,因此需要用递归来找
if(root.next != null){
return getNext(root.next);
}
return null;
}
而如果用层序遍历来解决,则会发现这道题和116题会是一模一样的代码:
public Node connect(Node root) {
if(root == null || root.left == null && root.right == null) return root;
LinkedList<Node> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int size = queue.size();//计算单层node的个数
Node cur = queue.poll();
//储存下一层node
if(cur.left != null) queue.add(cur.left);
if(cur.right != null) queue.add(cur.right);
for(int i=1; i<size; i++){
Node node = queue.poll();
//储存下一层node
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
//连接右节点
cur.next = node;
cur = node;
}
}
return root;
}
104.二叉树的最大深度
题目:力扣
这道题可以用层序遍历来做,遍历出来的层数就是结果。
public int maxDepth(TreeNode root) {
if(root == null) return 0;
int depth = 0;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
depth++;
int size = queue.size();//计算单层node的个数
for(int i=0; i<size; i++){
TreeNode node = queue.poll();
//储存下一层node
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
}
return depth;
}
111. 二叉树的最小深度
题目:力扣
同样可以用层序遍历来做,但是当遍历到有一个节点的左右孩子都不存在的时候,层序遍历就改停止了,这就是最小深度。
public int minDepth(TreeNode root) {
if(root == null) return 0;
int depth = 0;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
depth++;
int size = queue.size();//计算单层node的个数
for(int i=0; i<size; i++){
TreeNode node = queue.poll();
//判断是否 - 到达最小深度
if(node.left == null && node.right == null) return depth;
//储存下一层node
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
}
return depth;
}
参考资料:代码随想录