【Leetcode 作題學算法週刊】第七期

首發於微信公衆號《前端成長記》,寫於 2020.01.15

背景

本文記錄刷題過程當中的整個思考過程,以供參考。主要內容涵蓋:javascript

  • 題目分析設想
  • 編寫代碼驗證
  • 查閱他人解法
  • 思考總結

目錄

Easy

121.買賣股票的最佳時機

題目地址前端

題目描述

給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。java

若是你最多隻容許完成一筆交易(即買入和賣出一支股票),設計一個算法來計算你所能獲取的最大利潤。算法

注意你不能在買入股票前賣出股票。數組

示例 1:微信

輸入: [7,1,5,3,6,4]
輸出: 5
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 5 天(股票價格 = 6)的時候賣出,最大利潤 = 6-1 = 5 。
     注意利潤不能是 7-1 = 6, 由於賣出價格須要大於買入價格。

示例 2:優化

輸入: [7,6,4,3,1]
輸出: 0
解釋: 在這種狀況下, 沒有交易完成, 因此最大利潤爲 0。

題目分析設想

這道題,個人第一反應有點像求最大子序和,只不過這裏不是求連續,是求單個,轉換爲增益的思想來處理。固然也可使用兩次遍歷的笨辦法來求解。咱們分別來驗證一下。ui

編寫代碼驗證

Ⅰ.兩次遍歷設計

代碼:指針

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // 由於是利潤,因此不考慮負數
    let profit = 0
    for(let i = 0; i < prices.length; i++) {
        for(let j = i + 1; j < prices.length; j++) {
            profit = Math.max(prices[j] - prices[i], profit)
        }
    }
    return profit
};

結果:

  • 200/200 cases passed (384 ms)
  • Your runtime beats 25.89 % of javascript submissions
  • Your memory usage beats 19.85 % of javascript submissions (35.9 MB)
  • 時間複雜度 O(n^2)

Ⅱ.增益思想

代碼:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // 由於是利潤,因此不考慮負數
    let profit = 0
    let last = 0
    for(let i = 0; i < prices.length - 1; i++) {
        // 這裏其實能夠轉換爲每兩項價格相減後,再求最大子序和
        // prices[i + 1] - prices[i] 就是增益,和0比較是由於求利潤,不是求連續和
        last = Math.max(0, last + prices[i + 1] - prices[i])
        profit = Math.max(profit, last)
    }
    return profit
};

結果:

  • 200/200 cases passed (64 ms)
  • Your runtime beats 94.53 % of javascript submissions
  • Your memory usage beats 19.85 % of javascript submissions (35.9 MB)
  • 時間複雜度 O(n)

查閱他人解法

這裏看到兩種不一樣的思考,一種是理解爲波峯和波谷,找到波谷後的下一個波峯,判斷每一個波峯與波谷差值的大小。另一種是基於狀態機的動態規劃,也就是說把可能性都前置運算後,再進行比較。

Ⅰ.波峯波谷

代碼:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // 波谷
    let min = Infinity
    // 由於是利潤,因此不考慮負數
    let profit = 0
    for(let i = 0; i < prices.length; i++) {
        if (prices[i] < min) {
            min = prices[i]
        } else if (prices[i] - min > profit) {
            // 這裏是當前這個波峯和波谷的差值與歷史的進行比較
            profit = prices[i] - min
        }
    }
    return profit
};

結果:

  • 200/200 cases passed (68 ms)
  • Your runtime beats 86.75 % of javascript submissions
  • Your memory usage beats 21.34 % of javascript submissions (35.8 MB)
  • 時間複雜度 O(n)

Ⅱ.動態規劃

代碼:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // 動態初始數組
    let dp = new Array(prices.length).fill([])
    // 0:用戶手上不持股所能得到的最大利潤,特指賣出股票之後的不持股,非指沒有進行過任何交易的不持股
    // 1:用戶手上持股所能得到的最大利潤
    // 狀態 dp[i][0] 表示:在索引爲 i 的這一天,用戶手上不持股所能得到的最大利潤
    // 狀態 dp[i][1] 表示:在索引爲 i 的這一天,用戶手上持股所能得到的最大利潤
    // -prices[i] 就表示,在索引爲 i 的這一天,執行買入操做獲得的收益
    dp[0][0] = 0
    dp[0][1] = -prices[0]

    for(let i = 1; i < prices.length; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
        dp[i][1] = Math.max(dp[i - 1][1], -prices[i])
    }
    return dp[prices.length - 1][0]
};

結果:

  • 200/200 cases passed (72 ms)
  • Your runtime beats 75.01 % of javascript submissions
  • Your memory usage beats 12.43 % of javascript submissions (36.7 MB)
  • 時間複雜度 O(n)

這個思路還有一系列的優化過程,能夠點擊這裏查看

思考總結

不少問題均可以轉換成動態規劃的思想來解決,可是我這裏仍是更推薦使用增益思想,也能夠理解爲差分數組。可是若是題目容許屢次買入賣出,我會更推薦使用動態規劃來解決問題。

122.買賣股票的最佳時機Ⅱ

題目地址

題目描述

給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。

設計一個算法來計算你所能獲取的最大利潤。你能夠儘量地完成更多的交易(屢次買賣一支股票)。

注意:你不能同時參與多筆交易(你必須在再次購買前出售掉以前的股票)。

示例 1:

輸入: [7,1,5,3,6,4]
輸出: 7
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能得到利潤 = 5-1 = 4 。
     隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能得到利潤 = 6-3 = 3 。

示例 2:

輸入: [1,2,3,4,5]
輸出: 4
解釋: 在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能得到利潤 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接連購買股票,以後再將它們賣出。
     由於這樣屬於同時參與了多筆交易,你必須在再次購買前出售掉以前的股票。

示例 3:

輸入: [7,6,4,3,1]
輸出: 0
解釋: 在這種狀況下, 沒有交易完成, 因此最大利潤爲 0。

題目分析設想

上面剛剛作了算最大收益的,這題明顯是算累計收益的,因此能夠按如下幾個方向:

  • 一次遍歷,直接遍歷,不斷比較先後兩天價格,若是後一天收益高,則差值加到利潤,能夠理解爲貪心算法。
  • 波峯波谷,找到全部波峯波谷,差值相加便可
  • 動態規劃

編寫代碼驗證

Ⅰ.一次遍歷

代碼:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    let profit = 0
    for(let i = 1; i < prices.length; i++) {
        if (prices[i] > prices[i - 1]) {
            profit += prices[i] - prices[i - 1]
        }
    }
    return profit
};

結果:

  • 201/201 cases passed (68 ms)
  • Your runtime beats 77.02 % of javascript submissions
  • Your memory usage beats 13.55 % of javascript submissions (35.7 MB)
  • 時間複雜度 O(n)

Ⅱ.波峯波谷

代碼:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (!prices.length) return 0
    let profit = 0
    // 波峯波谷
    let min = max = prices[0]
    let i = 0
    while (i < prices.length - 1) {
        while(prices[i] >= prices[i + 1]) {
            i++
        }
        min = prices[i]
        while(prices[i] <= prices[i + 1]) {
            i++
        }
        max = prices[i]
        profit += max - min
    }
    return profit
};

結果:

  • 201/201 cases passed (68 ms)
  • Your runtime beats 77.02 % of javascript submissions
  • Your memory usage beats 14.4 % of javascript submissions (35.7 MB)
  • 時間複雜度 O(n)

Ⅲ.動態規劃

代碼:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // 動態初始數組
    let dp = new Array(prices.length).fill([])
    // 0:用戶手上不持股所能得到的最大利潤,特指賣出股票之後的不持股,非指沒有進行過任何交易的不持股
    // 1:用戶手上持股所能得到的最大利潤
    // 狀態 dp[i][0] 表示:在索引爲 i 的這一天,用戶手上不持股所能得到的最大利潤
    // 狀態 dp[i][1] 表示:在索引爲 i 的這一天,用戶手上持股所能得到的最大利潤
    // -prices[i] 就表示,在索引爲 i 的這一天,執行買入操做獲得的收益
    dp[0][0] = 0
    dp[0][1] = -prices[0]

    for(let i = 1; i < prices.length; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
        dp[i][1] = Math.max(dp[i - 1][1], dp[i][0] - prices[i])
    }
    return dp[prices.length - 1][0]
};

結果:

  • 201/201 cases passed (76 ms)
  • Your runtime beats 37.68 % of javascript submissions
  • Your memory usage beats 5.13 % of javascript submissions (36.7 MB)
  • 時間複雜度 O(n)

查閱他人解法

這裏看到了動態規劃的優化版,主要是下降空間複雜度。其餘的思路都區別不大。

