二分搜索的应用
简介
应用
典型应用如下:
序号 | 题目 |
---|---|
1 | 33. 搜索旋转排序数组 |
2 | 81. 搜索旋转排序数组 II |
3 | 378. 有序矩阵中第 K 小的元素 |
4 | 373. 查找和最小的 K 对数字 |
5 | 719. 找出第 K 小的数对距离 |
应用1:Leetcode 33. 搜索旋转排序数组
题目
分析
方法一
旋转后的数组,就形成了两个有序的子数组。
因为左右两部分子数组都是有序的,所以,在局部查找的时候,依然可以使用二分查找的方式查找。
算法步骤
对于每次找到的区间中点 nums[mid]
:
-
如果它在右侧的子区间
-
如果目标值
target
在[nums[0], nums[mid]]
之间,则移动区间右侧的指针; -
否则,就移动区间左侧指针。
-
-
如果它在左侧的子区间
-
如果目标值
target
在[nums[mid], nums[-1]]
之间,则移动区间右侧的指针; -
否则,就移动区间左侧指针。
-
方法二
比较直观的思路是:因为目标值 target
是固定的,所以,我们直接考虑 target
落在左侧子区间,还是右侧子区间。
算法步骤
对于待查找的目标值 target
:
-
如果它落在右侧的子区间
-
如果区间中点
nums[mid]
大于目标值target
,或者区间中点nums[mid]
小于nums[0]
,则移动右侧指针; -
否则就移动左侧指针。
-
-
如果它落在左侧的子区间
-
如果区间中点
nums[mid]
小于目标值target
,或者区间中点nums[mid]
大于等于nums[0]
,则移动左侧指针; -
否则就移动右侧指针。
-
代码实现
方法一
class Solution: def search(self, nums: List[int], target: int) -> int: if not nums: return -1 left, right = 0, len(nums) - 1 while left <= right: mid = left + (right - left) // 2 if nums[mid] == target: return mid if nums[0] <= nums[mid]: if nums[0] <= target < nums[mid]: right = mid - 1 else: left = mid + 1 else: if nums[mid] < target <= nums[len(nums)-1]: left = mid + 1 else: right = mid - 1 return -1
方法二
class Solution: def search(self, nums: List[int], target: int) -> int: n = len(nums) left, right = 0, n - 1 while left <= right: mid = left + (right - left) // 2 if nums[mid] == target: return mid if target >= nums[0]: if nums[mid] > target or nums[mid] < nums[0]: right = mid - 1 else: left = mid + 1 else: if nums[mid] < target or nums[mid] >= nums[0]: left = mid + 1 else: right = mid - 1 return -1
应用2:Leetcode 81. 搜索旋转排序数组 II
题目
分析
当数组中存在重复元素时,二分查找时,可能会遇到这种情况:
此时,无法判断区间应该在左侧,还是右侧缩小。
例如,
这种情况,只需要同时缩小左右两侧的边界即可,即左边界加 1,右边界减 1,然后在新的区间上继续二分查找即可。
代码实现
class Solution: def search(self, nums: List[int], target: int) -> bool: n = len(nums) if not nums: return False if n == 1: return nums[0] == target left, right = 0, len(nums) - 1 while left <= right: mid = left + (right - left) // 2 if nums[mid] == target: return True # 左右两侧元素,与区间中点元素相等,则同时移动左右指针 if nums[left] == nums[mid] and nums[right] == nums[mid]: left += 1 right -= 1 elif nums[left] <= nums[mid]: if nums[left] <= target < nums[mid]: right = mid - 1 else: left = mid + 1 else: if nums[mid] < target <= nums[-1]: left = mid + 1 else: right = mid - 1 return False
应用3:Leetcode 378. 有序矩阵中第 K 小的元素
题目
给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。
你必须找到一个内存复杂度优于的解决方案。
示例:
输入:
,
输出:
解释:矩阵中的元素为,第 小元素是
分析
方法一:二分查找
矩阵中最小的元素是
我们假设第
因此,我们可以定一个查找区间
这样,我们就可以,通过二分法,查找区间
对于排序之后的第
对于区间内的中间值
-
从左下往右上移动
-
如果遇到矩阵中元素大于
,则向右移动; -
如果遇到矩阵中元素小于
,则向上移动;
-
-
统计矩阵中,左上部分的元素个数
-
如果左上部分的元素个数大于
,则说明 较大,需要移动区间右侧指针; -
如果左上部分的元素个数小于
,则说明 较小,需要移动区间左侧指针;
-
方法二:归并排序
将矩阵中的每一行的第一个元素及其坐标压入优先级队列(小根堆),每次弹出一个元素,同时,将该行下一个元素及其坐标压入优先级队列,进行
代码实现
方法一
class Solution: def kthSmallest(self, matrix: List[List[int]], k: int) -> int: n = len(matrix) left, right = matrix[0][0], matrix[-1][-1] while left < right: mid = (left + right) // 2 if self.check(mid, matrix, k): right = mid else: left = mid + 1 return left @classmethod def check(cls, mid, matrix, k): n = len(matrix) # 左下角的坐标 i, j = n - 1, 0 num = 0 # 坐标在矩阵内部可以移动的路线,根据移动的路线,计算左侧元素数量 while i >= 0 and j < n: # 如果当前元素小于mid, 指针向右移动 if matrix[i][j] <= mid: num += i + 1 j += 1 # 如果当前元素大于mid,指针向上移动 else: i -= 1 # 左侧元素的个数大于k,则移动right指针,反之,移动left return num >= k
方法二
class Solution: def kthSmallest(self, matrix: List[List[int]], k: int) -> int: n = len(matrix) # 取出矩阵第一列 queue = [(matrix[i][0], i, 0) for i in range(n)] heapq.heapify(queue) for i in range(k - 1): num, x, y = heapq.heappop(queue) if y != n - 1: heapq.heappush(queue, (matrix[x][y + 1], x, y + 1)) num, _, _ = heapq.heappop(queue) return num
应用4:Leetcode 373. 查找和最小的 K 对数字
题目
给定两个以 升序排列 的整数数组
和 , 以及一个整数 。
定义一对值,其中第一个元素来自 ,第二个元素来自 。
请找到和最小的个数对 , , , 。
示例:
输入:
, ,
输出:
解释: 返回序列中的前 2 对数:
分析
方法一:二分查找
与前面 378. 有序矩阵中第 K 小的元素 的思路类似。
假设
算法步骤:
-
首先使用二分查找,找到第
个数对的和 ,二分查找的思路如下:-
查找区间:
; -
查找条件:满足数对和小于
的数对个数; -
查找过程中,我们维护两个指针
, ,使 顺序遍历 ,使 倒序遍历 :-
如果数对和小于区间中间值,则向右移动指针
; -
如果数对和大于区间中间值,则向左移动指针
;
-
-
-
然后,从小到大依次查找数对和小于
的元素组合,并加入结果集 ; -
再记录数对和等于
的元素组合,如果结果集 等于 ,则停止记录。
方法二:多路并归
假设
我们维护一个优先级队列,先将
压入优先级队列。
遍历
算法步骤:
-
先将
与 中所有元素的组合之和及对应的元素序号压入优先级队列; -
每次弹出堆顶的对象,并将元素组合加入结果集;
-
对于堆顶的对象的元素的序号组合
:-
如果
的下一个元素满足 ,就将 中的下一个元素压入优先级队列; -
否则,就继续弹出堆中的剩余元素。
-
-
重复弹出
次后,就得到和最小的 个数对了。
代码实现
方法一
class Solution: def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]: from typing import List class Solution: def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]: m, n = len(nums1), len(nums2) pair_sum = self.findKSmallest(nums1, nums2, k) result = [] # 找数对和小于 pair_sum 的数对 i = n - 1 for _num in nums1: while i >= 0 and _num + nums2[i] >= pair_sum: i -= 1 # 因为原数组是升序排列的,所以先顺序记录结果,保证小的结果在前面 for j in range(i + 1): result.append([_num, nums2[j]]) # 找数对和等于 pair_sum 的数对 i = n - 1 for _num in nums1: while i >= 0 and _num + nums2[i] > pair_sum: i -= 1 # 只寻找数对和等于pair_sum的组合,所以倒序记录 for j in range(i, -1, -1): if _num + nums2[j] == pair_sum: result.append([_num, nums2[j]]) if len(result) == k: return result return result def findKSmallest(self, nums1: List[int], nums2: List[int], k: int): """ 通过二分查找第 k 小的数对和 """ m, n = len(nums1), len(nums2) # 查找区间 left, right = nums1[0] + nums2[0], nums1[m - 1] + nums2[n - 1] while left < right: mid = (left + right) // 2 if self._check(mid, nums1, nums2, k): left = mid + 1 else: right = mid return left def _check(self, mid: int, nums1: List[int], nums2: List[int], k: int): m, n = len(nums1), len(nums2) count = 0 # 使用两个指针i, j,使i顺序遍历nums1,使j倒序遍历nums2 i, j = 0, n - 1 while i < m and j >= 0: # 记录数对和小于mid的个数 if nums1[i] + nums2[j] <= mid: count += j + 1 i += 1 else: j -= 1 return count < k
方法二
import heapq from typing import List class Solution: def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]: m, n = len(nums1), len(nums2) results = [] pq = [(nums1[i] + nums2[0], i, 0) for i in range(min(k, m))] while pq and len(results) < k: _, i, j = heapq.heappop(pq) results.append([nums1[i], nums2[j]]) if j + 1 < n: heapq.heappush(pq, (nums1[i] + nums2[j + 1], i, j + 1)) return results
应用4:Leetcode 786. 第 K 个最小的素数分数
题目
给你一个按递增顺序排序的数组
和一个整数 。数组 由 和若干 素数 组成,且其中所有整数互不相同。
对于每对满足的 和 ,可以得到分数 。
那么第个最小的分数是多少呢? 以长度为 的整数数组返回你的答案, 这里 且 。
示例 1:
输入:
输出:
解释:已构造好的分数,排序后如下所示:,很明显第三个最小的分数是
分析
方法一:二分查找
这里,我们假设第
并且当分母固定时,由于分子是一个递增序列,所以,该分数序列也是递增序列,因此,这个分数序列满足二分查找的条件。
算法步骤:
-
在查找区间:
,左右都是开区间 -
对区间进行折半查找,对于每一个区间的中点
:-
我们使用双指针的思路,维护两个指针
、 :使 指向分子,使 指向分母; -
通过指针
枚举所有的分母: ; -
通过指针
枚举所有的分子: ;为什么从
开始枚举?因为,我们要求的是小于
的分数组合,如果从零开始,即使 ,小于等于 的分数组合是 个,所以我们可以直接跳过这个组合。 -
对于任意一个分母
,指针 会不断向右移动,并且,保证 一直成立;当指针
无法移动时,显然,数组的前 个元素: 都可以作为分子,使得 成立,但是, 及其之后的元素不能作为分子。 -
我们将所有分数值小于
的组合个数累加起来,就得到小于 的分数组合个数 ,同时,我们将恰好等于 的元素组合也记录下来,用于最后的结果返回
-
-
区间缩小的策略,需要通过
判断:-
如果
, 则将右指针 指向 ; -
如果
, 则将左指针 指向 ;
-
-
当右指针
和左指针 相等时,则结束查找。
方法二:多路并归
假设数组
由于数组
也是严格递增的,这样的序列一共有
我们可以将这
初始条件,先将矩阵的第一列压入堆中,即将
每次从堆中弹出一个对象时,就将分子对应的元素后移一个位置,重复
代码实现
方法一
- Java
class Solution { double eps = 1e-8; int a, b; public int[] kthSmallestPrimeFraction(int[] arr, int k) { double left = 0, right = 1; while (right - left > eps) { double mid = (left + right) / 2; if (check(arr, mid) >= k) { right = mid; } else { left = mid; } } return new int[]{a, b}; } int check(int[] array, double x){ int count = 0; int i = 0; for (int j = 1; j < array.length; j++) { while (array[i + 1] * 1.0 / array[j] <= x) { i++; } if (array[i] * 1.0 / array[j] <= x) { count += i + 1; } // 记录分数值恰好等于 x 的元素组合 if (Math.abs(array[i] * 1.0 / array[j] - x) < eps) { a = array[i]; b = array[j]; } } return count; } }
- Python
from typing import List EPS = 1e-8 class Solution: def kthSmallestPrimeFraction(self, arr: List[int], k: int) -> List[int]: a, b = 0, 0 left, right = 0.0, 1.0 while right - left > EPS: mid = left + (right - left) / 2 result = self._check(arr, mid, k) if result[0]: right = mid a, b = result[1], result[2] else: left = mid return [a, b] def _check(self, array: List[int], x: float, k): count = 0 i = 0 a, b = 0, 0 for j in range(1, len(array)): while array[i + 1] * 1.0 / array[j] <= x: i += 1 if array[i] * 1.0 / array[j] <= x: count += i + 1 # 记录分数值恰好等于 x 的元素组合 if abs(array[i] * 1.0 / array[j] - x) < EPS: a = array[i] b = array[j] return count >= k, a, b
方法二
import heapq from typing import List class Node(object): def __init__(self, index_x: int, index_y: int, x: int, y: int): """ x/y """ self.index_x = index_x self.index_y = index_y self.x = x self.y = y def __lt__(self, other): return self.x * other.y < self.y * other.x class Solution: def kthSmallestPrimeFraction(self, arr: List[int], k: int) -> List[int]: n = len(arr) pq = [Node(0, j, arr[0], arr[j]) for j in range(n)] heapq.heapify(pq) for _ in range(k - 1): node = heapq.heappop(pq) if node.index_x + 1 < n: next_node = Node(node.index_x + 1, node.index_y, arr[node.index_x + 1], node.y) heapq.heappush(pq, next_node) node = pq[0] return [node.x, node.y]
应用5:Leetcode 719. 找出第 K 小的数对距离
题目
分析
因为第
那么,我们可以在区间:
算法步骤:
-
先将数组
从小到大排序; -
维护两个指针
、 ,在区间: ,进行二分查找; -
计算当前数对距离的折半值
: -
对于给定的数对距离
,我们需要计算:所有距离小于 的数对个数 ,这里也可以使用二分查找:-
枚举数组
中所有元素,并以它作为数对 的右端点 ; -
再对区间
,进行二分查找,找到所有大于等于 的最小下标 ;这样,在区间
内的所有数对 ,一定满足: 。 -
累加区间的数对个数
。
-
-
对于当前的距离
:-
如果
大于等于 ,说明当前距离值过大,则右指针 减 ; -
如果
小于 ,说明当前距离值过小,则左指针 加 ;
-
代码实现
import heapq class Solution: def smallestDistancePair(self, nums: List[int], k: int) -> int: nums.sort() n = len(nums) left, right = 0, nums[n - 1] - nums[0] while left <= right: mid = left + (right - left) // 2 if self.check(nums, k, mid): right = mid - 1 else: left = mid + 1 return left def check(self, nums: List[int], k: int, mid: int) -> bool: count = 0 for j in range(len(nums)): count += j - self.binary_search(nums, j, nums[j] - mid) return count >= k def binary_search(self, nums: List[int], end: int, target: int) -> int: left, right = 0, end while left < right: mid = left + (right - left) // 2 if nums[mid] < target: left = mid + 1 else: right = mid return left
应用6:Leetcode 540. 有序数组中的单一元素
题目
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足时间复杂度和 空间复杂度。
示例 1:
输入:
nums = [1,1,2,3,3,4,4,8,8]
输出: 2
分析
假设只出现出现一次的元素是
容易发现如下性质:
-
对于元素
左侧的元素,如果前后两个元素相等,即 ,那么,序号 一定是偶数; -
对于元素
右侧的元素,如果前后两个元素相等,即 ,那么,序号 一定是奇数。
可以看出,元素
因此,我们可以在数组
-
如果
是偶数,则比较 与 是否相等-
如果
,说明 ,需要移动左边界; -
如果
,说明 ,需要移动右边界。
-
-
如果
是奇数,则比较 与 是否相等-
如果
,说明 ,需要移动左边界; -
如果
,说明 ,需要移动右边界。
-
-
重复上述过程,直到找到元素
。
这里,可以利用按位异或的性质,得到
-
如果
是偶数,那么,有 ; -
如果
是奇数,那么,有 。
因此,在二分查找过程中,不需要判断
以如下例子,来说明查找过程:
-
第 1 次查找,查找区间:
, , , 移动左指针: ; -
第 2 次查找,查找区间:
, , , 移动右指针: ; -
第 3 次查找,查找区间:
, , , 移动左指针: ,找到只出现一次的元素 。
代码实现
from typing import List class Solution: def singleNonDuplicate(self, nums: List[int]) -> int: low, high = 0, len(nums) - 1 while low < high: mid = (low + high) // 2 if nums[mid] == nums[mid ^ 1]: low = mid + 1 else: high = mid return nums[low]
扩展
如果数组是无序的,就可以将所有元素进行按位异或即可找到只出现一次的元素。
本文作者:LARRY1024
本文链接:https://www.cnblogs.com/larry1024/p/17460083.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix