JS算法之数组中数字出现的次数

2,177 阅读4分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

数组中数字出现的次数

剑指Offer 56 - I. 数组中数字出现的次数

难度:中等

题目:leetcode-cn.com/problems/sh…

一个整型数组nums里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(N),空间复杂度是O(1)。

示例1:

 输入:nums = [4,1,4,6]
 输出:[1,6][6,1]

示例2:

 输入:nums = [1,2,10,4,1,4,3,3]
 输出:[2,10][10,2]

限制: 2 <= nuns.length <= 10000

题解

题目要求时间复杂度是O(N),空间复杂度是O(1),则不能通过暴力或者建表的方法去做。

位运算

在数组中判断两两元素是否相同,可以采用异或的方法。

对于数组中只出现一个不重复的元素,我们可以很轻松的通过全体异或的方式得到该值:a⊕a⊕b⊕b⊕c = 0⊕0⊕c = c,代码如下:

 // @params nums[]
 var singleNumber = (nums) => {
   let x = 0;
   for (let num of nums) {
     x ^= num;
   }
   return x;
 }
 ​
 console.log(singleNumber([2, 2, 4, 5, 4])) // 5

但是对于本题是不重复数字出现了两次,所以我们需要对其进行简化,步骤如下:

  1. 遍历nums执行异或,如a⊕a⊕b⊕b⊕c⊕d = 0⊕0⊕c⊕d = c⊕d

  2. 若c⊕d某二进制位为1,则c和d的二进制位一定不同,我们可以根据二进制位1,将数组拆分成两个子数组,并且每个数组中只有一个不重复数字。例如:

    对于[1,2,10,4,1,4,3,3],2和10是不重复数字,则

    2 -> 0010;10 -> 1010; 2和10这两个二进制数中首位不同,2与10得到8 -> 1000,即通过8来划分两个子数组。

  3. 拆分nums为两个子数组。

  4. 分别遍历两个子数组执行异或,这就将问题解耦啦,将两个子数组异或后的值返回即可。

 /**
  * @param {number[]} nums
  * @return {number[]}
  */
 var singleNumbers = function (nums) {
   let x = 0, // 子数组1不重复的数字
     y = 0, // 子数组2不重复的数字
     n = 0, // 用于划分数组
     m = 1; // 用于划分数组
   // 1. 遍历异或
   for (let num of nums) {
     n ^= num;
   }
   // 2.循环左移,计算出m
   while ((n & m) === 0)
     m <<= 1;
   // 3.划分子数组 (num & m) === 1和(num & m) === 0,括号一定要加!
   for (let num of nums) {
     if ((num & m) !== 0) x ^= num; // 4.子数组异或
     else y ^= num;
   }
   return [x, y]; // 返回结果
 };
 ​
 //console.log(singleNumbers([1,2,10,4,1,4,3,3]))
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

数组中数字出现的次数 II

剑指 Offer 56 - II. 数组中数字出现的次数 II

难度:中等

题目:leetcode-cn.com/problems/sh…

在一个数组nums中除一个数字只出现了一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例1:

 输入:nums = [3,4,3,3]
 输出:4

示例2:

 输入:nums = [9,1,7,9,7,9,7]
 输出:1

题解

法一 哈希表

遍历数组,统计每个数字出现次数,最后再遍历一遍哈希表,返回出现次数为1的值。

 /**
  * @param {number[]} nums
  * @return {number}
  */
 var singleNumber = function (nums) {
   const map = new Map();
   // 统计每个数字出现次数
   for (let num of nums) {
     if (map.has(num)) {
       map.set(num, map.get(num) + 1);
     } else {
       map.set(num, 1);
     }
   }
   // 遍历哈希表,找出出现次数为1的值
   for (let [_, value] of map.entries()) {
     if (value === 1) return value;
   }
 };
 ​
 //console.log(singleNumber([9, 1, 7, 9, 7, 9, 7]))
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

法二 数学公式法

若a、b、c中,a、b出现3次,c出现1次,则可以利用3*(a+b+c)-(3*a+3*b+c) = 2c算出c的值。

将数组里面的所有值进行记录并进行去重,可以用集合Set。

 var singleNumber = function (nums) {
   // 1.数组去重
   let set = new Set(nums);
   let sum1 = 0,
     sum2 = 0
   
   // 设置sum1,为例子中的(a+b+c)
   for (let num of set) {
     sum1 += num;
   }
   // 设置sum2,为例子中的(3*a+3*b+c)
   for (let num of nums) {
     sum2 += num;
   }
   // 返回结果
   return Math.floor((3 * sum1 - sum2) / 2)
 }
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

法三 位运算

这里我们无法像上道题那样采用异或的方法,但是我们可以沿用位运算的思路。

如果一个数字出现了三次,说明二进制表示的每一位也出现了三次。如果把所有出现三次的数字的二进制表示的每一位分别加起来,那么每一位的和都能被3整除。如果某一位的和能被3整除,那么那个只出现一次的数字二进制表示中对应的那一位是0;否则就是1.

上诉思路同样适用于数组中一个数字出现一次,其他数字出现奇数次问题(如果是偶数次,直接用异或)。

步骤:

  • 设置初始值
  • 让数组中每个数和掩码进行与运算,如果结果不为0,则count加1
  • 如果count能整除3,说明不是出现一次的数字
 var singleNumber = function (nums) {
   let res = 0;
   // 二进制最高位32位
   for (let bit = 0; bit < 32; bit++) {
     let mask = 1 << bit;
     let count = 0;
     for (let num of nums) {
       if (num & mask) count++;
     }
     if (count % 3) {
       res = res | mask;
     }
   }
   return res;
 }
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)