leetcode 之数学

232 阅读3分钟

该部分整体考察频率不高。

384. 打乱数组:洗牌算法,交换思想

题目 384.打乱数组

洗牌方法如下:

  1. 初始化原始数组和新数组,原始数组长度为 n;
  2. 从还没处理的数组(例如还剩 k 个),随机产生一个 [0, k) 之间的数字 p;
  3. 从剩下的 k 个数中把第 p 个数取出;
  4. 重复以上步骤。

所以,实现的思路如下:

  • 假设 待原地乱序的数组 nums,先做浅拷贝
  • 共循环 n 次,在第 i 次循环中( 0 <= i < n):
    • 在 [i, n) 中随机抽取一个下标 j;
    • 将第 i 个元素与第 j 个元素交换。
const Solution = function(nums) {
  this.nums = nums;
};

Solution.prototype.reset = function() {
  return this.nums;
};

Solution.prototype.shuffle = function() {
  const arr = this.nums.slice();  // 浅拷贝 slice()
  let len = arr.length;
  
  for (let i = 0; i < len; i++) {
    let randomId = i + Math.floor((len - i) * Math.random());  // [i, len - 1]
    // 交换!!!
    [arr[randomId], arr[i]] = [arr[i], arr[randomId]]; // 将随机选择的数摘出
  }
  return arr;
};

时间复杂度:O(n)。

空间复杂度:O(n)。

剑指62. 圆圈中最后剩下的数字:约瑟夫环问题,公式有两个上轮

题目剑指62.圆圈中最后剩下的数字

思路讲解

对于题目中给的示例,最后剩下的 数字3 的下标是 0,我们逆推:

  1. 第4轮反推,补上 m 个位置,然后模上当时的数组大小 2,那么 3 在这一轮的索引就是 (0 + 3) % 2 = 1.
  2. 第3轮反推,补上 m 个位置,然后模上当时的数组大小 3,位置就是 (1 + 3) % 3 = 1。
  3. 依此类推,我们可以得出该 「推导公式」: (该数字的当前索引 + m) % 上轮剩余数字的个数 = 该数字上一轮的索引
const lastRemaining = function(n, m) {  // 推导一次即可
  let ans = 0;  // 最后一轮剩下的唯一数字,此时索引为 0
  
  for (let i = 2; i <= n; i++) {  // i是上轮剩余数字的个数(初始为2),一直模拟到初始数组长度
    ans = (ans + m) % i;
  }
  return ans; // 实际返回的是 res[ans],因为题中给出的数值等于索引,而且没有给原数组,所以直接返回 ans
};

时间复杂度:O(n)。

空间复杂度:O(1)。

9. 回文数:整数反转

题目9.回文数

方法一:回文数 => 字符串 => 双指针判定。如果不允许转为字符串,那么思路如下:

「思路」 由题意可知,如果是负数,一定不是回文数;如果是正数,那么将它的倒序值计算出来,然后比较和原数值是否相等。

const isPalindrome = (x) => {
    if (x < 0)  return false;
    let res = 0;
    let num = x;  // 不能省略,因为x要保留原数,来作对比
    
    while (num) { // 整数反转
        res = res * 10 + num % 10;
        num = parseInt(num / 10); // 仅限正数时
    }
    return res == x;
};

或者将整数转换成字符串再反转,

const isPalindrome = (x) => {
    if (x < 0)  return false;

    let s = x.toString().split('').reverse().join('');
    let num = Number(s);

    return num == x;
};

7. 整数反转:取余带符号 + parseInt

题目7.整数反转

整数反转运算时注意:

1. 取余%是带正负号的-12 % 10 等于-2,12 % 10等于2,所以 res = res * 10 + x % 10这个公式本身就和原数值的正负号一致。

2. parseIntMath.floor的区别 : 当为正数时两者没区别都是向下取整;但为负数时,parseInt会向上取整,与Math.floor相反。

