Leetcode刷题记录
2026-03-22 21:48:26

42. 接雨水

遍历每一个柱子,向左向右找到最高的柱子,然后取左右最高柱子中较小的那个,减去当前柱子的高度,就是当前柱子能接的雨水。

优化点:找最高柱子的过程可以优化,用空间换时间。

如果用数组来存储这个数据,需要O(n)的空间,但是观察可以发现,每个位置的数据只用了一次,而不同位置的数字之间也存在着某种联系。

所以可以使用双指针来优化,一个指向左边最高的柱子,一个指向右边最高的柱子,然后向中间移动,直到两指针相遇,此时两指针所指的柱子就是最高的柱子。在这个移动的过程中,边移动边计算当前位置能接的雨水。这样可以将空间复杂度降低到O(1)。

24. 两两交换链表中的节点

首次的思路

一个链表,交换相邻的两个节点,直到没有相邻的两个节点可以交换。

思路:从头到尾,每两个为一组(不足两个的不用交换),步长为2,遍历链表,使用临时变量保存当前节点的下一个节点,然后交换当前节点和下一个节点的指针,然后移动到下一个节点的下一个节点。

要注意使用last 来更新最后一个交换的节点,否则会出现链表断裂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while(cur!=null && cur.next!=null) {
if(first!=1)last.next=cur.next;
else first=0;
//记录并更新上一个
last = cur;

//调整指针
temp=cur.next.next;
cur.next.next = cur;
cur.next=temp;

//这里要注意的是,链表已经交换了,cur位于后位,所以要更新cur到cur的下一个节点,而不是最开始的last.next.next。

//不过这里形式上是下一个,要明白这是下一个的下一个。
cur = cur.next;
}

指针交换如下图所示:

指针交换指针交换

代码更加优化

重新看一遍这个代码,有很多问题:变量命名不规范,容易搞混,代码顺序需要斟酌,逻辑不够清晰,没有使用哨兵节点,缺乏注释,等等。

进行修改

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
class Solution {
public ListNode swapPairs(ListNode head) {
// 伪头节点:统一处理所有轮次的连接(包括第一轮)
ListNode dummy = new ListNode(-1);
dummy.next = head;
// prevTail:每轮交换前的“前驱尾节点”(初始为伪头节点)
ListNode prevTail = dummy;

// 循环条件:至少有两个节点可交换
while (prevTail.next != null && prevTail.next.next != null) {
// 标记本轮要交换的两个节点
ListNode node1 = prevTail.next;
ListNode node2 = prevTail.next.next;

// 步骤1:前驱尾节点指向第二个节点(跨轮连接)
prevTail.next = node2;
// 步骤2:第一个节点指向第二个节点的下一个节点
node1.next = node2.next;
// 步骤3:第二个节点指向第一个节点(完成交换)
node2.next = node1;

// 步骤4:更新前驱尾节点为当前轮的第一个节点(交换后的尾节点)
prevTail = node1;
}

// 返回伪头节点的next(新链表头)
return dummy.next;
}
}

206. 反转链表

思路是在遍历链表的过程中,持续将节点的指针指向前一个节点,直到遍历到链表的末尾,此时最后一个节点就是新的头节点。
但关键是,next指针修改后会丢失,所以需要一个临时变量来保存下一个节点。

基于实现方式的不同,可以分为迭代和递归两种方式。

迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null)return head;
ListNode cur = head;
ListNode pre = null;
ListNode tmp = null;
while(cur!=null) {
tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
}

递归

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null,head);
}
public static ListNode reverse(ListNode pre,ListNode cur){
if(cur == null)return pre;

ListNode temp = cur.next;
cur.next = pre;
return reverse(cur, temp);
}
}

160. 相交链表

双指针,分别指向两个链表的头节点,然后同时向后移动,如果有相交,那么两个指针最终会相遇;如果没有相交,那么两个指针最终都会指向null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pA = headA;
ListNode pB = headB;
//System.out.print(null==null); 会为true
while(pA!=pB) {
if(pA==null)pA=headB;
else pA=pA.next;
if(pB==null)pB=headA;
else pB=pB.next;
}
return pA;
}
}

141. 环形链表

判断一个链表有没有环,可以使用快慢指针,快指针每次移动两步,慢指针每次移动一步,如果有环,那么快指针最终会追上慢指针;如果没有环,那么快指针最终会指向null。(这里的快慢指针的步长选择)

