力扣刷题笔记已开源:
传送门
【Author:ArtBoy 文艺倾年】
文章目录
- 励志
- 目录
- 一、剑指 Offer 12. 矩阵中的路径
- 二、剑指 Offer 13. 机器人的运动范围
- 三、剑指 Offer 26. 树的子结构
- 四、剑指 Offer 27. 二叉树的镜像
- 五、剑指 Offer 28. 对称的二叉树
- 六、剑指 Offer 32 - I. 从上到下打印二叉树
- 七、剑指 Offer 32 - II. 从上到下打印二叉树 II
- 八、剑指 Offer 32 - III. 从上到下打印二叉树 III
- 九、剑指 Offer 34. 二叉树中和为某一值的路径
- 十、剑指 Offer 36. 二叉搜索树与双向链表
- 十一、剑指 Offer 37. 序列化二叉树
- 十二、剑指 Offer 38. 字符串的排列
- 十三、剑指 Offer 54. 二叉搜索树的第 k 大节点
- 十四、剑指 Offer 55 - I. 二叉树的深度
- 十五、剑指 Offer 55 - II. 平衡二叉树
- 十六、剑指 Offer 64. 求 1 + 2 + … + n
- 十七、剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
- 十八、剑指 Offer 68 - II. 二叉树的最近公共祖先
励志
There’s no better way to overpower a trickle of doubt than with a flood of naked truth.
如洪水般的事实是打破丝丝疑虑的最好方式。
题目来源:
https://leetcode-cn.com/leetbook/detail/illustration-of-algorithm/
配合学习文档:
https://www.kancloud.cn/alex_wsc/dataalg/1853982
书籍推荐:
《漫画算法》
视频推荐:
https://www.bilibili.com/video/BV1E4411H73v
目录
一、剑指 Offer 12. 矩阵中的路径
题:
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
例如,在下面的 3×4 的矩阵中包含单词 “ABCCED”(单词中的字母已标出)。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["a","b"],["c","d"]], word = "abcd"
输出:false
提示:
1 <= board.length <= 200
1 <= board[i].length <= 200
board 和 word 仅由大小写英文字母组成
解:
解题思路:DFS + 剪枝
AC代码:
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for(int i = 0; i < board.length; i++){
for(int j = 0; j < board[0].length; j++){
if(dfs(board, words, i, j, 0)) return true;
}
}
return false;
}
boolean dfs(char[][] board, char[] words, int i, int j, int k){
if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != words[k]) return false; // 下标越界
if(k == words.length - 1) return true; // 全部匹配
board[i][j] = '\0'; // 无效标记符(下一步要上下左右探路,已经走过的不能走啦,'\0'防止和words中的字符重复)
boolean res = dfs(board, words, i+1, j, k+1) || dfs(board, words, i-1, j, k+1)
|| dfs(board, words, i, j+1, k+1) || dfs(board, words, i, j-1, k+1); // 上下右左试探,并联
board[i][j] = words[k];
return res;
}
}
M,N 分别为矩阵行列大小, K 为字符串 word 长度。
- 时间复杂度 O(3K MN): 最差情况下,需要遍历矩阵中长度为 K 字符串的所有方案,时间复杂度为 O(3K);矩阵中共有 MN个起点,时间复杂度为 O(MN)。
- 方案数计算: 设字符串长度为 K ,搜索中每个字符有上、下、左、右四个方向可以选择,舍弃回头(上个字符)的方向,剩下3种选择,因此方案数的复杂度为 O(3K)。
- 空间复杂度 O(K) : 搜索过程中的递归深度不超过 K ,因此系统因函数调用累计使用的栈空间占用 O(K)(因为函数返回后,系统调用的
栈空间会释放
)。最坏情况下 K = MN ,递归深度为 MN ,此时系统栈使用 O(MN) 的额外空间。
二、剑指 Offer 13. 机器人的运动范围
题:
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
示例 2:
输入:m = 3, n = 1, k = 0
输出:1
提示:
1 <= n,m <= 100
0 <= k <= 20
解:
解题思路:DFS+ 剪枝
数位之和计算模板:
int sums(int x)
int s = 0;
while(x != 0) {
s += x % 10; // 得到 x 的个位数字
x = x / 10; // 令 x 的十进制数向右移动一位,即删除个位数字。
}
return s;
该题中机器人一次只能移动一格,每次计算只需要x到x+1的数位和增量
(x + 1) % 10 != 0 ? s_x + 1 : s_x - 8;
说明:
- x = 11, x + 1 = 12, 数位和:2 ,3(2+1)
- x = 19,x + 1 = 20, 数位和:10,2(10-8)
根据可达解的结构和连通性,易推出机器人可 仅通过向右和向下移动
,访问所有可达解 。
AC代码:
class Solution {
int m, n, k;
boolean[][] visited;
public int movingCount(int m, int n, int k) {
this.m = m; this.n = n; this.k = k;
this.visited = new boolean[m][n];
return dfs(0, 0, 0, 0);
}
public int dfs(int i, int j, int si, int sj){ // 坐标,数位和
if(i >= m || j >= n || si + sj > k || visited[i][j]) return 0; // 剪枝
visited[i][j] = true; // 标记
int down = dfs(i+1, j, cal(i, si), sj); // 下寻找
int right = dfs(i, j+1, si, cal(j, sj)); // 右寻找
return 1 + down + right;
}
public int cal(int x, int sum){ // 数位和增量
return (x + 1) % 10 != 0 ? sum + 1 : sum - 8;
}
}
解题思路:BFS
AC代码:
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
int res = 0;
LinkedList<int[]> queue = new LinkedList<>();
queue.add(new int[]{0, 0, 0, 0}); // i,j,si,sj
while(!queue.isEmpty()){
int[] x = queue.removeFirst();
int i = x[0], j = x[1], si = x[2], sj = x[3];
if(i >= m || j >= n || si + sj > k || visited[i][j]) continue;
visited[i][j] = true;
res++;
queue.add(new int[]{i+1, j , cal(i,si), sj});
queue.add(new int[]{i, j+1, si, cal(j,sj)});
}
return res;
}
public int cal(int x, int sum){
return (x + 1)%10 != 0 ? sum + 1 : sum - 8;
}
}
三、剑指 Offer 26. 树的子结构
题:
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树 A:
3
/ \
4 5
/ \
1 2
给定的树 B:
4
/
1
返回 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
解:
解题思路:先序遍历A + 判断 A 是否包含 B
(双层dfs)
AC代码:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
// 空树不是任意树的子结构
if(A == null || B == null) return false;
// 当前节点判断
if(recur(A,B)) return true;
// 左右节点判断
return isSubStructure(A.left, B) || isSubStructure(A.right, B);
}
// 比对当前节点
boolean recur(TreeNode A, TreeNode B){
// B匹配结束
if(B == null) return true;
// 先判断为空,再判断它的val是否相等,否则会报空指针异常
if(A == null || A.val != B.val) return false;
return recur(A.left, B.left) && recur(A.right, B.right);
}
}
- 时间复杂度 O(MN): 其中 M, N 分别为树 A 和 树 B 的节点数量;先序遍历树 A 占用 O(M) ,每次调用recur(A, B) 判断占用 O(N) 。
- 空间复杂度 O(M): 当树 A 和树 B 都退化为链表时,递归调用深度最大。当 M≤N 时,遍历树 A 与递归判断的总递归深度为 M;当 M>N 时,最差情况为遍历至树 A 的叶节点,此时总递归深度为 M。
四、剑指 Offer 27. 二叉树的镜像
题:
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
例如输入:
4
/ \
2 7
/ \ / \
1 3 6 9
镜像输出:
4
/ \
7 2
/ \ / \
9 6 3 1
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
限制:
0 <= 节点个数 <= 1000
解:
解题思路:递归
AC代码:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode mirrorTree(TreeNode root) {
// 取值前必判空,防止空异常
if(root == null) return null;
TreeNode temp = root.left;
// 右翻转赋左
root.left = mirrorTree(root.right);
// temp翻转赋右
root.right = mirrorTree(temp);
return root;
}
}
- 时间复杂度 O(N): 其中 N 为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点,占用 O(N) 时间。
- 空间复杂度 O(N) : 最差情况下(当二叉树退化为链表),递归时系统需使用 O(N) 大小的栈空间。
解题思路:辅助栈遍历交换
AC代码:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
// 通过栈遍历整个树(储存待访问节点,弹出已访问的节点)
LinkedList<TreeNode> stack = new LinkedList<>(){{add(root);}};
while(!stack.isEmpty()){
// 弹出正在访问的节点
TreeNode node = stack.removeLast();
// 记录左右节点,下一次访问
if(node.left != null) stack.add(node.left);
if(node.right != null) stack.add(node.right);
// 交换
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
}
return root;
}
}
- 时间复杂度 O(N) : 其中 N 为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点,占用 O(N) 时间。
- 空间复杂度 O(N) : 如下图所示,最差情况下,栈 stack 最多同时存储
N
+
1
2
\frac{N+1}{2}
2N+1 个节点,占用O(N) 额外空间。
五、剑指 Offer 28. 对称的二叉树
题:
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
限制:
0 <= 节点个数 <= 1000
解:
解题思路:递归遍历、逐一比对
AC代码:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return recur(root.left, root.right);
}
boolean recur(TreeNode L, TreeNode R){
// L,R同时递归结束
if(L == null && R == null) return true;
// L或R有一提前结束,值不等
if(L == null || R == null || L.val != R.val) return false;
return recur(L.left, R.right) && recur(L.right, R.left);
}
}
- 时间复杂度 O(N) : 其中 N 为二叉树的节点数量,每次执行 recur() 可以判断一对节点是否对称,因此最多调用 N/2 次recur() 方法。
- 空间复杂度 O(N) : 如下图所示,最差情况下(二叉树退化为链表),系统使用 O(N) 大小的空间。
六、剑指 Offer 32 - I. 从上到下打印二叉树
题:
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回:
[3,9,20,15,7]
提示:
节点总数 <= 1000
解:
解题思路:BFS
AC代码:
/**
* 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> res = new ArrayList<>();
// BFS模板
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
TreeNode node = queue.removeFirst();
res.add(node.val);
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
// List转数组
int[] ans = new int[res.size()];
for(int i = 0; i < res.size(); i++){
ans[i] = res.get(i);
}
return ans;
}
}
- 时间复杂度 O(N) : N 为二叉树的节点数量,即 BFS 需循环 N 次。
- 空间复杂度 O(N) : 最差情况下,即当树为平衡二叉树时,最多有 N/2个树节点同时在 queue 中,使用 O(N) 大小的额外空间。
七、剑指 Offer 32 - II. 从上到下打印二叉树 II
题:
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
提示:
节点总数 <= 1000
解:
解题思路:BFS
AC代码:
/**
* 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;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
List<Integer> temp = new ArrayList<>();
// 层序遍历并输出
int num = queue.size(); // 获得该层的节点个数
while((num --) > 0){
TreeNode node = queue.removeFirst();
temp.add(node.val);
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
res.add(temp);
}
return res;
}
}
八、剑指 Offer 32 - III. 从上到下打印二叉树 III
题:
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[20,9],
[15,7]
]
提示:
节点总数 <= 1000
解:
解题思路:层序遍历
+ 双端队列
AC代码:
/**
* 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;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
LinkedList<Integer> temp = new LinkedList<>();
int num = queue.size(); // 层序遍历
while((num --) > 0){
TreeNode node = queue.removeFirst();
if(res.size() % 2 == 0){ // 刚开始长度为0
temp.add(node.val);
}else{
temp.addFirst(node.val);
}
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
res.add(temp);
}
return res;
}
}
解题思路:
AC代码:
/**
* 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;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
List<Integer> temp = new ArrayList<>();
int num = queue.size(); // 层序遍历
while((num --) > 0){
TreeNode node = queue.removeFirst();
temp.add(node.val);
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
if(res.size()%2 != 0) Collections.reverse(temp);
res.add(temp);
}
return res;
}
}
九、剑指 Offer 34. 二叉树中和为某一值的路径
题:
输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 target = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ / \
7 2 5 1
返回:
[
[5,4,11,2],
[5,8,4,5]
]
提示:
节点总数 <= 10000
解:
解题思路:先序遍历
+ 路径记录
AC代码:
/**
* 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 {
LinkedList<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int target) {
dfs(root, target);
return res;
}
void dfs(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 LinkedList(path));
}
dfs(root.left, tar);
dfs(root.right, tar);
path.removeLast();
}
}
十、剑指 Offer 36. 二叉搜索树与双向链表
题:
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例:
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。
解:
解题思路:中序遍历
+双向链表
+循环链表
值得注意的是:该题中二叉搜索树的中序遍历可实现递增序列
AC代码:
/*
// 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右移
pre = cur;
dfs(cur.right); // 右
}
}
- 时间复杂度 O(N) : N 为二叉树的节点数,中序遍历需要访问所有节点。
- 空间复杂度 O(N) : 最差情况下,即树退化为链表时,递归深度达到 N,系统使用 O(N) 栈空间。
十一、剑指 Offer 37. 序列化二叉树
题:
请实现两个函数,分别用来序列化和反序列化二叉树。
你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
示例:
输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]
解:
解题思路:层序遍历
AC代码:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if(root == null) return "[]";
StringBuilder res = new StringBuilder("[");
// BFS
LinkedList<TreeNode> queue = new LinkedList<>(){{add(root);}};
while(!queue.isEmpty()){
TreeNode node = queue.removeFirst();
if(node != null){
res.append(node.val + ",");
queue.add(node.left);
queue.add(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(",");
// BFS
int index = 0; // 遍历数组指针
TreeNode root = new TreeNode(Integer.parseInt(vals[index++]));
LinkedList<TreeNode> queue = new LinkedList<>(){{add(root);}};
while(!queue.isEmpty()){
TreeNode node = queue.removeFirst();
if(!vals[index].equals("null")){
node.left = new TreeNode(Integer.parseInt(vals[index]));
queue.add(node.left);
}
index ++;
if(!vals[index].equals("null")){
node.right = new TreeNode(Integer.parseInt(vals[index]));
queue.add(node.right);
}
index ++;
}
return root;
}
}
// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));
- 时间复杂度 O(N) : N 为二叉树的节点数,层序遍历需要访问所有节点,最差情况下需要访问 N + 1 个 null ,总体复杂度为
O( N + 1 2 \frac{N+1}{2} 2N+1) = O(N)。 - 空间复杂度 O(N) : 最差情况下,队列 queue 同时存储2N+1个节点(或 N+1 个 null ),使用 O(N) ;列表res 使用 O(N) 。
十二、剑指 Offer 38. 字符串的排列
题:
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
限制:
1 <= s 的长度 <= 8
解:
解题思路:dfs
+ 回溯
图解:
列出1,2,3所有的全排列逐一dfs遍历添加至res
对于一个长度为 n 的字符串(假设字符互不重复),其排列方案数共有:n×(n−1)×(n−2)…×2×1
swap() 函数:
AC代码:
class Solution {
List<String> res = new LinkedList<>();
char[] c;
public String[] permutation(String s) {
c = s.toCharArray();
dfs(0);
return res.toArray(new String[res.size()]);
}
void dfs(int x) {
if(x == c.length - 1) {
res.add(String.valueOf(c)); // 添加排列方案
return;
}
HashSet<Character> set = new HashSet<>();
for(int i = x; i < c.length; i++) {
if(set.contains(c[i])) continue; // 重复,因此剪枝
set.add(c[i]);
swap(i, x); // 交换,将 c[i] 固定在第 x 位
dfs(x + 1); // 开启固定第 x + 1 位字符
swap(i, x); // 恢复交换
}
}
void swap(int a, int b) {
char tmp = c[a];
c[a] = c[b];
c[b] = tmp;
}
}
- 时间复杂度 O(N!N): N 为字符串 s 的长度;时间复杂度和字符串排列的方案数成线性关系,方案数为N×(N−1)×(N−2)…×2×1 , 即复杂度为 O(N!);字符串拼接操作 join() 使用 O(N) ;因此总体时间复杂度为O(N!N)。
- 空间复杂度 O(N2): 全排列的递归深度为 N ,系统累计使用栈空间大小为 O(N);递归中辅助 Set 累计存储的字符数量最多为N + (N-1) + … + 2 + 1 = (N+1)N/2,即占用 O(N2) 的额外空间。
十三、剑指 Offer 54. 二叉搜索树的第 k 大节点
题:
给定一棵二叉搜索树,请找出其中第k大的节点。
示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
输出: 4
示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/ \
3 6
/ \
2 4
/
1
输出: 4
限制:
1 ≤ k ≤ 二叉搜索树元素个数
解:
解题思路:中序遍历倒序
AC代码:
/**
* 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; // 返回的结果,k
public int kthLargest(TreeNode root, int k) {
this.k = k;
dfs(root); // 逆中序遍历
return res;
}
void dfs(TreeNode root){
if(root == null) return;
dfs(root.right); // 右
k--;
if(k == 0){
res = root.val;
return;
}
dfs(root.left); // 左
}
}
- 时间复杂度 O(N): 当树退化为链表时(全部为右子节点),无论 k 的值大小,递归深度都为 N ,占用 O(N)时间。
- 空间复杂度 O(N): 当树退化为链表时(全部为右子节点),系统使用 O(N)大小的栈空间。
十四、剑指 Offer 55 - I. 二叉树的深度
题:
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
提示:
节点总数 <= 10000
解:
解题思路:DFS(后序遍历)
AC代码:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0; // 出口
return Math.max(maxDepth(root.left),maxDepth(root.right)) + 1;
}
}
- 时间复杂度 O(N): N 为树的节点数量,计算树的深度需要遍历所有节点。
- 空间复杂度 O(N): 最差情况下(当树退化为链表时),递归深度可达到 N。
解题思路:BFS
AC代码:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
LinkedList<TreeNode> queue = new LinkedList<>(){{add(root);}};
int res = 0;
while(!queue.isEmpty()){
LinkedList<TreeNode> temp = new LinkedList<>();
// 层序遍历
for(TreeNode node : queue){
if(node.left != null) temp.add(node.left);
if(node.right != null) temp.add(node.right);
}
queue = temp;
res ++;
}
return res;
}
}
- 时间复杂度 O(N): N 为树的节点数量,计算树的深度需要遍历所有节点。
- 空间复杂度 O(N) : 最差情况下(当树平衡时),队列 queue 同时存储 N/2 个节点。
十五、剑指 Offer 55 - II. 平衡二叉树
题:
输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
示例 1:
给定二叉树 [3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
返回 true 。
示例 2:
给定二叉树 [1,2,2,3,3,null,null,4,4]
1
/ \
2 2
/ \
3 3
/ \
4 4
返回 false 。
限制:
0 <= 树的结点个数 <= 10000
解:
解题思路:先序遍历
+ 判断深度
(从顶至底)
AC代码:
/**
* 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) {
if(root == null) return true;
return Math.abs(dfs(root.left) - dfs(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
// 返回节点的深度
int dfs(TreeNode root){
if(root == null) return 0;
return Math.max(dfs(root.left), dfs(root.right)) + 1;
}
}
十六、剑指 Offer 64. 求 1 + 2 + … + n
题:
求 1+2+…+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
示例 1:
输入: n = 3
输出: 6
示例 2:
输入: n = 9
输出: 45
限制:
1 <= n <= 10000
解:
解题思路:位运算
AC代码:
class Solution {
int res = 0;
public int sumNums(int n) {
boolean x = n > 1 && sumNums(n-1) > 0;
res += n;
return res;
}
}
十七、剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
题:
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 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 为不同节点且均存在于给定的二叉搜索树中。
解:
解题思路:递归
二叉查找树:值唯一、左小于右
祖先的定义: 若节点 p 在节点 root 的左(右)子树中,或 p = root,则称 root 是 p 的祖先。
最近公共祖先: 设节点 root 为节点 p,q 的某公共祖先,若其左子节点 root.left 和右子节点 root.right 都不是 p,q 的公共祖先,则称 root 是 “最近的公共祖先” 。
AC代码:
/**
* 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.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right,p,q);
// 在根节点左边
if(root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left,p,q);
// 异边
return root;
}
}
- 时间复杂度 O(N): 其中 N 为二叉树节点数;每循环一轮排除一层,二叉搜索树的层数最小为logN(满二叉树),最大为 N (退化为链表)。
- 空间复杂度 O(N) : 最差情况下,即树退化为链表时,递归深度达到树的层数 N 。
十八、剑指 Offer 68 - II. 二叉树的最近公共祖先
题:
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 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 为不同节点且均存在于给定的二叉树中。
解:
解题思路:
AC代码:
/**
* 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 && right != null) return root; // 左右子树都有
else return left != null ? left : right;
}
}