Ⅰ.動態規劃優化版

代碼:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // cash 表示持有現金
    // hold 表示持有股票
    let cash = new Array(prices.length).fill(null)
    let hold = new Array(prices.length).fill(null)

    cash[0] = 0
    hold[0] = -prices[0]

    for(let i = 1; i < prices.length; i++) {
        cash[i] = Math.max(cash[i - 1], hold[i - 1] + prices[i])
        hold[i] = Math.max(hold[i - 1], cash[i - 1] - prices[i])
    }
    return cash[prices.length - 1]
};

結果:

  • 201/201 cases passed (68 ms)
  • Your runtime beats 77.02 % of javascript submissions
  • Your memory usage beats 9.7 % of javascript submissions (36 MB)
  • 時間複雜度 O(n)

還能夠進一步進行狀態壓縮

代碼:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length < 2) return 0
    // cash 表示持有現金
    // hold 表示持有股票
    // 加了兩個變量來存儲上一次的值
    let cash = tempCash = 0
    let hold = tempHold = -prices[0]

    for(let i = 1; i < prices.length; i++) {
        cash = Math.max(tempCash, tempHold + prices[i])
        hold = Math.max(tempHold, tempCash - prices[i])

        tempCash = cash
        tempHold = hold
    }
    return tempCash
};

結果:

  • 201/201 cases passed (72 ms)
  • Your runtime beats 58.45 % of javascript submissions
  • Your memory usage beats 10.55 % of javascript submissions (35.8 MB)
  • 時間複雜度 O(n)

思考總結

就這道題而言,我會推薦使用一次遍歷的方式,也就是貪心算法,理解起來會十分清晰。固然,動態規劃的解決範圍更廣,基本上能夠解決這類型的全部題目。增益也是一個比較常見的手段。整體而言,這兩道股票題還比較簡單。

125.驗證迴文串

題目地址

題目描述

給定一個字符串,驗證它是不是迴文串,只考慮字母和數字字符,能夠忽略字母的大小寫。

說明:本題中,咱們將空字符串定義爲有效的迴文串。

示例:

輸入: "A man, a plan, a canal: Panama"
輸出: true

輸入: "race a car"
輸出: false

題目分析設想

這道題我有兩個方向,一是改變原輸入串,二是不改變原輸入串。

  • 改變原輸入串,能夠去掉非字母和數字的字符後,反轉判斷或者雙指針判斷或者單指針
  • 不改變原輸入串,直接雙指針判斷

主要做答方法就是反轉判斷,雙指針法以及二分法。

編寫代碼驗證

Ⅰ.反轉判斷

代碼:

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    // 正則去除不知足條件的字符
    let str = s.toLowerCase().replace(/[^0-9a-z]/g, '')
    return str === str.split('').reverse().join('')
};

結果:

  • 476/476 cases passed (72 ms)
  • Your runtime beats 95.33 % of javascript submissions
  • Your memory usage beats 47.7 % of javascript submissions (38.1 MB)
  • 時間複雜度: O(1)

Ⅱ.雙指針法(預處理字符)

代碼:

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    // 正則去除不知足條件的字符
    let str = s.toLowerCase().replace(/[^0-9a-z]/g, '')
    let len = str.length
    let l = 0
    let r = len - 1
    while(l < r) {
        if (str.charAt(l) !== str.charAt(r)) {
            return false
        }
        l++
        r--
    }
    return true
};

結果:

  • 476/476 cases passed (76 ms)
  • Your runtime beats 89.25 % of javascript submissions
  • Your memory usage beats 70.96 % of javascript submissions (37.4 MB)
  • 時間複雜度: O(n)

Ⅲ.單指針法(預處理字符)

代碼:

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    // 正則去除不知足條件的字符
    let str = s.toLowerCase().replace(/[^0-9a-z]/g, '')
    let len = str.length
    // 最多須要判斷的次數
    let max = len >>> 1
    let i = 0
    while(i < max) {
        if (len % 2) { // 奇數
            if (str.charAt(max - i - 1) !== str.charAt(max + i + 1)) {
                return false
            }
        } else { // 偶數
            if (str.charAt(max - i - 1) !== str.charAt(max + i)) {
                return false
            }
        }
        i++
    }
    return true
};

結果:

  • 476/476 cases passed (72 ms)
  • Your runtime beats 95.33 % of javascript submissions
  • Your memory usage beats 56.02 % of javascript submissions (38 MB)
  • 時間複雜度: O(n)

Ⅳ.雙指針法

代碼:

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    let len = s.length
    let l = 0
    let r = len - 1
    while (l < r) {
        if (!/[0-9a-zA-Z]/.test(s.charAt(l))) {
            l++
        } else if (!/[0-9a-zA-Z]/.test(s.charAt(r))) {
            r--
        } else {
            if(s.charAt(l).toLowerCase() !== s.charAt(r).toLowerCase()) {
                return false
            }
            l++
            r--
        }

    }
    return true
};

結果:

  • 476/476 cases passed (76 ms)
  • Your runtime beats 89.25 % of javascript submissions
  • Your memory usage beats 13.06 % of javascript submissions (42 MB)
  • 時間複雜度: O(n)

查閱他人解法

這裏看到一種利用棧的思路,先進後出,推一半入棧而後進行比較。

Ⅰ.利用棧

代碼:

/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    // 正則去除不知足條件的字符
    let str = s.toLowerCase().replace(/[^0-9a-z]/g, '')
    let mid = str.length >>> 1
    let stack = str.substr(0, mid).split('')
    // 起始位置若是字符個數爲奇數則跳過中間位
    for(let i = str.length % 2 ? mid + 1 : mid; i < str.length; i++) {
        const last = stack.pop()
        if (last !== str.charAt(i)) {
            return false
        }
    }
    return true
};

結果:

  • 476/476 cases passed (84 ms)
  • Your runtime beats 65.67 % of javascript submissions
  • Your memory usage beats 71.81 % of javascript submissions (37.4 MB)
  • 時間複雜度: O(n)

思考總結

整體而言,判斷迴文字符或者相關的題目,我更推薦採用雙指針法,思路很是清晰。這裏頭尾遞歸比較也能夠做答,就不在這裏列舉了。

136.只出現一次的數字

題目地址

題目描述

給定一個非空整數數組,除了某個元素只出現一次之外,其他每一個元素均出現兩次。找出那個只出現了一次的元素。

說明:

你的算法應該具備線性時間複雜度。 你能夠不使用額外空間來實現嗎?

輸入: [2,2,1]
輸出: 1

輸入: [4,1,2,1,2]
輸出: 4

題目分析設想

這題說明了線性時間複雜度,因此最多一次遍歷。很容易想到用 Hash 表或者其餘方式對各數字出現次數作個統計來求解,可是須要考慮如何不適用額外空間。這裏很明顯就指向了離散數學中的異或運算。

  • Hash 法,須要額外 O(n) 的空間
  • 異或運算

編寫代碼驗證

Ⅰ.Hash 法

代碼:

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function(nums) {
    let hash = {}
    for(let i = 0; i < nums.length; i++) {
        if (hash[nums[i]]) {
            hash[nums[i]] = false
        } else if (hash[nums[i]] === undefined) {
            hash[nums[i]] = true
        }
    }
    for(let i in hash) {
        if(hash[i]) {
            return parseInt(i)
        }
    }
};

結果:

  • 16/16 cases passed (72 ms)
  • Your runtime beats 68.39 % of javascript submissions
  • Your memory usage beats 5.49 % of javascript submissions (38.6 MB)
  • 時間複雜度: O(n)

Ⅱ.異或運算

簡單列一下幾條運算規則,利用這規則,發現很容易做答這道題。

  • 交換律: a^b^c = a^c^b
  • 任何數和 0 異或爲自己:a^0 = a
  • 相同的數異或爲 0:a^a = 0

代碼:

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function(nums) {
    let n = 0
    for(let i = 0; i < nums.length; i++) {
        n ^= nums[i]
    }
    return n
};

結果:

  • 16/16 cases passed (60 ms)
  • Your runtime beats 95.77 % of javascript submissions
  • Your memory usage beats 74.07 % of javascript submissions (35.3 MB)
  • 時間複雜度: O(n)

查閱他人解法

沒有發現其餘不一樣方向的解法。

思考總結

這裏的話第一想法大多都是藉助哈希表來實現,可是因爲有補充說明,因此更推薦使用異或算法。純粹是數學公式的應用場景之一,沒有什麼太多好總結的地方。

141.環形鏈表

題目地址

題目描述

給定一個鏈表,判斷鏈表中是否有環。

爲了表示給定鏈表中的環,咱們使用整數 pos 來表示鏈表尾鏈接到鏈表中的位置(索引從 0 開始)。 若是 pos 是 -1,則在該鏈表中沒有環。

