LeetCode 热题 100:https://leetcode.cn/studyplan/top-100-liked/
文章目录
- 一、哈希
- 1. 两数之和
- 49. 字母异位词分组
- 128. 最长连续序列
- 二、双指针
- 283. 移动零
- 11. 盛水最多的容器
- 15. 三数之和
- 42. 接雨水(待完成)
- 三、滑动窗口
- 3. 无重复字符的最长子串
- 438. 找到字符串中所有字母异位词
- 四、子串
- 560. 和为 K 的子数组
- 239. 滑动窗口最大值
- 76. 最小覆盖子串
- 补充:209. 长度最小的子数组
- 五、普通数组
- 53. 最大子数组和
- 56. 合并区间
- 189. 轮转数组
- 238. 除自身以外数组的乘积
- 41. 缺失的第一个正数(待完成)
- 六、矩阵
- 73. 矩阵置零
- 54. 螺旋矩阵
- 48. 旋转图像
- 240. 搜索二维矩阵 II
- 七、链表
- 160. 相交链表
- 206. 反转链表
- 234. 回文链表
- 141. 环形链表
- 142. 环形链表 II
- 21. 合并两个有序链表
- 2. 两数相加
- 19. 删除链表的倒数第 N 个结点
- 24. 两两交换链表中的节点
- 25. K 个一组翻转链表(待完成)
- 138. 随机链表的复制
- 148. 排序链表
- 23. 合并 K 个升序链表
- 146. LRU 缓存
一、哈希
1. 两数之和
思路:设置一个 map 容器,用于存储当前元素和索引。遍历时一边将数据存入 map,一边比从map中查找满足加和等于 target 的另一个元素。
class Solution {
/**
* 输入:nums = [2,7,11,15], target = 9
* 输出:[0,1]
* 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
*/
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(target - nums[i])) {
return new int[] {map.get(target - nums[i]), i};
}
map.put(nums[i], i);
}
return new int[] {};
}
}
49. 字母异位词分组
思路:设置一个 map 容器,key是排序后的字符组合,value是字母异位词的集合。
class Solution {
/**
* 输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
* 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
*/
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
char[] chars = str.toCharArray();
Arrays.sort(chars);
String sortStr = Arrays.toString(chars);
// 如果存在key,即:new String(chars),那么返回对应的 value;
// 否则将执行先初始化 key:new String(chars),value: new ArrayList<>(),然后在返回value。
map.computeIfAbsent(new String(chars), s -> new ArrayList<>()).add(str);
}
return new ArrayList<>(map.values());
}
}
128. 最长连续序列
思路:因为题目要求O(n)的时间复杂度,因此使用set对数组进行转存,并利用滑动窗口一次遍历即可得出连续序列的最长长度。
class Solution {
/**
* 输入:nums = [100,4,200,1,3,2]
* 输出:4
* 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
*/
public int longestConsecutive(int[] nums) {
if (nums.length == 0) {
return 0;
}
Set<Integer> set = new TreeSet<>();
for (int num : nums) {
set.add(num);
}
int co = 0;
for (Integer num : set) {
nums[co++] = num;
}
return sliderWindow(nums);
}
private int sliderWindow(int[] nums) {
int left = 0;
int len = nums.length;
int max = 1;
for (int right = 1; right < len; right++) {
if (nums[right] - nums[right - 1] != 1) {
left = right;
}
max = Math.max(right - left + 1, max);
}
return max;
}
}
二、双指针
283. 移动零
class Solution {
/**
* 输入: nums = [0,1,0,3,12]
* 输出: [1,3,12,0,0]
*/
public void moveZeroes(int[] nums) {
int j = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
nums[j++] = nums[i];
}
}
for (; j < nums.length; j++) {
nums[j] = 0;
}
}
}
11. 盛水最多的容器
思路:定义双指针,分别指向数组的最左边和最右边,每次往里移动较短的元素的指针。这里解释为什么要移动短的?
根据木桶原理,整个木桶盛水的最大体积取决于小的那一段木板。如果移动短的指针,体积可能变大,也可能不变,还有可能变小。但如果移动长的指针,体积一定会变小。因此在指针不断往里移动的同时,移动指向较短元素的指针能得出盛水最大的容量。
class Solution {
public int maxArea(int[] height) {
int len = height.length;
int left = 0;
int right = len - 1;
int maxArea = 0;
// 面积 = 短板 * 底边
// 向内移动短板,水槽短板 min(h[i], h[j]) 可能变大,下个水槽面积可能增大
// 向内移动长板,水槽短板 min(h[i], h[j]) 可能变小或不变,下个水槽面积一定减小(因为底边长变小)
while (left < right) {
maxArea = Math.max(Math.min(height[left], height[right]) * (right - left), maxArea);
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
}
15. 三数之和
思路:将数组排完序后进行遍历,遍历时选取当前元素的后一个元素和数组的最后一个元素为双指针。(注意对重复元素进行去重)
class Solution {
/**
* 输入: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] 。
* 注意,输出的顺序和三元组的顺序并不重要。
*/
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
int len = nums.length;
Arrays.sort(nums);
for (int i = 0; i < len; i++) {
if (nums[i] > 0) {
break;
}
if (i != 0 && nums[i] == nums[i - 1]) { // 去除重复元素
continue;
}
int left = i + 1;
int right = len - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) { // 去除重复元素
left++;
}
while (left < right && nums[right - 1] == nums[right]) {
right--;
}
left++;
right--;
} else if (sum > 0) {
right--;
} else {
left++;
}
}
}
return res;
}
}
42. 接雨水(待完成)
三、滑动窗口
3. 无重复字符的最长子串
思路:定义一个 map 容器, key 存储字符,value 存储当前字符索引。使用滑动窗口计算最长字串,当窗口内存在重复字符时,调整窗口的左边界,调整为重复元素索引的下一位,并且注意左边界不能向左移动。
class Solution {
/**
* 输入: s = "abcabcbb"
* 输出: 3
* 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
*/
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> map = new HashMap<>(); // key:字符,value:当前字符索引
int len = s.length();
int start = 0;
int max = 0;
for (int end = 0; end < len; end++) {
char ch = s.charAt(end);
if (map.containsKey(ch)) {
start = Math.max(map.get(ch) + 1, start);
// 处理 'abba',如果不用max比较当遍历到最后一个a时,start将会指向第一个b,即start-end范围是 bba
}
map.put(ch, end);
max = Math.max(end - start + 1, max);
}
return max;
}
}
438. 找到字符串中所有字母异位词
思路:使用数组统计字符串中26个字符的出现次数,固定滑动窗口大小,并使用 Arrays.equals(...)
方法一边遍历一边比较。
class Solution {
/**
* 输入: s = "cbaebabacd", p = "abc"
* 输出: [0,6]
* 解释:
* 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
* 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
*/
public List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList<>();
int sLen = s.length();
int pLen = p.length();
if (sLen < pLen) {
return res;
}
int[] sWin = new int[26];
int[] pWin = new int[26];
for (int i = 0; i < pLen; i++) {
sWin[s.charAt(i) - 'a']++;
pWin[p.charAt(i) - 'a']++;
}
if (Arrays.equals(sWin, pWin)) {
res.add(0);
}
for (int i = pLen; i < sLen; i++) {
sWin[s.charAt(i - pLen) - 'a']--;
sWin[s.charAt(i) - 'a']++;
if (Arrays.equals(sWin, pWin)) {
res.add(i - pLen + 1);
}
}
return res;
}
}
四、子串
560. 和为 K 的子数组
思路:首先计算前缀和,利用前缀和的差值确定子数组的和是否等于K。
如:下面数组求子数组和为 6,pre[4] - pre[1] == 6
就代表:num[1:3]
加和等于 6。
ind | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
---|---|---|---|---|---|---|---|---|---|
value | 4 | 1 | 2 | 3 | 0 | 6 | 2 | 4 | |
前缀和 pre | 0 | 4 | 5 | 7 | 10 | 10 | 16 | 18 | 22 |
注:这里我们预留第一个位置为0,代表索引为 0 的元素前缀和为 0。
class Solution {
/**
* 输入:nums = [1,2,3], k = 3
* 输出:2
*/
public int subarraySum(int[] nums, int k) {
int res = 0;
int len = nums.length;
int[] pre = new int[len + 1];
// 计算前缀和
for (int i = 0; i < len; i++) {
pre[i + 1] = pre[i] + nums[i];
}
for (int left = 0; left < len; left++) {
for (int right = left; right < len; right++) {
if (pre[right + 1] - pre[left] == k) {
res++;
}
}
}
return res;
}
}
上面做法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),因此用哈希表进行优化。
思路:设置一个 map 容器用于存储前缀和以及前缀和的个数,当计算前缀和的同时来查找是否存在 前缀和 - 目标和
,如果存在则说明存在子数组和等于 k。如:上述例子中,求子数组和为 6,当遍历到索引 4 时前缀和为 10, map 中存在键 key=“10-6”=4 {key=4,value=1}
,说明当前元素存在前缀和为 4 的情况。
ind | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
value | 4 | 1 | 2 | 3 | 0 | 6 | 2 | 4 |
累加的前缀和 | 4 | 5 | 7 | 10 | 10 | 16 | 18 | 22 |
class Solution {
/**
* 输入:nums = [1,2,3], k = 3
* 输出:2
*/
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>(); // key:前缀和,value: 前缀和的个数
int res = 0;
map.put(0, 1); // 前缀和为 0 的个数有一个
int sum = 0; // 记录前缀和
for (int num : nums) {
sum += num;
if (map.containsKey(sum - k)) {
res += map.get(sum - k);
}
map.put(sum, map.getOrDefault(sum, 0) + 1);
}
return res;
}
}
239. 滑动窗口最大值
思路:设置一个大顶堆,固定窗口大小,遍历时首先清除过期元素,然后将元素入堆。
值得注意的是,有些比较小的元素由于不在堆顶,不会立即删除。但是在后面如果到了堆顶,也会删除。
class Solution {
/**
* 输入: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
*/
public int[] maxSlidingWindow(int[] nums, int k) {
PriorityQueue<Elem> heap = new PriorityQueue<>((elem1, elem2) -> elem2.value - elem1.value);// 初始化大顶堆
int len = nums.length;
int[] res = new int[len - k + 1];
for (int i = 0; i < k; i++) {
heap.add(new Elem(nums[i], i));
}
res[0] = heap.element().value;
int co = 1;
for (int i = k; i < len; i++) {
while (!heap.isEmpty() && heap.element().index <= i - k) { // 处理不在窗口的元素
// 有些比较小的元素由于不在堆顶,不会立即删除。但是在后面如果到了堆顶,也会删除
// 如:nums = [5,6,-1,-2,3], k = 3
// 当窗口在[6,-1,-2]时,5还在堆内,但是当窗口在[-1,-2,3]时,会在堆顶被删除
heap.remove();
}
heap.add(new Elem(nums[i], i));
res[co++] = heap.element().value;
}
return res;
}
class Elem {
int value;
int index;
public Elem() {
}
public Elem(int value, int index) {
this.value = value;
this.index = index;
}
}
}
76. 最小覆盖子串
思路:分别设置两个数组用来存储字符的出现次数,利用滑动窗口边一边右移一边检查模式串是否被覆盖。
class Solution {
/**
* 输入:s = "ADOBECODEBANC", t = "ABC"
* 输出:"BANC"
* 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
*/
public String minWindow(String s, String t) {
if (s.length() < t.length()) {
return "";
}
int[] sChars = new int[128];
int[] tChars = new int[128];
for (char ch : t.toCharArray()) {
tChars[ch]++;
}
int left = 0;
int sLen = s.length();
int resLeft = -1;
int resRight = sLen;
for (int right = 0; right < sLen; right++) {
sChars[s.charAt(right)]++;
while (left <= right && isCovered(sChars, tChars)) {
if (right - left < resRight - resLeft) {
resLeft = left;
resRight = right;
}
sChars[s.charAt(left)]--;
left++;
}
}
return resLeft == -1 ? "" : s.substring(resLeft, resRight + 1);
}
private boolean isCovered(int[] sChars, int[] tChars) {
for (int i = 'A'; i <= 'Z'; i++) {
if (sChars[i] < tChars[i]) {
return false;
}
}
for (int i = 'a'; i <= 'z'; i++) {
if (sChars[i] < tChars[i]) {
return false;
}
}
return true;
}
}
上面代码在每次遍历的时候都需要检查子串是否被覆盖,因此可以考虑设置两个变量 sNum 和 tNum。tNum 用于记录 t 中不同字符的数量, sNum 用于记录 s 指定字符达到覆盖 t 的程度数量。如:当 s 的子串中如果 ‘a’ 的数量等于 t 中 ‘a’ 字符的数量时 sNum + 1,否则不变。
class Solution {
/**
* 输入:s = "ADOBECODEBANC", t = "ABC"
* 输出:"BANC"
* 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
*/
public String minWindow(String s, String t) {
int[] sChars = new int[128];
int[] tChars = new int[128];
int sNum = 0; // 记录 s 中的指定字符数量达到覆盖 t 程度的数量
int tNum = 0; // 记录 t 中有多少不同字符
for (char ch : t.toCharArray()) {
if (tChars[ch]++ == 0) {
tNum++;
}
}
int len = s.length();
int resLeft = -1;
int resRight = len;
int left = 0;
for (int right = 0; right < len; right++) {
if (++sChars[s.charAt(right)] == tChars[s.charAt(right)]) {
sNum++; // s中的该字符数量达到覆盖 t 中该字符的程度
}
while (left <= right && sNum == tNum) {
if (right - left < resRight - resLeft) { // 更新结果左右边界
resLeft = left;
resRight = right;
}
if (sChars[s.charAt(left)]-- == tChars[s.charAt(left)]) {
sNum--;
}
left++;
}
}
return resLeft == -1 ? "" : s.substring(resLeft, resRight + 1);
}
}
补充:209. 长度最小的子数组
最小覆盖子串题目类似:209. 长度最小的子数组
class Solution {
/**
* 输入:target = 7, nums = [2,3,1,2,4,3]
* 输出:2
* 解释:子数组 [4,3] 是长度最小且总和大于等于 target 的子数组。
*/
public int minSubArrayLen(int target, int[] nums) {
int len = nums.length;
int sum = 0;
int left = 0;
int res = len + 1;
for (int right = 0; right < len; right++) {
sum += nums[right];
while (left <= right && sum >= target) {
res = Math.min(right - left + 1, res);
sum -= nums[left++];
}
}
return res == len + 1 ? 0 : res;
}
}
五、普通数组
53. 最大子数组和
思路:设置变量 curr 用于记录子数组和,遍历数组时,当子数组和大于零时累加当前元素,否则令子数组和等于当前数组元素。
class Solution {
/**
* 输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
* 输出:6
* 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
*/
public int maxSubArray(int[] nums) {
int curr = 0;
int max = nums[0];
for (int i = 0; i < nums.length; i++) {
if (curr >= 0) {
curr += nums[i];
} else {
curr = nums[i];
}
max = Math.max(curr, max);
}
return max;
}
}
56. 合并区间
思路:定义内部类用于记录区间的左右端点,对二维数组按照左端点递增,左端点相同时右端点递增的规则排序。将数组第一个元素加入集合后进行遍历,若发现当前 数组元素左端点和集合最后一个元素的左端点相同
或者 集合最后一个元素的右端点大于数组的左端点
,则将集合的最后一个元素的右端点进行取大处理,否则将数组元素加入集合。
class Solution {
/**
* 输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
* 输出:[[1,6],[8,10],[15,18]]
* 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
*/
public int[][] merge(int[][] intervals) {
int len = intervals.length;
List<Range> list = new ArrayList<>();
Arrays.sort(intervals,
(range1, range2) -> range1[0] != range2[0] ? range1[0] - range2[0] : range1[1] - range2[1]);
list.add(new Range(intervals[0][0], intervals[0][1]));
for (int i = 1; i < len; i++) {
Range range = list.get(list.size() - 1);
if (range.begin == intervals[i][0] || range.end >= intervals[i][0]) {
range.end = Math.max(intervals[i][1], range.end); // Max比较大小是为了处理这种情况 [[1,4],[2,3]]
} else {
list.add(new Range(intervals[i][0], intervals[i][1]));
}
}
int size = list.size();
int[][] res = new int[size][2];
for (int i = 0; i < size; i++) {
res[i][0] = list.get(i).begin;
res[i][1] = list.get(i).end;
}
return res;
}
class Range {
int begin;
int end;
public Range() {
}
public Range(int begin, int end) {
this.begin = begin;
this.end = end;
}
}
}
189. 轮转数组
思路:先将数组全部翻转,然后对前 k 个元素和其余的元素分别做翻转。
class Solution {
/**
* 输入: nums = [1,2,3,4,5,6,7], k = 3
* 输出: [5,6,7,1,2,3,4]
* 解释:
* 向右轮转 1 步: [7,1,2,3,4,5,6]
* 向右轮转 2 步: [6,7,1,2,3,4,5]
* 向右轮转 3 步: [5,6,7,1,2,3,4]
*/
public void rotate(int[] nums, int k) {
int len = nums.length;
k %= len;
reverseArr(nums, 0, len - 1);
reverseArr(nums, 0, k - 1);
reverseArr(nums, k, len - 1);
}
private void reverseArr(int[] nums, int begin, int end) {
while (begin < end) {
int temp = nums[begin];
nums[begin] = nums[end];
nums[end] = temp;
begin++;
end--;
}
}
}
238. 除自身以外数组的乘积
思路:将数组元素累乘以后逐个相除可能会存在除零异常。因此,考虑分别求当前元素的左侧累乘积和右侧累乘积,最后再将两侧数组做累乘。
class Solution {
/**
* 输入: nums = [1,2,3,4]
* 输出: [24,12,8,6]
*/
public int[] productExceptSelf(int[] nums) {
int len = nums.length;
int[] left = new int[len];
int[] right = new int[len];
int[] res = new int[len];
left[0] = 1;
right[len - 1] = 1;
// nums: [1, 2, 3, 4]
// left: [1, 1, 2, 6]
// right: [24,12,4, 1]
for (int i = 1; i < len; i++) {
left[i] = nums[i - 1] * left[i - 1];
}
for (int i = len - 2; i >= 0; i--) {
right[i] = nums[i + 1] * right[i + 1];
}
for (int i = 0; i < len; i++) {
res[i] = left[i] * right[i];
}
return res;
}
}
41. 缺失的第一个正数(待完成)
六、矩阵
73. 矩阵置零
思路:设置矩阵行列大小的两个数组,用于对矩阵元素为零的行列进行标记。再次遍历矩阵,然后将标记过的行和列进行置零。
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
int[] rows = new int[m];
int[] columns = new int[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
rows[i] = 1;
columns[j] = 1;
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (rows[i] == 1 || columns[j] == 1) {
matrix[i][j] = 0;
}
}
}
}
}
54. 螺旋矩阵
思路:初始化矩阵的上下左右四个边界,按照 “从左向右、从上向下、从右向左、从下向上” 四个方向循环打印,每次都需要更新边界,并判断结束条件。
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<>();
int left = 0;
int right = matrix[0].length - 1;
int up = 0;
int down = matrix.length - 1;
while (true) {
for (int i = left; i <= right; i++) {
res.add(matrix[up][i]);
}
if (++up > down) {
break;
}
for (int i = up; i <= down; i++) {
res.add(matrix[i][right]);
}
if (left > --right) {
break;
}
for (int i = right; i >= left; i--) {
res.add(matrix[down][i]);
}
if (up > --down) {
break;
}
for (int i = down; i >= up; i--) {
res.add(matrix[i][left]);
}
if (++left > right) {
break;
}
}
return res;
}
}
48. 旋转图像
思路:先将矩阵转置,然后将左右对称的两列互换元素,即可达到顺时针旋转 90 度的效果。
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
for (int i = 0; i < n; i++) { // 矩阵转置
for (int j = i + 1; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
for (int i = 0; i < n; i++) { // 左右对称的两列互换
for (int j = 0; j < n / 2; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[i][n - 1 - j];
matrix[i][n - 1 - j] = temp;
}
}
}
}
240. 搜索二维矩阵 II
思路:利用 “每行的所有元素从左到右升序排列,每列的所有元素从上到下升序排列” 这个特点,从右上角开始向左下角的方向查找,当元素大于目标元素,这一列下面的元素都大于目标元素;当元素小于目标元素,这一行前面的元素都小于目标元素。
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length;
int n = matrix[0].length;
int x = 0; // 右上角
int y = n - 1;
while (x < m && y >= 0) {
if (matrix[x][y] > target) { // 当前元素大于target,这一列下面的元素都大于target
y--;
} else if (matrix[x][y] < target) { // 当前元素小于target,这一行前面的元素都小于target
x++;
} else {
return true;
}
}
return false;
}
}
也可以从左下角开始查找,代码如下:
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length;
int n = matrix[0].length;
int x = m - 1; // 左下角
int y = 0;
while (x >= 0 && y < n) {
if (matrix[x][y] > target) { // 当前元素大于target,这一行后面的元素都大于target
x--;
} else if (matrix[x][y] < target) { // 当前元素小于target,这一列上面的元素都小于target
y++;
} else {
return true;
}
}
return false;
}
}
七、链表
160. 相交链表
思路:利用乘法交换律,设两个链表相交前分别有 A B 个节点,相交部分有 C 个节点,那么 A+C+B=B+C+A。设置两个指针分别指向两个链表的头部,同时向后移动。当其中一个指针移动到结尾时,则转向指向另一个链表的头部,另一个指针步骤同上,最终两个指针会在相交处会面。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pa = headA;
ListNode pb = headB;
while (pa != pb) {
pa = pa == null ? headB : pa.next;
pb = pb == null ? headA : pb.next;
}
return pa;
}
}
注:如果两个链表不相交,也适合以上规律,最终两个指针都会指向空,也会跳出循环。
206. 反转链表
思路:链表头插法。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = new ListNode();
ListNode p;
p = head;
pre.next = null;
while(p != null){
ListNode temp = p.next;
p.next = pre.next;
pre.next = p;
p = temp;
}
return pre.next;
}
}
234. 回文链表
思路:本地的实现很多,这里采用栈进行辅助判断回文。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
Deque<ListNode> stack = new LinkedList<>();
ListNode p = head;
while (p != null) {
stack.push(p);
p = p.next;
}
while (head != null) {
p = stack.pop();
if (p.val != head.val) {
return false;
}
head = head.next;
}
return true;
}
}
141. 环形链表
思路1:使用 hash 表进行辅助判断是否存在环。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> set = new HashSet<>();
ListNode p = head;
while (p != null) {
if (set.contains(p)) {
return true;
}
set.add(p);
p = p.next;
}
return false;
}
}
思路2:使用快慢指针,slow 每次向前走一步,fast 每次向前走两步。
① 当存在环时,fast 由于走得快,会发生扣圈的情况,且最终与 slow 相遇。
② 当不存在环时,fast 可能在某次循环后,发生当前位置为空,或下一位置为空的两种情况,当然由于走的快,最终会返回 false。
总之,循环的结束条件,要么出现环 slow == fast,要么 fast 先一步为空。下面列举两种实现方式:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (true) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
}
}
// 推荐
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
ListNode slow = head;
ListNode fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
return false;
}
}
142. 环形链表 II
思路1:使用 hash 表。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
Set<ListNode> set = new HashSet<>();
ListNode p = head;
while (p != null) {
if (set.contains(p)) {
return p;
}
set.add(p);
p = p.next;
}
return null;
}
}
思路2:使用快慢指针,思路如下:
- 设
fast
每次走两个节点,slow
每次走一个节点。环外有a
个结点,环内有b
个结点。 - 第一次相遇时,
fast
走了f
步,slow
走了s
步。
①f = 2s
②f = s + nb
表示f
比s
多走了n*b
步,即n
圈。这样表示的原因在于扣圈。
化简得:f = 2nb, s = nb
,n
代表扣圈的次数,可能等于1,2,3,… - 设刚开始
slow
指针从开始到环的入口要走k
步:k = a + tb
,t
代表在环中循环的次数,可能等于0,1,2,3,…。因此当发生第一次相遇时,再走a
步即可重新回到入环的起点。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head;
ListNode fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
fast = head; // 令 fast 指针指向链表头部
break;
}
}
if (fast.next == null || fast.next.next == null) {
return null;
}
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
21. 合并两个有序链表
思路:设置两个指针,分别指向链表头部,逐个比较向后迭代即可。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode p1 = list1;
ListNode p2 = list2;
ListNode pre = new ListNode();
ListNode p = pre;
while (p1 != null && p2 != null) {
if (p1.val < p2.val) {
p.next = p1;
p = p1;
p1 = p1.next;
} else {
p.next = p2;
p = p2;
p2 = p2.next;
}
}
if (p1 != null) {
p.next = p1;
}
if (p2 != null) {
p.next = p2;
}
return pre.next;
}
}
2. 两数相加
思路:设置两个指针和进位标志,逐个向后相加迭代即可。
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode pre = new ListNode();
ListNode p = pre;
ListNode p1 = l1;
ListNode p2 = l2;
int sign = 0;
int sum;
while (p1 != null || p2 != null) {
if (p1 != null && p2 != null) {
sum = p1.val + p2.val + sign;
p1 = p1.next;
p2 = p2.next;
} else if (p1 != null) {
sum = p1.val + sign;
p1 = p1.next;
} else {
sum = p2.val + sign;
p2 = p2.next;
}
p.next = new ListNode(sum % 10);
p = p.next;
sign = sum / 10;
}
if (sign != 0) {
p.next = new ListNode(sign);
}
return pre.next;
}
}
19. 删除链表的倒数第 N 个结点
思路:让前面的指针先移动 n 步,之后前后指针共同移动直到前面的指针到尾部为止。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = new ListNode();
pre.next = head;
ListNode p = pre;
ListNode q = pre;
int co = 0;
while (p.next != null) {
if (++co > n) {
q = q.next;
}
p = p.next;
}
q.next = q.next.next;
return pre.next;
}
}
24. 两两交换链表中的节点
思路:链表节点两两交换位置,逐个向后迭代。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode pre = new ListNode(0);
pre.next = head;
ListNode p = head;
ListNode q = pre;
while (p != null && p.next != null) {
ListNode temp = p.next.next;
q.next = p.next;
q.next.next = p;
p.next = null;
q = p;
p = temp;
}
if (p != null) {
q.next = p;
}
return pre.next;
}
}
25. K 个一组翻转链表(待完成)
138. 随机链表的复制
思路:题意是让我们把下面的随机链表做整体复制,这里我们设置一个 map 容器,用于对应原始节点和复制的节点,存储以后再处理 next 指针和 random 指针。
/*
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
Map<Node, Node> map = new HashMap<>();
Node p = head;
while (p != null) {
Node copyNode = new Node(p.val);
map.put(p, copyNode);
p = p.next;
}
p = head;
while (p != null) {
Node copyNode = map.get(p);
if (p.random != null) {
copyNode.random = map.get(p.random);
}
if (p.next != null) {
copyNode.next = map.get(p.next);
}
p = p.next;
}
return map.get(head);
}
}
148. 排序链表
思路:这里我们采用堆结构辅助链表排序,将大顶堆构造好以后,一边出堆一边利用头插法对链表结构进行重塑。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
PriorityQueue<ListNode> queue = new PriorityQueue<>((a, b) -> b.val-a.val); // 大顶堆
while(head != null){
queue.offer(head); // 从堆底插入
head = head.next;
}
ListNode pre = new ListNode(0);
while(!queue.isEmpty()){
ListNode p = queue.poll(); // 出队列并调整堆
p.next = pre.next; // 头插法倒序
pre.next = p;
}
return pre.next;
}
}
23. 合并 K 个升序链表
思路:K 个有序链表重复调用两个有序链表的算法。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
/**
* 输入:lists = [[1,4,5],[1,3,4],[2,6]]
* 输出:[1,1,2,3,4,4,5,6]
*/
public ListNode mergeKLists(ListNode[] lists) {
int len = lists.length;
ListNode pre = null;
for (int i = 0; i < len; i++) {
pre = mergeTwoLists(pre, lists[i]);
}
return pre;
}
private ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode p1 = list1;
ListNode p2 = list2;
ListNode pre = new ListNode();
ListNode p = pre;
while (p1 != null && p2 != null) {
if (p1.val < p2.val) {
p.next = p1;
p = p1;
p1 = p1.next;
} else {
p.next = p2;
p = p2;
p2 = p2.next;
}
}
if (p1 != null) {
p.next = p1;
}
if (p2 != null) {
p.next = p2;
}
return pre.next;
}
}
146. LRU 缓存
输入:
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出:
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释:
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
思路:参考灵神的思路,想象有一摞书。
get:时将一本书(key) 抽出来,放在最上面。
put:放入一本新书,如果已经有这本书(key),把他抽出来放在最上面,并替换它的 value。如果没有这本书(key),就放在最上面。如果超出了 capacity 本书,就把最下面的书移除。
题目要求 get 和 put 都是 O(1) 的时间复杂度,因此考虑双向链表实现。
class LRUCache {
class Node {
int key, value;
Node prev, next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
Map<Integer, Node> map;
Node dummy;
int capacity;
public LRUCache(int capacity) {
map = new HashMap<>();
dummy = new Node(0, 0); // 头结点
this.capacity = capacity;
dummy.next = dummy;
dummy.prev = dummy;
}
public int get(int key) {
Node node = getNode(key);
return node != null ? node.value : -1;
}
public void put(int key, int value) {
Node node = getNode(key);
if (node != null) {
node.value = value; // 如果存在,则在getRoot方法里面已经放到了头部
return;
}
node = new Node(key, value);
map.put(key, node);
pushFirst(node); // 放在链表头部
if (map.size() > capacity) {
map.remove(dummy.prev.key);
remove(dummy.prev);
}
}
private Node getNode(int key) {
if (!map.containsKey(key)) {
return null;
}
Node node = map.get(key);
remove(node); // 删除旧节点
pushFirst(node); // 将新节点加到链表头部
return node;
}
private void pushFirst(Node node) {
node.next = dummy.next;
node.prev = dummy;
dummy.next.prev = node;
dummy.next = node;
}
private void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/