LT专项【二分法】

二分法

二分搜索是通过不断划分取中间点划分区间,以此在极大程度上减少查找的次数。
二分查找的前提,是整个数组是有序的,并且数组无重复元素
有序很好理解,对于无重复元素的条件而言,如果一旦有重复元素,使用二分查找法法返回的元素下标就可能不是唯一的。

第一种写法 全闭合区间 [left, right]

def find_target(nums: list, tgt: int):
	left = 0 
	right = len(nums) -1 
	while left  <= right:
		mid = (left + right)//2
		if nums[mid] == tgt: return mid 
		elif num[mid] > tgt:
		## 说明tgt在左边区间 即 [left, mid -1]
			right = mid - 1
		else:
		## 说明tgt在右边区间 即 [mid +1, right]
			left = mid + 1  
	return -1 
	##没有找到,返回-1 

第二种写法

def find_target(nums: list, tgt: int):
	left = 0
	right = len(nums) #由于是左闭右开的区间,因此right需要取到最右的开区间位置
	while left < right: 
		mid = (left + rigth) //2 
		if nums[mid] == tgt: return mid
		elif nums[mid] > tgt:
		## 说明tgt在左边区间,即[left, mid]
			right = mid 
		else:
		## 说明tgt在右边区间,即[mid+1, right]
			left = mid + 1 
	return -1 

而针对二分法的时间复杂度解析:
假设有N个元素
每次查找剩下的区间是:
N / 2 1 , N / 4 2 2 , N / 2 3 , N / 2 4 , N / 2 5 , , , N / 2 k N/2^1, N/42^2, N/2^3, N/2^4,N/2^5,,,N/2^k N/21N/422N/23N/24N/25N/2k
【k表示共进行了k次搜索】
所以, N / 2 k = 1 N/2^k = 1 N/2k=1
所以k=logN
所以二分法的时间复杂度是logN

涉及二分法题目

题目一:LT_35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法

输入: nums = [1,3,5,6], target = 5
输出: 2
输入: nums = [1,3,5,6], target = 2
输出: 1
输入: nums = [1,3,5,6], target = 7
输出: 4

