一、数组二分查找
为了练习所学到的二分查找方法,我们尽量优先使用二分查找来完成练习题目。
1.练习题目(第9天)
此题可以用二分查找的典型算法:
class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0 # 定义左边界指针,初始指向数组的起始位置
right = len(nums) - 1 # 定义右边界,初始指向数组的末尾位置
while left <= right: # 当左指针小于等于右指针时执行循环
mid = (left + right) // 2 # 计算中间位置索引
if nums[mid] == target: # 如果中间元素等于目标元素,则找到目标元素
return mid
elif target > nums[mid]: # 如果目标元素大于中间元素,说明目标元素可能在右半部分
left = mid + 1 # 更新左边界指针为中间位置的下一个位置
else: # 如果目标元素小于中间元素,说明目标元素可能在左半部分
right = mid - 1 # 更新右边界指针为中间位置的上一个位置
return -1 # 如果循环结束仍未找到目标元素,则返回-1表示未找到
此题与上题思路相似,仍是二分查找,但需要加上目标小于或大于数组全部元素的情况:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left = 0 # 定义左边界,初始指向数组的起始位置
right = len(nums) - 1 # 定义右边界,初始指向数组的末尾位置
while left < right: # 当左指针小于右指针时执行循环
mid = (left + right) // 2 # 计算中间位置索引
if nums[mid] == target: # 如果中间元素等于目标元素,则找到目标元素
return mid
elif target > nums[mid]: # 如果目标元素大于中间元素,说明目标元素可能在右半部分
left = mid + 1 # 更新左边界指针为中间位置的下一个位置
else: # 如果目标元素小于中间元素,说明目标元素可能在左半部分
right = mid - 1 # 更新右边界指针为中间位置的上一个位置
if left >= right: # 循环结束后,如果左指针大于等于右指针
if nums[left] >= target: # 且左指针位置的元素大于等于目标元素
return left # 返回左指针位置作为插入位置
else:
return left + 1 # 返回左指针位置的下一个位置作为插入位置
此题还可以使用更加简单直接的方法,并且在时间复杂度上有很大的优化:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
if target in nums: # 如果目标元素在数组中出现
for i in nums: # 遍历数组
if i == target: # 当找到目标元素时
return nums.index(i) # 返回目标元素的索引位置
else: # 如果目标元素不在数组中
if target < nums[0]: # 如果目标元素小于数组中的第一个元素
return 0 # 返回起始位置的索引0
if target > nums[-1]: # 如果目标元素大于数组中的最后一个元素
return len(nums) # 返回数组末尾位置的索引
else: # 否则,目标元素在数组中间的某个位置
for j in nums: # 遍历数组
if j < target < nums[nums.index(j)+1]: #找到小于目标且下一个数大于目标的数的索引
return nums.index(j)+1 # 返回该插入位置的索引值
此题仍然是二分查找的经典题目:
class Solution:
def guessNumber(self, n: int) -> int:
left = 1 # 定义左边界指针,初始指向数字范围的起始位置
right = n # 定义右边界指针,初始指向数字范围的末尾位置
while left <= right: # 当左指针小于等于右指针时执行循环
mid = (left + right) // 2 # 计算中间位置的数字
if guess(mid) == 0: # 如果猜测的数字与所选数字相等
return mid # 返回猜测的数字作为答案
elif guess(mid) == -1: # 如果猜测的数字偏小
right = mid - 1 # 更新右边界指针为中间位置的前一个位置
else: # 如果猜测的数字偏大
left = mid + 1 # 更新左边界指针为中间位置的后一个位置
2.练习题目(第10天)
这里我们可以分析在1之后的数字整除2的结果都是小于等于它的平方根的,由此,我们可以写一个简单直接的解法:
class Solution:
def mySqrt(self, x: int) -> int:
mid = x // 2 # 将中间位置初始化为 x 的一半
if x <= 1: # 如果 x 是0,1
return x # 直接返回x
else:
for i in range(mid + 1): # 遍历从0到中间位置+1的数字
if i * i <= x and (i + 1) * (i + 1) > x: # 当找到第一个平方小于等于x且下一个平方大于x的位置时
return i # 返回该位置作为平方根
但是这个方法并不理想,时间复杂度过高,有没有更快的方法呢,我们用二分查找的方法尝试一下:
class Solution:
def mySqrt(self, x: int) -> int:
if x <= 1: # 如果 x 小于等于1
return x # 直接返回 x
left, right = 0, x # 设置左右边界指针,初始左指针为0,右指针为x
while left <= right: # 当左指针小于等于右指针时执行循环
mid = left + (right - left) // 2 # 计算中间位置的数字,同时防止溢出错误
if mid ** 2 == x: # 如果中间位置的平方等于 x
return mid # 返回中间位置
elif mid ** 2 > x: # 如果中间位置的平方大于 x
right = mid - 1 # 更新右指针为中间位置的前一个位置
else: # 如果中间位置的平方小于 x
left = mid + 1 # 更新左指针为中间位置的后一个位置
return right # 若left>right,返回右指针作为平方根的整数部分
可以看到使用二分法的结果明显比之前的结果优化了许多。
我们先来试试简单方法,直接通过循环遍历查找元素并返回下标:
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
index1 = 1 # 初始化第一个索引为1
for i in range(1, len(numbers) + 1): # 遍历数组中的每个元素
temp = target - numbers[i - 1] # 计算目标值与当前元素的差值
for j in range(i + 1, len(numbers) + 1): # 在当前元素之后的位置查找匹配的元素
if numbers[j - 1] == temp: # 如果找到了匹配的元素
index2 = j # 将第二个索引设置为匹配元素的位置
index1 = i # 更新第一个索引为当前元素的位置
result = [index1, index2] # 将结果存储在列表中
return result # 返回结果
# 如果没有找到满足条件的元素组合,则返回一个空列表
return []
不出意外的,思路如此简单却是个中等题,肯定是在数据上刻意针对时间复杂度做了要求:
两层循环的时间复杂度过高,我们再试试双指针查找,通过不断调整左右指针的位置,使得它们对应的元素之和逼近目标值:
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
i, j = 0, len(numbers) - 1 # 初始化左右指针,i指向数组开头,j指向数组结尾
while i < j: # 当左指针小于右指针时进行循环
s = numbers[i] + numbers[j] # 计算当前左右指针对应的元素之和
if s > target: # 如果和大于目标值
j -= 1 # 右指针向左移动,使得和变小
elif s < target: # 如果和小于目标值
i += 1 # 左指针向右移动,使得和变大
else: # 如果和等于目标值,找到了满足条件的两个数
result = [i + 1, j + 1] # 将满足条件的两个索引保存在列表中
return result # 返回结果列表
# 如果没有找到满足条件的组合,则返回一个空列表
return []
此题仍可使用二分查找,而且效率很高,可以巧妙地运用二分法来筛选每天装载的货物数量,我们很容易理解,每天最小的运货量为最重的货物的质量,因为货物是无法拆分的,只能一趟运走,而最大的运货量就是一天直接把所有货运走,即货物质量的总和,我们可以以此为区间来用二分法逐步查找需要的运货量:
class Solution:
def shipWithinDays(self, weights: List[int], days: int) -> int:
left = max(weights) # 左边界初始化为货物中最重的重量
right = sum(weights) # 右边界初始化为货物总重量
while left < right: # 当左边界小于右边界时进行循环
mid = (left + right) // 2 # 中间位置为左右边界的平均值
Days = 1 # 需要的天数
count = 0 # 每天装载的货物重量
for weight in weights: # 遍历
count += weight # 将当前货物加入当天装载的重量
if count > mid: # 如果当天装载的重量超过了中间位置的限制
Days += 1 # 需要增加一天来继续装载货物
count = weight # 重新计算当天装载的重量,从当前货物开始
if Days > days: # 如果需要的天数大于指定的天数
left = mid + 1 # 调整左边界,增加中间位置的限制
else:
right = mid # 调整右边界,缩小中间位置的限制,这里为什么不是right=mid-1,因为存在Days=days的情况
return left # 返回左边界作为结果
3.练习题目(第11天)
不多说了,直接二分法查找:
# The isBadVersion API is already defined for you.
# def isBadVersion(version: int) -> bool:
class Solution:
def firstBadVersion(self, n: int) -> int:
left = 1 # 左边界初始化为第一个版本
right = n # 右边界初始化为最后一个版本
while left <= right: # 当左边界小于等于右边界时进行循环
mid = (left + right) // 2 # 中间位置为左右边界的平均值
if not isBadVersion(mid-1) and isBadVersion(mid): # 如果当前版本为错误版本,而前一个版本不是错误版本
return mid # 返回当前版本作为第一个错误版本
elif not isBadVersion(mid-1) and not isBadVersion(mid): # 如果当前版本和前一个版本都不是错误版本
left = mid + 1 # 调整左边界,查找范围缩小到右半部分
else: # 如果当前版本和前一个版本都是错误版本
right = mid - 1 # 调整右边界,查找范围缩小到左半部分
乍一看此题很简单,只需要找到目标值并返回下标 ,但是我们忽略了一个问题:时间复杂度,如何将算法的时间复杂度控制在O(log n)以下呢,很明显,二分查找,但是经过旋转的数组不再有序,而二分查找只能在有序数组中使用,我么如何将数组变得有序呢?二分法,旋转一次的数组从中间分开,必将分为有序与无序的两部分,我们可以对有序的部分进行操作,如何区分有序和无序呢?左边界处的元素小于等于中间位置的元素,说明左半部分是有序的,同理,左边界处的元素大于中间位置的元素,说明右半部分是有序的,以此来缩小范围。
代码如下:
class Solution:
def search(self, nums: List[int], target: int) -> int:
left=0 #初始化左边界left为数组的第一个索引
right=len(nums)-1 #右边界right为数组的最后一个索引
if nums==[]:
return -1 #如果数组为空,直接返回-1表示未找到目标值
else:
while left<=right:
mid=(left+right)//2 #在每次循环中,计算中间位置mid为左右边界的平均值
if nums[mid]==target:
return mid #如果中间位置的元素等于目标值target,返回中间位置mid作为结果
if nums[left]<=nums[mid]:#左边界处的元素<=中间位置的元素,说明左半部分有序
if nums[left]<=target<nums[mid]: #如果目标值在左半部分的范围内
right=mid-1 #将右边界right调整为mid-1,缩小搜索范围至左半部分
else:
left=mid+1 #否则,将左边界left调整为mid+1,缩小搜索范围至右半部分
else: #如果左边界处的元素大于中间位置的元素,说明右半部分是有序的
if nums[mid]<target<=nums[right]: #如果目标值在右半部分的范围内
left=mid+1 #将左边界left调整为mid+1,缩小搜索范围至右半部分
else: #否则,将右边界right调整为mid-1,缩小搜索范围至左半部分
right=mid-1
return -1
让我们先来解决这个问题,直接min(nums)得出结果:
class Solution:
def findMin(self, nums: List[int]) -> int:
return min(nums)
问题很容易解决,但时间复杂度较高,让我们用二分法再尝试一次:
class Solution:
def findMin(self, nums: List[int]) -> int:
left = 0 # 初始化左边界为数组的第一个索引
right = len(nums) - 1 # 初始化右边界为数组的最后一个索引
if not nums: # 如果数组为空
return -1 # 返回-1
else:
while left < right: # 在左边界小于右边界的条件下进行迭代
mid = (left + right) // 2 # 计算中间位置
if nums[mid] > nums[right]: # 如果中间位置的数大于右边界位置的数
left = mid + 1 # 缩小搜索范围至右半部分
else:
right = mid # 缩小搜索范围至左半部分
return nums[left] # 返回最小值(循环结束时左右边界重合,左边界对应的值即为最小值)
结果明显优于min()函数。
本文参考链接:
https://datawhalechina.github.io/leetcode-notes/#/ch01/01.04/01.04.01-Array-Binary-Search-01