刷题日记 - 9
最近更新:2025-10-20   |   字数总计:3.9k   |   阅读估时:17分钟   |   阅读量:
  1. 刷题日记 - 9
    1. 86. 分隔链表 - 力扣(LeetCode)
    2. 102. 二叉树的层序遍历 - 力扣(LeetCode)
    3. 二叉树的遍历 - 栈
      1. 144. 二叉树的前序遍历 - 力扣(LeetCode)
      2. 94. 二叉树的中序遍历 - 力扣(LeetCode)
      3. 145. 二叉树的后序遍历 - 力扣(LeetCode)
      4. 使用栈模拟二叉树前序、中序和后序遍历的区别
        1. 栈操作流程
        2. 栈操作中的关键差异
        3. 注意事项
    4. 902. 最大为 N 的数字组合 - 力扣(LeetCode)
      1. PDD面试题

刷题日记 - 9

86. 分隔链表 - 力扣(LeetCode)

难度:中等

链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public ListNode partition(ListNode head, int x) {

ListNode dummy1 = new ListNode(-111); // 赋值为-111,标记这是dummy节点
ListNode dummy2 = new ListNode(-111); // dummy1链表是小于x的节点,dummy2是大于等于x的节点
ListNode t1 = dummy1,t2 = dummy2;
ListNode p = head;

while(p!=null){ // 遍历整个初始链表,按照每个节点的值分别放入不同的dummy中
int val = p.val;
if(val<x){
t1.next = p;
t1 = t1.next;
}else if(val>=x){
t2.next = p;
t2 = t2.next;
}
p = p.next;
}

ListNode dummy = new ListNode(-1); // 最后将dummy1和dummy2串成一串。
ListNode t = dummy;
if(t1.val!=-111){
t.next = dummy1.next;
t = t1;
}
if(t2.val!=-111){
t.next = dummy2.next;
t = t2;
}
t.next = null;
return dummy.next;
}
}

102. 二叉树的层序遍历 - 力扣(LeetCode)

难度:中等

二叉树 队列 BFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>(); // 用于存储最终的层次遍历结果
if (root == null) return res; // 如果根节点为空,直接返回空列表

Queue<TreeNode> queue = new LinkedList<>(); // 创建一个队列,用于广度优先搜索
queue.offer(root); // 将根节点入队
queue.offer(null); // 用 null 作为层与层之间的分隔符

while (true) {
List<Integer> cur = new ArrayList<>(); // 用于存储当前层的节点值
while (queue.peek() != null) { // 当队列前端不是 null 时,处理当前层的所有节点
TreeNode curNode = queue.poll(); // 出队一个节点
cur.add(curNode.val); // 将该节点的值添加到当前层的列表中
if (curNode.left != null) queue.offer(curNode.left); // 如果左子节点不为空,入队
if (curNode.right != null) queue.offer(curNode.right); // 如果右子节点不为空,入队
}
res.add(cur); // 将当前层的结果添加到最终结果列表中
queue.poll(); // 移除层与层之间的分隔符(即 null)
if (queue.isEmpty()) break; // 如果队列为空,说明遍历结束,跳出循环
queue.offer(null); // 在队列末尾加入新的层分隔符
}
return res; // 返回最终的层次遍历结果
}
}

难点:

  • 层次分割的处理:通过在队列中加入 null 作为层与层之间的分隔符。
  • 利用队列实现层序遍历(二叉树的BFS)

二叉树的遍历 - 栈

不使用递归实现,而是使用栈。

144. 二叉树的前序遍历 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>(); // 用于存储前序遍历结果
if (root == null) return res; // 如果根节点为空,返回空列表

Stack<TreeNode> stack = new Stack<>(); // 创建一个栈用于模拟递归调用
stack.push(root); // 将根节点压入栈中

while (!stack.isEmpty()) { // 当栈不为空时,继续遍历
TreeNode p = stack.pop(); // 弹出栈顶节点
res.add(p.val); // 将该节点的值加入结果列表中

// 由于栈是后进先出 (LIFO),我们先压入右子节点,后压入左子节点
// 这样在下一次循环中,左子节点会先被处理,实现了前序遍历的顺序
if (p.right != null) stack.push(p.right); // 如果右子节点不为空,将右子节点压入栈
if (p.left != null) stack.push(p.left); // 如果左子节点不为空,将左子节点压入栈
}

return res; // 返回前序遍历结果
}
}

前序遍历的顺序是:根节点 -> 左子树 -> 右子树