二分法
(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 nums[mid] > target: 
            ##说明tgt在左区间, [left, mid-1]
                right = mid -1 
            elif nums[mid] < target:
            ##说明tgt在右区间,[mid + 1, rigth]
                left = mid + 1 
        return left 	

(2)方法二

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        ##左闭右开 
        left = 0 
        right = len(nums)
        while left < right:
            mid = (left + right)//2
            if nums[mid] == target: return mid 
            elif nums[mid] > target: 
            ##说明tgt在左区间,[left, mid)
                right = mid 
            elif nums[mid] < target:
            ##说明tgt在右区间,[mid+1, right)
                left = mid + 1 
        return left  

题目二:LT_14. 最长公共前缀

找到几个字符串中最长公共前缀

Input: ["flower","flow","flight"]
Output: "fl"
Input: ["dog","racecar","car"]
Output: ""
class Solution:
	def checkCommon(self, strs, tmp):
		for s in strs:
			if s.find(str1)!=0: return False 
		return True 
	
	def longestCommonPrefix(self, strs: List[str]) -> str:
		str1 = strs[0]
		left = 0 
		right = len(str1) -1 
		while left <= right:
			mid = (left + right ) //2 
			tmp = str1[0:mid+1]
			if self.checkCommon(strs,tmp):
			##说明ans需要再往右边一点 
				left = mid + 1 
			else:
			##说明ans在左边一点 
				right = mid - 1
		return str1[0:left] if right>=0 else ""	

题目三:LT_69. x 的平方根

给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5

例子

输入:x = 4
输出:2
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
class Solution:
    def mySqrt(self, x: int) -> int:
    	##区间是[0, x]之间寻找 
    	left, right = 0, x 
    	while left <= right: 
    		mid = (left + right)//2 
    		val = mid * mid 
    		if val == x : return mid 
    		elif val > x: 
    		##说明ans在左区间里找 即[left, mid-1]
    			right = mid -1 
    		else:
    		##说明ans在右区间 即 [mid+1, right]
    			left = mid + 1 
    	return right 

题目四:LT_287. 寻找重复数

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间

输入:nums = [1,3,4,2,2]
输出:2
输入:nums = [3,1,3,4,2]
输出:3

改编后的二分法
抽屉式的二分法请添加图片描述
nums = [1,3,4,2,2]
cnt表示比当前value小或者等于的有多少个
比如比2小或者等于的在nums数组有3个

比如 在这个例子中,left=1, right = 4 所以mid = (left + right) //2 = (1+4)//2 = 2
所以在nums中,小于或者等于mid的有3个。3 > mid = 2 所以答案是 在 left ~ mid 之间。
如果大于mid的话,则在mid+1 ~ right之间。 最后返回left 或者right都行.

class Solution:
    def countNums(self, nums, mid):
        cnt = 0 
        for num in nums:
            if num <= mid: cnt += 1 
        return cnt 

    def findDuplicate(self, nums: List[int]):
        left = 1 
        right = len(nums)-1 
        while left < right:
            mid = (left + right )//2
            if self.countNums(nums, mid)>mid:
            ##说明ans在左区间,而且大于的时候,mid也在候选区范围内 所以right = mid
                right = mid 
            else:
                left = mid+1
        return left 

【总结】
其实两种写法的关键区别点在 当nums[mid] > tgt的时候,这个时候mid有没有可能是潜在的答案,如果肯定mid不可能是答案,则right = mid-1;如果mid有可能是潜在的答案,则right=mid。
对于方法一:
比如题目 LT_35. 搜索插入位置中,输入: nums = [1,3,5,6], target = 5
如果nums[mid] = 6 > target =5 则这个时候 mid不可能是潜在的答案,所以right = mid -1
又比如题目寻找平方根,如果mid=3 则 mid=3的平方为9 >tgt=8, 这个时候mid=3 不可能是答案,所以right=mid-1

对于方法二:
比如题目 LT_287. 寻找重复数中,输入:nums = [1,3,4,2,2]
如果nums[mid=2] = 3 > mid = 2, 这个时候,mid=2 是潜在可能的答案,如果right = mid 需要取到这个值。

题目五:剑指 Offer II 006_排序好的twoSum问题

给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target

输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:26 之和等于目标数 8 。因此 index1 = 1, index2 = 3

思路:
如果要使用二分法的话,则可以先固定一个值,然后在剩下的数组中,寻找另一个值。

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        N = len(numbers)
        for i in range(N):       
            tgt = target - numbers[i] 
            left = i+1 
            right = N-1 
            while left <= right:
                mid = (left + right)//2 
                if numbers[mid] == tgt: return [i,mid]
                elif numbers[mid] > tgt: 
                ##说明在左区间,且mid不可取
                    right = mid - 1 
                else:
                    left = mid + 1 
        return []

时间复杂度: Nlogn (N次的二分法)

题目六:LT_378. 有序矩阵中第 K 小的元素

给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。
你必须找到一个内存复杂度优于 O(n2) 的解决方案

matrix = [
   [ 1,  5,  9],
   [10, 11, 13],
   [12, 13, 15]
],
k = 8,

返回 13。
解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13

方法一:暴力法
直接将二维数组转换成一维数组,进行排序,然后取第K个元素即可

class Solution:
    def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
    	##把二维数组转换成一维数组
    	array = sum(matrix, [])
    	##排序
    	ans = sorted(array)
    	return ans[k-1]

空间复杂度:O(n^2)
排序时间复杂度:O(n^2logn)
方法二:二分法(to be continued)
方法三:并归排序(to be continued)

题目七:LT_34. 在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
输入:nums = [], target = 0
输出:[-1,-1]

这道题有点不一样的是 有重复的数,所幸还是有序的。只要我们使用两次二分法,分别寻找左边界和右边界。

class Solution:
    def searchLeft(self, nums, target):
        left = 0 
        right = len(nums)-1
        while left <= right:
            mid = (left + right)//2 
            if nums[mid] == target:
            ##如果相等,需要再往左边探测看看
                right = mid -1 
            elif nums[mid] > target:
            ##说明在左区间,且mid不可能  [left, mid-1]
                right = mid -1 
            else:
            ##说明在右区,且mid不可能 [mid -1, right]
                left = mid+1
        if nums[left] == target: return left 
        else: return -1 

    def searchRight(self, nums, target):
        left, right = 0, len(nums)-1
        while left <= right:
            mid = (left + right)//2
            if nums[mid] == target:
            ##说明需要往右边再看看
                left = mid + 1 
            elif nums[mid] > target:
            ##说明在左边区间,且mid不可能
                right = mid -1 
            else:
                left = mid + 1 
        if nums[right]==target: return right 
        else: return -1 
        
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        ##special case
        if len(nums) ==0 or nums[-1] <target: return [-1,-1]
 
        left = self.searchLeft(nums, target)
        right = self.searchRight(nums, target)
        return [left, right]

题目八:LT_540. 有序数组中的单一元素

给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度

输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
输入: nums =  [3,3,7,7,10,11,11]
输出: 10

思路:
这里的思路是
如果都是成双成对的偶数的话,比如nums= [1,1,2,2,3,3,4,4,8,8],那么必定会有
如果index是偶数,则nums[index] = nums[index + 1]
如果index是奇数,则nums[index -1] = nums[index]

而题目中,出现有一个数是只出现一次,比如nums = [1,1,2,3,3,4,4,8,8]
我们需要找出这个单数,而这个单数的出现必定会打破上面的规则,即
如果index是偶数,则nums[index] = nums[index + 1]
如果index是奇数,则nums[index -1] = nums[index]

所以当索引 mid是偶数的时候,且nums[mid] = nums[mid + 1],则说明在[left, mid]这个区间是成双的偶数,ans在右区间;如果nums[mid] != nums[mid + 1], 则说明在左区间出现了这个单数,并打破了这个规则。所以ans在左区间

同理如果index是奇数,则nums[index -1] = nums[index], 则说明在[left, mid]这个区间是成双的偶数,ans在右区间。 如果nums[mid-1] != nums[mid ], 则说明在左区间出现了这个单数,并打破了这个规则。所以ans在左区间。

class Solution:
    def singleNonDuplicate(self, nums: List[int]) -> int:
        ##二分法
        left = 0
        right = len(nums)-1
        while left < right:
            mid = (left + right)//2
            ##如果mid 是偶数,则 
            if mid%2==0:
                if nums[mid] == nums[mid+1]:
                ##说明在[left, mid+1]这个区间是成双的偶数,ans在右区间 [mid+2,right]
                    left = mid + 2 
                else:
                ##说明在左区间出现了这个单数,并打破了这个规则。所以ans在左区间即 [left, mid]
                    right = mid 
            else:
           	##如果mid是奇数,则
                if nums[mid-1] == nums[mid]:
                ##说明在[left, mid]这个区间是成双的偶数,ans在右区间 [mid+1,right]
                    left = mid + 1 
                else:
                ##说明在左区间出现了这个单数,并打破了这个规则。所以ans在左区间即 [left, mid-1]
                    right = mid -1 
        return nums[right] 

【特别注意这里用的是 while left < right, 而不是 while left <= right, 是因为这里有right=mid,如果用while left <= right,则会出现无限循环。】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jianafeng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值