2023-06-06 15:11阅读: 19评论: 0推荐: 0

二分搜索的应用

简介

应用

典型应用如下:

应用1:Leetcode 33. 搜索旋转排序数组

题目

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

题目

81. 搜索旋转排序数组 II

分析

当数组中存在重复元素时,二分查找时,可能会遇到这种情况:

nums[left]=nums[mid]=nums[right]

此时,无法判断区间应该在左侧,还是右侧缩小。

例如,nums=[3,1,2,3,3,3,3,3],target=2,第一次二分时,区间中点:nums[3]=3,区间被分成两个子区间:nums[02]=[3,1,2]nums[47]=[3,3,3,3],此时,区间中点元素3 与左右边界元素相同。

这种情况,只需要同时缩小左右两侧的边界即可,即左边界加 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 小的元素

题目

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

分析

方法一:二分查找

矩阵中最小的元素是 matrix[0][0],最大的元素是 matrix[1][1],并且在矩阵中的每一行和每一列都是有序的。

我们假设第 k 小元素是 target,那么, target 一定满足:

matrix[0][0]<=target<=matrix[1][1]

因此,我们可以定一个查找区间 [left,right]=[matrix[0][0], matrix[1][1]],查找的目标值 target 只需要满足它是排序之后的第 k 小元素即可。

这样,我们就可以,通过二分法,查找区间 [left,right] 内的所有数值

对于排序之后的第 k 小元素,我们可以利用矩阵每行每列都是有序的性质,从矩阵左下角,寻找一条到右上角的路线,并通过计算路线左上半部分的区域的元素个数,从而判断区间中点是否满足它是第 k 小元素的条件。

对于区间内的中间值 mid,左右区间缩小的判断条件是:

  • 从左下往右上移动

    • 如果遇到矩阵中元素大于 mid,则向右移动;

    • 如果遇到矩阵中元素小于 mid,则向上移动;

  • 统计矩阵中,左上部分的元素个数

    • 如果左上部分的元素个数大于 k,则说明 mid 较大,需要移动区间右侧指针;

    • 如果左上部分的元素个数小于 k,则说明 mid 较小,需要移动区间左侧指针;

方法二:归并排序

将矩阵中的每一行的第一个元素及其坐标压入优先级队列(小根堆),每次弹出一个元素,同时,将该行下一个元素及其坐标压入优先级队列,进行 k1 次之后,第 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 对数字

题目

373. 查找和最小的 K 对数字

给定两个以 升序排列 的整数数组 nums1nums2 , 以及一个整数 k 。
定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 。
请找到和最小的 k 个数对 (u1,v1),  (u2,v2)  , ,  (uk,vk) 。

示例:

输入: nums1=[1,1,2], nums2=[1,2,3], k=2
输出: [1,1],[1,1]
解释: 返回序列中的前 2 对数:[1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]

分析

方法一:二分查找

与前面 378. 有序矩阵中第 K 小的元素 的思路类似。

假设 nums1nums2 中的元素个数分别为 m, n

算法步骤:

  • 首先使用二分查找,找到第 k 个数对的和 pairSum,二分查找的思路如下:

    • 查找区间:[left, right]=[nums1[0]+nums2[0], nums1[m1]+nums2[n1]]

    • 查找条件:满足数对和小于 k 的数对个数;

    • 查找过程中,我们维护两个指针 i, j,使 i 顺序遍历 nums1,使 j 倒序遍历 nums2

      • 如果数对和小于区间中间值,则向右移动指针 i

      • 如果数对和大于区间中间值,则向左移动指针 j

  • 然后,从小到大依次查找数对和小于 pairSum 的元素组合,并加入结果集 result

  • 再记录数对和等于 pairSum 的元素组合,如果结果集 result 等于 k,则停止记录。

方法二:多路并归

假设 nums1nums2 中的元素个数分别为 m, n

