首发于微信公众号《前端成长记》,写于 2020.01.15javascript
本文记录刷题过程当中的整个思考过程,以供参考。主要内容涵盖:前端
题目地址java
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。算法
若是你最多只容许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。数组
注意你不能在买入股票前卖出股票。微信
示例 1:优化
输入: [7,1,5,3,6,4] 输出: 5 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 由于卖出价格须要大于买入价格。
示例 2:ui
输入: [7,6,4,3,1] 输出: 0 解释: 在这种状况下, 没有交易完成, 因此最大利润为 0。
这道题,个人第一反应有点像求最大子序和,只不过这里不是求连续,是求单个,转换为增益的思想来处理。固然也可使用两次遍历的笨办法来求解。咱们分别来验证一下。设计
Ⅰ.两次遍历指针
代码:
/** * @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:
javascript 输入: [1,2,3,4,5] 输出: 4 解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能得到利润 = 5-1 = 4 。 注意你不能在第 1 天和第 2 天接连购买股票,以后再将它们卖出。 由于这样属于同时参与了多笔交易,你必须在再次购买前出售掉以前的股票。
javascript
示例 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)