算法记录

栈

20. 有效的括号

image-20260616031147052

思路:因为必须保证对称性,因此栈比较合适,只要每次反括号都能与前面对称,就是有效的。

注意:

  1. 如果每次到反括号时去判断,会多一步判断,时间很浪费。直接第一个括号时就push对应反括号,后面比较就很快。
  2. 最后还要判断stack是否为空,因为可能存在有前括号没有反括号的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();

for(char c : s.toCharArray()) {
if(c == '{') stack.push('}');
else if(c == '(') stack.push(')');
else if(c == '[') stack.push(']');
else {
if(stack.isEmpty() || stack.pop() != c) return false;
}
}
return stack.isEmpty();
}
}

滑动窗口

3. 无重复字符的最长子串

image-20260613201439342

思路:使用一个空间记录当前已走的字符和下标。每走一步判断该字符是否在当前最长字符串中重复。重复则更新当前长度。

注意:

  1. 即使字符重复也可能不在当前认为的最长字符中(此时表示可以增加当前最长字符串长度),因为其可能在某个重复字符被连带认为不是最长的了。

二叉树

236. 二叉树的最近公共祖先

image-20260614201111648

思路:判定条件即为,该节点左右子树或本身是否包含p、q。有题目求最近就可以考虑深度优先。

注意:

  1. 注意返回值的设定;
  2. 注意p、q的判定 与 左右子树递归 的时机判断。
  3. 两种方式:
    1. 直接返回root = q或q的root;层层返回,两种情况:
      1. pq在不同子树,此时判断是否左右子树返回都不为null即找到;
      2. pq某一节点为公共祖先,但只要判断root == p或q就会返回root,会被覆盖为公共祖先。
    2. 返回左右子树是否存在pq,此时需要注意判断pq是否为公共祖先。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
TreeNode res = null;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;

if(root == p || root == q) return root; // 如果为公共祖先会root覆盖

TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);

if(left == null && right == null) return null;
if(left == null) return right;
if(right == null) return left;

return root;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
TreeNode res = null;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
contains(root, p, q);
return res;
}

boolean contains(TreeNode root, TreeNode p, TreeNode q){
if(root == null) return false;

boolean leftContains = contains(root.left, p, q);
boolean rightContains = contains(root.right, p, q);

if((leftContains && rightContains) || ((root == q || root == p) && (leftContains || rightContains))) res = root; // 判断是否为公共祖先

return ((root == q || root == p) || leftContains || rightContains);
}
}

二分查找

33. 搜索旋转排序数组

image-20260729011824039

思路:利用有序特性,边二分边判断左右子区间是否有序,只要有序则判断是否在有序区间(利用有序特性),否则一直二分。注意不能直接左右子区间,不利用有序性,这将退化为On。

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
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;

if(right < 0) return -1;
if(right == 0) return target == nums[0] ? 0 : -1;

while (left <= right) {
int mid = left + (right - left) / 2;

if (nums[mid] == target)
return mid;

if (nums[left] <= nums[mid]) { // 左边有序
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else { // 右边有序
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}

return -1;
}
}

链表

21. 合并两个有序链表

image-20260616021237273

思路:两个指针记录两个链表下一个要比较的节点,依次递推即可。

注意:

  1. 直接创建dummy和移动的tail即可,因为不会新建节点,因此空间为O1。

23. 合并 K 个升序链表

image-20260614210448309

思路:基础的两个升序链表合并(两个tmpNode记录两链表当前比较的Node),由于是多个合并可以考虑lists的0到length-1的个子区间合并,即分治算法。

注意:

  1. 分治算法的区间mid = (left + right) >> 1,(left, mid),(mid + 1, right)
  2. 分治算法left > right最好也判断,因为初始条件可能length为0,这会导致区间为(0, -1)
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
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return divideMerge(lists, 0, lists.length - 1);
}

private ListNode divideMerge(ListNode[] lists, int left, int right) {
if(left == right) return lists[left];
if(left > right) return null; // 保证(0, -1)情况时的正确性

int mid = (left + right) >> 1;

ListNode leftList = divideMerge(lists, left, mid);
ListNode rightList = divideMerge(lists, mid + 1, right);

return mergeTwoLists(leftList, rightList);
}


private ListNode mergeTwoLists(ListNode first, ListNode second) {
// 因为上述分治算法中(0, -1)可能导致传入ListNode为 null,为了正确性必须先做判断
if(first == null || second == null) return first == null ? second : first;
ListNode res = new ListNode(0);
ListNode aPtr = first, bPtr = second, tail = res;

while(aPtr != null && bPtr != null) {
if(aPtr.val >= bPtr.val) {
tail.next = bPtr;
bPtr = bPtr.next;
} else {
tail.next = aPtr;
aPtr = aPtr.next;
}
tail = tail.next;
}
tail.next = aPtr == null ? bPtr : aPtr;

return res.next;
}
}

141. 环形链表

image-20260615184619475

思路:使用两个指针,slow和fast,使之每次各移动1、2步长;只要有换,由于步长相差一,则一定会在环中节点相遇。

注意:

  1. 代码细节:先移动,再判断,因为while已经判断过了,否则代码一致性和逻辑性存在问题。

206. 反转链表

image-20260613202043530

思路:

  1. 记录preNode 和 curNode,直到curNode为null,返回preNode,但这种为正向,导致会记录curNode下一个Node,nextNode;
  2. 原地翻转:反向更新,递归找到最后一个Node,层层返回,并每层做curNode.next.next指向curNode完成翻转。

注意:

  1. 翻转后将curNode记录为null,保证链表最终正确性。

贪心

121. 买卖股票的最佳时机

image-20260729003608015

思路:由于题目数组与最后要求为 从左至右 ,因此有一个点可以确定,最后数字买入一定在卖出左边且是处于卖出天数之前的最小价格。因此可得思路:右边作为遍历i,每次遍历时更新其左侧的最小价格,再更新最大利润即可。

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int maxProfit(int[] prices) {
int max = 0, min = prices[0];

for(int price : prices) {
min = Math.min(min, price);
max = Math.max(max, price - min);
}

return max;
}
}

动态规划DP

322. 零钱兑换

image-20260613195507148

思路:从0开始计算每个数额(0 - amount)的最少的硬币组合方式,直到amount。

注意:

  1. dp[0]作为起始应该是0而不是-1;否则一是后续不可达,二是不符合题意。
  2. 在更新dp数组时,因为考虑是最小值,而数组中的默认值是0,因此要分开讨论,否则最小值全为0;
  3. dp数组长度应该初始化为amount + 1;否则无法计算到amount。

图论

200. 岛屿数量

image-20260614211323896

思路:扫描二维数组,只要走到1的岛屿,就dfs找出同一个岛屿的所有格子,并将1置为0避免重复找。

注意:

  1. dfs中需要判断i,j是否超出二维数组边界,以及要跳过为0的格子。
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
class Solution {
int line = 0;
int colum = 0;
char[][] grid = null;

private void dfs(int i, int j) {
if(i < 0 || i >= line || j < 0 || j >= colum || grid[i][j] == '0') return ;
grid[i][j] = '0';

dfs(i - 1, j);
dfs(i + 1, j);
dfs(i, j + 1);
dfs(i, j - 1);
}

public int numIslands(char[][] grid) {
line = grid.length;
colum = grid[0].length;
this.grid = grid;
int res = 0;

for(int i = 0; i < line; i++) {
for(int j = 0; j < colum; j++) {
if(grid[i][j] == '0') continue;
else {
dfs(i, j);
res++;
}
}
}
return res;
}
}
评论加载中...