这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战
只出现一次的数字(题号136)
题目
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出: 1
示例 2:
输入: [4,1,2,1,2]
输出: 4
链接
解释
这题啊,这题是经典简我击(简单题我重拳出击)。
但最后的结果十分尴尬,简我击失败了。
仔细看看说明,有两点:
- 线性时间复杂度
- 不使用额外空间
线性时间复杂度其实就是O(n),没什么不好理解的,不使用额外空间就是不能使用额外的变量,这也很好理解。
总的来说就是遍历一次拿到结果,并且不能使用额外的遍历来存储相关内容。
由于这个限制,显然不能使用正常的hash,使用Map来记住数字出现的情况, 如果第二次出现就是去掉Map上的这个属性,最后遍历完整个数组,Map上剩余的唯一属性就是最后的结果了。
代码写起来也很简单👇:
var singleNumber = function(nums) {
const map = new Map()
for (const num of nums) {
if (!map.has(num)) {
map.set(num, true)
} else {
map.delete(num)
}
}
return [...map][0][0]
};
由于这不符合官方的说明,就不放在答案里了,这里看一眼得了。
笔者能想到的“线性”时间复杂度的方法就是从数组开头开始遍历,每次获取第一个元素在第一个位置后的indexOf,如果该数字的indexOf不为-1,那就证明这个数字存在两次,然后去掉数组开头和indexOf的位置的数据,更新数组。
这个操作也就是去掉两个重复的数字,如此直到遇到indexOf为-1的数字,那这个数字就是出现一次的数字的,如果干到最后数组就剩一个元素了,那剩余的最后一个数字也就是出现一次的数字了。
这种做法唯一的问题就是indexOf是否符合线性时间复杂度的规范,如果不管这个,那么该方法确实是符合题目要求的。
自己的答案(indexOf)
逻辑说完了,👇看看代码:
var singleNumber = function(nums) {
while (nums.length > 1) {
const secondIndex = nums.indexOf(nums[0], 1)
if (secondIndex < 0) return nums[0]
nums.splice(secondIndex, 1)
nums.shift()
}
return nums[0]
};
逻辑还是比较简单的,就是不知道secondIndex算不算额外的空间,其实也可以去掉这个变量名称,跟下面的代码糅合在一起,但理解起来可能不是很顺畅,于是还是这样写吧。
更好的方法(异或)
说实话,异或这个方法笔者是真的想不到,还记得异或的三个特性么?
- 一个数和 0 做 XOR 运算等于本身:
a ⊕ 0 = a - 一个数和其本身做 XOR 运算等于 0:
a ⊕ a = 0 - XOR 运算满足交换律和结合律:
a ⊕ b ⊕ a = (a ⊕ a) ⊕ b = 0 ⊕ b = b
这里其实可以推导一下,我们将所有的数字利用异或运算符链接起来,可以是一个这样的式子:
4 ^ 1 ^ 2 ^ 1 ^ 2
根据交换律和结合律,这个式子可以变成这样:
2 ^ 2 ^ 1 ^ 1 ^ 4
再根据上面的第二条定理:
一个数和其本身做 XOR 运算等于 0:
a ⊕ a = 0
可以将相同的数字数字都替换成0,那么此时的式子是这样的:
0 ^ 0 ^ 4
再根据第2条定理进行转换,式子就变成了这样:
0 ^ 4
此时再应用第一条定理:
一个数和 0 做 XOR 运算等于本身:
a ⊕ 0 = a
可以得到这个式子的结果其实就是4,也就是这个例子的答案。
关于0和0异或的操作不用纠结,因为不管是奇数0还是偶数0,根据第二条定理,最后都会变成一个0,到最后也就是会剩下0和出现一次的数字了。
所以最后的解法就是:
var singleNumber = function(nums) {
return nums.reduce((a,b)=> a^b)
};
比较难想到,不用太纠结。
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