Skip to content

Latest commit

 

History

History
156 lines (109 loc) · 5.37 KB

File metadata and controls

156 lines (109 loc) · 5.37 KB

136. 只出现一次的数字

Hi 大家好,我是张小猪。欢迎来到『宝宝也能看懂』系列特别篇 - 30-Day LeetCoding Challenge

这是一个 leetcode 官方的小活动,地址在这里(如果被重定向回中文站了,去掉域名里的 -cn 即可)。活动内容很简单,从 4 月 1 号开始,每天会选出一道题(国内时间中午 12 点更新),在 24 小时内完成即可获得一点小奖励。虽然奖励似乎也没什么用,不过作为一个官方的打卡活动,小猪还是来打一下卡吧,正好作为每天下班回家后的娱乐。

这里是 4 月 1 号的题,也是题目列表中的第 136 题 -- 『只出现一次的数字』

题目描述

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:

输入: [2,2,1]
输出: 1

示例 2:

输入: [4,1,2,1,2]
输出: 4

官方难度

EASY

解决思路

这道题似乎没什么好讲的,大概是作为活动的开篇,预热一下,吸引围观,激励用户参与。

下面给出几个方案,供小伙伴们参考。

方案 1

其实这是个凑数的方案(哈哈哈,没想到吧)。小猪相信不会有小伙伴们这么去写的,毕竟直接就到 O(n^2) 了,实在是太浪费了。权且作为最最基础的实现吧。

const singleNumber = nums => {
  const arr = [];
  for (const n of nums) {
    const idx = arr.indexOf(n);
    idx === -1 ? arr.push(n) : arr.splice(idx, 1);
  }
  return arr.pop();
};

方案 2

Set 代替了方案 1 中的数组,使得查询和删除都快了很多。不过代码还是太多了,并且也得基于额外的数据结构和空间,并不推荐。

const singleNumber = nums => {
  const set = new Set();
  for (const n of nums) {
    set.has(n) ? set.delete(n) : set.add(n);
  }
  for (const x of set.keys()) return x;
};

方案 3

用位操作代替了前两种方案里对于额外数据结构和空间的依赖。小猪觉得这个方案算是这道题的标准实现吧,才不会告诉你前两种方案是为了写文章才凑出来的呢 hia hia hia O(∩_∩)O

稍微解释一下异或这个位操作吧,它的行为逻辑可以理解为两个值不同则为 true,相同则为 false。如下表:

0 ^ 0 === 0
0 ^ 1 === 1
1 ^ 0 === 1
1 ^ 1 === 0

那么基于这个运算逻辑,我们可以发现,对于两个相同的数值进行异或运算,那么结果一定是 0。再看看我们题目中的数据,正好是成对的数字加上一个单独的数字。这时候再加上异或这个运算可以满足交换律和结合律,于是我们那些成对的数字运算完后正好为 0,而 0 与一个数字进行异或预算便是这个数字本身。

基于这个思路,我们可以得到如下的代码:

const singleNumber = nums => {
  let ret = 0;
  for (const n of nums) ret ^= n;
  return ret;
};

当然,都写成这样了,不如我们就直接一行吧:

const singleNumber = nums => nums.reduce((prev, cur) => prev ^ cur, 0);

拓展

关于异或操作的一些实际应用场景,这里再补充几个栗子吧。

交换两个数

let a = 10;
let b = 20;
a = a ^ b;
b = b ^ a;
a = b ^ a;
console.log(a, b) // 20, 10

稍微解释一下过程吧:

  1. 第一次异或操作得到了两个数字不同的位为 1,相同的位为 0,并保存进了变量 a
  2. 第二次异或操作相当于是把 b 中两个数不同的位取反,相同的位保持不变,于是 b 就变成了 a,然后保存进变量 b 中。
  3. 第三次异或操作继续把现在 b 中两个数不同的位取反,相同的位保持不变。注意,这时候的 b 中的值其实是 a,所以运算后得到的值是 b,然后保存进变量 a 里。
  4. 最终 a 就进了 b,b 就进了 a

两个数相加

let a = 10;
let b = 20;
while (b !== 0) {
  const a2 = a ^ b;
  const b2 = (a & b) << 1;
  a = a2;
  b = b2;
}
console.log(a) // 30

同理,不过这里面还用到了与操作和左移操作。原理其实和加法是一样的,就是按位相加,然后需要进位的地方就进位。不过在二进制中,可能的情况会比较少。具体如下:

  1. 通过异或运算找出按位相加后不会产生进位的地方。
  2. 通过与运算找出按位相加后会产生进位的地方,并做进位。
  3. 把上述两个值保存后继续执行相同的计算,直到加数减少为 0,那么被加数就变成了最初的两数之和。

总结

这是 『30-Day LeetCoding Challenge』 的第一题,没有太多可以说的,于是就稍微拓展了一点关于异或操作的内容。希望可以帮到有需要的小伙伴。

如果觉得不错的话,记得『三连』哦。小猪爱你们哟~ >.<

相关链接

我的微信公众号:张小猪粉鼻子