datawhale学习-算法入门与数组篇

文章详细介绍了数组二分查找算法在不同编程问题中的应用,包括查找目标值、插入位置、计算平方根、解决旋转数组问题以及查找最小值。通过示例代码展示了如何使用二分查找优化时间复杂度。
摘要由CSDN通过智能技术生成

一、数组二分查找

为了练习所学到的二分查找方法,我们尽量优先使用二分查找来完成练习题目。

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 

https://leetcode.cn/ 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值