540.有序数组中的单一元素
问题:给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。
示例:
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
输入: nums = [3,3,7,7,10,11,11]
输出: 10
思路:暴力,异或,也能过,但是不符合时间复杂度要求
class Solution {
public int singleNonDuplicate(int[] nums) {
int res = 0;
for(int num : nums){
res ^= num;
}
return res;
}
}
- 二分查找
根据区间中间节点与其左右节点是否相等,以及左右区间元素个数来判断,具体如下,mid为当前区间的中点
- 若中间节点等于左相邻节点,即
nums[mid] == nums[mid - 1]
,有两种情况:- 若区间[left, mid)中元素个数为偶数,则单一元素在左半区间,即[left, mid - 2];
- 若区间[left, mid)中元素个数为奇数,则单一元素在右半区间,即[mid + 1, right];
- 若中间节点等于右相邻节点,即
nums[mid] == nums[mid + 1]
,也有两种情况,类似于上面这种情况
class Solution {
public int singleNonDuplicate(int[] nums) {
int left = 0, right = nums.length - 1;
while(left < right){
int mid = left + (int) (right - left) / 2;
if(nums[mid] == nums[mid - 1]){
//根据区间[left, mid) 的个数,若mid等于左边元素,且左半区间数字个数为偶数,则单独元素在左半区间
if((mid - left) % 2 == 0){
right = mid - 2;
} else {
left = mid + 1;
}
} else if (nums[mid] == nums[mid + 1]){
if((right - mid) % 2 == 0){
left = mid + 2;
} else {
right = mid - 1;
}
} else {
return nums[mid];
}
}
return nums[right];
}
}
在评论中看到一个容易理解,且代码简洁的思路
- 唯一出现的元素一定是第奇数个元素,也就是索引为偶数的元素,在唯一出现的元素后的所有元素的两次出现顺序会由索引 偶数-奇数 变成 奇数-偶数。
- 采用二分法,并确保需要判断的中间元素的索引为偶数,然后与后一位元素判断是否相等。
- 当判断相等,则证明唯一出现的元素在中间元素后,否则在中间元素前。
class Solution {
public int singleNonDuplicate(int[] nums) {
int left = 0, right = nums.length - 1;
while(left < right){
int mid = left + (int) (right - left) / 2;
if((mid & 1) == 1) {
mid--;
}
if(nums[mid] == nums[mid + 1]){
left = mid + 2;
} else {
right = mid;
}
}
return nums[left];
}
}
- 官方题解,代码中有注释,大体思路如下:
若nums[mid]为单一元素,则其左右区间的元素个数都是偶数,
简单来说,就是若单一元素的下标为x,
则对于x左边的下标,元素对的第一个元素下标肯定为偶数,第二个元素下标肯定为奇数
对于x右边的下标,元素对的第一个元素下标肯定为奇数,第二个元素下标肯定为偶数
class Solution {
public int singleNonDuplicate(int[] nums) {
int left = 0, right = nums.length - 1;
while(left < right){
int mid = left + (int) (right - left) / 2;
//若当前下标为偶数,若等于其右相邻元素,即当前下标为元素对中的第一个元素下标,则说明单一元素在当前下标的右半区间,否则在左半区间
//若当前下标为奇数,若等于其左相邻元素,即当前下标为元素对中的第二个元素下标,则说明单一元素在当前下标的右半区间,否则在左半区间
if(nums[mid] == nums[mid ^ 1]){
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
}
}
细节1:
这里说下我对每次左右边界变化的理解
不论mid为奇数或者偶数,若下面满足条件,则肯定不会是单一元素下标,所以收缩左边界时排除了mid位置的元素
若没满足下面的条件,则有可能是单一元素,并不确定,所以下一个搜索区间需要包含当前mid位置的元素细节2:
当mid为偶数时,mid ^ 1 = mid + 1
当mid为奇数时,mid ^ 1 = mid - 1
整理思路,记录博客,以便复习。若有误,望指正~