二分搜索的应用

简介

应用

典型应用如下:

序号 题目
1 33. 搜索旋转排序数组
2 81. 搜索旋转排序数组 II
3 378. 有序矩阵中第 K 小的元素
4 373. 查找和最小的 K 对数字
5 719. 找出第 K 小的数对距离

应用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[0\cdots2]=[3,1,2]\)\(nums[4\cdots7]=[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(n^2)\) 的解决方案。

示例:

输入:\(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\) 较小,需要移动区间左侧指针;

方法二:归并排序

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

给定两个以 升序排列 的整数数组 \(nums1\)\(nums2\) , 以及一个整数 \(k\) 。
定义一对值 \((u,v)\),其中第一个元素来自 \(nums1\),第二个元素来自 \(nums2\) 。
请找到和最小的 \(k\) 个数对 \((u_1,v_1)\),  \((u_2,v_2)\)  , \(\cdots\),  \((u_k,v_k)\) 。

示例:

输入: \(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 小的元素 的思路类似。

假设 \(nums1\)\(nums2\) 中的元素个数分别为 \(m\), \(n\)

算法步骤:

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

    • 查找区间:\([left,\ right] = [nums1[0] + nums2[0],\ nums1[m-1] + nums2[n-1]]\)

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

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

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

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

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

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

方法二:多路并归

假设 \(nums1\)\(nums2\) 中的元素个数分别为 \(m\), \(n\)

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

\[(nums1[0] + nums2[0], 0, 0), (nums1[1] + nums2[0],1, 0), \cdots, (nums1[m-1] + nums2[0], m-1, 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\) 。数组 \(arr\)\(1\) 和若干 素数  组成,且其中所有整数互不相同。
对于每对满足 \(0 \le i < j < arr.length\)\(i\)\(j\) ,可以得到分数 \(\frac{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]\)的组合,都可以得到分数 \(\frac{arr[i]}{arr[j]}\),并且,它一定满足:

\[0 \le \frac{arr[i]}{arr[j]} \le 1 \]

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

算法步骤:

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

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

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

    • 通过指针 \(j\) 枚举所有的分母:\(arr[1], \cdots, arr[n - 1]\)

    • 通过指针 \(i\) 枚举所有的分子:\(arr[1], \cdots, arr[j - 1]\)

      为什么从 \(1\) 开始枚举?

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

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

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

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

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

    • 如果 \(count \ge k\), 则将右指针 \(right\) 指向 \(mid\)

    • 如果 \(count \lt k\), 则将左指针 \(left\) 指向 \(mid\)

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

方法二:多路并归

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

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

\[\frac{arr[i]}{arr[j]} = \frac{arr[0]}{arr[j]}, \cdots, \frac{arr[n -1]}{arr[j]} \]

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

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

初始条件,先将矩阵的第一列压入堆中,即将 \(n - 1\) 个分子最小的序列压入堆中,即:

\[\frac{arr[0]}{arr[j]} = \frac{arr[0]}{arr[0]}, \cdots, \frac{arr[0]}{arr[n - 1]} \]

每次从堆中弹出一个对象时,就将分子对应的元素后移一个位置,重复 \(k - 1\) 次后,堆顶对应的元素,就是第 \(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\) 小的数必然满足:\(0 \le k \le \max(nums) - \min(nums)\),因此,我们先将数组 \(nums\) 从小到大排序

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

算法步骤:

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

  • 维护两个指针 \(left\)\(right\),在区间:\([0, nums[-1] - nums[0]]\),进行二分查找;

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

    \(mid = (left + right) / 2\)

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

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

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

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

    • 累加区间的数对个数 \(j - i\)

  • 对于当前的距离 \(mid\)

    • 如果 \(count\) 大于等于 \(k\),说明当前距离值过大,则右指针 \(right\)\(1\)

    • 如果 \(count\) 小于 \(k\),说明当前距离值过小,则左指针 \(left\)\(1\)

代码实现

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(\log_{2}n)\) 时间复杂度和 \(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] \ne nums[mid + 1]\),说明 \(mid \ge x\),需要移动右边界。

  • 如果 \(mid\) 是奇数,则比较 \(nums[mid - 1]\)\(nums[mid]\) 是否相等

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

    • 如果 \(nums[mid] \ne nums[mid - 1]\),说明 \(mid \ge x\),需要移动右边界。

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

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

  • 如果 \(mid\) 是偶数,那么,有 \(mid + 1 = mid \oplus 1\)

  • 如果 \(mid\) 是奇数,那么,有 \(mid - 1 = mid \oplus 1\)

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

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

image

  • 第 1 次查找,查找区间:\([0, 8]\)\(mid = 4\), \(mid \oplus 1 = 5\), 移动左指针:\(left = 5\)

  • 第 2 次查找,查找区间:\([5, 8]\)\(mid = 6\), \(mid \oplus 1 = 7\), 移动右指针:\(right = 6\)

  • 第 3 次查找,查找区间:\([5, 6]\)\(mid = 5\), \(mid \oplus 1 = 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]

扩展

如果数组是无序的,就可以将所有元素进行按位异或即可找到只出现一次的元素。


posted @ 2023-06-06 15:11  LARRY1024  阅读(9)  评论(0编辑  收藏  举报