【算法拾光】二分查找|有序数组中的单一元素 + 寻找两个正序数组的中位数

题目 1:有序数组中的单一元素

链接:540. 有序数组中的单一元素 - 力扣(LeetCode)

题目分析

给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。要求找出只出现一次的那个数,并且时间复杂度 \(O(logn)\)、空间 \(O(1)\)。 例:nums = [1,1,2,3,3,4,4,8,8],答案是 2。 核心:配对结构天然形成二段性 ------ 单数之前的元素都是「偶数下标与下一个相同」(成对出现),单数之后全部错位。利用这个性质二分。

前提:数组下标从 0 开始,成对出现的元素满足 nums[2k] == nums[2k+1];单数出现后,这个规律被打破。

算法原理

利用「成对元素」的错位来判断单数在左还是右:

  1. 区间 [0, n-1],n 是数组长度(必为奇数)
  2. 取 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
  3. 循环 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))) 求出中位数。

这是二分系列最难的题之一,也是面试高频压轴,务必吃透。

算法原理

在较短数组上二分分割点,利用划分 + 交叉比较:

  1. 设较长 / 较短数组。始终让 nums1 是较短的那个(保证 left 循环边界正确),m ≤ n
  2. 二分 nums1 的分割点 i(左边有 i 个元素),则 nums2 分割点 j = (m+n+1)/2 - i,保证左半总元素数固定
  3. 每次检查是否满足交叉条件:
    • 若 nums1[i-1] > nums2[j]:nums1 左半太大,i 要左移 → right = i - 1
    • 否则:当前划分合法,记录边界,i 向右试探 → left = i + 1
  4. 循环结束后,根据边界计算中位数:
    • 总数为奇数:中位数 = 左半最大值
    • 总数为偶数:中位数 = (左半最大值 + 右半最小值) / 2
  5. 边界处理:用 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 兜底,奇偶分情况求中位数。理解「划分点把两个数组合并成左右两半且左半数量固定」是突破关键。


本篇总结

本篇两道题把二分从「查找」推向「划分」:

  1. 有序数组中的单一元素:用奇偶配对构造二段性,mid ^ 1 找配对,新颖且简洁
  2. 寻找两个正序数组的中位数:二分的最高阶 ------划分型二分,在短数组上二分分割点,交叉比较驱动收敛,O (log (min (m,n)))

到这里二分系列已经覆盖完整:普通二分、左右边界、二分答案、布尔二段性、无序 / 旋转数组、奇偶配对、划分型中位数。核心始终是 ------只要数据具有「二段性」就能二分。

相关推荐
Be for thing2 小时前
PyCharm安装与PyTorch项目配置|Jupyter Notebook与PyCharm使用对比
pytorch·学习·1024程序员节
weixin_440401692 小时前
大数据技术-人工智能编程题
大数据·人工智能·python·1024程序员节
代码不停2 小时前
分布式链路追踪 SkyWalking介绍
skywalking·1024程序员节
AIyy8662 小时前
学习资料分类整理的知识库工具使用思路:从统一入口到本地沉淀
1024程序员节
倔强的石头1062 小时前
模型合并技术_SLERP_TIES_Merge_DARE
人工智能·1024程序员节
只是甲2 小时前
本体工程十五讲 | 第 2 篇:知识表示的载体谱系——从三元组到 YAML 字段
nl2sql·1024程序员节·ai agent·智能问数·本体建模
Yuzhiyuxia2 小时前
【AI Agent面试系列】AI Agent基础知识
1024程序员节
虚无的纽扣2 小时前
【Linux】线程同步:条件不满足时,线程该怎么等
linux·服务器·1024程序员节
m0_564264182 小时前
2026年,我对“全栈”这个词的理解变了
1024程序员节