最近在自己编写一些小的算法的时候,深感自己的算法过于臃肿。碰巧Datawhale在新的一期组队学习中组织了数据结构与算法的课程学习。于是就参加了,再次感谢Datawhale~~
首先跟大家分享一下两个自己感觉比较好的学习资料,一个是 算法通关手册 ,也是Datawhale在本次组队学习中的学习资料;一个是B站上的视频 【北京大学】数据结构与算法Python版(完整版),老师讲的特别棒(也难得有Python版的数据结构课程,哈哈~)。
需要指出的是:本次博客的内容更像是对上述两个资料做的笔记,很多都是资料上的原内容,并非原创。
双指针(Two Pointers):指的是在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为对撞时针。如果两个指针方向相同,则称为快慢指针。如果两个指针分别属于不同的数组 / 链表,则称为分离双指针。在数组的区间问题上,暴力算法的时间复杂度往往是 O ( n 2 ) O(n^2) O(n2)。而双指针利用了区间「单调性」的性质,从而将时间复杂度降到了 O ( n ) O(n) O(n)。
对撞指针算法中有一个left指针,有一个right指针。left、right 分别指向序列第一个元素和最后一个元素,然后 left 指针不断递增,right 不断递减,直到两个指针的值相撞(即 left == right),或者满足其他要求的特殊条件为止。
例题1
给定一个升序排列的整数数组:numbers 和一个目标值 target。
从数组中找出满足相加之和等于 target 的两个数,并返回两个数在数组 中下的标值。
使用两个指针 left,right。left 指向数组第一个值最小的元素位置,right 指向数组值最大元素位置。判断两个位置上的元素的和与目标值的关系。如果元素和等于目标值,则返回两个元素位置。如果元素和大于目标值,则让 right 左移,继续检测。如果元素和小于目标值,则让 left 右移,继续检测。直到 left 和 right 移动到相同位置停止检测。如果最终仍没找到,则返回 [-1, -1]。
代码:
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
left = 0
right = len(numbers) - 1
while left < right:
total = numbers[left] + numbers[right]
if total == target:
return [left + 1, right + 1]
elif total < target:
left += 1
else:
right -= 1
return [-1, -1]
例题2
验证回文串。
判断是否为回文串(只考虑字符串中的字母和数字字符,并且忽略字母的大小写)。
使用两个指针 left,right。left 指向字符串开始位置,right 指向字符串结束位置。判断两个指针对应字符是否是字母或数字。 通过 left 右移、right 左移的方式过滤掉字母和数字以外的字符。然后判断 s[start] 是否和 s[end] 相等(注意大小写)。如果相等,则将 left 右移、right 左移,继续进行下一次过滤和判断。如果不相等,则说明不是回文串,直接返回 False。如果遇到 left == right,跳出循环,则说明该字符串是回文串,返回 True。
代码:
class Solution:
def isPalindrome(self, s: str) -> bool:
left = 0
right = len(s) - 1
while left < right:
if not s[left].isalnum():
left += 1
continue
if not s[right].isalnum():
right -= 1
continue
if s[left].lower() == s[right].lower():
left += 1
right -= 1
else:
return False
return True
例题3
给定 n 个非负整数 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an,每个数代表坐标中的一个点 ( i , a i ) (i,a_i) (i,ai)。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 ( i , a i ) (i,a_i) (i,ai) 和 ( i , 0 ) (i,0) (i,0)。
找出其中的两条垂直线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
使用两个指针 left,right。left 指向数组开始位置,right 指向数组结束位置。计算 left 和 right 所构成的面积值,同时维护更新最大面积值。判断 left 和 right 的高度值大小。如果 left 指向的直线高度比较低,则将 left 指针右移。如果 right 指向的直线高度比较低,则将 right 指针左移。如果遇到 left == right,跳出循环,最后返回最大的面积。
代码:
class Solution:
def maxArea(self, height: List[int]) -> int:
left = 0
right = len(height) - 1
ans = 0
while left < right:
area = min(height[left], height[right]) * (right-left)
ans = max(ans, area)
if height[left] < height[right]:
left += 1
else:
right -= 1
return ans
快慢指针指的是两个指针从同一侧开始遍历序列,且移动的步长一个快一个慢。移动快的指针被称为 「快指针(fast)」,移动慢的指针被称为「慢指针(slow)」。两个指针以不同速度、不同策略移动,直到快指针移动到数组尾端,或者两指针相交,或者满足其他特殊条件时为止。
例题1
题目
删除有序数组中的重复项。
要求
删除数组 nums 中的重复元素,使每个元素只出现一次。并输出去除重复元素之后数组的长度。
思路
定义两个快慢指针 slow,fast。其中 slow 指向去除重复元素后的数组的末尾位置。fast 指向当前元素。令 slow 在后, fast 在前。令 slow = 0,fast = 1。比较 slow 位置上元素值和 fast 位置上元素值是否相等。如果不相等,则将 slow 后移一位,将 fast 指向位置的元素复制到 slow 位置上。将 fast 右移 1 位。
代码:
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if len(nums) <= 1:
return len(nums)
slow, fast = 0, 1
while (fast < len(nums)):
if nums[slow] != nums[fast]:
slow += 1
nums[slow] = nums[fast]
fast += 1
return slow + 1
分离双指针:两个指针分别属于不同的数组 / 链表,两个指针分别在两个数组 / 链表中移动。
例题1
题目
两个数组的交集。
要求
编写一个函数来计算它们的交集。重复元素只计算一次。
思路
对数组 nums1、nums2 先排序。使用两个指针 left_1、left_2。left_1 指向第一个数组的第一个元素,即:left_1 = 0,left_2 指向第二个数组的第一个元素,即:left_2 = 0。如果 nums1[left_1] 等于 nums2[left_2],则将其加入答案数组(注意去重),并将 left_1 和 left_2 右移。
代码:
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
nums1.sort()
nums2.sort()
left_1 = 0
left_2 = 0
res = []
while left_1 < len(nums1) and left_2 < len(nums2):
if nums1[left_1] == nums2[left_2]:
if nums1[left_1] not in res:
res.append(nums1[left_1])
left_1 += 1
left_2 += 1
elif nums1[left_1] < nums2[left_2]:
left_1 += 1
elif nums1[left_1] > nums2[left_2]:
left_2 += 1
return res
滑动窗口(Sliding Window):在给定数组 / 字符串上维护一个固定长度或不定长度的窗口。可以对窗口进行滑动操作、缩放操作,以及维护最优解操作。
固定长度的滑动窗口的窗宽是不变的。
例题1
大小为 K 且平均值大于等于阈值的子数组数目。
返回长度为 k 且平均值大于等于 threshold 的子数组数目。
ans 用来维护答案数目。window_sum 用来维护窗口中元素的和。
left 、right 都指向序列的第一个元素,即:left = 0,right = 0。当窗口元素个数不足 k 个时,不断移动 right,先将 k 个元素填入窗口中。当窗口元素个数为 k 时,即:right - left + 1 >= k 时,判断窗口内的元素和平均值是否大于等于阈值。如果满足,则答案数目 + 1。然后向右移动 left,从而缩小窗口长度,即 left += 1,使得窗口大小始终保持为 k。向右移动 right,将元素填入窗口中。重复 3 ~ 5 步,直到 right 到达数组末尾。
class Solution:
def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int:
left = 0
right = 0
window_sum = 0
ans = 0
while right < len(arr):
window_sum += arr[right]
if right - left + 1 >= k:
if window_sum >= k * threshold:
ans += 1
window_sum -= arr[left]
left += 1
right += 1
return ans
不定长度的滑动窗口的窗宽是变动的。
例题1
无重复字符的最长子串 。
找出其中不含有重复字符的 最长子串 的长度。
一开始,left、right 都指向 0。将最右侧字符 s[right] 加入当前窗口 window 中,记录该字符个数。如果该窗口中该字符的个数多于 1 个,即 window[s[right]] > 1,则不断右移 left,缩小滑动窗口长度,并更新窗口中对应字符的个数,直到 window[s[right]] <= 1。维护更新无重复字符的最长子串长度。然后右移 right,直到 right >= len(nums) 结束。输出无重复字符的最长子串长度。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
left = 0
right = 0
window = dict()
ans = 0
while right < len(s):
if s[right] not in window:
window[s[right]] = 1
else:
window[s[right]] += 1
while window[s[right]] > 1:
window[s[left]] -= 1
left += 1
ans = max(ans, right - left + 1)
right += 1
return ans
例题2
长度最小的子数组。
给定一个只包含正整数的数组 nums 和一个正整数 target。找出数组中满足和大于等于 target 的长度最小的「连续子数组」,并返回其长度。如果不存在符合条件的子数组,返回 0。
一开始,left、right 都指向 0。向右移动 right,将最右侧元素加入当前窗口和 window_sum 中。如果 window_sum >= target,则不断右移 left,缩小滑动窗口长度,并更新窗口和的最小值,直到 window_sum < target。然后继续右移 right,直到 right >= len(nums) 结束。输出窗口和的最小值作为答案。
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
size = len(nums)
ans = size + 1
left = 0
right = 0
window_sum = 0
while right < size:
window_sum += nums[right]
while window_sum >= target:
ans = min(ans, right - left + 1)
window_sum -= nums[left]
left += 1
right += 1
return ans if ans != size + 1 else 0
例题3
乘积小于K的子数组。
给定一个正整数数组 nums和整数 k 。找出该数组内乘积小于 k 的连续的子数组的个数。
一开始,left、right 都指向 0。将最右侧元素加入当前子数组乘积 window_product 中。如果 window_product >= k ,则不断右移 left,缩小滑动窗口长度,并更新当前乘积值 window_product 直到 window_product < k。记录累积答案个数 += 1,向右移动 right,直到 right >= len(nums) 结束。输出累积答案个数。
class Solution:
def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
if k <= 1:
return 0
size = len(nums)
left = 0
right = 0
window_product = 1
count = 0
while right < size:
window_product *= nums[right]
while window_product >= k:
window_product /= nums[left]
left += 1
count += (right - left + 1)
right += 1
return count