为了实现这一顺序,我们需要注意压栈的顺序。首先将右子节点压入栈,再将左子节点压入栈。这样在下一个循环中,左子节点会先从栈中弹出并被处理,确保遍历顺序符合前序遍历的要求。

94. 二叉树的中序遍历 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>(); // 用于存储中序遍历结果
if (root == null) return res; // 如果根节点为空,返回空列表

Stack<TreeNode> stack = new Stack<>(); // 创建一个栈用于模拟递归调用
stack.push(root); // 将根节点压入栈中

while (!stack.isEmpty()) { // 当栈不为空时,继续遍历
TreeNode p = null;

// 将当前节点的所有左孩子压入栈
while (true) {
p = stack.peek(); // 查看栈顶节点
if (p.left != null) { // 如果当前节点有左子节点
stack.push(p.left); // 将左子节点压入栈
} else break; // 如果没有左子节点,停止压栈
}

// 依次弹出栈中的节点并处理
TreeNode pop;
while (!stack.isEmpty()) {
pop = stack.pop(); // 弹出栈顶节点 ⭐ 栈顶节点的左孩子要么没有,要么被处理完了
res.add(pop.val); // 将节点的值加入结果列表中

// 如果弹出的节点有右子节点,将其压入栈中 ⭐
if (pop.right != null) {
stack.push(pop.right); // 压入右子节点
break; // 进入下一轮循环处理右子树
}
}
}

return res; // 返回中序遍历结果
}
}

在处理左子树时,需要不断地将左孩子节点压入栈中,直到没有左孩子为止。只要没有左孩子,或者左孩子处理完毕,就可以弹出当前节点

当我们弹出栈顶节点时,如果该节点有右子树,我们需要将右子树的根节点压入栈,然后继续按照中序的顺序处理右子树。

145. 二叉树的后序遍历 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
// 结果列表,用于存储后序遍历的节点值
List<Integer> res = new ArrayList<>();
// 如果根节点为空,直接返回空的结果列表
if (root == null) return res;

// 使用栈来模拟递归调用的过程
Stack<TreeNode> stack = new Stack<>();
// 记录最后一个被弹出的节点,用于判断右子树是否已经被访问
TreeNode lastPop = null;
// 将根节点压入栈中
stack.push(root);

// 只要栈不为空,继续遍历
while (!stack.isEmpty()) {
// 递归入栈左孩子
TreeNode p = null;
while (true) {
p = stack.peek(); // 获取栈顶节点
if (p.left != null) {
// 如果有左孩子,将左孩子入栈
stack.push(p.left);
} else break; // 如果没有左孩子,退出内循环
}

// 开始出栈
TreeNode cur = null;
while (!stack.isEmpty()) { // 处理栈顶的节点
cur = stack.peek(); // 获取当前栈顶节点 ⭐还不能pop

// 如果右孩子存在且右孩子还未被处理
if (cur.right != null && lastPop != cur.right) {
// 将右孩子入栈,并退出内循环以继续处理右子树
stack.push(cur.right);
break;
} else {
// 否则,右子树已经处理完毕,可以安全地访问当前节点
res.add(cur.val); // 将当前节点的值加入结果列表
stack.pop(); // 弹出当前节点
lastPop = cur; // 更新最后一个被弹出的节点
}
}
}
// 返回结果列表
return res;
}
}

是否弹出当前节点 –> 不仅要判断左孩子是否被处理了,还要判断右孩子是否被处理。


使用栈模拟二叉树前序、中序和后序遍历的区别

栈操作流程
  • 前序遍历:
    • 根节点先入栈:首先将根节点入栈。
    • 先处理根,再处理子节点:从栈中弹出根节点,处理后依次将右孩子和左孩子入栈(注意先右后左,这样出栈时先处理左子树)。
    • 重复上述步骤:继续弹栈并处理,直到栈为空。
  • 中序遍历: ⭐
    • 左孩子依次入栈:先将当前节点的所有左孩子压入栈,直到没有左孩子为止。⭐
    • 弹栈并处理根节点:弹出栈顶节点,处理后,检查是否有右孩子。⭐
    • 处理右子树:如果有右孩子,将右孩子作为新的当前节点,重复左孩子入栈的过程。⭐
  • 后序遍历:
    • 左孩子先入栈:与中序遍历类似,先将左孩子入栈。⭐
    • 右孩子处理顺序:在弹出根节点前,必须先处理右孩子。需要特别注意判断右子树是否已经被处理过。⭐
    • 根节点最后处理:只有在左子树和右子树都处理完毕后,才处理根节点。⭐
