diff --git a/TwoPointers/01-introduction.md b/TwoPointers/01-introduction.md new file mode 100644 index 0000000..010b18c --- /dev/null +++ b/TwoPointers/01-introduction.md @@ -0,0 +1,42 @@ +# 双指针 + +作者:Junyi Li;审核: + +与其说是算法,双指针是一种比较好用的工具,没有空间消耗,用好的话可以解决遇到的很多问题。同时经典的快速排序,归并排序,或者字符串翻转经常会用到它。 + +此外,链表相关的题目由于不像数组那样可以直接访问到每一个节点,而且题目经常要求不使用外部空间来处理,在这种情况下,就经常性需要使用双指针啦~ + +# 分类 + +1. 背向双指针:就是两个指针朝着相反的方向走 + + Longest Palindromic Substring 的中心线枚举算法 + +2. 相向双指针:两个指针面对面走 + Two Sum 的一大类题(两位数的相关变形题) + Partition 的一大类题(quicksort,quickselect,partition) + +3. 同向双指针:两个指针朝着一个方向走 + 滑动窗口类 Sliding Window + 快慢指针类 Fast & Slow Pointers + +# 模版 + +针对以上的三种类型,都给出了模版。可是就像weiwei大佬说的[link](https://ojeveryday.github.io/AlgoWiki/#/BinarySearch/01-introduction?id=%e4%ba%8c%e5%88%86%e6%9f%a5%e6%89%be%e7%9a%84%e4%b8%89%e4%b8%aa%e6%a8%a1%e6%9d%bf),仅供参考哈。模版的意义在于可以帮助减少对某些边界条件的考虑以及对于某些问题的多想,节约脑细胞。 + +一般来说我们都需要用到两个变量,分别表示两个idx,一般是数组或者字符串中的idx。 + +!同时要注意的是,因为双指针是一种比较基础的操作,所以很容易有一些小变种,归并排序应该就算是一个简单的变种了吧。大家掌握基础,灵活使用。 + + + +NOTE: + +滑动窗口应该算是双指针的一种应用,同时还可以用queue来实现。 + +双指针的题目的时间复杂度都是一般On + + + + + diff --git "a/TwoPointers/02-\345\220\214\345\220\221\345\217\214\346\214\207\351\222\210.md" "b/TwoPointers/02-\345\220\214\345\220\221\345\217\214\346\214\207\351\222\210.md" new file mode 100644 index 0000000..d6ab8c2 --- /dev/null +++ "b/TwoPointers/02-\345\220\214\345\220\221\345\217\214\346\214\207\351\222\210.md" @@ -0,0 +1,325 @@ ++ # 同向双指针 + + ## 快慢指针类型: + + 快慢指针类型的题目只要想到这个思路就比较好理解。其难点一般是边界条件的判断。建议使用case来分析一下。一般我们输入测试的时候是空,1个节点,2个节点,3个节点,4个节点,5个节点。一般来说就差不多了。 + + #### [链表的中间结点](https://leetcode-cn.com/problems/middle-of-the-linked-list/) + + ```python + # Definition for singly-linked list. + # class ListNode: + # def __init__(self, x): + # self.val = x + # self.next = None + + class Solution: + def middleNode(self, head: ListNode) -> ListNode: + if not head: + return None + if not head.next: + return head + if not head.next.next: + return head.next + + slow = head + quick = head + + while quick.next: + slow = slow.next + quick = quick.next + if not quick.next: + return slow + quick = quick.next + if not quick.next: + return slow + + return slow + ``` + + + + #### [移动零](https://leetcode-cn.com/problems/move-zeroes/submissions/) + + 这道题目有个小点要注意一下,我们将右边不为0的移动到左指针指着的位置,当且仅当这个位置和right不一致。因为这样可以避免重复写这个操作。最后再遍历left指针到最后,将值变为0。 + + ```python + class Solution: + def moveZeroes(self, nums: List[int]) -> None: + """ + Do not return anything, modify nums in-place instead. + """ + right = left = 0 + # for left in range(len(nums)): + while right < len(nums): + if nums[right] != 0: + if right != left: + nums[left] = nums[right] + left += 1 + right += 1 + + while left < right: + if nums[left] != 0: + nums[left] = 0 + left += 1 + ``` + + + + ## 普通的同向双指针 + + 只要满足两个指针,都只能向一个方向走,就可以用同向双指针的方法来做 + + - 同向双指针 + - 每次删除左指针左边的数字 + - 只要当前和小于s,右指针继续向右移动 + - 时间复杂度O(N) + + ```python + 特殊case + + for left in range(len(nums)): + # while left in len(nums) - 1: # 用这个要记得给left做变化,容易忘记。但是这个对left的操作可以更加灵活 + while right int: + + """ + 给你字符串 s 和整数 k 。 + + 请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。 + + 英文中的 元音字母 为(a, e, i, o, u)。 + """ + + if not s: + return 0 + + letters = {"a", "e", "i", "o", "u"} + right = 0 + res = 0 + cnt = 0 + for left in range(len(s)): + while right < len(s) and right - left + 1 <= k: + if s[right] in letters: + cnt += 1 + right += 1 + res = max(res, cnt) + if s[left] in letters: + cnt -= 1 + + return res + + ``` + + + + #### [最长无重复字符的子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) + + ```python + class Solution: + """ + @param s: a string + @return: an integer + """ + def lengthOfLongestSubstring(self, s): + # write your code here + """ + 首先 所有这种 类似于在判断 最长或者最短的 符合某个条件的 子串 都可以使用双指针的方法来解决 + 其难点或者说trick点在于怎么判断满足题目要求的那个条件 + 1. 首先是bad case和edge的判断 + 2. 其次要考虑到不一定是字母的输入,所以使用数组进行处理的时候其实没有字典通用 + 3. 本身副指针负责添加数据,主指针负责删除数据。谨记这一点 + 4. 边界条件一定要划分清楚,基本使用几种边界case测试一下就知道了 + """ + if len(s)<=2: + return len(s) + left = 0 + right = 0 + res = 1 + + from collections import defaultdict + judge = defaultdict(int) + # "abcabcbb" + # print(ord(s[0])-97) + + for left in range(len(s)): + # + while right < len(s) and judge[s[right]]==0: + judge[s[right]]=1 + right+=1 # 切记 + # if judge[ord(s[right])-97]==0 + res = max(res, right-left) + # print(res) + judge[s[left]]=0 + + return res + ``` + + #### [和大于S的最小子数组](https://www.lintcode.com/problem/minimum-size-subarray-sum/description) + + ```python + class Solution: + """ + @param nums: an array of integers + @param s: An integer + @return: an integer representing the minimum size of subarray + """ + def minimumSize(self, nums, s): + # write your code here + """ + 是一个同向双指针的题目。 + 因为他需要使用的其实是两个边界来框定几个数字,然后比较这个数字和给定正整数s的大小。 + + 第二个问题是如何确定这个框内的和,求解的过程,我们肯定不能每次都求和一次。 + 我们使用一个临时值保存结果,进行结果的判断。如果不满足当前条件,那么右指针就不停右移。 + 一旦满足,就判断当前结果是否需要满足。更换左指针进行移动。不断的判断当前的结果是否依然满足条件,满足的话就进行判断。 + 注意 右边的负责添加数据,左边的负责删除数据。牢记 + """ + if not nums: + return -1 + res = float("inf") + left = 0 + right = 0 + tmp = 0 + for left in range(len(nums)): + while tmp =s: + res=min(res, right-left) + tmp-=nums[left] + + return res if res!=float("inf") else -1 + ``` + + #### [最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/) + + 给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。 + + 示例: + + 输入: S = "ADOBECODEBANC", T = "ABC" + 输出: "BANC" + 说明: + + 如果 S 中不存这样的子串,则返回空字符串 ""。 + 如果 S 中存在这样的子串,我们保证它是唯一的答案。 + + + + ```python + class Solution: + """ + @param source : A string + @param target: A string + @return: A string denote the minimum window, return "" if there is no such a string + """ + def minWindow(self, source , target): + # write your code here + """ + 所有涉及到求字符串 的符合某个条件的 子串,所以对应的 长度等 都可以使用双指针 + 考点依然是如何判断是否满足题目要求的那个条件 + 1. 以及对应的边界条件如何进行判断的问题 + 2. 如何判断是否满足条件呢?可以使用字典进行,因为我们不能确定包含的内容是否只有字母 + 3. 我认为可以使用一个flag来进行判断看是否当前的值满足要求 + """ + + if not source or not target: + return "" + from collections import defaultdict + dict = defaultdict(int) + for i in target: + dict[i] += 1 + + dict2 = defaultdict(int) + right = 0 + res = float("inf"), source + flag = 0 + flag_t = len(dict) + for left in range(len(source)): + while right < len(source) and flag != flag_t: + cur = source[right] + if cur in dict: + dict2[cur]+=1 + if dict2[cur]==dict[cur]: flag+=1 + right+=1 + if flag==flag_t: + if right-leftres[0]: + res = right-left, s[left:right] + + judge[s[left]]-=1 + if judge[s[left]]==0: + del judge[s[left]] + # print(res) + return res[0] + ``` diff --git "a/TwoPointers/03-\347\233\270\345\220\221\345\217\214\346\214\207\351\222\210.md" "b/TwoPointers/03-\347\233\270\345\220\221\345\217\214\346\214\207\351\222\210.md" new file mode 100644 index 0000000..a16bb4b --- /dev/null +++ "b/TwoPointers/03-\347\233\270\345\220\221\345\217\214\346\214\207\351\222\210.md" @@ -0,0 +1,152 @@ ++ # 相向双指针 + + 相向双指针主要是指两个指针面对面走,常见需要掌握的题目一般有下列: + + 1. quicksort + 2. 归并排序 + 3. 第k大的数字 + 4. 有效回文串 + 5. Two/Three Sum + 6. 判断有效三角形个数 + 7. Reverse 类 + + + + NOTE:基于比较的排序算法,做不到On,理论下界是Onlogn + + 模版例题 + + #### [611. 有效三角形的个数](https://leetcode-cn.com/problems/valid-triangle-number/) + + ``` + class Solution: + def triangleNumber(self, nums: List[int]) -> int: + if not nums: + return 0 + nums.sort() + ans = 0 + S = nums + for i in range(2, len(S)): + # for 循环中对每个i都做一次相向双指针的判断 + left, right = 0, i - 1 + while left < right: + if S[left] + S[right] > S[i]: + ans += right - left + right -= 1 + else: + left += 1 + return ans + ``` + + #### [125. 验证回文串](https://leetcode-cn.com/problems/valid-palindrome/) + + 给定一个字符串,判断其是否为一个回文串。只考虑字母和数字,忽略大小写。 + + ```python + class Solution: + def isPalindrome(self, s: str) -> bool: + # write your code here + if not s: + return True + + left = 0 + right = len(s) - 1 + # 相向双指针 + while left <= right: + while left <= right and not s[left].isalnum(): + left += 1 + while left <= right and not s[right].isalnum(): + right -= 1 + if left <= right: + if s[left].lower() != s[right].lower(): + return False + left += 1 + right -= 1 + + return True + ``` + + + + 快速排序: + + 快速排序中间的比较,不要把等于pivot包括进去,可以加快速度。但是也不能漏掉left<=right** 之所以不要等于,就是为了避免都是一堆相等的数字的情况,做了无用功。 + + 因为快速排序的本质是将数组分成两个部分,左边大于,右边小于,等于的无所谓在哪边。 + + 如果包含了等于就会有下面的情况出现。不包含就可以一轮推出了 + + [1,1,1,1,1] ==> [1,1,1,1,1] [] + + quicksort比归并排序要快,因为是inplace的,减少了开辟数组空间以及复制数据的时间损耗 + + + + ```python + # Quick Sort + class Solution: + def sortIntegers(self, A): + if A == None or len(A) == 0: + return + self.quickSort(A, 0, len(A) - 1) + + def quickSort(self, A, left, right): + if start >= end: + return + + ### partition 让所有小于pivot的数字在左边,大于pivot的数字在右边 + l = left + r = right + pivot = A[(start+end)//2] # 1. avoid the wroest situation + + while left <= right: # 2. <= not < 是为了避免漏掉数据 + # 3. = pivot的时候可以在做也可以在右。为了避免使重复数据存在的时候,会出现极端情况。=可以避免不均匀的划分 + while left <= right and A[left] < pivot: + left += 1 + while left <= right and A[right] > pivot: + right -= 1 + + if left <= right: + A[left], A[right] = A[right], A[left] + left += 1 + right -= 1 + ### partition的结束 + # 结束的时候,left和right会错位。left > right + self.quickSort(A, l, right) + self.quickSort(A, left, r) + ``` + + #### [11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/) + + ```python + class Solution: + def maxArea(self, height: List[int]) -> int: + """ + ok. 这个题目嘛。做过好多次还是会忘记。总之应该先找到切入点 + 首先说了n至少为2.那么就不用做非空判断. + 看一下这个图,左边和右边的比较短的那个柱子。会决定水的高。距离决定水的width。 + 那么就有了结果了。遍历一下。 + """ + res = 0 + left = 0 + right = len(height) - 1 + while left <= right: + lmax = height[left] + rmax = height[right] + h = min(lmax, rmax) + width = right - left + res = max(res, width * h) + if lmax < rmax: + left += 1 + else: + right -= 1 + return res + ``` + + + + + + + + \ No newline at end of file diff --git "a/TwoPointers/04-\350\203\214\345\220\221\345\217\214\346\214\207\351\222\210.md" "b/TwoPointers/04-\350\203\214\345\220\221\345\217\214\346\214\207\351\222\210.md" new file mode 100644 index 0000000..267a484 --- /dev/null +++ "b/TwoPointers/04-\350\203\214\345\220\221\345\217\214\346\214\207\351\222\210.md" @@ -0,0 +1,62 @@ +# 背向双指针 + +一般我们很少会考这个题目。不过我们拿一道例题来说明一下这个怎么做,会在很多题目中用到。 + +在一个排序数组中找到k个最接近target的题目【二分和双指针的应用】 + +#### [658. 找到 K 个最接近的元素](https://leetcode-cn.com/problems/find-k-closest-elements/) + +```python +class Solution: + def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]: + if len(arr) < k: + return None + if len(arr) == k: + return arr + + first_big_than_x = self.find_x(arr, x) + + right = first_big_than_x + left = right - 1 + l_res = [] + r_res = [] + for _ in range(k): + if left < 0: + r_res.append(arr[right]) + right += 1 + elif right >= len(arr): + l_res.append(arr[left]) + left -= 1 + elif self.is_left(arr, left, right, x): + l_res.append(arr[left]) + left -= 1 + else: + r_res.append(arr[right]) + right += 1 + return l_res[::-1] + r_res + + + def is_left(self, arr, left, right, x): + if abs(arr[left] - x) > abs(arr[right] - x): + return False + return True + + def find_x(self, arr, x): + left = 0 + right = len(arr) - 1 + while left + 1 < right: + mid = (left + right) // 2 + if arr[mid] == x: + return mid + elif arr[mid] > x: + right = mid + else: + left = mid + + if arr[left] > x: + return left + if arr[right] > x: + return right + return len(arr) +``` + diff --git a/TwoPointers/05-practices.md b/TwoPointers/05-practices.md new file mode 100644 index 0000000..cf7a8ea --- /dev/null +++ b/TwoPointers/05-practices.md @@ -0,0 +1,22 @@ +# 练习 + +## 题型 1:同向双指针 + +#### [19. 删除链表的倒数第N个节点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) + +#### [209. 长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/) + + + +## 题型 2:相向双指针 + +#### [125. 验证回文串](https://leetcode-cn.com/problems/valid-palindrome/) + +#### [215. 数组中的第K个最大元素](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/) + +#### [15. 三数之和](https://leetcode-cn.com/problems/3sum/) + +## 题型 3:背向双指针 + +#### [5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/) + diff --git a/TwoPointers/README.md b/TwoPointers/README.md index e69de29..af274f9 100644 --- a/TwoPointers/README.md +++ b/TwoPointers/README.md @@ -0,0 +1,3 @@ +本题解仿照weiwei的结构和写法,做了一点修改。 + +自己太拖延了 sry \ No newline at end of file