当需要求环形链表的入口节点时,可以在快慢指针相遇后,将其中一个指针重新指向链表头,然后两个指针每次移动一步,直到相遇,相遇的节点就是环的入口节点。

数学证明

假设链表的非环部分长度为 a,环的长度为 b。当慢指针走到环的入口时,快指针已经在环内走了 a 步(因为快指针速度是慢指针的 2 倍)。此时:
慢指针在环内的位置:0(环入口)
快指针在环内的位置:a % b(环内的相对位置)
两者的「追赶距离」为:b - (a % b)(快指针需要追上慢指针的步数)
由于快指针每次比慢指针多走 2-1=1 步,相当于每轮追赶距离减少 1,因此经过 b - (a % b) 轮后,快指针一定会追上慢指针(距离变为 0)。
一步和两步是一个很好的做法,其他步长判断位置困难,还有可能错漏。具体数学分析过于复杂,暂不展开。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null)return null;
ListNode fast = head;
ListNode slow = head;
while(fast.next!=null && fast.next.next!=null) {
fast = fast.next.next;
slow = slow.next;
if(fast == slow)return head;
}
return null;
}
}

51. N 皇后

N皇后,不能再经典的回溯题,逻辑很好理解,即使用回溯的方式枚举所有的可能,然后判断可行性,得到所有的可行解。
不过这个输出格式实在是想吐槽,莫名其妙的格式(不过应该是我对字符串以及组织不熟悉)。

1
2
3
4
5
6
7
8
9
10
11
12
for(int i=0;i<n;i++) {
if(c[i]==0 && x[i+num]==0 && xx[i-num+n-1]==0) {
c[i]=1; //column
x[i+num]=1; //left diagonal
xx[i-num+n-1]=1; //right diagonal
ans[num]=i;
f(ans,num+1);
c[i]=0;
x[i+num]=0;
xx[i-num+n-1]=0;
}
}

题目中要求的 字符串格式

1
2
3
4
5
6
7
8
9
10
11
//[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
if(num==n){
List<String> temp=new ArrayList<>();
for(int i=0;i<n;i++) {
char[] row = new char[n];
Arrays.fill(row, '.');
row[ans[i]] = 'Q';
temp.add(new String(row));
}
list.add(temp);
}

98. 验证二叉搜索树

1.中序遍历

排序二叉树的中序遍历是升序的。

所以可以将中序遍历输出到一个数组,再检查数组是否是升序的即可。
可以对其进行空间优化。遍历中进行判断,记录上一个节点的值,如果当前节点的值小于等于上一个节点的值,则不是二叉搜索树。

但是第一个点不是很好判断,如果设置一个初始值last=Long.MIN_VALUE, 可以解决这个问题。
也可以用一个组合节点来判断,如果是第一个就跳过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
long last = 0;//Long.MIN_VALUE;
public boolean isbigger = true;
public boolean first = true;
public boolean isValidBST(TreeNode root) {
if(root==null)return true;
boolean l = isValidBST(root.left);

if(first){first = false;}
else isbigger = isbigger && root.val > last;
//System.out.println(root.val+" "+isbigger);
last = root.val;

boolean r = isValidBST(root.right);

return isbigger;
}
}

2.递归检查

递归检查,设置上下限,递归检查左右子树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public boolean isValidBST(TreeNode root) {
"""
注意这里的上下限,如果是Integer.MIN_VALUE,则会导致死循环。
所以这里设置成null,表示无穷小。
其实我没有验证过,但是应该是可以的,思路是一样的。
"""
return check(root, null, null);
}
public boolean check(TreeNode root, Integer lower, Integer upper) {
if(root == null) return true;
if(lower!= null && root.val <= lower) return false;
if(upper!= null && root.val >= upper) return false;
return check(root.left, lower, root.val) && check(root.right, root.val, upper);
}
}

102. 二叉树的层序遍历

二叉树的遍历方式有很多种,深度优先搜索,广度优先搜索,层序遍历。深搜还可以分为前序遍历,中序遍历,后序遍历。广搜可以分为层序遍历,宽度遍历。

层序遍历就是先把根节点放入队列,然后依次弹出队列中的节点,并将其左右子节点放入队列,直到队列为空。

