声明:本题目中的暴力法,HashMap法不符合题目要求,只是提供一种解题的思路,仅供参考!!!
题目描述:
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。
解题思路:
思路1:二分查找
我们先对题目做一个详细的分析可以得到以下几个重要特性:
1.数组中的元素个个数一定为奇数个!!!
2.待查找的唯一一个元素的索引下标一定为偶数!!!
3.数组中的元素下标(为了方便论述,此处记做“i”)若为偶数,且nums[i] = = nums[i+1],则该元素在带查找元素的左边;若i为奇数,且nums[i] == nums[i+1] 则该元素在带查找元素的右边!!!
以下是上面特性的证明分析(可以跳过,但为了不失一般性建议看一看):
1.因为数组中唯有一个元素出现一次,其余元素都会出现两次,所以数组中元素的个数一定为奇数个。
2.分三类情况讨论,1:当带查找元素在数组起始位置则其下标为0,为偶数;2:当带查找元素在数组的尾部时,其下标为nums.length - 1又因为nums.length(数组长度)为奇数,所以其下标为偶数。3:当待查找的元素在数组中间时,由于在其右边的元素和其左边的元素均有偶数个,而在其左边的元素开始下标为0,加上偶数个元素带查找元素后左边的最后一个元素下标就为奇数,所以待查找元素下标为偶数!!!
3.因为数组有序,并且除去一个元素其他的元素均出现两次又因为由第2点我们已经分析出待查找元素的索引为偶数,所以其左侧元素起始索引为0(偶数)结束索引为奇数,右侧元素起始索引为奇数结束索引为nums.length-1为偶数(对于带查找元素在数组开头或者结尾处,易证,此处不过多叙述),所以若索引为奇数且nums[i] == nums[i+1]时在带查找元素的左侧,索引为偶数时且nums[i] == nums[i+1]时在待查找元素的右侧(该特点也是本题目能使用二分查找的一个关键点!!!)
有了以上的特性再进行二分法思路分析:
1.我们对数组中的所有偶数下标进行二分,左指针(left)指向数组开头右指针(right)指向数组结尾nums.length - 1(为偶数)每次取两个指针的中间值(mid)
2.我们将题目抽象就可以得到我们实际是要找出最靠左则的偶数下标(因为我们在上面已经分析了带查找元素的下标一定为偶数)
3.每次二分后为了确保mid的索引为偶数要进行判断,若为奇数则mid - 1(此处需要减一而不是加一是因为我们要找出最靠左侧的索引为偶数的值)
4.每次比较若nums[mid] == nums[mid+1]则说明在带查找元素的左边,则left = mid + 2否则让right移动到mid处,最后返回left(或right)
举一个执行流程示意图例子:
代码:
class Solution {
//Binary Search
//Time Complexity: O(logN)
//Space Complexity: O(1)
public int singleNonDuplicate(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (right < left) {
int mid = left + (right - left) / 2;
//对mid的奇偶性判断
//确保其为偶数
if (mid % 2 != 0) {
mid -= 1;
}
//在带查找的元素的左边
if (nums[mid] == nums[mid+1]) {
left = mid + 2;
} else {//在右边
right = mid;
}
}
return nums[left]; //or nums[right]
}
}
以下将提供HashMap法和暴力法,只为提供一种不同的思考方式,不做过多的叙述(仅供参考)
思路2:HashMap法
改题目最容易想到的方法就是利用HashMap,将数组中的元素作为键,其出现的次数作为值,最后返回值和其他元素不同的的元素。
代码:
class Solution {
//HashMap
//Tiem Complexity: O(N)
//Space Complexity: O(N)
public int singleNonDuplicate(int[] nums) {
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
if (!map.cotainsKey(num)) {
map.put(num,0);
} else {
map.put(num,map.get(num) + 1);
}
for (int key : map.keys()) {
if (map.get(key) == 0) {
return num;
}
}
}
return -1;
}
}
思路3:暴力法
我们从下标为1开始到下标为nums.length - 2结束暴力遍历数组并依次比较当前元素相邻的两个元素是否存在相同的元素,若都不存在则为带查找元素,返回即可;若暴力遍历完后也无返回(则说明带查找元素在数组的边界处),则再次对两个边界进行判断:若nums[nums.length - 1] == nums[nums.length - 2] 则说明在数组开头处,若nums[0] == nums[1]则说明在数组结尾处。
代码:
class Solution {
//Brutale
//Time Complexity :O(N)
//Space Complexity: O(1)
public int singleNonDuplicate(int[] nums) {
//边界条件判断
if (nums.length == 1) return nums[0];
for (int i = 1; i < nums.length - 2; i++) {
if ((nums[i] != nums[i-1]) && (nums[i] != nums[i+1])) {
return nums[i];
}
//对带查找值在边界处的判断
if (nums[nums.length - 1] == nums[nums.length - 2]) {
return nums[0];
}
if (nums[0] == nums[1]) {
return nums[nums.length - 1];
}
}
return -1;
}
}