
题目 1:有序数组中的单一元素
链接:540. 有序数组中的单一元素 - 力扣(LeetCode)
题目分析
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。要求找出只出现一次的那个数,并且时间复杂度 \(O(logn)\)、空间 \(O(1)\)。 例:nums = [1,1,2,3,3,4,4,8,8],答案是 2。 核心:配对结构天然形成二段性 ------ 单数之前的元素都是「偶数下标与下一个相同」(成对出现),单数之后全部错位。利用这个性质二分。
前提:数组下标从 0 开始,成对出现的元素满足
nums[2k] == nums[2k+1];单数出现后,这个规律被打破。
算法原理
利用「成对元素」的错位来判断单数在左还是右:
- 区间
[0, n-1],n 是数组长度(必为奇数) - 取
mid,判断 mid 的奇偶性来配对:- 若
mid是偶数,正常配对是nums[mid] == nums[mid+1] - 若
mid是奇数,正常配对是nums[mid] == nums[mid-1] - 统一判断:若
nums[mid] == nums[mid ^ 1](异或 1 在奇偶间切换配对下标),说明单数在右侧 →left = mid + 1 - 否则说明左侧配对已错位,单数在左侧 →
right = mid
- 若
- 循环
left < right,结束返回nums[left]
技巧:用
mid ^ 1拿到配对下标,奇偶统一处理,代码最简洁。
代码编写
cpp
int singleNonDuplicate(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while(left < right) {
int mid = left + (right - left) / 2;
if(nums[mid] == nums[mid ^ 1]) {
left = mid + 1; // 配对正常,单数在右侧
} else {
right = mid; // 配对错位,单数在左侧
}
}
return nums[left];
}
总结
本题利用奇偶配对 构造二段性:配对正常则单数在后,错位则单数在前。mid ^ 1 是处理配对下标的经典小技巧,配合 left < right 模板,O (logn) 解决。面试中偏中等,逻辑新颖。
题目 2:寻找两个正序数组的中位数
链接:4. 寻找两个正序数组的中位数 - 力扣(LeetCode)
题目分析
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。要求找出并返回这两个正序数组的中位数 ,算法的时间复杂度应该为 \(O(log(m+n))\)。 例:nums1 = [1,3], nums2 = [2],中位数 2.0。 核心:要求 O (log (m+n)),必须二分。中位数本质是把两个数组合并后处于中间位置的值;我们通过在较短数组上二分分割点,使左右两部分元素个数均衡且满足「左半最大值 ≤ 右半最小值」,从而 O (log (min (m,n))) 求出中位数。
这是二分系列最难的题之一,也是面试高频压轴,务必吃透。
算法原理
在较短数组上二分分割点,利用划分 + 交叉比较:
- 设较长 / 较短数组。始终让 nums1 是较短的那个(保证
left循环边界正确),m ≤ n - 二分 nums1 的分割点
i(左边有 i 个元素),则 nums2 分割点j = (m+n+1)/2 - i,保证左半总元素数固定 - 每次检查是否满足交叉条件:
- 若
nums1[i-1] > nums2[j]:nums1 左半太大,i 要左移 →right = i - 1 - 否则:当前划分合法,记录边界,i 向右试探 →
left = i + 1
- 若
- 循环结束后,根据边界计算中位数:
- 总数为奇数:中位数 = 左半最大值
- 总数为偶数:中位数 = (左半最大值 + 右半最小值) / 2
- 边界处理:用
INT_MIN/INT_MAX处理 i=0、i=m、j=0、j=n 的越界情况
核心难点:理解划分如何保证「左半元素个数 = 右半元素个数(或左多一个)」,以及交叉比较如何驱动二分收敛。
代码编写
cpp
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
if(nums1.size() > nums2.size()) swap(nums1, nums2);
int m = nums1.size(), n = nums2.size();
int totalLeft = (m + n + 1) / 2;
int left = 0, right = m;
while(left <= right) {
int i = left + (right - left) / 2;
int j = totalLeft - i;
int nums1LeftMax = (i == 0) ? INT_MIN : nums1[i - 1];
int nums1RightMin = (i == m) ? INT_MAX : nums1[i];
int nums2LeftMax = (j == 0) ? INT_MIN : nums2[j - 1];
int nums2RightMin = (j == n) ? INT_MAX : nums2[j];
if(nums1LeftMax > nums2RightMin) {
right = i - 1; // nums1 左半太大,i 左移
} else {
left = i + 1; // 划分合法,继续试探
}
}
// 循环结束后,用最终合法划分计算中位数
int i = left;
int j = totalLeft - i;
int nums1LeftMax = (i == 0) ? INT_MIN : nums1[i - 1];
int nums1RightMin = (i == m) ? INT_MAX : nums1[i];
int nums2LeftMax = (j == 0) ? INT_MIN : nums2[j - 1];
int nums2RightMin = (j == n) ? INT_MAX : nums2[j];
int leftMax = max(nums1LeftMax, nums2LeftMax);
int rightMin = min(nums1RightMin, nums2RightMin);
if((m + n) % 2 == 1) return leftMax;
return (leftMax + rightMin) / 2.0;
}
总结
本题是二分查找的巅峰题:通过在短数组上二分分割点 + 固定总数划分 + 交叉比较 ,达到 O (log (min (m,n)))。四个边界用 INT_MIN/INT_MAX 兜底,奇偶分情况求中位数。理解「划分点把两个数组合并成左右两半且左半数量固定」是突破关键。
本篇总结
本篇两道题把二分从「查找」推向「划分」:
- 有序数组中的单一元素:用奇偶配对构造二段性,
mid ^ 1找配对,新颖且简洁 - 寻找两个正序数组的中位数:二分的最高阶 ------划分型二分,在短数组上二分分割点,交叉比较驱动收敛,O (log (min (m,n)))
到这里二分系列已经覆盖完整:普通二分、左右边界、二分答案、布尔二段性、无序 / 旋转数组、奇偶配对、划分型中位数。核心始终是 ------只要数据具有「二段性」就能二分。
