代码随想录题单题解笔记(Java)· 上
本文是代码随想录笔记上篇,覆盖数组到栈与队列。
本笔记按 代码随想录题单 整理题解脚手架(Java),含力扣题与 卡码网 ACM 题。题解全集可参考 代码随想录题解。
笔记约定:
- 代码统一为 Java;卡码网题目可用 ACM
Main或按平台要求 - 每题包含:标签/难度徽章 → 题目 → 思路简述 → 参考代码 → 复杂度
- 题面与示例以力扣 / 卡码网原文为准;有图则嵌入
代码随想录题单 题解笔记(Java).assets/ - 「双指针」为复习清单,正文在首次出现的分类中
- 题目列表中 🌟 可自行标记收藏;勾选
[x]表示已写完题解(每完成一题须在各章题目列表同步打勾)
按分类顺序刷;图论章节优先配合 Java ACM 输入输出模板 练 ACM 读写。补思路时关键短语用 Highlightr:<mark style="background: #FFF3A3A6;">...</mark>;题面已尽量压缩空行,与经典 150 笔记一致。
目录
1. 数组 (Array)
核心思想: 数组相关:二分、双指针改写、滑动窗口、螺旋矩阵与前缀和(含卡码网 ACM 练习)。
题目列表
704. 二分查找 (Binary Search) 简单 标签:数组, 二分查找
27. 移除元素 (Remove Element) 简单 标签:数组, 双指针
977. 有序数组的平方 (Squares of a Sorted Array) 简单 标签:数组, 双指针, 排序
209. 长度最小的子数组 (Minimum Size Subarray Sum) 中等 标签:数组, 二分查找, 前缀和, 滑动窗口
59. 螺旋矩阵 II (Spiral Matrix II) 中等 标签:数组, 矩阵, 模拟
58. 区间和 (Range Sum) 简单 标签:数组, 前缀和, ACM
44. 开发商购买土地 (Developer Buy Land) 中等 标签:数组, 前缀和, ACM
704. 二分查找 (Binary Search)
数组二分查找简单
题目
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果 target 存在返回下标,否则返回 -1。
你必须编写一个具有 O(log n) 时间复杂度的算法。
示例 1:
输入:nums = [-1,0,3,5,9,12], target = 9
输出:4
解释:9 出现在 nums 中并且下标为 4
示例 2:
输入:nums = [-1,0,3,5,9,12], target = 2
输出:-1
解释:2 不存在 nums 中因此返回 -1
提示:
- 你可以假设
nums中的所有元素是不重复的。 n将在[1, 10000]之间。nums的每个元素都将在[-9999, 9999]之间。
思路简述
数组有序,用 闭区间二分:维护 [left, right],取 mid 与 target 比较;相等返回下标,nums[mid] < target 则丢左半,否则丢右半。找不到返回 -1。
参考代码
JAVA
class Solution {
public int search(int[] nums, int target) {
// left / right:当前搜索闭区间两端
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1); // 防溢出的中点
if (nums[mid] == target) {
int ans = mid;
return ans;
}
if (nums[mid] < target) {
left = mid + 1; // target 只可能在右半
} else {
right = mid - 1; // target 只可能在左半
}
}
return -1;
}
}
复杂度
- 时间:O(log n),每步区间减半
- 空间:O(1)
27. 移除元素 (Remove Element)
数组双指针简单
题目
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:
更改
nums数组,使nums的前k个元素包含不等于val的元素。nums的其余元素和nums的大小并不重要。返回
k。
用户评测:
评测机将使用以下代码测试您的解决方案:
PLAINTEXT
int[] nums = [...]; // 输入数组
int val = ...; // 要移除的值
int[] expectedNums = [...]; // 长度正确的预期答案。
// 它以不等于 val 的值排序。
int k = removeElement(nums, val); // 调用你的实现
assert k == expectedNums.length;
sort(nums, 0, k); // 排序 nums 的前 k 个元素
for (int i = 0; i < k; i++) {
assert nums[i] == expectedNums[i];
}
如果所有的断言都通过,你的解决方案将会 通过。
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2,,]
解释:你的函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3,,,_]
解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。 注意这五个元素可以任意顺序返回。 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
提示:
0 <= nums.length <= 1000 <= nums[i] <= 500 <= val <= 100
思路简述
原地删除:用 快慢指针。slow 指向下一个要写入「保留元素」的位置;fast 遍历全表,遇到不等于 val 的值就写到 nums[slow] 并 slow++。最后 slow 即为新长度 k。
参考代码
JAVA
class Solution {
public int removeElement(int[] nums, int val) {
// slow:下一个「保留元素」应写入的下标,也是当前有效长度
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast]; // 保留该元素
slow++;
}
// nums[fast] == val:跳过,相当于删除
}
int ans = slow;
return ans;
}
}
复杂度
- 时间:O(n),单次扫描
- 空间:O(1),原地修改
977. 有序数组的平方 (Squares of a Sorted Array)
数组双指针排序简单
题目
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
提示:
1 <= nums.length <= 10^4-10^4 <= nums[i] <= 10^4nums已按 非递减顺序 排序- 进阶:请你设计时间复杂度为
O(n)的算法解决本问题
思路简述
有序数组平方后两端最大、中间最小。用 双指针从两端向中间:每次比较 |nums[left]| 与 |nums[right]|,把较大平方填入结果数组末尾,再收缩对应指针,从而一次得到非递减序列。
参考代码
JAVA
class Solution {
public int[] sortedSquares(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
// left / right:原数组两端;write:结果从后往前填
int left = 0;
int right = n - 1;
int write = n - 1;
while (left <= right) {
int leftAbs = Math.abs(nums[left]);
int rightAbs = Math.abs(nums[right]);
if (leftAbs > rightAbs) {
ans[write] = leftAbs * leftAbs;
left++;
} else {
ans[write] = rightAbs * rightAbs;
right--;
}
write--;
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(1) 额外(不计返回数组);返回数组占 O(n)
209. 长度最小的子数组 (Minimum Size Subarray Sum)
数组二分查找前缀和滑动窗口中等
题目
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
1 <= target <= 10^91 <= nums.length <= 10^51 <= nums[i] <= 10^4- 进阶:如果你已经实现
O(n)时间复杂度的解法, 请尝试设计一个O(n log(n))时间复杂度的解法。
思路简述
全为正数,用 滑动窗口:右端 right 不断纳入元素累加 windowSum;当窗口和 >= target 时更新最短长度,并收缩左端 left 直至再次小于 target。每个下标最多进出窗口一次。
参考代码
JAVA
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int ans = Integer.MAX_VALUE;
// left:窗口左端;windowSum:当前窗口 [left, right] 元素和
int left = 0;
int windowSum = 0;
for (int right = 0; right < n; right++) {
windowSum += nums[right];
while (windowSum >= target) {
ans = Math.min(ans, right - left + 1); // 当前合法窗口长度
windowSum -= nums[left];
left++; // 尽量缩短窗口
}
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
复杂度
- 时间:O(n),每个元素最多进出窗口一次
- 空间:O(1)
59. 螺旋矩阵 II (Spiral Matrix II)
数组矩阵模拟中等
题目
给你一个正整数 n ,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
示例 1:
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]
示例 2:
输入:n = 1
输出:1
提示:
1 <= n <= 20
思路简述
按层 四边界模拟:top/bottom/left/right 圈定当前圈。依次向右→向下→向左→向上填入 1..n²,每走完一边就收缩对应边界;边界交叉则结束。必须写满 n² 格,时间下界 O(n²),此写法已达最优。
参考代码
JAVA
class Solution {
public int[][] generateMatrix(int n) {
int[][] ans = new int[n][n];
// top/bottom/left/right:当前未填完的矩形边界
int top = 0;
int bottom = n - 1;
int left = 0;
int right = n - 1;
int k = 1; // 下一个要填的数字
while (true) {
// 上边:左 → 右
for (int i = left; i <= right; i++) {
ans[top][i] = k++;
}
if (++top > bottom) {
break; // 上下边界交错,整圈结束
}
// 右边:上 → 下
for (int i = top; i <= bottom; i++) {
ans[i][right] = k++;
}
if (--right < left) {
break;
}
// 下边:右 → 左
for (int i = right; i >= left; i--) {
ans[bottom][i] = k++;
}
if (--bottom < top) {
break;
}
// 左边:下 → 上
for (int i = bottom; i >= top; i--) {
ans[i][left] = k++;
}
if (++left > right) {
break;
}
}
return ans;
}
}
复杂度
- 时间:O(n²),每个格子写入一次
- 空间:O(1) 额外(不计返回矩阵)
58. 区间和 (Range Sum)
数组前缀和ACM简单
题目
给定一个整数数组 Array,请计算该数组在每个指定区间内元素的总和。输入描述:第一行输入为整数数组 Array 的长度 n,接下来 n 行,每行一个整数,表示数组的元素。随后的输入为需要计算总和的区间下标:a,b (b >= a),直至文件结束。输出描述:输出每个指定区间内元素的总和。
示例 1:
输入:
5
1
2
3
4
5
0 1
1 3
输出:
3
9
提示:
- 数据范围: 0 < n <= 100000
思路简述
多次区间求和,暴力每次扫会超时。先建 前缀和:prefix[0]=0,prefix[i] 表示 arr[0..i-1] 的和;则区间 [a,b] 的和为 prefix[b+1]-prefix[a],单次查询 O(1)。n≤1e5 且查询到 EOF,此写法已达最优。
参考代码
JAVA
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = sc.nextInt();
}
// prefix[i] = arr[0] + ... + arr[i-1];prefix[0] = 0,方便区间差分
int[] prefix = new int[n + 1];
for (int i = 0; i < n; i++) {
prefix[i + 1] = prefix[i] + arr[i];
}
// 区间 [a, b](含端点)的和 = prefix[b+1] - prefix[a]
while (sc.hasNextInt()) {
int a = sc.nextInt();
int b = sc.nextInt();
int ans = prefix[b + 1] - prefix[a];
System.out.println(ans);
}
}
}
复杂度
- 时间:O(n + q),预处理 O(n),每次查询 O(1)
- 空间:O(n),前缀和数组
44. 开发商购买土地 (Developer Buy Land)
数组前缀和ACM中等
题目
在一个城市区域内,被划分成了n * m个连续的区块,每个区块都拥有不同的权值,代表着其土地价值。目前,有两家开发公司,A 公司和 B 公司,希望购买这个城市区域的土地。现在,需要将这个城市区域的所有区块分配给 A 公司和 B 公司。然而,由于城市规划的限制,只允许将区域按横向或纵向划分成两个子区域,而且每个子区域都必须包含一个或多个区块。
为了确保公平竞争,你需要找到一种分配方式,使得 A 公司和 B 公司各自的子区域内的土地总价值之差最小。注意:区块不可再分。输入描述:第一行输入两个正整数,代表 n 和 m。接下来的 n 行,每行输入 m 个正整数。输出描述:请输出一个整数,代表两个子区域内土地总价值之间的最小差距。
示例 1:
输入:
3 3
1 2 3
2 1 3
1 2 3
输出:
0
提示:
- 如果将区域按照如下方式划分: 1 2 | 3 2 1 | 3 1 2 | 3 两个子区域内土地总价值之间的最小差距可以达到 0。 数据范围: 1 <= n, m <= 100; n 和 m 不同时为 1。
思路简述
只能横切或纵切成两块非空矩形,等价于枚举切线。设全图总和为 total,一侧前缀和为 sum,则差距为 |2*sum-total|。读入时顺带做 行列求和 + 一维前缀:对行和 / 列和各扫一遍取最小差距。时间 O(nm) 已达读入下界。
参考代码
JAVA
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
// rowSum[i]:第 i 行元素之和;colSum[j]:第 j 列元素之和
int[] rowSum = new int[n];
int[] colSum = new int[m];
int total = 0; // 整块土地总价值
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
int v = sc.nextInt();
rowSum[i] += v;
colSum[j] += v;
total += v;
}
}
int ans = Integer.MAX_VALUE;
// 横切:在第 i 行与第 i+1 行之间切开(上半含 [0..i])
int prefix = 0;
for (int i = 0; i < n - 1; i++) {
prefix += rowSum[i];
ans = Math.min(ans, Math.abs(total - 2 * prefix)); // |total-prefix - prefix|
}
// 纵切:在第 j 列与第 j+1 列之间切开(左半含 [0..j])
prefix = 0;
for (int j = 0; j < m - 1; j++) {
prefix += colSum[j];
ans = Math.min(ans, Math.abs(total - 2 * prefix));
}
System.out.println(ans);
}
}
复杂度
- 时间:O(nm),读入并统计行列和后线性枚举切线
- 空间:O(n+m),仅存行和与列和
2. 链表 (Linked List)
核心思想: 虚拟头结点、反转、双指针与环检测。
题目列表
203. 移除链表元素 (Remove Linked List Elements) 简单 标签:链表, 递归
707. 设计链表 (Design Linked List) 中等 标签:设计, 链表
206. 反转链表 (Reverse Linked List) 简单 标签:链表, 递归
24. 两两交换链表中的节点 (Swap Nodes in Pairs) 中等 标签:链表, 递归
19. 删除链表的倒数第 N 个结点 (Remove Nth Node From End of List) 中等 标签:链表, 双指针
面试题 02.07. 链表相交 (Intersection of Two Linked Lists LCCI) 简单 标签:哈希表, 链表, 双指针
142. 环形链表 II (Linked List Cycle II) 中等 标签:哈希表, 链表, 双指针
203. 移除链表元素 (Remove Linked List Elements)
递归链表简单
题目
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
提示:
- 列表中的节点数目在范围
[0, 10^4]内 1 <= Node.val <= 500 <= val <= 50
思路简述
加 哑结点 统一处理头节点被删的情况。prev 跟在待审节点前:值等于 val 则跳过 curr,否则两指针同步前移。一趟扫描即可。
参考代码
JAVA
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(0, head); // 便于删除原头节点
ListNode prev = dummy;
ListNode curr = dummy.next;
while (curr != null) {
if (curr.val == val) {
prev.next = curr.next; // 跳过当前节点
curr = prev.next;
} else {
prev = curr;
curr = curr.next;
}
}
ListNode ans = dummy.next;
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
707. 设计链表 (Design Linked List)
设计链表中等
题目
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList 类:
MyLinkedList()初始化MyLinkedList对象。int get(int index)获取链表中下标为index的节点的值。如果下标无效,则返回-1。void addAtHead(int val)将一个值为val的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)将一个值为val的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)将一个值为val的节点插入到链表中下标为index的节点之前。如果index等于链表的长度,那么该节点会被追加到链表的末尾。如果index比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)如果下标有效,则删除链表中下标为index的节点。
示例 1:
输入:
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"][[], [1], [3], [1, 2], [1], [1], [1]]
输出:
[null, null, null, null, 2, null, 3]
解释:
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3
myLinkedList.get(1); // 返回 2
myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3
myLinkedList.get(1); // 返回 3
提示:
0 <= index, val <= 1000- 请不要使用内置的 LinkedList 库。
- 调用
get、addAtHead、addAtTail、addAtIndex和deleteAtIndex的次数不超过2000。
思路简述
用 带头尾哨兵的双向链表:真实节点夹在 head 与 tail 之间,插入/删除统一成「改四条指针」,无需特判空表。addAtHead / addAtTail 直接在哨兵旁 O(1) 插入;按索引操作时从更近的一端走到目标,最坏 O(n)。
若 addAtTail 复用 addAtIndex(size, val) 再从 head 走一遍,尾插会退化成 O(n);应直接插在 tail 前。
参考代码
JAVA
class MyLinkedList {
// 双向链表节点
private static class Node {
int val;
Node prev;
Node next;
Node(int val) {
this.val = val;
}
}
private final Node head; // 头哨兵,不存数据
private final Node tail; // 尾哨兵,不存数据
private int size; // 真实节点个数
public MyLinkedList() {
head = new Node(0);
tail = new Node(0);
head.next = tail;
tail.prev = head;
size = 0;
}
// 返回下标 index 处的真实节点;约定调用方保证 0 <= index < size
// 从更近的一端走,减少平均步数
private Node getNode(int index) {
Node curr;
if (index < size / 2) {
curr = head.next;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
} else {
curr = tail.prev;
for (int i = size - 1; i > index; i--) {
curr = curr.prev;
}
}
return curr;
}
// 在节点 curr 之前插入值为 val 的新节点
private void insertBefore(Node curr, int val) {
Node node = new Node(val);
Node prev = curr.prev;
// prev <-> node <-> curr
prev.next = node;
node.prev = prev;
node.next = curr;
curr.prev = node;
size++;
}
// 删除节点 curr(已保证是真实节点)
private void remove(Node curr) {
curr.prev.next = curr.next;
curr.next.prev = curr.prev;
size--;
}
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
int ans = getNode(index).val;
return ans;
}
public void addAtHead(int val) {
insertBefore(head.next, val); // 插在第一个真实节点前(或 tail 前)
}
public void addAtTail(int val) {
insertBefore(tail, val); // O(1) 尾插
}
public void addAtIndex(int index, int val) {
if (index > size) {
return; // 越界不插入
}
if (index < 0) {
index = 0; // 题意:负下标视为插到头部(约束下通常不会出现)
}
if (index == size) {
insertBefore(tail, val); // 追加到末尾
} else {
insertBefore(getNode(index), val);
}
}
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
remove(getNode(index));
}
}
复杂度
- 时间:
get/addAtIndex/deleteAtIndex为 O(n);addAtHead/addAtTail为 O(1) - 空间:O(n),存 n 个节点与常量哨兵
206. 反转链表 (Reverse Linked List)
递归链表简单
题目
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
提示:
- 链表中节点的数目范围是
[0, 5000] -5000 <= Node.val <= 5000- 进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
思路简述
迭代头插:维护 prev(已反转段的头)与 curr(待处理节点),每次把 curr 指向 prev 并前移,相当于不断把头插到新链表前端;扫完后 prev 即新头。时间 O(n)、额外空间 O(1),为双优。
递归写法:先递到末尾得到新头,归时把 head 接到 head.next(已反转段尾)后面并断开 head.next。正确但栈深 O(n),空间不如迭代。
参考代码
JAVA
class Solution {
public ListNode reverseList(ListNode head) {
// prev:已反转部分的头;初始 null 表示新链表为空
ListNode prev = null;
ListNode curr = head; // 当前待处理节点
while (curr != null) {
ListNode next = curr.next; // 先存后继,改指针后才能继续往后走
curr.next = prev; // 当前节点头插到已反转链表
prev = curr;
curr = next;
}
ListNode ans = prev; // 扫完后 prev 指向原末节点,即新头
return ans;
}
}
复杂度
- 时间:O(n),每个节点访问一次
- 空间:O(1)
24. 两两交换链表中的节点 (Swap Nodes in Pairs)
递归链表中等
题目
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
提示:
- 链表中节点的数目在范围
[0, 100]内 0 <= Node.val <= 100
思路简述
加 哑结点 统一处理头两节点的交换。每次取相邻一对 (curr, nextNode),用 prev 接上交换后的顺序:prev → nextNode → curr → 后续,再把 prev 移到 curr、curr 移到下一对起点。奇数个节点时最后单独留下即可。
参考代码
JAVA
class Solution {
public ListNode swapPairs(ListNode head) {
// dummy:哑结点,方便交换前两个节点时改「头」
ListNode dummy = new ListNode(0, head);
ListNode prev = dummy; // 当前待交换对的前驱
ListNode curr = dummy.next; // 对中第一个节点
while (curr != null && curr.next != null) {
ListNode nextNode = curr.next; // 对中第二个节点
// 交换:prev -> nextNode -> curr -> (原 nextNode 之后)
prev.next = nextNode;
curr.next = nextNode.next;
nextNode.next = curr;
// 移动到下一对:此时 curr 已是交换后的第二个节点
prev = curr;
curr = curr.next;
}
ListNode ans = dummy.next;
return ans;
}
}
复杂度
- 时间:O(n),每对节点处理常数次
- 空间:O(1)
19. 删除链表的倒数第 N 个结点 (Remove Nth Node From End of List)
链表双指针中等
题目
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
- 链表中结点的数目为
sz 1 <= sz <= 300 <= Node.val <= 1001 <= n <= sz- 进阶:你能尝试使用一趟扫描实现吗?
思路简述
一趟扫描用 快慢指针 + 哑结点:快指针先走 n 步,再与慢指针同步前进,直至快指针到达末尾;此时慢指针停在「待删节点的前驱」。哑结点保证删除头节点时写法一致。
参考代码
JAVA
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// dummy:便于删除头节点时统一改指针
ListNode dummy = new ListNode(0, head);
ListNode slow = dummy; // 最终停在待删节点的前驱
ListNode fast = dummy;
// 快指针先走 n 步,使快慢间距为 n
while (n > 0) {
fast = fast.next;
n--;
}
// 同步前进;fast 到末节点时,slow 的 next 即倒数第 n 个
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next; // 跳过待删节点
ListNode ans = dummy.next;
return ans;
}
}
复杂度
- 时间:O(sz),一趟扫描
- 空间:O(1)
面试题 02.07. 链表相交 (Intersection of Two Linked Lists LCCI)
哈希表链表双指针简单
题目
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。 在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。 由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。 这两个链表不相交,因此返回 null 。
提示:
listA中节点数目为mlistB中节点数目为n0 <= m, n <= 3 1041 <= Node.val <= 10^50 <= skipA <= m0 <= skipB <= n- 如果
listA和listB没有交点,intersectVal为0 - 如果
listA和listB有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1] - 进阶:你能否设计一个时间复杂度
O(n)、仅用O(1)内存的解决方案?
思路简述
两指针分别从 headA、headB 出发,走到末尾后 改走另一条链表。若相交,走过的路程均为 a+b+c,会在交点相遇;若不相交,最终同时走到 null。相对哈希表,额外空间 O(1)。
参考代码
JAVA
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pA = headA;
ListNode pB = headB;
// 不相交时会在 null 处相遇并退出;相交时在第一个公共节点相遇
while (pA != pB) {
// 走完本链后切换到另一条,对齐「独有段」长度差
pA = (pA == null) ? headB : pA.next;
pB = (pB == null) ? headA : pB.next;
}
ListNode ans = pA; // 交点,或 null
return ans;
}
}
复杂度
- 时间:O(m+n),两指针最多各走一遍两条链
- 空间:O(1)
142. 环形链表 II (Linked List Cycle II)
哈希表链表双指针中等
题目
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 10^4]内 -105 <= Node.val <= 10^5pos的值为-1或者链表中的一个有效索引- 进阶:你是否可以使用
O(1)空间解决此题?
思路简述
Floyd 判环:快慢指针同起点,快走两步慢走一步;若相遇则有环。设入环前长 a、环长 b、相遇点距入口 c,有 a ≡ c \pmod b,故将一指针重置到 head,两指针同步各走一步,再次相遇处即为环入口。相对哈希表,额外空间 O(1)。
参考代码
JAVA
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode fast = head;
ListNode slow = head;
// 第一阶段:判断是否有环;相遇点在环内某处
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
// 第二阶段:一指针回起点,同步前进找入口
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
ListNode ans = slow; // 环入口
return ans;
}
}
return null; // 无环
}
}
复杂度
- 时间:O(n),两阶段合计线性
- 空间:O(1)
3. 哈希表 (Hash Table)
核心思想: 计数、查找与多数组组合;三数/四数之和配合双指针。
题目列表
242. 有效的字母异位词 (Valid Anagram) 简单 标签:哈希表, 字符串, 排序
349. 两个数组的交集 (Intersection of Two Arrays) 简单 标签:数组, 哈希表, 双指针, 二分查找, 排序
202. 快乐数 (Happy Number) 简单 标签:哈希表, 数学, 双指针
1. 两数之和 (Two Sum) 简单 标签:数组, 哈希表
454. 四数相加 II (4Sum II) 中等 标签:数组, 哈希表
383. 赎金信 (Ransom Note) 简单 标签:哈希表, 字符串, 计数
15. 三数之和 (3Sum) 中等 标签:数组, 双指针, 排序
18. 四数之和 (4Sum) 中等 标签:数组, 双指针, 排序
242. 有效的字母异位词 (Valid Anagram)
哈希表字符串排序简单
题目
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的 字母异位词。
示例 1:
输入:s = "anagram", t = "nagaram"
输出:true
示例 2:
输入:s = "rat", t = "car"
输出:false
提示:
1 <= s.length, t.length <= 5 104s和t仅包含小写字母- 进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
思路简述
小写字母固定 26 种,用长度为 26 的计数数组:对 s 自增、对 t 自减,最终全为 0 则互为异位词。先比长度可剪枝。相对排序 O(n log n),此为 计数哈希 双优。
也可用两个 int[26] 分别统计再 Arrays.equals,复杂度同阶;单数组少一次比较与一份计数。
参考代码
JAVA
class Solution {
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false; // 长度不同必不是异位词
}
int[] cnt = new int[26]; // cnt[c-'a']:字母 c 在 s 中比 t 中多出的次数
for (int i = 0; i < s.length(); i++) {
cnt[s.charAt(i) - 'a']++;
cnt[t.charAt(i) - 'a']--;
}
for (int c : cnt) {
if (c != 0) {
return false;
}
}
return true;
}
}
复杂度
- 时间:O(n),n 为串长
- 空间:O(1),固定 26 格
349. 两个数组的交集 (Intersection of Two Arrays)
数组哈希表双指针二分查找排序简单
题目
给定两个数组 nums1 和 nums2 ,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
提示:
1 <= nums1.length, nums2.length <= 10000 <= nums1[i], nums2[i] <= 1000
思路简述
把 nums1 放入 HashSet,再扫 nums2:命中则加入结果集(结果集也用 Set 自动去重)。期望时间 O(n+m)。值域若很小也可用布尔数组代替哈希。
HashSet<Integer> 不能 toArray(new int[n]);需手动填 int[],或 stream().mapToInt(Integer::intValue).toArray()。
参考代码
JAVA
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set = new HashSet<>();
for (int num : nums1) {
set.add(num); // nums1 中出现过的数
}
Set<Integer> ansSet = new HashSet<>();
for (int num : nums2) {
if (set.contains(num)) {
ansSet.add(num); // 两边都有,Set 保证唯一
}
}
// HashSet 无法直接转 int[],逐个拆箱写入
int[] ans = new int[ansSet.size()];
int i = 0;
for (int num : ansSet) {
ans[i++] = num;
}
return ans;
}
}
复杂度
- 时间:O(n+m),哈希期望线性
- 空间:O(n+m),最坏存下两个数组中的去重元素
202. 快乐数 (Happy Number)
哈希表数学双指针简单
题目
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例 1:
输入:n = 19
输出:true
解释:12 + 92 = 82 82 + 22 = 68 62 + 82 = 100 12 + 02 + 02 = 1
示例 2:
输入:n = 2
输出:false
提示:
1 <= n <= 231 - 1
思路简述
不断把数换成各位平方和,非快乐数会进入环。用 快慢指针 判环:慢走一步、快走两步;若相遇值为 1 则快乐,否则陷入非 1 环。相对 HashSet 记录出现过的数,额外空间 O(1)。
HashSet 记录已出现值也可判环,但空间 O(环长)。各位平方和用 d*d,不要用 Math.pow(返回 double 且更慢)。
参考代码
JAVA
class Solution {
// 计算各位数字的平方和
private int getNext(int n) {
int sum = 0;
while (n > 0) {
int d = n % 10;
sum += d * d;
n /= 10;
}
return sum;
}
public boolean isHappy(int n) {
int slow = n;
int fast = getNext(n);
// 快慢相遇:要么都到 1,要么在非 1 的环里
while (fast != 1 && slow != fast) {
slow = getNext(slow);
fast = getNext(getNext(fast));
}
return fast == 1;
}
}
复杂度
- 时间:O(log n) 量级,平方和很快落到有界范围后常数步判环
- 空间:O(1)
1. 两数之和 (Two Sum)
数组哈希表简单
题目
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
提示:
2 <= nums.length <= 10^4-109 <= nums[i] <= 10^9-109 <= target <= 10^9- 只会存在一个有效答案
- 进阶:你可以想出一个时间复杂度小于
O(n^2)的算法吗?
思路简述
一遍扫描:用 HashMap 存「已遍历数值 → 下标」。对当前 nums[i],查 target-nums[i] 是否已出现;有则返回答案,否则把当前数入表。先查后放,避免同一元素用两次。
参考代码
JAVA
class Solution {
public int[] twoSum(int[] nums, int target) {
// map:数值 -> 下标(只存当前下标左侧的元素)
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i];
if (map.containsKey(need)) {
int[] ans = new int[]{map.get(need), i};
return ans;
}
map.put(nums[i], i); // 当前数留给后面的元素匹配
}
return new int[]{-1, -1}; // 题保证有解,不会走到这
}
}
复杂度
- 时间:O(n),期望每次哈希 O(1)
- 空间:O(n),哈希表
454. 四数相加 II (4Sum II)
数组哈希表中等
题目
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < nnums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:两个元组如下: 1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0 2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1
提示:
n == nums1.lengthn == nums2.lengthn == nums3.lengthn == nums4.length1 <= n <= 200-228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228
思路简述
本题求的是 下标元组个数(可重复取值按不同下标多次计),不是「去重四元组」。把数组两两分组:先枚举 nums1[i]+nums2[j] 的所有和写入哈希表计数,再枚举 nums3[k]+nums4[l],累加 -(sum) 在表中的出现次数。时间 O(n²)。
不宜套用「排序 + 四指针 / 像 18 题那样剪枝」:那是找不重复数值组合;本题四数组彼此独立、要统计下标组合,排序后双指针也难正确处理重数,且最坏仍接近 O(n⁴)。
参考代码
JAVA
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
// sum12 -> 该两数之和出现次数
Map<Integer, Integer> cnt = new HashMap<>();
for (int a : nums1) {
for (int b : nums2) {
int sum = a + b;
cnt.put(sum, cnt.getOrDefault(sum, 0) + 1);
}
}
int ans = 0;
for (int c : nums3) {
for (int d : nums4) {
// 需要 a+b == -(c+d)
ans += cnt.getOrDefault(-(c + d), 0);
}
}
return ans;
}
}
复杂度
- 时间:O(n²),两次数组两两枚举
- 空间:O(n²),哈希表存前两数组之和
383. 赎金信 (Ransom Note)
哈希表字符串计数简单
题目
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
示例 1:
输入:ransomNote = "a", magazine = "b"
输出:false
示例 2:
输入:ransomNote = "aa", magazine = "ab"
输出:false
示例 3:
输入:ransomNote = "aa", magazine = "aab"
输出:true
提示:
1 <= ransomNote.length, magazine.length <= 10^5ransomNote和magazine由小写英文字母组成
思路简述
小写字母 26 种,用计数数组统计 magazine 各字符库存;再扫 ransomNote,每用一个就 库存减一,一旦变为负说明不够。相对哈希表,定长数组常数更优。
参考代码
JAVA
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
if (ransomNote.length() > magazine.length()) {
return false; // 赎金信更长则必不够用
}
int[] cnt = new int[26]; // magazine 中各字母剩余次数
for (char ch : magazine.toCharArray()) {
cnt[ch - 'a']++;
}
for (char ch : ransomNote.toCharArray()) {
if (--cnt[ch - 'a'] < 0) {
return false; // 该字母用超了
}
}
return true;
}
}
复杂度
- 时间:O(|ransomNote| + |magazine|)
- 空间:O(1),固定 26 格
15. 三数之和 (3Sum)
数组双指针排序中等
题目
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,
输出:的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:0,0,0
解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000-10^5 <= nums[i] <= 10^5
思路简述
先排序,枚举第一个数 i,对右侧做 双指针 找两数之和为 -nums[i]。和偏小则 left++,偏大则 right--;命中后左右同时收缩,并跳过相同值去重。配合剪枝:最小三数和已 >0 可整段结束,当前数配最大两数仍 <0 则换更大的 i。
参考代码
JAVA
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n - 2; i++) {
// 最小的三个数都 > 0,后面更大,不可能再有和为 0
if (nums[i] + nums[i + 1] + nums[i + 2] > 0) {
break;
}
// 相同的第一个数只枚举一次,避免重复三元组
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 当前数即使配上最大的两个数仍 < 0,换更大的 nums[i]
if (nums[i] + nums[n - 1] + nums[n - 2] < 0) {
continue;
}
int left = i + 1;
int right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum < 0) {
left++;
} else if (sum > 0) {
right--;
} else {
ans.add(Arrays.asList(nums[i], nums[left], nums[right]));
left++;
right--;
// 跳过与刚加入解相同的左右值,保证不重复
while (left < right && nums[left] == nums[left - 1]) {
left++;
}
while (left < right && nums[right] == nums[right + 1]) {
right--;
}
}
}
}
return ans;
}
}
复杂度
- 时间:O(n²),排序 O(n log n) + 枚举×双指针
- 空间:O(1) 额外(不计答案与排序开销)
18. 四数之和 (4Sum)
数组双指针排序中等
题目
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < na、b、c和d互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:2,2,2,2
提示:
1 <= nums.length <= 200-109 <= nums[i] <= 10^9-109 <= target <= 10^9
思路简述
在三数之和上再套一层枚举:排序后固定 i、j,对右侧 双指针 找两数使四数之和等于 target;同层跳过相同值去重,并用最小/最大四数和剪枝。nums[i] 可达 ±1e9,求和必须用 (long) 防止溢出。
易错:long sum = a + b + c + d 会先按 int 相加再拓宽;应写成 (long) a + b + c + d。
参考代码
JAVA
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue; // 第一个数去重
}
// 最小四数和已超过 target,后面更大,结束
if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
break;
}
// 当前 nums[i] 配上最大三数仍不够,换更大的 i
if ((long) nums[i] + nums[n - 3] + nums[n - 2] + nums[n - 1] < target) {
continue;
}
for (int j = i + 1; j < n - 2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue; // 第二个数去重
}
if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
break;
}
if ((long) nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target) {
continue;
}
int left = j + 1;
int right = n - 1;
while (left < right) {
// 必须先把其中一个扩成 long,避免 int 溢出
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if (sum == target) {
ans.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
left++;
right--;
while (left < right && nums[left] == nums[left - 1]) {
left++;
}
while (left < right && nums[right] == nums[right + 1]) {
right--;
}
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
}
return ans;
}
}
复杂度
- 时间:O(n³),排序 + 两层枚举 × 双指针
- 空间:O(1) 额外(不计答案与排序开销)
4. 字符串 (String)
核心思想: 反转、替换、旋转与 KMP/匹配入门。
题目列表
344. 反转字符串 (Reverse String) 简单 标签:双指针, 字符串
541. 反转字符串 II (Reverse String II) 简单 标签:双指针, 字符串
剑指 Offer 05. 替换空格 (Replace Spaces) 简单 标签:字符串
151. 翻转字符串里的单词 (Reverse Words in a String) 中等 标签:双指针, 字符串
剑指 Offer 58 - II. 左旋转字符串 (Left Rotate String) 简单 标签:数学, 双指针, 字符串
28. 找出字符串中第一个匹配项的下标 (Find the Index of the First Occurrence in a String) 简单 标签:双指针, 字符串, 字符串匹配
459. 重复的子字符串 (Repeated Substring Pattern) 简单 标签:字符串, 字符串匹配
344. 反转字符串 (Reverse String)
双指针字符串简单
题目
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]
示例 2:
输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]
提示:
1 <= s.length <= 10^5s[i]都是 ASCII 码表中的可打印字符
思路简述
左右指针相向交换:left 从首、right 从尾,交换后相向移动,直到相遇。原地修改,额外空间 O(1)。
参考代码
JAVA
class Solution {
public void reverseString(char[] s) {
int left = 0;
int right = s.length - 1;
while (left < right) {
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
541. 反转字符串 II (Reverse String II)
双指针字符串简单
题目
给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符,再重新计数。
如果剩余字符少于
k个,则将剩余字符全部反转。如果剩余字符小于
2k但大于或等于k个,则反转前k个字符,其余字符保持原样。
示例 1:
输入:s = "abcdefg", k = 2
输出:"bacdfeg"
示例 2:
输入:s = "abcd", k = 2
输出:"bacd"
提示:
1 <= s.length <= 10^4s仅由小写英文组成1 <= k <= 10^4
思路简述
按步长 2k 分段:每段只反转前 k 个字符;若剩余不足 k 则整段反转。转成 char[] 原地交换,最后 new String(ch) 还原。时间 O(n)。
参考代码
JAVA
class Solution {
public String reverseStr(String s, int k) {
char[] ch = s.toCharArray();
int n = ch.length;
// 每 2k 一段,只处理该段前 k 个(或不足 k 的剩余)
for (int start = 0; start < n; start += 2 * k) {
int end = Math.min(start + k - 1, n - 1);
reverse(ch, start, end);
}
return new String(ch); // char[] → String
}
private void reverse(char[] ch, int left, int right) {
while (left < right) {
char temp = ch[left];
ch[left] = ch[right];
ch[right] = temp;
left++;
right--;
}
}
}
复杂度
- 时间:O(n),每个字符参与常数次交换
- 空间:O(n),字符数组(不计的话可视为改写输出所需)
剑指 Offer 05. 替换空格 (Replace Spaces)
字符串简单
题目
请实现一个函数,把字符串 s 中的每个空格替换成 %20。
力扣现已将该题迁移/改写为「LCR 122. 路径加密」;此处按代码随想录原题「替换空格」收录经典题面。
示例 1:
输入:s = "We are happy."
输出:"We%20are%20happy."
提示:
0 <= s.length <= 10000
思路简述
经典题:空格换成三个字符 %20。先统计空格数,扩容到 n + 2*spaceCnt,再从后往前填:非空格原样拷贝,空格写入 %20。倒填可避免覆盖未处理字符。
力扣现题 LCR 122. 路径加密 是把 . 换成空格,长度不变,直接 char[] 原地改即可。
参考代码
JAVA
class Solution {
// 经典:空格 → %20
public String replaceSpace(String s) {
int spaceCnt = 0;
for (char c : s.toCharArray()) {
if (c == ' ') {
spaceCnt++;
}
}
// 每个空格多占 2 格(' ' → "%20")
char[] ch = new char[s.length() + spaceCnt * 2];
int i = s.length() - 1; // 原串从后往前
int j = ch.length - 1; // 新数组从后往前填,避免覆盖
while (i >= 0) {
char c = s.charAt(i);
if (c == ' ') {
ch[j--] = '0';
ch[j--] = '2';
ch[j--] = '%';
} else {
ch[j--] = c;
}
i--;
}
return new String(ch);
}
}
复杂度
- 时间:O(n)
- 空间:O(n),结果数组
151. 翻转字符串里的单词 (Reverse Words in a String)
双指针字符串中等
题目
给你一个字符串 s ,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例 1:
输入:s = "the sky is blue"
输出:"blue is sky the"
示例 2:
输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3:
输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。
提示:
1 <= s.length <= 10^4s包含英文大小写字母、数字和空格' 's中 至少存在一个 单词- 进阶:如果字符串在你使用的编程语言中是一种可变数据类型,请尝试使用
O(1)额外空间复杂度的 原地 解法。
思路简述
三步(Java 先转 char[]):① 双指针去掉首尾/多余空格,单词间只留一个;② 反转整个有效区间;③ 再逐词反转。整体 O(n);相对 split+反转列表,更贴近原地思想。
参考代码
JAVA
class Solution {
public String reverseWords(String s) {
char[] arr = s.toCharArray();
// 1. 清除多余空格,返回有效长度
int len = removeSpace(arr);
// 2. 反转整个有效区间
reverse(arr, 0, len - 1);
// 3. 再反转每个单词
int slow = 0;
for (int fast = 0; fast < len; fast++) {
if (arr[fast] == ' ') {
reverse(arr, slow, fast - 1);
slow = fast + 1;
}
}
reverse(arr, slow, len - 1); // 最后一个单词
return new String(arr, 0, len);
}
private void reverse(char[] arr, int left, int right) {
while (left < right) {
char temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
// 原地压缩:单词之间只保留一个空格,去掉首尾空格;返回新长度
private int removeSpace(char[] arr) {
int n = arr.length;
int slow = 0;
int fast = 0;
while (fast < n) {
while (fast < n && arr[fast] == ' ') {
fast++; // 跳过空格
}
if (fast >= n) {
break;
}
if (slow > 0) {
arr[slow++] = ' '; // 单词之间补一个空格
}
while (fast < n && arr[fast] != ' ') {
arr[slow++] = arr[fast++]; // 拷贝单词
}
}
return slow;
}
}
复杂度
- 时间:O(n)
- 空间:O(n)(
char[];若语言可变字符串则可做到 O(1) 额外)
剑指 Offer 58 - II. 左旋转字符串 (Left Rotate String)
数学双指针字符串简单
题目
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串 "abcdefg" 和数字 2,该函数将返回左旋转两位得到的结果 "cdefgab"。
力扣现已将该题迁移/改写为「LCR 182. 动态口令」;此处按代码随想录原题「左旋转字符串」收录经典题面。
示例 1:
输入:s = "abcdefg", k = 2
输出:"cdefgab"
示例 2:
输入:s = "lrloseumgh", k = 6
输出:"umghlrlose"
提示:
1 <= k < s.length <= 10000
思路简述
左旋 k 位 = 把前 k 个字符挪到末尾。用三次 局部反转:先反转整串,再反转前 n-k 段、后 k 段(或反过来:先反前 k、再反后 n-k、最后反整串)。O(n) 时间、O(1) 额外(不计 char[])。
力扣现题方法名多为 dynamicPassword(password, target),与经典 reverseLeftWords(s, n) 同一题。
参考代码
JAVA
class Solution {
// 经典接口;LCR 182 可把方法名改成 dynamicPassword,参数同理
public String reverseLeftWords(String s, int k) {
char[] ch = s.toCharArray();
int n = ch.length;
// 整串反转 → 反转前 n-k → 反转后 k,等价于左旋 k
reverse(ch, 0, n - 1);
reverse(ch, 0, n - k - 1);
reverse(ch, n - k, n - 1);
return new String(ch);
}
private void reverse(char[] s, int left, int right) {
while (left < right) {
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
}
复杂度
- 时间:O(n)
- 空间:O(n)(
char[];辅助变量 O(1))
28. 找出字符串中第一个匹配项的下标 (Find the Index of the First Occurrence in a String)
双指针字符串字符串匹配简单
题目
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。 第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
提示:
1 <= haystack.length, needle.length <= 10^4haystack和needle仅由小写英文字符组成
思路简述
用 KMP:先对 needle 求前缀表 next[i] = 以 i 结尾的最长相等前后缀长度(不减一写法);匹配时失配则 j = next[j-1] 回退,主串指针不回头。时间 O(n+m)。
参考代码
JAVA
class Solution {
public int strStr(String haystack, String needle) {
int m = needle.length();
if (m == 0) {
return 0; // 空模式约定返回 0(现约束下通常不会出现)
}
// next[i]:needle[0..i] 的最长相等前后缀长度(不减一)
int[] next = new int[m];
for (int right = 1, left = 0; right < m; right++) {
while (left > 0 && needle.charAt(left) != needle.charAt(right)) {
left = next[left - 1]; // 前后缀继续缩短
}
if (needle.charAt(left) == needle.charAt(right)) {
left++;
}
next[right] = left;
}
// i 扫 haystack,j 表示当前已匹配到 needle 的长度
for (int i = 0, j = 0; i < haystack.length(); i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = next[j - 1];
}
if (haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if (j == m) {
int ans = i - m + 1; // 匹配起点
return ans;
}
}
return -1;
}
}
复杂度
- 时间:O(n+m),n、m 分别为两串长度
- 空间:O(m),前缀表
459. 重复的子字符串 (Repeated Substring Pattern)
字符串字符串匹配简单
题目
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
示例 1:
输入:s = "abab"
输出:true
解释:可由子串 "ab" 重复两次构成。
示例 2:
输入:s = "aba"
输出:false
示例 3:
输入:s = "abcabcabcabc"
输出:true
解释:可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)
提示:
1 <= s.length <= 10^4s由小写英文字母组成
思路简述
若 s 由长度为 period 的子串重复而成,则其最长相等前后缀满足 n % (n - next[n-1]) == 0 且周期 period = n - next[n-1] < n。对 s 建 KMP 前缀表(不减一)即可 O(n) 判定。
枚举因子再 startsWith/substring 能过,但反复建串偏慢。另有一句话写法:(s+s).substring(1, 2*n-1).contains(s),本质也是找周期。
参考代码
JAVA
class Solution {
public boolean repeatedSubstringPattern(String s) {
int n = s.length();
// next[i]:s[0..i] 的最长相等前后缀长度
int[] next = new int[n];
for (int right = 1, left = 0; right < n; right++) {
while (left > 0 && s.charAt(left) != s.charAt(right)) {
left = next[left - 1];
}
if (s.charAt(left) == s.charAt(right)) {
left++;
}
next[right] = left;
}
int longest = next[n - 1]; // 整串最长相等前后缀
int period = n - longest; // 最小周期长度
// 能由子串重复构成 ⟺ 存在周期 period,且 period 整除 n,且不是「整串自身」
return longest > 0 && n % period == 0;
}
}
复杂度
- 时间:O(n)
- 空间:O(n),前缀表
5. 双指针 (Two Pointers)
核心思想: 复习专题:题目正文见前文对应分类,此处仅作清单回顾。
题目列表
27. 移除元素 (Remove Element)
344. 反转字符串 (Reverse String)
剑指 Offer 05. 替换空格 (Replace Spaces)
151. 翻转字符串里的单词 (Reverse Words in a String)
206. 反转链表 (Reverse Linked List)
19. 删除链表的倒数第 N 个结点 (Remove Nth Node From End of List)
面试题 02.07. 链表相交 (Intersection of Two Linked Lists LCCI)
142. 环形链表 II (Linked List Cycle II)
15. 三数之和 (3Sum)
18. 四数之和 (4Sum)
正文见上文对应题号小节,此处不重复占位。
6. 栈与队列 (Stack & Queue)
核心思想: 栈队列互相实现、括号匹配、单调队列与堆。
题目列表
232. 用栈实现队列 (Implement Queue using Stacks) 简单 标签:栈, 设计, 队列
225. 用队列实现栈 (Implement Stack using Queues) 简单 标签:栈, 设计, 队列
20. 有效的括号 (Valid Parentheses) 简单 标签:栈, 字符串
1047. 删除字符串中的所有相邻重复项 (Remove All Adjacent Duplicates In String) 简单 标签:栈, 字符串
150. 逆波兰表达式求值 (Evaluate Reverse Polish Notation) 中等 标签:栈, 数组, 数学
239. 滑动窗口最大值 (Sliding Window Maximum) 困难 标签:队列, 数组, 滑动窗口, 单调队列, 堆(优先队列)
347. 前 K 个高频元素 (Top K Frequent Elements) 中等 标签:数组, 哈希表, 分治, 桶排序, 计数, 排序, 堆(优先队列)
232. 用栈实现队列 (Implement Queue using Stacks)
栈设计队列简单
题目
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x)将元素 x 推到队列的末尾int pop()从队列的开头移除并返回元素int peek()返回队列开头的元素boolean empty()如果队列为空,返回true;否则,返回false
说明:
你 只能 使用标准的栈操作 —— 也就是只有
push to top,peek/pop from top,size, 和is empty操作是合法的。你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
示例 1:
输入:["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:[null, null, null, 1, 1, false]
解释:MyQueue myQueue = new MyQueue(); myQueue.push(1); // queue is: [1] myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue) myQueue.peek(); // return 1 myQueue.pop(); // return 1, queue is [2] myQueue.empty(); // return false
提示:
1 <= x <= 9- 最多调用
100次push、pop、peek和empty - 假设所有操作都是有效的 (例如,一个空的队列不会调用
pop或者peek操作) - 进阶:你能否实现每个操作均摊时间复杂度为
O(1)的队列?换句话说,执行n个操作的总时间复杂度为O(n),即使其中一个操作可能花费较长时间。
思路简述
用两个栈:in 专责入队,out 专责出队。push 只压入 in;pop/peek 时若 out 为空,把 in 全部倒入 out(顺序反转一次后队首在栈顶)。每个元素最多搬一次,操作 均摊 O(1)。
参考代码
JAVA
class MyQueue {
private final Deque<Integer> in; // 入队栈
private final Deque<Integer> out; // 出队栈
public MyQueue() {
in = new ArrayDeque<>();
out = new ArrayDeque<>();
}
// 只往 in 压,对应队列尾
public void push(int x) {
in.push(x);
}
// out 为空时,把 in 倒入 out,此时 out 栈顶即队首
private void transfer() {
if (out.isEmpty()) {
while (!in.isEmpty()) {
out.push(in.pop());
}
}
}
public int pop() {
transfer();
return out.pop();
}
public int peek() {
transfer();
return out.peek();
}
public boolean empty() {
return in.isEmpty() && out.isEmpty();
}
}
复杂度
- 时间:均摊 O(1);单次
pop/peek最坏 O(n)(倒栈) - 空间:O(n)
225. 用队列实现栈 (Implement Stack using Queues)
栈设计队列简单
题目
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x)将元素 x 压入栈顶。int pop()移除并返回栈顶元素。int top()返回栈顶元素。boolean empty()如果栈是空的,返回true;否则,返回false。
注意:
你只能使用队列的标准操作 —— 也就是
push to back、peek/pop from front、size和is empty这些操作。你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
示例 1:
输入:[ "MyStack", "push", "push", "top", "pop", "empty" ] [ [ ], [1], [2], [], [], [] ]
输出:[null, null, null, 2, 2, false]
解释:MyStack myStack = new MyStack(); myStack.push(1); myStack.push(2); myStack.top(); // 返回 2 myStack.pop(); // 返回 2 myStack.empty(); // 返回 False
提示:
1 <= x <= 9- 最多调用
100次push、pop、top和empty - 每次调用
pop和top都保证栈不为空 - 进阶:你能否仅用一个队列来实现栈。
思路简述
目标是让 队首始终等于栈顶,这样 pop/top 都是 O(1)。
单队列:
push后把前面size-1个元素出队再入队,新元素转到队首。双队列:新元素入空队列
q2,再把原队列q1全部挪到q2,最后交换引用,使q1始终存完整栈。
参考代码(解法一:单队列)
JAVA
class MyStack {
private final Deque<Integer> q; // 只用队列的入队尾 / 出队首
public MyStack() {
q = new ArrayDeque<>();
}
// 新元素入队后,把旧元素轮转到后面,使队首 = 栈顶
public void push(int x) {
int n = q.size();
q.offer(x);
for (int i = 0; i < n; i++) {
q.offer(q.poll());
}
}
public int pop() {
return q.poll(); // 队首即栈顶
}
public int top() {
return q.peek();
}
public boolean empty() {
return q.isEmpty();
}
}
参考代码(解法二:双队列)
JAVA
class MyStack {
private Deque<Integer> q1; // 始终存放「完整栈」,队首为栈顶
private Deque<Integer> q2; // 辅助队列,push 时暂存
public MyStack() {
q1 = new ArrayDeque<>();
q2 = new ArrayDeque<>();
}
public void push(int x) {
q2.offer(x); // 新元素先进入空的辅助队列(成为队首)
while (!q1.isEmpty()) {
q2.offer(q1.poll()); // 旧元素接到新元素后面
}
// 交换:让 q1 重新指向完整栈
Deque<Integer> tmp = q1;
q1 = q2;
q2 = tmp;
}
public int pop() {
return q1.poll();
}
public int top() {
return q1.peek();
}
public boolean empty() {
return q1.isEmpty();
}
}
复杂度
- 时间:两种写法均为
pushO(n);pop/top/emptyO(1) - 空间:O(n)
20. 有效的括号 (Valid Parentheses)
栈字符串简单
题目
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([])"
输出:true
示例 5:
输入:s = "([)]"
输出:false
提示:
1 <= s.length <= 10^4s仅由括号'()[]{}'组成
思路简述
遇左括号时,向栈中压入对应的右括号;遇右括号则与栈顶弹出比较,不匹配或栈空则非法。扫完后栈必须为空。用栈做 括号匹配,O(n)。
参考代码
JAVA
class Solution {
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char ch : s.toCharArray()) {
if (ch == '(') {
stack.push(')'); // 压入期望的右括号
} else if (ch == '[') {
stack.push(']');
} else if (ch == '{') {
stack.push('}');
} else {
// 右括号:栈空或与期望不符
if (stack.isEmpty() || stack.pop() != ch) {
return false;
}
}
}
return stack.isEmpty(); // 全部匹配完才合法
}
}
复杂度
- 时间:O(n)
- 空间:O(n),最坏全是左括号
1047. 删除字符串中的所有相邻重复项 (Remove All Adjacent Duplicates In String)
栈字符串简单
题目
给出由小写字母组成的字符串 s,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 s 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例 1:
输入:"abbaca"
输出:"ca"
解释:例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
提示:
1 <= s.length <= 10^5s仅由小写英文字母组成。
思路简述
用 栈(或 StringBuilder 当栈):当前字符与栈顶相同则弹出(消去一对),否则压入。一次扫描等价于反复消消乐,O(n)。
在 StringBuilder 上反复 deleteCharAt 虽正确,但每次删除是 O(n),最坏 O(n²),n=1e5 易超时。
参考代码
JAVA
class Solution {
public String removeDuplicates(String s) {
StringBuilder stack = new StringBuilder(); // 栈顶 = 末尾字符
for (char ch : s.toCharArray()) {
int n = stack.length();
if (n > 0 && stack.charAt(n - 1) == ch) {
stack.deleteCharAt(n - 1); // 与栈顶相同,消掉一对
} else {
stack.append(ch);
}
}
return stack.toString();
}
}
复杂度
- 时间:O(n),每个字符最多入栈/出栈一次(末尾删改 O(1) 摊还)
- 空间:O(n)
150. 逆波兰表达式求值 (Evaluate Reverse Polish Notation)
栈数组数学中等
题目
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
有效的算符为
'+'、'-'、'*'和'/'。每个操作数(运算对象)都可以是一个整数或者另一个表达式。
两个整数之间的除法总是 向零截断 。
表达式中不含除零运算。
输入是一个根据逆波兰表示法表示的算术表达式。
答案及所有中间计算结果可以用 32 位 整数表示。
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = ["10","6","9","3","+","-11","","/","","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 = ((10 * (6 / (12 * -11))) + 17) + 5 = ((10 * (6 / -132)) + 17) + 5 = ((10 * 0) + 17) + 5 = (0 + 17) + 5 = 17 + 5 = 22
提示:
1 <= tokens.length <= 10^4tokens[i]是一个算符("+"、"-"、""或"/"),或是在范围[-200, 200]内的一个整数- 逆波兰表达式:
- 逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
- 平常使用的算式则是一种中缀表达式,如
( 1 + 2 ) ( 3 + 4 )。 - 该算式的逆波兰表达式写法为
( ( 1 2 + ) ( 3 4 + ) )。 - 逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成
1 2 + 3 4 +也可以依据次序计算出正确结果。 - 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
思路简述
后缀表达式天然适合 栈:数字入栈;遇到运算符弹出两个操作数(注意先出的是右操作数),算完再压回。最后栈中唯一元素即结果。
参考代码
JAVA
class Solution {
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String token : tokens) {
// 数字:长度>1(如 -11)或首字符是数字
if (token.length() > 1 || Character.isDigit(token.charAt(0))) {
stack.push(Integer.parseInt(token));
continue;
}
int right = stack.pop(); // 后弹出的是右操作数
int left = stack.pop();
switch (token) {
case "+":
stack.push(left + right);
break;
case "-":
stack.push(left - right);
break;
case "*":
stack.push(left * right);
break;
case "/":
stack.push(left / right); // 向零截断,Java int 除法即如此
break;
default:
break;
}
}
int ans = stack.peek();
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(n)
239. 滑动窗口最大值 (Sliding Window Maximum)
队列数组滑动窗口单调队列堆(优先队列)困难
题目
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:滑动窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
1 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^41 <= k <= nums.length
思路简述
维护下标的 单调递减双端队列:队首始终是当前窗口最大值下标。新元素入队前从队尾弹出所有更小/相等者;窗口滑出时若队首下标过期则弹出。每个下标进出队列至多一次,O(n)。
参考代码
JAVA
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] ans = new int[n - k + 1];
// 存下标;对应值单调递减,队首为窗口最大值
Deque<Integer> deque = new ArrayDeque<>();
for (int j = 0; j < n; j++) {
// 新元素更大则挤掉队尾更小的候选
while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[j]) {
deque.pollLast();
}
deque.offerLast(j);
int left = j - k + 1; // 当前窗口左端
if (left < 0) {
continue; // 窗口尚未成型
}
// 队首下标已滑出窗口则丢弃
while (deque.peekFirst() < left) {
deque.pollFirst();
}
ans[left] = nums[deque.peekFirst()];
}
return ans;
}
}
复杂度
- 时间:O(n),每个下标最多入队、出队一次
- 空间:O(k),队列中最多 k 个下标
347. 前 K 个高频元素 (Top K Frequent Elements)
数组哈希表分治桶排序计数快速选择排序堆(优先队列)中等
题目
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入:nums = [1,1,1,2,2,3], k = 2
输出:[1,2]
示例 2:
输入:nums = [1], k = 1
输出:[1]
示例 3:
输入:nums = [1,2,1,2,1,2,3,1,3,2], k = 2
输出:[1,2]
提示:
1 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4k的取值范围是[1, 数组中不相同的元素的个数]- 题目数据保证答案唯一,换句话说,数组中前
k个高频元素的集合是唯一的 - 进阶:你所设计算法的时间复杂度 必须 优于
O(n log n),其中n是数组大小。
思路简述
先哈希计数。取前 k 高频有两种常见写法:
桶排序:
buckets[f]存频次为f的数,从高往低取,时间 O(n)。大小为 k 的小根堆:堆里按频次比较,超过 k 就弹出最小频次,最后堆中即答案,时间 O(n log k)。
桶数组长度 n+1 会有空槽,但只是引用占位,渐进仍是 O(n),换时间最优。不必做成「优先队列里每个频次挂一个 List」——对每个 (num,freq) 直接进小根堆更简单;若坚持按频次聚组,用 TreeMap<Integer, List<Integer>> 也可,但多 log 因子。
参考代码(解法一:桶排序,时间最优)
JAVA
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> freq = new HashMap<>();
for (int num : nums) {
freq.put(num, freq.getOrDefault(num, 0) + 1);
}
// buckets[f]:出现次数恰好为 f 的所有数值;下标最大为 n
List<Integer>[] buckets = new List[nums.length + 1];
for (Map.Entry<Integer, Integer> e : freq.entrySet()) {
int f = e.getValue();
if (buckets[f] == null) {
buckets[f] = new ArrayList<>();
}
buckets[f].add(e.getKey());
}
int[] ans = new int[k];
int idx = 0;
// 从高频到低频取
for (int f = buckets.length - 1; f >= 0 && idx < k; f--) {
if (buckets[f] == null) {
continue;
}
for (int num : buckets[f]) {
ans[idx++] = num;
if (idx == k) {
return ans;
}
}
}
return ans;
}
}
参考代码(解法二:小根堆,空间更紧、实现直观)
JAVA
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> freq = new HashMap<>();
for (int num : nums) {
freq.put(num, freq.getOrDefault(num, 0) + 1);
}
// 小根堆:堆顶是当前堆中频次最小的;大小超过 k 就丢掉它
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
for (Map.Entry<Integer, Integer> e : freq.entrySet()) {
pq.offer(new int[]{e.getKey(), e.getValue()});
if (pq.size() > k) {
pq.poll();
}
}
int[] ans = new int[k];
for (int i = k - 1; i >= 0; i--) {
ans[i] = pq.poll()[0]; // 弹出顺序是频次升序,倒着填可略好看
}
return ans;
}
}
复杂度
- 桶排序:时间 O(n),空间 O(n)(含长度 n+1 的桶数组引用)
- 小根堆:时间 O(n log k),空间 O(n + k)(哈希 + 堆,无大段空桶)














