LeetCode 701-725. 题解合集
701. Insert into a Binary Search Tree(二叉搜索树中的插入操作)
题目描述
给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value,将值插入二叉搜索树。
返回插入后二叉搜索树的根节点。输入数据保证新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。你可以返回
任意有效的结果。
示例 1
- 输入:
root= [4,2,7,1,3],val= 5 - 输出:[4,2,7,1,3,5]
- 解释:另一个满足题目要求的结果是 [5,2,7,1,3,
null,null,null,null,null,4]。
示例 2
- 输入:
root= [40,20,60,10,30,50,70],val= 25 - 输出:[40,20,60,10,30,50,70,
null,null,25]
示例 3
- 输入:
root= [4,2,7,1,3,null,null,null,null,null,null],val= 5 - 输出:[4,2,7,1,3,5]
提示: - 树中的节点数在
[0, 10^4]范围内 -10^8 <= Node.val <= 10^8- 所有值
Node.val是独一无二的 -10^8 <= val <= 10^8- 保证
val在原始 BST 中不存在
解题思路
解法一(递归,主提交):根据二叉搜索树性质,若 val < root.val 则插入左子树,
否则插入右子树;空节点处新建节点。返回新根。时间复杂度 O(h),空间复杂度 O(h)。
解法二(迭代):与递归等价,用指针沿树下降,遇到空位插入即可。时间复杂度 O(h),
空间复杂度 O(1)。
代码实现
class Solution {
public static 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;
}
}
// 解法一:递归
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
}
if (val < root.val) {
root.left = insertIntoBST(root.left, val);
} else {
root.right = insertIntoBST(root.right, val);
}
return root;
}
// 解法二:迭代
public TreeNode insertIntoBST2(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
}
TreeNode cur = root;
while (true) {
if (val < cur.val) {
if (cur.left == null) {
cur.left = new TreeNode(val);
break;
}
cur = cur.left;
} else {
if (cur.right == null) {
cur.right = new TreeNode(val);
break;
}
cur = cur.right;
}
}
return root;
}
// 层序转数组(本地验证用,空节点用 -1 表示)
private static java.util.List<Integer> toLevelList(TreeNode root) {
java.util.List<Integer> res = new java.util.ArrayList<>();
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode node = q.poll();
if (node == null) {
res.add(-1);
continue;
}
res.add(node.val);
q.offer(node.left);
q.offer(node.right);
}
while (!res.isEmpty() && res.get(res.size() - 1) == -1) {
res.remove(res.size() - 1);
}
return res;
}
private static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode node = q.poll();
if (arr[i] != null) {
node.left = new TreeNode(arr[i]);
q.offer(node.left);
}
++i;
if (i < arr.length && arr[i] != null) {
node.right = new TreeNode(arr[i]);
q.offer(node.right);
}
++i;
}
return root;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
TreeNode r1 = build(new Integer[]{4, 2, 7, 1, 3});
TreeNode res1 = sol.insertIntoBST(r1, 5);
++total; if (toLevelList(res1).equals(java.util.Arrays.asList(4, 2, 7, 1, 3, 5))) ++pass; else System.out.println("FAIL test1: " + toLevelList(res1));
TreeNode r2 = build(new Integer[]{40, 20, 60, 10, 30, 50, 70});
TreeNode res2 = sol.insertIntoBST(r2, 25);
++total; if (toLevelList(res2).equals(java.util.Arrays.asList(40, 20, 60, 10, 30, 50, 70, -1, -1, 25))) ++pass; else System.out.println("FAIL test2: " + toLevelList(res2));
TreeNode r3 = build(new Integer[]{4, 2, 7, 1, 3});
TreeNode res3 = sol.insertIntoBST2(r3, 5);
++total; if (toLevelList(res3).equals(java.util.Arrays.asList(4, 2, 7, 1, 3, 5))) ++pass; else System.out.println("FAIL test3: " + toLevelList(res3));
TreeNode r4 = build(new Integer[]{});
TreeNode res4 = sol.insertIntoBST2(null, 5);
++total; if (res4 != null && res4.val == 5) ++pass; else System.out.println("FAIL test4");
if (pass == total) {
System.out.println("== 0701 PASS ==");
} else {
System.out.println("== 0701 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(h) | O(h) |
| 解法二 | O(h) | O(1) |
702. Search in a Sorted Array of Unknown Size(搜索长度未知的有序数组)
题目描述
给定一个升序整数数组并写入内存。但你不知道数组的大小。与 ArrayReader 接口交互即可:
你可以调用 ArrayReader.get(index) 获取数组 index 下标对应的值。
- 如果
0 <= index < arr.length,返回arr[index] - 否则返回
2^31 - 1(即2147483647)
返回与target相等的下标;若数组中不存在target,返回-1。
示例 1
- 输入:reader = [-1,0,3,5,9,12],
target= 9 - 输出:4
- 解释:下标 4 处值为 9,返回 4。
示例 2
- 输入:reader = [-1,0,3,5,9,12],
target= 2 - 输出:-1
提示: 1 <= arr.length <= 10^4-10^4 <= arr[i] <= 10^4arr按升序排列-10^4 <= target <= 10^4
解题思路
解法一(倍增右边界 + 二分,主提交):数组长度未知,先以 1,2,4,8,... 倍增找到第一个
get(r) >= target 的右边界(或越界返回 Integer.MAX_VALUE),再在 [l, r] 上标准二分。
时间复杂度 O(log n),空间复杂度 O(1)。
解法二(直接倍增二分右边界 + 左闭右闭二分):细节稍有不同,同样 O(log n)。
代码实现
class Solution {
// 与 LeetCode 相同的 ArrayReader 接口(本地辅助)
public interface ArrayReader {
int get(int index);
}
// 解法一:倍增右边界 + 标准二分
public int search(ArrayReader reader, int target) {
int l = 0, r = 1;
while (reader.get(r) < target) {
l = r;
r <<= 1;
}
while (l <= r) {
int mid = (l + r) >>> 1;
int v = reader.get(mid);
if (v == target) {
return mid;
}
if (v < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return -1;
}
// 解法二:仅倍增边界 + 线性逼近(等价二分)
public int search2(ArrayReader reader, int target) {
int hi = 1;
while (reader.get(hi) < target) {
hi <<= 1;
}
int lo = 0;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
int v = reader.get(mid);
if (v == target) {
return mid;
}
if (v < target) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return -1;
}
public static void main(String[] args) {
final int[] arr = {-1, 0, 3, 5, 9, 12};
ArrayReader reader = new ArrayReader() {
public int get(int index) {
if (index < 0 || index >= arr.length) {
return Integer.MAX_VALUE;
}
return arr[index];
}
};
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.search(reader, 9) == 4) ++pass; else System.out.println("FAIL test1: " + sol.search(reader, 9));
++total; if (sol.search(reader, 2) == -1) ++pass; else System.out.println("FAIL test2: " + sol.search(reader, 2));
++total; if (sol.search(reader, 12) == 5) ++pass; else System.out.println("FAIL test3: " + sol.search(reader, 12));
++total; if (sol.search(reader, -1) == 0) ++pass; else System.out.println("FAIL test4: " + sol.search(reader, -1));
++total; if (sol.search2(reader, 9) == 4) ++pass; else System.out.println("FAIL test5: " + sol.search2(reader, 9));
++total; if (sol.search2(reader, 2) == -1) ++pass; else System.out.println("FAIL test6: " + sol.search2(reader, 2));
++total; if (sol.search2(reader, 12) == 5) ++pass; else System.out.println("FAIL test7: " + sol.search2(reader, 12));
++total; if (sol.search2(reader, -1) == 0) ++pass; else System.out.println("FAIL test8: " + sol.search2(reader, -1));
if (pass == total) {
System.out.println("== 0702 PASS ==");
} else {
System.out.println("== 0702 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(log n) | O(1) |
| 解法二 | O(log n) | O(1) |
703. Kth Largest Element in a Stream(数据流中的第 K 大元素)
题目描述
设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k
个不同的元素。
请实现 KthLargest 类:
KthLargest(int k, int[] nums)使用整数k和整数流nums初始化对象。int add(int val)将val插入数据流nums后,返回当前数据流中第k大的元素。
示例
- 输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]] - 输出:
[null, 4, 5, 5, 8, 8] - 解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3); // return 4
kthLargest.add(5); // return 5
kthLargest.add(10); // return 8
kthLargest.add(9); // return 8
kthLargest.add(4); // return 8
提示: 1 <= k <= 10^40 <= nums.length <= 10^4-10^4 <= nums[i] <= 10^4-10^4 <= val <= 10^4- 最多调用
add方法10^4次 - 题目数据保证,在查找第
k大元素时,数组中至少有k个元素
解题思路
解法一(小根堆保持 k 个元素,主提交):维护一个容量为 k 的小根堆,堆顶即第 k 大元素。
初始化时将 nums 全部加入堆,超过 k 就弹出最小;add(val) 先入堆,若堆大小超过 k
则弹出堆顶,返回堆顶。时间复杂度 O(log k)(add),空间复杂度 O(k)。
解法二(快选 / n 项排序思路):当数据总量不大时也可用有序数组维护,插入 O(k);仅作参考。
代码实现
import java.util.PriorityQueue;
import java.util.ArrayList;
import java.util.List;
class KthLargest {
private final PriorityQueue<Integer> pq;
private final int k;
public KthLargest(int k, int[] nums) {
this.k = k;
this.pq = new PriorityQueue<>(k);
for (int v : nums) {
add(v);
}
}
public int add(int val) {
pq.offer(val);
if (pq.size() > k) {
pq.poll();
}
return pq.peek();
}
// ===== 本地测试 =====
public static void main(String[] args) {
int pass = 0, total = 0;
KthLargest kth = new KthLargest(3, new int[]{4, 5, 8, 2});
++total; if (kth.add(3) == 4) ++pass; else System.out.println("FAIL test1: " + kth.add(3));
++total; if (kth.add(5) == 5) ++pass; else System.out.println("FAIL test2");
++total; if (kth.add(10) == 5) ++pass; else System.out.println("FAIL test3");
++total; if (kth.add(9) == 8) ++pass; else System.out.println("FAIL test4");
++total; if (kth.add(4) == 8) ++pass; else System.out.println("FAIL test5");
if (pass == total) {
System.out.println("== 0703 PASS ==");
} else {
System.out.println("== 0703 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(log k)(add) | O(k) |
| 解法二 | O(k)(add) | O(k) |
704. Binary Search(二分查找)
题目描述
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,写一个函数搜索
nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 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]之间
解题思路
解法一(左闭右闭标准二分,主提交):l = 0, r = n - 1,mid 用无符号右移防溢出;
根据 nums[mid] 与 target 比较收缩区间。时间复杂度 O(log n),空间复杂度 O(1)。
解法二(左闭右开二分):l = 0, r = n,循环条件 l < r,收缩时 r = mid 或 l = mid + 1。
时间复杂度 O(log n),空间复杂度 O(1)。
代码实现
class Solution {
// 解法一:左闭右闭
public int search(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = (l + r) >>> 1;
if (nums[mid] == target) {
return mid;
}
if (nums[mid] < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return -1;
}
// 解法二:左闭右开
public int search2(int[] nums, int target) {
int l = 0, r = nums.length;
while (l < r) {
int mid = (l + r) >>> 1;
if (nums[mid] == target) {
return mid;
}
if (nums[mid] < target) {
l = mid + 1;
} else {
r = mid;
}
}
return -1;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] nums = {-1, 0, 3, 5, 9, 12};
++total; if (sol.search(nums, 9) == 4) ++pass; else System.out.println("FAIL test1");
++total; if (sol.search(nums, 2) == -1) ++pass; else System.out.println("FAIL test2");
++total; if (sol.search(nums, -1) == 0) ++pass; else System.out.println("FAIL test3");
++total; if (sol.search(new int[]{5}, 5) == 0) ++pass; else System.out.println("FAIL test4");
++total; if (sol.search2(nums, 9) == 4) ++pass; else System.out.println("FAIL test5");
++total; if (sol.search2(nums, 2) == -1) ++pass; else System.out.println("FAIL test6");
++total; if (sol.search2(nums, 12) == 5) ++pass; else System.out.println("FAIL test7");
++total; if (sol.search2(new int[]{5}, 7) == -1) ++pass; else System.out.println("FAIL test8");
if (pass == total) {
System.out.println("== 0704 PASS ==");
} else {
System.out.println("== 0704 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(log n) | O(1) |
| 解法二 | O(log n) | O(1) |
705. Design HashSet(设计哈希集合)
题目描述
不使用任何内建的哈希表库设计一个哈希集合(HashSet)。
实现 MyHashSet 类:
void add(key)向哈希集合中插入值key。void remove(key)将给定值key从哈希集合中删除。如果哈希集合中没有这个值,什么也不做。boolean contains(key)返回哈希集合中是否存在这个值key。
示例
- 输入:
["MyHashSet", "add", "add", "contains", "contains", "add", "contains", "remove", "contains"]
[[], [1], [2], [1], [3], [2], [2], [2], [2]] - 输出:
[null,null,null,true,false,null,true,null,false] - 解释:
MyHashSet myHashSet = new MyHashSet();
myHashSet.add(1); // set = [1]
myHashSet.add(2); // set = [1, 2]
myHashSet.contains(1); // 返回 True
myHashSet.contains(3); // 返回 False ,(未找到)
myHashSet.add(2); // set = [1, 2]
myHashSet.contains(2); // 返回 True
myHashSet.remove(2); // set = [1]
myHashSet.contains(2); // 返回 False ,(已移除)
提示: 0 <= key <= 10^6- 最多调用
10^4次add、remove和contains
解题思路
解法一(桶 + 链地址法,主提交):key 范围 10^6,可开 769 个桶(素数),每个桶内用
LinkedList/数组存储,取模定位桶。平均 O(1) 增删查。时间复杂度 O(1)(均摊),
空间复杂度 O(n)。
解法二(位图 BitSet):key <= 10^6,直接用 boolean[1000001] 或 BitSet 标记,
实现最简。时间复杂度 O(1),空间复杂度 O(10^6)。
代码实现
import java.util.Arrays;
class MyHashSet {
// ===== 解法一:链地址法 =====
private static final int SIZE = 769;
private final java.util.LinkedList<Integer>[] buckets;
public MyHashSet() {
buckets = new java.util.LinkedList[SIZE];
for (int i = 0; i < SIZE; ++i) {
buckets[i] = new java.util.LinkedList<>();
}
}
private int hash(int key) {
return key % SIZE;
}
public void add(int key) {
int idx = hash(key);
java.util.LinkedList<Integer> b = buckets[idx];
if (!b.contains(key)) {
b.add(key);
}
}
public void remove(int key) {
int idx = hash(key);
java.util.LinkedList<Integer> b = buckets[idx];
b.remove((Integer) key);
}
public boolean contains(int key) {
return buckets[hash(key)].contains(key);
}
// ===== 解法二:位图(BitSet) =====
private static class MyHashSet2 {
private boolean[] bits;
MyHashSet2() {
bits = new boolean[1000001];
}
void add(int key) {
bits[key] = true;
}
void remove(int key) {
bits[key] = false;
}
boolean contains(int key) {
return bits[key];
}
}
public static void main(String[] args) {
int pass = 0, total = 0;
MyHashSet s = new MyHashSet();
s.add(1);
s.add(2);
++total; if (s.contains(1)) ++pass; else System.out.println("FAIL test1");
++total; if (!s.contains(3)) ++pass; else System.out.println("FAIL test2");
s.add(2);
++total; if (s.contains(2)) ++pass; else System.out.println("FAIL test3");
s.remove(2);
++total; if (!s.contains(2)) ++pass; else System.out.println("FAIL test4");
MyHashSet2 s2 = new MyHashSet2();
s2.add(1000000);
++total; if (s2.contains(1000000)) ++pass; else System.out.println("FAIL test5");
s2.remove(1000000);
++total; if (!s2.contains(1000000)) ++pass; else System.out.println("FAIL test6");
s2.add(0);
++total; if (s2.contains(0)) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0705 PASS ==");
} else {
System.out.println("== 0705 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(1)(均摊) | O(n) |
| 解法二 | O(1) | O(10^6) |
706. Design HashMap(设计哈希映射)
题目描述
不使用任何内建的哈希表库设计一个哈希映射(HashMap)。
实现 MyHashMap 类:
MyHashMap()用空映射初始化对象void put(int key, int value)向HashMap插入一个键值对(key, value)。如果key
已经存在于映射中,则更新其对应的值value。int get(int key)返回特定的key所映射的value;如果映射中不包含key的映射,
返回-1。void remove(key)如果映射中存在key的映射,则移除key和它所映射的value。
示例
- 输入:
["MyHashMap", "put", "put", "get", "get", "put", "get", "remove", "get"]
[[], [1, 1], [2, 2], [1], [3], [2, 1], [2], [2], [2]] - 输出:
[null,null,null, 1, -1,null, 1,null, -1] - 解释:
MyHashMap myHashMap = new MyHashMap();
myHashMap.put(1, 1); //myHashMap现在为 [[1,1]]
myHashMap.put(2, 2); //myHashMap现在为 [[1,1], [2,2]]
myHashMap.get(1); // 返回 1 ,myHashMap现在为 [[1,1], [2,2]]
myHashMap.get(3); // 返回 -1(未找到),myHashMap现在为 [[1,1], [2,2]]
myHashMap.put(2, 1); //myHashMap现在为 [[1,1], [2,1]](更新已有的值)
myHashMap.get(2); // 返回 1 ,myHashMap现在为 [[1,1], [2,1]]
myHashMap.remove(2); // 删除键为 2 的数据,myHashMap现在为 [[1,1]]
myHashMap.get(2); // 返回 -1 (未找到),myHashMap现在为 [[1,1]]
提示: 0 <= key, value <= 10^6- 最多调用
10^4次put、get和remove
解题思路
解法一(链地址法 + 桶内线性查找,主提交):开 769 个桶(素数),每桶存
int[]{key, value} 对。put 先查同 key 更新,否则尾插;get/remove 都在桶内线性找。
均摊 O(1)。时间复杂度 O(1)(均摊),空间复杂度 O(n)。
解法二(开放寻址法):key 范围 1e6 时可开 int[1e6+1][2] 直接数组映射,value 用
-1 表示不存在。最简直接。时间复杂度 O(1),空间复杂度 O(10^6)。
代码实现
import java.util.LinkedList;
class MyHashMap {
// ===== 解法一:链地址法 =====
private static final int SIZE = 769;
private final LinkedList<int[]>[] buckets;
public MyHashMap() {
buckets = new LinkedList[SIZE];
for (int i = 0; i < SIZE; ++i) {
buckets[i] = new LinkedList<>();
}
}
private int hash(int key) {
return key % SIZE;
}
public void put(int key, int value) {
LinkedList<int[]> b = buckets[hash(key)];
for (int[] e : b) {
if (e[0] == key) {
e[1] = value;
return;
}
}
b.add(new int[]{key, value});
}
public int get(int key) {
LinkedList<int[]> b = buckets[hash(key)];
for (int[] e : b) {
if (e[0] == key) {
return e[1];
}
}
return -1;
}
public void remove(int key) {
LinkedList<int[]> b = buckets[hash(key)];
b.removeIf(e -> e[0] == key);
}
// ===== 解法二:直接数组 =====
private static class MyHashMap2 {
private int[][] map;
MyHashMap2() {
map = new int[1000001][2];
for (int[] e : map) {
e[1] = -1;
}
}
void put(int key, int value) {
map[key][1] = value;
}
int get(int key) {
return map[key][1];
}
void remove(int key) {
map[key][1] = -1;
}
}
public static void main(String[] args) {
int pass = 0, total = 0;
MyHashMap m = new MyHashMap();
m.put(1, 1);
m.put(2, 2);
++total; if (m.get(1) == 1) ++pass; else System.out.println("FAIL test1");
++total; if (m.get(3) == -1) ++pass; else System.out.println("FAIL test2");
m.put(2, 1);
++total; if (m.get(2) == 1) ++pass; else System.out.println("FAIL test3");
m.remove(2);
++total; if (m.get(2) == -1) ++pass; else System.out.println("FAIL test4");
MyHashMap2 m2 = new MyHashMap2();
m2.put(1000000, 7);
++total; if (m2.get(1000000) == 7) ++pass; else System.out.println("FAIL test5");
m2.remove(1000000);
++total; if (m2.get(1000000) == -1) ++pass; else System.out.println("FAIL test6");
m2.put(0, 0);
++total; if (m2.get(0) == 0) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0706 PASS ==");
} else {
System.out.println("== 0706 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(1)(均摊) | O(n) |
| 解法二 | O(1) | O(10^6) |
707. Design Linked List(设计链表)
题目描述
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:
val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用
双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是
0-index 的。
在链表类中实现这些功能:
get(index):获取链表中第index个节点的值。如果索引无效,则返回-1。addAtHead(val):在链表的第一个元素之前添加一个值为val的节点。插入后,新节点将
成为链表的第一个节点。addAtTail(val):将值为val的节点追加到链表的最后一个元素。addAtIndex(index, val):在链表中的第index个节点之前添加值为val的节点。如果
index等于链表的长度,则该节点将附加到链表的末尾。如果index大于链表长度,则不会
插入节点。如果index小于0,则在头部插入节点。deleteAtIndex(index):如果索引index有效,则删除链表中的第index个节点。
示例
- 输入:
["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); // 链表变为 1
myLinkedList.addAtTail(3); // 链表变为 1 -> 3
myLinkedList.addAtIndex(1, 2); // 链表变为 1 -> 2 -> 3
myLinkedList.get(1); // 返回 2
myLinkedList.deleteAtIndex(1); // 现在链表是 1 -> 3
myLinkedList.get(1); // 返回 3
提示: - 所有
val值都在[1, 1000]之内 - 操作次数将在
[1, 1000]之内 - 请不要使用内置的
LinkedList库
解题思路
解法一(双链表 + 哨兵节点,主提交):使用哑头 head 与哑尾 tail,prev/next 双向
链接,维护 size。插入删除都通过找到第 index 个节点或其前驱完成。时间复杂度
O(index)(最坏 O(n)),空间复杂度 O(n)。
解法二(带头节点的单链表实现):只维护哑头与 size,操作均 O(index)。
代码实现
class MyLinkedList {
private static class Node {
int val;
Node prev, next;
Node(int val) {
this.val = val;
}
}
private final Node head; // 哑头
private final Node tail; // 哑尾
private int size;
public MyLinkedList() {
head = new Node(-1);
tail = new Node(-1);
head.next = tail;
tail.prev = head;
size = 0;
}
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
Node cur = head;
for (int i = 0; i <= index; ++i) {
cur = cur.next;
}
return cur.val;
}
public void addAtHead(int val) {
addAtIndex(0, val);
}
public void addAtTail(int val) {
addAtIndex(size, val);
}
public void addAtIndex(int index, int val) {
if (index < 0) {
index = 0;
}
if (index > size) {
return;
}
Node prev = head;
for (int i = 0; i < index; ++i) {
prev = prev.next;
}
Node next = prev.next;
Node node = new Node(val);
node.prev = prev;
node.next = next;
prev.next = node;
next.prev = node;
++size;
}
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
Node cur = head;
for (int i = 0; i <= index; ++i) {
cur = cur.next;
}
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
--size;
}
// ===== 本地测试 =====
public static void main(String[] args) {
int pass = 0, total = 0;
MyLinkedList ml = new MyLinkedList();
ml.addAtHead(1); // 1
ml.addAtTail(3); // 1 -> 3
ml.addAtIndex(1, 2); // 1 -> 2 -> 3
++total; if (ml.get(1) == 2) ++pass; else System.out.println("FAIL test1: " + ml.get(1));
ml.deleteAtIndex(1);
++total; if (ml.get(1) == 3) ++pass; else System.out.println("FAIL test2: " + ml.get(1));
MyLinkedList ml2 = new MyLinkedList();
ml2.addAtIndex(0, 10); // 10
ml2.addAtIndex(0, 20); // 20 -> 10
ml2.addAtIndex(1, 30); // 20 -> 30 -> 10
++total; if (ml2.get(0) == 20 && ml2.get(1) == 30 && ml2.get(2) == 10) ++pass; else System.out.println("FAIL test3");
++total; if (ml2.get(5) == -1) ++pass; else System.out.println("FAIL test4");
ml2.deleteAtIndex(0);
++total; if (ml2.get(0) == 30) ++pass; else System.out.println("FAIL test5: " + ml2.get(0));
if (pass == total) {
System.out.println("== 0707 PASS ==");
} else {
System.out.println("== 0707 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(index)(最坏 O(n)) | O(n) |
| 解法二 | O(index)(最坏 O(n)) | O(n) |
708. Insert into a Sorted Circular Linked List(循环有序列表的插入)
题目描述
给定循环单调非递减列表中的一个点,写一个函数向这个列表中插入一个新元素 insertVal,
使这个列表仍然是循环升序的。
给定的可以是这个列表中任意一个顶点的指针,不一定是最小元素的那个指针。
如果一个列表有多个满足条件的插入位置,你可以选择任意一个位置插入新值后,整个列表仍然
有序。如果列表为空(给定的节点是 null),你需要创建一个循环有序列表并返回这个节点。
否则,请返回原先给定的节点。
下面的图可以帮您理解这个问题中的循环上升的元素:
示例 1
- 输入:
head= [3,4,1],insertVal= 2 - 输出:[3,4,1,2]
- 解释:在上图中,有一个包含三个元素的循环有序列表,你获得值为 3 的节点的指针,我们需要
向表中插入元素 2。新插入的节点应该在 1 和 3 之间,插入之后,整个列表仍然是有序的。
示例 2
- 输入:
head= [],insertVal= 1 - 输出:[1]
- 解释:列表为空(给定的节点是
null),创建一个循环有序列表并返回这个节点。
示例 3
- 输入:
head= [1],insertVal= 0 - 输出:[1,0]
提示: 0 <= Number of Nodes <= 5 * 10^4-10^6 <= Node.val <= 10^6-10^6 <= insertVal <= 10^6
解题思路
解法一(双指针绕环找插入点,主提交):head == null 时新建自环节点。否则用
prev = head, curr = head.next 绕环行走:若找到
prev.val <= insertVal <= curr.val 的区间直接插入;或者遇到环中“回折点”
(prev.val > curr.val,即最大值到最小值处),说明 insertVal 大于所有值或小于所有值,
也在此处插入。若整个环都相同(未找到 break 条件),绕完一圈后停在任一位置插入。
时间复杂度 O(n),空间复杂度 O(1)。
解法二(先找最小节点再单次扫描):从最小节点开始沿升序找第一个
insertVal <= curr.val 的位置插入。需先绕环找最小。时间复杂度 O(n),空间复杂度 O(1)。
代码实现
class Solution {
// 与 LeetCode 相同的 Node 定义
public static class Node {
public int val;
public Node next;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, Node _next) {
val = _val;
next = _next;
}
}
// 解法一:双指针绕环
public Node insert(Node head, int insertVal) {
Node node = new Node(insertVal);
if (head == null) {
node.next = node;
return node;
}
Node prev = head, curr = head.next;
while (curr != head) {
if ((prev.val <= insertVal && insertVal <= curr.val)
|| (prev.val > curr.val && (insertVal >= prev.val || insertVal <= curr.val))) {
break;
}
prev = curr;
curr = curr.next;
}
prev.next = node;
node.next = curr;
return head;
}
// 解法二:先找最小再插入
public Node insert2(Node head, int insertVal) {
if (head == null) {
Node node = new Node(insertVal);
node.next = node;
return node;
}
// 找最大节点(其后即最小节点)
Node max = head;
while (max.next != head && max.next.val >= max.val) {
max = max.next;
}
Node start = max.next; // 最小节点
if (insertVal <= start.val || insertVal >= max.val) {
Node node = new Node(insertVal);
node.next = start;
max.next = node;
return head;
}
Node prev = start, curr = start.next;
while (curr.val < insertVal) {
prev = curr;
curr = curr.next;
}
Node node = new Node(insertVal);
prev.next = node;
node.next = curr;
return head;
}
// 本地测试:数组转循环链表,返回给定 head 等价引用
private static Node build(int[] arr) {
if (arr.length == 0) {
return null;
}
Node head = new Node(arr[0]);
Node cur = head;
for (int i = 1; i < arr.length; ++i) {
cur.next = new Node(arr[i]);
cur = cur.next;
}
cur.next = head;
return head;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
Node h1 = build(new int[]{3, 4, 1});
++total; if (java.util.Arrays.equals(toArr(sol.insert(h1, 2)), new int[]{3, 4, 1, 2})) ++pass; else System.out.println("FAIL test1: " + java.util.Arrays.toString(toArr(sol.insert(h1, 2))));
Node h2 = build(new int[]{});
Node r2 = sol.insert(null, 1);
++total; if (r2.val == 1 && r2.next == r2) ++pass; else System.out.println("FAIL test2");
Node h3 = build(new int[]{1});
++total; if (java.util.Arrays.equals(toArr(sol.insert(h3, 0)), new int[]{1, 0})) ++pass; else System.out.println("FAIL test3: " + java.util.Arrays.toString(toArr(sol.insert(h3, 0))));
Node h4 = build(new int[]{3, 4, 1});
++total; if (java.util.Arrays.equals(toArr(sol.insert2(h4, 2)), new int[]{3, 4, 1, 2})) ++pass; else System.out.println("FAIL test4: " + java.util.Arrays.toString(toArr(sol.insert2(h4, 2))));
Node h5 = build(new int[]{1, 3, 5});
++total; if (java.util.Arrays.equals(toArr(sol.insert2(h5, 4)), new int[]{1, 3, 4, 5})) ++pass; else System.out.println("FAIL test5: " + java.util.Arrays.toString(toArr(sol.insert2(h5, 4))));
Node h6 = build(new int[]{1, 3, 5});
++total; if (java.util.Arrays.equals(toArr(sol.insert2(h6, 0)), new int[]{1, 3, 5, 0})) ++pass; else System.out.println("FAIL test6: " + java.util.Arrays.toString(toArr(sol.insert2(h6, 0))));
if (pass == total) {
System.out.println("== 0708 PASS ==");
} else {
System.out.println("== 0708 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(1) |
709. To Lower Case(转换成小写字母)
题目描述
给你一个字符串 s,将该字符串中的大写字母转换成相同的小写字母,返回新的字符串。
示例 1
- 输入:
s= "Hello" - 输出:"hello"
示例 2
- 输入:
s= "here" - 输出:"here"
示例 3
- 输入:
s= "LOVELY" - 输出:"lovely"
提示: 1 <= s.length <= 100s由 ASCII 字符集中的可打印字符组成
解题思路
解法一(ASCII 位运算,主提交):大写字母 'A'~'Z' 的 ASCII 码区间 65~90,
加上 32 即对应小写。用 | 32 实现。时间复杂度 O(n),空间复杂度 O(1)。
解法二(String 内置方法):直接用 s.toLowerCase()。时间复杂度 O(n),空间复杂度 O(n)。
代码实现
class Solution {
// 解法一:位运算
public String toLowerCase(String s) {
char[] cs = new char[s.length()];
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (c >= 'A' && c <= 'Z') {
c |= 32;
}
cs[i] = c;
}
return new String(cs);
}
// 解法二:内置方法
public String toLowerCase2(String s) {
return s.toLowerCase();
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.toLowerCase("Hello").equals("hello")) ++pass; else System.out.println("FAIL test1");
++total; if (sol.toLowerCase("here").equals("here")) ++pass; else System.out.println("FAIL test2");
++total; if (sol.toLowerCase("LOVELY").equals("lovely")) ++pass; else System.out.println("FAIL test3");
++total; if (sol.toLowerCase("aBcD123!@#").equals("abcd123!@#")) ++pass; else System.out.println("FAIL test4");
++total; if (sol.toLowerCase2("Hello").equals("hello")) ++pass; else System.out.println("FAIL test5");
++total; if (sol.toLowerCase2("LOVELY").equals("lovely")) ++pass; else System.out.println("FAIL test6");
if (pass == total) {
System.out.println("== 0709 PASS ==");
} else {
System.out.println("== 0709 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n)(结果串) |
| 解法二 | O(n) | O(n) |
710. Random Pick with Blacklist(黑名单中的随机数)
题目描述
给定一个整数 n 和一个无重复黑名单整数数组 blacklist。设计一种算法,从
[0, n - 1] 范围内的任意整数中选取一个未加入黑名单 blacklist 的整数。任何在上述范围内
且不在黑名单 blacklist 中的整数都应该有同等的可能性被返回。
优化你的算法,使它最小化调用语言内置随机函数的次数。
实现 Solution 类:
Solution(int n, int[] blacklist)初始化整数n和被加入黑名单blacklist的整数int pick()返回一个范围为[0, n - 1]且不在黑名单blacklist中的随机整数
示例 1
输入
["Solution", "pick", "pick", "pick", "pick", "pick", "pick", "pick"]
[[7, [2, 3, 5]], [], [], [], [], [], [], []]
输出
[null, 0, 4, 1, 6, 1, 0, 4]
- 解释:Solution solution =
newSolution(7, [2, 3, 5]);
solution.pick(); // 返回0,任何[0,1,4,6]的整数都可以。注意,对于每一个pick的调用,
// 0、1、4和6的返回概率必须相等(即概率为1/4)。
提示: 1 <= n <= 10^90 <= blacklist.length <= min(10^5, n - 1)0 <= blacklist[i] < nblacklist中所有值都不同pick最多被调用2 * 10^4次
解题思路
解法一(白名单重映射,主提交):白名单长度为 m = n - blacklist.length。把 [0, m) 区间
看成一个可随机索引的空间:随机取 x in [0, m),若 x 不在黑名单直接返回;若在黑名单,
则映射到从 [m, n) 挑出的“白名单尾部”元素(因为 [m, n) 中黑名单元素肯定也存在,
白名单尾元素数量恰等于 [0, m) 中黑名单元素数量)。用 Map 记录黑名单 -> 映射值。
初始化一次性建好映射;pick 只调用一次随机函数。时间复杂度 O(1)(pick),
初始化 O(b),空间复杂度 O(b)。
解法二(到白名单下标等价转换):含义相同,把黑名单元素重新映射,实现略有差异。
代码实现
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
class Solution {
private final Map<Integer, Integer> d = new HashMap<>();
private final Random rand = new Random();
private final int m;
public Solution(int n, int[] blacklist) {
java.util.Set<Integer> black = new java.util.HashSet<>();
for (int b : blacklist) {
black.add(b);
}
m = n - blacklist.length;
// 尾部的白名单元素集
int tail = n - 1;
for (int b : blacklist) {
if (b < m) {
while (black.contains(tail)) {
--tail;
}
d.put(b, tail--);
}
}
}
public int pick() {
int x = rand.nextInt(m);
return d.getOrDefault(x, x);
}
public static void main(String[] args) {
int pass = 0, total = 0;
Solution sol = new Solution(7, new int[]{2, 3, 5});
java.util.Set<Integer> white = new java.util.HashSet<>();
for (int i = 0; i < 10000; ++i) {
int v = sol.pick();
white.add(v);
if (v < 0 || v >= 7 || v == 2 || v == 3 || v == 5) {
System.out.println("FAIL pick invalid: " + v);
pass = -100;
break;
}
}
if (pass >= 0) {
++total;
if (white.equals(new java.util.HashSet<>(java.util.Arrays.asList(0, 1, 4, 6)))) {
++pass;
} else {
System.out.println("FAIL coverage: " + white);
}
}
Solution sol2 = new Solution(3, new int[]{1});
++total;
boolean ok2 = true;
for (int i = 0; i < 1000; ++i) {
int v = sol2.pick();
if (v != 0 && v != 2) {
ok2 = false;
break;
}
}
if (ok2) ++pass; else System.out.println("FAIL test2");
if (pass == total) {
System.out.println("== 0710 PASS ==");
} else {
System.out.println("== 0710 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(1)(pick) | O(b) |
| 解法二 | O(1)(pick) | O(b) |
711. Number of Distinct Islands II(不同岛屿的数量 II)
题目描述
给定一个 m x n 二进制数组表示的网格 grid,一个岛屿由四连通(上、下、左、右四个方向)
的 1 组成(代表陆地)。你可以认为网格的四周被海水包围。
如果两个岛屿的形状相同,或者通过旋转(顺时针旋转 90°,180°,270°)、翻转(左右翻转、
上下翻转)后形状相同,那么就认为这两个岛屿是相同的。
返回这个网格中形状不同的岛屿的数量。
示例 1
- 输入:
grid= [[1,1,0,0,0],[1,0,0,0,0],[0,0,0,0,1],[0,0,0,1,1]] - 输出: 1
- 解释: 这两个是相同的岛屿。因为我们通过 180° 旋转第一个岛屿,两个岛屿的形状相同。
示例 2
- 输入:
grid= [[1,1,0,0,0],[1,1,0,0,0],[0,0,0,1,1],[0,0,0,1,1]] - 输出: 1
提示: m == grid.lengthn == grid[i].length1 <= m, n <= 50grid[i][j]不是0就是1
解题思路
解法一(DFS 提取形状 + 8 种变换归一化,主提交):对每块岛屿 DFS 收集其所有格子坐标
(相对初始点归一化)。对每个形状,枚举 8 种等距变换:
(x,y)、(-x,y)、(x,-y)、(-x,-y)、(y,x)、(-y,x)、(y,-x)、(-y,-x),
每种变换后平移使最小 x、最小 y 为 0,再按字典序排序,取字典序最小的作为“规范化形状”,
用 Set<List<Integer>> 去重。时间复杂度 O(8·格数·排序),空间复杂度 O(格数)。
解法二(方向串 + 旋转矩阵):用方向字符串标记形状同时做旋转归一,逻辑与解法一本质相同。
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class Solution {
private int[][] grid;
private int m, n;
public int numDistinctIslands2(int[][] grid) {
this.grid = grid;
m = grid.length;
n = grid[0].length;
Set<List<Integer>> shapes = new HashSet<>();
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 1) {
List<int[]> cells = new ArrayList<>();
dfs(i, j, cells);
shapes.add(normalize(cells));
}
}
}
return shapes.size();
}
private void dfs(int i, int j, List<int[]> cells) {
grid[i][j] = 0;
cells.add(new int[]{i, j});
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] d : dirs) {
int ni = i + d[0], nj = j + d[1];
if (ni >= 0 && ni < m && nj >= 0 && nj < n && grid[ni][nj] == 1) {
dfs(ni, nj, cells);
}
}
}
// 8 种等距变换后取字典序最小的规范化形状
private List<Integer> normalize(List<int[]> cells) {
List<List<int[]>> transforms = new ArrayList<>();
for (int t = 0; t < 8; ++t) {
List<int[]> cur = new ArrayList<>();
for (int[] c : cells) {
int x = c[0], y = c[1];
switch (t) {
case 0: cur.add(new int[]{x, y}); break;
case 1: cur.add(new int[]{-x, y}); break;
case 2: cur.add(new int[]{x, -y}); break;
case 3: cur.add(new int[]{-x, -y}); break;
case 4: cur.add(new int[]{y, x}); break;
case 5: cur.add(new int[]{-y, x}); break;
case 6: cur.add(new int[]{y, -x}); break;
default: cur.add(new int[]{-y, -x}); break;
}
}
// 平移
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE;
for (int[] c : cur) {
minX = Math.min(minX, c[0]);
minY = Math.min(minY, c[1]);
}
for (int[] c : cur) {
c[0] -= minX;
c[1] -= minY;
}
transforms.add(cur);
}
List<List<Integer>> list = new ArrayList<>();
for (List<int[]> t : transforms) {
t.sort((a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
List<Integer> flat = new ArrayList<>();
for (int[] c : t) {
flat.add(c[0]);
flat.add(c[1]);
}
list.add(flat);
}
list.sort((a, b) -> {
for (int i = 0; i < a.size(); ++i) {
if (!a.get(i).equals(b.get(i))) {
return a.get(i) - b.get(i);
}
}
return 0;
});
return list.get(0);
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.numDistinctIslands2(new int[][]{{1, 1, 0, 0, 0}, {1, 0, 0, 0, 0}, {0, 0, 0, 0, 1}, {0, 0, 0, 1, 1}}) == 1) ++pass; else System.out.println("FAIL test1");
++total; if (sol.numDistinctIslands2(new int[][]{{1, 1, 0, 0, 0}, {1, 1, 0, 0, 0}, {0, 0, 0, 1, 1}, {0, 0, 0, 1, 1}}) == 1) ++pass; else System.out.println("FAIL test2");
++total; if (sol.numDistinctIslands2(new int[][]{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}) == 1) ++pass; else System.out.println("FAIL test3");
++total; if (sol.numDistinctIslands2(new int[][]{{1, 1, 0}, {0, 1, 0}, {0, 0, 1}}) == 2) ++pass; else System.out.println("FAIL test4");
++total; if (sol.numDistinctIslands2(new int[][]{{1}}) == 1) ++pass; else System.out.println("FAIL test5");
if (pass == total) {
System.out.println("== 0711 PASS ==");
} else {
System.out.println("== 0711 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(8·K·logK)(K 为最大岛屿格数) | O(K) |
| 解法二 | O(8·K·logK) | O(K) |
712. Minimum ASCII Delete Sum for Two Strings(两个字符串的最小ASCII删除和)
题目描述
给定两个字符串 s1 和 s2,返回使两个字符串相等所需删除字符的 ASCII 值的最小和。
示例 1
- 输入:
s1= "sea",s2= "eat" - 输出: 231
- 解释: 在 "sea" 中删除 "
s" 并将 "s" 的值(115)加入总和。在 "eat" 中删除 "t" 并将 116 加入
总和。结束时,两个字符串相等,我们可以得到 115 + 116 = 231 达到最小值。
示例 2
- 输入:
s1= "delete",s2= "leet" - 输出: 403
- 解释: 在 "delete" 中删除 "dee" 字符串变成 "let",将 100[
d]+101[e]+101[e] 加入总和。
在 "leet" 中删除 "e" 将 101[e] 加入总和。结束时,两个字符串都等于 "let",结果即为
100+101+101+101 = 403。如果改为将两个字符串转换为 "lee" 或 "eet",我们会得到 433 或 417,
比答案更大。
提示: 0 <= s1.length, s2.length <= 1000s1和s2由小写英文字母组成
解题思路
解法一(二维 DP,主提交):dp[i][j] 表示 s1[0..i) 与 s2[0..j) 相等所需最小 ASCII
删除和。若 s1[i-1] == s2[j-1] 则 dp[i][j] = dp[i-1][j-1];否则删去其中一个字符取较小
——min(dp[i-1][j] + s1[i-1], dp[i][j-1] + s2[j-1])。边界:dp[i][0] 累加删空整个 s1,
dp[0][j] 同理。时间复杂度 O(m·n),空间复杂度 O(m·n)(可滚动至 O(n))。
解法二(滚动数组优化):只保留两行 dp,空间 O(n)。
代码实现
class Solution {
// 解法一:二维 DP
public int minimumDeleteSum(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; ++i) {
dp[i][0] = dp[i - 1][0] + s1.charAt(i - 1);
}
for (int j = 1; j <= n; ++j) {
dp[0][j] = dp[0][j - 1] + s2.charAt(j - 1);
}
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j] + s1.charAt(i - 1),
dp[i][j - 1] + s2.charAt(j - 1));
}
}
}
return dp[m][n];
}
// 解法二:滚动数组
public int minimumDeleteSum2(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[] dp = new int[n + 1];
for (int j = 1; j <= n; ++j) {
dp[j] = dp[j - 1] + s2.charAt(j - 1);
}
for (int i = 1; i <= m; ++i) {
int pre = dp[0];
dp[0] += s1.charAt(i - 1);
for (int j = 1; j <= n; ++j) {
int tmp = dp[j];
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
dp[j] = pre;
} else {
dp[j] = Math.min(dp[j] + s1.charAt(i - 1), dp[j - 1] + s2.charAt(j - 1));
}
pre = tmp;
}
}
return dp[n];
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.minimumDeleteSum("sea", "eat") == 231) ++pass; else System.out.println("FAIL test1: " + sol.minimumDeleteSum("sea", "eat"));
++total; if (sol.minimumDeleteSum("delete", "leet") == 403) ++pass; else System.out.println("FAIL test2: " + sol.minimumDeleteSum("delete", "leet"));
++total; if (sol.minimumDeleteSum("a", "b") == 195) ++pass; else System.out.println("FAIL test3: " + sol.minimumDeleteSum("a", "b"));
++total; if (sol.minimumDeleteSum("", "") == 0) ++pass; else System.out.println("FAIL test4");
++total; if (sol.minimumDeleteSum2("sea", "eat") == 231) ++pass; else System.out.println("FAIL test5");
++total; if (sol.minimumDeleteSum2("delete", "leet") == 403) ++pass; else System.out.println("FAIL test6");
++total; if (sol.minimumDeleteSum2("a", "") == 97) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0712 PASS ==");
} else {
System.out.println("== 0712 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m·n) | O(m·n) |
| 解法二 | O(m·n) | O(n) |
713. Subarray Product Less Than K(乘积小于 K 的子数组)
题目描述
给你一个整数数组 nums 和一个整数 k,请你返回子数组内所有元素的乘积严格小于 k 的
连续子数组的数目。
示例 1
- 输入:
nums= [10,5,2,6],k= 100 - 输出:8
- 解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2]、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。 需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。
示例 2
- 输入:
nums= [1,2,3],k= 0 - 输出:0 提示:
1 <= nums.length <= 3 * 10^41 <= nums[i] <= 10000 <= k <= 10^6
解题思路
解法一(滑动窗口,主提交):右指针 r 不断右移并累乘 prod;当 prod >= k 时左指针 l
右移、除以 nums[l],直到 prod < k 或 l > r。此时以 r 结尾且乘积小于 k 的子数组
个数为 r - l + 1(含单元素),累加即可。乘积用 long 防溢出。
时间复杂度 O(n),空间复杂度 O(1)。
解法二(二分 + 对数技巧思路):log 乘积前缀 + 二分可用,但复杂度 O(n log n),滑动窗口
更优。仅作对比参考。
代码实现
class Solution {
// 解法一:滑动窗口
public int numSubarrayProductLessThanK(int[] nums, int k) {
if (k <= 1) {
return 0;
}
int l = 0;
long prod = 1;
int ans = 0;
for (int r = 0; r < nums.length; ++r) {
prod *= nums[r];
while (prod >= k) {
prod /= nums[l++];
}
ans += r - l + 1;
}
return ans;
}
// 解法二:对数 + 前缀二分(参考)
public int numSubarrayProductLessThanK2(int[] nums, int k) {
if (k <= 1) {
return 0;
}
int n = nums.length;
double[] logPre = new double[n + 1];
for (int i = 0; i < n; ++i) {
logPre[i + 1] = logPre[i] + Math.log(nums[i]);
}
double limit = Math.log(k);
int ans = 0;
for (int l = 0; l < n; ++l) {
// 找最大 r 使 logPre[r+1] - logPre[l] < limit
int lo = l, hi = n;
while (lo < hi) {
int mid = (lo + hi + 1) >>> 1;
if (logPre[mid] - logPre[l] < limit) {
lo = mid;
} else {
hi = mid - 1;
}
}
ans += lo - l;
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.numSubarrayProductLessThanK(new int[]{10, 5, 2, 6}, 100) == 8) ++pass; else System.out.println("FAIL test1: " + sol.numSubarrayProductLessThanK(new int[]{10, 5, 2, 6}, 100));
++total; if (sol.numSubarrayProductLessThanK(new int[]{1, 2, 3}, 0) == 0) ++pass; else System.out.println("FAIL test2");
++total; if (sol.numSubarrayProductLessThanK(new int[]{1, 1, 1}, 2) == 6) ++pass; else System.out.println("FAIL test3");
++total; if (sol.numSubarrayProductLessThanK(new int[]{10, 9, 10, 4, 3, 8, 3, 3, 6, 2, 10, 10, 9, 3}, 19) == 18) ++pass; else System.out.println("FAIL test4: " + sol.numSubarrayProductLessThanK(new int[]{10, 9, 10, 4, 3, 8, 3, 3, 6, 2, 10, 10, 9, 3}, 19));
++total; if (sol.numSubarrayProductLessThanK2(new int[]{10, 5, 2, 6}, 100) == 8) ++pass; else System.out.println("FAIL test5");
++total; if (sol.numSubarrayProductLessThanK2(new int[]{1, 2, 3}, 0) == 0) ++pass; else System.out.println("FAIL test6");
++total; if (sol.numSubarrayProductLessThanK2(new int[]{1, 1, 1}, 2) == 6) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0713 PASS ==");
} else {
System.out.println("== 0713 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n log n) | O(n) |
714. Best Time to Buy and Sell Stock with Transaction Fee(买卖股票的最佳时机含手续费)
题目描述
给定一个整数数组 prices,其中 prices[i] 表示第 i 天的股票价格;整数 fee 代表了交易
股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出
它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例 1
- 输入:
prices= [1, 3, 2, 8, 4, 9], fee = 2 - 输出:8
- 解释:能够达到的最大利润:
在此处买入
prices[0]= 1,在此处卖出prices[3]= 8,在此处买入prices[4]= 4,在此处卖出prices[5]= 9,总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8。
示例 2
- 输入:
prices= [1,3,7,5,10,3], fee = 3 - 输出:6 提示:
1 <= prices.length <= 5 * 10^41 <= prices[i] < 5 * 10^40 <= fee < 5 * 10^4
解题思路
解法一(状态机 DP,主提交):用 hold 表示持有股票时的最大现金(可为负),
cash 表示不持有时的最大现金。每天:cash = max(cash, hold + price - fee)(卖出),
hold = max(hold, cash - price)(买入)。遍历一次即得答案 cash。
时间复杂度 O(n),空间复杂度 O(1)。
解法二(贪心):把手续费视为买入成本的一部分,minPrice 跟踪“有效买入价”;当卖出有利时
累加利润并抬高买入价(仍可继续持有合并交易)。时间复杂度 O(n),空间复杂度 O(1)。
代码实现
class Solution {
// 解法一:状态机 DP
public int maxProfit(int[] prices, int fee) {
int cash = 0;
int hold = -prices[0];
for (int i = 1; i < prices.length; ++i) {
cash = Math.max(cash, hold + prices[i] - fee);
hold = Math.max(hold, cash - prices[i]);
}
return cash;
}
// 解法二:贪心(有效买入价)
public int maxProfit2(int[] prices, int fee) {
int n = prices.length;
int buy = prices[0] + fee;
int profit = 0;
for (int i = 1; i < n; ++i) {
if (prices[i] + fee < buy) {
buy = prices[i] + fee; // 更便宜的买入机会
} else if (prices[i] > buy) {
profit += prices[i] - buy;
buy = prices[i]; // 抬高门槛,避免对同一段重复计费
}
}
return profit;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.maxProfit(new int[]{1, 3, 2, 8, 4, 9}, 2) == 8) ++pass; else System.out.println("FAIL test1: " + sol.maxProfit(new int[]{1, 3, 2, 8, 4, 9}, 2));
++total; if (sol.maxProfit(new int[]{1, 3, 7, 5, 10, 3}, 3) == 6) ++pass; else System.out.println("FAIL test2: " + sol.maxProfit(new int[]{1, 3, 7, 5, 10, 3}, 3));
++total; if (sol.maxProfit(new int[]{1, 2, 3}, 1) == 1) ++pass; else System.out.println("FAIL test3");
++total; if (sol.maxProfit(new int[]{5}, 1) == 0) ++pass; else System.out.println("FAIL test4");
++total; if (sol.maxProfit2(new int[]{1, 3, 2, 8, 4, 9}, 2) == 8) ++pass; else System.out.println("FAIL test5");
++total; if (sol.maxProfit2(new int[]{1, 3, 7, 5, 10, 3}, 3) == 6) ++pass; else System.out.println("FAIL test6");
++total; if (sol.maxProfit2(new int[]{5}, 1) == 0) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0714 PASS ==");
} else {
System.out.println("== 0714 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(1) |
715. Range Module(Range 模块)
题目描述
Range 模块是跟踪数字范围的模块。设计一个数据结构来跟踪表示为半开区间的范围并查询它们。
半开区间 [left, right) 表示所有 left <= x < right 的实数 x。
实现 RangeModule 类:
RangeModule()初始化数据结构的对象。void addRange(int left, int right)添加半开区间[left, right),跟踪该区间中的每个实数。 添加与当前跟踪的数字部分重叠的区间时,应当添加在区间[left, right)中尚未跟踪的任何 数字到该区间中。boolean queryRange(int left, int right)只有在当前正在跟踪区间[left, right)中的每一个 实数时,才返回true,否则返回false。void removeRange(int left, int right)停止跟踪半开区间[left, right)中当前正在跟踪的 每个实数。
示例
输入
["RangeModule", "addRange", "removeRange", "queryRange", "queryRange", "queryRange"]
[[], [10, 20], [14, 16], [10, 14], [13, 15], [16, 17]]
输出
[null, null, null, true, false, true]
- 解释:
RangeModule rangeModule = new RangeModule();rangeModule.addRange(10, 20);rangeModule.removeRange(14, 16);rangeModule.queryRange(10, 14); 返回true(区间 [10, 14) 中的每个数都正在被跟踪)rangeModule.queryRange(13, 15); 返回false(未跟踪区间 [13, 15) 中像 14, 14.03, 14.17 这样的数字)rangeModule.queryRange(16, 17); 返回true(尽管执行了删除操作,区间 [16, 17) 中的数字 16 仍然会被跟踪) 提示: 1 <= left < right <= 10^9- 在单个测试用例中,对
addRange、queryRange和removeRange的调用总数不超过10^4次
解题思路
解法一(有序集合维护不相交区间,主提交):用 TreeMap<Integer, Integer> 保存
起点 -> 终点,且任意两个区间互不重叠、不相邻(合并后)。每次操作先取
floorKey(left) 与 floorKey(right) 附近区间,区间合并/切分时利用 subMap 批量删除。
addRange:收集所有与[left, right)相交的区间合并;queryRange:找floorKey(left)的区间是否覆盖[left, right);removeRange:删除被覆盖部分,并把新边界保留为剩余区间。 平均每次操作 O(log n + 区间数)。时间复杂度 O(log n + |S|),空间复杂度 O(|S|)。 解法二(动态开点线段树):维护区间是否被跟踪的布尔线段树。坐标 1e9 需动态开点。
代码实现
import java.util.Map;
import java.util.TreeMap;
class RangeModule {
private final TreeMap<Integer, Integer> map = new TreeMap<>();
public RangeModule() {
}
public void addRange(int left, int right) {
// 找到可能与 [left, right) 相交的区间范围
Integer lk = map.floorKey(left);
if (lk != null && map.get(lk) >= left) {
left = lk;
}
Integer rk = map.floorKey(right);
if (rk != null && map.get(rk) > right) {
right = map.get(rk);
}
// 删除 [left, right) 覆盖的所有区间
map.subMap(left, true, right, true).clear();
map.put(left, right);
}
public boolean queryRange(int left, int right) {
Integer lk = map.floorKey(left);
return lk != null && map.get(lk) >= right;
}
public void removeRange(int left, int right) {
Integer lk = map.floorKey(left);
if (lk != null && map.get(lk) > left) {
int r = map.get(lk);
if (r > right) {
// [lk, r) 被 [left, right) 完全包含
map.put(right, r);
}
map.put(lk, left);
}
map.subMap(left, true, right, false).clear();
}
// ===== 本地测试 =====
public static void main(String[] args) {
int pass = 0, total = 0;
RangeModule rm = new RangeModule();
rm.addRange(10, 20);
rm.removeRange(14, 16);
++total; if (rm.queryRange(10, 14)) ++pass; else System.out.println("FAIL test1");
++total; if (!rm.queryRange(13, 15)) ++pass; else System.out.println("FAIL test2");
++total; if (rm.queryRange(16, 17)) ++pass; else System.out.println("FAIL test3");
RangeModule rm2 = new RangeModule();
rm2.addRange(5, 8);
rm2.addRange(10, 12);
++total; if (!rm2.queryRange(5, 12)) ++pass; else System.out.println("FAIL test4");
rm2.addRange(8, 10);
++total; if (rm2.queryRange(5, 12)) ++pass; else System.out.println("FAIL test5");
rm2.removeRange(6, 11);
++total; if (!rm2.queryRange(5, 12)) ++pass; else System.out.println("FAIL test6");
++total; if (rm2.queryRange(5, 6) && rm2.queryRange(11, 12)) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0715 PASS ==");
} else {
System.out.println("== 0715 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | ||
|---|---|---|---|---|
| 解法一 | O(log n + 相交区间数) | O( | S | ) |
| 解法二 | O(log 坐标范围) | O(n)(动态开点) |
716. Max Stack(最大栈)
题目描述
设计一个最大栈数据结构,既支持栈操作,又支持查找栈中最大元素。
实现 MaxStack 类:
MaxStack()初始化栈对象void push(int x)将元素x压入栈中。int pop()移除栈顶元素并返回这个元素。int top()返回栈顶元素,无需移除。int peekMax()检索并返回栈中最大元素,无需移除。int popMax()检索并返回栈中最大元素,并将其移除。如果有多个最大元素,只要移除 最靠近栈顶的那个。
示例
输入
["MaxStack", "push", "push", "push", "top", "popMax", "top", "peekMax", "pop", "top"]
[[], [5], [1], [5], [], [], [], [], [], []]
输出
[null, null, null, null, 5, 5, 1, 5, 1, 5]
- 解释:
MaxStack stk = new MaxStack();stk.push(5); // [5] - 5 既是栈顶元素,也是最大元素 stk.push(1); // [5, 1] - 栈顶元素是 1,最大元素是 5 stk.push(5); // [5, 1, 5] - 5 既是栈顶元素,也是最大元素 stk.top(); // 返回 5,栈没有改变 stk.popMax(); // 返回 5,栈发生改变,栈顶元素不再是最大元素 stk.top(); // 返回 1 stk.peekMax(); // 返回 5 stk.pop(); // 返回 1 stk.top(); // 返回 5 提示: -10^7 <= x <= 10^7- 最多调用
10^4次push、pop、top、peekMax和popMax - 调用
pop、top、peekMax或popMax时,栈中至少存在一个元素
解题思路
解法一(双栈,主提交):stack 存元素、maxStack 同步存“当前位置的最大值”。
push 时 maxStack 压入 max(x, maxStack.peek());pop 时两栈同时出栈;
peekMax 即 maxStack.peek();popMax 需要暂存弹出元素以恢复栈——用一个临时栈反复
pop 直到栈顶等于当前最大,弹出最大后把暂存元素压回。均摊 O(n)(popMax),
其他 O(1)。空间复杂度 O(n)。
解法二(双向链表 + TreeMap 值->节点集合):链表维护栈顺序、TreeMap 维护最大值到节点
集合,所有操作理论 O(log n)。实现较繁。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.TreeMap;
class MaxStack {
private final Deque<Integer> stack = new ArrayDeque<>();
private final Deque<Integer> maxStack = new ArrayDeque<>();
public MaxStack() {
}
public void push(int x) {
stack.push(x);
maxStack.push(maxStack.isEmpty() ? x : Math.max(x, maxStack.peek()));
}
public int pop() {
maxStack.pop();
return stack.pop();
}
public int top() {
return stack.peek();
}
public int peekMax() {
return maxStack.peek();
}
public int popMax() {
int mx = peekMax();
Deque<Integer> tmp = new ArrayDeque<>();
while (top() != mx) {
tmp.push(pop());
}
pop(); // 弹出最大值
while (!tmp.isEmpty()) {
push(tmp.pop());
}
return mx;
}
// ===== 本地测试 =====
public static void main(String[] args) {
int pass = 0, total = 0;
MaxStack stk = new MaxStack();
stk.push(5);
stk.push(1);
stk.push(5);
++total; if (stk.top() == 5) ++pass; else System.out.println("FAIL test1");
++total; if (stk.popMax() == 5) ++pass; else System.out.println("FAIL test2: " + stk.popMax());
++total; if (stk.top() == 1) ++pass; else System.out.println("FAIL test3: " + stk.top());
++total; if (stk.peekMax() == 5) ++pass; else System.out.println("FAIL test4: " + stk.peekMax());
++total; if (stk.pop() == 1) ++pass; else System.out.println("FAIL test5: " + stk.pop());
++total; if (stk.top() == 5) ++pass; else System.out.println("FAIL test6: " + stk.top());
MaxStack stk2 = new MaxStack();
stk2.push(2);
stk2.push(3);
stk2.push(1);
++total; if (stk2.popMax() == 3) ++pass; else System.out.println("FAIL test7: " + stk2.popMax());
++total; if (stk2.top() == 1) ++pass; else System.out.println("FAIL test8: " + stk2.top());
if (pass == total) {
System.out.println("== 0716 PASS ==");
} else {
System.out.println("== 0716 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(1);popMax 均摊 O(n) | O(n) |
| 解法二 | O(log n) | O(n) |
717. 1-bit and 2-bit Characters(1 比特与 2 比特字符)
题目描述
有两种特殊字符:
- 第一种字符可以用一比特
0表示 - 第二种字符可以用两比特(
10或11)表示 给你一个以0结尾的二进制数组bits,如果最后一个字符必须是一个一比特字符,则返回true。
示例 1
- 输入:bits = [1, 0, 0]
- 输出:
true - 解释: 唯一的编码方式是一个两比特字符和一个一比特字符。 所以最后一个字符是一比特字符。
示例 2
- 输入:bits = [1, 1, 1, 0]
- 输出:
false - 解释: 唯一的编码方式是两比特字符和两比特字符。 所以最后一个字符不是一比特字符。 提示:
1 <= bits.length <= 1000bits[i]为0或1bits以0结尾
解题思路
解法一(线性扫描,主提交):从头开始,若遇到 1 则它必是两比特字符的开头,跳过两位;
若遇到 0 则是一比特字符,跳一位。结束时若正好停在最后一个元素(即 i == n-1),
说明最后一个 0 是独立的一比特字符。时间复杂度 O(n),空间复杂度 O(1)。
解法二(从末尾倒数连续的 1 的个数):最后一个字符是一比特字符,当且仅当从倒数第二位
往前连续 1 的个数为偶数(因为每个两比特字符必须以 1 开头且占两位)。时间复杂度 O(n),
空间复杂度 O(1)。
代码实现
class Solution {
// 解法一:线性扫描
public boolean isOneBitCharacter(int[] bits) {
int n = bits.length;
int i = 0;
while (i < n - 1) {
if (bits[i] == 1) {
i += 2;
} else {
i += 1;
}
}
return i == n - 1;
}
// 解法二:末尾连续 1 的个数
public boolean isOneBitCharacter2(int[] bits) {
int n = bits.length;
int cnt = 0;
int i = n - 2;
while (i >= 0 && bits[i] == 1) {
++cnt;
--i;
}
return cnt % 2 == 0;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.isOneBitCharacter(new int[]{1, 0, 0})) ++pass; else System.out.println("FAIL test1");
++total; if (!sol.isOneBitCharacter(new int[]{1, 1, 1, 0})) ++pass; else System.out.println("FAIL test2");
++total; if (sol.isOneBitCharacter(new int[]{0, 1, 1, 0})) ++pass; else System.out.println("FAIL test3: " + sol.isOneBitCharacter(new int[]{0, 1, 1, 0}));
++total; if (sol.isOneBitCharacter(new int[]{0})) ++pass; else System.out.println("FAIL test4");
++total; if (sol.isOneBitCharacter2(new int[]{1, 0, 0})) ++pass; else System.out.println("FAIL test5");
++total; if (!sol.isOneBitCharacter2(new int[]{1, 1, 1, 0})) ++pass; else System.out.println("FAIL test6");
++total; if (sol.isOneBitCharacter2(new int[]{0})) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0717 PASS ==");
} else {
System.out.println("== 0717 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(1) |
718. Maximum Length of Repeated Subarray(最长重复子数组)
题目描述
给两个整数数组 nums1 和 nums2,返回两个数组中公共的、长度最长的子数组的长度。
示例 1
- 输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
- 输出:3
- 解释:长度最长的公共子数组是 [3,2,1] 。 提示:
1 <= nums1.length, nums2.length <= 10000 <= nums1[i], nums2[i] <= 100
解题思路
解法一(二维 DP,主提交):dp[i][j] 表示以 nums1[i]、nums2[j] 结尾的最长公共后缀长度。
若 nums1[i] == nums2[j] 则 dp[i][j] = dp[i-1][j-1] + 1,否则为 0;全程记录最大值。
时间复杂度 O(m·n),空间复杂度 O(m·n)(可滚动到 O(n))。
解法二(滚动数组):只保留上一行 dp,从后往前更新避免覆盖。时间复杂度 O(m·n),
空间复杂度 O(n)。
代码实现
class Solution {
// 解法一:二维 DP
public int findLength(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int[][] dp = new int[m + 1][n + 1];
int ans = 0;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
ans = Math.max(ans, dp[i][j]);
}
}
}
return ans;
}
// 解法二:滚动数组
public int findLength2(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int[] dp = new int[n + 1];
int ans = 0;
for (int i = 1; i <= m; ++i) {
for (int j = n; j >= 1; --j) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[j] = dp[j - 1] + 1;
ans = Math.max(ans, dp[j]);
} else {
dp[j] = 0;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.findLength(new int[]{1, 2, 3, 2, 1}, new int[]{3, 2, 1, 4, 7}) == 3) ++pass; else System.out.println("FAIL test1: " + sol.findLength(new int[]{1, 2, 3, 2, 1}, new int[]{3, 2, 1, 4, 7}));
++total; if (sol.findLength(new int[]{0, 0, 0, 0, 0}, new int[]{0, 0, 0, 0, 0}) == 5) ++pass; else System.out.println("FAIL test2");
++total; if (sol.findLength(new int[]{1, 2, 3}, new int[]{4, 5, 6}) == 0) ++pass; else System.out.println("FAIL test3");
++total; if (sol.findLength2(new int[]{1, 2, 3, 2, 1}, new int[]{3, 2, 1, 4, 7}) == 3) ++pass; else System.out.println("FAIL test4");
++total; if (sol.findLength2(new int[]{0, 0, 0, 0, 0}, new int[]{0, 0, 0, 0, 0}) == 5) ++pass; else System.out.println("FAIL test5");
++total; if (sol.findLength2(new int[]{1, 2, 3}, new int[]{4, 5, 6}) == 0) ++pass; else System.out.println("FAIL test6");
if (pass == total) {
System.out.println("== 0718 PASS ==");
} else {
System.out.println("== 0718 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m·n) | O(m·n) |
| 解法二 | O(m·n) | O(n) |
719. Find K-th Smallest Pair Distance(找出第 K 小的数对距离)
题目描述
数对 (a,b) 由整数 a 和 b 组成,其数对距离定义为 a 和 b 的绝对差值。
给你一个整数数组 nums 和一个整数 k,数对由 nums[i] 和 nums[j] 组成且满足
0 <= i < j < nums.length。返回所有数对距离中第 k 小的数对距离。
示例 1
- 输入:
nums= [1,3,1],k= 1 - 输出:0
- 解释:数对和对应的距离如下:(1,3) -> 2,(1,1) -> 0,(3,1) -> 2。距离第 1 小的数对是 (1,1) ,距离为 0 。
示例 2
- 输入:
nums= [1,1,1],k= 2 - 输出:0
示例 3
- 输入:
nums= [1,6,1],k= 3 - 输出:5 提示:
n == nums.length2 <= n <= 10^40 <= nums[i] <= 10^61 <= k <= n * (n - 1) / 2
解题思路
解法一(排序 + 二分答案 + 双指针计数,主提交):排序后所有数对距离在
[0, nums[n-1] - nums[0]] 之间。二分猜一个距离 mid,统计距离 <= mid 的数对个数
(双指针:对每个右端 i,左端 j 单调右移,ans += i - j);若个数 >= k 说明答案
在左半区,否则在右半区。时间复杂度 O(n log n + n log(maxDist)),空间复杂度 O(1)。
解法二(桶计数排序思路):距离最大值不超过 1e6,可开计数数组+前缀和统计,但受限于
nums 值域。附参考。
代码实现
import java.util.Arrays;
class Solution {
// 解法一:二分答案 + 双指针计数
public int smallestDistancePair(int[] nums, int k) {
Arrays.sort(nums);
int n = nums.length;
int lo = 0, hi = nums[n - 1] - nums[0];
while (lo < hi) {
int mid = (lo + hi) >>> 1;
int cnt = 0;
for (int i = 0, j = 0; i < n; ++i) {
while (j < i && nums[i] - nums[j] > mid) {
++j;
}
cnt += i - j;
}
if (cnt >= k) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.smallestDistancePair(new int[]{1, 3, 1}, 1) == 0) ++pass; else System.out.println("FAIL test1: " + sol.smallestDistancePair(new int[]{1, 3, 1}, 1));
++total; if (sol.smallestDistancePair(new int[]{1, 1, 1}, 2) == 0) ++pass; else System.out.println("FAIL test2");
++total; if (sol.smallestDistancePair(new int[]{1, 6, 1}, 3) == 5) ++pass; else System.out.println("FAIL test3: " + sol.smallestDistancePair(new int[]{1, 6, 1}, 3));
++total; if (sol.smallestDistancePair(new int[]{1, 2, 3, 4}, 6) == 3) ++pass; else System.out.println("FAIL test4: " + sol.smallestDistancePair(new int[]{1, 2, 3, 4}, 6));
++total; if (sol.smallestDistancePair(new int[]{9, 10, 7, 10, 6, 1, 5, 4, 9, 8}, 18) == 2) ++pass; else System.out.println("FAIL test5: " + sol.smallestDistancePair(new int[]{9, 10, 7, 10, 6, 1, 5, 4, 9, 8}, 18));
if (pass == total) {
System.out.println("== 0719 PASS ==");
} else {
System.out.println("== 0719 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n + n log W) | O(log n)(排序栈) |
| 解法二 | O(n + W) | O(W) |
720. Longest Word in Dictionary(词典中最长的单词)
题目描述
给出一个字符串数组 words 组成的一本英语词典。返回 words 中最长的一个单词,该单词是
由 words 词典中其他单词逐步添加一个字母组成。
若其中有多个可行的答案,则返回答案中字典序最小的单词。
若无答案,则返回空字符串。
示例 1
- 输入:
words= ["w","wo","wor","worl", "world"] - 输出:"world"
- 解释:单词"world"可由"
w", "wo", "wor", 和 "worl"逐步添加一个字母组成。
示例 2
- 输入:
words= ["a", "banana", "app", "appl", "ap", "apply", "apple"] - 输出:"apple"
- 解释:"apply" 和 "apple" 都能由词典中的单词组成。但是 "apple" 的字典序小于 "apply"。 提示:
1 <= words.length <= 10001 <= words[i].length <= 30- 所有输入的字符串
words[i]都只包含小写字母
解题思路
解法一(排序 + 前缀集合校验,主提交):把单词按长度升序、同长按字典序排序;用 Set
存放所有可作为前缀的单词(空串可作起始)。遍历每个单词,若其去掉末字符的前缀在集合中,
则该单词可构成,加入集合并更新答案(保证字典序最小:同长只保留字典序小者)。
时间复杂度 O(n·L·log n),空间复杂度 O(n·L)。
解法二(Trie + DFS):把单词插入字典树,DFS 从根开始沿 isEnd 路径走,收集最长路径;
兼得前缀判断。时间复杂度 O(总字符数),空间复杂度 O(总字符数)。
代码实现
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
class Solution {
// 解法一:排序 + 前缀集合
public String longestWord(String[] words) {
Arrays.sort(words, (a, b) -> a.length() != b.length() ? a.length() - b.length() : a.compareTo(b));
Set<String> prefix = new HashSet<>();
prefix.add("");
String ans = "";
for (String w : words) {
if (prefix.contains(w.substring(0, w.length() - 1))) {
prefix.add(w);
if (w.length() > ans.length()) {
ans = w;
}
}
}
return ans;
}
// 解法二:Trie + DFS
private static class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isEnd;
String word;
}
private TrieNode root = new TrieNode();
public String longestWord2(String[] words) {
for (String w : words) {
TrieNode node = root;
for (char c : w.toCharArray()) {
int idx = c - 'a';
if (node.children[idx] == null) {
node.children[idx] = new TrieNode();
}
node = node.children[idx];
}
node.isEnd = true;
node.word = w;
}
return dfs2(root);
}
private String dfs2(TrieNode node) {
String best = node.word == null ? "" : node.word;
for (TrieNode ch : node.children) {
if (ch != null && ch.isEnd) {
String cand = dfs2(ch);
if (cand.length() > best.length()
|| (cand.length() == best.length() && cand.compareTo(best) < 0)) {
best = cand;
}
}
}
return best;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.longestWord(new String[]{"w", "wo", "wor", "worl", "world"}).equals("world")) ++pass; else System.out.println("FAIL test1: " + sol.longestWord(new String[]{"w", "wo", "wor", "worl", "world"}));
++total; if (sol.longestWord(new String[]{"a", "banana", "app", "appl", "ap", "apply", "apple"}).equals("apple")) ++pass; else System.out.println("FAIL test2: " + sol.longestWord(new String[]{"a", "banana", "app", "appl", "ap", "apply", "apple"}));
++total; if (sol.longestWord(new String[]{"b", "br", "bre", "brea", "break", "brea"}).equals("break")) ++pass; else System.out.println("FAIL test3: " + sol.longestWord(new String[]{"b", "br", "bre", "brea", "break", "brea"}));
++total; if (sol.longestWord2(new String[]{"w", "wo", "wor", "worl", "world"}).equals("world")) ++pass; else System.out.println("FAIL test4");
++total; if (sol.longestWord2(new String[]{"a", "banana", "app", "appl", "ap", "apply", "apple"}).equals("apple")) ++pass; else System.out.println("FAIL test5");
if (pass == total) {
System.out.println("== 0720 PASS ==");
} else {
System.out.println("== 0720 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n·L·log n) | O(n·L) |
| 解法二 | O(总字符数) | O(总字符数) |
721. Accounts Merge(账户合并)
题目描述
给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素
accounts[i][0] 是名称(name),其余元素是 emails 表示该账户的邮箱地址。
现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。
请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。
一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。
合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是按字符 ASCII 顺序
排序的邮箱地址。账户本身可以以任意顺序返回。
示例 1
- 输入:accounts = [["John", "johnsmith@mail.com", "john00@mail.com"],
["John", "johnnybravo@mail.com"],
["John", "johnsmith@mail.com", "
john_newyork@mail.com"], ["Mary", "mary@mail.com"]] - 输出:[["John", 'john00@mail.com', '
john_newyork@mail.com', 'johnsmith@mail.com'], ["John", "johnnybravo@mail.com"], ["Mary", "mary@mail.com"]] - 解释:第一个和第三个 John 是同一个人,因为他们有共同的邮箱地址 "johnsmith@mail.com"。
第二个 John 和 Mary 是不同的人,因为他们的邮箱地址没有被其他帐户使用。
你可以以任何顺序返回这些列表,例如答案 [['Mary','mary@mail.com'],
['John','johnnybravo@mail.com'],['John','john00@mail.com','
john_newyork@mail.com', 'johnsmith@mail.com']] 也是可以的。 提示: 1 <= accounts.length <= 10002 <= accounts[i].length <= 101 <= accounts[i][j].length <= 30accounts[i][0]由英文字母组成accounts[i][j](j > 0)是形如xxxx@xxx.com的邮箱地址- 每个账户的邮箱按顺序排列给出,但不一定有序
解题思路
解法一(并查集合并邮箱,主提交):把每个邮箱映射到一个整数 id。遍历账户,把同一账户的 所有邮箱 union 到一起(用首个邮箱作为代表),并记录每个邮箱归属的人名。合并完成后,把 每个根 id 下的邮箱收集、排序、加上人名。时间复杂度 O(Σ|emails|·α + K·logK), 空间复杂度 O(邮箱数)。 解法二(DFS 构图):邮箱为节点,同一账户的邮箱彼此连边;对每个连通分量收集邮箱。
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
class Solution {
// 并查集
private static class UnionFind {
int[] parent;
int[] rank;
UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
void union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) {
return;
}
if (rank[ra] < rank[rb]) {
parent[ra] = rb;
} else if (rank[ra] > rank[rb]) {
parent[rb] = ra;
} else {
parent[rb] = ra;
++rank[ra];
}
}
}
public List<List<String>> accountsMerge(List<List<String>> accounts) {
Map<String, Integer> emailId = new HashMap<>();
Map<String, String> emailName = new HashMap<>();
UnionFind uf = new UnionFind(accounts.size() * 10 + 1);
int id = 0;
for (List<String> acc : accounts) {
String name = acc.get(0);
int firstId = -1;
for (int i = 1; i < acc.size(); ++i) {
String email = acc.get(i);
if (!emailId.containsKey(email)) {
emailId.put(email, id);
emailName.put(email, name);
++id;
}
if (firstId == -1) {
firstId = emailId.get(email);
} else {
uf.union(firstId, emailId.get(email));
}
}
}
// 根 -> 邮箱列表
Map<Integer, List<String>> groups = new TreeMap<>();
for (Map.Entry<String, Integer> e : emailId.entrySet()) {
int root = uf.find(e.getValue());
groups.computeIfAbsent(root, k -> new ArrayList<>()).add(e.getKey());
}
List<List<String>> ans = new ArrayList<>();
for (List<String> emails : groups.values()) {
Collections.sort(emails);
List<String> row = new ArrayList<>();
row.add(emailName.get(emails.get(0)));
row.addAll(emails);
ans.add(row);
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
List<List<String>> acc1 = new ArrayList<>();
acc1.add(Arrays.asList("John", "johnsmith@mail.com", "john00@mail.com"));
acc1.add(Arrays.asList("John", "johnnybravo@mail.com"));
acc1.add(Arrays.asList("John", "johnsmith@mail.com", "john_newyork@mail.com"));
acc1.add(Arrays.asList("Mary", "mary@mail.com"));
List<List<String>> r1 = sol.accountsMerge(acc1);
++total; if (r1.size() == 3) ++pass; else System.out.println("FAIL test1 size: " + r1.size());
List<List<String>> acc2 = new ArrayList<>();
acc2.add(Arrays.asList("a", "a@mail.com", "b@mail.com"));
acc2.add(Arrays.asList("a", "b@mail.com", "c@mail.com"));
List<List<String>> r2 = sol.accountsMerge(acc2);
++total; if (r2.size() == 1 && r2.get(0).size() == 4 && r2.get(0).get(0).equals("a")
&& r2.get(0).get(1).equals("a@mail.com") && r2.get(0).get(2).equals("b@mail.com")
&& r2.get(0).get(3).equals("c@mail.com")) ++pass; else System.out.println("FAIL test2: " + r2);
List<List<String>> acc3 = new ArrayList<>();
acc3.add(Arrays.asList("Gabe", "Gabe0@m.co", "Gabe3@m.co", "Gabe1@m.co"));
acc3.add(Arrays.asList("Kevin", "Kevin3@m.co", "Kevin5@m.co", "Kevin0@m.co"));
acc3.add(Arrays.asList("Ethan", "Ethan5@m.co", "Ethan4@m.co", "Ethan0@m.co"));
acc3.add(Arrays.asList("Hanzo", "Hanzo3@m.co", "Hanzo1@m.co", "Hanzo0@m.co"));
acc3.add(Arrays.asList("Fern", "Fern5@m.co", "Fern1@m.co", "Fern0@m.co"));
List<List<String>> r3 = sol.accountsMerge(acc3);
++total; if (r3.size() == 5) ++pass; else System.out.println("FAIL test3 size: " + r3.size());
if (pass == total) {
System.out.println("== 0721 PASS ==");
} else {
System.out.println("== 0721 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(ΣE·α) | O(E) |
| 解法二 | O(ΣE) | O(E) |
722. Remove Comments(删除注释)
题目描述
给一个 C++ 程序,删除程序中的注释。这个程序 source 是一个数组,其中 source[i] 表示
源代码中的第 i 行。源代码表示的是由换行符分隔的多行文本。
注释有两种风格:
- 行注释:
//和行尾之间。对于//之后的字符将不做处理。 - 块注释:
/*和*\/之间。块注释可以跨越多行。 你要写一个函数,输出解析后的代码。解析后的代码中,应该去掉注释、保留空白字符(空格、 制表符、换行符等)。比如"int a; /* comment *\/"解析后"int a; "(注意空格被保留)。 输入的源码保证是合法的,即块注释总是闭合的,且//不会出现在块注释内部,/*不会 出现在行注释或块注释内部。 提示: - 如果(在块注释中)出现
"*\/",它的作用只是结束块注释。块的层级结构不会嵌套。
示例 1
- 输入:source = ["/*Test program /", "
intmain()", "{ ", " // variable declaration ", "inta,b,c;", "/ This isatest", " multiline ", " comment for ", " testing */","a = b + c;", "}"] - 输出:["
intmain()", "{ ", " ", "inta,b,c;", "a=b+c;", "}"] - 解释:示例代码可以写成下面这样:
/*Test program / // 删除注释
intmain() // 保留 { // variable declaration // 行注释被删除inta,b,c; // 保留 / This isatest // 块注释开始 multiline comment for testing */ // 块注释结束a=b+c; // 保留 }
示例 2
- 输入:source = ["
a/comment", "line", "more_comment/b"] - 输出:["ab"]
- 解释:原始的 source 字符串是 "
a/*comment\nline`nmore_comment*\/b`",其中我们用粗体显示了 换行符。删除注释后,隐含的换行符被删除,留下字符串 "ab" 用换行符分隔成数组时就是 ["ab"]。 提示: 1 <= source.length <= 1000 <= source[i].length <= 200source[i]中只包含 ASCII 码字符- 保证块注释总是正确地闭合,
//不会出现在块注释内部,/*不会出现在行注释或块注释内部。
解题思路
解法一(状态机逐字符扫描,主提交):维护 inBlock 标志。逐行扫描:
- 非块注释中遇到
//:跳过本行余下字符; - 遇到
/*:置inBlock = true并跳过这两个字符; - 块注释中遇到
*\/:置inBlock = false并跳过; 其余普通字符加入当前行缓冲。若一行缓冲非空,则将其加入结果(块注释产生的空行不出现在 输出中)。时间复杂度 O(总字符数),空间复杂度 O(总字符数)。 解法二(正则替换思路):把整体拼成一个字符串后用正则(/\*.*?\*\/|//[^\n]*)处理,再按 换行切分。.*?非贪婪需注意,直接实现较容易出错,作为对比说明。
代码实现
import java.util.ArrayList;
import java.util.List;
class Solution {
// 解法一:状态机
public List<String> removeComments(String[] source) {
List<String> ans = new ArrayList<>();
boolean inBlock = false;
StringBuilder cur = new StringBuilder();
for (String line : source) {
int i = 0;
while (i < line.length()) {
if (inBlock) {
if (i + 1 < line.length() && line.charAt(i) == '*' && line.charAt(i + 1) == '/') {
inBlock = false;
i += 2;
} else {
++i;
}
} else {
if (i + 1 < line.length() && line.charAt(i) == '/' && line.charAt(i + 1) == '/') {
i = line.length(); // 行注释,跳过余下
} else if (i + 1 < line.length() && line.charAt(i) == '/' && line.charAt(i + 1) == '*') {
inBlock = true;
i += 2;
} else {
cur.append(line.charAt(i));
++i;
}
}
}
if (!inBlock && cur.length() > 0) {
ans.add(cur.toString());
cur.setLength(0);
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
String[] src1 = {"/*Test program */", "int main()", "{ ", " // variable declaration ",
"int a, b, c;", "/* This is a test", " multiline ", " comment for ",
" testing */", "a = b + c;", "}"};
List<String> r1 = sol.removeComments(src1);
List<String> e1 = new ArrayList<>();
e1.add("int main()");
e1.add("{ ");
e1.add(" ");
e1.add("int a, b, c;");
e1.add("a = b + c;");
e1.add("}");
++total; if (r1.equals(e1)) ++pass; else System.out.println("FAIL test1: " + r1);
String[] src2 = {"a/*comment", "line", "more_comment*/b"};
List<String> r2 = sol.removeComments(src2);
++total; if (r2.size() == 1 && r2.get(0).equals("ab")) ++pass; else System.out.println("FAIL test2: " + r2);
String[] src3 = {"a//comment", "b"};
List<String> r3 = sol.removeComments(src3);
++total; if (r3.equals(new ArrayList<>(java.util.Arrays.asList("a", "b")))) ++pass; else System.out.println("FAIL test3: " + r3);
if (pass == total) {
System.out.println("== 0722 PASS ==");
} else {
System.out.println("== 0722 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | ||||
|---|---|---|---|---|---|---|
| 解法一 | O(Σ | source[i] | ) | O(Σ | source[i] | ) |
723. Candy Crush(粉碎糖果)
题目描述
这个问题是实现一个简单的消除算法。
给定一个 m x n 的二维整数数组 board 代表糖果所在的方格,不同的正整数 board[i][j]
代表不同种类的糖果,如果 board[i][j] == 0 代表 (i, j) 这个位置是空的。
给定的方格是玩家移动后的游戏状态,现在需要你根据以下规则粉碎糖果,使得整个方格处于稳定
状态并最终输出:
- 如果有三个及以上水平或者垂直相连的同种糖果,同一时间将它们粉碎,即将这些位置变成空的。
- 在同时粉碎掉这些糖果之后,如果有一个空的位置上方还有糖果,那么上方的糖果就会下落 直到碰到下方的糖果或者底部,这些糖果都是同时下落,也不会有新的糖果从顶部出现并落下来。
- 通过前两步的操作,可能又会出现可以粉碎的糖果,请继续重复前面的操作。
- 当不存在可以粉碎的糖果,也就是状态稳定之后,请输出最终的状态。
示例 1
- 输入:
board= [[110,5,112,113,114],[210,211,5,213,214],[310,311,3,313,314], [410,411,412,5,414],[5,1,512,3,3],[610,4,1,613,614], [710,1,2,713,714],[810,1,2,1,1],[1,1,2,2,2],[4,1,4,4,1014]] - 输出: [[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[110,0,0,0,114],[210,0,0,0,214], [310,0,0,113,314],[410,0,0,213,414],[610,211,112,313,614],[710,311,412,613,714], [810,411,512,713,1014]]
示例 2
- 输入:
board= [[1,3,5,5,2],[3,4,3,3,1],[3,2,4,5,2],[2,4,4,5,5],[1,4,4,1,1]] - 输出: [[1,3,0,0,0],[3,4,0,5,2],[3,2,0,3,1],[2,4,0,5,2],[1,4,3,1,1]] 提示:
m == board.lengthn == board[i].length3 <= m, n <= 501 <= board[i][j] <= 2000
解题思路
解法一(标记 + 下落循环,主提交):循环执行两阶段直到没有可消除的糖果:
- 标记:对每个格子向右检查是否连续 3 个同值,向下检查是否连续 3 个同值;满足则把对应 位置取绝对值标记为负数(用绝对值保留原值,避免一列同时数行与列);
- 消除下落:把负值的格子置 0,然后逐列从底向上收集非 0 值,重新从底部填充,顶部补 0。 时间复杂度 O((m·n)²)(最坏需多轮),空间复杂度 O(1)(就地)。 解法二(每轮先整体标记到新矩阵再下落):思路一致,使用额外布尔矩阵标记待消除位,便于 理解;空间 O(m·n)。
代码实现
import java.util.Arrays;
class Solution {
// 解法一:标记 + 就地下落
public int[][] candyCrush(int[][] board) {
int m = board.length, n = board[0].length;
while (true) {
boolean changed = false;
// 1. 标记
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] == 0) {
continue;
}
int val = Math.abs(board[i][j]);
// 横向:右连续 >=3
if (j + 2 < n && Math.abs(board[i][j + 1]) == val && Math.abs(board[i][j + 2]) == val) {
for (int k = 0; j + k < n && Math.abs(board[i][j + k]) == val; ++k) {
board[i][j + k] = -val;
}
changed = true;
}
// 纵向:下连续 >=3
if (i + 2 < m && Math.abs(board[i + 1][j]) == val && Math.abs(board[i + 2][j]) == val) {
for (int k = 0; i + k < m && Math.abs(board[i + k][j]) == val; ++k) {
board[i + k][j] = -val;
}
changed = true;
}
}
}
if (!changed) {
break;
}
// 2. 消除与下落
for (int j = 0; j < n; ++j) {
int w = m - 1;
for (int i = m - 1; i >= 0; --i) {
if (board[i][j] > 0) {
board[w--][j] = board[i][j];
}
}
while (w >= 0) {
board[w--][j] = 0;
}
}
}
return board;
}
// 解法二:布尔标记矩阵版
public int[][] candyCrush2(int[][] board) {
int m = board.length, n = board[0].length;
while (true) {
boolean[][] mark = new boolean[m][n];
boolean changed = false;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] == 0) {
continue;
}
int v = board[i][j];
if (j + 2 < n && board[i][j + 1] == v && board[i][j + 2] == v) {
mark[i][j] = mark[i][j + 1] = mark[i][j + 2] = true;
changed = true;
}
if (i + 2 < m && board[i + 1][j] == v && board[i + 2][j] == v) {
mark[i][j] = mark[i + 1][j] = mark[i + 2][j] = true;
changed = true;
}
}
}
if (!changed) {
break;
}
for (int j = 0; j < n; ++j) {
int w = m - 1;
for (int i = m - 1; i >= 0; --i) {
if (!mark[i][j]) {
board[w--][j] = board[i][j];
}
}
while (w >= 0) {
board[w--][j] = 0;
}
}
}
return board;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] b1 = {
{110, 5, 112, 113, 114}, {210, 211, 5, 213, 214}, {310, 311, 3, 313, 314},
{410, 411, 412, 5, 414}, {5, 1, 512, 3, 3}, {610, 4, 1, 613, 614},
{710, 1, 2, 713, 714}, {810, 1, 2, 1, 1}, {1, 1, 2, 2, 2}, {4, 1, 4, 4, 1014}};
int[][] e1 = {
{0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {110, 0, 0, 0, 114},
{210, 0, 0, 0, 214}, {310, 0, 0, 113, 314}, {410, 0, 0, 213, 414},
{610, 211, 112, 313, 614}, {710, 311, 412, 613, 714}, {810, 411, 512, 713, 1014}};
int[][] r1 = sol.candyCrush(b1);
++total; if (Arrays.deepEquals(r1, e1)) ++pass; else System.out.println("FAIL test1: " + Arrays.deepToString(r1));
int[][] b2 = {{1, 3, 5, 5, 2}, {3, 4, 3, 3, 1}, {3, 2, 4, 5, 2}, {2, 4, 4, 5, 5}, {1, 4, 4, 1, 1}};
int[][] e2 = {{1, 3, 0, 0, 0}, {3, 4, 0, 5, 2}, {3, 2, 0, 3, 1}, {2, 4, 0, 5, 2}, {1, 4, 3, 1, 1}};
int[][] r2 = sol.candyCrush2(b2);
++total; if (Arrays.deepEquals(r2, e2)) ++pass; else System.out.println("FAIL test2: " + Arrays.deepToString(r2));
int[][] b3 = {{1, 1, 1}, {2, 2, 2}, {3, 3, 3}};
int[][] r3 = sol.candyCrush(b3);
++total; if (Arrays.deepEquals(r3, new int[][]{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}})) ++pass; else System.out.println("FAIL test3: " + Arrays.deepToString(r3));
if (pass == total) {
System.out.println("== 0723 PASS ==");
} else {
System.out.println("== 0723 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O((m·n)²)(最坏) | O(1) |
| 解法二 | O((m·n)²)(最坏) | O(m·n) |
724. Find Pivot Index(寻找数组的中心下标)
题目描述
给你一个整数数组 nums,请计算数组的中心下标。
数组中心下标是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为 0,因为在下标的左侧不存在元素。这一点
对于中心下标位于数组最右端同样适用。
如果数组有多个中心下标,应该返回最靠近左边的那一个。如果数组不存在中心下标,返回 -1。
示例 1
- 输入:
nums= [1, 7, 3, 6, 5, 6] - 输出:3
- 解释:中心下标是 3 。左侧数之和
sum=nums[0]+nums[1]+nums[2]= 1 + 7 + 3 = 11, 右侧数之和sum=nums[4]+nums[5]= 5 + 6 = 11,二者相等。
示例 2
- 输入:
nums= [1, 2, 3] - 输出:-1
- 解释:数组中不存在满足此条件的中心下标。
示例 3
- 输入:
nums= [2, 1, -1] - 输出:0
- 解释:中心下标是 0 。左侧数之和
sum= 0 ,(下标 0 左侧不存在元素),右侧数之和sum=nums[1] + nums[2] = 1 + -1 = 0提示: 1 <= nums.length <= 10^4-1000 <= nums[i] <= 1000
解题思路
解法一(前缀和,主提交):计算出数组总和 total;从左到右维护左侧和 lt,当
2 * lt == total - nums[i](即 lt == 右侧和)时返回 i。时间复杂度 O(n),
空间复杂度 O(1)。
解法二(前缀和数组):先建前缀和数组再枚举 i。O(n) 时间、O(n) 空间。
代码实现
class Solution {
// 解法一:总和 + 单变量
public int pivotIndex(int[] nums) {
int total = 0;
for (int v : nums) {
total += v;
}
int lt = 0;
for (int i = 0; i < nums.length; ++i) {
if (2 * lt == total - nums[i]) {
return i;
}
lt += nums[i];
}
return -1;
}
// 解法二:前缀和数组
public int pivotIndex2(int[] nums) {
int n = nums.length;
int[] pre = new int[n + 1];
for (int i = 0; i < n; ++i) {
pre[i + 1] = pre[i] + nums[i];
}
for (int i = 0; i < n; ++i) {
if (pre[i] == pre[n] - pre[i + 1]) {
return i;
}
}
return -1;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.pivotIndex(new int[]{1, 7, 3, 6, 5, 6}) == 3) ++pass; else System.out.println("FAIL test1: " + sol.pivotIndex(new int[]{1, 7, 3, 6, 5, 6}));
++total; if (sol.pivotIndex(new int[]{1, 2, 3}) == -1) ++pass; else System.out.println("FAIL test2");
++total; if (sol.pivotIndex(new int[]{2, 1, -1}) == 0) ++pass; else System.out.println("FAIL test3");
++total; if (sol.pivotIndex(new int[]{-1, -1, -1, -1, -1, 0}) == 2) ++pass; else System.out.println("FAIL test4: " + sol.pivotIndex(new int[]{-1, -1, -1, -1, -1, 0}));
++total; if (sol.pivotIndex2(new int[]{1, 7, 3, 6, 5, 6}) == 3) ++pass; else System.out.println("FAIL test5");
++total; if (sol.pivotIndex2(new int[]{1, 2, 3}) == -1) ++pass; else System.out.println("FAIL test6");
if (pass == total) {
System.out.println("== 0724 PASS ==");
} else {
System.out.println("== 0724 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(n) |
725. Split Linked List in Parts(分隔链表)
题目描述
给你一个头结点为 head 的单链表和一个整数 k,请你设计一个算法将链表分隔为 k 个
连续的部分。
每部分的长度应该尽可能的相等:任意两部分的长度差距不能超过 1,这可能导致有些部分为 null。
这 k 个部分应该按照在链表中出现的顺序排列,并且排在前面的部分的长度应该大于或等于排在
后面的长度。
返回一个由上述 k 部分组成的数组。
示例 1
- 输入:
head= [1,2,3],k= 5 - 输出:[[1],[2],[3],[],[]]
- 解释:第一个元素输出 [1] ,第二个元素输出 [2] ,第三个元素输出 [3] ,第四个和第五个为 空 []。
示例 2
- 输入:
head= [1,2,3,4,5,6,7,8,9,10],k= 3 - 输出:[[1,2,3,4],[5,6,7],[8,9,10]]
- 解释:输入被分成了几个连续的部分,并且每部分的长度相差不超过 1。前面部分的长度大于等于 后面部分的长度。 提示:
- 链表中节点的数目在范围
[0, 1000] 0 <= Node.val <= 10001 <= k <= 50
解题思路
解法一(先求长再均分,主提交):先遍历统计链表长度 n。前 n % k 段每段长 n/k + 1,
其余每段长 n/k。第二次遍历按段长切分:每段先记录段头,再前进 len - 1 步后断开。
时间复杂度 O(n),空间复杂度 O(k)(结果数组)。
解法二(长度数组预分配):先求长并预分配 parts[k] 长度,再逐个截取。思路一致,
只是把长度算在前。
代码实现
import java.util.Arrays;
class Solution {
public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
// 解法一:求长 + 均分断开
public ListNode[] splitListToParts(ListNode head, int k) {
int n = 0;
ListNode cur = head;
while (cur != null) {
++n;
cur = cur.next;
}
int base = n / k;
int extra = n % k;
ListNode[] ans = new ListNode[k];
cur = head;
for (int i = 0; i < k; ++i) {
int len = base + (i < extra ? 1 : 0);
if (len == 0) {
ans[i] = null;
continue;
}
ans[i] = cur;
for (int j = 1; j < len; ++j) {
cur = cur.next;
}
ListNode next = cur.next;
cur.next = null;
cur = next;
}
return ans;
}
// 解法二:预分配长度
public ListNode[] splitListToParts2(ListNode head, int k) {
int n = 0;
ListNode cur = head;
while (cur != null) {
++n;
cur = cur.next;
}
int base = n / k, extra = n % k;
int[] lens = new int[k];
for (int i = 0; i < k; ++i) {
lens[i] = base + (i < extra ? 1 : 0);
}
ListNode[] ans = new ListNode[k];
cur = head;
for (int i = 0; i < k; ++i) {
if (lens[i] == 0) {
ans[i] = null;
continue;
}
ans[i] = cur;
for (int j = 1; j < lens[i]; ++j) {
cur = cur.next;
}
ListNode next = cur.next;
cur.next = null;
cur = next;
}
return ans;
}
private static ListNode build(int[] arr) {
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
for (int v : arr) {
cur.next = new ListNode(v);
cur = cur.next;
}
return dummy.next;
}
private static String fmt(ListNode[] parts) {
StringBuilder sb = new StringBuilder("[");
for (ListNode p : parts) {
sb.append('[');
while (p != null) {
sb.append(p.val);
if (p.next != null) {
sb.append(',');
}
p = p.next;
}
sb.append(']');
}
return sb.append(']').toString();
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
ListNode[] r1 = sol.splitListToParts(build(new int[]{1, 2, 3}), 5);
++total; if (fmt(r1).equals("[[1][2][3][][]]")) ++pass; else System.out.println("FAIL test1: " + fmt(r1));
ListNode[] r2 = sol.splitListToParts(build(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}), 3);
++total; if (fmt(r2).equals("[[1,2,3,4][5,6,7][8,9,10]]")) ++pass; else System.out.println("FAIL test2: " + fmt(r2));
ListNode[] r3 = sol.splitListToParts2(build(new int[]{1, 2, 3, 4, 5}), 2);
++total; if (fmt(r3).equals("[[1,2,3][4,5]]")) ++pass; else System.out.println("FAIL test3: " + fmt(r3));
ListNode[] r4 = sol.splitListToParts2(null, 3);
++total; if (fmt(r4).equals("[[][][]]")) ++pass; else System.out.println("FAIL test4: " + fmt(r4));
if (pass == total) {
System.out.println("== 0725 PASS ==");
} else {
System.out.println("== 0725 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(k) |
| 解法二 | O(n) | O(k) |