在之前刷力扣的过程中,我每刷道题都是先看题解然后再去边写边看,很多内容根本没消化,在进度上欺骗自己,感觉之前刷的都忘了,能力没有什么提升,现在还是一道都不会做。所以这次报了卡哥的算法训练营,希望能够按计划地有效刷题。本次刷题,我用的是 Java 语言解题,有余力的话也可能会加上 C++ 的题解。以下是算法训练营第十四天的刷题记录和思考笔记。
一、二叉树理论基础 二叉树的种类 满二叉树:如果一棵二叉树只有度为 0 的结点和度为 2 的结点,并且度为 0 的结点在同一层上,则这棵二叉树为满二叉树(深度为 k, 有 $2^k-1$ 个节点的二叉树)。
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1 ~ $2^{h-1}$ 个节点。
二叉搜索树是有数值的数,二叉搜索树是一个有序树:
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树。
平衡二叉搜索树:又被称为 AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了 1。
二叉树的存储方式 二叉树可以链式存储,也可以顺序存储。
顺序存储的方式就是用数组,如果父节点的数组下标是 $i$,那么它的左孩子就是 $i \times 2 + 1$,右孩子就是 $i \times 2 + 2$:
链式存储则是通过指针把分布在各个地址的节点串联一起:
二叉树的遍历方式 二叉树主要有两种遍历方式(图论中最基本的两种遍历方式):
深度优先遍历:先往深走,遇到叶子节点再往回走。
广度优先遍历:一层一层的去遍历。
在深度优先遍历中:有三个顺序,前中后序遍历, 这里前中后,其实指的就是中间节点的遍历顺序,
二叉树的定义 二叉树的定义 和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子:
1 2 3 4 5 6 7 8 9 10 11 12 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; } }
二、二叉树的递归遍历 递归算法的三个要素:
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Solution { public List<Integer> preorderTraversal (TreeNode root) { List<Integer> result = new ArrayList <Integer>(); preorder(root, result); return result; } public void preorder (TreeNode root, List<Integer> result) { if (root == null ) { return ; } result.add(root.val); preorder(root.left, result); preorder(root.right, result); } } class Solution { public List<Integer> inorderTraversal (TreeNode root) { List<Integer> res = new ArrayList <>(); inorder(root, res); return res; } void inorder (TreeNode root, List<Integer> list) { if (root == null ) { return ; } inorder(root.left, list); list.add(root.val); inorder(root.right, list); } } class Solution { public List<Integer> postorderTraversal (TreeNode root) { List<Integer> res = new ArrayList <>(); postorder(root, res); return res; } void postorder (TreeNode root, List<Integer> list) { if (root == null ) { return ; } postorder(root.left, list); postorder(root.right, list); list.add(root.val); } }
三、二叉树的迭代遍历 递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
因此,我们可以用迭代法(非递归的方式)来实现二叉树的前后中序遍历。
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { public List<Integer> preorderTraversal (TreeNode root) { List<Integer> result = new ArrayList <>(); if (root == null ){ return result; } Stack<TreeNode> stack = new Stack <>(); stack.push(root); while (!stack.isEmpty()){ TreeNode node = stack.pop(); result.add(node.val); if (node.right != null ){ stack.push(node.right); } if (node.left != null ){ stack.push(node.left); } } return result; } }
中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进 result 数组中),这就造成了处理顺序和访问顺序是不一致的。那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { public List<Integer> inorderTraversal (TreeNode root) { List<Integer> result = new ArrayList <>(); if (root == null ){ return result; } Stack<TreeNode> stack = new Stack <>(); TreeNode cur = root; while (cur != null || !stack.isEmpty()){ if (cur != null ){ stack.push(cur); cur = cur.left; }else { cur = stack.pop(); result.add(cur.val); cur = cur.right; } } return result; } }
再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转 result 数组,输出的结果顺序就是左右中了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution { public List<Integer> postorderTraversal (TreeNode root) { List<Integer> result = new ArrayList <>(); if (root == null ){ return result; } Stack<TreeNode> stack = new Stack <>(); 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; } }