题目:
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
解题一(双层for循环超时):
定义两个指针left用于查找第一个元素,right用于查找第二个元素,对left和right进行双层循环找出在数组中只出现过一次的元素,时间复杂度超时。
class Solution {
public:
int singleNumber(vector<int>& nums) {
//双层遍历left用于循环查找第一个元素,right用于循环查找第二个元素
int length = nums.size();
int left = 0;
int right = 1;
while(right!=length){
for(int i =right;i<length;i++){
if(nums[i]==nums[left]){
left++;
break;
}
right = left+1;
}
}
return left;
}
};
解题二(一次循环):
用vector容器代替内层循环体结构,仅通过一次遍历可以查找到只出现一次的数,vector容器中find(vec.begin(),vec.end(),nums[i])函数可以查找当前容器是否存在相同的函数,vec.erase()函数用于删除在容器中指定存在的元素。利用vector的函数接口替代第二层循环体的作用,如下为笔者代码:
class Solution {
public:
int singleNumber(vector<int>& nums) {
int length = nums.size();
vector<int> vec;
for(int i=0;i<length;i++){
if(vec.empty()){
vec.push_back(nums[i]);
continue;
}
auto it = find(vec.begin(), vec.end(), nums[i]);
if(it == vec.end()){
vec.push_back(nums[i]);
}
else{
vec.erase(remove(vec.begin(), vec.end(), nums[i]), vec.end());
}
}
int result = vec[0];
return result;
}
};
解题三(位运算):
如果不考虑时间复杂度和空间复杂度的限制,本题可以有很多种的解法。使用集合存储数字,遍历数组中的每个数字,如果集合中没有该数字,则将数字加入集合,如果集合中已经有该数字,则将该数字从集合中删除,最后剩下的数字就是只出现一次的数字;使用哈希表存储每个数字和该数字出现的次数,遍历数组即可得到每个数字出现的次数,并更新哈希表,最后遍历哈希表,得到只出现一次的数字。上述解法都额外使用 了O(n)的空间,其中n是数组长度。
使用位运算,可以做到线性时间复杂度和常数空间复杂度。使用异或运算有以下三个性质:
1、任何数和0做异或运算,结果仍然是原来的数。
2、任何数和其自身做异或运算,结果是0。
3、异或运算满足交换律和结合律。
假设数组中有2m+1个数,其中有m个数各出现两次,一个数出现一次,根据异或运算性质3,数组中的全部元素的异或运算结果总是可以写成如下形式:
根据性质2和性质1,上述可以简化和计算得到如下结果:
因此,数组中的全部元素的异或运算结果即为数组中只出现一次的数字,如下代码为:
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (auto e: nums) ret ^= e;
return ret;
}
};
笔者小记:
使用位运算,可以做到线性时间复杂度和常数空间复杂度。使用异或运算有以下三个性质:
1、任何数和0做异或运算,结果仍然是原来的数。
2、任何数和其自身做异或运算,结果是0。
3、异或运算满足交换律和结合律。
其中“^”符号为异或运算,可以有效替代一些有规则的计算操作。
除此之外,笔者在补充一些位运算的操作符号:
1、按位与运算:对两个操作数的每一位进行“与”运算。两个对应位都为 1
时,结果才为 1
,否则为 0
。
int a = 5; // 二进制: 0101
int b = 3; // 二进制: 0011
int result = a & b; // 结果: 0001,即 1
2、按位或运算:两个对应位中有一个为 1
,结果就是 1
,只有两个对应位都为 0
时,结果才为 0
。
int a = 5; // 二进制: 0101
int b = 3; // 二进制: 0011
int result = a | b; // 结果: 0111,即 7
3、按位异或运算:对应位相同则结果为 0
,不同则结果为 1
。
int a = 5; // 二进制: 0101
int b = 3; // 二进制: 0011
int result = a ^ b; // 结果: 0110,即 6
4、按位取反运算:即将 1
变成 0
,将 0
变成 1
。
int a = 5; // 二进制: 0101
int result = ~a; // 结果: 1010 (按位反转) 在 32 位系统上结果为 -6(补码表示)
5、左移:左移时,空出的位会用 0
填充。左移操作相当于将数值乘以 2
的指定次幂。
int a = 5; // 二进制: 0101
int result = a << 1; // 左移 1 位,结果: 1010,即 10
6、右移:右移时,空出的位会根据符号位来填充(对于有符号数,通常填充符号位;对于无符号数,填充 0
)。右移操作相当于将数值除以 2
的指定次幂。
int a = 5; // 二进制: 0101
int result = a >> 1; // 右移 1 位,结果: 0010,即 2
赋值运算符与位运算符的组合
你也可以将位运算与赋值运算符组合,简化写法。这些组合操作符包括:
&=
: 按位与并赋值|=
: 按位或并赋值^=
: 按位异或并赋值<<=
: 左移并赋值>>=
: 右移并赋值