We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
首发于微信公众号《前端成长记》,写于 2020.01.15
本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:
题目地址
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
示例 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。
这道题,我的第一反应有点像求最大子序和,只不过这里不是求连续,是求单个,转换为增益的思想来处理。当然也可以使用两次遍历的笨办法来求解。我们分别来验证一下。
Ⅰ.两次遍历
代码:
/** * @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 };
Ⅱ.动态规划
/** * @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] };
这个思路还有一系列的优化过程,可以点击这里查看
很多问题都可以转换成动态规划的思想来解决,但是我这里还是更推荐使用增益思想,也可以理解为差分数组。但是如果题目允许多次买入卖出,我会更推荐使用动态规划来解决问题。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入: [7,1,5,3,6,4] 输出: 7 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
输入: [1,2,3,4,5] 输出: 4 解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。 因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 ```javascript 示例 3: ```javascript 输入: [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 };
Ⅱ.波峰波谷
/** * @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 };
Ⅲ.动态规划
/** * @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] };
这里看到了动态规划的优化版,主要是降低空间复杂度。其他的思路都区别不大。
Ⅰ.动态规划优化版
/** * @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] };
还可以进一步进行状态压缩
/** * @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 };
就这道题而言,我会推荐使用一次遍历的方式,也就是贪心算法,理解起来会十分清晰。当然,动态规划的解决范围更广,基本上可以解决这类型的所有题目。增益也是一个比较常见的手段。总体而言,这两道股票题还比较简单。
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
示例:
输入: "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 };
Ⅲ.单指针法(预处理字符)
/** * @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 };
Ⅳ.双指针法
/** * @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 };
这里看到一种利用栈的思路,先进后出,推一半入栈然后进行比较。
Ⅰ.利用栈
/** * @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 };
总体而言,判断回文字符或者相关的题目,我更推荐采用双指针法,思路非常清晰。这里头尾递归比较也可以作答,就不在这里列举了。
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
输入: [2,2,1] 输出: 1 输入: [4,1,2,1,2] 输出: 4
这题说明了线性时间复杂度,所以最多一次遍历。很容易想到用 Hash 表或者其他方式对各数字出现次数做个统计来求解,但是需要考虑如何不适用额外空间。这里很明显就指向了离散数学中的异或运算。
Ⅰ.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) } } };
Ⅱ.异或运算
简单列一下几条运算规则,利用这规则,发现很容易作答这道题。
/** * @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 };
没有发现其他不同方向的解法。
这里的话第一想法大多都是借助哈希表来实现,但是由于有补充说明,所以更推荐使用异或算法。纯粹是数学公式的应用场景之一,没有什么太多好总结的地方。
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 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 };
Ⅱ.特殊标识法
/** * @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 };
Ⅲ.双指针法
/** * @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 } };
这里发现一个有意思的思路,通过链路导致。如果是环,那么倒置后的尾节点等于倒置前的头节点。如果不是环,那么就是正常的倒置不相等。
Ⅰ.倒置法
/** * @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(1),要么是改变原数据本身,要么是使用双指针法。这里我比较推荐双指针法,当然倒置法也比较巧妙。
(完)
本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验 如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork (转载请注明出处:https://chenjiahao.xyz)
The text was updated successfully, but these errors were encountered:
No branches or pull requests
背景
本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:
目录
Easy
121.买卖股票的最佳时机
题目地址
题目描述
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
示例 1:
示例 2:
题目分析设想
这道题,我的第一反应有点像求最大子序和,只不过这里不是求连续,是求单个,转换为增益的思想来处理。当然也可以使用两次遍历的笨办法来求解。我们分别来验证一下。
编写代码验证
Ⅰ.两次遍历
代码:
结果:
O(n^2)
Ⅱ.增益思想
代码:
结果:
O(n)
查阅他人解法
这里看到两种不同的思考,一种是理解为波峰和波谷,找到波谷后的下一个波峰,判断每个波峰与波谷差值的大小。另外一种是基于状态机的动态规划,也就是说把可能性都前置运算后,再进行比较。
Ⅰ.波峰波谷
代码:
结果:
O(n)
Ⅱ.动态规划
代码:
结果:
O(n)
这个思路还有一系列的优化过程,可以点击这里查看
思考总结
很多问题都可以转换成动态规划的思想来解决,但是我这里还是更推荐使用增益思想,也可以理解为差分数组。但是如果题目允许多次买入卖出,我会更推荐使用动态规划来解决问题。
122.买卖股票的最佳时机Ⅱ
题目地址
题目描述
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
示例 2:
题目分析设想
上面刚刚做了算最大收益的,这题明显是算累计收益的,所以可以按以下几个方向:
编写代码验证
Ⅰ.一次遍历
代码:
结果:
O(n)
Ⅱ.波峰波谷
代码:
结果:
O(n)
Ⅲ.动态规划
代码:
结果:
O(n)
查阅他人解法
这里看到了动态规划的优化版,主要是降低空间复杂度。其他的思路都区别不大。
Ⅰ.动态规划优化版
代码:
结果:
O(n)
还可以进一步进行状态压缩
代码:
结果:
O(n)
思考总结
就这道题而言,我会推荐使用一次遍历的方式,也就是贪心算法,理解起来会十分清晰。当然,动态规划的解决范围更广,基本上可以解决这类型的所有题目。增益也是一个比较常见的手段。总体而言,这两道股票题还比较简单。
125.验证回文串
题目地址
题目描述
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
示例:
题目分析设想
这道题我有两个方向,一是改变原输入串,二是不改变原输入串。
主要作答方法就是反转判断,双指针法以及二分法。
编写代码验证
Ⅰ.反转判断
代码:
结果:
O(1)
Ⅱ.双指针法(预处理字符)
代码:
结果:
O(n)
Ⅲ.单指针法(预处理字符)
代码:
结果:
O(n)
Ⅳ.双指针法
代码:
结果:
O(n)
查阅他人解法
这里看到一种利用栈的思路,先进后出,推一半入栈然后进行比较。
Ⅰ.利用栈
代码:
结果:
O(n)
思考总结
总体而言,判断回文字符或者相关的题目,我更推荐采用双指针法,思路非常清晰。这里头尾递归比较也可以作答,就不在这里列举了。
136.只出现一次的数字
题目地址
题目描述
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
题目分析设想
这题说明了线性时间复杂度,所以最多一次遍历。很容易想到用 Hash 表或者其他方式对各数字出现次数做个统计来求解,但是需要考虑如何不适用额外空间。这里很明显就指向了离散数学中的异或运算。
O(n)
的空间编写代码验证
Ⅰ.Hash 法
代码:
结果:
O(n)
Ⅱ.异或运算
简单列一下几条运算规则,利用这规则,发现很容易作答这道题。
代码:
结果:
O(n)
查阅他人解法
没有发现其他不同方向的解法。
思考总结
这里的话第一想法大多都是借助哈希表来实现,但是由于有补充说明,所以更推荐使用异或算法。纯粹是数学公式的应用场景之一,没有什么太多好总结的地方。
141.环形链表
题目地址
题目描述
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
题目分析设想
这道题的本质其实就是对象的比较,而对应的相等应当是引用同样的内存,可以想象成数组中找到同样的元素。所以第一个想法就是哈希表,当然也可以使用快慢指针来做处理。由于哈希表需要额外的内存,所以可以做优化,比如直接改变原对象,做特殊标识或者其他方式。
编写代码验证
Ⅰ.哈希表
代码:
结果:
O(n)
Ⅱ.特殊标识法
代码:
结果:
O(n)
Ⅲ.双指针法
代码:
结果:
O(n)
查阅他人解法
这里发现一个有意思的思路,通过链路导致。如果是环,那么倒置后的尾节点等于倒置前的头节点。如果不是环,那么就是正常的倒置不相等。
Ⅰ.倒置法
代码:
结果:
O(n)
思考总结
一般去重或者找到重复项用哈希的方式都能解决,但是在这题里,题目期望空间复杂度是
O(1)
,要么是改变原数据本身,要么是使用双指针法。这里我比较推荐双指针法,当然倒置法也比较巧妙。(完)
The text was updated successfully, but these errors were encountered: