目录
二叉树
- 满二叉树,完全二叉树;二叉搜索树(有序树,左<根<右),平衡二叉搜索树(左右子树高度差最大为1)
- 存储方式:链式(节点元素,左右指针)和顺序存储(序号一层一层顺序编号,节点i,左孩子2i+1,右孩子2i+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; } }
2024.3.28
二叉树的递归遍历
(144 前序遍历 94 中序遍历 145 后序遍历 )
递归三部曲:
- 确定递归函数的参数和返回值
- 确定终止条件
- 确定单层递归的逻辑:主要是三个的顺序不一样
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result=new ArrayList<>();
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);
}
2024.3.29
非递归遍历(栈)
前序遍历:中左右,入栈顺序(中-右-左)
List<Integer> result=new ArrayList<>();
if(root==null){
return result;
}
Deque<TreeNode> deque = new ArrayDeque<>();
deque.push(root);
while(!deque.isEmpty()){
TreeNode node=deque.pop();
result.add(node.val);
if(node.right!=null){
deque.push(node.right);
}
if(node.left!=null){
deque.push(node.left);
}
}
return result;
后序遍历:左右中 将左和右换顺序,再将最后的输出颠倒顺序
注:List中使用size()获取列表长度,并使用.get()获取元素,set(i,num) 将num赋值给下标为i的元素,设置元素。
int temp;
for(int i=0,j=result.size()-1;i<j;i++,j--){
temp=result.get(i);
result.set(i,result.get(j));
result.set(j,temp);
}
中序遍历:访问的节点和处理的节点不一样,需要另外一个cur节点指向当前处理的元素
注:创建栈:
- Stack<Integer> stack = new Stack<>();
- Deque<Integer> stack = new ArrayDeque<>();
- Deque<Integer> stack = new LinkedList<>();
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); //为空时,将中节点pop出并加入结果中;开始遍历右节点
cur=cur.right;
}
}
return result;
二叉树的统一迭代法
(前中后序遍历都只需要变换几行代码的顺序即可)
将遍历过但是还没有处理的节点在入栈的时候后面设置一个null节点;当栈不为空时,每次先把栈顶的节点弹出,最后加入的时候再设置null。注意:入栈的顺序与遍历的顺序相反
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
2024.3.31
102 二叉树的层序遍历(队列)
注:创建队列
- Queue<String> queue = new LinkedList<>(); 或者
PriorityQueue
、ArrayDeque
等实现类 - Deque<String> deque = new ArrayDeque<>();
- Deque<String> deque = new LinkedList<>();
创建二级列表 List<List<Integer>> resList=new ArrayList<List<Integer>>();
首先判断root是否为空,不为空的话就创建一个队列,将该节点加入队列;
当队列不为空的时候,要新建一个列表将这一层的值存储,获取当前队列的长度size,就是当前层的节点数;如果size>0,就一直把队列中的第一个节点poll出并加入列表中,并看当前左右子节点是否为空,不为空加入栈中,并将size--;最后将这个列表加入二级列表中;继续下一层的判断和遍历。
2024.4.1
226 翻转二叉树
递归中的顺序特别重要
中序的话会将子树重复翻转,可以换成在中后面继续交换左子树的左右子节点,因为对于中间的,左右子树一交换就把左子树交换到了右边,因此不能再交换右子树了(最好不用)
前后序就是定义一个交换函数,然后左右子树递归遍历。
如果用层序遍历,就把节点加入链表的那一行代码改成节点的左右子树交换即可。
2024.4.2
101 对称二叉树
即判断左右两个子树是否可以相互翻转。需要收集左右孩子的信息,然后返回给父节点,才能知道是否是对称二叉树,所以需要后续遍历。
一个返回值为boolean的compare函数,确定参数为左右两个子树,结束条件:左空右不空,左不空右空;左右都空,左右不空但不相等,这时只剩下一种相等的情况,递归比较左的左和右的右以及左的右和右的左,最后如果两个都为true则返回true。(这时说明以这两个为节点的树是互相翻转的)
2024.4.3
104 二叉树的最大深度(后序)
- 深度:每个节点到根节点的距离
- 高度:每个节点到叶子节点的距离
根节点的高度其实就是二叉树的最大深度(采用 后序遍历:左右中)
递归,迭代,层序均可;递归后序遍历时,每次到中 取左右孩子高度的最大值+1,一层一层往上返回数值。
559 n叉树的最大深度
一样的递归
int depth=0;
if(root.children!=null){
for(Node i:root.children){
depth=Math.max(depth,maxDepth(i));
}
}
return depth+1;
111 二叉树的最小深度(后序)
根节点到叶子节点的最短路径;即根节点的最小高度
误区:不能像最大深度取左右子树的最大值一样取最小值,因为可能会出现左子树为空,但是右不为空,这时候其实最小深度并不是0,取最小就会导致为0。
if(root.left==null && root.right!=null){
return right+1;
}
if(root.left!=null && root.right==null){
return left+1;
}
return Math.min(left,right)+1;
222 完全二叉树的节点个数
每一层都被填满,直到最后一层;如果最后一层不满,节点都应该在左边。一个节点也称为满二叉树;递归到左右子树为满二叉树时,就可以直接计算节点(2^n-1)的数量。
如何判断是不是满二叉树:完全二叉树中,如果递归向左遍历的深度等于递归向右遍历的深度,就是满二叉树。
if(root==null){
return 0;
}
TreeNode left=root.left;
TreeNode right=root.right;
int leftDepth=0,rightDepth=0;
while(left!=null){
left=left.left;
leftDepth++;
}
while(right!=null){
right=right.right;
rightDepth++;
}
if(leftDepth==rightDepth){
return (2<<leftDepth)-1; //注意(2<<1) 相当于2^2,所以leftDepth初始为0,<<相当于是乘于2的n次方
}
int leftCount=countNodes(root.left);
int rightCount=countNodes(root.right);
return leftCount+rightCount+1; //递归遍历:左右中
2024.4.4
110 平衡二叉树
使用一个递归函数,使用后序遍历求节点的高度;首先递归计算左右子树的高度,如果高度是-1,就return -1(因为其中一个子树是-1,说明不是平衡二叉树,那么整个都不是,就要一直将-1返回);如果当前节点的左右子树高度都不是-1且差的绝对值>1,那么就不是平衡二叉树,则返回-1;如果是平衡二叉树,就返回当前节点的高度(左右子树高度的最大值+1),以便下一次的比较判断。
257 二叉树的所有路径(前序)
回溯:回溯和递归是一一对应的,有一个递归,就要有一个回溯
本题中要有两个列表,分别存储遍历的这条路径的节点和最终的结果;使用前序遍历,在递归中,终止条件为遍历的这个节点为叶子节点,满足终止条件就要将paths中的元素转换为字符串形成一个路径,因此要先将该节点加入paths中,否则就会漏掉;在满足终止条件的时候只转换就可以。后面在进行左右的时候,当左/右叶子节点不为空的时候再进行递归,这样每次就不用判断节点是否为空;每次递归(会加入一个元素到paths中)完之后会回溯到上一个节点,即将paths中的最后一个节点移除。
public List<String> binaryTreePaths(TreeNode root) {
List<String> res=new ArrayList<>(); //存最终结果
if(root==null){
return res;
}
List<Integer> paths=new ArrayList<>();
traversal(root,paths,res);
return res;
}
private void traversal(TreeNode root, List<Integer> paths, List<String> res) {
paths.add(root.val);
if(root.left==null && root.right==null){
//终止条件,输出
StringBuilder sb=new StringBuilder();
for(int i=0;i<paths.size()-1;i++){
sb.append(paths.get(i)).append("->");
}
sb.append(paths.get(paths.size() - 1));// 添加最后一个节点
res.add(sb.toString());// 收集一个路径
return;
}
// 递归和回溯是同时进行,所以要放在同一个花括号里
if(root.left!=null){
traversal(root.left,paths,res);
paths.remove(paths.size()-1); //回溯
}
if(root.right!=null){
traversal(root.right,paths,res);
paths.remove(paths.size()-1); //回溯
}
}
2024.4.5
404 左叶子之和
遍历的话很容易找到是否是叶子节点,但是不知道其是左叶子还是右叶子节点,所以最好是遍历到其父节点,这个节点的左子节点不为空,并且左子节点的左右孩子都为空,这时候就是找到这个左子节点是左叶子
使用后序遍历,将左叶子的和一层一层返回;当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。
if(root==null){
return 0;
}
int leftValue=sumOfLeftLeaves(root.left);
if(root.left!=null&&root.left.left==null&&root.left.right==null){
leftValue=root.left.val;
}
int rightValue=sumOfLeftLeaves(root.right);
int sum=leftValue+rightValue;
return sum;
513 找树左下角的值
找到树的最后一行最左边的值;也就是保证优先左边搜索,然后记录深度最大的叶子节点就是。
本题没有中节点的处理逻辑,因此前中后序都可以。递归:
private int Deep=-1;
private int result=0;
public int findBottomLeftValue(TreeNode root) {
findLeftValue(root,0);
return result;
}
private void findLeftValue(TreeNode root,int deep){
if(root.left==null&&root.right==null){
//如果是叶子节点,就更新最大深度和值;不是就继续递归
if(deep>Deep){
result=root.val;
Deep=deep;
}
}
if(root.left!=null){
deep++;
findLeftValue(root.left,deep);
deep--; //回溯
}
if(root.right!=null){
deep++;
findLeftValue(root.right,deep);
deep--; //回溯
}
}
最后可以简化为(隐藏着回溯,因为deep的值并没有改变,只是每次在方法中传入的值增加)
if(root.left!=null){
findLeftValue(root.left,deep+1);
}
if(root.right!=null){
findLeftValue(root.right,deep+1);
}
层序遍历迭代:
Queue<TreeNode> queue = new LinkedList<>(); //队列先进先出
queue.offer(root);
int res=0;
while(!queue.isEmpty()){
int size=queue.size(); //获取每一层的节点个数,方便后续出队列
for(int i=0;i<size;i++){
TreeNode node=queue.poll();
if(i==0){
res=node.val; // i=0 即第一个节点,就是这一层最左边的节点
}
if(node.left!=null){ //每poll出一个节点,就将该节点的下一层左右孩子加入队列
queue.offer(node.left);
}
if(node.right!=null){
queue.offer(node.right);
}
}
}
return res;
2024.4.6
112 路经总和
这里也不涉及中节点的处理,前中后序都可以
每次都要先判断root==null时return false,算有没有一条路经总和等于给定值,可以每次递归传入的总和是原来的总和减去上一个节点的值,这样到叶子节点的时候就可以判断当前传入的总和与叶子节点的val是否相等,相等则true 向上传递,都不满足返回false。
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root==null){
return false;
}
if(root.left==null && root.right==null ){
return targetSum==root.val;
}
if(hasPathSum(root.left,targetSum-root.val)){
return true;
}
if(hasPathSum(root.right,targetSum-root.val)){
return true;
}
return false;
}
2024.4.7
106 从中序与后序遍历序列构造二叉树
- 后序数组为null,返回null
- 后序数组最后一个元素为根节点
- 寻找中序数组的位置作为切割点
- 切中序数组
- 切后序数组(根据中序切出来的左中序的长度来切)
- 递归处理左右区间
总体不改变序列长度,只是每次递归时传入开始位置和结束位置作为参数。左闭右开区间保持一致。将中序存储在map中,方便查找每次递归时根节点的位置,通过后序找到根节点,再找到根节点在中序中的索引位置,构造树root;计算中序中左中序的长度,以便在后续中分割。
2024.4.8
105 从前序与中序遍历序列构造二叉树
思路一样,根节点在前序的前面位置
2024.4.9
108 将有序数组转换为二叉搜索树
取根节点之后,如果左右子树长度是偶数的话,取最中间的左边还是右边作为根节点都是可以的,可以构造不同的二叉树;明确递归的左右区间定义
public TreeNode sorted(int[] nums,int left,int right){
if(left>=right){
return null;
}
int mid=left + (right - left) / 2; //这样写是为了防止left和right都是最大int,(left+right)/2就越界了,在二分法 中尤其需要注意!这里是数组的下标,不存在这个问题
TreeNode root=new TreeNode(nums[mid]);
root.left=sorted(nums,left,mid);
root.right=sorted(nums,mid+1,right);
return root;
} 最后在主函数中调用这个递归函数
543 二叉树的直径
直径就是路径经过节点数的最大值-1,而任意一条路径均可以被看作由某个节点为起点,从其左儿子和右儿子向下遍历的路径拼接得到。即左子树的最大深度+右子树的最大深度+1(该节点)为路径经过的节点个数,用result进行更新记录最大值,递归函数的返回值是该节点的最大深度
int result;
public int diameterOfBinaryTree(TreeNode root) {
result=1;
depth(root);
return result-1; //路径长度等于节点个数-1
}
public int depth(TreeNode node){
if(node==null){
return 0;
}
int L=depth(node.left);
int R=depth(node.right);
result=Math.max(result,L+R+1); //记录更新最长路径的节点个数
return Math.max(L,R)+1; //返回该节点为根的最大深度
}
2024.4.10
98 验证二叉搜索树
- 可以采用中序遍历,如果遍历出的数组单调递增,就说明是一个二叉搜索树。
- 如果要设置一个比较值进行节点值的比较,最好用
private long prev = Long.MIN_VALUE;
因为-2^31 <= Node.val <= 2^31-1,可以取到int的最小值(防止从最开始时候就不满足prev<节点的值,直接返回false。中序遍历正常情况下一个新节点的值要比prev大,才能是二叉搜索树)
误区:对于每个节点不能只判断它左子节点的值比他小,右子节点的值比他大,因为一个搜索二叉树要满足其左右子树的所有节点的值都比它小或者大!!!
- 引入一个前驱节点 pre,每次新的节点与他的前驱节点比较大小,中序递归
TreeNode pre=null;
public boolean isValidBST(TreeNode root) {
if(root==null){
return true;
}
boolean left=isValidBST(root.left); //左
if(pre!=null && pre.val>=root.val){ //中(第一次pre为null,直接将root给pre)
return false;
}
pre=root;
boolean right=isValidBST(root.right); //右(这里没有再次判断是因为每次到右子树就会递归上面的两行代码)
return left && right;
}
2024.4.11
236 二叉树的最近公共祖先
要想从下往上找,并向上返回,可以使用后序遍历递归
因为题目中说每个节点的值都不一样,并且p!=q,因此有两种情况:
- p和q分别在一个子树的左右子树中,那么左和右遍历结果都不为空(左和右中必分别有p和q),则可以返回该root
- 如果这个root就是p或q,那么就返回root,不用管该节点的下面是不是有另一个值;如果有就是向上传递root;如果没有,就又成为了第一种情况。
左右分别递归,到中时,判断左右是否为空,分为四种情况。函数返回值是最近公共祖先节点(如果在最下面找到了公共祖先怎么返回到最上面:这样的话另一边子树肯定没有出现即为null,这时候就返回不是null的节点;如果一个在左子树的最下面,一个在右子树的最下面,两边不为null,到是最近公共祖先的时候,就会返回这个公共祖先节点)
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root==null){
return root;
}
if(root==p || root==q){
return root;
}
TreeNode left=lowestCommonAncestor(root.left,p,q);
TreeNode right=lowestCommonAncestor(root.right,p,q);
if(left!=null && right!=null){
return root;
}
else if(left==null && right!=null){
return right;
}
else if(left!=null && right==null){
return left;
}
else{
return null;
}
}
2024.4.12
(之后都是hot100的题)
230 二叉搜索树中第k小的元素
采用中序遍历,用栈存储节点,后进先出,root不为空或者栈不为空的时候进行循环,如果root不为空,就要将root进栈,一直向左遍历;然后将栈顶元素给pop,这时候pop出的就是比较小的节点,k--,判断k是否等于0,若等于 break结束循环,若不等,将root赋值为其右节点(相当于有个回溯的过程,pop出这个节点,就要把其右孩子进栈,进行循环判断)
public int kthSmallest(TreeNode root, int k) {
Deque<TreeNode> stack=new ArrayDeque<TreeNode>();
while(root!=null || !stack.isEmpty()){
while(root!=null){
stack.push(root);
root=root.left;
}
root=stack.pop();
k--;
if(k==0){
break;
}
root=root.right;
}
return root.val;
}
199 二叉树的右视图
从右边看这个树,从顶到底的顺序输出节点的值;相当于是层序遍历中每一层的最后一个元素存到List中。队列(先将root进入,记录队列的size,root出来时将其左右子节点进入,出一个节点将左右子节点进入,并且size--,为0的时候说明这一层结束,并且将此时的poll出的节点的值加入到列表中;不为0的时候就循环poll出节点,offer子节点进)
public List<Integer> rightSideView(TreeNode root) {
List<Integer> list=new ArrayList<>();
if(root==null){ //二叉树为空的时候,返回的是一个空的列表
return list;
}
Queue<TreeNode> queue=new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
int size=queue.size();
while(size!=0){
TreeNode node=queue.poll();
size--;
if(size==0){
list.add(node.val);
}
if(node.left!=null){ //这里要先判断是否为空节点,是空节点就不加入了
queue.offer(node.left);
}
if(node.right!=null){
queue.offer(node.right);
}
}
}
return list;
}
2024.4.13
114 二叉树展开为链表
题目中说 展开后的单链表应该与二叉树先序遍历顺序相同
所以先前序遍历将节点加入到列表中,之后用pre和cur两个指针分别向后移动将pre的left为null,right为cur节点。
public void flatten(TreeNode root) {
List<TreeNode> list=new ArrayList<>();
pre(root,list);
int size=list.size();
for(int i=1;i<size;i++){
TreeNode pre=list.get(i-1),cur=list.get(i);
pre.left=null;
pre.right=cur;
}
}
public void pre(TreeNode root,List list){
if(root!=null){
list.add(root);
pre(root.left,list);
pre(root.right,list);
}
}
2024.4.14
※437 路经总和 Ⅲ
定义节点的前缀和为:由根结点到当前结点的路径上所有节点的和。curr-targetSum能在HashMap中找到,说明从找到的那个节点的下一个节点到当前节点的路经总和为targetSum。
使用HashMap存储,key为节点的前缀和,value为和出现的次数。
在遍历的时候记录之前每个节点的前缀和,用前序中左右递归;用res记录curr-targetSum在HashMap中出现的次数,出现几次即为几条路经总和为targetSum。
- 对于当前节点,先判断是否为空,每次都定义res=0。(是为了计算当前节点之前的路径中路径总和为目标的次数)
- 之后计算出到根节点的路经总和curr。
- 在HashMap中找curr-targetSum是否存在,存在的话就将其value给res,不存在则取0;
- 左右子节点递归,并将其返回值累加到res上(这样就会将每个节点之前路径中出现的次数加在一起向上传递直至root)
- 最后还要进行回溯,将这个节点的curr值出现从HashMap中删去,以免产生干扰。
public int pathSum(TreeNode root, int targetSum) {
Map<Long,Integer> prefix=new HashMap<Long,Integer>();
prefix.put(0L,1);
return dfs(root,prefix,0,targetSum);
}
public int dfs(TreeNode root,Map<Long,Integer> prefix,long curr,int targetSum){
if(root==null){
return 0;
}
int res=0;
curr+=root.val;
res=prefix.getOrDefault(curr-targetSum,0);
prefix.put(curr,prefix.getOrDefault(curr,0)+1);
res+=dfs(root.left,prefix,curr,targetSum);
res+=dfs(root.right,prefix,curr,targetSum);
prefix.put(curr,prefix.getOrDefault(curr,0)-1);
//回溯到上一节点,以消除当前节点对这个累加和计数的影响。
return res;
}
※124 二叉树中的最大路径和(难)
节点的最大贡献值:在以该节点为根节点的子树中寻找以该节点为起点的一条路径,使得该路径上的节点值之和最大。
- 空节点的最大贡献值等于0。
- 非空节点的最大贡献值等于节点值与其子节点中的最大贡献值之和(对于叶节点而言,最大贡献值等于节点值)。
因为任意一条路径,都可以看做这个节点的左右子树中路径相连。则该节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值,如果子节点的最大贡献值为正,则计入该节点的最大路径和,否则不计入该节点的最大路径和。
后序遍历,先看左右子树再考虑加不加入当前节点的最大路径和中
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
public int maxGain(TreeNode node){
if(node==null){
return 0;
}
// 递归计算左右子节点的最大贡献值
// 只有在最大贡献值大于 0 时,才会选取对应子节点;因为负数会让和减小,因此这个节点就不考虑,将贡献值设为0加入路径和中
int leftGain=Math.max(maxGain(node.left),0);
int rightGain=Math.max(maxGain(node.right),0);
// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
int priceNewpath=node.val+leftGain+rightGain; //一次次传递过来的就是左右子树中路径和的最大值,就看加上这个节点的值以后是更大还是变小了(左右节点也可以连接成路径,所以要进行判断是不是最大的和)
maxSum=Math.max(priceNewpath,maxSum); //对于每个节点,更新这个节点处的最大路径和
// 返回节点的最大贡献值
return node.val+Math.max(leftGain,rightGain); //用于一次一次的递归,这里向上传递的就是这个子树中路径最大值
}
有部分代码随想录的二叉树的题没有刷,先把hot100里的刷了,之后再补吧!