题目描述
思路解析
一句话答案:LeetCode 540 有序数组中的单一元素:要找的是那个只出现一次的数;它像一根楔子,把右边的配对整体挤后一格,二分时把 mid 对齐偶数、看它和下一格是否成对就能定位,时间 O(log n)、空间 O(1)。
除一个数外全部成对,到底在找谁
给一个升序(从小到大排好)的整数数组,除一个数只出现一次外,其余每个数都恰好挨着出现两次。要在 O(log n) 时间、O(1) 空间里找出这个数,不许从头扫一遍。题面例子 nums=[1,1,2,3,3,4,4,8,8],成对的是 1、3、4、8,落单的是 2,返回 2。
两两一组扫过去,为什么摸不到 O(log n)
一个直白的办法是从头两两一组看,哪组对不上,落单的就在那。数组有 n 个数,最坏要看到接近末尾那组才发现,比满约 n 次,是 O(n)。n 涨到百万就得看百万组,题目点名的 O(log n) 把这条路堵死;就算改用全体异或抵消也还是 O(n),够不着对数要求。
有序又成对,凭什么能砍掉一半
钥匙在『成对』留下的下标规律上。没有那个落单的数时,每对的第一个都在偶数下标、第二个在紧跟的奇数下标:0、1 一对,2、3 一对,一路整齐。插进来一个落单的数,它像一根楔子,把右边所有的对整体往后挤一格——楔子右边,每对的第一个反倒跑到了奇数下标。
于是整根数组以落单的数为界:界左边『偶数下标和它下一格相等』一直成立,界右边这条规律一破就再不复原。正因为界单调,才能二分——每轮只留还可能藏落单数的那半,另一半整片丢掉。
mid 跟谁比、往哪半收,奇偶怎么摆平
用 lo、hi 两个指针圈住还没排除的范围,起初 lo=0、hi=len(nums)−1。每轮取 mid=(lo+hi)//2,比之前先摆到偶数下标:mid 是奇数就 mid−=1 退回前一格,好让 mid 和 mid+1 凑成一对。再看 nums[mid] 和 nums[mid+1]:相等说明这对规矩、落单数在右,lo 跳到 mid+2 跨过它;不相等说明偶数位配对已乱、落单数就在 mid 或更左,hi 收到 mid(mid 自己可能是答案,别减 1)。循环写 while lo<hi,缩到 lo、hi 撞上那格就是答案。
拿 [1,1,2,3,3,4,4,8,8] 手动收三轮
起点 lo=0、hi=8。第一轮 mid=4 是偶数不用动,nums[4]=3 与 nums[5]=4 不相等,落单数在左,hi 收到 4。第二轮 lo=0、hi=4,mid=2 仍是偶数,nums[2]=2 与 nums[3]=3 不相等,hi 再收到 2。第三轮 lo=0、hi=2,mid=(0+2)//2=1 是奇数,先 mid−=1 退到 0,nums[0]=1 与 nums[1]=1 相等,落单数在右,lo 跳到 mid+2=2。此时 lo=hi=2 撞上,循环停,返回 nums[2]=2。
配对当场比错,就因为 mid 落奇数位没退回
两处一步走错答案就偏了。mid 落奇数位却不退回偶数,nums[mid] 会跟前一对的尾巴去比,成对前提整个塌掉、结论全反,补上 mid−=1 才稳。收右指针若写成 hi=mid−1,碰上 mid 正好是落单数那轮就被跳过去、收敛到错格,得写 hi=mid。每轮范围减半,n 个数最多 log₂n 轮,时间 O(log n);全程只动 lo、hi、mid 三指针,空间 O(1)。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
笨办法谁都会:两两一组看,哪组对不上单的就在那。可数组百万长就慢了。题目特意给了「有序」这个条件,就是在提示你——能二分。
这是全题的命根子。单一元素像一根楔子插进队列,把它后面所有配对整体往右挤了一格,配对的「奇偶节奏」就此被打乱。下面用图把这个节奏画出来。
只要把 mid 永远对齐到偶数下标,再看它和下一格是不是一对:是一对,说明楔子还在右边;不是一对,说明楔子就在 mid 这里或更左。盯住下面 lo/hi/mid 三根指针怎么收缩。
看节奏 · 单一元素之前:先肉眼感受节奏。最左这一对 1、1:第一个 1 在下标 0(偶数),它的搭档紧跟在下标 1。单一元素左边的每一对都这样——左半边在偶数位、右半边在奇数位。
看节奏 · 楔子出现:走到下标 2,2 找不到搭档——它就是那根楔子。从它往后(已变灰的部分),所有配对被它顶得整体右移一格,奇偶节奏从这里被掰断。
看节奏 · 单一元素之后:看楔子右边的第一对 3、3:第一个 3 跑到了下标 3(奇数)!节奏反过来了——右半边在偶数位、左半边在奇数位。这个「奇偶翻转点」正是单一元素的位置,二分要找的就是它。
开局 · 二分范围:正式开二分。lo 指数组头(下标 0)、hi 指数组尾(下标 8),单一元素一定落在这两根指针之间。我们要靠上面那条「偶数下标配对」规则,一轮轮把这个区间砍半。
第1轮 · 取 mid:取中点 mid=(0+8)/2=4。它正好是偶数下标,不用调整。规则要求 mid 必须落在偶数位,所以下一步要先检查这点。
第1轮 · mid 对齐偶数:mid=4 是偶数,直接拿它和右邻 arr[5] 组成一对(紫底窗口 [4,5])。如果 mid 是奇数,就要先 mid−1 退回偶数位再配对——这是不能省的一步。
第1轮 · 比这一对:arr[4]=3 和 arr[5]=4 对不上!偶数位的配对被打乱了,说明楔子就在 mid 这里或它左边。所以收右指针:hi = mid = 4,mid 本身可能是答案,不能丢。
第1轮 · 收 hi=mid:hi 收到 4,右半截下标 5~8 整段变灰出局。一刀砍掉一半,区间缩成 [0,4]。注意是 hi=mid 不是 mid−1,因为 mid=4 自己还可能就是楔子。
第2轮 · 取 mid:新区间 [0,4] 重新取中点,mid=(0+4)/2=2。又是偶数,省心。继续按规则配对。
第2轮 · 配对 [2,3]:mid=2 偶数,配上右邻成窗口 [2,3],里头是 arr[2]=2 与 arr[3]=3。比一比这一对是否相等。
第2轮 · 比这一对:arr[2]=2 ≠ arr[3]=3,又没配上。和第 1 轮一个道理:偶数位配对乱了,楔子在 mid 或更左。再次收右指针 hi=mid=2。
第2轮 · 收 hi=mid:hi 收到 2,下标 3、4 再变灰出局。区间又砍半,缩成 [0,2]。两轮下来 9 个数只剩 3 个候选了。
第3轮 · 取 mid:区间 [0,2] 取中点,mid=(0+2)/2=1——这回是奇数!规则规定配对必须从偶数位起,所以这一步还不能直接比,得先把 mid 拨回偶数。
第3轮 · mid 退回偶数:mid 是奇数,执行 mid = mid−1 = 0,把它退回最近的偶数下标。现在 mid 指 0,可以放心配对了。这一步正是很多人写错二分的地方。
第3轮 · 配对 [0,1]:对齐后从 mid=0 配出窗口 [0,1],里头是 arr[0]=1 与 arr[1]=1。这一对看起来很整齐,比一下。
第3轮 · 比这一对:arr[0]=1 == arr[1]=1,配上了!偶数位的配对完好,说明从这里到 mid+1 都是规规矩矩成对的,楔子在右边。所以推左指针 lo = mid+2 = 2,跳过这完整的一对。
第3轮 · 推 lo=mid+2:lo 跳到 2(跨过 0、1 这完整一对),下标 0、1 变灰出局。现在 lo 和 hi 都指向 2,下一步它们就要撞上,二分该停了。
二分结束 · lo==hi==2:lo 和 hi 撞到一起,循环条件 lo<hi 不再成立,二分停下。区间收成单独一个下标 2——所有成对的数都被排除了,剩下的这一个孤点就是单一元素。
取出答案:返回 arr[2] = 2,正是那个落单的数。9 个元素只比了 3 轮配对就锁定,全程 O(log n),没有一次多余的扫描。
雷区实演 · 楔子在右端:换个楔子靠右的例子 [3,3,7,7,10,11,11]:偶数位的 (0,1)、(2,3) 都配上 → lo 不断右推,最后停在下标 4,arr[4]=10。右边 11、11 又成对,刚好夹出 10。
边界三连:三种极端:长度 1、楔子在头、楔子在尾。lo<hi 的循环和「配上推右/没配上收左」天然把它们都兜住了。
面试追问:把「楔子打乱奇偶节奏」「mid 对齐偶数」讲透,再补一句异或法,是这题的面试加分项。
参考代码
class Solution: def singleNonDuplicate(self, nums): lo, hi = 0, len(nums) - 1 while lo < hi: mid = (lo + hi) // 2 if mid % 2 == 1: # mid 退回偶数下标 mid -= 1 if nums[mid] == nums[mid + 1]: # 配上 → 楔子在右 lo = mid + 2 else: # 没配上 → 楔子在左(含 mid) hi = mid return nums[lo]复杂度
- 时间复杂度:O(log n),每轮区间砍半,最多 log n 轮
- 空间复杂度:O(1),只用 lo/hi/mid 三个指针,原地完成
易错点
面试追问把动画讲成自己的话
追问为什么有序+成对能二分?
追问mid 对齐偶数还有别的写法吗?
追问如果不要求 O(log n) 怎么做?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
找到 K 个最接近的元素
LeetCode 658 · 中等 · 沿着 二分套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题