栈操作中的关键差异
  • 前序遍历
    • 栈的顺序:由于前序遍历是先处理根节点,因此在入栈时,需要先将右孩子压入栈,再将左孩子压入栈,这样出栈时能够先处理左子树。
  • 中序遍历
    • 递归性强:中序遍历在访问节点时,需要先将所有左孩子压栈,出栈后再检查右子树。这种操作使得中序遍历在栈的使用上较为直观和递归。
  • 后序遍历
    • 节点的双重检查:后序遍历最复杂的部分在于对右子树的处理,需要在出栈之前确认右孩子是否已经访问过。如果右孩子还未访问,则需要将右孩子入栈继续处理。
注意事项
  • 前序遍历
    • 需要注意入栈顺序,先右后左,以确保左子树先于右子树处理。
  • 中序遍历
    • 处理左子树时,需要注意什么时候该弹栈处理根节点,以及处理右子树的时机。
  • 后序遍历
    • 必须时刻跟踪最后访问的节点,确保右子树在根节点之前被访问。需要利用一个额外的指针或变量记录上一次处理的节点(即“lastPop”),来判断右子树是否已经访问。

所以其实只有中序遍历和后续遍历才符合用栈模拟递归

902. 最大为 N 的数字组合 - 力扣(LeetCode)

难度:困难

类似PDD一面手撕算法题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Solution {
int res; // 用于记录最终的结果数量

public int atMostNGivenDigitSet(String[] digits, int n) {
// 将 String[] 数组转换为 char[] 数组
// 因为后面用char[]数组,最后才发现给的原来是String[]...
char[] chars = new char[digits.length];
for (int i = 0; i < digits.length; i++) {
chars[i] = digits[i].charAt(0);
}
// 调用辅助函数计算结果
return fun(chars, n);
}

public int fun(char[] digits, int n) {
String nString = String.valueOf(n); // 将 n 转换为字符串以便逐位处理
int len = nString.length(); // 获取 n 的位数

res = 0; // 初始化结果为 0
Arrays.sort(digits); // 对数字数组进行排序
int digitsNum = digits.length; // 获取可用数字的数量

// 计算位数比 n 小的所有可能数的数量
for (int i = 1; i <= len - 1; i++) {
res += (int) Math.pow(digitsNum, i);
}

// 处理与 n 位数相同的情况
dfs(nString.toCharArray(), digits, 0, false);
return res;
}

private void dfs(char[] n, char[] digits, int pos, boolean flag) {
int len = n.length; // n 的长度
int digitsNum = digits.length; // 可用数字的数量
if (pos == len) { // 极端情况下 需要跳出dfs
if (flag) res += 1; // 如果 flag 为 true,表示构造的这个数等于n
return;
}

char cur = n[pos]; // 当前处理的 n 中的字符
int targetCurIdx = -1; // 用于记录小于当前位数字的最大可用数字的索引
for (int i = digitsNum - 1; i >= 0; i--) { // 从大到小遍历 digits 数组
if (digits[i] == cur) {
// 如果当前位数字等于 n 中对应位数字
dfs(n, digits, pos + 1, true); // 当前这一位相等,在下一位寻找<=的值
}
if (digits[i] < cur) {
// 如果找到一个小于 n 当前位的数字 只要这一位小于,后面的剩余位都可以随便放置
targetCurIdx = i; // 记录其索引
break;
}
}

// 计算小于当前位但位数相同的所有可能数的数量
int useableNum = targetCurIdx + 1;
res += useableNum * Math.pow(digitsNum, len - pos - 1);
}
}

PDD面试题

题意与上题差不多,但是只要求返回小于N的最大值。

