二分搜索的应用
简介
应用
典型应用如下:
序号 | 题目 |
---|---|
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
题目
分析
当数组中存在重复元素时,二分查找时,可能会遇到这种情况:
此时,无法判断区间应该在左侧,还是右侧缩小。
例如,\(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 小的元素
题目
给你一个 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\) 一定满足:
因此,我们可以定一个查找区间 \([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 对数字
题目
给定两个以 升序排列 的整数数组 \(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\) 中所有元素的组合之和及对应的元素序号,即
压入优先级队列。
遍历 \(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 个最小的素数分数
题目
给你一个按递增顺序排序的数组 \(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,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\) 是严格递增,因此,序列
也是严格递增的,这样的序列一共有 \(n\) 组。
我们可以将这 \(n\) 组序列,看成一个 \(n\) 维矩阵。
初始条件,先将矩阵的第一列压入堆中,即将 \(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 小的数对距离
题目
分析
因为第 \(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. 有序数组中的单一元素
题目
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 \(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]\)两个元素即可。
以如下例子,来说明查找过程:
-
第 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]
扩展
如果数组是无序的,就可以将所有元素进行按位异或即可找到只出现一次的元素。