【Leetcode 做题学算法周刊】第九期

首发于微信公众号《前端成长记》,写于 2021.01.11

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

171.Excel表列序号

题目地址

题目描述

给定一个Excel表格中的列名称,返回其相应的列序号。

例如,

    A -> 1
    B -> 2
    C -> 3
    ...
    Z -> 26
    AA -> 27
    AB -> 28
    ...

示例:

输入: "A"
输出: 1

输入: "AB"
输出: 28

输入: "ZY"
输出: 701

题目分析设想

这道题其实就是上面168题的逆推导,也是就26进制转10进制。所以解法也很直接,做转换即可。无非区别在于正序和倒序处理罢了。

编写代码验证

Ⅰ.正序转换

代码:

/**
 * @param {string} s
 * @return {number}
 */
var titleToNumber = function(s) {
    let res = 0
    for(let i = 0; i < s.length; i++) {
        res = res * 26 + (s.charCodeAt(i) - 'A'.charCodeAt() + 1)
    }
    return res
};

结果:

  • 1000/1000 cases passed (84 ms)
  • Your runtime beats 69.77 % of javascript submissions
  • Your memory usage beats 33.33 % of javascript submissions (36.1 MB)
  • 时间复杂度: O(n)

Ⅱ.倒序转换

代码:

/**
 * @param {string} s
 * @return {number}
 */
var titleToNumber = function(s) {
    let res = 0
    let mul = 1
    for(let i = s.length - 1; i >= 0; i--) {
        res += (s.charCodeAt(i) - 'A'.charCodeAt() + 1) * mul
        mul *= 26
    }
    return res
};

结果:

  • 1000/1000 cases passed (100 ms)
  • Your runtime beats 20.5 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (34.7 MB)
  • 时间复杂度: O(n)

查阅他人解法

看了一下解法,大部分都是常规的倒序解法。正序的话其实就是秦九韶算法。

思考总结

这道题没有什么难度,就是一个进制转换的过程,比较容易想到的也就是倒序遍历,转换求解。

172.阶乘后的零

题目地址

题目描述

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

示例:

输入: 3
输出: 0
解释: 3! = 6, 尾数中没有零。

输入: 5
输出: 1
解释: 5! = 120, 尾数中有 1 个零.

说明: 你算法的时间复杂度应为 O(log n) 。

题目分析设想

这道题明显不适合算出来阶乘的值再来算有多少个零,因为很容易溢出,且复杂度高。所以这里需要找到规律,这里规律还很明显,零的产生一定是由 2x5(4看作2x2),所以本质上还是看 5 的数量,当然 5^n 还需要再累加。

编写代码验证

Ⅰ.累加5的每个幂的个数

代码:

/**
 * @param {number} n
 * @return {number}
 */
var trailingZeroes = function(n) {
    let res = 0
    while (n) {
        n = n / 5 | 0
        res += n
    }
    return res
};

结果:

  • 502/502 cases passed (76 ms)
  • Your runtime beats 55.62 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (34.5 MB)
  • 时间复杂度: O(log(n))

其实除5也可以等同于 xxx/5 + xxx/25 + xxx/125 + xxx/5^n,当5^n超过阶乘数的时候,必然取0。

代码:

/**
 * @param {number} n
 * @return {number}
 */
var trailingZeroes = function(n) {
    let res = 0
    let num = 5
    while (n >= num) {
        res += n / num | 0
        num *= 5
    }
    return res
};

结果:

  • 502/502 cases passed (76 ms)
  • Your runtime beats 55.62 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (34.3 MB)
  • 时间复杂度: O(log(n))

查阅他人解法

没有看见更优的算法,但是看到一个偏硬核的,其实也就是上面的解法的提前枚举。因为我们之前数值运算有MAX边界,所以5^n是可枚举。

Ⅰ.枚举

代码:

/**
 * @param {number} n
 * @return {number}
 */
var trailingZeroes = function(n) {
    return (n/5 | 0)+(n/25 | 0)+(n/125 | 0)+(n/625 | 0)+(n/3125 | 0)+(n/15625 | 0)+(n/78125 | 0)+(n/390625 | 0)
    +(n/1953125 | 0)+(n/9765625 | 0)+(n/48828125 | 0)+(n/244140625 | 0)+(n/1220703125 | 0);
};

结果:

  • 502/502 cases passed (80 ms)
  • Your runtime beats 37.95 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (34.4 MB)
  • 时间复杂度: O(log(n))

思考总结

枚举的方式纯属一乐,这道题的本质还是找到5的关键点,将问题进行转换求解即可。

190.颠倒二进制位

题目地址

题目描述

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

示例:

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

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

提示:

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

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

题目分析设想

看到这道题,明显关键点就在各种位运算符的使用。但是也可以取巧通过字符串处理,所以下面就尝试从这两个方向去作答。

编写代码验证

Ⅰ.位运算

代码:

/**
 * @param {number} n - a positive integer
 * @return {number} - a positive integer
 */
var reverseBits = function(n) {
    let t = 32, r = 0;
    while(t--) {
        r = (r << 1) + (n & 1); // 输出结果左移并加上最后一位
        n >>= 1 // 待处理数字右移
    }
    return r >>> 0;
};

结果:

  • 600/600 cases passed (100 ms)
  • Your runtime beats 49.62 % of javascript submissions
  • Your memory usage beats 70.65 % of javascript submissions (39.4 MB)
  • 时间复杂度: O(1)

Ⅱ.字符串处理

代码:

/**
 * @param {number} n - a positive integer
 * @return {number} - a positive integer
 */
