Leetcode 专题训练

132 阅读16分钟

今天是 2024年9月21日,因为是周末,在家里没有事情干,决定做几道 Leetcode 题目,欢迎大家支持!

1. [564] 寻找最近的回文数

给定一个表示整数的字符串 n ,返回与它最近的回文整数(不包括自身)。如果不止一个,返回较小的那个。

“最近的”定义为两个整数差的绝对值最小。

 

示例 1:

输入: n = "123"
输出: "121"
示例 2:

输入: n = "1"
输出: "0"
解释: 0 和 2是最近的回文,但我们返回最小的,也就是 0。
 

提示:

1 <= n.length <= 18
n 只由数字组成
n 不含前导 0
n 代表在 [1, 1018 - 1] 范围内的整数

尝试完成:

/**
 * @param {string} n
 * @return {string}
 */
var nearestPalindromic = function(n) {
  const big = BigInt(n);
  const nums = [big+1n, big-1n];

  while(true){
    const distance = getDistance(nums[0]);

    if(distance === 0n) break;
    nums[0] += distance;
  }

  while(true){
    const distance = getDistance(nums[1]);

    if(distance === 0n) break;
    nums[1] -= distance;
  }

  function getDistance(m) {
    const str = m.toString();
    const len = str.length;
    let i = 0;
    let j = len - 1;
    while(i < j) {
      if(str[i] !== str[j]) return BigInt(10 ** i);
      i++;
      j--;
    }

    return 0n;
  }

  return big - nums[1] > nums[0] - big ? nums[0].toString() : nums[1].toString();
};

我的思路: 这段代码的目的是找到给定数字 n 的最近回文数。它妙在以下几个地方:

  1. 使用 BigInt 代码使用了 BigInt 类型来处理大整数,这使得它能够处理超出普通数字类型的整数。

  2. 双向搜索: 代码同时从 n+1n-1 开始搜索,这样可以确保找到最近的回文数,无论是在 n 的左边还是右边。

  3. 距离计算 getDistance 函数: 这个函数用于计算给定数字与回文数之间的距离。它通过比较数字的对称位置的字符来确定需要添加或删除的最小位数。

  4. 优化的循环条件: 循环会一直执行直到找到回文数。当 getDistance 返回 0n 时,表示当前的 nums[0]nums[1] 已经是回文数,循环可以停止。

  5. 字符串处理:getDistance 函数中,通过将数字转换为字符串,然后比较字符串的对称位置的字符,这是一种有效的检查回文的方法。

  6. 条件判断选择最小距离: 在最后,代码通过比较 nnums[0]nums[1] 之间的距离来选择最近的回文数。这里使用了 big - nums[1] > nums[0] - big 来判断 nnums[1] 的距离是否大于与 nums[0] 的距离,从而选择较小的一个。

  7. 避免重复计算: 代码避免了重复计算 n+1n-1 的情况,因为它在找到第一个回文数后就停止了搜索。

  8. 简洁性: 尽管功能强大,但代码非常简洁,易于理解。

得分结果: 100% 100%

总结提升: 朴素的想法就是从原来的数开始每次计算两个值,加1 然后减1 看哪个先找到回文数。

但是这样很容易超时,因为循环的次数太多了。我们对上面的算法进行优化,充分利用将数字看成是字符串这一先决条件。我们不必每次都增加 1 或者 减少 1. 例如,如果原数字为 4324 那么按照原来的方法,寻找 4325 4326 4327 ... 4333 这么多尝试可以简化成一步,那就是不是每次增加 1 而是每次增加 10,从 4324 直接到 4334.

考虑到原来的数本身就可能是回文数,因此我们给其加减 1 得到初始的两个值,从这两个值开始分别探寻离的最近的两侧回文数。

当两侧数位上的数不相等的时候,不是将它们变的一样,而是通过每次递增1或者递减1的方式,这样做是为了保证探寻过程的递增性或者递减性,同时也解决了进位的问题。