Math.floor(12 / 10)  // 1
Math.floor(-12 / 10)  // -2

parseInt(12 / 10)  // 1
parseInt(-12 / 10)  // -1
const reverse = (x) => {
  let res = 0;
  while (x) {
    res = res * 10 + x % 10;  // 取余是带符号的
    if (res < Math.pow(-2, 31) || res > Math.pow(2, 31) - 1) {
      return 0;
    }
    x = parseInt(x / 10);  // 注意,正负数均存在的情况建议使用~~
  }
  return res;
}

136. 只出现一次的数字:异或

题目136.只出现一次的数字

异或是一个逻辑运算符,数学符号为js 中用^表示。其运算法则为:a⊕b = (¬a∧b)∨(a∧¬b)。如果a、b两个值不相同,则异或结果为1;如果值相同,则异或结果为0

「异或运算的几个性质」

  1. 任何数和0做异或运算,结果仍是原来的数a ^ 0 = a
  2. 满足交换律a ^ b = b ^ a
  3. 满足结合律a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c

根据题意可知,数组中含有2m+1个元素,其中m个数各出现2次,只有1个数出现了一次。根据异或运算的交换律和结合律,数组中全部元素的异或运算结果总是可以写成如下形式:

(a1 ^ a1)^(a2 ^ a2)^(a3 ^ a3) ^...^(am ^ am) ^ b = b。

所以 「将数组中全部元素进行异或,结果就是数组中只出现一次的数字」。

解法一:
const singleNumber = (nums) => {
  let ans = 0;  // 恒等律 a ^ 0 = a
  for (let n of nums) {  // of遍历值
    ans ^= n;
  }
  return ans;
};

解法二:
const singleNumber = (nums) => {
    return nums.reduce((a, b) => a ^ b);
};

时间复杂度:O(n)。

空间复杂度:O(1)。

异或的解法满足线性时间复杂度和常数空间复杂度,如果不考虑复杂度的限制,还可以用哈希表存储每个数字和该数字出现的次数,遍历数组即可得到数字出现的次数,并更新哈希表,最后遍历哈希表,得到只出现一次的数字。

法二:哈希表 (时间、空间复杂度均为 O(n),不如异或法。)

哈希表存储结构{数组元素值:元素出现的次数}。

const singleNumber = function(nums) {
  const hashMap = new Map();
  for (let i = 0; i < nums.length; i++) {
    if (hashMap.has(nums[i]))  hashMap.set(nums[i], 2);
    else  hashMap.set(nums[i], 1);
  }
  for (let [key, value] of hashMap) {
    if (value === 1) return key;
  }
  return -1;
};

时间复杂度:O(n)。

空间复杂度:O(n)。

1013. 将数组分成和相等的三个部分:count <= 0

题目1013.将数组分成和相等的三个部分

注意题中要求三部分的索引是有序的!

如果有和相等的三个部分,那么sum被 3 取余后等于0,而且拆分的3个子部分和都应该等于sum / 3。所以,总体思路 「先判断sum能不能被3取余等于0,然后遍历数组,每找到一个和等于sum / 3的子数组,就令count--,tmp = 0

注意return count <= 0,而不是count == 0。这是因为当arr = [0, 0, 0, 0]时,也符合存在和相等的三个部分,此时count = -1,如果 return count == 0会返回false,与我们的预期不符合。

const canThreePartsEqualSum = (arr) => {
  let sum  = arr.reduce((a, b) => a + b, 0);
  if (sum % 3)  return false;  // 如果累计和不能被 3 取余,那返回 false
  
  let count = 3;  // 3个子部分,判断是否有3个或以上的子数组的累计和满足该条件
  let tmp = 0;
  for (let num of arr) {
    tmp += num;
    if (tmp == sum / 3) {
      count--;
      tmp = 0;
    }
  }
  return count <= 0;  // 注意
}

时间复杂度:O(n)。

空间复杂度:O(1)。