题目与思路:
主要练习左闭右闭写法和左闭右开写法,注意边界条件处理。比较好理解,除了很久没有写代码遇到了indent报错的问题,其余比较顺利,贴上两种写法的代码:
class Solution:
def search(self, nums: List[int], target: int) -> int:
#左闭右闭
left, right = 0, len(nums) - 1
#左闭右闭左右界相等时仍有1个元素,需要继续循环。
while left <= right:
#防止相加溢出,实际上在本题中不会。
mid = (right - left) // 2 + left
if nums[mid] == target:
return mid
#目标值比中值小,在当前区域的左半边
elif nums[mid] > target:
#更新上界,右闭区间的上界参与计算因此要去除本次已计算过的mid
right = mid - 1
#目标值比中值大,在当前区域的右半边
else:
left = mid + 1
#未找到,返回-1
return -1
class Solution:
def search(self, nums: List[int], target: int) -> int:
#左闭右开
left, right = 0, len(nums)
#左闭右开左右界相等时没有元素,不需要继续循环。
while left < right:
#防止相加溢出,实际上在本题中不会。
mid = (right - left) // 2 + left
if nums[mid] == target:
return mid
#目标值比中值小,在当前区域的左半边
elif nums[mid] > target:
#更新上界,右开区间的上界不参与计算因此直接用mid代替
right = mid
#目标值比中值大,在当前区域的右半边
else:
left = mid + 1
#未找到,返回-1
return -1
思考本题的拓展练习35.搜索插入位置和34. 在排序数组中查找元素的第一个和最后一个位置的时候考虑研究二分法在找不到元素时left和right的终点在哪里。得到结论如下:
- 左闭右闭区间:查不到元素时,有三种情况,第一种情况target在区间左边外边,target<min(nums),此时显然left=0,而right=-1;第二种情况target在区间右边外边,target>max(nums),此时left=len(nums),而right=len(nums)-1;第三种情况target在区间内,min(nums)<target<max(nums),此时不妨假设数组中连续的两个值m和n有m<target<n,最终循环停止时必定有left=index(m),而right=index(n)。
- 左闭右开区间:也是类似三种情况,第一种情况有left=right=0,第二种情况有left=right=len(arr),第三种情况,由于右开,因此最后一次更新时必定更新下界,于是必有left=right=index(n)
由于二分查找的停止条件有以上特性,可以用于解决拓展练习:
35.搜索插入位置直接使用左闭右开区间,除了元素在数组内返回mid,其余情况只需返回left即是第一个大于target的位置。代码如下:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums)
while left < right:
mid = (right - left) // 2 + left
if target > nums[mid]: left = mid + 1
elif target < nums[mid]: right = mid
else: return mid
return left
34. 在排序数组中查找元素的第一个和最后一个位置,这是要找边界,首先考虑一般情况,target在数组内,我们在找到target的时候不去return,而是继续更新边界:在nums[mid]==target时,如果我们继续更新上界,那么搜索区间会一直向左缩小,直到找到了左边界停止。相反,如果一直更新下界,那么搜索区间会一直向右缩小,直到找到了右边界停止。此时以左闭右闭区间为例,找左边界时left会停在左边界位置,right会在左边界-1的位置;找右边界时,left停在右边界+1的位置而right=右边界。再考虑找不到的情况,实际上在上面已经分析过了,以左闭右闭区间为例,最终如果找不到目标元素,必定有三种情况,这三种情况都有left>right。
class Solution:
def find_first(self, nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (right - left) // 2 + left
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
def find_last(self, nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (right - left) // 2 + left
if nums[mid] <= target:
left = mid + 1
else:
right = mid - 1
return right
def searchRange(self, nums: List[int], target: int) -> List[int]:
first = self.find_first(nums, target)
last = self.find_last(nums, target)
# 检查找到的位置是否有效
if first <= last:
return [first, last]
else:
return [-1, -1]
暴力解法:两层循环嵌套,第一层找数,第二层移动数组。说起来简单,实际操作还是有一些细节。首先在python中,range(n)
在循环开始之前就已经被评估并创建了,它生成了一个固定长度的迭代器。循环的次数由这个迭代器的长度决定,而这个长度是在循环开始时就已经确定的。因此第一层循环只能使用while循环代替。其次,在循环中,当if条件成立,开始移动数组时,当前index也需要向前移一位,否则遇到连续删除的情况会遗漏。暴力实现代码如下:
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
i,length = 0, len(nums)
while i <= length - 1:
if nums[i] == val:
for j in range(i+1,length):
nums[j-1] = nums[j]
length -= 1
i-=1
i+=1
return length
双指针解法:使用快慢指针操作数组,快指针遍历数组并查找不等于val的值,慢指针就是新数组的index,一旦快指针遇到不等于val的值,就将其覆盖到当前慢指针的index上(实际上自己在做题的时候不是采用了”覆盖“而是采用了”互换“,计算量稍大了一些但总体还是O(n)的)。以下是双指针法实现代码:
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
fast,slow,length = 0,0,len(nums)
while fast < length:
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
实际上本题可以打乱顺序,也就有了另一种更少移动元素的解法,具体是双指针分别从左右两端开始,左指针正常向右移动,一旦遇到等于val的情况,用右指针的值覆盖这个位置,右指针左移。代码如下:
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
left,right = 0,len(nums)-1
while(left <= right):
if nums[left] == val:
nums[left] = nums[right]
right -= 1
else:
left += 1
return left
拓展题26.移除有序数组中的重复元素-双指针,快指针遇到不等于现在慢指针的元素,慢指针右移并将其指向元素覆盖为快指针元素。代码如下:
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if len(nums) < 2: 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
拓展题283.移动零-双指针,快指针指到非零元素时交换快慢指针元素且慢指针右移。直到遍历整个数组。
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
fast,slow = 0,0
while fast < len(nums):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast],nums[slow]
slow+=1
fast+=1
拓展题844.比较含退格的字符串,这里跳过进出栈还原字符串的算法。分别定义指针从两个字符串尾开始向前遍历,定义函数next_valid_char_index去跳过需要被回退的字符:如果指针指向‘#’则回退计数器+1,如果指针指向字母,查看回退计数器,大于零则跳过该字符并且计数器-1,等于零则返回当前index值;主函数循环开始则先调用回退函数,如果此时没有遍历完全,则比较两个字符,如果不等直接返回false,直到其中一个遍历完全,此时比较索引即可判断两个字符串是否相等。代码如下:
class Solution:
def next_valid_char_index(self, s, i):
backspace_count = 0
while i >= 0:
if s[i] == '#':
backspace_count += 1
elif backspace_count > 0:
backspace_count -= 1
else:
break
i -= 1
return i
def backspaceCompare(self, s: str, t: str) -> bool:
i, j = len(s) - 1, len(t) - 1
while i >= 0 or j >= 0:
i = self.next_valid_char_index(s, i)
j = self.next_valid_char_index(t, j)
if i >= 0 and j >= 0 and s[i] != t[j]:
return False
i -= 1
j -= 1
return i == j
拓展题977. 有序数组的平方:左右指针,最大值一定出现在最左或者最右,每次比较左右数的平方大小,大的append进新数组,最终反向输出数组。代码如下:
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
left,right = 0, len(nums)-1
res = []
while left <= right:
if nums[left] ** 2 >= nums[right] ** 2:
res.append(nums[left]**2)
left += 1
else:
res.append(nums[right]**2)
right -= 1
return res[::-1]
今日总结:
今天的内容其实不算多也不算难,贵在坚持吧,细节方面需要注意。
二分法尤其是边界条件的处理;双指针法中的快慢指针个人理解到慢指针划过区域为新数组,快指针遍历去寻找新数组元素,双指针完成了两层循环要做的事情。其实做完题感觉也不全是快慢指针,一些双指针题目似乎还挺有技巧性,多积累经验吧。
目前还是萌新状态,今日存疑——循环不变量,看了书还是没能理解(只能理解书上例子和代码随想录上的例子,告诉我这是循环不变量我可以能明白,但是遇到新问题问我如何设置循环不变量可能会有问题)。就感觉这个问题有点不会描述?感觉需要多积累经验回过头再看这个问题。