写于2021.1.20
以前我没有刷过LeetCode,这段时间才刚刚开始,充满了盲目性。索性去知乎搜了搜如何刷题,解答也是五花八门。其中我觉得最有效的就是分模块刷题,既有方向感,而且容易找到题目之间的联系,总结经验。因为看了《labudadong的算法小抄》,所以决定先从二叉树开始刷起。据书中所说,只要掌握了二叉树的三种遍历方式,就足以解决大部分二叉树题目。这几天我已经刷了几道了,也感受到了一些二叉树的魅力。确实,无非就是递归和迭代罢了,掌握了这两个核心思想,就可以有一些思路了。刷题过程中存在最大的问题就是忍不住想看答案,也许这就是人的怠惰心理吧,不愿意自己动脑子,只想形成某种思维定势。这是不可取的,我希望慢慢改正。我始终相信一切都能通过学习获得,让我们一起进步吧。
这篇文章主要用于我自己的笔记及记录,任何一个题目都可以通过在力扣官网输入题目号查找。LeetCode虽然也为我们提供了分模块的题目,但是二叉树这一块我觉得分的不太好,所以自己再总结一下。题目都是用Java实现的,如果你觉得我的笔记有错,请您赐教;如果我的笔记太垃圾,那就只看一下题目列表吧,或许会省去你找题目的时间。
二叉树的题目主要就是两大算法:递归和BFS。递归包括二叉树的前序、中序、后序遍历;很多题目的基础模板就是这三种遍历模式。其中前序遍历的题目占大多数,先确定对根节点的操作,然后确定对根节点左右子树的操作。BFS主要是对二叉树层序遍历的要求,不少题目都可以用层序遍历来实现。此外,莫里斯遍历可以实现常数空间的二叉树遍历,值得一探究竟。
这是一个虚假的二叉树题目,用来熟悉一下LeetCode的代码书写和题目提交规程。
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
你可以按任意顺序返回答案。
(1)暴力枚举
即遍历两遍数组,找到nums[j] = target - nums[i],两个for循环就可以解决,这是最基本的解题方法,大多数情况下使用这种方法会超时。
(2)哈希表
方法一的困难之处就在于查找target - nums[i],如果我们将target - nums[i]存储起来,事后进行查找时,就可以根据下标值进行随机存取。所以,我们用哈希表将其存储,存储方法为:如果哈希表中有target - nums[i]的值,就说明找到,直接返回哈希下标;如果没有target - nums[i]的值,就将本身nums[i]添加进去,这样总会有剩下的值与自己本身匹配。
(3)二分查找
记得以前在洛谷刷时,里面二分查找模块就有这道题目。在数组nums中使用二分查找,查找的目标就是target - nums[i],如果成功就返回下标值mid,失败就返回-1;LeetCode的该题目没有失败的情况,也就是说总存在这样两个数等于目标值。
/**
* 1, 暴力求解
* 略
*/
/**
* 2,哈希表
*/
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer,Integer> maptable = new HashMap<Integer,Integer>();
int len = nums.length;
for( int i = 0 ; i < len ; i++){
if(maptable.containsKey(target - nums[i])){
return new int[]{maptable.get(target - nums[i]),i};
}else{
maptable.put(nums[i],i);
}
}
return new int[0];
}
}
/**
* 3,二分查找
* 结果超时
*/
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] tmp = new int[2];
for(int i =0; i < nums.length; i++){
tmp[0] = i;
tmp[1] = BinarySearch(nums, target - nums[i]);
}
return tmp;
}
public int BinarySearch(int[] nums, int x){
//二分查找要对nums先进行排序
Arrays.sort(nums);
int l = 0;
int r = nums.length;
int mid = (l + r) / 2;
while(l < r){
if(nums[mid] == x){
return mid;
}
if(nums[mid] < x){
l = mid + 1;
}
if(nums[mid] > x){
r = mid;
}
}
return -1;//题目给出的数组总能找到答案,这个返回值有无均可。
}
}
(1)暴力求解时间复杂度为O(N2)
(2)哈希表时间复杂度为O(N),查找时是O(1)
(3)二分查找时间复杂度为O(NlogN)
这种题目其实也是我们学习的一个思路:即,一题多解;日后我们可能还会使用多题一解,也就是那种模板题目。只要我们找到其中的套路,稍微动动脑筋或许就可以做出一道题目来;当然,对于较难的题目,恐怕我们还是要潜下心来去仔细思考,有自己的见解是最好的。
给定一个二叉树的根节点 root ,返回它的 中序 遍历。
(1)递归
递归十分的简洁,先遍历根节点,再依次遍历左右结点。
(2)迭代
迭代相比于递归稍微繁琐,需要我们自己去维护栈,而递归时自己就可以维护栈;但是根据经验来说,迭代比递归消耗更少的时间,因为递归本质上是函数的调用,而迭代省去了这部分时间。迭代的思路是这样的:找到该树的最左节点,按照中序遍历的规则,依次输出左节点、根节点和右节点。那么如何实现我们这种思路呢?我们观察到,一直找到最左结点才输出,想像一颗二叉树,从根节点到最左节点,最左节点先输出,而根节点最后输出;是不是符合栈的先进后出的特性呢。所以我们用栈来实现。
//递归方法
class Solution {
//递归问题要记得将list设置成全局
List<Integer> list = new ArrayList<Integer>();
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null){
return list;
}
// if(root.left != null)
inorderTraversal(root.left);
list.add(root.val);
// if(root.right != null)
inorderTraversal(root.right);
return list;
}
}
//迭代方法
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<Integer>();
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode cur = root;
while(cur != null || !stack.empty()){
//这是一个或运算,有一个为真值就可以进入循环,当两者都为false时才跳出循环
while(cur != null){
stack.add(cur);
cur = cur.left;
}
cur = stack.pop();
list.add(cur.val);
cur = cur.right;
}
return list;
}
}
两种方法的时间复杂度一样,均为O(n)。
这两种方法都是我们要必须掌握的方法,因为这也许是许多题目的雏形,我们借助中序遍历的这种递归和迭代可以实现其它的一些题目。往后看,你就会发现题目的解决方案是多么相似,只需找到我们要改动的点即可。
给定一个整数 n,生成所有由 1 … n 为节点所组成的 二叉搜索树 。
我的思考:
应该就是分两步:1,生成树 2,输出树
1,生成树的时候。每个根节点都可以生成不同的树,而根节点i的左子树就是由1…i组成的;右子树由i+1…n组成。
2,应该是已经在Main中被实现了,我们只需要返回我们自己生成的树即可。
解题思路:
根节点i的左子树就是由1…i组成的;右子树由i+1…n组成。递归地划分左子树和右子树,最后进行子树的合并:左子树集合中任选一颗 和右子树集合中的树连接在根节点上。
class Solution {
public List<TreeNode> generateTrees(int n) {
if(n == 0){
return new LinkedList<TreeNode>();
}
return generateTrees(1, n);
}
public List<TreeNode> generateTrees(int start, int end) {
List<TreeNode> allTrees = new LinkedList<TreeNode>();
if(start > end){
allTrees.add(null);
return allTrees;
}
//枚举所有可能的根节点
for(int i =start; i<= end; i++){
//生成左子树
List<TreeNode> leftTrees = generateTrees(start, i-1);
//生成右子树
List<TreeNode> rightTrees = generateTrees(i+1, end);
//子树合并
for(TreeNode left : leftTrees){
for(TreeNode right : rightTrees){
TreeNode cur = new TreeNode(i);
cur.left = left;
cur.right = right;
allTrees.add(cur);
}
}
}
return allTrees;
}
}
好复杂,不会分析,求教。
仍然是递归地划分子树,只不过我们在划分的时候要寻找其中的特性,比方这道题包含的特性就是二叉搜索树的左子树根节点右子树的大小关系:根节点i的左子树就是由1…i组成的;右子树由i+1…n组成,整个二叉树就是左右子树集合的笛卡尔积,做了P96可能会有更深刻的理解。
动态规划,不看也罢:原问题可以分解为子问题,并且子问题可以复用
对于二叉搜索树而言,左子树要小于等于根节点,右子树要大于等于根节点
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
当结点为空时,只会有一种情况,当节点为一个时,也只有一种情况
G(n):长度为n的序列的二叉搜索树的个数
F(i):以i为根节点的二叉树的个数
G(n) = F(1)…+F(n)
当 i 为根节点时,其左子树节点个数为 i-1 个,右子树节点为 n-i
F(i) = G(i-1)G(n-i)
即,左子树集合数目右子树集合数目
综合上面两个公式就得
G(n) = G(0)*G(n-1) + G(1)*G(n-2)…
class Solution {
public int numTrees(int n) {
int[] dp = new int[n+1];
dp[0] = 1;//F(0) = 1
dp[1] = 1;//F(1) = 1
for(int i = 2 ; i <= n; i++){
for(int j = 1; j <= i ; j++){
dp[i] += dp[j - 1] * dp[i - j];
//G(n) = G(0)*G(n-1) + G(1)*G(n-2)...
}
}
return dp[n];
}
}
时间复杂度为O(n)。
这是一道动态规划的题目,与这个二叉树系列的题目联系基本为零,所以将它收录在另一个新开的笔记《动态规划,不看也罢》中。上面的题目P95主要是让我们生成所有的二叉树,而这个题目只要求我们返回个数,这就决定了我们用不一样的方法去解决。
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
先中序遍历,存储数组,数组内顺序递增,就是false
[1 2 3 4 5 6]—>[1 5 3 4 2 6]
class Solution {
public boolean isValidBST(TreeNode root) {
List<TreeNode> list = new ArrayList<TreeNode>();
inorder(root, list);
int len = list.size();
// boolean flag;
for(int i = 0; i < len - 1; i++){
//题目描述中等于也是不符合的。
if(list.get(i).val >= list.get(i+1).val){
return false;
}
}
return true;
}
public void inorder(TreeNode root, List<TreeNode> list){
if(root == null){
return ;
}
inorder(root.left, list);
list.add(root);
inorder(root.right, list);
}
}
中序遍历一遍节点,比较大小又一遍,额外需要一个数组存储。
用非递归的方法中序遍历可实现一次遍历就可以,所以该题目我的方法仍需改进。
给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。
利用二叉搜索树的性质,即,中序遍历是按照数字序列的顺序输出的我们只要找到不符合顺序的两个结点,并作交换,就可以得到正确的二叉树.
class Solution {
public void recoverTree(TreeNode root) {
List<TreeNode> list = new ArrayList<TreeNode>();
inorder(root, list);
findSwap(root, list);
}
//中序遍历
public List<TreeNode> inorder(TreeNode root, List<TreeNode> list){
if(root == null){
return list;
}
inorder(root.left, list);
list.add(root);
inorder(root.right, list);
return list;
}
//寻找需要交换的两个结点并交换
public void findSwap(TreeNode root, List<TreeNode> list){
int len = list.size();
TreeNode x = null;
TreeNode y = null;
//因为是两两比较,所以比较到倒数第二个就可以了
for(int i = 0; i < len-1; i++){
if(list.get(i).val > list.get(i+1).val){
y = list.get(i+1);
if(x == null){
x = list.get(i);
}
}
}
int tmp;
if(x != null && y != null){
tmp = x.val;
x.val = y.val;
y.val = tmp;
}
}
}
空间复杂度为O(n)。不符合题目要求。
本题有莫里斯遍历的解法,日后可以尝试实现。
给定两个二叉树,编写一个函数来检验它们是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
递归
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
//用递归或广度优先搜索
//先判断两个父节点是否相同
if(p == null && q == null){
return true;
}else if(p == null || q == null){
return false;
}else if(p.val != q.val){
return false;
}
// 如果不为空就判断值是否相同
else {
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
}
}
这道题目还算简单。
给定一个二叉树,检查它是否是镜像对称的。
(1)递归方法
设置两个指针p,q;一个向左遍历,一个向右遍历,检查二者是否相等。
(2)迭代方法
思路上和递归是一样的,只不过是把递归转换成队列保存形式而已.
递归转队列常用的方法之一就是转成队列
//方法一
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null){
return true;
}
return check(root.left, root.right);
}
public boolean check(TreeNode p, TreeNode q){
if(p == null && q == null){
return true;
}
if(p == null || q == null){
return false;
}
if(p.val != q.val){
return false;
}
return check(p.left, q.right) && check(p.right, q.left);
}
}
//方法二
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null){
return true;
}
return check(root, root);
}
public boolean check(TreeNode p, TreeNode q){
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(p);//两个根节点入队列
queue.offer(q);
while(!queue.isEmpty()){
p = queue.poll();
q = queue.poll();
if(p == null && q == null){
//???
continue;
}
if(p == null || q== null){
//这就相当于两个结点不一致
return false;
}
if(p.val != q.val){
//结点值不一致
return false;
}
//比较两个对称位置的结点知否相同
queue.offer(p.left);
queue.offer(q.right);
queue.offer(p.right);
queue.offer(q.left);
}
return true;
}
}
两种方法都是要遍历所有节点。
不同的题目有不同的解法,希望自己是真正明白了算法的意思,而不是单纯的记住。
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
解决层序遍历的问题还得靠队列来实现,队列的先进先出特点正符合我们的按层遍历的要求,这其实也是BFS的一种思想。开始时,我们将根节点入队列,根节点出队列时将他的左右子节点入队列,这样我们就得到了第二层的结点;依次操作,我们就可以得到 层序遍历的结果。另外,按照题目要求,我们需要返回一个二维数组,也即要把每一层 都分开,这时候我们只需要对源程序稍作修改即可: 我们在进行层序遍历时,可以观察到当上一层的结点从队列出来时,刚进去的是下一层结点,而当上层结点都出去时,下层结点也都刚刚进去。所以,只要我们实时返回一个队列大小,就可以解决按层输出的问题。
//层序遍历模板
class Solution {
public void levelOrder(TreeNode root) {
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
TreeNode node = queue.poll();
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
}
}
}
//按层返回的层序遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new ArrayDeque<>();
List<List<Integer>> list = new ArrayList<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
int n = queue.size();
List<Integer> l = new ArrayList<>();
for(int i =0 ; i < n; i++){
TreeNode node = queue.poll();
l.add(node.val);
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
}
list.add(l);
}
return list;
}
}
每个结点进队列一次,时间复杂度为O(N)。
上面代码部分我给出了层序遍历的模板,单看层序遍历还是很简单的,只是一个队列的运用。若要符合题目要求,再稍作改进即可。我们平时刷题何尝不是这样,一套模板就可以打败还几道题,这就要求我们做到善于总结,举一反三。
对于层序遍历的题目,另有一篇笔记收录了八个题目,链接如下:
一套代码八题目,层序遍历不迷路.
给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
(1) 层次遍历+标记
首先实现二叉树的层序遍历,我们在层序遍历的基础上稍作修改,实现锯齿形层序遍历。 若根节点为第0层,可以观察到,奇数层结点倒序输出即可。我们加一个变量level来标记奇数层或偶数层。这要求我们熟练掌握队列等数据结构。Java中的ArrayDeque性能较好,功能较多,可模拟栈、队列、双端队列,推荐大家使用。 这种方法存在着一个问题:我只用了ArrayDeque的队首队尾取元素,而加元素的时候都是往末尾加,这就导致了输出结果乱序,不符合要求。
(2)改进1:还是在队首取元素,不过加元素的时候改为按奇偶层来添加,从右向左添加到头部,从左向右添加到尾部。不过只通过了13/33个例子,仍然存在错误。
(3)改进2:取元素依然是始终在队首取,加元素在队尾加,但是当添加结点的值时,改为按照奇偶层来添加。
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
int level = 0;
while(!queue.isEmpty()){
level += 1;
Deque<Integer> l = new LinkedList<Integer>();
int n = queue.size();
for(int i = 0; i < n ; i++){
//始终在队列头部取结点
TreeNode node = queue.poll();
if(level % 2 == 0){
//偶数层,从右到左输出,加到双端队列头部
l.addFirst(node.val);
}else{
//奇数层,从左向右输出,加到双端队列尾部
l.addLast(node.val);
}
if(node.left != null)
queue.add(node.left);
if(node.right != null)
queue.add(node.right);
}
list.add(new LinkedList<>(l));//需要进行类型转换
}
return list;
}
}
每个结点遍历一次,空间复杂度为O(n)。
改题目其实就是考察我们对层序遍历的理解,基于此,再考察我们数据结构用的怎么样。所以说,练好数据结构,算法刷题就又多了一个工具。
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
(1)我们用递归来实现。可以观察到,计算二叉树的深度,就是计算根节点到叶节点 的最远结点数。那么如何计算呢?最容易想到的就是计算节点数,只要在往叶节点遍历时+1即可; 那么深度计算呢?知道了根节点左子树的结点数和右子树的节点数,然后比较两者大小即可。
(2)广度优先搜索实现。我们换一种思维,求解二叉树的最大深度,换句话说,就是求二叉树的层数,对不对?还记得我们上面两道题是有关于层序遍历呢吗?当题目要求我们以二维数组的形式按层返回二叉树的结点时, 我们对BFS稍作修改,每次从队列中拿出当前层的所有结点来拓展,那么现在,我们只要再设置一个变量记录层数不就解决此题了吗?
// DFS
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
int left = maxDepth(root.left) + 1;
int right = maxDepth(root.right) + 1;
return Math.max(left, right);
}
}
//BFS
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
Queue<TreeNode> queue = new ArrayDeque<>();
queue.add(root);
int count = 0;//记录层数
while(!queue.isEmpty()){
int n = queue.size();
while(n > 0){
TreeNode node = queue.poll();
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
count ++;
}
return count;
}
}
在时间上来说,二者没有什么差别,都需要遍历完所有的节点,所以时间复杂度为O(n)。空间上略有区别,递归需要去维护栈,而栈的大小取决于二叉树的深度,BFS的方法则取决于队列的大小。
一连做了三道题目,对于二叉树的广度优先搜索有了初步的理解,希望自己在以后碰到类似题目时也可以独立解决。
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
用递归来解决问题。根据前序遍历我们可以知道二叉树的根节点,根据中序遍历我们可以知道根节点的左右节点。递归思想:找到一个根节点,再去寻找他的左右子结点,而左右子节点又可以找到自己的根节点,以此递归,直到遍历完所有的结点。
具体:根据前序遍历找到根节点,然后根据根节点在中序遍历中找到根节点所属位置,这样就可以确定左右子树的个数,我们分别为两个数组设置两个指针标记前后位置,当位置不符合顺序时,就是递归的出口。用哈希表来标记根节点在中序遍历中的位置,这样做是因为我们需要反复查找根节点在中序中的位置。
class Solution {
//哈希表设置为全局
HashMap<Integer, Integer> map;
public TreeNode buildTree(int[] preorder, int[] inorder) {
map = new HashMap<>();
//创建哈希表
int n = preorder.length;
for(int i =0; i< n ;i++){
map.put(inorder[i], i);
}
return myBuild(preorder, inorder, 0, n-1, 0, n-1);
}
public TreeNode myBuild(int[] preorder, int[] inorder, int pre_left, int pre_right, int in_left, int in_right){
//递归出口
if(pre_left > pre_right){
return null;
}
//或者
if(in_left > in_right){
return null;
}
//创建根节点
TreeNode root = new TreeNode(preorder[pre_left]);
//获取根节点在中序的位置
int root_index = map.get(preorder[pre_left]);
//左子树的大小
int subTree = root_index - in_left;
//获取左右子树并划分
//左子树
root.left = myBuild(preorder, inorder, pre_left+1, pre_left+subTree, in_left, root_index-1);
//右子树
root.right = myBuild(preorder, inorder, pre_left+subTree+1, pre_right, root_index+1, in_right);
return root;
}
}
时间复杂度为O(n)。
这一道题目只是篇幅较大,LeetCode把它定义为中等难度,可是我没看题解之前不会做。不会做的点大概有这样几方面:
(1)只想到了要用递归,要利用两个遍历的特性构造二叉树,但是没想到用结点数量来划分左右子树。
(2)不知道结点之间如何连接。我觉得可以这样想,分析以前做过的二叉树递归题目,什么情况下是递归出口呢?就是当结点为空时,我们return null;当子树的划分只有一个结点时,就会连接在根节点上,递归函数逐层返回时,就会连接成一个完整的树。
(3)想都没想递归出口会是什么。
根据一棵树的中序遍历与后序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
让我们试试上一道题的思路:后续遍历最后一个元素是根节点,找到根节点在中序遍历中的位置,就可以划分左右子树,然后根据左右子树的个数在后序遍历中查找下一个子树的划分范围。
依次递归。
class Solution {
//哈希表来存储
private HashMap<Integer, Integer> map;
public TreeNode buildTree(int[] inorder, int[] postorder) {
map = new HashMap<>();
int n = inorder.length;
for(int i = 0; i < n; i++){
map.put(inorder[i], i);
}
return myBuild(inorder, postorder, 0, n-1, 0, n-1);
}
public TreeNode myBuild(int[] inorder, int[] postorder, int in_left, int in_right, int post_left, int post_right){
//递归出口
if(in_left > in_right){
return null;
}
if(post_left > post_right){
return null;
}
//创建根节点
TreeNode root = new TreeNode(postorder[post_right]);
//获取根节点在中序位置
int rootindex = map.get(root.val);
//获取左子树大小
int subTree = rootindex - in_left;
//连接左子树
root.left = myBuild(inorder, postorder, in_left, rootindex-1, post_left, post_left+subTree-1);
//连接右子树
root.right = myBuild(inorder, postorder, rootindex+1, in_right, post_left+subTree, post_right-1);
return root;
}
}
时间复杂度为O(n)。时间上击败98%的用户,而空间只击败16%的用户,说明空间利用效率比不好。
要额外注意子树划分的范围,还有那些边界点,可能一不小心就会陷入疯狂。
给定一个二叉树,返回其节点值自底向上的层序遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
这道题目与P102二叉树层序遍历I的区别就在于这是倒序输出每一层结点。最简单的方法就是在之前的基础上加一个辅助栈。还有一种方法就是将list改装一下,每次添加元素时,加到它的首部,就能够实现倒序的输出。
//方法一
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
Stack<List<Integer>> stack = new Stack<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
List<Integer> l = new ArrayList<>();
int n = queue.size();
while(n > 0){
TreeNode node = queue.poll();
l.add(node.val);
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
stack.add(l);
}
while(!stack.isEmpty()){
list.add(stack.pop());
}
return list;
}
}
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> list = new LinkedList<>();
Stack<List<Integer>> stack = new Stack<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
List<Integer> l = new ArrayList<>();
int n = queue.size();
while(n > 0){
TreeNode node = queue.poll();
l.add(node.val);
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
list.add(0, l);
//public void add(int index,E element):将元素添加到指定索引位置
}
return list;
}
}
时间复杂度是O(n)。
两种方法的时间和空间消耗差不多,后者略好一点。相较于之前做的P102,只需做一点小小的修改即可。所以说,多刷点题,没坏处,哦,坏处可能就是会秃。
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
题目给出的是一个升序的数组,也就是说,我们只要知道一个元素做根节点,那么根节点的左右子树自然也就是该元素在数组中左右两边的元素。那递归进行划分不就完事了吗。题目让构造的是高度平衡二叉树,也就是说根节点两边的结点数量要基本一致,所以每次选取中间元素进行划分就行。 每次都是划分一半的元素作为根节点的左右子树,所以高度都是相同的,你想啊,左边2个,右边2个,创建数时得先创建左右结点,这不就一样高了吗。
还有就是答案不唯一。
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return DFS(nums, 0, nums.length - 1);
}
public TreeNode DFS(int[] nums, int left, int right){
if(left > right){
return null;
}
int mid = (left + right)/2;
TreeNode node = new TreeNode(nums[mid]);
node.left = DFS(nums, left, mid - 1);
node.right = DFS(nums, mid + 1, right);
return node;
}
}
时间复杂度:O(n)。
空间复杂度:O(logn),其中 n 是数组的长度。空间复杂度不考虑返回值,因此空间复杂度主要取决于递归栈的深度,递归栈的深度是 O(logn)。
这题虽然归结为简单,但是我觉得不简单,考察了子树的划分、递归、BST的性质。如果在某个点上卡壳了,恐怕就做不出来了。
P1382, P876, P109
给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
这题与上题的不同就是,此题采用链表,不能直接定位元素所在位置。那怎么找到根节点进行划分呢?
最简单的方法就是先将链表转换成数组,再按上题方法计算。
另一种方法就是,设置两个指针,也就是快慢指针法,快的走两步,慢的走一步,正好是二倍的关系,这样当快指针走到末尾时,慢指针正好位于中间位置。
//方法一
class Solution {
public TreeNode sortedListToBST(ListNode head) {
//别学我,哈哈哈,太多零了
int[] nums = new int[1000000];
int i =0;
while(head != null){
nums[i ++] = head.val;
head = head.next;
}
//这里的i-1要特别注意
return DFS(nums, 0, i - 1);
}
public TreeNode DFS(int[] nums, int left, int right){
if(left > right){
return null;
}
int mid = (left + right)/2;
TreeNode node = new TreeNode(nums[mid]);
node.left = DFS(nums, left, mid - 1);
node.right = DFS(nums, mid + 1, right);
return node;
}
}
class Solution {
public TreeNode sortedListToBST(ListNode head) {
return DFS(head, null);
}
public ListNode getMid(ListNode left, ListNode right){
ListNode slow = left;
ListNode fast = left;
while(fast != right && fast.next != right){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
public TreeNode DFS(ListNode left, ListNode right){
if(left == right){
return null;
}
ListNode mid = getMid(left, right);
TreeNode node = new TreeNode(mid.val);
node.left = DFS(left, mid);
node.right = DFS(mid.next, right);
return node;
}
}
这两个题目最根本的就是根节点位置的查找。下面这个题目恰巧就是描述这个问题的。
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
上文提到的快慢指针法
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
给你一棵二叉搜索树,请你返回一棵 平衡后 的二叉搜索树,新生成的树应该与原来的树有着相同的节点值。
如果一棵二叉搜索树中,每个节点的两棵子树高度差不超过 1 ,我们就称这棵二叉搜索树是 平衡的 。
如果有多种构造方法,请你返回任意一种。
我们已经做过将有序数组和有序链表转换成平衡二叉树的办法,那么这道题最简单的解决方法就是先中序遍历二叉树,将结果存到数组中,然后根据我们以前的方法进行划分。
注意审题:将二叉搜索树变平衡,前提是二叉搜索树,如果是普通的二叉树我们就不能采用这种方法了。
class Solution {
private ArrayList<Integer> list;
public TreeNode balanceBST(TreeNode root) {
if(root == null){
return null;
}
list = new ArrayList<>();
inorder(root);
return DFS(0, list.size() - 1);
}
public void inorder(TreeNode root){
if(root == null){
return ;
}
inorder(root.left);
list.add(root.val);
inorder(root.right);
}
public TreeNode DFS(int left, int right){
if(left > right){
return null;
}
int mid = (left + right)/2;
TreeNode root = new TreeNode(list.get(mid));
root.left = DFS(left, mid - 1);
root.right = DFS(mid + 1, right);
return root;
}
}
中序遍历时每个节点遍历一次,构造树时每个节点遍历一次。时间复杂度为O(n)。
上面一连三个体都可以采用相似方法解决,我们只要搞清楚其中一个的本质,就可以举一反三。
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
既然是判定是否为平衡二叉树,那么我们只要计算出左右节点的高度进行比较,如果高度差的绝对值不超过1,那就是平衡二叉树;反之则不是。
注意,平衡二叉树中每个子树也都要是平衡二叉树。
//方法一
class Solution {
public boolean isBalanced(TreeNode root) {
if(root == null){
return true;
}
int left = balanced(root.left);
int right = balanced(root.right);
if(Math.abs(left - right) <= 1
&& isBalanced(root.left)//左子树
&& isBalanced(root.right)//右子树
){
return true;
}
return false;
}
public int balanced(TreeNode root){
if(root == null){
return 0;
}
int left = balanced(root.left) + 1;
int right = balanced(root.right) + 1;
return left>right ? left : right;
}
}
上面的方法需要重复调用计算高度的balance方法,效率低。我们可以看到是在isBalance方法中重复调用,那么我们怎么优化呢?方法一是先判断整个树,再去判断子树。
如果我们先判断子树是不是就不用重复判断了呢。
//方法二
class Solution {
public boolean isBalanced(TreeNode root) {
if(root == null){
return true;
}
return balanced(root) != -1;
}
public int balanced(TreeNode root){
if(root == null){
return 0;
}
int left = balanced(root.left);
int right = balanced(root.right);
if(Math.abs(left - right) > 1
|| left == -1 || right == -1
){
return -1;
}
return left>right ? left+1 : right+1;
}
}
第一种方法相较于方法二来说效率更低,需要调用更多次的函数。前者是自顶向下,后者是自底向上。
本质上都是递归,只要理解如何计算二叉树的高度,问题就可以迎刃而解。
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
这道题目是不是似曾相识呢?是的,不就是让我们计算二叉树的高度吗?那来吧。
注意:以前的题目我们返回的都是最大值,而现在需要返回最小值,就会存在一种问题:如果根节点左子树为空,右子树不为空,那么最小深度一定是以右子树为准,而返回最小值的话会返回左子树的0.修改方法就是我们以叶节点为递归出口,而不是以根节点为空时做出口。
class Solution {
public int minDepth(TreeNode root) {
//只判断这个会出错
if(root == null){
return 0;
}
if(root.left == null && root.right == null){
return 1;
}
int tmp = Integer.MAX_VALUE;
if(root.left != null)
tmp = Math.min(minDepth(root.left) , tmp);
if(root.right != null)
tmp = Math.min(minDepth(root.right) , tmp);
return tmp + 1;
}
}
时间复杂度为O(N)。
注意与二叉树最大深度的区别
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。
方法一:递归。
如果我们计算每一条路径的总和,如果这个和与target相同,则找到路径。但是这样会存在一个问题:当递归地计算路径和时我们返回什么?如果返回和值,那么当遍历完左子树去遍历右子树地时候,这个和值也是会影响右子树。如果换种方式,遍历时我们将路径和target改为sum - root.val,那么每次比较的就是当前结点是否和target相同。
方法二:BFS
二叉树的遍历经常用递归和BFS实现,那么我们求和时,也可以用BFS实现。采用BFS需要新开一个队列记录我们每个结点路径的和值。
//方法一
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
//判断该树是否为空
if(root == null){
return false;
}
//判断是否为叶节点
if(root.left == null && root.right == null){
return root.val == targetSum;
}
return hasPathSum(root.left, targetSum - root.val)||hasPathSum(root.right, targetSum - root.val);
}
}
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
Queue<TreeNode> queue = new ArrayDeque<>();
Queue<Integer> sum = new ArrayDeque<>();
//判断该树是否为空
if(root == null){
return false;
}
queue.add(root);
sum.add(root.val);
while(!queue.isEmpty()){
TreeNode node = queue.poll();
int pathSum = sum.poll();
//判断是否为叶节点
if(node.left == null && node.right == null){
//不能直接返回,否则二者不相等将无法继续判断
if(pathSum == targetSum){
return true;
}
}
if(node.left != null){
queue.add(node.left);
sum.add(pathSum + node.left.val);
}
if(node.right != null){
queue.add(node.right);
sum.add(pathSum + node.right.val);
}
}
return false;
}
}
时间复杂度相同,都为O(n)。
掌握递归和BFS模板,何愁做不出来!
给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
说明: 叶子节点是指没有子节点的节点。
这道题目与上一题的不同之处是此题不仅需要判断,还要我们输出所有的路径。这就意味着我们要单设一个数据结构来记录路径。
class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
//记录最终结果
res = new LinkedList<>();
//记录路径
path = new LinkedList<>();
DFS(root, targetSum);
return res;
}
public void DFS(TreeNode root, int targetSum){
if(root == null){
return ;
}
path.offerLast(root.val);
int sum = targetSum - root.val;
//判断是否为叶节点
if(root.left == null && root.right == null){
if(sum == 0){
res.add(new LinkedList<>(path));
}
}
DFS(root.left, sum);
DFS(root.right, sum);
path.pollLast();
}
}
每个节点遍历一次,时间复杂度为O(n)。
不看题解竟然没做出来,水平不行啊,有待提高。
给你二叉树的根结点 root ,请你将它展开为一个单链表:
展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
展开后的单链表应该与二叉树 先序遍历 顺序相同。
新声明一个TreeNode是不行的,因为题目的意思是让我们在原树上做改动。我们可以先将树按先序遍历存储在链表中,再将链表转换成题目要求的树。
改进版:
LeetCode描述:注意到前序遍历访问各节点的顺序是根节点、左子树、右子树。如果一个节点的左子节点为空,则该节点不需要进行展开操作。如果一个节点的左子节点不为空,则该节点的左子树中的最后一个节点被访问之后,该节点的右子节点被访问。该节点的左子树中最后一个被访问的节点是左子树中的最右边的节点,也是该节点的前驱节点。因此,问题转化成寻找当前节点的前驱节点。
具体做法是,对于当前节点,如果其左子节点不为空,则在其左子树中找到最右边的节点,作为前驱节点,将当前节点的右子节点赋给前驱节点的右子节点,然后将当前节点的左子节点赋给当前节点的右子节点,并将当前节点的左子节点设为空。对当前节点处理结束后,继续处理链表中的下一个节点,直到所有节点都处理结束。
我的理解:
将二叉树转换成链表时是按照前序遍历的顺序来的,而前序遍历总是在遍历完左子树之后才遍历右子树;具体地说,是遍历完当前结点左子树的最右结点,然后再去遍历右子树。这样的话,如果我们将整个右子树作为那个最右节点的子树,这时候再将整个左子树作为根节点的右子树,以此往复循环,不就能在不申请新的空间的前提下,完成链表转换。
//方法一
class Solution {
List<TreeNode> list = new ArrayList<>();
public void flatten(TreeNode root) {
if(root == null){
return ;
}
preorder(root);
for(int i = 0; i< list.size()-1; i++){
TreeNode cur = list.get(i);
TreeNode right = list.get(i+1);
cur.left = null;
cur.right = right;
}
}
public void preorder(TreeNode root){
if(root == null){
return ;
}
list.add(root);
preorder(root.left);
preorder(root.right);
}
}
//方法二
class Solution {
public void flatten(TreeNode root) {
TreeNode cur = root;
//判断当前节点是否为空
while(cur != null){
//判断是否有左子树
if(cur.left != null){
//不为空,找到其最右节点
TreeNode tmp = cur.left;
while(tmp.right != null){
tmp = tmp.right;
}
//找到之后,将当前根节点的右子树赋值给它
tmp.right = cur.right;
//将整个左子树作为根节点的右子树
cur.right = cur.left;
//根节点左子树置空
cur.left = null;
}
cur = cur.right;
}
}
}
时间复杂度为O(n)。
注意数组中存储的是树结点,而不是结点的值。只有存储结点,我们才能利用结点的信息来构建完整的树,只存储值,不能完全构建树。
方法二可以实现不申请新的空间,即空间复杂度为O(1)。
做完本题目,可以尝试去做Morris算法。P99也可以用Morris算法,也就是莫里斯遍历二叉树。
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
(1)空间复杂度为O(n)。按照层序遍历的思想,对每一层进队列的结点,如果是本层最后一个结点就指向NULL,否则,指向下一个进队列的节点。
(2)空间复杂度为O(1)。确定两种类型的next指针:一是根节点的左右节点之间的连接,二是连接不同父节点的左右子树。具体方法:找到该层的最左节点,确定其两种类型的连接,然后对于本层其它节点同样此操作。
(3)递归方法;
//方法一
class Solution {
public Node connect(Node root) {
Deque<Node> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
int n = queue.size();
while(n > 0){
Node node = queue.poll();
//判断是不是本层最后一个节点
if(n == 1){
node.next = null;
}else{
node.next = queue.element();
//element 只返回不移出
}
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
}
return root;
}
}
//方法二
class Solution {
public Node connect(Node root) {
if(root == null){
return root;
}
Node leftmost = root;//最左节点
//如果最左节点的左子树为空,那么说明到了最后一层了
while(leftmost.left != null){
Node head = leftmost;
//head就是当前层的各个节点,为空说明当前层完成了遍历
while(head != null){
//第一种连接:根节点的左右子树
head.left.next = head.right;
//第二种连接:不同根节点之间的连接
if(head.next != null){
//当前节点在该层有next节点
head.right.next = head.next.left;
}
//继续本层的下一个节点
head = head.next;
}
//当前层完成后,继续下一层
//去下一层的最左节点
leftmost = leftmost.left;
}
return root;
}
}
//方法三
class Solution {
public Node connect(Node root) {
if(root == null){
return root;
}
if(root.left != null){
root.left.next = root.right;
if(root.next != null){
root.right.next = root.next.left;
}
connect(root.left);
connect(root.right);
}
return root;
}
}
三种方法时间复杂度相当,空间上后两种符合题目要求。
层次遍历简单易懂,只要明白层次遍历内涵即可;常量空间的方法不易理解之处就是如何确定两种类型的next指针,确定方法之后就可以尝试了。
且看与下题的不同之处。
给定一个二叉树
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
此题与上题的不同之处在于此题不是满二叉树。这就改变了next指针的连接类型。
(1)空间复杂度O(n).仍采用层次遍历的方法。
(2)空间复杂度O(1).此时不再是两种类型的连接,而是多种类型的next连接。
这就失去了头绪,如果最左节点为空,那么我们也找不到它。解决办法就是在最左方加一个哑节点,用来串联next指针。 思路与上题一样,遍历本层的同时,创建下一层next指针,然后再按此方法逐层遍历。(将每一层想像成一个链表)
//方法一
class Solution {
public Node connect(Node root) {
Deque<Node> q = new ArrayDeque<>();
if(root != null){
q.add(root);
}
while(!q.isEmpty()){
int n = q.size();
while(n > 0){
Node node = q.poll();
if(n != 1){
node.next = q.element();
}
if(node.left != null){
q.add(node.left);
}
if(node.right != null){
q.add(node.right);
}
n --;
}
}
return root;
}
}
//方法二
class Solution {
public Node connect(Node root) {
if(root == null){
return root;
}
//cur负责第 N 层的遍历
Node cur = root;
while(cur != null){
//哑节点在N + 1层
Node dummy = new Node(0);
//pre负责第 N + 1 层的遍历
Node pre = dummy;
while(cur != null){
if(cur.left != null){
pre.next = cur.left;
pre = pre.next;
}
if(cur.right != null){
pre.next = cur.right;
pre = pre.next;
}
//N层的节点遍历
cur = cur.next;
}
//去下一层
cur = dummy.next;
}
return root;
}
}
一旦在某层的节点之间建立了 next 指针,那这层节点实际上形成了一个链表。因此,如果先去建立某一层的 next 指针,再去遍历这一层,就无需再使用队列了。
翻转一棵二叉树。
这道题目应该去联想一下P101对称二叉树。
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null){
return null;
}
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
给定一个不含重复元素的整数数组 nums 。一个以此数组直接递归构建的 最大二叉树 定义如下:
二叉树的根是数组 nums 中的最大元素。
左子树是通过数组中 最大值左边部分 递归构造出的最大二叉树。
右子树是通过数组中 最大值右边部分 递归构造出的最大二叉树。
返回有给定数组 nums 构建的 最大二叉树 。
相似题目:P105, P106
思路:
先找到数组中最大的那个数来构造根节点,根据此数两边的数构造根节点的左右子树。然后递归地构造树即可。
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return myBuild(nums, 0, nums.length-1);
}
public TreeNode myBuild(int[] nums, int left, int right){
//递归出口
if(left > right){
return null;
}
//首先找到最大值的下标
int rootindex = Maxi(nums, left, right);
//创建根节点
TreeNode root = new TreeNode(nums[rootindex]);
//左子树
root.left = myBuild(nums, left, rootindex-1);
//右子树
root.right = myBuild(nums, rootindex+1, right);
return root;
}
public int Maxi(int[] nums,int left, int right){
//返回指定范围内的最大值的下标值
int j = right;
int maxnum = nums[j];
for(int i = left ; i < right ; i++){
if(nums[i] > maxnum){
maxnum = nums[i];
j = i;
}
}
return j;
}
}
时间复杂度为O(n2)。
与两个相似题目一样,都是在递归地构造树。
给定一棵二叉树,返回所有重复的子树。对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。
两棵树重复是指它们具有相同的结构以及相同的结点值。
序列化二叉树。将每个子树的序列保存到HashMap中,若有重复的,则记录重复次数。
class Solution {
Map<String, Integer> map;
List<TreeNode> list;
public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
map = new HashMap<>();
list = new ArrayList<>();
collect(root);
return list;
}
public String collect(TreeNode root){
//序列化
if(root == null){
return "#";
}
//先保存的是子树,根据先序遍历保存
//注意中序遍历可能会出错,因为有些情况序列化之后结果相同
String s = root.val + ',' + collect(root.left) + ',' + collect(root.right);
//将出现的次数加入map
map.put(s, map.getOrDefault(s, 0) + 1);
//map.getOrDefault(s, 0):没有s返回0,有的话返回出现的次数。
if(map.get(s) == 2){
//有相同的则将根节点加入列表
list.add(root);
}
return s;
}
}
时间复杂度为O(N2)。
序列化这个概念是第一次接触。