目录
- 合并二叉树
- 二叉树的直径
- 把二叉搜索树转换为累加树
- 路径总和 III
- 前 K 个高频元素
- 打家劫舍 III
- 实现 Trie
- 二叉树中的最大路径和
- *二叉树展开为链表
- 二叉树的最大深度
- 最小的k个数
- *数据流中的中位数
- *验证二叉搜索树
- *不同的二叉搜索树
- *二叉树的中序遍历
- *二叉树的镜像
- *从上到下打印二叉树 II
- *从上到下打印二叉树 III
- *二叉搜索树的后序遍历序列
- 重建二叉树*
- *树的子结构
- *对称的二叉树
- *从上到下打印二叉树
- 二叉树中和为某一值的路径
- 二叉搜索树与双向链表
- 序列化二叉树
- 二叉搜索树的第k大节点
- 平衡二叉树
- 二叉搜索树的最近公共祖先
- 二叉树的最近公共祖先
- 实现二叉树先序、中序和后序遍历
合并二叉树
1 题目描述
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
示例 1:
输入:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出:
合并后的树:
3
/ \
4 5
/ \ \
5 4 7
注意: 合并必须从两个树的根节点开始。
2 解题(Java)
前序遍历:
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1 == null) return root2;
if (root2 == null) return root1;
root1.val += root2.val;
root1.left = mergeTrees(root1.left, root2.left);
root1.right = mergeTrees(root1.right, root2.right);
return root1;
}
}
3 复杂性分析
- 时间复杂度O(n);
- 空间复杂度O(n);
二叉树的直径
1 题目描述
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
注意:两结点之间的路径长度是以它们之间边的数目表示。
2 解题(Java)
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
int res = 1;// 经过最多的节点数
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return res - 1;
}
int depth(TreeNode node) {
if (node == null) return 0;
int left = depth(node.left);// 左子节点为根的树的深度
int right = depth(node.right);// 右子节点为根的树的深度
int cur = left + right + 1; // 当前节点为根节点经过的最多节点数
res = Math.max(res, cur); // 更新res
return Math.max(left, right) + 1; // 返回当前节点为根的树的深度
}
}
3 复杂性分析
- 时间复杂度:O(N),其中 N 为二叉树的节点数,每个结点被访问一次;
- 空间复杂度:O(N),其中 N 为二叉树的高度,退化为链表是时间复杂度为O(N);
把二叉搜索树转换为累加树
1 题目描述
给出二叉搜索树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
提醒一下,二叉搜索树满足下列约束条件:
- 节点的左子树仅包含键 小于 节点键的节点。
- 节点的右子树仅包含键 大于 节点键的节点。
- 左右子树也必须是二叉搜索树。
示例 1:
输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]
示例 2:
输入:root = [0,null,1]
输出:[1,null,1]
示例 3:
输入:root = [1,0,2]
输出:[3,3,2]
示例 4:
输入:root = [3,2,4,1]
输出:[7,9,4,10]
提示:
- 树中的节点数介于0 和104之间。
- 每个节点的值介于-104和104之间。
- 树中的所有值互不相同 。
- 给定的树为二叉搜索树。
2 解题Java
解题思路
反序中序遍历该二叉搜索树,记录过程中的节点值之和,并不断更新当前遍历到的节点的节点值。
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
int sum;
public TreeNode convertBST(TreeNode root) {
if (root != null) {
convertBST(root.right);
sum += root.val;
root.val = sum;
convertBST(root.left);
}
return root;
}
}
3 复杂性分析
- 时间复杂度:O(n)。其中 n 是二叉搜索树的节点数,每一个节点恰好被遍历一次;
- 空间复杂度:O(n)。为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树退化为链表,为 O(n);
路径总和 III
1 题目描述
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例 1:
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
示例 2:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3
提示:
- 二叉树的节点个数的范围是 [0,1000]
- -109 <= Node.val <= 109
- -1000 <= targetSum <= 1000
2 解题(Java)
前缀和+回溯法(前序遍历):
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
//key为从根节点到当前节点的前缀和,value为此前缀和的数目
Map<Long, Integer> cache = new HashMap<>();
int res;
int sum;
public int pathSum(TreeNode root, int sum) {
this.sum = sum;
//初始化:空节点,前缀和为0,个数为1
cache.put(0L, 1);
recur(root, 0);
return res;
}
private void recur(TreeNode node, long pre) {
if(node == null) return;
pre += node.val;
res += cache.getOrDefault(pre - sum, 0);
cache.put(pre, cache.getOrDefault(pre, 0) + 1);
recur(node.left, pre);
recur(node.right, pre);
// 回溯到父节点前复原前缀和的数目
cache.put(pre, cache.get(pre) - 1);
}
}
3 复杂性分析
- 时间复杂度:O(N),每个节点都遍历一次;
- 空间复杂度:O(N),prefixMap占用O(N)空间;
前 K 个高频元素
1 题目描述
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
- 1 <= nums.length <= 105
- k 的取值范围是 [1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。
2 解题(Java)
HashMap + 堆排序:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
// int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数,根据第二个元素设计小根堆
PriorityQueue<int[]> heap = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]);
for (Integer num : map.keySet()) {
if (heap.size() == k) {
if (heap.peek()[1] < map.get(num)) {
heap.poll();
heap.offer(new int[]{num, map.get(num)});
}
} else {
heap.offer(new int[]{num, map.get(num)});
}
}
int[] res = new int[k];
int i = 0;
while (!heap.isEmpty()) res[i++] = heap.poll()[0];
return res;
}
}
3 复杂性分析
- 时间复杂度:O(Nlogk),其中 N 为数组的长度。我们首先遍历原数组,并使用哈希表记录出现次数,需 O(N) 时间;随后遍历HashMap,由于堆的大小至多为 k,因此每次堆操作需要 O(logk) 的时间,需 O(Nlogk)的时间;总时间复杂度为O(Nlogk);
- 空间复杂度:O(N)。哈希表 O(N),堆 O(k),总 O(N);
打家劫舍 III
1 题目描述
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:
输入: [3,4,5,1,3,null,1]
3
/ \
4 5
/ \ \
1 3 1
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
2 解题(Java)
动态规划+后序遍历:
打劫节点A分两种情况:
- 打劫A:node.val + 不打劫A左节点的A左节点 + 不打劫A右节点的A右节点;
- 不打劫A:Math.max(打劫A左节点,不打劫A左节点)+ Math.max(打劫A右节点,不打劫A右节点);
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
int[] rootStatus = dfs(root);
return Math.max(rootStatus[0], rootStatus[1]);
}
public int[] dfs(TreeNode node) {
if (node == null) {
return new int[]{0, 0};
}
int[] l = dfs(node.left);
int[] r = dfs(node.right);
int selected = node.val + l[1] + r[1];
int notSelected = Math.max(l[0], l[1]) + Math.max(r[0], r[1]);
return new int[]{selected, notSelected};
}
}
3 复杂性分析
- 时间复杂度:O(n);
- 空间复杂度:O(n);
实现 Trie
1 题目描述
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
- Trie() 初始化前缀树对象。
- void insert(String word) 向前缀树中插入字符串 word 。
- boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
- boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
提示:
- 1 <= word.length, prefix.length <= 2000
- word 和 prefix 仅由小写英文字母组成
- insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次
2 解题(Java)
解题思路
Trie,又称前缀树或字典树,是一棵有根树,其每个节点包含以下字段:
- 指向子节点的指针数组 children。对于本题而言,数组长度为 26,即小写英文字母的数量。此时 children[0] 对应小写字母 a,children[1] 对应小写字母 b,…,children[25] 对应小写字母 z;
- 布尔字段 isEnd,表示该节点是否为字符串的结尾;
插入字符串
我们从字典树的根开始,插入字符串。对于当前字符对应的子节点,有两种情况:
- 子节点存在。沿着指针移动到子节点,继续处理下一个字符;
- 子节点不存在。创建一个新的子节点,记录在 children 数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符;
重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾。
查找前缀/查找字符串
我们从字典树的根开始,查找前缀。对于当前字符对应的子节点,有两种情况:
- 子节点存在。沿着指针移动到子节点,继续搜索下一个字符;
- 子节点不存在。说明字典树中不包含该前缀,返回空指针;
重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符。
若搜索到了前缀的末尾,就说明字典树中存在该前缀。此外,若前缀末尾对应节点的 isEnd 为真,则说明字典树中存在该字符串。
代码
class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26];
isEnd = false;
}
public void insert(String word) {
Trie node = this;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null;
}
private Trie searchPrefix(String prefix) {
Trie node = this;
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
return null;
}
node = node.children[index];
}
return node;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
3 复杂性分析
- 时间复杂度:初始化为 O(1),其余操作为 O(|S|),其中 |S| 是每次插入或查询的字符串的长度;
- 空间复杂度:O(∣T∣⋅Σ),其中 |T| 为所有插入字符串的长度之和,Σ 为字符集的大小,本题 Σ=26;
二叉树中的最大路径和
1 题目描述
路径被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中至多出现一次 。该路径至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为15 + 20 + 7 = 42
提示:
- 树中节点数目范围是 [1, 3 * 104]
- -1000 <= Node.val <= 1000
2 解题(Java)
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
if (root == null) return 0;
maxGain(root);
return maxSum;
}
// 返回节点的最大贡献值
public int maxGain(TreeNode node) {
if (node == null) return 0;
// 递归计算左右子节点的最大贡献值
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
// 以该节点为根节点的最大路径和
int curSum = node.val + leftGain + rightGain;
// 更新答案
maxSum = Math.max(maxSum, curSum);
return node.val + Math.max(leftGain, rightGain);
}
}
3 复杂性分析
- 时间复杂度O(n):n是二叉树中的节点个数,对每个节点访问不超过2次;
- 时间复杂度O(n):空间复杂度取决于二叉树高度,最坏情况下,二叉树高度等于二叉树中的节点个数;
*二叉树展开为链表
1 题目描述
给你二叉树的根结点 root ,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而left子指针始终为 null 。
- 展开后的单链表应该与二叉树 先序遍历 顺序相同。
示例 1:
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [0]
输出:[0]
提示:
- 树中结点数在范围 [0, 2000] 内
- -100 <= Node.val <= 100
进阶:你可以使用原地算法(O(1) 额外空间)展开这棵树吗?
2 解题(Java)
解题思路:寻找当前节点右子节点的前驱节点
- 前序遍历访问各节点的顺序是根节点、左子树、右子树;
- 如果一个节点的左子节点为空,则该节点不需要进行展开操作。如果一个节点的左子节点不为空,则该节点的左子树中的最后一个节点被访问之后,该节点的右子节点被访问;
- 该节点的左子树中最后一个被访问的节点是左子树中的最右边的节点,也是该节点右子节点的前驱节点;
- 因此,问题转化成寻找当前节点右子节点的前驱节点;
- 具体做法是:对于当前节点,如果其左子节点不为空,则在其左子树中找到最右边的节点,作为右子节点的前驱节点,将当前节点的右子节点赋给前驱节点的右子节点,然后将当前节点的左子节点赋给当前节点的右子节点,并将当前节点的左子节点设为空;
- 对当前节点处理结束后,继续处理链表中的下一个节点,直到所有节点都处理结束;
代码
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
public void flatten(TreeNode root) {
while (root != null) {
if (root.left != null) {
TreeNode rightPre = root.left;
while (rightPre.right != null) rightPre = rightPre.right;
rightPre.right = root.right;
root.right = root.left;
root.left = null;
}
root = root.right;
}
}
}
3 复杂性分析
- 时间复杂度O(n):其中 n 是二叉树的节点数。展开为单链表的过程中,需要对每个节点访问一次,在寻找右子节点的前驱节点的过程中,每个节点最多被额外访问一次;
- 空间复杂度O(1):相当于原地转换;
二叉树的最大深度
1 题目描述
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
2 解题(Java)
后序遍历:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
else return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
3 复杂性分析
- 时间复杂度O(n):每个结点都访问一次;
- 空间复杂度O(n):最坏情况下,二叉树退化为链表,递归深度为N;最好情况下为平衡二叉树,递归深度为logN;
最小的k个数
1 题目描述
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
限制:
- 0 <= k <= arr.length <= 10000
- 0 <= arr[i] <= 10000
2 解题(Java)
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if (k == 0 || arr.length == 0) return new int[0];
PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);
for (int i=0; i<arr.length; i++) {
if (heap.size() == k) {
if (arr[i] < heap.peek()) {
heap.poll();
heap.offer(arr[i]);
}
} else {
heap.offer(arr[i]);
}
}
int[] res = new int[k];
int i = 0;
while (!heap.isEmpty()) res[i++] = heap.poll();
return res;
}
}
3 复杂性分析
- 时间复杂度O(Nlogk):其中 N 是数组 arr 的长度,由于最大堆实时维护前 k 小值,所以插入删除都是 O(logk) 的时间复杂度,最坏情况下数组里 N 个数都会插入一次,所以一共需要 O(Nlogk) 的时间复杂度;
- 空间复杂度O(k):最大堆里存 k 个数;
*数据流中的中位数
1 题目描述
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
- void addNum(int num) - 从数据流中添加一个整数到数据结构中。
- double findMedian() - 返回目前所有元素的中位数。
示例 1:
输入:
[“MedianFinder”,“addNum”,“addNum”,“findMedian”,“addNum”,“findMedian”]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
示例 2:
输入:
[“MedianFinder”,“addNum”,“findMedian”,“addNum”,“findMedian”]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]
限制:
最多会对 addNum、findMedian 进行 50000 次调用。
2 解题(Java)
class MedianFinder {
PriorityQueue<Integer> minHeap, maxHeap;
public MedianFinder() {
minHeap = new PriorityQueue<>(); // 小顶堆,保存较大的一半
maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1); // 大顶堆,保存较小的一半
}
public void addNum(int num) {
if(minHeap.size() > maxHeap.size()) {
minHeap.offer(num);
maxHeap.offer(minHeap.poll());
} else {
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
}
}
public double findMedian() {
return minHeap.size() != maxHeap.size() ? minHeap.peek() : (minHeap.peek() + maxHeap.peek()) / 2.0;
}
}
3 复杂性分析
- 时间复杂度:
- 查找中位数 O(1) : 获取堆顶元素使用 O(1) 时间;
- 添加数字 O(logN) : 堆的插入和弹出操作使用O(logN) 时间;
- 空间复杂度 O(N) : 其中 N 为数据流中的元素数量,最小堆和最大堆共同保存 N 个元素;
*验证二叉搜索树
1 题目描述
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
示例 2:
2 解题(Java)
前序遍历判断:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean isValidBST(TreeNode node, long lower, long upper) {
if (node == null) return true;
if (node.val <= lower || node.val >= upper) return false;
return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);
}
}
3 复杂性分析
- 时间复杂度O(n):其中 n 为二叉树的节点个数。在递归调用中二叉树的每个节点被访问一次,故时间复杂度为 O(n);
- 空间复杂度O(n):最坏情况下二叉树为一条链,递归达到 n 层,故空间复杂度为 O(n) ;
*不同的二叉搜索树
1 题目描述
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
2 解题(Java)
动态规划:
class Solution {
public int numTrees(int n) {
int[] dp = new int[n+1];// dp[n]代表节点数为n所能构成的二叉搜索树的数目
dp[0] = 1; dp[1] = 1;
for (int i=2; i<=n; i++) {
for (int j=0; j<=i-1; j++) {
dp[i] += dp[j] * dp[i-j-1]; //j代表左子树的个数
}
}
return dp[n];
}
}
3 复杂性分析
- 时间复杂度O(n ^ 2):n 为二叉搜索树的节点个数,共 n 个值需要求解,每个值求解平均需要 O(n) 时间复杂度,因此总时间复杂度为 O(n ^ 2);
- 空间复杂度O(n):需要 O(n) 空间存储 dp 数组;
*二叉树的中序遍历
1 题目描述
给定一个二叉树的根节点 root ,返回它的 中序 遍历。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
示例 4:
输入:root = [1,2]
输出:[2,1]
示例 5:
输入:root = [1,null,2]
输出:[1,2]
提示:
- 树中节点数目在范围 [0, 100] 内
- -100 <= Node.val <= 100
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
2 解题(Java)
代码
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Deque<TreeNode> stack = new LinkedList<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
res.add(root.val);
root = root.right;
}
return res;
}
}
3 复杂性分析
- 时间复杂度O(n):其中 n 为二叉树节点的个数,每个节点会被访问一次;
- 空间复杂度O(n):空间复杂度取决于栈深度,而栈深度在二叉树退化为链表的情况下会达到 O(n)级别;
*二叉树的镜像
1 题目描述
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
例如输入:
镜像输出:
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
限制:
0 <= 节点个数 <= 1000
2 解题(Java)
后序遍历:
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
TreeNode tmp = root.left;
root.left = mirrorTree(root.right);
root.right = mirrorTree(tmp);
return root;
}
}
3 复杂性分析
- 时间复杂度 O(N): 其中 N 为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点,使用 O(N) 时间;
- 空间复杂度 O(N) : 最差情况下(当二叉树退化为链表),递归时系统需使用 O(N) 大小的栈空间;
*从上到下打印二叉树 II
1 题目描述
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
例如:
给定二叉树: [3,9,20,null,null,15,7],
返回其层次遍历结果:
提示:
节点总数 <= 1000
2 解题(Java)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Deque<TreeNode> queue = new LinkedList<TreeNode>() {{
offer(root);
}};
while (!queue.isEmpty()) {
List<Integer> subRes = new ArrayList<>();
for (int i=queue.size(); i>0; i--) {
TreeNode node = queue.poll();
subRes.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
res.add(subRes);
}
return res;
}
}
3 复杂性分析
- 时间复杂度 O(N) : N 为二叉树的节点数量,即 BFS 需循环 N 次。
- 空间复杂度 O(N) : 最差情况下,即当树为平衡二叉树时,最多有 N/2 个树节点同时在 queue 中,使用 O(N) 大小的额外空间。
*从上到下打印二叉树 III
1 题目描述
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
例如:
给定二叉树: [3,9,20,null,null,15,7],
返回其层次遍历结果:
提示:
节点总数 <= 1000
2 解题(Java)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Deque<TreeNode> queue = new LinkedList<TreeNode>() {{
offer(root);
}};
while (!queue.isEmpty()) {
List<Integer> subRes = new LinkedList<>();
for (int i=queue.size(); i>0; i--) {
TreeNode node = queue.poll();
if (res.size() % 2 == 0) {
subRes.add(node.val);
}
else {
subRes.add(0, node.val);
}
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
res.add(subRes);
}
return res;
}
}
3 复杂性分析
- 时间复杂度 O(N) : N 为二叉树的节点数量,即 BFS 需循环 N 次,占用 O(N) ;
- 空间复杂度 O(N) : 最差情况下,即当树为满二叉树时,最多有 N/2 个树节点 同时 在 deque 中,占用 O(N) 大小的额外空间;
*二叉搜索树的后序遍历序列
1 题目描述
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。
参考以下这颗二叉搜索树:
示例 1:
输入: [1,6,3,2,5]
输出: false
示例 2:
输入: [1,3,2,6,5]
输出: true
提示:
数组长度 <= 1000
2 解题(Java)
2.1 解题思路
后序遍历倒序: [ 根节点 | 右子树 | 左子树 ]
- 在后序遍历的倒序中,如果r[i] < r[i-1],则r[i]必为某结点的左子节点,且其父节点root为r[0],r[i],…r[i-1]中大于且最接近r[i]的节点,后面遍历到的节点都应小于父节点;
- 遍历 “后序遍历的倒序” 会多次遇到递减节点 r[i],若所有的递减节点 r[i]对应的父节点 root 都满足以上条件,则可判定为二叉搜索树;
- 根据以上特点,考虑借助 单调栈 实现:
- 借助一个单调栈 stack 存储值递增的节点;
- 每当遇到值递减的节点 r[i],则通过出栈来更新节点r[i]的父节点 root;
- 每轮判断r[i]和root的值关系:
- 如果r[i] > root,则说明不满足二叉搜索树的定义,返回false;
- 如果r[i] < root,则说明暂时满足二叉树定义,继续遍历;
2.2 算法流程
- 初始化:单调栈stack,父节点值root = Integer.MAX_VALUE;
- 倒序遍历postorder:记每个节点为r[i]:
- 判断:若r[i] > root,说明此后序遍历序列不满足二叉搜索树定义,返回false;
- 更新父节点root:当栈不为空且r[i] < stack.peek()时,循环执行出栈,并将栈节点赋给root;
- 入栈:将当前节点r[i]入栈;
- 若遍历完成,说明后序遍历满足二叉搜索树定义,返回true;
2.3 代码
class Solution {
public boolean verifyPostorder(int[] postorder) {
Deque<Integer> stack = new LinkedList<>();
int root = Integer.MAX_VALUE;
for (int i=postorder.length-1; i>=0; i--) {
if (postorder[i] > root) return false;
while (!stack.isEmpty() && stack.peek() > postorder[i]) {
root = stack.pop();
}
stack.push(postorder[i]);
}
return true;
}
}
3 复杂性分析
- 时间复杂度 O(N) : 遍历 postorder 所有节点,各节点均入栈一次,有的结点出栈一次,使用 O(N) 时间;
- 空间复杂度 O(N) : 最差情况下,单调栈 stack 存储所有节点,使用 O(N) 额外空间;
重建二叉树*
1 题目描述
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
示例:
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下二叉树:
3
/ \
9 20
/ \
15 7
2 解题(Java)
2.1 解题思路
前序遍历性质: 节点按照 [ 根节点 | 左子树 | 右子树 ] 排序;
中序遍历性质: 节点按照 [ 左子树 | 根节点 | 右子树 ] 排序;
根据以上性质,可得出以下推论:
- 前序遍历的首元素为树的根节点 node 的值;
- 在中序遍历中搜索根节点 node 的索引 ,可将 中序遍历 划分为 [ 左子树 | 根节点 | 右子树 ] ;
- 根据中序遍历中的左 / 右子树的节点数量,可将 前序遍历 划分为 [ 根节点 | 左子树 | 右子树 ] ;
- 通过以上3步,可确定3个节点 :1.树的根节点 2.左子树根节点 3.右子树根节点;
- 对于树的左、右子树,仍可使用以上步骤划分子树的左右子树;
- 以上子树的递推性质是 分治算法 的体现,因此考虑通过递归对所有子树进行划分;
2.2 分治法解析
- 递推参数: 子树根节点在前序遍历的索引 root 、子树在中序遍历的左边界 left 、子树在中序遍历的右边界 right ;
- 终止条件: 当 left > right ,代表已经越过叶节点,此时返回 null ;
- 递推工作:
- 建立根节点 node : 节点值为 preorder[root] ;
- 划分左右子树: 查找根节点在中序遍历 inorder 中的索引 i ;(使用哈希表 dic 存储中序遍历的值与索引的映射,查找操作的时间复杂度为 O(1))
- 构建左右子树: 开启左右子树递归:
根节点索引 | 中序遍历左边界 | 中序遍历右边界 | |
---|---|---|---|
左子树 | root + 1 | left | i - 1 |
右子树 | root + i - left + 1 | i + 1 | right |
右子树根节点索引 = 根节点索引 + 左子树长度 + 1
- 返回值: 返回 node ,作为上一层递归中根节点的左 / 右子节点;
2.3 代码
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
Map<Integer, Integer> dic = new HashMap<>();
int[] preorder;
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
for (int i=0; i<inorder.length; i++) {
dic.put(inorder[i], i);
}
return recur(0, 0, inorder.length - 1);
}
private TreeNode recur(int preRoot, int inLow, int inHigh) {// preRoot是树的根节点在前序的位置,inLow是树中序遍历的最左边,inHigh是树中序遍历的最右边
if (inLow > inHigh) return null;// 递归终止
TreeNode root = new TreeNode(preorder[preRoot]);// 建立根节点
int index = dic.get(preorder[preRoot]);// 划分根节点、左子树、右子树,i是根结点在中序中的位置
root.left = recur(preRoot + 1, inLow, index - 1);// 开启左子树递归
root.right = recur(preRoot + index - inLow + 1, index + 1, inHigh);// 开启右子树递归
return root;// 返回根节点
}
}
3 复杂性分析
- 时间复杂度 O(N) : 其中 N 为树的节点数量。初始化 HashMap 需遍历 inorder,使用 O(N) 时间;递归共建立 N 个节点,每层递归中的节点建立、搜索操作使用 O(1)时间,共使用 O(N)时间。
- 空间复杂度 O(N) : HashMap 占用 O(N) 额外空间。最差情况下,树退化为链表,递归深度达到 N ,占用 O(N) 额外空间;最好情况下,树为满二叉树,递归深度为 logN,占用 O(logN) 额外空间;
*树的子结构
1 题目描述
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树 A:
给定的树 B:
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
示例 1:
输入:A = [1,2,3], B = [3,1]
输出:false
示例 2:
输入:A = [3,4,5,1,2], B = [4,1]
输出:true
限制:
0 <= 节点个数 <= 10000
2 解题(Java)
2.1 解题思路
判断树B是否是树A的子结构,需完成以下两步工作:
- 前序遍历树A中的每个节点nA;(对应函数isSubStructure(A, B))
- 判断树A中以nA为根节点的子树是否包含树B,即nA从顶到底和B从顶到底能对应上,且A可能有余;(对应函数recur(A, B))
2.2 算法流程
recur(A, B)函数:
- 终止条件:
- 当节点B为空:说明树B已匹配完成(越过子节点),返回true;
- B不为空,A为空 :匹配失败,返回false;
- A和B的值不同:匹配失败,返回false;
- 返回值:
- 判断A和B的左子节点是否相等,即recur(A.left, B.left);
- 同时判断A和B的右子节点是否相等,即recur(A.right, B.right);
isSubStructure(A, B)函数:
- 特例处理:当树A为空或树B为空时,直接返回false;
- 返回值:若树B是树A的子结构,则必满足以下三种情况之一,因此用或||连接:
- 以节点A为根节点的子树包含树B,对应recur(A, B);
- 树B是树A左子树的子结构,对应isSubStructure(A.left, B);
- 树B是树A右子树的子结构,对应isSubStructure(A.right, B);
2.3 代码
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
return (A != null && B != null) && (recur(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B));
}
private boolean recur(TreeNode A, TreeNode B) {
if(B == null) return true;
if(A == null || A.val != B.val) return false;
return recur(A.left, B.left) && recur(A.right, B.right);
}
}
3 复杂性分析
- 时间复杂度 O(MN): 其中 M,N分别为树 A 和 树 B 的节点数量;先序遍历树 A 占用 O(M) ,每次调用 recur(A,B) 判断占用 O(N) ;
- 空间复杂度 O(M) : 最差情况为遍历至树 A 叶子节点,此时总递归深度为 M;
*对称的二叉树
1 题目描述
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
限制:
0 <= 节点个数 <= 1000
2 解题(Java)
2.1 递归
代码
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
return root == null ? true : isSymmetric(root.left, root.right);
}
public boolean isSymmetric(TreeNode p, TreeNode q) {
if (p == null && q == null) {
return true;
}
if (p == null || q == null) {
return false;
}
return p.val == q.val && isSymmetric(p.left, q.right) && isSymmetric(p.right, q.left);
}
}
复杂性分析
- 时间复杂度 O(N) : 其中 N 为二叉树的节点数量,调用N次 isSymmetric() ;
- 空间复杂度 O(N):最差情况下(见下图),二叉树退化为链表,系统使用 O(N) 大小的栈空间;
2.2 迭代
代码
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
return root == null ? true : isSymmetric(root.left, root.right);
}
public boolean isSymmetric(TreeNode u, TreeNode v) {
Deque<TreeNode> queue = new LinkedList<>();
queue.offer(u);
queue.offer(v);
while (!queue.isEmpty()) {
u = queue.poll();
v = queue.poll();
if (u == null && v == null) {
continue;
}
if ((u == null || v == null) || (u.val != v.val)) {
return false;
}
queue.offer(u.left);
queue.offer(v.right);
queue.offer(u.right);
queue.offer(v.left);
}
return true;
}
}
复杂性分析
- 时间复杂度 O(N) : 其中 N 为二叉树的节点数量,每个节点都要访问一次 ;
- 空间复杂度 O(N):这里需要用一个队列来维护节点,每个节点最多进队一次,出队一次,队列中最多不会超过 N 个点,故渐进空间复杂度为 O(N);
*从上到下打印二叉树
1 题目描述
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
例如:
给定二叉树: [3,9,20,null,null,15,7]
返回:
[3,9,20,15,7]
提示:
节点总数 <= 1000
2 解题(Java)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int[] levelOrder(TreeNode root) {
if (root == null) return new int[0];
List<Integer> ans = new ArrayList<>();
Deque<TreeNode> queue = new LinkedList<TreeNode>() {{offer(root);}};
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
ans.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
int[] res = new int[ans.size()];
for (int i=0; i<res.length; i++) {
res[i] = ans.get(i);
}
return res;
}
}
3 复杂性分析
- 时间复杂度 O(N): N 为二叉树的节点数量,遍历N次;
- 空间复杂度 O(N): 最差情况下,当树为平衡二叉树时,最多有 N/2个树节点同时在 queue 中,使用 O(N)大小的额外空间;
二叉树中和为某一值的路径
1 题目描述
输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
示例:
给定如下二叉树,以及目标和 sum = 22,
返回:
提示:
节点总数 <= 10000
2 解题(Java)
2.1 解题思路
前序遍历+路径记录+回溯法:
- 前序遍历:按照“根、左、右”的顺序,遍历树的所有节点;
- 路径记录:在前序遍历中,记录从根节点到当前节点的路径。当路径为根节点到叶节点形成的路径且各节点值的和等于目标值sum时,将此路径加入结果列表;
2.2 算法流程
pathSum(root, sum)函数:
- 初始化:结果列表res,路径列表path;
- 返回值:返回res即可;
recur(root, tar)函数:
- 递推参数:当前节点root,当前目标值tar;
- 终止条件:若节点root为空,直接返回;
- 递推工作:
- 路径更新:将当前节点值root.val加入路径path;
- 目标值更新:tar = tar - root.val(即目标值tar从sum减至0);
- 路径记录:当root为叶节点且目标值tar=0时,则将此路径path加入res;
- 前序遍历:递归左/右子节点;
- 路径恢复:向上回溯前,需将当前节点从路径path中删除,即执行path.remove(path.size() - 1);
2.3 代码
前序遍历:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
recur(root, sum);
return res;
}
void recur(TreeNode root, int tar) {
if (root == null) return;
path.add(root.val);
tar -= root.val;
if (tar == 0 && root.left == null && root.right == null) {
res.add(new ArrayList(path));
}
recur(root.left, tar);
recur(root.right, tar);
path.remove(path.size() - 1);
}
}
3 复杂性分析
- 时间复杂度 O(N) : N 为二叉树的节点数,前序遍历需要遍历所有节点。
- 空间复杂度 O(N): 最差情况下,即树退化为链表时,path 存储所有树节点,使用 O(N) 额外空间。
二叉搜索树与双向链表
1 题目描述
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例:
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。
2 解题(Java)
2.1 解题思路
- 解法基于性质:二叉搜索树的中序遍历为递增序列。
- 将二叉搜索树转换成一个“排序的循环双向链表”,其中包含三个要素:
- 排序链表:节点应从小到大排序,因此应使用中序遍历从小到大访问树的节点;
- 双向链表:在构建相邻节点(设前驱节点pre,当前节点cur)关系时,不仅应pre.right = cur,也应cur.left = pre;
- 循环链表:设链表头节点head和尾节点tail,则应构建head.left = tail和tail.right = head;
- 经上述分析,考虑用中序遍历访问树的各节点cur,并在访问每个节点时构建cur和前驱节点pre的引用指向,中序遍历完成后,最后构建头节点和尾节点的引用指向即可;
2.2 算法流程
dfs(cur):开启中序遍历
- 终止条件:当节点cur为空,代表越过叶节点,直接返回;
- 递归左子树,即dfs(cur.left);
- 构建链表:
- 当pre为空时:代表正在访问链表头节点,记为head;
- 当pre不为空时:修改双向节点引用,即pre.right = cur,cur.left = pre;
- 保存cur:更新pre = cur,即节点cur是后继节点的pre;
- 递归右子树:即dfs(cur.right);
treeToDoublyList(root):
- 特例处理:若节点root为空,直接返回;
- 转化为双向链表:调用dfs(root);
- 构建循环链表:中序遍历完成后,head指向头节点,pre指向尾节点,因此修改head和pre的双向节点引用即可;
- 返回值:返回链表的头节点head即可;
2.3 代码
/*
// Definition for a Node.
class Node {
public int val;
public Node left;
public Node right;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val,Node _left,Node _right) {
val = _val;
left = _left;
right = _right;
}
};
*/
class Solution {
Node pre, head;
public Node treeToDoublyList(Node root) {
if (root == null) return null;
dfs(root);
head.left = pre;
pre.right = head;
return head;
}
void dfs(Node cur) {
if(cur == null) return;
dfs(cur.left);
if(pre != null) pre.right = cur;
else head = cur;
cur.left = pre;
pre = cur;
dfs(cur.right);
}
}
3 复杂性分析
- 时间复杂度 O(N) : N 为二叉树的节点数,中序遍历需要访问所有节点。
- 空间复杂度 O(N) : 最差情况下,即树退化为链表时,递归深度达到 N,系统使用 O(N) 栈空间。
序列化二叉树
1 题目描述
请实现两个函数,分别用来序列化和反序列化二叉树。
示例:
2 解题(Java)
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if (root == null) return "[]";
StringBuilder res = new StringBuilder("[");
Deque<TreeNode> queue = new LinkedList<TreeNode>() {{
offer(root);
}};
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node != null) {
res.append(node.val + ",");
queue.offer(node.left);
queue.offer(node.right);
} else {
res.append("null,");
}
}
res.deleteCharAt(res.length()-1);
res.append("]");
return res.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if (data.equals("[]")) return null;
String[] vals = data.substring(1, data.length()-1).split(",");
TreeNode root = new TreeNode(Integer.parseInt(vals[0]));
Deque<TreeNode> queue = new LinkedList<TreeNode>() {{
offer(root);
}};
int i = 1;
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (!vals[i].equals("null")) {
node.left = new TreeNode(Integer.parseInt(vals[i]));
queue.offer(node.left);
}
i++;
if (!vals[i].equals("null")) {
node.right = new TreeNode(Integer.parseInt(vals[i]));
queue.offer(node.right);
}
i++;
}
return root;
}
}
注:
相比于题目给定的 “[1,2,3,null,null,4,5]” 会多输出两个null 。 但本题的测试的是 序列化 和 反序列化 是否可逆,因此 “序列化列表的形式” 并未限制,只要两个函数可以互逆就好了。
3 复杂性分析
- 时间复杂度 O(N) : N为二叉树的节点数,按层构建二叉树需要遍历整个vals ,其长度最大为 2N+1 ;
- 空间复杂度 O(N) : 最差情况下,队列 queue 同时存储 (N+1)/2个节点,因此使用 O(N) 额外空间;
二叉搜索树的第k大节点
1 题目描述
给定一棵二叉搜索树,请找出其中第k大的节点。
示例 1:
示例 2:
限制:
1 ≤ k ≤ 二叉搜索树元素个数
2 解题(Java)
2.1 解题思路
- 二叉搜索树的中序遍历为递增序列;
- 易得二叉搜索树的中序遍历倒序为递减序列;
- 因此转化为求“中序遍历倒序的第k个节点”;
4. 递归遍历时计数,统计当前节点的序号;
5. 递归到第k个节点时,记录结果res;
6. 记录结果后,遍历失去意义,提前终止(即返回);
2.2 递归解析
- 终止条件:当节点root为空(越过叶节点),直接返回;
- 递归右子树:即dfs(root.right);
- 三项工作:
- 提前返回:如果k == 0,代表已找到目标节点,无需继续遍历,因此直接返回;
- 统计序号:执行k = k -1(即从k减至0);
- 记录结果:若k == 0,代表当前节点为第k大的节点,因此记录res = root.val;
- 递归左子树:dfs(root.left);
2.3 代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
int res, k;
public int kthLargest(TreeNode root, int k) {
this.k = k;
dfs(root);
return res;
}
private void dfs(TreeNode root) {
if (root == null || k <= 0) return;
dfs(root.right);
if (--k == 0) res = root.val;
dfs(root.left);
}
}
3 复杂性分析
- 时间复杂度 O(N): 当树退化为链表时(全部为右子节点),无论 k 的值大小,递归深度都为 N ,使用 O(N) 时间;
- 空间复杂度 O(N) : 当树退化为链表时(全部为右子节点),系统使用 O(N) 大小的栈空间;
平衡二叉树
1 题目描述
输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
示例 1:
给定二叉树 [3,9,20,null,null,15,7]
返回 true 。
示例 2:
给定二叉树 [1,2,2,3,3,null,null,4,4]
返回 false 。
限制:
1 <= 树的结点个数 <= 10000
2 解题(Java)
2.1 解题思路
对二叉树做后序遍历,从底至顶返回子树深度,若判定某子树不是平衡树则“剪枝”,直接向上返回。
2.2 算法流程
recur(root)函数:
- 返回值:
- 返回值:
- 当节点root左右子树的深度差<2,返回当前子树的深度,即节点root的左/右子树的深度最大值+1(max(left, right) + 1);
- 当节点root左右子树的深度差>2,返回-1,代表此子树不是平衡树;
- 终止条件:
- 当root为空:说明越过叶节点,返回高度0;
- 当左/右子树的深度=-1:代表此树的左/右子树不是平衡树,因此剪枝,直接返回-1;
- 返回值:
isBalanced(root)函数:
- 返回值:若recur(root) != -1,说明此树平衡,返回true;否则返回false.
2.3 代码
后序遍历:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isBalanced(TreeNode root) {
return recur(root) != -1;
}
private int recur(TreeNode root) {
if (root == null) return 0;
int left = recur(root.left);
if (left == -1) return -1;
int right = recur(root.right);
if (right == -1) return -1;
return Math.abs(left - right) < 2 ? Math.max(left, right) + 1 : -1;
}
}
3 复杂性分析
- 时间复杂度 O(N): N 为树的节点数;最差情况下,需要递归遍历树的所有节点;
- 空间复杂度 O(N): 最差情况下(树退化为链表时),递归需要使用 O(N) 的栈空间;
二叉搜索树的最近公共祖先
1 题目描述
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
说明:
- 所有节点的值都是唯一的;
- p、q 为不同节点且均存在于给定的二叉搜索树中;
2 解题(Java)
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(root != null) {
if(root.val < p.val && root.val < q.val) // p,q 都在 root 的右子树中
root = root.right; // 遍历至右子节点
else if(root.val > p.val && root.val > q.val) // p,q 都在 root 的左子树中
root = root.left; // 遍历至左子节点
else break;
}
return root;
}
}
3 复杂性分析
- 时间复杂度 O(N): 其中 N 为二叉树节点数;每循环一轮排除一层,二叉搜索树的层数最小为 logN (满二叉树),最大为 N (退化为链表);
- 空间复杂度 O(1): 使用常数大小的额外空间;
二叉树的最近公共祖先
1 题目描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉树中。
2 解题(Java)
2.1 解题思路
-
最近公共祖先: 设节点 root 为节点 p, q 的某公共祖先,若其左子节点 root.left 和右子节点 root.right 都不是 p,q 的公共祖先,则称 root 是 “最近的公共祖先” 。
-
故,若 root是 p, q 的 最近公共祖先 ,则只可能为以下情况之一:
- p 和 q 在 root 的子树中,且分列 root 的 异侧(即分别在左、右子树中);
- p = root,且 q 在 root 的左或右子树中;
- q = root,且 p 在 root 的左或右子树中;
-
考虑通过递归对二叉树进行后序遍历,当遇到节点 p 或 q 时返回。从底至顶回溯,当节点 p, q 在节点 root 的异侧时,节点 root 即为最近公共祖先,则向上返回 root。
2.2 代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left == null) return right;
if(right == null) return left;
return root;
}
}
注:
先递归至底部,再对返回值做处理,并回溯,因此是后序遍历。
3 复杂性分析
- 时间复杂度 O(N) : 其中 N 为二叉树节点数,最差情况下,需要递归遍历树的所有节点;
- 空间复杂度 O(N) : 最差情况下,递归深度达到 N ,系统使用 O(N) 大小的额外空间;
实现二叉树先序、中序和后序遍历
1 题目描述
分别按照二叉树先序,中序和后序打印所有的节点。
示例1
输入
{1,2,3}
返回值
[[1,2,3],[2,1,3],[2,3,1]]
备注:
n < 10 ^ 6
2 解题(Java)
import java.util.*;
/*
* public class TreeNode {
* int val = 0;
* TreeNode left = null;
* TreeNode right = null;
* }
*/
public class Solution {
/**
*
* @param root TreeNode类 the root of binary tree
* @return int整型二维数组
*/
List<Integer> ans = new ArrayList<>();
public int[][] threeOrders (TreeNode root) {
int[][] res = new int[3][];
//开始先序遍历,并将结果保存到数组
preorder(root);
res[0] = new int[ans.size()];
for (int i=0; i<ans.size(); i++) res[0][i] = ans.get(i);
ans.clear();
//开始中序遍历,并将结果保存到数组
inorder(root);
res[1] = new int[ans.size()];
for (int i=0; i<ans.size(); i++) res[1][i] = ans.get(i);
ans.clear();
//开始后序遍历,并将结果保存到数组
postorder(root);
res[2] = new int[ans.size()];
for (int i=0; i<ans.size(); i++) res[2][i] = ans.get(i);
//返回结果
return res;
}
void preorder(TreeNode root) {
if (root != null) {
ans.add(root.val);
preorder(root.left);
preorder(root.right);
}
}
void inorder(TreeNode root) {
if (root != null) {
inorder(root.left);
ans.add(root.val);
inorder(root.right);
}
}
void postorder(TreeNode root) {
if (root != null) {
postorder(root.left);
postorder(root.right);
ans.add(root.val);
}
}
}