层次遍历和广搜的区别是,层序遍历是按层次遍历,广搜是按宽度遍历。
尽管都是使用队列,但是层次遍历会在每一层加入队列后停下一个间隔,记录一下这个层的宽度(通常是队列的大小),而广搜则是一次性将所有节点都加入队列,然后按宽度遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
while(!queue.isEmpty()) {
List<Integer>list =new ArrayList<>();
int Size = queue.size();
for(int i=1;i<=Size;i++) {
TreeNode temp =queue.poll();
list.add(temp.val);
if(temp.left!=null)queue.offer(temp.left);
if(temp.right!=null)queue.offer(temp.right);
}
res.add(list);
}
return res;
}
}

105. 从前序与中序遍历序列构造二叉树

也是经典题,如果能明白中序遍历和前序遍历的特点,那么这道题就很简单。

递归

很显然,中序遍历的左右子树是由根节点分开的。而前序遍历的第一个节点就是根节点。前序遍历的第二个节点到倒数第二个节点就是左子树,倒数第二个节点到最后一个节点就是右子树。

所以,我们可以先找到根节点在中序遍历中的位置,然后递归地构造左右子树。
在中序遍历中寻找根节点的算法可以使用MAP,也可以使用线性查找。

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
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return work(0,preorder.length,0,preorder.length,preorder,inorder);
}
public TreeNode work(int pl,int pr,int il,int ir,int[] preorder,int[] inorder) {
if(pl>=pr){
return null;
}
TreeNode res = null;
int val = preorder[pl];
//这里可以使用map对索引查找进行优化
for(int i=il;i<ir;i++) {
if(inorder[i]==val){
res = new TreeNode(val);
int leftsize = i - il;
//rightsize = ir - i;
//System.out.println(pl+1+" "+(pl+1+leftsize)+" "+il+" "+i);
//System.out.println(pl+1+leftsize+" "+(pr)+" "+(i+1)+" "+ir);
res.left = work(pl+1,pl+1+leftsize,il,i,preorder,inorder);
res.right = work(pl+1+leftsize,pr,i+1,ir,preorder,inorder);
//if(res.left==null) System.out.println("NULL");
//else System.out.println(res.left.val+" "+res.right.val);
}
}
return res;
}
}

迭代

没看看懂这一块

215. 数组中的第K个最大元素

1.堆排序

本来我看那个数据规模,K次找最大,复杂度就是O(K * N),10^9应该能过的,结果没有,就是复杂度还不够。

标签里有堆排序,所以用堆排序来做。(很长时间没看堆排序,所以有些生疏,在更新堆的时候搞错了边界,需要注意)

堆排序的代码

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

class Solution {
int [] heap = new int[100005];
int cur=1;
public int findKthLargest(int[] nums, int k) {
int len = nums.length;
int ans = -1;
for(int i=0;i<len;i++){
insert(nums[i]);
}
for(int i=0;i<k;i++) {
ans = deleteTop();
}
return ans;
}
void insert(int num) {
heap[cur]=num;
int tmp = cur;
boolean flag = true;
while(flag && tmp/2>=1) {
if(heap[tmp]>heap[tmp/2]) {
swap(tmp,tmp/2);
}
else {
flag = false;
}
tmp/=2;
}
cur++;
}

int deleteTop() {
int res = heap[1];
heap[1] = heap[cur-1];
cur--;
int tmp = 1;
while(tmp*2<cur && (heap[tmp]<heap[tmp*2] || heap[tmp]<heap[tmp*2+1])) {
if(heap[tmp*2]>heap[tmp*2+1]) {
swap(tmp,tmp*2);
tmp = tmp*2;
}
else {
swap(tmp,tmp*2+1);
tmp = tmp*2+1;
}
}
return res;
}
void swap(int i,int j) {
int temp =heap[i];
heap[i]=heap[j];
heap[j]=temp;
}
}

堆排序其实很简单,只有两个操作,插入和删除。

  • 插入就往堆的末尾插入一个元素,然后从堆的最后向上更新堆。
  • 删除就是删除堆顶元素,将最后一个元素放到堆顶,然后从堆顶向下更新堆。

堆是一棵顺序存储的完全二叉树。

其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆为小根堆。反之,每个结点的关键字都不小于其孩子结点的关键字,这样的堆为大根堆。

实现起来很简单,就是维护一个数组数据个数,然后每次插入和删除的时候,都更新堆。(更新的时候好像还可以使用递归的写法,以后再研究)

堆排序的时间复杂度是O(NlogN),N是数组的长度,logN是堆的高度。
对于本题,时间复杂度是O(NlogN),非常接近于O(N)。

2.快速选择算法

快排每次都选择一个元素作为基准,然后将比基准小的放左边,比基准大的放右边。按这个基准分割,左边的元素都比基准小,右边的元素都比基准大。这个基准的位置就不会变了,即最终的位置。在排序过程中,只要这个位置是第K个,就说明这个就是第K大的元素。

[TODO]

具体代码还没写,先放这。

239. 滑动窗口最大值

1.优先队列(堆)

维护之前K个元素的最大值,使用优先队列来维护这个最大值,每次移动窗口的时候,加入新的元素,重新统计最大值;但不需要专门去掉不在窗口的旧元素,可以同时记录他的下标,用于判断这个最大的是不是在窗口内,不是则找到窗口内最大的那个。

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
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length-k+1];
PriorityQueue<int[]> pq = new PriorityQueue<>(new Comparator<int[]>() {
public int compare(int[] a, int[] b) {
return a[0]!=b[0]?b[0]-a[0]:b[1]-a[1];
}
});

for(int i=0;i<k;i++) {
int[] t = {nums[i],i};
pq.add(t);
}
int index = 0;
res[index++]=pq.peek()[0];
for(int i=k;i<nums.length;i++) {
int[] t = {nums[i],i};
pq.add(t);
while(pq.peek()[1]<index) {
pq.poll();
}
res[index++]=pq.peek()[0];
}
return res;
}
}

2.单调队列

维护一个队列内部单调的双端队列
保证队列内元素都是 窗口内的
同时,使用特定的入队方式,保证队尾都是比队首靠右的,
同时,保证队列内元素单调递减,这样队首就是当前窗口的最大值。

维护这个单调队列,要求实现入队和出队的规则

  • 入队:push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
  • pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
  • 保持如上规则,每次窗口移动的时候,只要取队首就可以返回当前窗口的最大值。
    滑动窗口最大值的更新滑动窗口最大值的更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length-k+1];
Deque<Integer> dq = new LinkedList<>();
for(int i=0;i<k;i++) {
while (!dq.isEmpty() && dq.peekLast()<nums[i]) {
dq.pollLast();
}
dq.addLast(nums[i]);
}
int index = 0;
res[index++]=dq.getFirst();
for(int i=k;i<nums.length;i++){
while (!dq.isEmpty() && dq.peekLast()<nums[i]) {
dq.pollLast();
}
dq.addLast(nums[i]);
int temp = nums[index-1];
if(temp==dq.getFirst())dq.pollFirst();
res[index++]=dq.getFirst();
}
return res;
}
}

279. 完全平方数

思路

最简单的思路就是枚举所有的平方数的组合,查看是否和n相等,寻找最小的组合;
但这样会很差,很多重复的判断,很多不必要的判断。
显然,可以察觉到,尽量用大的平方数进行相加,能减少数量。

所以在使用搜索的方式时,需要从大数开始枚举,通过回溯的方式,完成对组合的枚举(然后再加一些剪枝优化)

但是,很明显有重复搜索(重叠子问题),所以可以进行记忆化搜索。
同时发现,f[n]只与f[m] (m<n)有关,满足最优子结构
可以用动态和规划,直接进行迭代。(实际上这就是一个完全背包问题)

1.直接搜索(ai)

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
import java.util.ArrayList;
import java.util.List;

class Solution {
// 全局变量记录最小数量,初始为最大值
private int minCount;

public int numSquares(int n) {
// 初始化最小数量为极大值
minCount = Integer.MAX_VALUE;

// 步骤1:收集所有不超过n的平方数(从大到小,优先选大的减少递归)
List<Integer> squares = new ArrayList<>();
for (int i = (int) Math.sqrt(n); i >= 1; i--) {
squares.add(i * i);
}

// 步骤2:回溯(起始索引0,剩余值n,已选数量0)
backtrack(squares, 0, n, 0);

return minCount;
}

/**
* 回溯核心方法
* @param squares 所有不超过n的平方数(从大到小)
* @param start 当前枚举的起始索引(避免重复组合)
* @param remain 剩余需要凑的数值
* @param count 已选的平方数数量
*/
private void backtrack(List<Integer> squares, int start, int remain, int count) {
// 剪枝1:当前数量已≥已知最小值,无需继续递归(不可能更优)
if (count >= minCount) {
return;
}

// 终止条件:剩余值为0,找到有效组合,更新最小值
if (remain == 0) {
minCount = count;
return;
}

// 枚举平方数(从start开始,避免重复组合)
for (int i = start; i < squares.size(); i++) {
int square = squares.get(i);

// 剪枝2:当前平方数超过剩余值,跳过(选了会超,无意义)
if (square > remain) {
continue;
}

// 选择当前平方数:递归(剩余值-平方数,数量+1)
backtrack(squares, i, remain - square, count + 1);
}
}
}

2.记忆化搜索

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
class Solution {
int[] memo; // 备忘录:memo[remain] = 凑出remain的最少数量
public int numSquares(int n) {
// 初始化备忘录:-1表示未计算
memo = new int[n + 1];
Arrays.fill(memo, -1);
memo[0] = 0; // 边界:0需要0个平方数
// 收集平方数
List<Integer> squares = new ArrayList<>();
for (int i = 1; i*i <= n; i++) squares.add(i*i);
// 记忆化递归
return dfs(squares, n);
}
// 递归函数:返回凑出remain的最少数量
private int dfs(List<Integer> squares, int remain) {
if (memo[remain] != -1) return memo[remain]; // 直接返回已计算的解
int minCount = Integer.MAX_VALUE;
for (int s : squares) {
if (s > remain) continue;
// 递归计算子问题:remain-s的最少数量
int subCount = dfs(squares, remain - s);
if (subCount != Integer.MAX_VALUE) {
minCount = Math.min(minCount, subCount + 1);
}
}
memo[remain] = minCount; // 记录到备忘录
return minCount;
}
}

3.动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
// 初始化:默认极大值(表示初始无法凑出)
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0; // 基础子问题

// 收集所有平方数
List<Integer> squares = new ArrayList<>();
for (int i = 1; i*i <= n; i++) squares.add(i*i);

// 从底向上计算每个j的最优解
for (int j = 1; j <= n; j++) {
for (int s : squares) {
if (j >= s) {
dp[j] = Math.min(dp[j], dp[j - s] + 1);
}
}
}
return dp[n];
}
}

这套代码的循环不太一样。具体见‘背包’中的完全背包问题

4.数学分析

奇技淫巧
根据「四平方和定理」:
任何自然数都可以表示为最多 4 个完全平方数的和。
如果 n 是 4^k*(8m+7) 形式,则必须用 4 个平方数。
否则,判断是否能由 1 个、2 个平方数组成,剩下的都是 3 个。
这种方法时间复杂度为 O(√n),远优于 DP 的 O(n√n):

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 {
public int numSquares(int n) {
// 1. 判断是否是完全平方数(1个)
if (isPerfectSquare(n)) {
return 1;
}

// 2. 判断是否是4^k*(8m+7)形式(必须4个)
while (n % 4 == 0) {
n /= 4;
}
if (n % 8 == 7) {
return 4;
}

// 3. 判断是否能由2个平方数组成
int maxBase = (int) Math.sqrt(n);
for (int i = 1; i <= maxBase; i++) {
if (isPerfectSquare(n - i * i)) {
return 2;
}
}

// 剩下的都是3个
return 3;
}

// 辅助函数:判断是否是完全平方数
private boolean isPerfectSquare(int x) {
int sqrt = (int) Math.sqrt(x);
return sqrt * sqrt == x;
}
}

这种完全背包问题,322.零钱兑换也是相似的解法

41 缺失的第一个正数

思路

  1. 朴素浅显的思路是,遍历数组,依次将每个数加入HashSet。完成后,遍历HashSet,找到最小的正整数。
    时间复杂度是O(n),空间复杂度是O(n)

  2. 但这样会有空间上的浪费,浪费一个hashset的空间。这样就可以自己依赖原本的数组,来记录每个数是否出现过,自己实现一个hashset。但是数组是覆盖整个int范围的,没办法进行特殊标记。
    但题目要求的不是最小的数,而是最小的正整数。所以可以先过滤一遍,将所有负数和0都过滤掉,变为超出长度的数值。
    然后再遍历数组,记录每个数是否出现,将对应下标的数字变为负数,表示已经出现过。
    最后再遍历数组,找到第一个正数,该数的下标就是最小的正整数。
    (提醒,由于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

class Solution {
//规定了正数,就是突破点
public int firstMissingPositive(int[] nums) {
int MAX = nums.length;

for(int i = 0;i<nums.length;i++) {
if(nums[i]<=0)nums[i]=MAX+1;
//将负数转化成超出范围的正数,这里设定为 长度+1;这样就全都是正数了,可以用负数对下标进行标记了
}

for(int i = 0;i<nums.length;i++) {
int temp = Math.abs(nums[i]);
// 负数变为正数,再判断范围,这样可以圈定1~n范围的数
// 出现过的数标记为负数(如果原本就是负数,说明已经被标记了)
if(temp <= MAX) nums[temp-1]= - Math.abs(nums[temp-1]);
}

for(int i = 0;i<nums.length;i++) {
if(nums[i]>0)return i+1;
}
//都是负数,就说明1~n都有,就返回n+1
return nums.length+1;
}
}

76. 最小覆盖子串

思路

暴力法:枚举子串,判断是否包含t的所有字符,时间复杂度是O(n^3),空间复杂度是O(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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class Solution {
public String minWindow(String s, String t) {
int l = 0, r = 0;
int tlen = t.length();
int slen = s.length();
int minlenth=slen+1;
int ansl =-1;
int ansr =-1;
//怎么判断子串包含?
//枚举子串

int needCount = tlen;
HashMap<Character, Integer> map = new HashMap<>();
for(char c : t.toCharArray()) {
map.put(c, map.getOrDefault(c, 0)+1);
}
while(r<slen){
char rchar = s.charAt(r);

//如果rc是其中一个
if(map.containsKey(rchar)) {
if(map.get(rchar) > 0) {
needCount--;
}
map.put(rchar, map.get(rchar) - 1);
}

//如果已经包含了全部,该移动l了
while(needCount == 0) {
//更新最小
if(r - l + 1 < minlenth) {
minlenth = r - l + 1;
ansl = l;
ansr = r;
}
// l是其中一个
char lchar = s.charAt(l);
if(map.containsKey(lchar)) {
//fix: if(map.get(lchar) >= 0) {
//如果其他逻辑都是对的,这里不应该出现正数,正数时needCount会大于0的;可以修改为等0
if(map.get(lchar) == 0) {
needCount++;
}
map.put(lchar,map.get(lchar) + 1);
}
l++;
}
r++;
}
System.out.println(minlenth);
if(ansl == -1)return "";
else return s.substring(ansl, ansr+1);
}
}
//这么梳理下来也没有很难

4. 寻找两个正序数组的中位数

思路

处理边界情况的地方非常多,需要考虑到各种边界情况!
多看、多验证、多思考!

1.二分 + kth元素

找第 k 小的数,可以用二分法,时间复杂度是 O(log(min(m,n)))。

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 double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int total = m + n;

if (total % 2 == 1) {
// 奇数:找第 (total+1)/2 小的数
return findKth(nums1, 0, nums2, 0, (total + 1) / 2);
} else {
// 偶数:找第 total/2 和 total/2+1 小的数的平均值
double left = findKth(nums1, 0, nums2, 0, total / 2);
double right = findKth(nums1, 0, nums2, 0, total / 2 + 1);
return (left + right) / 2.0;
}
}

// 寻找两个有序数组中第k小的数(k从1开始)
private double findKth(int[] nums1, int i, int[] nums2, int j, int k) {
// 如果nums1已经用完,直接从nums2中取
if (i >= nums1.length) return nums2[j + k - 1];
// 如果nums2已经用完,直接从nums1中取
if (j >= nums2.length) return nums1[i + k - 1];
// 如果k==1,返回两个数组当前元素的最小值
if (k == 1) return Math.min(nums1[i], nums2[j]);

// 找到两个数组中的第k/2个元素
int mid1 = (i + k/2 - 1 < nums1.length) ?
nums1[i + k/2 - 1] : Integer.MAX_VALUE;
int mid2 = (j + k/2 - 1 < nums2.length) ?
nums2[j + k/2 - 1] : Integer.MAX_VALUE;

// 比较并舍弃较小的那一半
if (mid1 < mid2) {
return findKth(nums1, i + k/2, nums2, j, k - k/2);
} else {
return findKth(nums1, i, nums2, j + k/2, k - k/2);
}
}
}

2.分割数组

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
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 确保nums1是较短的数组,简化逻辑
if (nums1.length > nums2.length) {
return findMedianSortedArrays(nums2, nums1);
}

int m = nums1.length, n = nums2.length;
int totalLeft = (m + n + 1) / 2; // 分割线左边应该有多少个元素

// 在nums1中二分查找合适的分割线
int left = 0, right = m;

while (left < right) {
// i: nums1中分割线右边的第一个元素下标
// j: nums2中分割线右边的第一个元素下标
int i = left + (right - left + 1) / 2;
int j = totalLeft - i;

if (nums1[i - 1] > nums2[j]) {
// 分割线需要左移
right = i - 1;
} else {
left = i;
}
}

int i = left;
int j = totalLeft - i;

// 处理边界情况
int nums1LeftMax = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
int nums1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];
int nums2LeftMax = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
int nums2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];

if ((m + n) % 2 == 1) {
// 奇数:中位数在分割线左边
return Math.max(nums1LeftMax, nums2LeftMax);
} else {
// 偶数:中位数是左右两边的平均值
return (Math.max(nums1LeftMax, nums2LeftMax) +
Math.min(nums1RightMin, nums2RightMin)) / 2.0;
}
}
}

2429.最小异或

思路

统计num2中的位数
先从大到小(从左到右),消耗num1中的1位,记录消耗的x
比num1中少,就直接是答案
比num1多,就填补在之前x中没填补的低位里。

理清楚答案就是怎么实现了,很明显要用位运算
我还在傻傻搞while循环,结果发现直接用位运算就可以了,根本不需要循环。

我的

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
class Solution {
public int minimizeXor(int num1, int num2) {
boolean[] xrray = new boolean[32];
boolean[] n1 = new boolean[32];

int temp = num2;
int num = 0;
while(temp!=0){
num+=(temp&1);
temp>>=1;
}

temp = num1;
int index = 0;
while(temp!=0) {
n1[index] = (temp&1)==1;
temp>>=1;
index++;
}

while(index>=0) {
if(n1[index] && num>0) {
num--;
xrray[index]=true;
}
index--;
}

index=0;
while(num>0) {
if(!xrray[index]){
xrray[index]=true;
num--;
}
index++;
}

int ans =0;
int mul = 1;
for(boolean i:xrray) {
if(i)ans+=mul;
mul<<=1;
}
return ans;
}
}

大模型的

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
class Solution {
public int minimizeXor(int num1, int num2) {
// 步骤1:统计num2的二进制中1的个数(记为cnt)
int cnt = Integer.bitCount(num2);
if (cnt == 0) return 0; // 特殊情况:num2没有1,结果直接为0

int res = 0;
// 步骤2:优先保留num1高位的1(消耗cnt)
// 从最高位(30位,因为int是32位,符号位忽略)到低位遍历
for (int i = 30; i >= 0 && cnt > 0; i--) {
// 如果num1的第i位是1,就保留这个1,消耗一个cnt
if ((num1 & (1 << i)) != 0) {
res |= (1 << i);
cnt--;
}
}

// 步骤3:剩余的cnt补到最低位的0上
for (int i = 0; i <= 30 && cnt > 0; i++) {
// 如果当前位是0,就设置为1,消耗一个cnt
if ((res & (1 << i)) == 0) {
res |= (1 << i);
cnt--;
}
}

return res;
}
}

技巧(leetcode题解)

这真是个技巧,num1 & (num1 - 1) 可以将 num1 的二进制表示中最低位的 1 变成 0。例如,如果 num1 是 6(二进制为 110),那么 num1 - 1 是 5(二进制为 101),num1 & (num1 - 1) 就是 4(二进制为 100)。这个操作可以用来快速地消除 num1 中的最低位的 1。
我没招了。

1
2
3
4
5
6
7
8
9
class Solution {
public int minimizeXor(int num1, int num2) {
int c1 = Integer.bitCount(num1);
int c2 = Integer.bitCount(num2);
for (; c2 < c1; c2++) num1 &= num1 - 1; // 最低的 1 变成 0
for (; c2 > c1; c2--) num1 |= num1 + 1; // 最低的 0 变成 1
return num1;
}
}