1. 程式人生 > 實用技巧 >【Leetcode 做題學演算法週刊】第九期

【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)