这是我参与8月更文挑战的第29天,活动详情查看:8月更文挑战
有序数组中的单一元素(题号 540)
题目
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。 示例 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 <= 1050 <= nums[i] <= 105进阶: 采用的方案可以在O(log n)时间复杂度和O(1)空间复杂度中运行吗?
链接
解释
这题啊,这题是经典二分。
正常人的第一反应应该是直接扫一遍,扫一遍当然能拿到最终的答案,并且非常简单,时间复杂度是O(n)。
不过扫一遍需要注意两种特殊情况,就是单个数字出现在数组头或者数组尾部的时候,数组尾部很好说,那数组头部呢?正常情况下没问题,可当数组长度为 1 时,数组头部也是数组尾部,也就是依旧会出现单个元素的情况,所以不管是头部还是尾部都需要做处理。
扫一遍的情况就介绍到这里,现在看看二分要怎么操作。
普通的二分自然是无法解决这个问题的,这题很明显是二分的变种。
正常二分直接比较值即可,不管是理解还是操作起来都十分简单,但这题难就难在变种操作,要不然应该就是简单题了。
回到题目,如果需要找到单个元素的位置,那么就需要对奇偶数进行判断,这里的奇偶数指的是数组的长度是奇数还是偶数。
让我们想一下,因为题目说了,数组中只有一个单个元素,其他元素都是成双成对的,那么此时数组的长度必是奇数。
在二分的时候,每次取到mid值后,需要找到mid对应到相同的数字,如果不存在,那直接GG,这个mid就是单个的数字。
如果存在,需要找到mid对应的重复数字,之后拿到重复数字两边的位置,分为左区间和右区间,比较两个区间的长度,因为此时还没有找到单个的那个数字,所以左区间和右区间必然有一个的长度是奇数。
很显然,单个数字就在奇数区间内部,此时将左指针和右指针指向奇数区间的两头,变为一个新的区间,后续的操作也就在这个区间上进行了。
后面就是一样的操作了,持续缩短奇数区间的长度,直到最后找到单个数字。
这题和普通二分的区别还有一点,因为必出一个单个数组,并且数组的长度比会有一个奇数一个偶数,所以最后的结果会在mid所在的位置上出现,不用考虑找不到的情况,如果找不到,咱们的代码复杂度需要提升不少,好在不需要。
自己的答案(遍历)
var singleNonDuplicate = function (nums) {
if (nums[0] !== nums[1]) {
return nums[0]
}
if (nums[nums.length - 2] !== nums[nums.length - 1]) {
return nums[nums.length - 1]
}
for (let i = 1; i < nums.length; i++) {
if (nums[i] !== nums[i - 1] && nums[i] !== nums[i + 1]) {
return nums[i];
}
}
};
扫一遍的做法不用多说,大家的第一反应应该都是这个做法,缺点就是空间复杂度略高,达到了O(n)。
更好的方法(二分)
二分的思路在解释部分说过了,现在看看代码👇:
var singleNonDuplicate = function (nums) {
let left = 0
let right = nums.length
let L
let R
while (left <= right) {
const mid = left + right >> 1
if (nums[mid] === nums[mid + 1]) {
L = mid
R = mid + 1
} else if (nums[mid] === nums[mid - 1]) {
L = mid - 1
R = mid
} else {
return nums[mid]
}
if (L % 2 === 0 && R % 2 == 1) {
left = mid + 1
} else {
right = mid - 1
}
}
};
left代表左指针,right代表右指针,利用L和R来记录mid两侧的位置,然后根据奇偶数来更新左右指针的位置,从而得出最后的答案。
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