我们维护一个优先级队列,先将 nums2[0]nums1 中所有元素的组合之和及对应的元素序号,即

(nums1[0]+nums2[0],0,0),(nums1[1]+nums2[0],1,0),,(nums1[m1]+nums2[0],m1,0)

压入优先级队列。

遍历 nums2 中剩余元素,即可得到结果集。

算法步骤:

  • 先将 nums2[0]nums1 中所有元素的组合之和及对应的元素序号压入优先级队列;

  • 每次弹出堆顶的对象,并将元素组合加入结果集;

  • 对于堆顶的对象的元素的序号组合 (i,j)

    • 如果 nums2[j] 的下一个元素满足 j+1<n,就将 nums2 中的下一个元素压入优先级队列;

    • 否则,就继续弹出堆中的剩余元素。

  • 重复弹出 k 次后,就得到和最小的 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 个最小的素数分数

题目

786. 第 K 个最小的素数分数

给你一个按递增顺序排序的数组 arr 和一个整数 k 。数组 arr1 和若干 素数  组成,且其中所有整数互不相同。
对于每对满足 0i<j<arr.lengthij ,可以得到分数 arr[i]arr[j]
那么第 k 个最小的分数是多少呢?  以长度为 2 的整数数组返回你的答案, 这里 answer[0]==arr[i] 且 answer[1]==arr[j]

示例 1:

输入:arr=[1,2,3,5],k=3
输出:[2,5]
解释:已构造好的分数,排序后如下所示: 1/5,1/3,2/5,1/2,3/5,2/3,很明显第三个最小的分数是 2/5

分析

方法一:二分查找

这里,我们假设第 k 小的分数为 x,那么,对于任意两个元素arr[i],arr[j]的组合,都可以得到分数 arr[i]arr[j],并且,它一定满足:

0arr[i]arr[j]1

并且当分母固定时,由于分子是一个递增序列,所以,该分数序列也是递增序列,因此,这个分数序列满足二分查找的条件。

算法步骤:

  • 在查找区间:(0,1),左右都是开区间

  • 对区间进行折半查找,对于每一个区间的中点 mid

    • 我们使用双指针的思路,维护两个指针 ij:使 i 指向分子,使 j 指向分母;

    • 通过指针 j 枚举所有的分母:arr[1],,arr[n1]

    • 通过指针 i 枚举所有的分子:arr[1],,arr[j1]

      为什么从 1 开始枚举?

      因为,我们要求的是小于 x 的分数组合,如果从零开始,即使 arr[0]arr[j]x,小于等于 x 的分数组合是 0 个,所以我们可以直接跳过这个组合。

    • 对于任意一个分母 arr[j],指针 i 会不断向右移动,并且,保证 arr[i]arr[j]x 一直成立;

      当指针 i 无法移动时,显然,数组的前 i+1 个元素:arr[0],,arr[i] 都可以作为分子,使得 arr[i]arr[j]x 成立,但是,arr[i+1]及其之后的元素不能作为分子。

    • 我们将所有分数值小于 x 的组合个数累加起来,就得到小于 x 的分数组合个数 count,同时,我们将恰好等于 x 的元素组合也记录下来,用于最后的结果返回

  • 区间缩小的策略,需要通过 count判断:

    • 如果 countk, 则将右指针 right 指向 mid

    • 如果 count<k, 则将左指针 left 指向 mid

  • 当右指针 right 和左指针 left 相等时,则结束查找。

方法二:多路并归

假设数组 arr 的长度为 n ,当分母固定为某一个值 arr[j] 时,分子可以在 arr[0],,arr[j1] 中选择。

由于数组 arr 是严格递增,因此,序列

arr[i]arr[j]=arr[0]arr[j],,arr[n1]arr[j]

也是严格递增的,这样的序列一共有 n 组。

我们可以将这 n 组序列,看成一个 n 维矩阵。

初始条件,先将矩阵的第一列压入堆中,即将 n1 个分子最小的序列压入堆中,即:

arr[0]arr[j]=arr[0]arr[0],,arr[0]arr[n1]

每次从堆中弹出一个对象时,就将分子对应的元素后移一个位置,重复 k1 次后,堆顶对应的元素,就是第 k 小的分数。

代码实现

方法一

  • 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 小的数对距离

题目

719. 找出第 K 小的数对距离

分析

因为第 k 小的数必然满足:0kmax(nums)min(nums),因此,我们先将数组 nums 从小到大排序

那么,我们可以在区间:[0,max(nums)min(nums)] 通过二分查找,找到第 k 小的数对。

算法步骤:

  • 先将数组 nums 从小到大排序

  • 维护两个指针 leftright,在区间:[0,nums[1]nums[0]],进行二分查找;

  • 计算当前数对距离的折半值 mid

    mid=(left+right)/2

  • 对于给定的数对距离 mid,我们需要计算:所有距离小于 mid 的数对个数 count,这里也可以使用二分查找:

    • 枚举数组 nums 中所有元素,并以它作为数对 (nums[i],nums[j]) 的右端点 nums[j]

    • 再对区间 nums[0],,nums[j],进行二分查找,找到所有大于等于 nums[j]mid最小下标 i

      这样,在区间 [i,j] 内的所有数对 (nums[i],nums[j]),一定满足:nums[j]nums[i]<k

    • 累加区间的数对个数 ji

  • 对于当前的距离 mid

    • 如果 count 大于等于 k,说明当前距离值过大,则右指针 right1

    • 如果 count 小于 k,说明当前距离值过小,则左指针 left1

代码实现

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. 有序数组中的单一元素

题目

540. 有序数组中的单一元素

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

示例 1:

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

分析

假设只出现出现一次的元素是 nums[x],而其余元素都出现了两次,因此数组的长度一定是奇数,并且由于数组是有序的,那么,相同的元素一定是相邻的。

容易发现如下性质:

  • 对于元素 nums[x] 左侧的元素,如果前后两个元素相等,即 nums[i]=nums[i+1],那么,序号 i 一定是偶数;

  • 对于元素 nums[x] 右侧的元素,如果前后两个元素相等,即 nums[i]=nums[i+1],那么,序号 i 一定是奇数。

可以看出,元素 nums[x] 是两个子数组的分界,满足二段性。

因此,我们可以在数组 nums 上使用二分查找,每次取左右边界的平均值 mid ,根据 mid 的奇偶性判断:

  • 如果 mid 是偶数,则比较 nums[mid]nums[mid+1] 是否相等

    • 如果 nums[mid]=nums[mid+1],说明 mid<x,需要移动左边界;

    • 如果 nums[mid]nums[mid+1],说明 midx,需要移动右边界。

  • 如果 mid 是奇数,则比较 nums[mid1]nums[mid] 是否相等

    • 如果 nums[mid]=nums[mid1],说明 mid<x,需要移动左边界;

    • 如果 nums[mid]nums[mid1],说明 midx,需要移动右边界。

  • 重复上述过程,直到找到元素 nums[x]

这里,可以利用按位异或的性质,得到 mid 与其相邻的元素,如下:

  • 如果 mid 是偶数,那么,有 mid+1=mid1

  • 如果 mid 是奇数,那么,有 mid1=mid1

因此,在二分查找过程中,不需要判断 mid 的奇偶性,只需要比较 nums[mid]nums[mid1]两个元素即可。

以如下例子,来说明查找过程:

image

  • 第 1 次查找,查找区间:[08]mid=4, mid1=5, 移动左指针:left=5

  • 第 2 次查找,查找区间:[58]mid=6, mid1=7, 移动右指针:right=6

  • 第 3 次查找,查找区间:[56]mid=5, mid1=4, 移动左指针:left=6,找到只出现一次的元素 4

代码实现

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 中国大陆许可协议进行许可。

posted @   LARRY1024  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.