说正事前,先说下我的感悟:不要忘了位运算符这个东西,有时候还真挺好用的,你要玩得好,这玩意跟傅里叶变换一样神奇,啥都能给你整出来(还不是题目出的巧~)(我承认我很狭隘,之前只会用常见的运算符T.T)
今日刷LC的每日一题,接触到了位运算,题目如下(对应260. 只出现一次的数字III):
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。
请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
示例 1:
输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
/**
* @param {number[]} nums
* @return {number[]}
*/
var singleNumber = function(nums) {
};
这题因为要找只出现一次的,那就是异或
咯!
异或规则及规律
规则:
- 将操作数当作32位的比特序列(由0和1组成),按位操作符操作数字的二进制形式,但是返回值依然是标准的JavaScript数值。(就说这个透明化的二进制操作,是不是就很赞)
- 异或
^
:两位数相比,有且只有一个1,返回1;否则,返回0。
规律:
-
任何数字
x^0=x
,x^x=0
-
如果
c=a^b
,那么a===c^b
,b===c^a
(是不是很神奇,像加减法而胜过加减法,因为不用管先后顺序)
let a = 1;//001
let b = 2;//010
let c = a^b;//011
console.log(a ^ c);//2,即010
a^b^c^a^c===b
(就问你神不神奇)- 异或结果位为1的位置正是
a
、b
不一样的地方。故规律2中,c
表示a
和b
的最后两位不同 - 任意数值
x^-1=~x
(即x的反码,但这个反码表示不是正数)
let a = -1;//111
let b = 2;//010
console.log(a ^ b);//-3,即101=101=>100=>011
- 不论奇偶,
x&-x
结果都只有一位1。同时这也代表着x对应的位置也为1,若x为偶数,则结果是一串只有一个1的二进制,若x为奇数,则结果必为1.
//x为偶数
x=0001011010
-x=1110100110
&=0000000010
//x为奇数
x=1010011011
-x=0101100101
&=0000000001
下面开始解题思路:
- 使用规律1、2、3、4,把整个数组中的数相异或,得到结果
c
(表示两个仅出现一次的数字的异或结果); - 结合规律4和规律6的启发,我们让
diff = c^-c
,把数组中的所有数与diff与操作一遍(只有0或非零两种可能),就可以区分出两组数,同时a
、b
因为diff对应位的数字不同,被分别分在了两组中;将其中一组数全部异或,就能得到a
或b
了; - 通过
c^步骤2的结果
就能得到另一个结果,然后return即可。
完整代码如下:
/**
* @param {number[]} nums
* @return {number[]}
*/
var singleNumbers = function(nums) {
let c = 0;
for(let num of nums){
c ^= num;
}
const diff = c&-c;
let nums1 = 0;
for(let num of nums){
//这里如果要用等于0的情况请务必这么写,因为比较操作符的优先级大于位运算符
//if((diff & num) === 0) nums1 ^= num;
if(diff & num) nums1 ^= num;
}
return [nums1,nums1^c];
};
效率很可观,本菜鸟第一次写不知道位运算这么个方法,用的遍历,执行用时280ms,内存100%,low爆了!
啥?你说规律5没用到?我先写着啊!万一以后有用呢~
这个写过后,解决136.只出现一次的数字I简直就是小case,一行代码搞定了~
再去看看137.只出现一次的数字III,这个更妙了。
给定一个非空整数数组,除了某个元素只出现1次以外,其余每个元素均出现了3次。找出只出现了1次的元素。
要求:时间复杂度O(kN),空间复杂度O(1)
你是不是觉得上述规律都不好使了?还不是因为你位运算符玩的不遛。其实我也想不出来,看的官方解答:
//满足题目要求,用时72ms,内存36.3M
var singleNumber = function(nums) {
let seen_once = 0, seen_twice = 0;
for(let num of nums){
//`seen_once`只会把当下自己没有且`seen_twice`也没有的数纳入
seen_once = ~seen_twice & (seen_once ^ num);
//`seen_twice`只会把当下自己没有且`seen_once`也没有的数纳入
seen_twice = ~seen_once & (seen_twice ^ num);
}
return seen_once;
};
- 当
seen_twice
没碰到出现2次的数时,seen_twice===0
; - 当
seen_once
发现自己存的数出现第2次时,立马把该数丢掉,扔给了seen_twice
- 数字只有出现的第1次(存下)和第2次(抵消),才会被
seen_once
存下,当数字第3次出现,此时seen_twice
中已经存入该数,故seen_once
直接忽略此数; - 数字只有出现的第2次(存下)和第3次(抵消),才会被
seen_twice
存下,当数字第3次出现时,seen_twice
立马丢掉该数
[1, 2, 1, 1]
seen_once 0001 0011 0010 0010
seen_twice 0000 0000 0001 0000
大致就是这个意思啦~ 打死我都想不到~
我写的两种都是循环
//这个空间复杂度不符合,用时88ms,内存35.2M,我个人认为这方法还不错
var singleNumber = function(nums) {
let unique = new Set();
for(let i=0;i<nums.length;i++){
if(!unique.has(nums[i]) && nums.lastIndexOf(nums[i])===i) return nums[i];
unique.add(nums[i]);
}
};
//这个时间复杂度不符合,用时100ms,内存36.4M
var singleNumber = function(nums) {
nums.sort((a,b)=>a-b);
let c = nums[0], i=0;
while(i<nums.length){
c ^= nums[i+1];
if(c===0){
i += 3;
c = nums[i];
continue;
}
else{
break;
}
}
return nums[i];
};
看了题解,竟然还有人写只出现k次的数,其他数出现p次的算法…小生不才,就不多打扰大佬们了~