文章目录
- 一 、排序算法
- 二 、二分查找
- 1 二分查找讲解
- 2 二分查找题目
- (1)二分查找
- (2)在排序数组中查找元素的第一个和最后一个位置
- (3)两数之和 II - 输入有序数组
- 三、数组双指针
- 1对撞指针
- 对撞指针题目1:两数之和 II - 输入有序数组
- 对撞指针题目2:验证回文串
- 对撞指针题目3:盛最多水的容器
- 2 快慢指针
- 快慢指针题目1:删除有序数组中的重复项
- 3 分离双指针
- 分离双指针题目1:两个数组的交集
- 分离双指针题目2:判断子序列
- 四 、数组滑动窗口
- 固定长度窗口滑动
- 题目:大小为 K 且平均值大于等于阈值的子数组数目
- 不定长度滑动窗口
- 题目1:无重复字符的最长子串
- 题目2:长度最小的子数组
一 、排序算法
LeetCode讲解算法1-排序算法(Python版)
二 、二分查找
1 二分查找讲解
二分查找的基本算法思想为:通过确定目标元素所在的区间范围,反复将查找范围减半,直到找到元素或找不到该元素为止。
以下是二分查找算法的基本步骤:
- 1 初始化:首先,确定要查找的有序数据集合。可以是一个数组或列表,确保其中的元素按照升序或者降序排列。
- 2 确定查找范围:将整个有序数组集合的查找范围确定为整个数组范围区间,即左边界 left和右边界 right。
- 3 计算中间元素:根据mid=(left+right)/2计算出中间元素mid。
- 4 比较中间元素:将目标元素target与中间元素nums[mid]进行比较。
- 4.1 如果目标元素target==nums[mid],说明找到target,因此返回中间元素的下标位置mid。
- 4.2如果target<nums[mid],说明目标元素在左边部分[left,mid-1],更新右边界为中间元素的前一个位置,即right=mid-1。
- 4.3如果target>nums[mid],说明目标元素在右边部分[lmid+1,right],更新左边界为中间元素的后一个位置,即left=mid+1。
- 重复步骤 3∼4,直到找到目标元素时返回中间元素下标位置,或者查找范围缩小为空(左边界大于右边界),表示目标元素不存在,此时返回 −1。
区间的开闭问题
左闭右闭区间:初始化时,left=0,right=len(nums)−1。left 为数组第一个元素位置,
right 为数组最后一个元素位置。区间 [left,right] 左右边界上的点都能取到。
在二分查找的实际问题中,最常见的
mid 取值公式有两个:
mid = (left + right) // 2。
mid = (left + right + 1) // 2 。
式子中 // 所代表的含义是「中间数向下取整」。当待查找区间中的元素个数为奇数个,使用这两种取值公式都能取到中间元素的下标位置。
而当待查找区间中的元素个数为偶数时,使用 mid = (left + right) // 2 式子我们能取到中间靠左边元素的下标位置,使用 mid = (left + right + 1) // 2 式子我们能取到中间靠右边元素的下标位置。
除了上面提到的这两种写法,我们还经常能看到下面两个公式:
mid = left + (right - left) // 2。
mid = left + (right - left + 1) // 2。
这两个公式其实分别等同于之前两个公式,可以看做是之前两个公式的另一种写法。这种写法能够防止整型溢出问题(Python 语言中整型不会溢出,其他语言可能会有整型溢出问题)。
left+right 的数据量不会超过整型变量最大值时,这两种写法都没有问题。在 left+right 的数据量可能会超过整型变量最大值时,最好使用第二种写法。所以,为了统一和简化二分查找算法的写法,建议统一写成第二种写法。
二分查找算法的写法中,while 语句出界判断条件通常有两种:
left <= right。
left < right。
我们究竟应该使用哪一种写法呢?
我们先来判断一下导致 while 语句出界的条件是什么。
如果判断语句为 left <= right,并且查找的元素不在有序数组中,则 while 语句的出界条件是 left > right,也就是 left == right + 1,写成区间形式就是 [right+1,right],此时待查找区间为空,待查找区间中没有元素存在,此时终止循环时,可以直接返回 −1。比如说区间 [3,2], 此时左边界大于右边界,直接终止循环,返回 −1 即可。
如果判断语句为left < right,并且查找的元素不在有序数组中,则 while 语句出界条件是 left == right,写成区间形式就是 [right,right]。此时区间不为空,待查找区间还有一个元素存在,我们并不能确定查找的元素不在这个区间中,此时终止循环时,如果直接返回 −1 就是错误的。比如说区间 [2,2],如果元素 nums[2] 刚好就是目标元素 target,此时终止循环,返回 −1 就漏掉了这个元素。
2 二分查找题目
(1)二分查找
描述:给定一个升序的数组 nums,和一个目标值 target。
要求:返回 target 在数组中的位置,如果找不到,则返回 -1。
示例输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
class Solution:def search(self, nums: List[int], target: int) -> int:left, right = 0, len(nums) - 1# 在区间 [left, right] 内查找 targetwhile left <= right:# 取区间中间节点mid = (left + right) // 2# 如果找到目标值,则直接返回中心位置if nums[mid] == target:return mid# 如果 nums[mid] 小于目标值,则在 [mid + 1, right] 中继续搜索elif nums[mid] < target:left = mid + 1# 如果 nums[mid] 大于目标值,则在 [left, mid - 1] 中继续搜索else:right = mid - 1# 未搜索到元素,返回 -1return -1
(2)在排序数组中查找元素的第一个和最后一个位置
描述:给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。
要求:找出给定目标值在数组中的开始位置和结束位置。
说明:要求使用时间复杂度为 O(logn) 的算法解决问题。
示例输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
进行两次二分查找,第一次尽量向左搜索。第二次尽量向右搜索。
这是因为数组中可能会有值等于 target 的重复元素,比如与在数组 [1, 2, 3, 3, 3, 3, 5, 6] 中查找元素 3 的第一个和最后一个位置。第一次尽量向左是为了查找元素 3 在数组中最左边的位置,第二次尽量向右是为了查找元素 3 在数组中最右边的位置。
class Solution:def searchRange(self, nums: List[int], target: int) -> List[int]:ans = [-1, -1]n = len(nums)if n == 0:return ansleft = 0right = n - 1while left < right:mid = left + (right - left) // 2if nums[mid] < target:left = mid + 1else:right = midif nums[left] != target:return ansans[0] = leftleft = 0right = n - 1while left < right:mid = left + (right - left + 1) // 2if nums[mid] > target:right = mid - 1else:left = midif nums[left] == target:ans[1] = leftreturn ans
(3)两数之和 II - 输入有序数组
描述:给定一个下标从 1 开始计数、升序排列的整数数组:numbers 和一个目标值 target。
要求:从数组中找出满足相加之和等于 target 的两个数,并返回两个数在数组中下的标值。
示例输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9。因此 index1 = 1, index2 = 2。返回 [1, 2]。
因为数组是有序的,可以考虑使用二分查找来减少时间复杂度。具体做法如下:
- 使用一重循环遍历数组,先固定第一个数,即 numsbers[i]。
- 然后使用二分查找的方法寻找符合要求的第二个数。
- 使用两个指针 left,right。left 指向数组第一个数的下一个数,right 指向数组值最大元素位置。
- 判断第一个数numsbers[i] 和两个指针中间元素 numbers[mid] 的和与目标值的关系。
(1)如果 numbers[mid]+numbers[i]<target,排除掉不可能区间 [left,mid],在 [mid+1,right] 中继续搜索。
(2) 如果 numbers[mid]+numbers[i]≥target,则第二个数可能在 [left,mid] 中,则在 [left,mid] 中继续搜索。 - 直到 left 和 right 移动到相同位置停止检测。如果 numbers[left]+numbers[i]==target,则返回两个元素位置 [left+1,i+1](下标从 1 开始计数)。
- 如果最终仍没找到,则返回 [−1,−1]。
class Solution:def twoSum(self, numbers: List[int], target: int) -> List[int]:for i in range(len(numbers)):left, right = i + 1, len(numbers) - 1while left < right:mid = left + (right - left) // 2if numbers[mid] + numbers[i] < target:left = mid + 1else:right = midif numbers[left] + numbers[i] == target:return [i + 1, left + 1]return [-1, -1]
三、数组双指针
双指针(Two Pointers):指的是在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为「对撞指针」。如果两个指针方向相同,则称为「快慢指针」。如果两个指针分别属于不同的数组 / 链表,则称为「分离双指针」。
在数组的区间问题上,暴力算法的时间复杂度往往是 O(n 2 )。而双指针利用了区间「单调性」的性质,可以将时间复杂度降到 O(n)。
1对撞指针
对撞指针:指的是两个指针 left、right 分别指向序列第一个元素和最后一个元素,然后 left 指针不断递增,right 不断递减,直到两个指针的值相撞(即 left==right),或者满足其他要求的特殊条件为止。
== 求解步骤==
1使用两个指针 left,right。left 指向序列第一个元素,即:left=0,right 指向序列最后一个元素,即:right=len(nums)−1。
2在循环体中将左右指针相向移动,当满足一定条件时,将左指针右移,left+=1。当满足另外一定条件时,将右指针左移,right−=1。
3 直到两指针相撞(即 left==right),或者满足其他要求的特殊条件时,跳出循环体。
== 伪代码 ==
left, right = 0, len(nums) - 1while left < right:if 满足要求的特殊条件:return 符合条件的值 elif 一定条件 1:left += 1elif 一定条件 2:right -= 1return 没找到 或 找到对应值
== 对撞指针一般用来解决有序数组或者字符串问题:==
1查找有序数组中满足某些约束条件的一组元素问题:比如二分查找、数字之和等问题。
2 字符串反转问题:反转字符串、回文数、颠倒二进制等问题。
对撞指针题目1:两数之和 II - 输入有序数组
描述:给定一个下标从 1 开始计数、升序排列的整数数组:numbers 和一个目标值 target。
要求:从数组中找出满足相加之和等于 target 的两个数,并返回两个数在数组中下的标值。
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
class Solution:def twoSum(self, numbers: List[int], target: int) -> List[int]:left = 0right = len(numbers) - 1while left < right:total = numbers[left] + numbers[right]if total == target:return [left + 1, right + 1]elif total < target:left += 1else:right -= 1return [-1, -1]
对撞指针题目2:验证回文串
描述:给定一个字符串 s。
要求:判断是否为回文串(只考虑字符串中的字母和数字字符,并且忽略字母的大小写)。
输入: “A man, a plan, a canal: Panama”
输出:true
解释:“amanaplanacanalpanama” 是回文串。
输入:“race a car”
输出:false
解释:“raceacar” 不是回文串。
class Solution:def isPalindrome(self, s: str) -> bool:left = 0right = len(s) - 1while left < right:if not s[left].isalnum():left += 1continueif not s[right].isalnum():right -= 1continueif s[left].lower() == s[right].lower():left += 1right -= 1else:return Falsereturn True
对撞指针题目3:盛最多水的容器
描述:给定 n个非负整数 a1,a2,a3…,an。每个数代表坐标中的一个点(i,ai)。在坐标内画 n 条垂直线,垂直线 i的两个端点分别为 (i,ai)和 (i,0)。
要求:找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
class Solution:def maxArea(self, height: List[int]) -> int:left = 0right = len(height) - 1ans = 0while left < right:area = min(height[left], height[right]) * (right-left)ans = max(ans, area)if height[left] < height[right]:left += 1else:right -= 1return ans
2 快慢指针
快慢指针:指的是两个指针从同一侧开始遍历序列,且移动的步长一个快一个慢。移动快的指针被称为 「快指针(fast)」,移动慢的指针被称为「慢指针(slow)」。两个指针以不同速度、不同策略移动,直到快指针移动到数组尾端,或者两指针相交,或者满足其他特殊条件时为止。
== 求解步骤 ==
1、使用两个指针 slow、fast。slow 一般指向序列第一个元素,即:slow=0,fast 一般指向序列第二个元素,即:fast=1。
2、在循环体中将左右指针向右移动。当满足一定条件时,将慢指针右移,即 slow+=1。当满足另外一定条件时(也可能不需要满足条件),将快指针右移,即 fast+=1。
3、到快指针移动到数组尾端(即 fast==len(nums)−1),或者两指针相交,或者满足其他特殊条件时跳出循环体。
== 伪代码==
slow = 0
fast = 1
while 没有遍历完:if 满足要求的特殊条件:slow += 1fast += 1
return 合适的值
== 适用范围 ==
快慢指针一般用于处理数组中的移动、删除元素问题,或者链表中的判断是否有环、长度问题。关于链表相关的双指针做法我们到链表章节再详细讲解。
快慢指针题目1:删除有序数组中的重复项
描述:给定一个有序数组 nums。
要求:删除数组 nums 中的重复元素,使每个元素只出现一次。并输出去除重复元素之后数组的长度。
说明:不能使用额外的数组空间,在原地修改数组,并在使用 O(1) 额外空间的条件下完成。
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
class Solution:def removeDuplicates(self, nums):""":param nums: list[int]:return:"""if len(nums) <= 1:return len(nums)slow, fast = 0, 1while (fast < len(nums)):if nums[slow] != nums[fast]:#比较 slow 位置上元素值和 fast 位置上元素值是否相等slow += 1nums[slow] = nums[fast] #将fast指向的元素复制到slow位置上fast += 1return slow + 1 #返回新数组长度if __name__ =="__main__":nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]c=Solution()print(c.removeDuplicates(nums))
3 分离双指针
分离双指针:两个指针分别属于不同的数组,两个指针分别在两个数组中移动。
== 求解步骤 ==
1、使用两个指针left_1,left_2。left_1指向第一个数组的第一个元素:left_1=0;left_2指向第二个数组的第一个元素:left_2=0;
2、当满足一定条件时,两个指针同时右移,left_1+=1,left_2+=1。
3、当满足另外一定条件时,left_1右移,left_1+=1。
3、当满足其他一定条件时,left_2右移,left_2+=1。
4、当其中一个数组遍历完时或者满足其他特殊条件时跳出循环体
== 伪代码==
left_1 = 0
left_2 = 0while left_1 < len(nums1) and left_2 < len(nums2):if 一定条件 1:left_1 += 1left_2 += 1elif 一定条件 2:left_1 += 1elif 一定条件 3:left_2 += 1
== 适用范围 ==
分离双指针一般用于处理有序数组合并,求交集、并集问题。
分离双指针题目1:两个数组的交集
描述:给定两个数组 nums1 和 nums2。
要求:返回两个数组的交集。重复元素只计算一次。
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
class Solution:def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:nums1.sort()nums2.sort()left_1 = 0left_2 = 0res = []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 += 1left_2 += 1elif nums1[left_1] < nums2[left_2]:left_1 += 1elif nums1[left_1] > nums2[left_2]:left_2 += 1return res
分离双指针题目2:判断子序列
描述:给定字符串 s 和 t。
要求:判断 s 是否为 t 的子序列。
输入:s = “abc”, t = “ahbgdc”
输出:True
class Solution:def isSubsequence(self, s: str, t: str) -> bool:size_s = len(s)size_t = len(t)i, j = 0, 0while i < size_s and j < size_t:if s[i] == t[j]:#遇到s[i] == t[j] ,i向右移动i += 1j += 1#不断右移动jreturn i == size_s
四 、数组滑动窗口
滑动窗口算法(Sliding Window):在给定数组 / 字符串上维护一个固定长度或不定长度的窗口。可以对窗口进行滑动操作、缩放操作,以及维护最优解操作。
滑动操作:窗口可按照一定方向进行移动。最常见的是向右侧移动。
缩放操作:对于不定长度的窗口,可以从左侧缩小窗口长度,也可以从右侧增大窗口长度。
滑动窗口利用了双指针中的快慢指针技巧,我们可以将滑动窗口看做是快慢指针两个指针中间的区间,也可以将滑动窗口看做是快慢指针的一种特殊形式。
== 适用范围==
滑动窗口算法一般用来解决一些查找满足一定条件的连续区间的性质(长度等)的问题。该算法可以将一部分问题中的嵌套循环转变为一个单循环,因此它可以减少时间复杂度。
按照窗口长度的固定情况,我们可以将滑动窗口题目分为以下两种:
固定长度窗口:窗口大小是固定的。
不定长度窗口:窗口大小是不固定的。
求解最大的满足条件的窗口。
求解最小的满足条件的窗口。
固定长度窗口滑动
假设窗口的固定大小为 window_size。
1、使用两个指针 left、right。初始时,left、right 都指向序列的第一个元素,即:left=0,right=0,区间 [left,right] 被称为一个「窗口」。
2、当窗口未达到 window_size 大小时,不断移动 right,先将数组前 window_size 个元素填入窗口中,即 window.append(nums[right])。
3、当窗口达到 window_size 大小时,即满足 right - left + 1 >= window_size 时,判断窗口内的连续元素是否满足题目限定的条件。
(1)如果满足,再根据要求更新最优解。
(2)然后向右移动 left,从而缩小窗口长度,即 left += 1,使得窗口大小始终保持为 window_size 。
4、向右移动 right,将元素填入窗口中,即 window.append(nums[right])。
5、重复 2∼4 步,直到 right 到达数组末尾。
== 伪代码==
left = 0
right = 0while right < len(nums):window.append(nums[right])# 超过窗口大小时,缩小窗口,维护窗口中始终为 window_size 的长度if right - left + 1 >= window_size:# ... 维护答案window.popleft()left += 1# 向右侧增大窗口right += 1
题目:大小为 K 且平均值大于等于阈值的子数组数目
描述:给定一个整数数组 arr 和两个整数 k 和 threshold 。
要求:返回长度为 k 且平均值大于等于 threshold 的子数组数目。
输入:arr = [2,2,2,2,5,5,5,8], k = 3, threshold = 4
输出:3
解释:子数组 [2,5,5],[5,5,5] 和 [5,5,8] 的平均值分别为 4,5 和 6 。其他长度为 3 的子数组的平均值都小于 4 (threshold 的值)。
class Solution:def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int:left = 0right = 0window_sum = 0ans = 0while right < len(arr):window_sum += arr[right]if right - left + 1 >= k:if window_sum >= k * threshold:ans += 1window_sum -= arr[left]left += 1right += 1return ans
不定长度滑动窗口
不定长度滑动窗口算法(Sliding Window):在给定数组 / 字符串上维护一个不定长度的窗口。可以对窗口进行滑动操作、缩放操作,以及维护最优解操作。
== 操作步骤==
1、使用两个指针 left、right。初始时,left、right 都指向序列的第一个元素。即:left=0,right=0,区间 [left,right] 被称为一个「窗口」。
2、将区间最右侧元素添加入窗口中,即 window.add(s[right])。
3、然后向右移动 right,从而增大窗口长度,即 right += 1。直到窗口中的连续元素满足要求。
4、此时,停止增加窗口大小。转向不断将左侧元素移出窗口,即window.popleft(s[left])。
5、移动 left,从而缩小窗口长度,即 left += 1。直到窗口中的连续元素不再满足要求。
6、重复 2 ~ 5 步,直到 right 到达序列末尾。
== 伪代码==
left = 0
right = 0while right < len(nums):window.append(nums[right])while 窗口需要缩小:# ... 可维护答案window.popleft()left += 1# 向右侧增大窗口right += 1
题目1:无重复字符的最长子串
描述:给定一个字符串 s。
要求:找出其中不含有重复字符的最长子串的长度。
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
class Solution:def lengthOfLongestSubstring(self, s: str) -> int:left = 0right = 0window = dict()ans = 0while right < len(s):#向右移动right,将s[right]加入当前窗口window,记录个数if s[right] not in window:window[s[right]] = 1else:window[s[right]] += 1#如果该窗口中该字符的个数多于1个,则缩小窗口长度,并更新窗口中对应字符的个数。直到window[s[right]]《=1while window[s[right]] > 1:window[s[left]] -= 1left += 1ans = max(ans, right - left + 1) #维护更新无重复字符的最长子串长度。right += 1#继续右移return ans
题目2:长度最小的子数组
描述:给定一个只包含正整数的数组 nums 和一个正整数 target。
要求:找出数组中满足和大于等于 target 的长度最小的「连续子数组」,并返回其长度。如果不存在符合条件的子数组,返回 0。
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
class Solution:def minSubArrayLen(self, target: int, nums: List[int]) -> int:size = len(nums)ans = size + 1left = 0right = 0window_sum = 0while right < size:window_sum += nums[right]while window_sum >= target:ans = min(ans, right - left + 1)window_sum -= nums[left]left += 1right += 1return ans if ans != size + 1 else 0