示例 1:
輸入:head = [3,2,0,-4], pos = 1
輸出:true
解釋:鏈表中有一個環,其尾部鏈接到第二個節點。

示例 2:
輸入:head = [1,2], pos = 0
輸出:true
解釋:鏈表中有一個環,其尾部鏈接到第一個節點。

示例 3:
輸入:head = [1], pos = -1
輸出:false
解釋:鏈表中沒有環。

進階:

你能用 O(1)(即,常量)內存解決此問題嗎?

題目分析設想

這道題的本質其實就是對象的比較,而對應的相等應當是引用一樣的內存,能夠想象成數組中找到一樣的元素。因此第一個想法就是哈希表,固然也可使用快慢指針來作處理。因爲哈希表須要額外的內存,因此能夠作優化,好比直接改變原對象,作特殊標識或者其餘方式。

  • 哈希表,直接利用哈希表存儲,也可使用 Map/Set 等等,直接判斷對象相等便可
  • 特殊標識,哈希表須要額外空間,能夠直接在原對象上打標識,或者置爲空等等特殊標識都可
  • 雙指針法,一快一慢,若是是環,那必然會存在相等的時候,若是不是環,那快的先走完

編寫代碼驗證

Ⅰ.哈希表

代碼:

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    let hashArr = []
    // val 可能爲 0 ,因此不能直接 !head
    while (head !== null) {
        if (hashArr.includes(head)) {
            return true
        } else {
            hashArr.push(head)
            head = head.next
        }
    }
    return false
};

結果:

  • 17/17 cases passed (116 ms)
  • Your runtime beats 12.03 % of javascript submissions
  • Your memory usage beats 5.05 % of javascript submissions (38.5 MB)
  • 時間複雜度: O(n)

Ⅱ.特殊標識法

代碼:

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    while (head && head.next) {
        if (head.FLAG) {
            return true
        } else {
            head.FLAG = true
            head = head.next
        }
    }
    return false
};

結果:

  • 17/17 cases passed (76 ms)
  • Your runtime beats 78.6 % of javascript submissions
  • Your memory usage beats 16.32 % of javascript submissions (37.5 MB)
  • 時間複雜度: O(n)

Ⅲ.雙指針法

代碼:

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    if (head && head.next) {
        let slow = head
        let fast = head.next
        while(slow !== fast) {
            if (fast && fast.next) {
                // 快指針須要比慢指針移動速度快,才能追上,因此是 .next.next
                fast = fast.next.next
                slow = slow.next
            } else {
                // 快指針走到頭了,因此必然不是環
                return false
            }
        }
        return true
    } else {
        return false
    }
};

結果:

  • 17/17 cases passed (76 ms)
  • Your runtime beats 78.6 % of javascript submissions
  • Your memory usage beats 56.97 % of javascript submissions (36.6 MB)
  • 時間複雜度: O(n)

查閱他人解法

這裏發現一個有意思的思路,經過鏈路致使。若是是環,那麼倒置後的尾節點等於倒置前的頭節點。若是不是環,那麼就是正常的倒置不相等。

Ⅰ.倒置法

代碼:

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    if (head === null || head.next === null) return false
    if (head === head.next) return true
    let p = head.next
    let q = p.next
    let x = head
    head.next = null
    // 至關於每遍歷一個鏈表,就把後面的指向前面一項,這樣當循環的時候,會反方向走出環形
    while(q !== null) {
        p.next = x
        x = p
        p = q
        q = q.next
    }
    return p === head
};

結果:

  • 17/17 cases passed (72 ms)
  • Your runtime beats 90.05 % of javascript submissions
  • Your memory usage beats 35.91 % of javascript submissions (36.8 MB)
  • 時間複雜度: O(n)

思考總結

通常去重或者找到重複項用哈希的方式都能解決,可是在這題裏,題目指望空間複雜度是 O(1),要麼是改變原數據自己,要麼是使用雙指針法。這裏我比較推薦雙指針法,固然倒置法也比較巧妙。

(完)


本文爲原創文章,可能會更新知識點及修正錯誤,所以轉載請保留原出處,方便溯源,避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗
若是能給您帶去些許幫助,歡迎 ⭐️ star 或 ✏️ fork
(轉載請註明出處: https://chenjiahao.xyz)
相關文章
相關標籤/搜索