2. [231] 2的幂 同理 342 326

给你一个整数 n,请你判断该整数是否是 2 的幂次方。如果是,返回 true ;否则,返回 false 。

如果存在一个整数 x 使得 n == 2x ,则认为 n 是 2 的幂次方。

 

示例 1:

输入:n = 1
输出:true
解释:20 = 1
示例 2:

输入:n = 16
输出:true
解释:24 = 16
示例 3:

输入:n = 3
输出:false
 

提示:

-231 <= n <= 231 - 1

尝试完成:

/**
 * @param {number} n
 * @return {boolean}
 */
var isPowerOfTwo = function(n) {

  function _isPowerOfTwo(n, m){
    if(n===0) return false;
    let big = n;
    while(true) {
      if(big % m === 0) {
        big = big / m;
        continue;
      }
      if(big !== 1) return false;
  
      return true;
    }
  }
  

  return _isPowerOfTwo(n, 2);
};

得分结果: 42.82% 26.35%

总结提升:

  1. BigInt 不能做取余操作;
  2. 可以用位运算加快速度,但只针对 2.
var isPowerOfTwo = function(n) {
  if(n<=0) return false;
  const m = n - 1;
  return (m & n) === 0;
}

3. [504] 七进制数

给定一个整数 num,将其转化为 7 进制,并以字符串形式输出。

 

示例 1:

输入: num = 100
输出: "202"
示例 2:

输入: num = -7
输出: "-10"
 

提示:

-107 <= num <= 107

尝试完成:

/**
 * @param {number} num
 * @return {string}
 */
var convertToBase7 = function(num) {
  return num.toString(7);
};

我的思路: 使用内置方法即可。

得分结果: 28.74% 93.10%

总结提升: 自己写一下吧:考虑到输入是 number 类型的,不需要转 BigInt

/**
 * @param {number} num
 * @return {string}
 */
var convertToBase7 = function(num) {
  if(num === 0) return "0";
  let rst = "";

  let n = Math.abs(num);
  while(n > 0){
    const mod = n % 7;
    rst =mod + rst;
    n = Math.floor(n/7);
  }

  if(num < 0) {
    rst = '-' + rst;
  }
  return rst;
};

注意:

  1. while(n>0)
  2. if(n===0)

4. [263] 丑数

丑数 就是只包含质因数 2、3 和 5 的正整数。

给你一个整数 n ,请你判断 n 是否为 丑数 。如果是,返回 true ;否则,返回 false 。

 

示例 1:

输入:n = 6
输出:true
解释:6 = 2 × 3
示例 2:

输入:n = 1
输出:true
解释:1 没有质因数,因此它的全部质因数是 {2, 3, 5} 的空集。习惯上将其视作第一个丑数。
示例 3:

输入:n = 14
输出:false
解释:14 不是丑数,因为它包含了另外一个质因数 7 。
 

提示:

-231 <= n <= 231 - 1

尝试完成:

/**
 * @param {number} n
 * @return {boolean}
 */
var isUgly = function(n) {
  if(n===0) return false;
  while(true){
    if(n % 2 === 0) {n = n /2; continue}
    if(n % 3 === 0) {n = n /3; continue}
    if(n % 5 === 0) {n = n /5; continue}
    if(n === 1) return true;
    return false;
  }
};

我的思路: 考虑 n 等于 0 的特殊情况和 n 等于 1 为出口。

得分结果: 45.66% 74.72%

5. [190] 颠倒二进制位

颠倒给定的 32 位无符号整数的二进制位。

提示:

请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -1073741825。
 

示例 1:

输入:n = 00000010100101000001111010011100
输出:964176192 (00111001011110000010100101000000)
解释:输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
     因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
示例 2:

输入:n = 11111111111111111111111111111101
输出:3221225471 (10111111111111111111111111111111)
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
     因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。
 

提示:

输入是一个长度为 32 的二进制字符串
 

进阶: 如果多次调用这个函数,你将如何优化你的算法?

尝试完成:

/**
 * @param {number} n - a positive integer
 * @return {number} - a positive integer
 */
var reverseBits = function(n) {
    n = n.toString(2);
    const r = 32 - n.length;
    const m = '0b'+('0'.repeat(r) + n).split("").reverse().join("");
    return Number(m);
};  

我的思路: 这道题需要在前面补 0 因为运算之后 number 的前导 0 会消失。

得分结果: 20.20% 12.58%

总结提升:

  1. 题目中没有说输入是字符串,实际上例子中的数在 js 中是非法的,在做题自测的时候使用如下的形式,即自行在前面加上 0b:
console.log(reverseBits(0b00000010100101000001111010011100))

6. [191] 位1的个数

编写一个函数,获取一个正整数的二进制形式并返回其二进制表达式中 
设置位
 的个数(也被称为汉明重量)。

 

示例 1:

输入:n = 11
输出:3
解释:输入的二进制串 1011 中,共有 3 个设置位。
示例 2:

输入:n = 128
输出:1
解释:输入的二进制串 10000000 中,共有 1 个设置位。
示例 3:

输入:n = 2147483645
输出:30
解释:输入的二进制串 11111111111111111111111111111101 中,共有 30 个设置位。
 

提示:

1 <= n <= 231 - 1
 

进阶:

如果多次调用这个函数,你将如何优化你的算法?

尝试完成:

/**
 * @param {number} n
 * @return {number}
 */
var hammingWeight = function(n) {
  const str = n.toString(2).split("");
  let rst = 0;
  str.forEach((item)=>{
    if(item === '1') rst += 1;
  }, 0)

  return rst;
};

得分结果: 77.88% 5.29%

总结提升: 使用 reduce 完成:

/**
 * @param {number} n
 * @return {number}
 */
var hammingWeight = function(n) {
  return n.toString(2).split("").reduce((acc, item) => acc + (item === '1' ? 1 : 0), 0);
};

59.92% 82.23%

7. [476] 数字的补数

对整数的二进制表示取反(0 变 1 ,1 变 0)后,再转换为十进制表示,可以得到这个整数的补数。

例如,整数 5 的二进制表示是 "101" ,取反后得到 "010" ,再转回十进制表示得到补数 2 。
给你一个整数 num ,输出它的补数。

 

示例 1:

输入:num = 5
输出:2
解释:5 的二进制表示为 101(没有前导零位),其补数为 010。所以你需要输出 2 。
示例 2:

输入:num = 1
输出:0
解释:1 的二进制表示为 1(没有前导零位),其补数为 0。所以你需要输出 0 。
 

提示:

1 <= num < 231

尝试完成:

/**
 * @param {number} num
 * @return {number}
 */
var findComplement = function(num) {
  return Number('0b'+num.toString(2).split("").map(
    item => (item==='0'?'1':'0')
  ).join(""))
};

得分结果: 39.53% 46.51%

总结提升: num.toString(2) 之后得到的字符串不会是以 0b 开头的,只有 '0' 和 '1'.

8. [461] 汉明距离

两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。

给你两个整数 x 和 y,计算并返回它们之间的汉明距离。

 

示例 1:

输入:x = 1, y = 4
输出:2
解释:
1   (0 0 0 1)
4   (0 1 0 0)
       ↑   ↑
上面的箭头指出了对应二进制位不同的位置。
示例 2:

输入:x = 3, y = 1
输出:1
 

提示:

0 <= x, y <= 231 - 1
 

注意:本题与 2220. 转换数字的最少位翻转次数 相同。

尝试完成:

/**
 * @param {number} x
 * @param {number} y
 * @return {number}
 */
var hammingDistance = function(x, y) {
  return (x ^ y).toString(2).split("").reduce((acc, item)=>acc+(item==="1"?1:0),0)
};