我的第一想法就是这个:

  • 先将目标值n转换为char[]便于一位一位地处理

  • digits排序,便于从大到小遍历(不用二分查找,因为最多也就9个数字)

  • 进入dfs函数,从n的高位依次往下遍历,当前位位置为pos,值为cur = char[pos]-'0',在digits数组中从大到小寻找targetCurIdx,值小于等于cur的元素的下标。

    • 如果targetCur等于cur,那么进入下一层``dfs,寻找下一位的targetCur`
    • 如果targetCur小于cur,那么直接可以确定后面的所有值了,全用digits中最大的值填充即可
    • 如果targetCur=-1curdigits中的所有值都小,表明当前位是不能作为低于目标位的,要回到上一层dfs去,尝试用前面的位作为“低”位。

    如果dfs都到了最后一层,表明当前构造出的值等于目标值,也需要回到上一层dfs。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class Main {
public static void main(String[] args) {
int[] digits = {2, 5, 4};
int n = 2544;
System.out.println(new Solution().fun(digits, n));
}
}

class Solution {

public int fun(int[] digits, int n) {
String nString = String.valueOf(n);
int len = nString.length();
int digitsNum = digits.length;

Arrays.sort(digits);
int res = dfs(nString.toCharArray(), digits, 0, 0);

if (res == -1) {
res = 0;
for (int i = 1; i <= len - 1; i++) {
res = res * 10 + digits[digitsNum - 1];
}
}
return res;
}

private int dfs(char[] n, int[] digits, int pos, int res) {
int len = n.length;
int digitsNum = digits.length;
if (pos == len) {
return -1;
}

int cur = n[pos] - '0';
int targetCurIdx = -1;
for (int i = digitsNum - 1; i >= 0; i--) {
if (digits[i] == cur) {
int tmp = dfs(n, digits, pos + 1, res * 10 + digits[i]);
if (tmp != -1) {
return tmp;
}
} else if (digits[i] < cur) {
targetCurIdx = i;
break;
}
}

if (targetCurIdx == -1) {
return -1;
}

res = res * 10 + digits[targetCurIdx];
int leftPosCount = len - 1 - pos;
for (int i = 0; i < leftPosCount; i++) {
res = res * 10 + digits[digitsNum - 1];
}
return res;
}
}

可惜我想的比较复杂,当时又有点紧张,现场没有手撕出来。/(ㄒoㄒ)/~~


也可以不用dfs,用一个指针遍历即可,主要是找到“低”位

分三种情况,详情看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
public class Main {
public static void main(String[] args) {
int[] digits = {5, 9};
int n = 5555;
System.out.println(new Solution().fun(digits, n));
}
}

class Solution {

public int fun(int[] digits, int n) {
int digitsNum = digits.length;
char[] digitChars = new char[digitsNum];
char max = '0';
char min = '9';

for (int i = 0; i < digitsNum; i++) {
digitChars[i] = (char) (digits[i] + '0');
max = digitChars[i] > max ? digitChars[i] : max; // 记录max和min方便后续判断与赋值
min = digitChars[i] < min ? digitChars[i] : min;
}
Arrays.sort(digitChars);

char[] nChars = String.valueOf(n).toCharArray();
int len = nChars.length;

char[] res = new char[len];
Arrays.fill(res, '0');

int p = 0;
int state = 0;
while (p < nChars.length) {
int targetIdx = -1;
char cur = nChars[p];
for (int i = digitsNum - 1; i >= 0; i--) {
if (digitChars[i] == cur) {
res[p] = digitChars[i];
targetIdx = -2;
break;
} else if (digitChars[i] < cur) {
targetIdx = i;
res[p] = digitChars[targetIdx];
break;
}
}

if (targetIdx == -2) { // 只有在前面都等于的时候继续
p++;
} else {
if (targetIdx == -1) state = 1;
if (targetIdx != -1) state = 2;
break;
}
; // 退出while只有三种情况: 1. p=len,这个组合出来的数一定是等于n的 2. 在p这个位置 找到了比对应位置小的 3. 或者找不到对应小的。
}
// 反向
if (state == 0) {
p--;
while (p >= 0 && res[p] <= min) {
res[p] = max;
p--;
}
if (p == -1) {
res[0] = '0';
} else {
for (int i = digitsNum - 1; i >= 0; i--) {
if (digitChars[i] < res[p]) {
res[p] = digitChars[i];
}
}
}
}
if (state == 1) {
for (int i = len - 1; i >= 0; i--) {
if (i < p) break;
res[i] = max;
}
p--;
while (p >= 0 && res[p] <= min) {
res[p] = max;
p--;
}
if (p == -1) {
res[0] = '0';
} else {
for (int i = digitsNum - 1; i >= 0; i--) {
if (digitChars[i] < res[p]) {
res[p] = digitChars[i];
}
}
}
}
if (state == 2) {
for (int i = len - 1; i >= 0; i--) {
if (i <= p) break;
res[i] = max;
}
}

// 将res转化为数字
int resInt = 0;
for (int i = 0; i < len; i++) {
char cur = res[i];
resInt = resInt * 10 + (cur - '0');
}
return resInt; // 返回最终结果
}
}

其实还是挺难的,过程比较繁杂。