剑指 Offer II 070. 排序数组中只出现一次的数字

133 阅读1分钟

题目

给定一个只包含整数的有序数组 nums ,每个元素都会出现两次,唯有一个数只会出现一次,请找出这个唯一的数字。

你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。

 

示例 1:

输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2

示例 2:

输入: nums =  [3,3,7,7,10,11,11]
输出: 10

 

提示:

  • 1 <= nums.length <= 105
  • 0 <= nums[i] <= 105

题解

题目要求时间复杂度为O(logN), 想到用二分查找

nums = [1,1,2,3,3,4,4,8,8] 为例,假设只出现一次的元素为X, 则 x = 2, 此时, x的左侧为[1,1]2个元素,x的右侧为[3,3,4,4,8,8]有6个元素, 发现 x 的左右两侧均有偶数个元素,且数组的长度为奇数

由于数组是有序且递增的,数组中的相同元素必然相邻。 所以可以推出以下结论:

  1. x的左边下标 y, 如果nums[y] === nums[y+1], 则y为偶数
  2. x的右边下标 z, 如果nums[z] === nums[z+1], 则z为奇数

由于x是相同元素的开始下标的奇偶性分界,因此可以使用二分查找寻找下标 x

初始化左右边界left = 0, right = nums.length - 1。每次取左右边界的中间值mid = Math.floor((right - left ) / 2) + left。 根据 mid的奇偶性来决定和左边还是右边的相邻元素比较:

  1. 如果mid是偶数,则比较nums[mid]nums[mid+1] 是否相等
  2. 如果mid是奇数,则比较nums[mid]nums[mid-1] 是否相等

如果比较的结果是相等的,则x > mid, 更新左边界left = mid, 如果是不等的,则x <= mid, 更新右边界right = mid。一直继续二分查找,直到left === right确定下标x的值,则nums[x]为只出现一次的数字

小技巧 利用位运算的按位异或性质,可以得到mid和相邻数之间的关系:

  1. mid为偶数时, mid + 1 === mid ^ 1
  2. mid为奇数时, mid - 1 === mid ^ 1

因此,在二分查找中,可以利用mid^1性质,不需要判断mid的奇偶性

参考代码

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNonDuplicate = function(nums) {
    // 声明二分查找的左右指针,分别指向数组的开头和末尾
    let left = 0, right = nums.length - 1;
    while(left < right) {
        let mid = Math.floor((right - left) / 2) + left;
        // mid 为偶数, mid + 1 = mid ^ 1
        // mid 为奇数,  mid - 1 = mid ^ 1
        // 位运算,简化奇偶判断
        if (nums[mid] == nums[mid ^ 1]) {
            // 说明 mid 不是只出现一次的数字, 所以往右半区搜索,更新left指针
            left = mid + 1;
        } else {
            // 说明mid 可能是只出现一次的数字,所以往左半区搜索,更新right指针
            right = mid;
        }
    }

    return nums[right];
};

原题链接

剑指 Offer II 070. 排序数组中只出现一次的数字