首發於微信公衆號《前端成長記》,寫於 2020.01.15
本文記錄刷題過程當中的整個思考過程,以供參考。主要內容涵蓋:javascript
題目地址前端
給定一個數組,它的第 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 };
結果:
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 };
結果:
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 };
結果:
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] };
結果:
O(n)
這個思路還有一系列的優化過程,能夠點擊這裏查看
不少問題均可以轉換成動態規劃的思想來解決,可是我這裏仍是更推薦使用增益思想,也能夠理解爲差分數組。可是若是題目容許屢次買入賣出,我會更推薦使用動態規劃來解決問題。
給定一個數組,它的第 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 };
結果:
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 };
結果:
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] };
結果:
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] };
結果:
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 };
結果:
O(n)
就這道題而言,我會推薦使用一次遍歷的方式,也就是貪心算法,理解起來會十分清晰。固然,動態規劃的解決範圍更廣,基本上能夠解決這類型的全部題目。增益也是一個比較常見的手段。整體而言,這兩道股票題還比較簡單。
給定一個字符串,驗證它是不是迴文串,只考慮字母和數字字符,能夠忽略字母的大小寫。
說明:本題中,咱們將空字符串定義爲有效的迴文串。
示例:
輸入: "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('') };
結果:
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 };
結果:
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 };
結果:
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 };
結果:
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 };
結果:
O(n)
整體而言,判斷迴文字符或者相關的題目,我更推薦採用雙指針法,思路很是清晰。這裏頭尾遞歸比較也能夠做答,就不在這裏列舉了。
給定一個非空整數數組,除了某個元素只出現一次之外,其他每一個元素均出現兩次。找出那個只出現了一次的元素。
說明:
你的算法應該具備線性時間複雜度。 你能夠不使用額外空間來實現嗎?
輸入: [2,2,1] 輸出: 1 輸入: [4,1,2,1,2] 輸出: 4
這題說明了線性時間複雜度,因此最多一次遍歷。很容易想到用 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) } } };
結果:
O(n)
Ⅱ.異或運算
簡單列一下幾條運算規則,利用這規則,發現很容易做答這道題。
代碼:
/** * @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 };
結果:
O(n)
沒有發現其餘不一樣方向的解法。
這裏的話第一想法大多都是藉助哈希表來實現,可是因爲有補充說明,因此更推薦使用異或算法。純粹是數學公式的應用場景之一,沒有什麼太多好總結的地方。
給定一個鏈表,判斷鏈表中是否有環。
爲了表示給定鏈表中的環,咱們使用整數 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)(即,常量)內存解決此問題嗎?
這道題的本質其實就是對象的比較,而對應的相等應當是引用一樣的內存,能夠想象成數組中找到一樣的元素。因此第一個想法就是哈希表,固然也可使用快慢指針來作處理。因爲哈希表須要額外的內存,因此能夠作優化,好比直接改變原對象,作特殊標識或者其餘方式。
Ⅰ.哈希表
代碼:
/** * @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 };
結果:
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 };
結果:
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 } };
結果:
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 };
結果:
O(n)
通常去重或者找到重複項用哈希的方式都能解決,可是在這題裏,題目指望空間複雜度是 O(1)
,要麼是改變原數據自己,要麼是使用雙指針法。這裏我比較推薦雙指針法,固然倒置法也比較巧妙。
(完)
本文爲原創文章,可能會更新知識點及修正錯誤,所以轉載請保留原出處,方便溯源,避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗
若是能給您帶去些許幫助,歡迎 ⭐️ star 或 ✏️ fork
(轉載請註明出處: https://chenjiahao.xyz)