本系列是算法通关手册LeeCode的学习笔记
算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn)
本系列为自用笔记,如有版权问题,请私聊我删除。
目录
一,二分查找(Binary Search Algorithm)
一,二分查找(Binary Search Algorithm)
算法思想:通过确定目标元素所在的区间范围,反复将查找范围减半,直到找到元素或找不到元素为止。
步骤:
初始化:确定有序线性表;
确定查找范围,即左右边界 left, right;
计算中间位置元素 mid = left + (left + right) // 2;
比较中间位置元素与目标元素,确定下一次搜索范围;
重复直到找到 mid 位置或 left > right
代码实现:
def binary2Search(self, nums: [int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif target < nums[mid]:
right = mid -1
else:
left = mid + 1
return -1
上述代码中,使用查找的思路,如果找到了 mid 则立即返回,如果跑完循环也没找到,则返回 -1,每次查找的搜索区间也不相同,因为已经对 mid 位置元素进行判断,所以可以排除 mid 位置的元素,缩小查找区间。
间接法代码实现:
class Solution:
def binarySearch(self, nums: [int], target: int) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if target > nums[mid]:
left = mid + 1
else:
right = mid
return left if nums[left] == target else -1
与图中的过程不同。上述是二分查找的排除法思路,可以看到在运行过程中,我们在不断排除没有目标元素的区间,从而达到不断减少搜索范围的目的,直到 left = right 时才退出循环,判断该位置元素是否与目标元素相等。
代码中采用 mid = left + (right - left) // 2 的方式,是为了避免整形溢出。
特别的:
array = [1, 2, 2, 2, 2, 3, 4, 5, 5]
def binary1(nums, t):
left = 0
right = len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] < t:
left = mid + 1
else:
right = mid
return left
def binary2(nums, t):
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left + 1) // 2
if nums[mid] > t:
right = mid - 1
else:
left = mid
return left
print(binary1(array, 2))
print(binary2(array, 2))
# 1
# 4
在binary1中 ,若array长度为 6 ,则 left = 0, right = 5
mid = 0 + (5 - 0) // 2 = 2
mid 位置为中间两个元素靠左的那个
如果当前位置的元素 nums[mid] < t 则目标位置一定在当前位置的右侧,当前位置已经
检查过了, left = mid + 1。
如果当前位置的元素nums[mid] >= t ,则移动右边界
因为左边界是从 0 位置开始的,所以这样可以保证 left 指向目标元素第一次出现的位置
如果在等于时也移动左边界,那么 right 会一直停在大于目标元素的位置。
同理在binary2中,left = 0, right = 5
mid = 0 + (5 - 0 + 1) // 2 = 3
mid 位置为中间两个元素靠右的那个
如果当前位置的元素 nums[mid] > t 则目标位置一定在当前位置的左侧,当前位置已经
检查过了, right = mid - 1。
如果当前位置的元素nums[mid] <= t,则移动左边界
因为右边界是为最右侧开始的,这样可以保证 left 指向目标元素最后一次出现的位置。
可见例题
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
二分查找例题
对于旋转数组问题,取得中间位置元素后,有两种情况
第一种:
nums[mid] > nums[left]
此时若 target < nums[left] 或 target > nums[mid]
则不在左侧, left = mid + 1
否则 right = mid
第二种:
nums[mid] < nums[left]
此时若 target < nums[mid] 或 target > nums[right]
则不在右侧,right = mid
否则 left = mid + 1
通过代码如下:
class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
if nums[mid] >= nums[left]:
if nums[mid] < target or target < nums[left]:
left = mid + 1
else:
right = mid
else:
if target > nums[right] or target < nums[mid]:
right = mid
else:
left = mid + 1
if nums[left] == target:
return left
return -1
2,278. 第一个错误的版本 - 力扣(LeetCode)
第二题是一道经典的二分查找
# The isBadVersion API is already defined for you.
# def isBadVersion(version: int) -> bool:
class Solution:
def firstBadVersion(self, n: int) -> int:
left = 0
right = n
while left < right:
mid = left + (right - left) // 2
if not isBadVersion(mid):
left = mid + 1
else:
right = mid
return left
3,852. 山脉数组的峰顶索引 - 力扣(LeetCode)
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
def binarySearch(nums, target: int) -> bool:
print(nums)
left = -1
right = len(nums)
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid
else:
right = mid
return nums[right] == target
for length in matrix:
print(length)
print(length[0], length[-1])
# 如果target 在该行范围内,二分搜索
if length[0] <= target and length[-1] >= target:
return binarySearch(length, target)
return False
二分查找总结:
二分查找的思想非常简单,但在使用时会遇到各种各样的问题,最容易遇到的问题时陷入死循环或是对边界的处理,在这里我强烈推荐up的视频
而在确保语法正确的前提下,二分查找的难点是 大于号、小于号的判断条件。
二,双指针(Two Pointers)
在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。
双指针又分为对撞指针、快慢指针、分离指针。
1. 对撞指针
两个指针 left 、right 分别指向序列的第一个元素和最后一个元素,然后 left 指针不断递增,right 指针不断递减,直到两个指针的值相撞,或满足其他条件为止。
步骤:
left = 0 左指针指向第一个元素, right = len(nums) - 1 右指针指向最后一个元素;
满足条件时 left +=1 或 right -= 1;
直到两指针相撞或其他条件时,跳出循环。
伪代码模板:
left, right = 0, len(nums) - 1
while left < right:
if 满足条件:
return 符合要求的答案
elif 条件1:
left += 1
elif 条件2:
right -= 1
return 无满足条件的值
例题:
1. 167. 两数之和 II - 输入有序数组 - 力扣(LeetCode)
可以使用对撞指针,因为数组是有序的,所以先将 left = 0,指向最小元素
将 right = len(nums) - 1,指向最大元素;
如果两数之和大于目标值,则将右指针减一;
如果两数之和小于目标值,则将左指针加一;
返回结果
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
left = 0
right = len(numbers) - 1
while left < right:
ans = numbers[left] + numbers[right]
if ans == target:
return [left + 1, right + 1]
elif ans < target:
left += 1
else:
right -= 1
return [-1, -1]
可以使用对撞指针,如果当前元素不是字母,则跳过
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
因为盛水的多少取决于较短的板,因此在 left 较低时,移动 left 可能会使容量变大,而移动right 如果新的 right - 1 的值大于 right,则容量取决于 left,若新的 right - 1 的值小于 left,容量变小,因此移动的条件是,指向高度较短时,移动。
class Solution:
def maxArea(self, height: List[int]) -> int:
left = 0
right = len(height) - 1
ans = 0
while left < right:
h = min(height[left], height[right])
v = h * (right - left)
ans = max(ans, v)
if height[left] < height[right]:
left += 1
else:
right -= 1
return ans
2. 快慢指针
两个指针从同一侧开始遍历序列,且移动的步长一个快一个慢,移动快的为 fast,移动慢的为 slow。两个指针以不同速度,不同策略移动,直到快指针移动到数组尾端或两指针相交。
步骤:
slow = 0,慢指针指向序列第一个元素,fast = 1,快指针指向序列第二个元素;
在循环体中依据相应策略移动快慢指针;
满足条件时,跳出循环体。
伪代码模板:
slow = 0
fast = 1
while 没有遍历完:
if 满足条件:
slow += 1
fast += 1
return 结果
例题:
1. 26. 删除有序数组中的重复项 - 力扣(LeetCode) fast 用于检查是否与前一个元素相同,slow 记录修改元素的位置
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if len(nums) == 1:
return 1
slow = 1 # 指向第一个可能被修改的位置
fast = 1
while fast < len(nums):
if nums[fast - 1] != nums[fast]:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
3. 分离双指针
两个指针分别属于不同的数组,两个指针分别在不同的数组中移动
步骤:
使用两个指针,left1 = 0,指向第一个数组起始元素,left2 = 0 指向第二个数组起始元素;
满足一定条件时,两个指针同时右移;
满足某条件1,left1 += 1;
满足某条件2,right += 1;
其中一个数组遍历完或满足条件时退出。
伪代码模板:
left_1 = 0
left_2 = 0
while left_1 < len(nums1) and left_2 < len(nums2):
if 一定条件:
left_1 += 1
left_2 += 1
elif 一定条件1:
left_1 += 1
elif 一定条件2:
left_2 += 1
return 结果
例题:
1. 349. 两个数组的交集 - 力扣(LeetCode)
将两个数组排序后,使用分离指针遍历数组,将符合条件的元素加入到返回数组中
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
res = []
left1 = 0
left2 = 0
nums1.sort()
nums2.sort()
while left1 < len(nums1) and left2 < len(nums2):
if nums1[left1] == nums2[left2]:
if nums1[left1] not in res:
res.append(nums1[left1])
if nums1[left1] < nums2[left2]:
left1 += 1
else:
left2 += 1
return res
双指针总结
双指针分为「对撞指针」、「快慢指针」、「分离双指针」。
对撞指针,两个指针方向相反。适合解决查找有序数组中满足某些约束条件的一组元素问题、字符串反转问题。
快慢指针,两个指针方向相同。适合解决数组中的移动、删除元素问题,或者链表中的判断是否有环、长度问题。
分离指针,两个指针分别属于不同的数组 ,链表。适合解决有序数组合并,求交集、并集问题。
三,滑动窗口(Sliding Window)
在给定数组或字符串上,维护一个窗口,可以对窗口进行滑动操作、缩放操作以及维护最优解操作。
滑动操作,窗口可以按照一定的方向移动。
缩放操作,可以从左侧缩小窗口长度,也可以从右侧。
滑动窗口利用了双指针中快慢指针的技巧,可以将滑动窗口看作是快慢指针中间的区间,也可以看作是快慢指针的一种特殊形式。
滑动窗口分为【固定长度滑动窗口】和【不定长度滑动窗口】
1. 固定长度滑动窗口
在给定数组或字符串上维护一个固定长度的窗口
步骤:
使用两个指针 left,right。初始时同时指向第一个元素 left = 0, right = 0;
当窗口未达到规定大小时,不断移动 right,将元素加入窗口;
达到规定大小后,判断窗口内元素是否满足条件,若满足,维护最优解;
移动 left 缩小窗口长度,并移动 right 加入新元素到窗口,以保持窗口大小不变;
伪代码模板:
left = 0
right = 0
while right < len(nums):
window.append(nums[right])
if right - left + 1 >- windowSize:
window.popleft()
left += 1
right += 1
例题:
1343. 大小为 K 且平均值大于等于阈值的子数组数目 - 力扣(LeetCode)
class Solution:
def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int:
ans = 0
target = k * threshold
left = 0
right = 0
window = 0
while right < len(arr):
window += arr[right]
if right - left + 1 == k:
if window >= target:
ans += 1
window -= arr[left]
left += 1
right += 1
return ans
因为需要的是窗口的和,因此可以将窗口直接记录为求和。
2. 不定长度滑动窗口
在给定数组或字符串上维护一个不定长度的窗口
步骤:
使用两个指针 left,right。初始时同时指向第一个元素 left = 0, right = 0;
将区间最右侧的元素加入到窗口中,然后移动 right 增大窗口长度;
直到窗口中的连续元素满足要求;
然后移动 left 直到窗口中的连续元素不满足要求
伪代码模板:
left = 0
right = 0
while right < len(nums):
window.append(nums[right])
while 窗口需要缩小:
window.popleft()
left += 1
right += 1
例题:
1. 3. 无重复字符的最长子串 - 力扣(LeetCode)
使用哈希表来判断新加入的元素是否重复,如果重复则移动 left 直到没有重复元素,并在这个过程中维护最优值
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. 209. 长度最小的子数组 - 力扣(LeetCode)
注意细节,更新最优值的时机,需要自己模拟几遍
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
left = 0
right = 0
window = 0
flag = False
res = float('inf')
while right < len(nums):
window += nums[right]
while window >= target:
flag = True
window -= nums[left]
res = min(res, right - left + 1)
left += 1
right += 1
if not flag:
return 0
return res
3. 713. 乘积小于 K 的子数组 - 力扣(LeetCode)
在统计数组数目时,如果 [ i: j ] 符合条件,则 i 到 j 的连乘都符合条件
即 i * ( i + 1) ; i * ( i + 1) * ( i + 2) 。。。
共有 j - i + 1 种
class Solution:
def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
left = 0
right = 0
window = float(1)
ans = 0
while right < len(nums):
window *= nums[right]
while window >= k and left <= right:
window /= nums[left]
left += 1
ans += right - left + 1
right += 1
return ans
滑动窗口总结
使用滑动窗口时,要注意维护窗口的规模与最优值。
最优值更新的时机也是该技巧的难点之一
算法通关手册(LeetCode) | 算法通关手册(LeetCode)
原文内容在这里,如有侵权,请联系我删除。