diff --git a/notes/Leetcode 题解 - 双指针.md b/notes/Leetcode 题解 - 双指针.md index 51f04528..9ce627d1 100644 --- a/notes/Leetcode 题解 - 双指针.md +++ b/notes/Leetcode 题解 - 双指针.md @@ -11,6 +11,8 @@ * [8. 三数之和](#8-三数之和) * [9. 接雨水](#9-接雨水) * [10. 最小覆盖子串](#10-最小覆盖子串) + * [11. 无重复字符的最长子串](#11-无重复字符的最长子串) + * [参考资料](#参考资料) @@ -35,6 +37,8 @@ Output: index1=1, index2=2 - 如果 sum \> target,移动较大的元素,使 sum 变小一些; - 如果 sum \< target,移动较小的元素,使 sum 变大一些。 +这样移动不会错过答案:sum \< target 时,numbers[i] 与 i、j 之间任何元素的和都不超过 sum,i 不可能属于答案;sum \> target 时同理排除 j。 + 数组中的元素最多遍历一次,时间复杂度为 O(N)。只使用了两个额外变量,空间复杂度为 O(1)。

@@ -71,11 +75,9 @@ Explanation: 1 * 1 + 2 * 2 = 5 题目描述:判断一个非负整数是否为两个整数的平方和。 -可以看成是在元素为 0\~target 的有序数组中查找两个数,使得这两个数的平方和为 target,如果能找到,则返回 true,表示 target 是两个整数的平方和。 +可以看成是在元素为 0\~target 的有序数组中查找两个数,使得这两个数的平方和为 target,做法与[有序数组的 Two Sum](#1-有序数组的-two-sum)相同,只是把和换成了平方和。 -本题和 167\. Two Sum II - Input array is sorted 类似,只有一个明显区别:一个是和为 target,一个是平方和为 target。本题同样可以使用双指针得到两个数,使其平方和为 target。 - -本题的关键是右指针的初始化,实现剪枝,从而降低时间复杂度。设右指针为 x,左指针固定为 0,为了使 02 + x2 的值尽可能接近 target,可以将 x 取为 sqrt(target)。 +本题的关键是右指针的初始化,实现剪枝,从而降低时间复杂度。两个数都不会超过 sqrt(target),因此右指针从 sqrt(target) 开始即可。 因为最多只需要遍历一次 0\~sqrt(target),所以时间复杂度为 O(sqrt(target))。又因为只使用了两个额外的变量,因此空间复杂度为 O(1)。 @@ -113,8 +115,8 @@ Given s = "leetcode", return "leotcede". 为了快速判断一个字符是不是元音字符,将全部元音字符添加到集合 HashSet 中,从而以 O(1) 的时间复杂度进行该操作。 -- 时间复杂度为 O(N):只需要遍历所有元素一次 -- 空间复杂度 O(N):Java 的字符串不可修改,需要一个与 s 等长的字符数组保存结果 +- 时间复杂度为 O(N):只需要遍历所有元素一次; +- 空间复杂度为 O(N):Java 的字符串不可修改,需要一个与 s 等长的字符数组保存结果。

@@ -230,7 +232,9 @@ public void merge(int[] nums1, int m, int[] nums2, int n) { [Leetcode](https://leetcode.com/problems/linked-list-cycle/description/) / [力扣](https://leetcode-cn.com/problems/linked-list-cycle/description/) -使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。 +使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。这是因为进入环之后,快指针每次比慢指针多走一步,两者的距离每次缩小 1,不会跳过慢指针。 + +求环的入口时,快慢指针都要从头节点出发,做法见[剑指 Offer 23](23.%20链表中环的入口结点.md)。 ```java public boolean hasCycle(ListNode head) { @@ -267,6 +271,8 @@ Output: 通过删除字符串 s 中的一些字符能得到字符串 t,可以认为 t 是 s 的子序列。使用双指针可以判断一个字符串是否为另一个字符串的子序列。 +遍历 d 时,比当前答案短、或者一样长但字典序更大的字符串直接跳过。 + ```java public String findLongestWord(String s, List d) { String longestWord = ""; @@ -275,14 +281,14 @@ public String findLongestWord(String s, List d) { if (l1 > l2 || (l1 == l2 && longestWord.compareTo(target) < 0)) { continue; } - if (isSubstr(s, target)) { + if (isSubsequence(s, target)) { longestWord = target; } } return longestWord; } -private boolean isSubstr(String s, String target) { +private boolean isSubsequence(String s, String target) { int i = 0, j = 0; while (i < s.length() && j < target.length()) { if (s.charAt(i) == target.charAt(j)) { @@ -307,13 +313,18 @@ Output: [[-1, -1, 2], [-1, 0, 1]] 题目描述:找出所有和为 0 且不重复的三元组。 -先排序,然后枚举第一个数 nums[i],在它右边用第 1 题的双指针方法寻找和为 -nums[i] 的两个数。去重的关键是跳过相同的值:i 跳过与前一个数相同的值,找到一组解之后,两个指针也跳过相同的值。时间复杂度为 O(N2)。 +先排序,然后枚举第一个数 nums[i],在它右边用[有序数组的 Two Sum](#1-有序数组的-two-sum)的双指针方法寻找和为 -nums[i] 的两个数。去重的关键是跳过相同的值:i 跳过与前一个数相同的值,找到一组解之后,两个指针也跳过相同的值。nums[i] 大于 0 时三个数都大于 0,可以直接结束。时间复杂度为 O(N2)。以 [-2, -2, 0, 0, 2, 2] 为例: + +

```java public List> threeSum(int[] nums) { Arrays.sort(nums); List> ret = new ArrayList<>(); for (int i = 0; i < nums.length - 2; i++) { + if (nums[i] > 0) { + break; + } if (i > 0 && nums[i] == nums[i - 1]) { continue; } @@ -354,7 +365,7 @@ Output: 6 位置 i 上方能存的水,取决于它左边最高的柱子和右边最高的柱子中较矮的那一个:water[i] = min(leftMax, rightMax) - height[i]。 -用两个指针从两端向中间移动,分别维护 leftMax 和 rightMax。如果 leftMax < rightMax,左指针处的水位只由 leftMax 决定,因为右边已经有一根不低于 rightMax 的柱子,可以直接结算左指针并右移;反之结算右指针并左移。 +用两个指针从两端向中间移动,leftMax、rightMax 分别是 [0, l] 和 [r, n - 1] 中最高的柱子。如果 leftMax < rightMax,左指针右边最高的柱子不低于 rightMax,水位只由 leftMax 决定,可以直接结算左指针并右移;反之结算右指针并左移。不需要额外的数组,空间复杂度为 O(1)。

@@ -390,6 +401,19 @@ Output: "BANC" 使用滑动窗口。need[c] 表示窗口还缺几个字符 c,missing 表示一共还缺几个字符。右指针不断扩大窗口,直到 missing 为 0,此时窗口包含了 t 的所有字符;然后移动左指针缩小窗口,在窗口仍然满足条件时更新答案。 +need[c] 可以小于 0,表示窗口中的 c 比需要的多。因此只有加入前 need[c] 大于 0 的 c 才让 missing 减一,移出后 need[c] 大于 0 时 missing 才加一。例如 s = "AAAB"、t = "AAB": + +```text +操作 窗口 need[A] need[B] missing +初始 2 1 3 +加入 A A 1 1 2 +加入 A AA 0 1 1 +加入 A AAA -1 1 1 多出一个 A,missing 不变 +加入 B AAAB -1 0 0 满足条件,记录长度 4 +移出 A AAB 0 0 0 仍满足条件,记录长度 3 +移出 A AB 1 0 1 又缺一个 A,停止收缩 +``` + ```java public String minWindow(String s, String t) { int[] need = new int[128]; @@ -414,3 +438,38 @@ public String minWindow(String s, String t) { return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen); } ``` + +## 11. 无重复字符的最长子串 + +3\. Longest Substring Without Repeating Characters (Medium) + +[Leetcode](https://leetcode.com/problems/longest-substring-without-repeating-characters/) / [力扣](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) + +```text +Input: s = "pwwkew" +Output: 3 +``` + +题目描述:求不含重复字符的最长子串的长度,字符都在 ASCII 范围内。 + +使用滑动窗口,保证窗口 [l, r] 内没有重复字符。last[c] 是字符 c 上次出现位置的下一位,右指针遇到 c 时,左指针移到 max(l, last[c]):c 上次出现的位置可能已经在窗口左边,左指针不能后退。时间复杂度为 O(N)。 + +同一道题的另一种写法见[剑指 Offer 48](48.%20最长不含重复字符的子字符串.md)。 + +```java +public int lengthOfLongestSubstring(String s) { + int[] last = new int[128]; // 字符上次出现位置的下一位 + int ret = 0; + for (int l = 0, r = 0; r < s.length(); r++) { + char c = s.charAt(r); + l = Math.max(l, last[c]); + ret = Math.max(ret, r - l + 1); + last[c] = r + 1; + } + return ret; +} +``` + +## 参考资料 + +- [LeetCode 题库](https://leetcode.com/problemset/) diff --git a/notes/pics/lc-two-pointers-3sum.png b/notes/pics/lc-two-pointers-3sum.png new file mode 100644 index 00000000..bc1fd771 Binary files /dev/null and b/notes/pics/lc-two-pointers-3sum.png differ diff --git a/notes/pics/trapping-rain-water.png b/notes/pics/trapping-rain-water.png index b6efd64d..3043376d 100644 Binary files a/notes/pics/trapping-rain-water.png and b/notes/pics/trapping-rain-water.png differ