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,这一列下面的元素都大于targety--;} else if (matrix[x][y] < target) { // 当前元素小于target,这一行前面的元素都小于targetx++;} 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,这一行后面的元素都大于targetx--;} else if (matrix[x][y] < target) { // 当前元素小于target,这一列上面的元素都小于targety++;} 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);*/