我的思路: 考虑到输入是 number 类型并且为正,可以直接使用位运算中的异或加快速度。

得分结果: 72.64% 78.30%

总结提升: 需要加强 reduce 的使用技巧。

9. [477] 汉明距离总和

两个整数的 汉明距离 指的是这两个数字的二进制数对应位不同的数量。

给你一个整数数组 nums,请你计算并返回 nums 中任意两个数之间 汉明距离的总和 。

 

示例 1:

输入:nums = [4,14,2]
输出:6
解释:在二进制表示中,4 表示为 0100 ,14 表示为 1110 ,2表示为 0010 。(这样表示是为了体现后四位之间关系)
所以答案为:
HammingDistance(4, 14) + HammingDistance(4, 2) + HammingDistance(14, 2) = 2 + 2 + 2 = 6
示例 2:

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

提示:

1 <= nums.length <= 104
0 <= nums[i] <= 109
给定输入的对应答案符合 32-bit 整数范围

尝试完成:

/**
 * @param {number[]} nums
 * @return {number}
 */
var totalHammingDistance = function(nums) {
  const record = Array(32).fill(0);
  const record2 = Array(32).fill(0);

  for(let i = 0; i < nums.length; i++) {
    const cur = getBit(nums[i]);
    for(let j = 0; j < 32; j++) {
      if(cur[j] === '0'){
        record[j]++;
      } else {
        record2[j]++;
      }
    }
  }

  function getBit (n) {
    const str = n.toString(2);
    const rst = ('0'.repeat(32-str.length)+str).split("");
    return rst;
  }

  let re = 0;
  for(let k = 0; k < 32; k++) {
    re += record[k]*record2[k];
  }

  return re;
};

我的思路:

  1. 不能计算两两汉明距离然后求和,会超时。
  2. 461 的方法只是计算一次性两个数的距离快,计算成群的并不快;这是因为成群计算需要复用已经计算得到的结果,而 461 的解法计算一次快却不能复用。
  3. 所以我们需要改进算法;汉明距离是 32 位总共的统计结果,并且每一位之间是独立的,所以我们可以计算第 1 位的距离之和,然后加上第 2 位距离之和,一直加到第 32 位。
  4. 所以我们只需要写出计算某一位汉明距离之和的算法就可以了,考虑 0 1 1 11 0 0 11 1 1 0 如果我们要计算第 1 位汉明距离之和,只需统计有两个 1 和一个 0,这样距离就是 2 * 1 = 2
  5. 所以这个题是在考独立性下面的乘法和加法原理。

得分结果: 23.08% 15.38%

总结提升:

  1. 最优算法需要考虑输入的规模,大规模问题的处理,可能采用完全不同的算法。
  2. 改进取某位数 (val >> i) & 1
var totalHammingDistance = function(nums) {
    let ans = 0, n = nums.length;
    for (let i = 0; i < 30; ++i) {
        let c = 0;
        for (const val of nums) {
            c += (val >> i) & 1;
        }
        ans += c * (n - c);
    }
    return ans;
};

10. [693] 交替位二进制数

给定一个正整数,检查它的二进制表示是否总是 0、1 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。

 

示例 1:

输入:n = 5
输出:true
解释:5 的二进制表示是:101
示例 2:

输入:n = 7
输出:false
解释:7 的二进制表示是:111.
示例 3:

输入:n = 11
输出:false
解释:11 的二进制表示是:1011.
 

提示:

1 <= n <= 231 - 1

尝试完成:

/**
 * @param {number} n
 * @return {boolean}
 */
var hasAlternatingBits = function(n) {
  return ((n ^ (n>>1)) &  ((n ^ (n>>1))+1)) === 0
};

我的思路: 先异或,然后判断是否全 1.

得分结果: 90.70% 65.12%

总结提升: 要是 js 中有 ~^ 就好了!

11. [172] 阶乘后的 0

