目录
二叉树的前中后序遍历是什么,仅仅是三个顺序不同的 List 吗
认识二叉树解法
对于二叉树的两种解法模式
- 1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个
traverse
函数配合外部变量来实现,这叫「遍历」的思维模式。对应着回溯算法的核心- 2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。对应着动态规划的核心
- 核心:如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。
深入理解二叉树的前中后序遍历
二叉树的前中后序遍历是什么,仅仅是三个顺序不同的 List 吗
- 二叉树的三种遍历,并不只是不同遍历顺序,它更重要的一种思想是在于我们对二叉树结点处理的时机
- 前序位置的代码是在刚刚进入一个二叉树结点的时候进行执行
- 后序位置的代码是将要离开一一个二叉树结点的时候执行
- 中序位置的代码是处理好二叉树左子树,即将要开始遍历右子树的时候进行的
后序遍历有什么特殊之处
- 前序位置本身其实没有什么特别的性质,之所以你发现好像很多题都是在前序位置写代码,实际上是因为我们习惯把那些对前中后序位置不敏感的代码写在前序位置罢了。
- 但是后序遍历有非常大的玄妙,就是在递归的时候,前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。
- 对应着后序位置的代码是自底向上,前序遍历的代码是自顶向下
为什么多叉树没有中序遍历
- 因为二叉树的每个节点只会进行唯一一次左子树切换右子树,而多叉树节点可能有很多子节点,会多次切换子树去遍历,所以多叉树节点没有「唯一」的中序遍历位置。
二叉树的重要性
举个例子,比如两个经典排序算法 快速排序 和 归并排序,对于它俩,你有什么理解?
为什么快速排序和归并排序能和二叉树扯上关系?我们来简单分析一下他们的算法思想和代码框架:
快速排序的逻辑是,若要对
nums[lo..hi]
进行排序,我们先找一个分界点p
,通过交换元素使得nums[lo..p-1]
都小于等于nums[p]
,且nums[p+1..hi]
都大于nums[p]
,然后递归地去nums[lo..p-1]
和nums[p+1..hi]
中寻找新的分界点,最后整个数组就被排序了。void sort(int[] nums, int lo, int hi) { /****** 前序遍历位置 ******/ // 通过交换元素构建分界点 p int p = partition(nums, lo, hi); /************************/ sort(nums, lo, p - 1); sort(nums, p + 1, hi); }
- 先构造分界点,然后去左右子数组构造分界点,你看这不就是一个二叉树的前序遍历
若要对
nums[lo..hi]
进行排序,我们先对nums[lo..mid]
排序,再对nums[mid+1..hi]
排序,最后把这两个有序的子数组合并,整个数组就排好序了。// 定义:排序 nums[lo..hi] void sort(int[] nums, int lo, int hi) { int mid = (lo + hi) / 2; // 排序 nums[lo..mid] sort(nums, lo, mid); // 排序 nums[mid+1..hi] sort(nums, mid + 1, hi); /****** 后序位置 ******/ // 合并 nums[lo..mid] 和 nums[mid+1..hi] merge(nums, lo, mid, hi); /*********************/ }
- 先对左右子数组排序,然后合并(类似合并有序链表的逻辑),你看这是不是二叉树的后序遍历框架?另外,这不就是传说中的分治算法嘛,不过如此呀。
- 说了这么多,旨在说明,二叉树的算法思想的运用广泛,甚至可以说,只要涉及递归,都可以抽象成二叉树的问题。
总结
- 遇到一道二叉树的题目时的通用思考过程是:是否可以通过遍历一遍二叉树得到答案?如果不能的话,是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案? 如果需要设计到子树信息, 建议使用后续遍历.
104 二叉树的最大深度
- 首先我们想一下,能不能通过遍历全部的子节点来得到答案呢?好像是可以的,我们遍历每一个结点,然后通过记录每一个结点深度,弄一个外部的变量来记录最大的深度,在进入一个结点,增加一个深度(在前序位置写),在离开这个结点的时候,减少一个深度(后序位置写)
class Solution { int maxDepth; int length=0; public int maxDepth(TreeNode root) { helper(root); return maxDepth; } private void helper(TreeNode root) { if (root==null){ return; } //前序位置 length++; helper(root.left); //中序位置 helper(root.right); //后序位置 maxDepth=Math.max(maxDepth,length); length--; } }
- 其次我们看看能不能通过分解为子问题来解决这个问题,我们知道一棵树的最大的深度是左右子树的最大深度+当前根节点(1),那我们的递归的含义就是输入一个根节点,就能得到这个根节点的最大深度
class Solution { public int maxDepth(TreeNode root) { return helper(root); } /* 传入一颗根节点就能得到最大深度 */ private int helper(TreeNode root) { if (root==null){ return 0; } int left=helper(root.left); int right=helper(root.right); //后序位置 return Math.max(left,right)+1; } }
543二叉树的直径
- 这道题的意思就是一个根节点左子树的最大深度+右子树的深度,很简单,就是用递归来写,其注意点就是将求最大深度和求最大直径放在一起,都是在后序部分写,因为是为了得到子树的信息
class Solution { int max; public int diameterOfBinaryTree(TreeNode root) { helper(root); return max; } /** * 这个递归函数的语义是一个根节点最大深度 * @param root * @return */ private int helper(TreeNode root) { if (root==null){ return 0; } //前序位置 int left= helper(root.left); int right= helper(root.right); //后序位置 max=Math.max(left+right,max); return Math.max(left,right)+1; } }
94二叉树的中序遍历
- 中序位置的意义就是,处理好了左子树,即将开始遍历右子树的时候执行。
class Solution { List<Integer> list=new LinkedList<>(); public List<Integer> inorderTraversal(TreeNode root) { helper(root); return list; } private void helper(TreeNode root) { if (root==null){ return; } helper(root.left); //中序位置 list.add(root.val); helper(root.right); } }
Offer27 二叉树的镜像
- 首先看能不能通过遍历来解决问题,思路上是可以的,我们通过遍历每一个结点,将每一个结点的左右子树反转一下
class Solution { public TreeNode mirrorTree(TreeNode root) { helper(root); return root; } private void helper(TreeNode root) { if (root==null){ return; } //在前序的地方进行 也就是进入结点的时候,进行操作 TreeNode temp=root.left; root.left=root.right; root.right=temp; helper(root.left); helper(root.right); } }
- 能不能通过分解子问题来解决这个问题,我们首先解决左右子树的镜像,然后将根节点的右子树连接原来的左子树,左子树连接成原来的右子树,至于左右子树怎么反转,交给递归函数,我们只需要知道怎么处理每个结点该干什么
class Solution { public TreeNode mirrorTree(TreeNode root) { return helper(root); } /** * 递归函数的语义就是将一个根节点的树镜像反转,返回根节点 * @param root */ private TreeNode helper(TreeNode root) { if (root==null){ return null; } TreeNode left= helper(root.left); TreeNode right= helper(root.right); root.left=right; root.right=left; return root; } }
114二叉树展开为链表
- 首先我们想一想能不能通过遍历来完成,乍一看是可以的,就是我一边遍历,然后在中序的位置,将结点放到我们新建的链表中,但是题目的要求是其必须在原本的结构上去实现
- 所以遍历这种思维行不通,那我们怎么做呢?我们定义一个递归方法,其语义就是传入一个根节点,然后将其变成中序遍历的链表,我们对每个结点应该做什么呢?就是将左子树变成其链表后,右子树变成链表后,将右子树的链表接在左子树后面,然后将左子树接到根节点的右子树
public void flatten(TreeNode root) { helper(root); } /** * 传入一个根节点,变成一个中序遍历的链表 * @param root */ private TreeNode helper(TreeNode root) { if (root==null){ return null; } TreeNode left= helper(root.left); TreeNode right= helper(root.right); //在后序位置的进行操作,因为在后序位置可以得到左右子树的信息 //将根节点左子树连接拉直后的左子树的链表 root.left=null; root.right=left; //将现在根结点的左连接的链表的最后一个结点连接拉着后的右子树的链表 TreeNode p=root; while (p!=null&&p.right!=null){ p=p.right; } p.right=right; return root; }
- 有一个细节点,就是在于将右子树连接到左子树,我们应该从连接过左子树的根节点开始统计,而不是从拉直的左子树的链表的头节点开始找尾结点(因为可能左子树为空,如果我们连接,那么就会把右子树的拉直的链表丢失)
101对称二叉树
- 首先看看能不能通过遍历来解决,每个结点不能独自完成,因为我们要跟另一个结点去比较,才能比较是不是对称的,但是我们可以通过队列来实现
- 通过分解子问题,定义一个递归,来判读两棵树是不是对称的,我们要完成的就是判断当前的结点的情况,如果当前两颗树的结点存在空的情况,或者当都不为空,当前的值是不一样的,直接返回为错误,否则判断当前的树的左树和另一颗树的右树,或者当前树的右树和另一颗树的左树是否对称
public boolean isSymmetric(TreeNode root) { if (root==null){ return true; } //判读两颗子树是不是对称的 return check(root.left,root.right); } private boolean check(TreeNode left, TreeNode right) { if (left==null||right==null){ return left==right; } if (left.val!=right.val){ return false; } return check(left.right,right.left)&&check(left.left,right.right); }
- 这道题其实就是层序遍历的一个应用,因为对称的结点都是处于同一层,所以我们可以用队列来实现这种(迭代写法)
class Solution { Deque<TreeNode> queue=new LinkedList<>(); public boolean isSymmetric(TreeNode root) { if (root==null){ return true; } queue.offer(root.right); queue.offer(root.left); while (!queue.isEmpty()){ TreeNode right= queue.poll(); TreeNode left=queue.poll(); if (right==null&&left==null){ continue; } if (right==null||left==null){ return false; } if (right.val!=left.val){ return false; } queue.offer(right.left); queue.offer(left.right); queue.offer(right.right); queue.offer(left.left); } return true; } }
Offer32 从上到下打印二叉树II
- 就是简单的进行利用队列来实现二叉树的层序遍历,当我们for循环的时候,需要先定义出队列中元素的个数(这一层的个数),否则队列的个数回改变
class Solution { public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> res=new LinkedList<>(); Deque<TreeNode> queue=new LinkedList<>(); if (root==null){ return res; } queue.offer(root); while (!queue.isEmpty()){ int size=queue.size(); LinkedList<Integer> level=new LinkedList<>(); for (int i = 0; i < size; i++) { TreeNode node = queue.poll(); level.add(node.val); if (node.left!=null){ queue.offer(node.left); } if (node.right!=null){ queue.offer(node.right); } } res.add(level); } return res; } }
class Solution { public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> res=new LinkedList<>(); Deque<TreeNode> queue=new LinkedList<>(); if (root==null){ return res; } queue.offer(root); while (!queue.isEmpty()){ int size=queue.size(); LinkedList<Integer> level=new LinkedList<>(); for (int i = 0; i < size; i++) { TreeNode node = queue.poll(); if(res.size()%2==0){ level.add(node.val); }else { level.addFirst(node.val); } if (node.left!=null){ queue.offer(node.left); } if (node.right!=null){ queue.offer(node.right); } } res.add(level); } return res; } }
Offer55II 平衡二叉树
- 首先我们看看能不能用遍历去完成,理论上是可以的,我们遍历每一个结点,然后用她的左子树的高度减去它右子树的高度,如果大于1,就是错误的,但是求树的深度,也需要递归,那么复杂度会很高,所以最好的解法是反过来思考,只计算一次最大深度,计算的过程中在后序遍历位置顺便判断二叉树是否平衡:对于每个节点,先算出来左右子树的最大高度,然后在后序遍历的位置根据左右子树的最大高度判断平衡性。
class Solution { boolean result=true; public boolean isBalanced(TreeNode root) { helper(root); return result; } /** * 获得一棵树的高度 * @param root * @return */ private int helper(TreeNode root) { if (root==null){ return 0; } int left= helper(root.left); int right= helper(root.right); //后序位置,得到了当前左右子树的高度 if (Math.abs(left-right)>1){ result=false; } return Math.max(left,right)+1; } }
Git原理之公共祖先
- 对于搜索二叉树这种找公共祖先是有规律的,如果当前根节点的值都大于这两个值中的最大值,那么可能要去左子树找,如果小于两个值中的较小值,那么就要去右子树找,如果刚好这个值是在这两个值的区间内(闭区间),就说明该节点是最近公共祖先
- 前提条件:所有节点的值都是唯一的。p、q 为不同节点且均存在于给定的二叉搜索树中。
class Solution { TreeNode node; public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (p.val>q.val){ TreeNode temp=p; p=q; q=temp; } helper(root,p,q); return node; } private void helper(TreeNode root, TreeNode p, TreeNode q) { if (root==null){ return ; } if (root.val>=p.val&&root.val<=q.val){ node=root; return; } if (root.val>q.val){ helper(root.left,p,q); }else{ helper(root.right,p,q); } } }
Offer68II二叉树的最近公共祖先
两种情况的概况
- 当分布处于一颗节点的左右子树上
- 当LCA本身是一个我们要求的值
- 我们定义一个递归函数,求出一棵树中其两个节点的公共祖先,我们对每一颗节点的处理就是,如果当前节点是其中一个节点(所有节点的值都是唯一的。p、q 为不同节点且均存在于给定的二叉树中),那么就直接返回,符合第一种情况,如果不是第一种,那么对于该节点,还有一种情况,就是左右子树中各存在着一个节点,如果两种都不满足,就说明该公共祖宗节点是在该节点的左子树或者右子树
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { return helper(root,p,q); } /** * 如果找到其中的一个值,就返回找到的这个节点 * @param root * @param p * @param q * @return */ private TreeNode helper(TreeNode root, TreeNode p, TreeNode q) { if (root==null){ return null; } if (root.val==p.val||root.val==q.val){ //说明当前节点的值是p或者q的一个值 return root; //为什么可以直接返回,因为符合第二种情况啊,因为p,q肯定的存在于树中的 //所以当前根节点的左右子树中肯定存在另一个值 //符合第二种的情况 } TreeNode left= helper(root.left,p,q); TreeNode right= helper(root.right,p,q); if (left!=null&&right!=null){ //说明在左右子树都找了一个对应的节点 //符合第一种情况 return root; } //到这说明根节点必定不是的,那么就出现在左右子树中 return left==null?right:left; }
617合并二叉树
- 是否能通过遍历来解决,可以,把两个子树的所有结点都遍历一遍就是OK
class Solution { public TreeNode mergeTrees(TreeNode root1, TreeNode root2) { return helper(root1,root2); } /** * 返回两颗子树合并好的树根节点 * @param root1 * @param root2 * @return */ private TreeNode helper(TreeNode root1, TreeNode root2) { if (root1==null&&root2==null){ return null; } if (root1==null){ return root2; } if (root2==null){ return root1; } //到这说明两棵树都不为空 //首先将根节点进行合并 TreeNode left=helper(root1.left,root2.left); TreeNode right=helper(root1.right, root2.right); TreeNode root=new TreeNode(root1.val+root2.val); root.right=right; root.left=left; return root; } }
构造树的问题
Offer07 重建二叉树
class Solution { public TreeNode buildTree(int[] preorder, int[] inorder) { return helper(preorder,0,preorder.length-1,inorder,0,inorder.length-1); } private TreeNode helper(int[] preorder, int pStart, int pEnd, int[] inorder, int iStart, int iEnd) { if (pStart>pEnd){ return null; } int val=preorder[pStart]; TreeNode root=new TreeNode(val); int index=0;//对应中序的结点位置 for (int i = 0; i < inorder.length; i++) { if (val==inorder[i]){ index=i; } } int leftLength=index-iStart; TreeNode left=helper(preorder,pStart+1,pStart+leftLength,inorder,iStart,index-1); TreeNode right=helper(preorder,pStart+leftLength+1,pEnd,inorder,index+1,iEnd); root.left=left; root.right=right; return root; } }
Offer26树的子结构
- 子结构什么意思,就是找一个树中会不会包含另一颗树,我们能不能用遍历的方法做呢?,我们对一个结点来说,我们判断这个结点跟我们的结点是不是同一颗树,但是这样判断是不是同一颗树需要递归,遍历节点也需要递归,第一个函数用递归搭配着helper函数的语义来完成A是否包含B,helper的语义是从A开始是否能匹配上B的所有结点
/** * 这个函数的意义是A是否包含B * @param A * @param B * @return */ public boolean isSubStructure(TreeNode A, TreeNode B) { if (A==null||B==null){ return false; } if (A.val==B.val){ boolean result= helper(A.left,B.left)&&helper(A.right,B.right); if (result){ return result; } } return isSubStructure(A.left,B)||isSubStructure(A.right,B); } /** * 查看A的根节点开始是否能匹配上B的所有节点 * @return */ private boolean helper(TreeNode A, TreeNode B) { if (B==null){ return true; } if (A==null){ return false; } if (A.val!=B.val){ return false; }else { return helper(A.left,B.left)&&helper(A.right,B.right); } }
Offer 32 从上到下打印二叉树
class Solution { int arr[]=new int [0]; public int[] levelOrder(TreeNode root) { Deque<TreeNode> queue=new LinkedList<>(); LinkedList<Integer> list=new LinkedList<>(); if (root==null){ return arr; } queue.offer(root); while (!queue.isEmpty()){ int size=queue.size(); for (int i = 0; i < size; i++) { TreeNode node = queue.poll(); list.add(node.val); if (node.left!=null){ queue.offer(node.left); } if (node.right!=null){ queue.offer(node.right); } } } arr=new int[list.size()]; int index=0; for (int i:list) { arr[index++]=i; } return arr; } }
Offer34 二叉树和为某一值的路径
- 遍历可以解决,当遍历到叶子节点的时候,判断路径之和是否等于我们给定的值,如果等于就将其加入到我们的序列之中,在进入一个节点的时候添加这个节点的值(前序位置),在离开这个节点的时候减去这个值(后序位置)
class Solution { List<List<Integer>> lists=new LinkedList<>(); public List<List<Integer>> pathSum(TreeNode root, int target) { helper(root,target,new LinkedList<Integer>(),0); return lists; } private void helper(TreeNode root, int target, LinkedList<Integer> list,int sum) { if (root==null){ return; } //前序位置 sum+=root.val; list.add(root.val); if (root.left==null&&root.right==null){ //说明到达了叶子节点 if (sum==target){ lists.add(new LinkedList<>(list)); } } helper(root.left,target,list,sum); helper(root.right,target,list,sum); //后序位置 离开这个节点 sum-=root.val; list.removeLast(); } }
437路径总和
- 34题要我们得到从根节点到叶子节点的路径和为target的路径,这道题最大的难点是在可以不从根节点开始,我们的遍历是从根节点开始的,所以得到的路径和是基于根节点开始的(后序是离开一个节点的时候,等我们离开根节点,也就是往上走),难道我们要从每个节点开始进行34类似的操作(不需要判断其是不是叶子节点了)
/** * 这个函数用来遍历每一个节点 * @param root * @param targetSum * @return */ public int pathSum(TreeNode root, int targetSum) { if (root == null) { return 0; } int ret = rootSum(root,targetSum,0); ret += pathSum(root.left, targetSum); ret += pathSum(root.right, targetSum); return ret; } /** * 从这个节点出发,找到路径为targetSum的路径树 * @param root * @param targetSum * @param sum * @return */ public int rootSum(TreeNode root,int targetSum,long sum) { int ret = 0; if (root == null) { return 0; } int val = root.val; sum+=val; if (sum == targetSum) { ret++; } ret += rootSum(root.left,targetSum,sum); ret += rootSum(root.right, targetSum,sum); sum-=val; return ret; }
- 那我们能不能考虑一下,从头节点得到一个值,来获得一个区间是k值呢?比如从10到3的值是18,然后我们知道10到10的大小是10,那么我们就知道在5到3是合为8,这不就是我们的前缀和的思想吗,参考560题
class Solution { HashMap<Long,Integer> map=new HashMap<>(); long pathSum; int targetSum; int res = 0; public int pathSum(TreeNode root, int targetSum) { this.targetSum=targetSum; this.pathSum=0; map.put(0L,1);//前缀和为0的个数为1 helper(root); return res; } private void helper(TreeNode root) { if (root==null){ return; } //前序位置 pathSum+=root.val;//记录前缀和 进入一个节点的时候 //为什么是在前序记录 因为在前序是根左右,能保证先出现的前缀和是来自于上层的(但是路径方向必须是向下的(只能从父节点到子节点)。 // 从二叉树的根节点开始,路径和为 pathSum - targetSum 的路径条数 // 就是路径和为 targetSum 的路径条数 res+=map.getOrDefault(pathSum-targetSum,0);//找有多少个 map.put(pathSum,map.getOrDefault(pathSum,0)+1); helper(root.left); helper(root.right); //后序位置离开这个节点的时候 map.put(pathSum,map.get(pathSum)-1); pathSum-=root.val; } }
Ofer36 二叉搜索树与双向链表
- 采用分解的思想,递归的函数的意思就是将一个树变成循环双向链表,返回头节点,其每次我们处理将根节点连接在左子树双向链表的后面和右子树双向链表的前面,其他的交给递归语义来解决
class Solution { public Node treeToDoublyList(Node root) { return helper(root); } /** * 将一颗树变成循环链表,返回头节点 * @param root * @return */ private Node helper(Node root) { if (root==null){ return null; } Node leftHead=helper(root.left); Node rightHead=helper(root.right); Node leftTail,rightTail; //在后序位置 因为已经得到了子树的信息 //将根节点放到两个链表的中间 if (leftHead!=null){ leftTail=leftHead.left; leftTail.right=root; root.left=leftTail; }else { leftHead=leftTail=root; } if (rightHead!=null){ rightTail=rightHead.left; rightHead.left=root; root.right=rightHead; }else { rightHead=rightTail=root; } //将连接的两个子链表变成大的循环链表 leftHead.left=rightTail; rightTail.right=leftHead; return leftHead; } }
Offer37序列化二叉树
层序遍历的解法
String PART=","; String NULL="#"; // Encodes a tree to a single string. public String serialize(TreeNode root) { if (root == null) return ""; StringBuilder sb=new StringBuilder(); Deque<TreeNode> queue=new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()){ int size=queue.size(); for (int i = 0; i < size; i++) { TreeNode node=queue.poll(); if (node==null){ sb.append(NULL).append(PART); continue; } sb.append(node.val).append(PART); queue.offer(node.left); queue.offer(node.right); } } return sb.toString(); } // Decodes your encoded data to tree. public TreeNode deserialize(String data) { if (data.isEmpty()) return null; //得到元素的值 String arr[]=data.split(PART); //第一个是根节点 TreeNode root=new TreeNode(Integer.parseInt(arr[0])); Deque<TreeNode> queueHead=new LinkedList<>(); queueHead.offer(root); for (int i = 1; i <arr.length;) { TreeNode root1=queueHead.poll(); if (arr[i].equals(NULL)){ root1.left=null; i++; }else { TreeNode leftNode=new TreeNode(Integer.parseInt(arr[i])); root1.left=leftNode; queueHead.offer(leftNode); i++; } if (arr[i].equals(NULL)){ root1.right=null; i++; }else { TreeNode rightNode=new TreeNode(Integer.parseInt(arr[i])); root1.right=rightNode; queueHead.offer(rightNode); i++; } } return root; }
- 采用遍历的思维去解决,我们可以用前序遍历,或者后序遍历,在必要的地方加上标记和空指针的标记,就能得到序列化的字符串
- 因为我们记录了空指针的标记,所以我们只通过前序或者后序就能反序列化,因为要么最后一个是根节点,或者最前面一个是根节点,通过递归对于前序,确定了根节点(每次取序列的最开始的节点),先通过剩下的序列来构造出左子树,然后构造右子树,为因为有空结点的标记,遇到了空结点,递归会终止,所以可以区分开左右子树,但是中序遍历是不可以的
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ public class Codec { String PART=","; String NULL="#"; // Encodes a tree to a single string. public String serialize(TreeNode root) { StringBuilder sb=new StringBuilder(); helper(root,sb); return sb.toString(); } private void helper(TreeNode root, StringBuilder sb) { //利用前序结点,得到前序列化 if (root==null){ sb.append(NULL).append(PART); return; } //前序位置 sb.append(root.val).append(PART); helper(root.left,sb); helper(root.right,sb); } // Decodes your encoded data to tree. public TreeNode deserialize(String data) { LinkedList<String> nodes=new LinkedList<>(); for (String str:data.split(PART)) { nodes.add(str); } return helperBuild(nodes); } private TreeNode helperBuild(LinkedList<String> nodes) { if (nodes.isEmpty()){ return null; } //第一个是根结点 String val=nodes.removeFirst(); if (NULL.equals(val)){ //说明当前这个结点是个空结点 return null; //return 就不会继续往下执行了 } TreeNode root=new TreeNode(Integer.parseInt(val)); TreeNode left=helperBuild(nodes); TreeNode right=helperBuild(nodes); root.left=left; root.right=right; return root; } } // Your Codec object will be instantiated and called as such: // Codec codec = new Codec(); // codec.deserialize(codec.serialize(root));
124二叉树的最大路径和
- 我们思考,怎么求的二叉树的最大直径,我们不过
maxDepth
计算最大深度,oneSideMax
计算「单边」最大路径和,然后我们记录根节点+左右单边的最大路径和就能得到这个节点的最大路路径和class Solution { int res=Integer.MIN_VALUE; public int maxPathSum(TreeNode root) { if (root==null){ return 0; } helper(root); return res; } /** * 求从一个节点出发的单边最大的路径和 * @param root * @return */ private int helper(TreeNode root) { if (root==null){ return 0; } int left=Math.max(0,helper(root.left)); int right=Math.max(0,helper(root.right)); res=Math.max(left+right+root.val,res); // 实现函数定义,左右子树的最大单边路径和加上根节点的值 // 就是从根节点 root 为起点的最大单边路径和 return Math.max(left,right)+root.val; } }
BST二叉树
- 1、对于 BST 的每一个节点
node
,左子树节点的值都比node
的值要小,右子树节点的值都比node
的值大。- 2、对于 BST 的每一个节点
node
,它的左侧子树和右侧子树都是 BST。核心框架
void BST(TreeNode root, int target) { if (root.val == target) // 找到目标,做点什么 if (root.val < target) BST(root.right, target); if (root.val > target) BST(root.left, target); }
void traverse(TreeNode root) { if (root == null) return; traverse(root.left); // 中序遍历代码位置 print(root.val); traverse(root.right); }
特性
Offer54二叉搜索树的第K大结点
- 我们知道如果搜索二叉树如果想要是降序排列的,应该是右根左,所以我们应该在中序位置去找,也就是处理完了右子树,然后处理根节点,然后处理左子树(将要去处理左子树的时候进行操作)
class Solution { int count=0; int num=0; public int kthLargest(TreeNode root, int k) { helper(root,k); return num; } private void helper(TreeNode root, int k) { if (root==null){ return; } helper(root.right,k); count++; if (count==k){ num=root.val; return; } helper(root.left,k); } }
Offer33 二叉树搜索树的后序遍历序列
- 后序遍历是左右根,加上搜索二叉树的特性是左>根>右,数组的最后一个元素是根节点,然后去找第一个大于这个值的下标,这个index就是对应的右子树结点,0到index-1就是左子树的结点,如果index后面还有小于最后一个结点的值,说明不符合二分搜索树的概念,每次只处理一个结点的情况,其他的交给递归函数
class Solution { public boolean verifyPostorder(int[] postorder) { return helper(postorder,0,postorder.length-1);//判断这个范围内的结点是否能组成 } private boolean helper(int[] postorder, int start, int end) { if (start>end){ return true; } int val=postorder[end]; int leftStart=start; int leftEnd=start; int rightStart=start; while (postorder[rightStart]<val){ rightStart++; } leftEnd=rightStart-1; int rightEnd=rightStart; while (postorder[rightEnd]>val){ rightEnd++; } return rightEnd==end&&helper(postorder,leftStart,leftEnd)&&helper(postorder,rightStart,rightEnd-1); //找到了第一个值大于分界点的索引 } }
98验证二叉搜索树
- 首先我们用遍历的思维去想,我们在单位BST的特性,如果用中序遍历去遍历,我们的BST树得到的结构肯定是一个递增的序列,如果我们去用中序遍历,如果发现不是一个递增序列,那么就说明不是一个BST
class Solution { long per=Long.MIN_VALUE; public boolean isValidBST(TreeNode root) { return helper(root); } private boolean helper(TreeNode root) { if (root==null){ return true; } if (!helper(root.left)){ return false; } //中序位置 if (root.val>per){ per=root.val; }else { return false; } if (!helper(root.right)){ return false; } return true; } }
- 是否可以用分解的思维完成呢?应该是可以的,因为我们怎么知道一颗树是BST呢,如果其左子树是BST,右子树也是BST,且根节点也满足BST的要求,那么我们就可以知道这个树是BST,我们定义的递归函数的语义就是判断一颗树是BST,我们要处理的就是这个根节点是否满足BST的要求,我们是否只单独的比较根节点的值大于左子树根节点值,小于右子树根节点的值呢?我们来看一种情况
- 对于这种,对于6,为什么不能单单跟15比,虽然6是15的左子树啊,但是也是10的右子树啊,必须满足这两种情况啊,所以对一个节点,我们小于当它作为左子树的根节点(15),必须大于作为左子树的根节点(10),这需要我们去递归的时候进行记录
public boolean isValidBST(TreeNode root) { // return helper(root); return helper1(root,null,null); } private boolean helper1(TreeNode root, TreeNode min, TreeNode max) { if (root==null){ return true; } // 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST if (min != null && root.val <= min.val) return false; if (max != null && root.val >= max.val) return false; // 限定左子树的最大值是 root.val,右子树的最小值是 root.val return helper1(root.left, min, root) && helper1(root.right, root, max); }
538把二叉搜索树转换为累加树
- 通过遍历,怎么遍历呢?右根左,这种类型的中序遍历,用一个sum来记录
class Solution { int sum=0; public TreeNode convertBST(TreeNode root) { return helper(root); } private TreeNode helper(TreeNode root) { if (root==null){ return null; } helper(root.right); sum+=root.val; root.val=sum; helper(root.left); return root; } }
BST构造
- 我们将一个结点作为根节点,那么我们怎么得到以这个节点为根节点的BST个数呢?当然是得到左子树的BST个数*右子树BST个数,这就有分解问题的概念出现了,我们定义递归函数的语义在[start,end]区间 ,就是获得mid为根节点(mid可以是区间的任何一个元素),左子树的范围是[start,.mid-1],右子树的范围为[mid+1,end]的BST的个数之和,当区间为空的时候,返回1(终止条件)
- 设置一个demo,因为存在重叠子问题,
class Solution { int demo[][]; public int numTrees(int n) { demo=new int[n+1][n+1]; helper(1,n); return demo[1][n]; } /** * 闭区间 [start,end]的二叉树的个数 * @param start * @param end * @return */ private int helper(int start, int end) { if (start>end){ return 1; } if (demo[start][end]!=0){ return demo[start][end]; } int res=0; for (int mid = start; mid<=end ; mid++) { int left=helper(start,mid-1); int right=helper(mid+1,end); res+=left*right; } demo[start][end]=res; return res; } }
快速排序
/* 二叉树遍历框架 */ void traverse(TreeNode root) { if (root == null) { return; } /****** 前序位置 ******/ print(root.val); /*********************/ traverse(root.left); traverse(root.right); }
void sort(int[] nums, int lo, int hi) { if (lo >= hi) { return; } // 对 nums[lo..hi] 进行切分 // 使得 nums[lo..p-1] <= nums[p] < nums[p+1..hi] int p = partition(nums, lo, hi); // 去左右子数组进行切分 sort(nums, lo, p - 1); sort(nums, p + 1, hi); }
- 快速排序是先将一个元素排好序,然后再将剩下的元素排好序,就是一个二叉树的经典的前序思路
- 快速排序的核心无疑是
partition
函数,partition
函数的作用是在nums[lo..hi]
中寻找一个分界点p
,通过交换元素使得nums[lo..p-1]
都小于等于nums[p]
,且nums[p+1..hi]
都大于nums[p]
:
- 因为
partition
函数每次都将数组切分成左小右大两部分,恰好和二叉搜索树左小右大的特性吻合,所以最后变成一颗二叉搜索树- 那就不得不说二叉搜索树不平衡的极端情况,极端情况下二叉搜索树会退化成一个链表,导致操作效率大幅降低。
归并排序
核心框架
// 定义:排序 nums[lo..hi] void sort(int[] nums, int lo, int hi) { if (lo == hi) { return; } int mid = (lo + hi) / 2; // 利用定义,排序 nums[lo..mid] sort(nums, lo, mid); // 利用定义,排序 nums[mid+1..hi] sort(nums, mid + 1, hi); /****** 后序位置 ******/ // 此时两部分子数组已经被排好序 // 合并两个有序数组,使 nums[lo..hi] 有序 merge(nums, lo, mid, hi); /*********************/ } // 将有序数组 nums[lo..mid] 和有序数组 nums[mid+1..hi] // 合并为有序数组 nums[lo..hi] void merge(int[] nums, int lo, int mid, int hi);
- 归,不断将原数组一分为2(只是逻辑上),实际上就是不断的分为更小的区间,直到每个子数组只剩下一个元素
- 并 不断的将相邻的两个有序的数组(逻辑上的)合并成一个更大的有序子数组,直到合并到整个数组
- 其时间复杂度是O(nlogn),n是因为要递归将数组分为n个子数组,logn是因为每次处理的方式是折半处理,所以是logn
- 这个算法是稳定的,因为是从前往后合并子数组的,先会将值相同靠前的会先被放入合并的数组
- 空间复杂度是O(n)
二叉树的核心框架
void traverse(TreeNode root) { if (root == null) { return; } traverse(root.left); traverse(root.right); /****** 后序位置 ******/ print(root.val); /*********************/ }