var reverseBits = function(n) {
    return parseInt(
        n.toString(2).split('').reverse().join('').padEnd(32, 0),
        2
    )
};

结果:

  • 600/600 cases passed (104 ms)
  • Your runtime beats 33.27 % of javascript submissions
  • Your memory usage beats 88.8 % of javascript submissions (39.1 MB)
  • 时间复杂度: O(1)

查阅他人解法

这里还看见另外一种位运算符的使用

Ⅰ.位移+换位

代码:

/**
 * @param {number} n - a positive integer
 * @return {number} - a positive integer
 */
var reverseBits = function(n) {
    let t = 32, r = 0;
    while(t--) {
        r = (r << 1) | (n & 1); // 输出结果左移并末位替换
        n >>= 1 // 待处理数字右移
    }
    return r >>> 0;
};

结果:

  • 600/600 cases passed (96 ms)
  • Your runtime beats 61.41 % of javascript submissions
  • Your memory usage beats 91.5 % of javascript submissions (39 MB)
  • 时间复杂度: O(1)

思考总结

这题的考点也就在位运算了,熟悉熟悉很容易就能处理。不过熟悉位运算符对于一些运算来讲还是非常有意义的。

191.位1的个数

题目地址

题目描述

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 1 的个数(也被称为汉明重量)。

示例:

输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。

输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。

输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。

提示:

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

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

题目分析设想

做了上道题之后,这道题,还可以使用位运算符来处理。同样,通过字符串获取也可以实现,但这显然不是这题的考点。

编写代码验证

Ⅰ.位运算

代码:

/**
 * @param {number} n - a positive integer
 * @return {number}
 */
var hammingWeight = function(n) {
    let t = 32, r = 0;
    while(t--) {
        if (n & 1 === 1) r++; // 判断最后一位
        n >>= 1 // 待处理数字右移
    }
    return r;
};

结果:

  • 601/601 cases passed (88 ms)
  • Your runtime beats 84.03 % of javascript submissions
  • Your memory usage beats 91.49 % of javascript submissions (38.8 MB)
  • 时间复杂度: O(1)

Ⅱ.字符串处理

代码:

/**
 * @param {number} n - a positive integer
 * @return {number}
 */
var hammingWeight = function(n) {
    return n.toString(2).replaceAll('0', '').length
};

结果:

  • 601/601 cases passed (92 ms)
  • Your runtime beats 68.73 % of javascript submissions
  • Your memory usage beats 88.54 % of javascript submissions (38.9 MB)
  • 时间复杂度: O(1)

查阅他人解法

实现方法就太多了,大同小异,既然考位运算,那就再换成左移作答吧。其他的思路也都大同小异,无非就是加减、几位运算的区别。

Ⅰ.位运算-1左移

代码:

/**
 * @param {number} n - a positive integer
 * @return {number} - a positive integer
 */
var reverseBits = function(n) {
    let t = 32, r = 0;
    while(t--) {
        if ((n & (1 << (32 - t))) != 0) {
            r++;
        }
    }
    return r;
};

结果:

  • 601/601 cases passed (104 ms)
  • Your runtime beats 21.48 % of javascript submissions
  • Your memory usage beats 82.75 % of javascript submissions (39 MB)
  • 时间复杂度: O(1)

思考总结

说白了如果做了上道题之后,这题就没有什么价值了,只是写法问题,解法可太多了。

198.打家劫舍

题目地址

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 400

题目分析设想

这道题可以转化一下,简化之后其实就是奇数项和偶数项的和的比较。最基础的思路就是分别求和,然后记录当前比较结果后再继续分别求和。如果每次利用上每次的结果的话,那就变成了一个动态规划求解的问题了,所以这道题我们循序渐进来作答。

编写代码验证

Ⅰ.分别求和

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function(nums) {
    let evenTotal = 0, oddTotal = 0;
    for(let i = 0; i < nums.length; i++) {
        if (i & 1) {
            evenTotal += nums[i]
            evenTotal = Math.max(evenTotal, oddTotal) // 取两种方式的更大值
        } else {
            oddTotal += nums[i]
            oddTotal = Math.max(evenTotal, oddTotal) // 取两种方式的更大值
        }
    }
    return Math.max(evenTotal, oddTotal)
};

结果:

  • 69/69 cases passed (84 ms)
  • Your runtime beats 53.82 % of javascript submissions
  • Your memory usage beats 61.74 % of javascript submissions (37.6 MB)
  • 时间复杂度: O(n)

Ⅱ.动态规划

代码:

/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function(nums) {
    const len = nums.length;
    if (len === 0) return 0
    if (len === 1) return nums[0]
    if (len === 2) return Math.max(nums[0], nums[1])
    // dp[i] 表示偷i+1间的最大收益
    let dp = [nums[0], Math.max(nums[0], nums[1])]
    for(let i = 2; i < len; i++) {
        dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i])
    }
    return dp[nums.length - 1]
};

结果:

  • 69/69 cases passed (72 ms)
  • Your runtime beats 95.68 % of javascript submissions
  • Your memory usage beats 26.94 % of javascript submissions (37.9 MB)
  • 时间复杂度: O(n)

查阅他人解法

看了一下解法,基本都是动态规划求解。

思考总结

这道题如果看多了,基本能直接转化成一个动态规划的问题,有点类似于买卖股票。但是这题我更建议分别求和每次做比较的方式,这样的话其实空间复杂度是明显更低的。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

posted on 2021-01-11 18:01  McChen  阅读(88)  评论(0编辑  收藏  举报