给定一个整数 n ,返回 n! 结果中尾随零的数量。

提示 n! = n * (n - 1) * (n - 2) * ... * 3 * 2 * 1

 

示例 1:

输入:n = 3
输出:0
解释:3! = 6 ,不含尾随 0
示例 2:

输入:n = 5
输出:1
解释:5! = 120 ,有一个尾随 0
示例 3:

输入:n = 0
输出:0
 

提示:

0 <= n <= 104
 

进阶:你可以设计并实现对数时间复杂度的算法来解决此问题吗?

尝试完成:

/**
 * @param {number} n
 * @return {number}
 */
var trailingZeroes = function(n) {
  let rst = 0;

  for(let i = 1; i <=n; i++) {
    rst += get5(i);
  }

  function get5(m){
    let c = 0;
    while(m > 1){
      if(m % 5 === 0) {
        m = m / 5;
        c++;
      } else {
        return c;
      }
    }

    return c;
  }

  return rst;
};

我的思路: 后面有多少个 0 实际上是在问一共有多少质因数 5,因为 0 只有 2*5 才会出现,并且 2 的个数一定大于 5.

得分结果: 30.12% 90.68%

总结提升: 所谓对数时间复杂度无非就是做一个缓存。

12. [458] 可怜的小猪

有 buckets 桶液体,其中 正好有一桶 含有毒药,其余装的都是水。它们从外观看起来都一样。为了弄清楚哪只水桶含有毒药,你可以喂一些猪喝,通过观察猪是否会死进行判断。不幸的是,你只有 minutesToTest 分钟时间来确定哪桶液体是有毒的。

喂猪的规则如下:

选择若干活猪进行喂养
可以允许小猪同时饮用任意数量的桶中的水,并且该过程不需要时间。
小猪喝完水后,必须有 minutesToDie 分钟的冷却时间。在这段时间里,你只能观察,而不允许继续喂猪。
过了 minutesToDie 分钟后,所有喝到毒药的猪都会死去,其他所有猪都会活下来。
重复这一过程,直到时间用完。
给你桶的数目 buckets ,minutesToDie 和 minutesToTest ,返回 在规定时间内判断哪个桶有毒所需的 最小 猪数 。

 

示例 1:

输入:buckets = 1000, minutesToDie = 15, minutesToTest = 60
输出:5
示例 2:

输入:buckets = 4, minutesToDie = 15, minutesToTest = 15
输出:2
示例 3:

输入:buckets = 4, minutesToDie = 15, minutesToTest = 30
输出:2
 

提示:

1 <= buckets <= 1000
1 <= minutesToDie <= minutesToTest <= 100

尝试完成:

/**
 * @param {number} buckets
 * @param {number} minutesToDie
 * @param {number} minutesToTest
 * @return {number}
 */
var poorPigs = function(buckets, minutesToDie, minutesToTest) {
  const info = (~~(minutesToTest / minutesToDie))+1;
  let rst = 0;
  let _  = 1;
  while(_ < buckets){
    _ *= info;
    rst++;
  }
  return rst;
  
};

我的思路: 这个题太数学了,主要是一只猪能喝 5 次水代表什么意思。

得分结果: 100% 37.50%

总结提升: 本质上还是基本题没有想清楚。知道 n 只猪一次最多可以检测 2^n 桶水;是因为每只猪喝水之后有两种状态,根据乘法原理就可以得到 2^n; 现在题目改成了多次喝水,那么一直猪的状态就变了,以喝水 3 次为例,一只猪现在的状态由 die/live 变成了 die1/die2/die3/live (die2 表示第二次喝水的时候嘎了) 也就是 4种状态,那么 n 只猪喝水 3 此最多可以表示 4^n 个状态。

Js 对任意底求对数:

function myLog(m, n) {
  if(n < 1) return null;
  return Math.log(m) / Math.log(n);
}

console.log(27,3); // 3.0000000000000004