diff --git a/README.md b/README.md index e706c4b144..c969002390 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ English version repo and Gitbook is on [english branch](https://github.com/labul Gitbook 地址:https://labuladong.gitbook.io/algo/ -2、建议关注我的公众号 **labuladong**,坚持高质量原创,说是最良心最硬核的技术公众号都不为过。本仓库的文章就是从公众号里整理出来的**一部分**内容,公众号后台回复关键词【电子书】可以获得这份小抄的完整版本;回复【加群】可以加入我们的刷题群,和大家一起讨论算法问题,分享内推机会: +3、建议关注我的公众号 **labuladong**,坚持高质量原创,说是最良心最硬核的技术公众号都不为过。本仓库的文章就是从公众号里整理出来的**一部分**内容,公众号后台回复关键词【电子书】可以获得这份小抄的完整版本;回复【加群】可以加入我们的刷题群,和大家一起讨论算法问题,分享内推机会:

diff --git "a/pictures/LRU\347\256\227\346\263\225/put.jpg" "b/pictures/LRU\347\256\227\346\263\225/put.jpg" new file mode 100644 index 0000000000..ca580eb634 Binary files /dev/null and "b/pictures/LRU\347\256\227\346\263\225/put.jpg" differ diff --git a/pictures/souyisou.png b/pictures/souyisou.png new file mode 100644 index 0000000000..15372ce6f6 Binary files /dev/null and b/pictures/souyisou.png differ diff --git a/pictures/table_qr2.jpg b/pictures/table_qr2.jpg new file mode 100644 index 0000000000..039e577284 Binary files /dev/null and b/pictures/table_qr2.jpg differ diff --git "a/pictures/\346\255\243\345\210\231/1.jpeg" "b/pictures/\346\255\243\345\210\231/1.jpeg" new file mode 100644 index 0000000000..e5977fcae6 Binary files /dev/null and "b/pictures/\346\255\243\345\210\231/1.jpeg" differ diff --git "a/pictures/\346\255\243\345\210\231/2.jpeg" "b/pictures/\346\255\243\345\210\231/2.jpeg" new file mode 100644 index 0000000000..f3fa3711a5 Binary files /dev/null and "b/pictures/\346\255\243\345\210\231/2.jpeg" differ diff --git "a/pictures/\346\255\243\345\210\231/3.jpeg" "b/pictures/\346\255\243\345\210\231/3.jpeg" new file mode 100644 index 0000000000..11d9838ddc Binary files /dev/null and "b/pictures/\346\255\243\345\210\231/3.jpeg" differ diff --git "a/pictures/\346\255\243\345\210\231/4.jpeg" "b/pictures/\346\255\243\345\210\231/4.jpeg" new file mode 100644 index 0000000000..24de168927 Binary files /dev/null and "b/pictures/\346\255\243\345\210\231/4.jpeg" differ diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/README.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/README.md" index 5075ab2b62..8dae5afb4b 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/README.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/README.md" @@ -8,6 +8,6 @@ 这就是思维模式的框架,**本章都会按照以上的模式来解决问题,辅助读者养成这种模式思维**,有了方向遇到问题就不会抓瞎,足以解决一般的动态规划问题。 -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: +欢迎关注我的公众号 labuladong,查看全部文章: ![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" index 7276292f5e..c757fff832 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" @@ -1,5 +1,25 @@ # 动态规划之KMP字符匹配算法 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [经典动态规划:最长公共子序列](https://labuladong.gitbook.io/algo) + * [特殊数据结构:单调栈](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[28.实现 strStr()](https://leetcode-cn.com/problems/implement-strstr) + +**-----------** + KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高,但是确实有点复杂。 很多读者抱怨 KMP 算法无法理解,这很正常,想到大学教材上关于 KMP 算法的讲解,也不知道有多少未来的 Knuth、Morris、Pratt 被提前劝退了。有一些优秀的同学通过手推 KMP 算法的过程来辅助理解该算法,这是一种办法,不过本文要从逻辑层面帮助读者理解算法的原理。十行代码之间,KMP 灰飞烟灭。 @@ -37,7 +57,7 @@ int search(String pat, String txt) { } ``` -对于暴力算法,如果出现不匹配字符,同时回退 `txt` 和 `pat` 的指针,嵌套 for 循环,时间复杂度 $O(MN)$,空间复杂度$O(1)$。最主要的问题是,如果字符串中重复的字符比较多,该算法就显得很蠢。 +对于暴力算法,如果出现不匹配字符,同时回退 `txt` 和 `pat` 的指针,嵌套 for 循环,时间复杂度 `O(MN)`,空间复杂度`O(1)`。最主要的问题是,如果字符串中重复的字符比较多,该算法就显得很蠢。 比如 txt = "aaacaaab" pat = "aaab": @@ -399,12 +419,14 @@ public class KMP { KMP 算法也就是动态规划那点事,我们的公众号文章目录有一系列专门讲动态规划的,而且都是按照一套框架来的,无非就是描述问题逻辑,明确 `dp` 数组含义,定义 base case 这点破事。希望这篇文章能让大家对动态规划有更深的理解。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[上一篇:贪心算法之区间调度问题](../动态规划系列/贪心算法之区间调度问题.md) +**_____________** + +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:团灭 LeetCode 股票买卖问题](../动态规划系列/团灭股票问题.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" index d474e003e8..9b0c22f40a 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" @@ -1,6 +1,26 @@ # 动态规划之博弈问题 -上一篇文章 [几道智力题](../高频面试系列/一行代码解决的智力题.md) 中讨论到一个有趣的「石头游戏」,通过题目的限制条件,这个游戏是先手必胜的。但是智力题终究是智力题,真正的算法问题肯定不会是投机取巧能搞定的。所以,本文就借石头游戏来讲讲「假设两个人都足够聪明,最后谁会获胜」这一类问题该如何用动态规划算法解决。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [40张图解:TCP三次握手和四次挥手面试题](https://labuladong.gitbook.io/algo) + * [如何计算完全二叉树的节点数](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[877.石子游戏](https://leetcode-cn.com/problems/stone-game) + +**-----------** + +上一篇文章 [几道智力题](https://labuladong.gitbook.io/algo) 中讨论到一个有趣的「石头游戏」,通过题目的限制条件,这个游戏是先手必胜的。但是智力题终究是智力题,真正的算法问题肯定不会是投机取巧能搞定的。所以,本文就借石头游戏来讲讲「假设两个人都足够聪明,最后谁会获胜」这一类问题该如何用动态规划算法解决。 博弈类问题的套路都差不多,下文举例讲解,其核心思路是在二维 dp 的基础上使用元组分别存储两个人的博弈结果。掌握了这个技巧以后,别人再问你什么俩海盗分宝石,俩人拿硬币的问题,你就告诉别人:我懒得想,直接给你写个算法算一下得了。 @@ -43,7 +63,7 @@ dp[0][1].fir = 9 意味着:面对石头堆 [3, 9],先手最终能够获得 9 dp[1][3].sec = 2 意味着:面对石头堆 [9, 1, 2],后手最终能够获得 2 分。 ``` -我们想求的答案是先手和后手最终分数之差,按照这个定义也就是 $dp[0][n-1].fir - dp[0][n-1].sec$,即面对整个 piles,先手的最优得分和后手的最优得分之差。 +我们想求的答案是先手和后手最终分数之差,按照这个定义也就是 `dp[0][n-1].fir - dp[0][n-1].sec`,即面对整个 piles,先手的最优得分和后手的最优得分之差。 ### 二、状态转移方程 @@ -183,41 +203,14 @@ int stoneGame(int[] piles) { 希望本文对你有帮助。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) -[Hanmin](https://github.com/Miraclemin/) 提供 Python3 代码: -```python -def stoneGame(self, piles:List[int]) -> int: - n = len(piles) - ##初始化dp数组,用三维数组表示 - dp = [[[0,0] for _ in range(0,n)] for _ in range(n)] - ##填入base case - for i in range(0,n): - dp[i][i][0] = piles[i] - dp[i][i][1] = 0 - ##斜着遍历数组 - for l in range(2,n+1): - for i in range(0,n-l+1): - j = l + i - 1 - ##先手选择最左边或者最右边的分数 - left = piles[i] + dp[i+1][j][1] - right = piles[j] + dp[i][j-1][1] - ##套用状态转移方程 - if left > right: - dp[i][j][0] = left - dp[i][j][1] = dp[i+1][j][0] - else: - dp[i][j][0] = right - dp[i][j][1] = dp[i][j-1][0] - res = dp[0][n-1] - return res[0] - res[1] -``` +**_____________** -[上一篇:动态规划之子序列问题解题模板](../动态规划系列/子序列问题模板.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:贪心算法之区间调度问题](../动态规划系列/贪心算法之区间调度问题.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" index 2605d4c9f2..6dc1c93778 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" @@ -1,5 +1,27 @@ # 动态规划之四键键盘 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [如何高效寻找素数](https://labuladong.gitbook.io/algo) + * [动态规划解题套路框架](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[651.四键键盘](https://leetcode-cn.com/problems/4-keys-keyboard) + +**-----------** + +PS:现在这到题好想变成会员题目了?我当时做的时候还是免费的。 + 四键键盘问题很有意思,而且可以明显感受到:对 dp 数组的不同定义需要完全不同的逻辑,从而产生完全不同的解法。 首先看一下题目: @@ -168,13 +190,12 @@ def dp(n, a_num, copy): 根据这个事实,我们重新定义了状态,重新寻找了状态转移,从逻辑上减少了无效的子问题个数,从而提高了算法的效率。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:团灭 LeetCode 打家劫舍问题](../动态规划系列/抢房子.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:动态规划之正则表达](../动态规划系列/动态规划之正则表达.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" index 605bccc2c9..ba289f56d0 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" @@ -1,177 +1,297 @@ # 动态规划之正则表达 -之前的文章「动态规划详解」收到了普遍的好评,今天写一个动态规划的实际应用:正则表达式。如果有读者对「动态规划」还不了解,建议先看一下上面那篇文章。 +

+GitHub + + + +

-正则表达式匹配是一个很精妙的算法,而且难度也不小。本文主要写两个正则符号的算法实现:点号「.」和星号「*」,如果你用过正则表达式,应该明白他们的用法,不明白也没关系,等会会介绍。文章的最后,介绍了一种快速看出重叠子问题的技巧。 +![](../pictures/souyisou.png) -本文还有一个重要目的,就是教会读者如何设计算法。我们平时看别人的解法,直接看到一个面面俱到的完整答案,总觉得无法理解,以至觉得问题太难,自己太菜。我力求向读者展示,算法的设计是一个螺旋上升、逐步求精的过程,绝不是一步到位就能写出正确算法。本文会带你解决这个较为复杂的问题,让你明白如何化繁为简,逐个击破,从最简单的框架搭建出最终的答案。 +相关推荐: + * [我写了首诗,把滑动窗口算法算法变成了默写题](https://labuladong.gitbook.io/algo) + * [二分查找高效判定子序列](https://labuladong.gitbook.io/algo) -前文无数次强调的框架思维,就是在这种设计过程中逐步培养的。下面进入正题,首先看一下题目: +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: -![title](../pictures/%E6%AD%A3%E5%88%99/title.png) +[10.正则表达式匹配](https://leetcode-cn.com/problems/regular-expression-matching/) -### 一、热身 +**-----------** -第一步,我们暂时不管正则符号,如果是两个普通的字符串进行比较,如何进行匹配?我想这个算法应该谁都会写: +正则表达式是一个非常强力的工具,本文就来具体看一看正则表达式的底层原理是什么。力扣第 10 题「正则表达式匹配」就要求我们实现一个简单的正则匹配算法,包括「.」通配符和「*」通配符。 + +这两个通配符是最常用的,其中点号「.」可以匹配任意一个字符,星号「*」可以让之前的那个字符重复任意次数(包括 0 次)。 + +比如说模式串 `".a*b"` 就可以匹配文本 `"zaaab"`,也可以匹配 `"cb"`;模式串 `"a..b"` 可以匹配文本 `"amnb"`;而模式串 `".*"` 就比较牛逼了,它可以匹配任何文本。 + +题目会给我们输入两个字符串 `s` 和 `p`,`s` 代表文本,`p` 代表模式串,请你判断模式串 `p` 是否可以匹配文本 `s`。我们可以假设模式串只包含小写字母和上述两种通配符且一定合法,不会出现 `*a` 或者 `b**` 这种不合法的模式串, + +函数签名如下: ```cpp -bool isMatch(string text, string pattern) { - if (text.size() != pattern.size()) - return false; - for (int j = 0; j < pattern.size(); j++) { - if (pattern[j] != text[j]) - return false; - } - return true; -} +bool isMatch(string s, string p); ``` -然后,我稍微改造一下上面的代码,略微复杂了一点,但意思还是一样的,很容易理解吧: +对于我们将要实现的这个正则表达式,难点在那里呢? + +点号通配符其实很好实现,`s` 中的任何字符,只要遇到 `.` 通配符,无脑匹配就完事了。主要是这个星号通配符不好实现,一旦遇到 `*` 通配符,前面的那个字符可以选择重复一次,可以重复多次,也可以一次都不出现,这该怎么办? + +对于这个问题,答案很简单,对于所有可能出现的情况,全部穷举一遍,只要有一种情况可以完成匹配,就认为 `p` 可以匹配 `s`。那么一旦涉及两个字符串的穷举,我们就应该条件反射地想到动态规划的技巧了。 + +### 一、思路分析 + +我们先脑补一下,`s` 和 `p` 相互匹配的过程大致是,两个指针 `i, j` 分别在 `s` 和 `p` 上移动,如果最后两个指针都能移动到字符串的末尾,那么久匹配成功,反之则匹配失败。 + +**如果不考虑 `*` 通配符,面对两个待匹配字符 `s[i]` 和 `p[j]`,我们唯一能做的就是看他俩是否匹配**: ```cpp -bool isMatch(string text, string pattern) { - int i = 0; // text 的索引位置 - int j = 0; // pattern 的索引位置 - while (j < pattern.size()) { - if (i >= text.size()) - return false; - if (pattern[j++] != text[i++]) +bool isMatch(string s, string p) { + int i = 0, j = 0; + while (i < s.size() && j < p.size()) { + // 「.」通配符就是万金油 + if (s[i] == p[j] || p[j] == '.') { + // 匹配,接着匹配 s[i+1..] 和 p[j+1..] + i++; j++; + } else { + // 不匹配 return false; + } } - // 相等则说明完成匹配 - return j == text.size(); + return i == j; } ``` -如上改写,是为了将这个算法改造成递归算法(伪码): +那么考虑一下,如果加入 `*` 通配符,局面就会稍微复杂一些,不过只要分情况来分析,也不难理解。 + +**当 `p[j + 1]` 为 `*` 通配符时,我们分情况讨论下**: + +1、如果 `s[i] == p[j]`,那么有两种情况: + +1.1 `p[j]` 有可能会匹配多个字符,比如 `s = "aaa", p = "a*"`,那么 `p[0]` 会通过 `*` 匹配 3 个字符 `"a"`。 -```python -def isMatch(text, pattern) -> bool: - if pattern is empty: return (text is empty?) - first_match = (text not empty) and pattern[0] == text[0] - return first_match and isMatch(text[1:], pattern[1:]) +1.2 `p[i]` 也有可能匹配 0 个字符,比如 `s = "aa", p = "a*aa"`,由于后面的字符可以匹配 `s`,所以 `p[0]` 只能匹配 0 次。 + +2、如果 `s[i] != p[j]`,只有一种情况: + +`p[j]` 只能匹配 0 次,然后看下一个字符是否能和 `s[i]` 匹配。比如说 `s = "aa", p = "b*aa"`,此时 `p[0]` 只能匹配 0 次。 + +综上,可以把之前的代码针对 `*` 通配符进行一下改造: + +```cpp +if (s[i] == p[j] || p[j] == '.') { + // 匹配 + if (j < p.size() - 1 && p[j + 1] == '*') { + // 有 * 通配符,可以匹配 0 次或多次 + } else { + // 无 * 通配符,老老实实匹配 1 次 + i++; j++; + } +} else { + // 不匹配 + if (j < p.size() - 1 && p[j + 1] == '*') { + // 有 * 通配符,只能匹配 0 次 + } else { + // 无 * 通配符,匹配无法进行下去了 + return false; + } +} ``` -如果你能够理解这段代码,恭喜你,你的递归思想已经到位,正则表达式算法虽然有点复杂,其实是基于这段递归代码逐步改造而成的。 +整体的思路已经很清晰了,但现在的问题是,遇到 `*` 通配符时,到底应该匹配 0 次还是匹配多次?多次是几次? -### 二、处理点号「.」通配符 +你看,这就是一个做「选择」的问题,要把所有可能的选择都穷举一遍才能得出结果。动态规划算法的核心就是「状态」和「选择」,**「状态」无非就是 `i` 和 `j` 两个指针的位置,「选择」就是 `p[j]` 选择匹配几个字符**。 -点号可以匹配任意一个字符,万金油嘛,其实是最简单的,稍加改造即可: +### 二、动态规划解法 -```python -def isMatch(text, pattern) -> bool: - if not pattern: return not text - first_match = bool(text) and pattern[0] in {text[0], '.'} - return first_match and isMatch(text[1:], pattern[1:]) +根据「状态」,我们可以定义一个 `dp` 函数: + +```cpp +bool dp(string& s, int i, string& p, int j); ``` -### 三、处理「*」通配符 +`dp` 函数的定义如下: -星号通配符可以让前一个字符重复任意次数,包括零次。那到底是重复几次呢?这似乎有点困难,不过不要着急,我们起码可以把框架的搭建再进一步: +**若 `dp(s, i, p, j) = true`,则表示 `s[i..]` 可以匹配 `p[j..]`;若 `dp(s, i, p, j) = false`,则表示 `s[i..]` 无法匹配 `p[j..]`**。 -```python -def isMatch(text, pattern) -> bool: - if not pattern: return not text - first_match = bool(text) and pattern[0] in {text[0], '.'} - if len(pattern) >= 2 and pattern[1] == '*': - # 发现 '*' 通配符 - else: - return first_match and isMatch(text[1:], pattern[1:]) +根据这个定义,我们想要的答案就是 `i = 0, j = 0` 时 `dp` 函数的结果,所以可以这样使用这个 `dp` 函数: + +```cpp +bool isMatch(string s, string p) { + // 指针 i,j 从索引 0 开始移动 + return dp(s, 0, p, 0); ``` -星号前面的那个字符到底要重复几次呢?这需要计算机暴力穷举来算,假设重复 N 次吧。前文多次强调过,写递归的技巧是管好当下,之后的事抛给递归。具体到这里,不管 N 是多少,当前的选择只有两个:匹配 0 次、匹配 1 次。所以可以这样处理: +可以根据之前的代码写出 `dp` 函数的主要逻辑: -```py -if len(pattern) >= 2 and pattern[1] == '*': - return isMatch(text, pattern[2:]) or \ - first_match and isMatch(text[1:], pattern) -# 解释:如果发现有字符和 '*' 结合, - # 或者匹配该字符 0 次,然后跳过该字符和 '*' - # 或者当 pattern[0] 和 text[0] 匹配后,移动 text +```cpp +bool dp(string& s, int i, string& p, int j) { + if (s[i] == p[j] || p[j] == '.') { + // 匹配 + if (j < p.size() - 1 && p[j + 1] == '*') { + // 1.1 通配符匹配 0 次或多次 + return dp(s, i, p, j + 2) + || dp(s, i + 1, p, j); + } else { + // 1.2 常规匹配 1 次 + return dp(s, i + 1, p, j + 1); + } + } else { + // 不匹配 + if (j < p.size() - 1 && p[j + 1] == '*') { + // 2.1 通配符匹配 0 次 + return dp(s, i, p, j + 2); + } else { + // 2.2 无法继续匹配 + return false; + } + } +} ``` -可以看到,我们是通过保留 pattern 中的「\*」,同时向后推移 text,来实现「*」将字符重复匹配多次的功能。举个简单的例子就能理解这个逻辑了。假设 `pattern = a*`, `text = aaa`,画个图看看匹配过程: -![example](../pictures/%E6%AD%A3%E5%88%99/example.png) +**根据 `dp` 函数的定义**,这几种情况都很好解释: -至此,正则表达式算法就基本完成了, +1.1 通配符匹配 0 次或多次 -### 四、动态规划 +将 `j` 加 2,`i` 不变,含义就是直接跳过 `p[j]` 和之后的通配符,即通配符匹配 0 次: -我选择使用「备忘录」递归的方法来降低复杂度。有了暴力解法,优化的过程及其简单,就是使用两个变量 i, j 记录当前匹配到的位置,从而避免使用子字符串切片,并且将 i, j 存入备忘录,避免重复计算即可。 +![](../pictures/正则/1.jpeg) -我将暴力解法和优化解法放在一起,方便你对比,你可以发现优化解法无非就是把暴力解法「翻译」了一遍,加了个 memo 作为备忘录,仅此而已。 +将 `i` 加 1,`j` 不变,含义就是 `p[j]` 匹配了 `s[i]`,但 `p[j]` 还可以继续匹配,即通配符匹配多次的情况: -```py -# 带备忘录的递归 -def isMatch(text, pattern) -> bool: - memo = dict() # 备忘录 - def dp(i, j): - if (i, j) in memo: return memo[(i, j)] - if j == len(pattern): return i == len(text) +![](../pictures/正则/2.jpeg) - first = i < len(text) and pattern[j] in {text[i], '.'} - - if j <= len(pattern) - 2 and pattern[j + 1] == '*': - ans = dp(i, j + 2) or \ - first and dp(i + 1, j) - else: - ans = first and dp(i + 1, j + 1) - - memo[(i, j)] = ans - return ans - - return dp(0, 0) +两种情况只要有一种可以完成匹配即可,所以对上面两种情况求或运算。 + +1.2 常规匹配 1 次 + +由于这个条件分支是无 `*` 的常规匹配,那么如果 `s[i] == p[j]`,就是 `i` 和 `j` 分别加一: + +![](../pictures/正则/3.jpeg) + +2.1 通配符匹配 0 次 + +类似情况 1.1,将 `j` 加 2,`i` 不变: + +![](../pictures/正则/1.jpeg) + +2.2 如果没有 `*` 通配符,也无法匹配,那只能说明匹配失败了: + +![](../pictures/正则/4.jpeg) -# 暴力递归 -def isMatch(text, pattern) -> bool: - if not pattern: return not text +看图理解应该很容易了,现在可以思考一下 `dp` 函数的 base case: - first = bool(text) and pattern[0] in {text[0], '.'} +**一个 base case 是 `j == p.size()` 时**,按照 `dp` 函数的定义,这意味着模式串 `p` 已经被匹配完了,那么应该看看文本串 `s` 匹配到哪里了,如果 `s` 也恰好被匹配完,则说明匹配成功: - if len(pattern) >= 2 and pattern[1] == '*': - return isMatch(text, pattern[2:]) or \ - first and isMatch(text[1:], pattern) - else: - return first and isMatch(text[1:], pattern[1:]) +```cpp +if (j == p.size()) { + return i == s.size(); +} ``` -**有的读者也许会问,你怎么知道这个问题是个动态规划问题呢,你怎么知道它就存在「重叠子问题」呢,这似乎不容易看出来呀?** +**另一个 base case 是 `i == s.size()` 时**,按照 `dp` 函数的定义,这种情况意味着文本串 `s` 已经全部被匹配了,那么是不是只要简单地检查一下 `p` 是否也匹配完就行了呢? -解答这个问题,最直观的应该是随便假设一个输入,然后画递归树,肯定是可以发现相同节点的。这属于定量分析,其实不用这么麻烦,下面我来教你定性分析,一眼就能看出「重叠子问题」性质。 +```cpp +if (i == s.size()) { + // 这样行吗? + return j == p.size(); +} +``` -先拿最简单的斐波那契数列举例,我们抽象出递归算法的框架: +**这是不正确的,此时并不能根据 `j` 是否等于 `p.size()` 来判断是否完成匹配,只要 `p[j..]` 能够匹配空串,就可以算完成匹配**。比如说 `s = "a", p = "ab*c*"`,当 `i` 走到 `s` 末尾的时候,`j` 并没有走到 `p` 的末尾,但是 `p` 依然可以匹配 `s`。 -```py -def fib(n): - fib(n - 1) #1 - fib(n - 2) #2 +所以我们可以写出如下代码: + +```cpp +int m = s.size(), n = p.size(); + +if (i == s.size()) { + // 如果能匹配空串,一定是字符和 * 成对儿出现 + if ((n - j) % 2 == 1) { + return false; + } + // 检查是否为 x*y*z* 这种形式 + for (; j + 1 < p.size(); j += 2) { + if (p[j + 1] != '*') { + return false; + } + } + return true; +} ``` -看着这个框架,请问原问题 f(n) 如何触达子问题 f(n - 2) ?有两种路径,一是 f(n) -> #1 -> #1, 二是 f(n) -> #2。前者经过两次递归,后者进过一次递归而已。两条不同的计算路径都到达了同一个问题,这就是「重叠子问题」,而且可以肯定的是,**只要你发现一条重复路径,这样的重复路径一定存在千万条,意味着巨量子问题重叠。** +根据以上思路,就可以写出完整的代码: -同理,对于本问题,我们依然先抽象出算法框架: +```cpp +/* 计算 p[j..] 是否匹配 s[i..] */ +bool dp(string& s, int i, string& p, int j) { + int m = s.size(), n = p.size(); + // base case + if (j == n) { + return i == m; + } + if (i == m) { + if ((n - j) % 2 == 1) { + return false; + } + for (; j + 1 < n; j += 2) { + if (p[j + 1] != '*') { + return false; + } + } + return true; + } -```py -def dp(i, j): - dp(i, j + 2) #1 - dp(i + 1, j) #2 - dp(i + 1, j + 1) #3 + // 记录状态 (i, j),消除重叠子问题 + string key = to_string(i) + "," + to_string(j); + if (memo.count(key)) return memo[key]; + + bool res = false; + + if (s[i] == p[j] || p[j] == '.') { + if (j < n - 1 && p[j + 1] == '*') { + res = dp(s, i, p, j + 2) + || dp(s, i + 1, p, j); + } else { + res = dp(s, i + 1, p, j + 1); + } + } else { + if (j < n - 1 && p[j + 1] == '*') { + res = dp(s, i, p, j + 2); + } else { + res = false; + } + } + // 将当前结果记入备忘录 + memo[key] = res; + + return res; +} ``` -提出类似的问题,请问如何从原问题 dp(i, j) 触达子问题 dp(i + 2, j + 2) ?至少有两种路径,一是 dp(i, j) -> #3 -> #3,二是 dp(i, j) -> #1 -> #2 -> #2。因此,本问题一定存在重叠子问题,一定需要动态规划的优化技巧来处理。 +代码中用了一个哈希表 `memo` 消除重叠子问题,因为正则表达算法的递归框架如下: -### 五、最后总结 +```cpp +bool dp(string& s, int i, string& p, int j) { + dp(s, i, p, j + 2); // 1 + dp(s, i + 1, p, j); // 2 + dp(s, i + 1, p, j + 1); // 3 +} +``` -通过本文,你深入理解了正则表达式的两种常用通配符的算法实现。其实点号「.」的实现及其简单,关键是星号「*」的实现需要用到动态规划技巧,稍微复杂些,但是也架不住我们对问题的层层拆解,逐个击破。另外,你掌握了一种快速分析「重叠子问题」性质的技巧,可以快速判断一个问题是否可以使用动态规划套路解决。 +那么,如果让你从 `dp(s, i, p, j)` 得到 `dp(s, i+2, p, j+2)`,至少有两条路径:`1 -> 2 -> 2` 和 `3 -> 3`,那么就说明 `(i+2, j+2)` 这个状态存在重复,这就说明存在重叠子问题。 -回顾整个解题过程,你应该能够体会到算法设计的流程:从简单的类似问题入手,给基本的框架逐渐组装新的逻辑,最终成为一个比较复杂、精巧的算法。所以说,读者不必畏惧一些比较复杂的算法问题,多思考多类比,再高大上的算法在你眼里也不过一个脆皮。 +动态规划的时间复杂度为「状态的总数」*「每次递归花费的时间」,本题中状态的总数当然就是 `i` 和 `j` 的组合,也就是 `M * N`(`M` 为 `s` 的长度,`N` 为 `p` 的长度);递归函数 `dp` 中没有循环(base case 中的不考虑,因为 base case 的触发次数有限),所以一次递归花费的时间为常数。二者相乘,总的时间复杂度为 `O(MN)`。 -如果本文对你有帮助,欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚~ +空间复杂度很简单,就是备忘录 `memo` 的大小,即 `O(MN)`。 +**_____________** +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[上一篇:动态规划之四键键盘](../动态规划系列/动态规划之四键键盘.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[下一篇:最长公共子序列](../动态规划系列/最长公共子序列.md) +

+ +

-[目录](../README.md#目录) \ No newline at end of file diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" index a90557e80c..a5e0a86de6 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" @@ -1,26 +1,51 @@ # 动态规划设计:最长递增子序列 -很多读者反应,就算看了前文[动态规划详解](动态规划详解进阶.md),了解了动态规划的套路,也不会写状态转移方程,没有思路,怎么办?本文就借助「最长递增子序列」来讲一种设计动态规划的通用技巧:数学归纳思想。 +

+GitHub + + + +

-最长递增子序列(Longest Increasing Subsequence,简写 LIS)是比较经典的一个问题,比较容易想到的是动态规划解法,时间复杂度 O(N^2),我们借这个问题来由浅入深讲解如何写动态规划。比较难想到的是利用二分查找,时间复杂度是 O(NlogN),我们通过一种简单的纸牌游戏来辅助理解这种巧妙的解法。 +![](../pictures/souyisou.png) + +相关推荐: + * [动态规划设计:最大子数组](../动态规划系列/最大子数组.md) + * [一文学会递归解题](../投稿/一文学会递归解题.md) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[300.最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence) + +**-----------** + +也许有读者看了前文 [动态规划详解](../动态规划系列/动态规划详解进阶.md),学会了动态规划的套路:找到了问题的「状态」,明确了 `dp` 数组/函数的含义,定义了 base case;但是不知道如何确定「选择」,也就是不到状态转移的关系,依然写不出动态规划解法,怎么办? + +不要担心,动态规划的难点本来就在于寻找正确的状态转移方程,本文就借助经典的「最长递增子序列问题」来讲一讲设计动态规划的通用技巧:**数学归纳思想**。 + +最长递增子序列(Longest Increasing Subsequence,简写 LIS)是非常经典的一个算法问题,比较容易想到的是动态规划解法,时间复杂度 O(N^2),我们借这个问题来由浅入深讲解如何找状态转移方程,如何写出动态规划解法。比较难想到的是利用二分查找,时间复杂度是 O(NlogN),我们通过一种简单的纸牌游戏来辅助理解这种巧妙的解法。 先看一下题目,很容易理解: ![title](../pictures/%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97/title.png) -注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。下面先来一步一步设计动态规划算法解决这个问题。 +注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。下面先来设计动态规划算法解决这个问题。 ### 一、动态规划解法 动态规划的核心设计思想是数学归纳法。 -相信大家对数学归纳法都不陌生,高中就学过,而且思路很简单。比如我们想证明一个数学结论,那么我们先假设这个结论在 $k int: - n = len(nums) - ## dp 数组全部初始化为1 - dp = [1 for x in range(0,n)] - for i in range(0,n): - for j in range(0,i): - if nums[i] > nums[j]: - dp[i] = max(dp[i],dp[j]+1) - res = 0 - for temp in dp: - res = max(temp,res) - return res -``` -**二分查找解法** - -```python -def lengthOfLIS(self, nums: List[int]) -> int: - top = [] - ##牌堆初始化为0 - piles = 0 - for num in nums: - ## num为要处理的扑克牌 - - ##二分查找 - left, right = 0, piles - while left < right: - mid = (left + right ) // 2 - ##搜索左侧边界 - if top[mid] > num: - right = mid - ##搜索右侧边界 - elif top[mid] < num: - left = mid + 1 - else: - right = mid - if left == piles: - ##没有找到合适的堆,新建一堆 - piles += 1 - ##把这张牌放到牌堆顶 - top[left] = num - return piles - ##牌堆数就是LIS的长度 -``` +**_____________** -[上一篇:动态规划答疑篇](../动态规划系列/最优子结构.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:编辑距离](../动态规划系列/编辑距离.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" index 99d17caaa0..0181c186df 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" @@ -1,33 +1,67 @@ # 动态规划详解 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [经典动态规划:完全背包问题](https://labuladong.gitbook.io/algo) + * [Git/SQL/正则表达式的在线练习平台](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[509.斐波那契数](https://leetcode-cn.com/problems/fibonacci-number) + +[322.零钱兑换](https://leetcode-cn.com/problems/coin-change) + +**-----------** + 这篇文章是我们号半年前一篇 200 多赞赏的成名之作「动态规划详解」的进阶版。由于账号迁移的原因,旧文无法被搜索到,所以我润色了本文,并添加了更多干货内容,希望本文成为解决动态规划的一部「指导方针」。 -再说句题外话,我们的公众号开号至今写了起码十几篇文章拆解动态规划问题,我都整理到了公众号菜单的「文章目录」中,**它们都提到了动态规划的解题框架思维,本文就系统总结一下**。这段时间本人也从非科班小白成长到刷通半个 LeetCode,所以我总结的套路可能不适合各路大神,但是应该适合大众,毕竟我自己也是一路摸爬滚打过来的。 +动态规划问题(Dynamic Programming)应该是很多读者头疼的,不过这类问题也是最具有技巧性,最有意思的。本书使用了整整一个章节专门来写这个算法,动态规划的重要性也可见一斑。 -算法技巧就那几个套路,如果你心里有数,就会轻松很多,本文就来扒一扒动态规划的裤子,形成一套解决这类问题的思维框架。废话不多说了,上干货。 +刷题刷多了就会发现,算法技巧就那几个套路,**我们后续的动态规划系列章节,都在使用本文的解题框架思维**,如果你心里有数,就会轻松很多。所以本文放在第一章,来扒一扒动态规划的裤子,形成一套解决这类问题的思维框架,希望能够成为解决动态规划问题的一部指导方针。本文就来讲解该算法的基本套路框架,下面上干货。 -**动态规划问题的一般形式就是求最值**。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求**最长**递增子序列呀,**最小**编辑距离呀等等。 +**首先,动态规划问题的一般形式就是求最值**。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求**最长**递增子序列呀,**最小**编辑距离呀等等。 既然是要求最值,核心问题是什么呢?**求解动态规划的核心问题是穷举**。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。 -动态规划就这么简单,就是穷举就完事了?我看到的动态规划问题都很难啊! +动态规划这么简单,就是穷举就完事了?我看到的动态规划问题都很难啊! 首先,动态规划的穷举有点特别,因为这类问题**存在「重叠子问题」**,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。 而且,动态规划问题一定会**具备「最优子结构」**,才能通过子问题的最值得到原问题的最值。 -另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出**正确的「状态转移方程**」才能正确地穷举。 +另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出**正确的「状态转移方程」**才能正确地穷举。 以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。具体什么意思等会会举例详解,但是在实际的算法问题中,**写出状态转移方程是最困难的**,这也就是为什么很多朋友觉得动态规划问题困难的原因,我来提供我研究出来的一个思维框架,辅助你思考状态转移方程: -明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case。 +**明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义**。 -下面通过斐波那契数列问题和凑零钱问题来详解动态规划的基本原理。前者主要是让你明白什么是重叠子问题(斐波那契数列严格来说不是动态规划问题),后者主要举集中于如何列出状态转移方程。 +按上面的套路走,最后的结果就可以套这个框架: -请读者不要嫌弃这个例子简单,**只有简单的例子才能让你把精力充分集中在算法背后的通用思想和技巧上,而不会被那些隐晦的细节问题搞的莫名其妙**。想要困难的例子,历史文章里有的是。 +```python +# 初始化 base case +dp[0][0][...] = base +# 进行状态转移 +for 状态1 in 状态1的所有取值: + for 状态2 in 状态2的所有取值: + for ... + dp[状态1][状态2][...] = 求最值(选择1,选择2...) +``` + +下面通过斐波那契数列问题和凑零钱问题来详解动态规划的基本原理。前者主要是让你明白什么是重叠子问题(斐波那契数列没有求最值,所以严格来说不是动态规划问题),后者主要举集中于如何列出状态转移方程。 ### 一、斐波那契数列 +请读者不要嫌弃这个例子简单,**只有简单的例子才能让你把精力充分集中在算法背后的通用思想和技巧上,而不会被那些隐晦的细节问题搞的莫名其妙**。想要困难的例子,历史文章里有的是。 + **1、暴力递归** 斐波那契数列的数学形式就是递归的,写成代码就是这样: @@ -39,21 +73,21 @@ int fib(int N) { } ``` -这个不用多说了,学校老师讲递归的时候似乎都是拿这个举例。我们也知道这样写代码虽然简洁易懂,但是十分低效,低效在哪里?假设 n = 20,请画出递归树。 - -PS:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。 +这个不用多说了,学校老师讲递归的时候似乎都是拿这个举例。我们也知道这样写代码虽然简洁易懂,但是十分低效,低效在哪里?假设 n = 20,请画出递归树: ![](../pictures/动态规划详解进阶/1.jpg) +PS:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。 + 这个递归树怎么理解?就是说想要计算原问题 `f(20)`,我就得先计算出子问题 `f(19)` 和 `f(18)`,然后要计算 `f(19)`,我就要先算出子问题 `f(18)` 和 `f(17)`,以此类推。最后遇到 `f(1)` 或者 `f(2)` 的时候,结果已知,就能直接返回结果,递归树不再向下生长了。 -**递归算法的时间复杂度怎么计算?子问题个数乘以解决一个子问题需要的时间。** +**递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间。** -子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。 +首先计算子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。 -解决一个子问题的时间,在本算法中,没有循环,只有 f(n - 1) + f(n - 2) 一个加法操作,时间为 O(1)。 +然后计算解决一个子问题的时间,在本算法中,没有循环,只有 `f(n - 1) + f(n - 2)` 一个加法操作,时间为 O(1)。 -所以,这个算法的时间复杂度为 O(2^n),指数级别,爆炸。 +所以,这个算法的时间复杂度为二者相乘,即 O(2^n),指数级别,爆炸。 观察递归树,很明显发现了算法低效的原因:存在大量重复计算,比如 `f(18)` 被计算了两次,而且你可以看到,以 `f(18)` 为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止 `f(18)` 这一个节点被重复计算,所以这个算法及其低效。 @@ -70,7 +104,7 @@ int fib(int N) { if (N < 1) return 0; // 备忘录全初始化为 0 vector memo(N + 1, 0); - // 初始化最简情况 + // 进行带备忘录的递归 return helper(memo, N); } @@ -79,8 +113,7 @@ int helper(vector& memo, int n) { if (n == 1 || n == 2) return 1; // 已经计算过 if (memo[n] != 0) return memo[n]; - memo[n] = helper(memo, n - 1) + - helper(memo, n - 2); + memo[n] = helper(memo, n - 1) + helper(memo, n - 2); return memo[n]; } ``` @@ -93,7 +126,7 @@ int helper(vector& memo, int n) { ![](../pictures/动态规划详解进阶/3.jpg) -递归算法的时间复杂度怎么算?子问题个数乘以解决一个子问题需要的时间。 +**递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间。** 子问题个数,即图中节点的总数,由于本算法不存在冗余计算,子问题就是 `f(1)`, `f(2)`, `f(3)` ... `f(20)`,数量和输入规模 n = 20 成正比,所以子问题个数为 O(n)。 @@ -103,7 +136,7 @@ int helper(vector& memo, int n) { 至此,带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。实际上,这种解法和迭代的动态规划已经差不多了,只不过这种方法叫做「自顶向下」,动态规划叫做「自底向上」。 -啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 `f(20)`,向下逐渐分解规模,直到 `f(1)` 和 `f(2)` 触底,然后逐层返回答案,这就叫「自顶向下」。 +啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 `f(20)`,向下逐渐分解规模,直到 `f(1)` 和 `f(2)` 这两个 base case,然后逐层返回答案,这就叫「自顶向下」。 啥叫「自底向上」?反过来,我们直接从最底下,最简单,问题规模最小的 `f(1)` 和 `f(2)` 开始往上推,直到推到我们想要的答案 `f(20)`,这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算。 @@ -130,11 +163,11 @@ int fib(int N) { ![](../pictures/动态规划详解进阶/fib.png) -为啥叫「状态转移方程」?为了听起来高端。你把 f(n) 想做一个状态 n,这个状态 n 是由状态 n - 1 和状态 n - 2 相加转移而来,这就叫状态转移,仅此而已。 +为啥叫「状态转移方程」?其实就是为了听起来高端。你把 `f(n)` 想做一个状态 `n`,这个状态 `n` 是由状态 `n - 1` 和状态 `n - 2` 相加转移而来,这就叫状态转移,仅此而已。 -你会发现,上面的几种解法中的所有操作,例如 return f(n - 1) + f(n - 2),dp[i] = dp[i - 1] + dp[i - 2],以及对备忘录或 DP table 的初始化操作,都是围绕这个方程式的不同表现形式。可见列出「状态转移方程」的重要性,它是解决问题的核心。很容易发现,其实状态转移方程直接代表着暴力解法。 +你会发现,上面的几种解法中的所有操作,例如 `return f(n - 1) + f(n - 2)`,`dp[i] = dp[i - 1] + dp[i - 2]`,以及对备忘录或 DP table 的初始化操作,都是围绕这个方程式的不同表现形式。可见列出「状态转移方程」的重要性,它是解决问题的核心。而且很容易发现,其实状态转移方程直接代表着暴力解法。 -**千万不要看不起暴力解,动态规划问题最困难的就是写出状态转移方程**,即这个暴力解。优化方法无非是用备忘录或者 DP table,再无奥妙可言。 +**千万不要看不起暴力解,动态规划问题最困难的就是写出这个暴力解,即状态转移方程**。只要写出暴力解,优化方法无非是用备忘录或者 DP table,再无奥妙可言。 这个例子的最后,讲一个细节优化。细心的读者会发现,根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1): @@ -152,7 +185,9 @@ int fib(int n) { } ``` -有人会问,动态规划的另一个重要特性「最优子结构」,怎么没有涉及?下面会涉及。斐波那契数列的例子严格来说不算动态规划,因为没有涉及求最值,以上旨在演示算法设计螺旋上升的过程。下面,看第二个例子,凑零钱问题。 +这个技巧就是所谓的「**状态压缩**」,如果我们发现每次状态转移只需要 DP table 中的一部分,那么可以尝试用状态压缩来缩小 DP table 的大小,只记录必要的数据,上述例子就相当于把DP table 的大小从 `n` 缩小到 2。后续的动态规划章节中我们还会看到这样的例子,一般来说是把一个二维的 DP table 压缩成一维,即把空间复杂度从 O(n^2) 压缩到 O(n)。 + +有人会问,动态规划的另一个重要特性「最优子结构」,怎么没有涉及?下面会涉及。斐波那契数列的例子严格来说不算动态规划,因为没有涉及求最值,以上旨在说明重叠子问题的消除方法,演示得到最优解法逐步求精的过程。下面,看第二个例子,凑零钱问题。 ### 二、凑零钱问题 @@ -165,42 +200,52 @@ int coinChange(int[] coins, int amount); 比如说 `k = 3`,面值分别为 1,2,5,总金额 `amount = 11`。那么最少需要 3 枚硬币凑出,即 11 = 5 + 5 + 1。 -你认为计算机应该如何解决这个问题?显然,就是把所有可能的凑硬币方法都穷举出来,然后找找看最少需要多少枚硬币。 +你认为计算机应该如何解决这个问题?显然,就是把所有肯能的凑硬币方法都穷举出来,然后找找看最少需要多少枚硬币。 **1、暴力递归** 首先,这个问题是动态规划问题,因为它具有「最优子结构」的。**要符合「最优子结构」,子问题间必须互相独立**。啥叫相互独立?你肯定不想看数学证明,我用一个直观的例子来讲解。 -比如说,你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。 +比如说,假设你考试,每门科目的成绩都是互相独立的。你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。 得到了正确的结果:最高的总成绩就是总分。因为这个过程符合最优子结构,“每门科目考到最高”这些子问题是互相独立,互不干扰的。 -但是,如果加一个条件:你的语文成绩和数学成绩会互相制约,此消彼长。这样的话,显然你能考到的最高总成绩就达不到总分了,按刚才那个思路就会得到错误的结果。因为子问题并不独立,语文数学成绩无法同时最优,所以最优子结构被破坏。 +但是,如果加一个条件:你的语文成绩和数学成绩会互相制约,数学分数高,语文分数就会降低,反之亦然。这样的话,显然你能考到的最高总成绩就达不到总分了,按刚才那个思路就会得到错误的结果。因为子问题并不独立,语文数学成绩无法同时最优,所以最优子结构被破坏。 -回到凑零钱问题,为什么说它符合最优子结构呢?比如你想求 `amount = 11` 时的最少硬币数(原问题),如果你知道凑出 `amount = 10` 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1 的硬币)就是原问题的答案,因为硬币的数量是没有限制的,子问题之间没有相互制,是互相独立的。 +回到凑零钱问题,为什么说它符合最优子结构呢?比如你想求 `amount = 11` 时的最少硬币数(原问题),如果你知道凑出 `amount = 10` 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1 的硬币)就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制,是互相独立的。 + +PS:关于最优子结构的问题,后文[动态规划答疑篇](https://labuladong.gitbook.io/algo) 还会再举例探讨。 那么,既然知道了这是个动态规划问题,就要思考**如何列出正确的状态转移方程**? -**先确定「状态」**,也就是原问题和子问题中变化的变量。由于硬币数量无限,所以唯一的状态就是目标金额 `amount`。 +1、**确定 base case**,这个很简单,显然目标金额 `amount` 为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。 + +2、**确定「状态」,也就是原问题和子问题中会变化的变量**。由于硬币数量无限,硬币的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的「状态」就是目标金额 `amount`。 + +3、**确定「选择」,也就是导致「状态」产生变化的行为**。目标金额为什么变化呢,因为你在选择硬币,你每选择一枚硬币,就相当于减少了目标金额。所以说所有硬币的面值,就是你的「选择」。 -**然后确定 `dp` 函数的定义**:当前的目标金额是 `n`,至少需要 `dp(n)` 个硬币凑出该金额。 +4、**明确 `dp` 函数/数组的定义**。我们这里讲的是自顶向下的解法,所以会有一个递归的 `dp` 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。就本题来说,状态只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量。所以我们可以这样定义 `dp` 函数: -**然后确定「选择」并择优**,也就是对于每个状态,可以做出什么选择改变当前状态。具体到这个问题,无论当的目标金额是多少,选择就是从面额列表 `coins` 中选择一个硬币,然后目标金额就会减少: +`dp(n)` 的定义:输入一个目标金额 `n`,返回凑出目标金额 `n` 的最少硬币数量。 + +搞清楚上面这几个关键点,解法的伪码就可以写出来了: ```python # 伪码框架 def coinChange(coins: List[int], amount: int): + # 定义:要凑出金额 n,至少要 dp(n) 个硬币 def dp(n): # 做选择,选择需要硬币最少的那个结果 for coin in coins: res = min(res, 1 + dp(n - coin)) return res - # 我们要求的问题是 dp(amount) + + # 题目要求的最终结果是 dp(amount) return dp(amount) ``` -**最后明确 base case**,显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1: +根据伪码,我们加上 base case 即可得到最终的答案。显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1: ```python def coinChange(coins: List[int], amount: int): @@ -230,13 +275,13 @@ def coinChange(coins: List[int], amount: int): ![](../pictures/动态规划详解进阶/5.jpg) -**时间复杂度分析:子问题总数 x 每个子问题的时间**。 +**递归算法的时间复杂度分析:子问题总数 x 每个子问题的时间**。 子问题总数为递归树节点个数,这个比较难看出来,是 O(n^k),总之是指数级别的。每个子问题中含有一个 for 循环,复杂度为 O(k)。所以总时间复杂度为 O(k * n^k),指数级别。 **2、带备忘录的递归** -只需要稍加修改,就可以通过备忘录消除子问题: +类似之前斐波那契数列的例子,只需要稍加修改,就可以通过备忘录消除子问题: ```python def coinChange(coins: List[int], amount: int): @@ -245,7 +290,7 @@ def coinChange(coins: List[int], amount: int): def dp(n): # 查备忘录,避免重复计算 if n in memo: return memo[n] - + # base case if n == 0: return 0 if n < 0: return -1 res = float('INF') @@ -261,13 +306,15 @@ def coinChange(coins: List[int], amount: int): return dp(amount) ``` -不画图了,很显然「备忘录」大大减小了子问题数目,完全消除了子问题的冗余,所以子问题总数不会超过金额数 n,即子问题数目为 O(n)。处理一个子问题的时间不变,仍是 O(k),所以总的时间复杂度是 O(kn)。 +不画图了,很显然「备忘录」大大减小了子问题数目,完全消除了子问题的冗余,所以子问题总数不会超过金额数 `n`,即子问题数目为 O(n)。处理一个子问题的时间不变,仍是 O(k),所以总的时间复杂度是 O(kn)。 **3、dp 数组的迭代解法** -当然,我们也可以自底向上使用 dp table 来消除重叠子问题,`dp` 数组的定义和刚才 `dp` 函数类似,定义也是一样的: +当然,我们也可以自底向上使用 dp table 来消除重叠子问题,关于「状态」「选择」和 base case 与之前没有区别,`dp` 数组的定义和刚才 `dp` 函数类似,也是把「状态」,也就是目标金额作为变量。不过 `dp` 函数体现在函数参数,而 `dp` 数组体现在数组索引: + +**`dp` 数组的定义:当目标金额为 `i` 时,至少需要 `dp[i]` 枚硬币凑出**。 -**`dp[i] = x` 表示,当目标金额为 `i` 时,至少需要 `x` 枚硬币**。 +根据我们文章开头给出的动态规划代码框架可以写出如下解法: ```cpp int coinChange(vector& coins, int amount) { @@ -275,8 +322,9 @@ int coinChange(vector& coins, int amount) { vector dp(amount + 1, amount + 1); // base case dp[0] = 0; + // 外层 for 循环在遍历所有状态的所有取值 for (int i = 0; i < dp.size(); i++) { - // 内层 for 在求所有子问题 + 1 的最小值 + // 内层 for 循环在求所有选择的最小值 for (int coin : coins) { // 子问题无解,跳过 if (i - coin < 0) continue; @@ -305,12 +353,15 @@ PS:为啥 `dp` 数组初始化为 `amount + 1` 呢,因为凑成 `amount` 金 备忘录、DP table 就是在追求“如何聪明地穷举”。用空间换时间的思路,是降低时间复杂度的不二法门,除此之外,试问,还能玩出啥花活? -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: +之后我们会有一章专门讲解动态规划问题,如果有任何问题都可以随时回来重读本文,希望读者在阅读每个题目和解法时,多往「状态」和「选择」上靠,才能对这套框架产生自己的理解,运用自如。 + -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:学习数据结构和算法读什么书](../算法思维系列/为什么推荐算法4.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:动态规划答疑篇](../动态规划系列/最优子结构.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" index 6bd0de9774..b00b96cd50 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" @@ -1,11 +1,39 @@ # 团灭 LeetCode 股票买卖问题 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [动态规划之KMP字符匹配算法](https://labuladong.gitbook.io/algo) + * [如何判断回文链表](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/) + +[买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/) + +[买卖股票的最佳时机 III](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/) + +[买卖股票的最佳时机 IV](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/) + +[最佳买卖股票时机含冷冻期](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/) + +[买卖股票的最佳时机含手续费](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) + +**-----------** + 很多读者抱怨 LeetCode 的股票系列问题奇技淫巧太多,如果面试真的遇到这类问题,基本不会想到那些巧妙的办法,怎么办?**所以本文拒绝奇技淫巧,而是稳扎稳打,只用一种通用方法解决所用问题,以不变应万变**。 这篇文章用状态机的技巧来解决,可以全部提交通过。不要觉得这个名词高大上,文学词汇而已,实际上就是 DP table,看一眼就明白了。 -PS:本文参考自[英文版 LeetCode 的一篇题解](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/discuss/39038)。 - 先随便抽出一道题,看看别人的解法: ```cpp @@ -388,243 +416,14 @@ int maxProfit_k_any(int max_k, int[] prices) { 具体到股票买卖问题,我们发现了三个状态,使用了一个三维数组,无非还是穷举 + 更新,不过我们可以说的高大上一点,这叫「三维 DP」,怕不怕?这个大实话一说,立刻显得你高人一等,名利双收有没有,所以给个在看/分享吧,鼓励一下我。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) -[Hanmin](https://github.com/Miraclemin/) 提供 Python3 代码: - -**第一题,k = 1** - -```python -def maxProfit(self, prices: List[int]) -> int: - dp_i_0,dp_i_1 = 0,float('-inf') - for price in prices: - dp_i_0 = max(dp_i_0, dp_i_1 + price) - dp_i_1 = max(dp_i_1, -price) - return dp_i_0 -``` -**第二题,k = +infinity** - -```python -def maxProfit_k_inf(self, prices: List[int]) -> int: - dp_i_0,dp_i_1 = 0,float('-inf') - for price in prices: - temp = dp_i_0 - dp_i_0 = max(dp_i_0, dp_i_1 + price) - dp_i_1 = max(dp_i_1, temp - price) - return dp_i_0 -``` -**第三题,k = +infinity with cooldown** - -```python -def maxProfit_with_cool(self, prices: List[int]) -> int: - dp_i_0,dp_i_1 = 0,float('-inf') - dp_pre_0 = 0 ##代表 dp[i-2][0] - for price in prices: - temp = dp_i_0 - dp_i_0 = max(dp_i_0, dp_i_1 + price) - dp_i_1 = max(dp_i_1, dp_pre_0 - price) - dp_pre_0 = temp - return dp_i_0 -``` -**第四题,k = +infinity with fee** -```python -def maxProfit_with_fee(self, prices: List[int], fee: int) -> int: - dp_i_0,dp_i_1 = 0,float('-inf') - for price in prices: - temp = dp_i_0 - dp_i_0 = max(dp_i_0, dp_i_1 + price) - dp_i_1 = max(dp_i_1, temp - price -fee) - return dp_i_0 -``` -**第五题,k = 2** - -```python -def maxProfit_k_2(self, prices: List[int]) -> int: - dp_i10,dp_i11 = 0,float('-inf') - dp_i20,dp_i21 = 0,float('-inf') - for price in prices: - dp_i20 = max(dp_i20, dp_i21 + price) - dp_i21 = max(dp_i21, dp_i10 - price) - dp_i10 = max(dp_i10, dp_i11 + price) - dp_i11 = max(dp_i11, -price) - return dp_i20 -``` -**第六题,k = any integer** - -```python -def maxProfit_k_any(self, max_k: int, prices: List[int]) -> int: - n = len(prices) - if max_k > n // 2: - return self.maxProfit_k_inf(prices) - else: - dp = [[[None, None] for _ in range(max_k + 1)] for _ in range(n)] - for i in range(0,n): - for k in range(max_k,0,-1): - if i-1 == -1:## 处理 base case - dp[i][k][0] = 0 - ## 解释: - ## dp[i][k][0] = max(dp[-1][k][0], dp[-1][k][1] + prices[i]) - ## = max(0, -infinity + prices[i]) = 0 - dp[i][k][1] = -prices[i] - ## 解释: - ## dp[i][1] = max(dp[-1][k][1], dp[-1][k][0] - prices[i]) - ## = max(-infinity, 0 - prices[i]) = -prices[i] - continue - dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) - dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) - return dp[n - 1][max_k][0]; -``` -[z2z23n0](https://github.com/YuzeZhang/) 提供 C++ 代码: - -**第一题,k = 1** - -```c++ -int maxProfit(vector& prices) { - int n = prices.size(); - // base case - int dp_i_0 = 0, dp_i_1 = INT_MIN; - // dp_i_0: 第i天的最大利润, 0表示不持有股票 - // dp_i_1: 第i天的最大利润, 1表示持有股票 - - for (int i = 1; i <= n; i++) { - // 如果今天不持有股票,表示昨天也不持有股票或是今天卖掉了股票 - dp_i_0 = max(dp_i_0, dp_i_1 + prices[i - 1]); - // 如果今天持有股票,表示昨天就持有股票或是今天买了股票 - dp_i_1 = max(dp_i_1, -prices[i - 1]); - } - return dp_i_0; -} -``` - -**第二题,k = +infinity** - -```c++ -int maxProfit(vector& prices) { - int n = prices.size(); - // base case - int dp_i_0 = 0, dp_i_1 = INT_MIN; - // dp_i_0: 第i天的最大利润, 0表示不持有股票 - // dp_i_1: 第i天的最大利润, 1表示持有股票 - - for (int i = 1; i <= n; i++) { - int temp = dp_i_0; - // 如果今天不持有股票,表示昨天也不持有股票或是今天卖掉了股票 - dp_i_0 = std::max(dp_i_0, dp_i_1 + prices[i - 1]); - // 如果今天持有股票,表示昨天就持有股票或是今天买了股票 - dp_i_1 = std::max(dp_i_1, temp - prices[i - 1]); - } - return dp_i_0; -} -``` - -**第三题,k = +infinity with cooldown** - -```c++ -int maxProfit(vector& prices) { - int n = prices.size(); - // base case - // dp_i_0: 第i天的最大利润, 0表示不持有股票 - // dp_i_1: 第i天的最大利润, 1表示持有股票 - int dp_i_0 = 0, dp_i_1 = INT_MIN; - // 表示第(i-2)天的最大利润,并且未持有股票 - int prev_dp_i_0 = 0; - for (int i = 1; i <= n; i++) { - // temp和prev_dp_i_0用来记录第(i-2)天(前天)的最大利润 - int temp = dp_i_0; - // 如果今天不持有股票,表示昨天也不持有股票或是今天卖掉了股票 - dp_i_0 = std::max(dp_i_0, dp_i_1 + prices[i - 1]); - // 如果今天持有股票,表示昨天就持有股票或是今天买了股票 - dp_i_1 = std::max(dp_i_1, prev_dp_i_0 - prices[i - 1]); - prev_dp_i_0 = temp; - } - return dp_i_0; -} -``` - -**第四题,k = +infinity with fee** - -```c++ -int maxProfit(vector& prices, int fee) { - int n = prices.size(); - // base case - int dp_i_0 = 0, dp_i_1 = INT_MIN; - // dp_i_0: 第i天的最大利润, 0表示不持有股票 - // dp_i_1: 第i天的最大利润, 1表示持有股票 - - for (int i = 1; i <= n; i++) { - int temp = dp_i_0; - // 如果今天不持有股票,表示昨天也不持有股票或是今天卖掉了股票 - dp_i_0 = std::max(dp_i_0, dp_i_1 + prices[i - 1]); - // 如果今天持有股票,表示昨天就持有股票或是今天买了股票 - //tips:为什么不在卖股票的时候减掉transaction fee?因为在base case中, dp_i_1 == INT_MIN, INT_MIN + prices[i] - fee 可能会造成整型溢出 - dp_i_1 = std::max(dp_i_1, temp - prices[i - 1] - fee); - } - return dp_i_0; -} -``` -**第五题,k = 2** - -```c++ -int maxProfit(vector& prices) { - int size = prices.size(); - int max_k = 2; - - // i从1开始而不是0,这样可以使base case是 dp[0][0][0] 和 dp[0][0][1] 而不是 dp[-1][0][0] 或者 dp[-1][0] - int dp[size + 1][max_k + 1][2]; - - // 初始化 - for (int k = max_k; k >= 0; k--) { - dp[0][k][0] = 0; - dp[0][k][1] = INT_MIN; - } - - for (int i = 1; i <= size; i++) { - dp[i][0][0] = 0; - dp[i][0][1] = INT_MIN; - for (int k = max_k; k >= 1; k--) { - dp[i][k][0] = std::max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i - 1]); - dp[i][k][1] = std::max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i - 1]); - } - } - return dp[size][max_k][0]; -} -``` - -**第六题,k = any integer** - -```c++ -int maxProfit(int k, vector &prices) { - int size = prices.size(); - - if (k > size / 2) { - return maxProfitLimitless(prices); - } - // i从1开始而不是0,这样可以使base case是 dp[0][0][0] 和 dp[0][0][1] 而不是 dp[-1][0][0] 或者 dp[-1][0][1] - int dp[size + 1][k + 1][2]; - - // 初始化 - for (int j = k; j >= 0; j--) { - dp[0][j][0] = 0; - dp[0][j][1] = INT_MIN; - } - - for (int i = 1; i <= size; i++) { - dp[i][0][0] = 0; - dp[i][0][1] = INT_MIN; - for (int j = k; j >= 1; j--) { - dp[i][j][0] = std::max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i - 1]); - dp[i][j][1] = std::max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1]); - } - } - return dp[size][k][0]; -} -``` +**_____________** -[上一篇:动态规划之KMP字符匹配算法](../动态规划系列/动态规划之KMP字符匹配算法.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:团灭 LeetCode 打家劫舍问题](../动态规划系列/抢房子.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" index 9e837d3c4e..60d836f0ea 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" @@ -1,5 +1,26 @@ # 动态规划之子序列问题解题模板 +**学好算法全靠套路,认准 labuladong 就够了**。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [洗牌算法](https://labuladong.gitbook.io/algo) + * [twoSum问题的核心思想](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[516.最长回文子序列](https://leetcode-cn.com/problems/longest-palindromic-subsequence) + +**-----------** + 子序列问题是常见的算法问题,而且并不好解决。 首先,子序列问题本身就相对子串、子数组更困难一些,因为前者是不连续的序列,而后两者是连续的,就算穷举你都不一定会,更别说求解相关的算法问题了。 @@ -37,7 +58,7 @@ for (int i = 1; i < n; i++) { ```java int n = arr.length; -int[][] dp = new int[n][n]; +int[][] dp = new dp[n][n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { @@ -67,7 +88,7 @@ for (int i = 0; i < n; i++) { 之前解决了「最长回文子串」的问题,这次提升难度,求最长回文子序列的长度: -![](../pictures/最长回文子序列/1.jpg) +![](../pictures/最长回文子序列/title.jpg) 我们说这个问题对 dp 数组的定义是:**在子串 `s[i..j]` 中,最长回文子序列的长度为 `dp[i][j]`**。一定要记住这个定义才能理解算法。 @@ -141,12 +162,14 @@ int longestPalindromeSubseq(string s) { 至此,最长回文子序列的问题就解决了。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[上一篇:经典动态规划问题:高楼扔鸡蛋(进阶)](../动态规划系列/高楼扔鸡蛋进阶.md) +**_____________** + +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:动态规划之博弈问题](../动态规划系列/动态规划之博弈问题.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,labuladong 带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" index e931b4e524..8dae329bc6 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" @@ -1,5 +1,30 @@ # 团灭 LeetCode 打家劫舍问题 +**学好算法全靠套路,认准 labuladong 就够了**。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [动态规划之四键键盘](https://labuladong.gitbook.io/algo) + * [经典动态规划:子集背包问题](/https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[198.打家劫舍](https://leetcode-cn.com/problems/house-robber) + +[213.打家劫舍II](https://leetcode-cn.com/problems/house-robber-ii) + +[337.打家劫舍III](https://leetcode-cn.com/problems/house-robber-iii) + +**-----------** + 有读者私下问我 LeetCode 「打家劫舍」系列问题(英文版叫 House Robber)怎么做,我发现这一系列题目的点赞非常之高,是比较有代表性和技巧性的动态规划题目,今天就来聊聊这道题目。 打家劫舍系列总共有三道,难度设计非常合理,层层递进。第一道是比较标准的动态规划问题,而第二道融入了环形数组的条件,第三道更绝,把动态规划的自底向上和自顶向下解法和二叉树结合起来,我认为很有启发性。如果没做过的朋友,建议学习一下。 @@ -225,127 +250,12 @@ int[] dp(TreeNode root) { 实际上,这个解法比我们的解法运行时间要快得多,虽然算法分析层面时间复杂度是相同的。原因在于此解法没有使用额外的备忘录,减少了数据操作的复杂性,所以实际运行效率会快。 +**_____________** -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - -[5ooo](https://github.com/5ooo) 提供 House Robber I C++ 解法代码: - -```c++ -class Solution { -public: - int rob(vector& nums) { - int dp_i = 0; //第i间房子最多能抢的钱 - int dp_i_1 = 0; //第i+1间房子最多能抢的钱 - int dp_i_2 = 0; //第i+2间房子最多能抢的钱 - - //从最后一间房子开始,往前移动 - for (int i = nums.size() - 1; i >= 0; i--) { - dp_i = max(dp_i_1, nums[i] + dp_i_2); - dp_i_2 = dp_i_1; - dp_i_1 = dp_i; - } - - return dp_i; - } -}; -``` - -[5ooo](https://github.com/5ooo) 提供 House Robber II C++ 解法代码: - -```c++ -class Solution { -public: - int rob(vector& nums) { - if (nums.size() == 1) - return nums[0]; - - return max(robRange(nums, 0, nums.size() - 2), - robRange(nums, 1, nums.size() - 1)); - } - - int robRange(vector& nums, int start, int end) { - int dp_i = 0; //第i间房子最多能抢的钱 - int dp_i_1 = 0; //第i+1间房子最多能抢的钱 - int dp_i_2 = 0; //第i+2间房子最多能抢的钱 - - for (int i = end; i >= start; i--) { - dp_i = max(dp_i_1, nums[i] + dp_i_2); - dp_i_2 = dp_i_1; - dp_i_1 = dp_i; - } - - return dp_i; - } -}; -``` - -[5ooo](https://github.com/5ooo) 提供 House Robber III C++ 解法代码: - -```c++ -class Solution { -public: - int rob(TreeNode* root) { - if (root == nullptr) - return 0; - - // 利用备忘录消除重叠子问题 - if (memo.find(root) != memo.end()) - return memo[root]; - - // 抢,然后去下下家 - int do_it = root->val + - (root->left == nullptr ? - 0 : rob(root->left->left) + rob(root->left->right)) + - (root->right == nullptr ? - 0 : rob(root->right->left) + rob(root->right->right)); - - // 不抢,然后去下家 - int not_do = rob(root->left) + rob(root->right); - - int ret = max(do_it, not_do); - memo[root] = ret; - - return ret; - } -private: - unordered_map memo; -}; -``` - -```c++ -class Solution { -public: - int rob(TreeNode* root) { - //ret[0]表示不抢root,获取的最大钱数 - //ret[1]表示抢root,获取的最大钱数 - vector ret = dp(root); - return max(ret[0], ret[1]); - } - - - vector dp(TreeNode* root) { - if (root == nullptr) - return {0, 0}; - - vector left = dp(root->left); - vector right = dp(root->right); - - //抢当前的,则接下来不能抢 - int rob = root->val + left[0] + right[0]; - - //不抢当前的,接下来可抢可不抢,取收益大的 - int not_rob = max(left[0], left[1]) + max(right[0], right[1]); - - return {not_rob, rob}; - } -}; -``` - -[上一篇:团灭 LeetCode 股票买卖问题](../动态规划系列/团灭股票问题.md) - -[下一篇:动态规划之四键键盘](../动态规划系列/动态规划之四键键盘.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[目录](../README.md#目录) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" index d544a4b16d..8ca9dc1a42 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" @@ -1,5 +1,22 @@ # 动态规划答疑篇 +**学好算法全靠套路,认准 labuladong 就够了**。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [搜索引擎背后的经典数据结构和算法](https://labuladong.gitbook.io/algo) + * [动态规划之四键键盘](https://labuladong.gitbook.io/algo) + +**-----------** + 这篇文章就给你讲明白两个问题: 1、到底什么才叫「最优子结构」,和动态规划什么关系。 @@ -18,7 +35,7 @@ 再举个例子:假设你们学校有 10 个班,你已知每个班的最大分数差(最高分和最低分的差值)。那么现在我让你计算全校学生中的最大分数差,你会不会算?可以想办法算,但是肯定不能通过已知的这 10 个班的最大分数差推到出来。因为这 10 个班的最大分数差不一定就包含全校学生的最大分数差,比如全校的最大分数差可能是 3 班的最高分和 6 班的最低分之差。 -这次我给你提出的问题就**不符合最优子结构**,因为你没办通过每个班的最优值推出全校的最优值,没办法通过子问题的最优值推出规模更大的问题的最优值。前文「动态规划详解」说过,想满足最优子结构,子问题之间必须互相独立。全校的最大分数差可能出现在两个班之间,显然子问题不独立,所以这个问题本身不符合最优子结构。 +这次我给你提出的问题就**不符合最优子结构**,因为你没办通过每个班的最优值推出全校的最优值,没办法通过子问题的最优值推出规模更大的问题的最优值。前文「动态规划详解」说过,想满足最优子结,子问题之间必须互相独立。全校的最大分数差可能出现在两个班之间,显然子问题不独立,所以这个问题本身不符合最优子结构。 **那么遇到这种最优子结构失效情况,怎么办?策略是:改造问题**。对于最大分数差这个问题,我们不是没办法利用已知的每个班的分数差吗,那我只能这样写一段暴力代码: @@ -37,7 +54,7 @@ return result; 当然,上面这个例子太简单了,不过请读者回顾一下,我们做动态规划问题,是不是一直在求各种最值,本质跟我们举的例子没啥区别,无非需要处理一下重叠子问题。 -前文「不同定义不同解法」和「高楼扔鸡蛋进阶」就展示了如何改造问题,不同的最优子结构,可能导致不同的解法和效率。 +前文不[同定义不同解法](../动态规划系列/动态规划之四键键盘.md) 和 [高楼扔鸡蛋进阶](../动态规划系列/高楼扔鸡蛋问题.md) 就展示了如何改造问题,不同的最优子结构,可能导致不同的解法和效率。 再举个常见但也十分简单的例子,求一棵二叉树的最大值,不难吧(简单起见,假设节点中的值都是非负数): @@ -100,7 +117,7 @@ for (int l = 2; l <= n; l++) { **2、遍历的终点必须是存储结果的那个位置**。 -下面来具体解释上面两个原则是什么意思。 +下面来距离解释上面两个原则是什么意思。 比如编辑距离这个经典的问题,详解见前文「编辑距离详解」,我们通过对 `dp` 数组的定义,确定了 base case 是 `dp[..][0]` 和 `dp[0][..]`,最终答案是 `dp[m][n]`;而且我们通过状态转移方程知道 `dp[i][j]` 需要从 `dp[i-1][j]`, `dp[i][j-1]`, `dp[i-1][j-1]` 转移而来,如下图: @@ -129,12 +146,13 @@ for (int i = 1; i < m; i++) 现在,你应该理解了这两个原则,主要就是看 base case 和最终结果的存储位置,保证遍历过程中使用的数据都是计算完毕的就行,有时候确实存在多种方法可以得到正确答案,可根据个人口味自行选择。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:动态规划解题框架](../动态规划系列/动态规划详解进阶.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:回溯算法解题框架](../算法思维系列/回溯算法详解修订版.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" index 86b697610e..d158110d30 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" @@ -1,5 +1,25 @@ # 最长公共子序列 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [回溯算法解题套路框架](https://labuladong.gitbook.io/algo) + * [经典动态规划:高楼扔鸡蛋(进阶)](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[1143.最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence) + +**-----------** + 最长公共子序列(Longest Common Subsequence,简称 LCS)是一道非常经典的面试题目,因为它的解法是典型的二维动态规划,大部分比较困难的字符串问题都和这个问题一个套路,比如说编辑距离。而且,这个算法稍加改造就可以用于解决其他问题,所以说 LCS 算法是值得掌握的。 题目就是让我们求两个字符串的 LCS 长度: @@ -117,83 +137,12 @@ else: -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - -[labuladong](https://github.com/labuladong) 提供Python解法代码: - -```python -def longestCommonSubsequence(str1, str2) -> int: - m, n = len(str1), len(str2) - # 构建 DP table 和 base case - dp = [[0] * (n + 1) for _ in range(m + 1)] - # 进行状态转移 - for i in range(1, m + 1): - for j in range(1, n + 1): - if str1[i - 1] == str2[j - 1]: - # 找到一个 lcs 中的字符 - dp[i][j] = 1 + dp[i-1][j-1] - else: - dp[i][j] = max(dp[i-1][j], dp[i][j-1]) - - return dp[-1][-1] -``` - -[Jinglun Zhou](https://github.com/Jasper-Joe) 提供C++解法代码: - -```CPP -class Solution { -public: - int longestCommonSubsequence(string str1, string str2) { - int m=str1.size(), n=str2.size(); - // 构建DP table 和 base case - vector> dp(m+1,vector(n+1)); - // dp[i][j]表示: 字符串str1[0:i]和字符串str2[0:j]的最大公共子序列 - // 进行状态转移 - for(int i=1;i<=m;i++) - for(int j=1;j<=n;j++) - if(str1[i-1]==str2[j-1]) // 两个字符相等,必然可以构成子问题的最优解 - // 找到一个lcs中的字符 - dp[i][j]=1+dp[i-1][j-1]; - else //如果两个字符不相等,我们往前看一个字符 - //寄希望于str1[i-2]和str2[j-1]相等,或者str1[i-1]和str2[j-2] - //如果他们两个当中有任何一个可以匹配, 我们就有机会更新当前dp值 - dp[i][j]=max(dp[i-1][j],dp[i][j-1]); - return dp[m][n]; // 根据dp的定义,答案就存储在dp[m][n]中 - } -}; -``` - -[weijiew](https://github.com/weijiew) 提供Java解法代码: - -```java -class Solution { - public int longestCommonSubsequence(String text1, String text2) { - int m = text1.length(), n = text2.length(); - // 构建 DP table 和 base case - // dp[i][j] 表示: 字符串 str1[0:i] 和字符串 str2[0:j] 的最大公共子序列 - int[][] dp = new int[m+1][n+1]; - // 进行状态转移 - for(int i = 1; i <= m; i++){ - for(int j = 1; j <= n; j++){ - if(text1.charAt(i-1) == text2.charAt(j-1)){ // 若两个字符相等,必然可以构成子问题的最优解 - // 这个字符存在于 lcs 之中 - dp[i][j] = dp[i-1][j-1] + 1; - }else{ - // 此时 text1[i] != text2[j] 则表示至少有一个不在 lcs 中(要么 text1[i] 不在,要么 text2[j]不在,或者都不在)。 - // 所以当前结果就相当于之前结果的中最大的那一个 - dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]); - } - } - } - return dp[m][n]; - } -} -``` +**_____________** -[上一篇:动态规划之正则表达](../动态规划系列/动态规划之正则表达.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:学习算法和刷题的思路指南](../算法思维系列/学习数据结构和算法的高效方法.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" index eb1eb30e1d..27b8c223aa 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" @@ -1,5 +1,25 @@ # 编辑距离 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [labuladong优质作者扶持计划](https://labuladong.gitbook.io/algo) + * [动态规划设计:最大子数组](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[72.编辑距离](https://leetcode-cn.com/problems/edit-distance) + +**-----------** + 前几天看了一份鹅场的面试题,算法部分大半是动态规划,最后一题就是写一个计算编辑距离的函数,今天就专门写一篇文章来探讨一下这个问题。 我个人很喜欢编辑距离这个问题,因为它看起来十分困难,解法却出奇得简单漂亮,而且它是少有的比较实用的算法(是的,我承认很多算法问题都不太实用)。下面先来看下题目: @@ -23,6 +43,7 @@ 设两个字符串分别为 "rad" 和 "apple",为了把 `s1` 变成 `s2`,算法会这样进行: ![](../pictures/editDistance/edit.gif) + ![](../pictures/editDistance/1.jpg) 请记住这个 GIF 过程,这样就能算出编辑距离。关键在于如何做出正确的操作,稍后会讲。 @@ -222,13 +243,13 @@ int min(int a, int b, int c) { } ``` -### 四、扩展延伸 +### 三、扩展延伸 一般来说,处理两个字符串的动态规划问题,都是按本文的思路处理,建立 DP table。为什么呢,因为易于找出状态转移的关系,比如编辑距离的 DP table: ![](../pictures/editDistance/4.jpg) -还有一个细节,既然每个 `dp[i][j]` 只和它附近的三个状态有关,空间复杂度是可以压缩成 $O(min(M, N))$ 的(M,N 是两个字符串的长度)。不难,但是可解释性大大降低,读者可以自己尝试优化一下。 +还有一个细节,既然每个 `dp[i][j]` 只和它附近的三个状态有关,空间复杂度是可以压缩成 `O(min(M, N))` 的(M,N 是两个字符串的长度)。不难,但是可解释性大大降低,读者可以自己尝试优化一下。 你可能还会问,**这里只求出了最小的编辑距离,那具体的操作是什么**?你之前举的修改公众号文章的例子,只有一个最小编辑距离肯定不够,还得知道具体怎么修改才行。 @@ -258,103 +279,14 @@ class Node { ![](../pictures/editDistance/6.jpg) -以上就是编辑距离算法的全部内容,如果本文对你有帮助,**欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚**~ - -![labuladong](../pictures/labuladong.png) -[labuladong](https://github.com/labuladong) 提供Java解法代码: -```JAVA -int minDistance(String s1, String s2) { - int m = s1.length(), n = s2.length(); - int[][] dp = new int[m + 1][n + 1]; - // base case - for (int i = 1; i <= m; i++) - dp[i][0] = i; - for (int j = 1; j <= n; j++) - dp[0][j] = j; - // 自底向上求解 - for (int i = 1; i <= m; i++) - for (int j = 1; j <= n; j++) - if (s1.charAt(i-1) == s2.charAt(j-1)) - dp[i][j] = dp[i - 1][j - 1]; - else - dp[i][j] = min( - dp[i - 1][j] + 1, - dp[i][j - 1] + 1, - dp[i-1][j-1] + 1 - ); - // 储存着整个 s1 和 s2 的最小编辑距离 - return dp[m][n]; -} - -int min(int a, int b, int c) { - return Math.min(a, Math.min(b, c)); -} -``` - -[Jinglun Zhou](https://github.com/Jasper-Joe) 提供C++解法代码: - -```CPP - -class Solution { -public: - int minDistance(string s1, string s2) { - int m=s1.size(), n=s2.size(); - vector> dp(m+1,vector(n+1)); - for(int i=1;i<=m;i++) - dp[i][0]=i; // base case: 当s2为空,s1需要删除所有字符才能与s2相等 - for(int j=1;j<=n;j++) - dp[0][j]=j; // base case: 当s1为空, s1需要不断插入新字符才能与s2相等 - //自底向上求解 - for(int i=1;i<=m;i++) - for(int j=1;j<=n;j++) - if(s1[i-1]==s2[j-1]) // 两个字符串当前的字符一样 - dp[i][j]=dp[i-1][j-1]; - else // 两个字符串当前的字符不同 - //使得s1[0:i]和s2[0:j]相同的最短编辑距离可通过插入,删除或替换三种操作其中一种得到 - dp[i][j]=min({ - dp[i-1][j]+1, // 删除s1[i]这个字符 - dp[i][j-1]+1, // 在s1[i]后面加一个和s2[j]相同的字符 - dp[i-1][j-1]+1}); // 将s1[i]的字符替换为s2[j]的字符 - //储存着整个 s1 和 s2 的最小编辑距离 - return dp[m][n]; - } -}; -``` - -[Hanmin](https://github.com/Miraclemin/) 提供 Python3 代码: - -```python -def minDistance(self, word1: str, word2: str) -> int: - m, n= len(word1), len(word2) - dp = [[0 for i in range(0,n+1)] for j in range(0,m+1)] - for i in range(1,m+1): - dp[i][0] = i ##base case:当s2为空,s1需要删除所有的字符 - for j in range(1,n+1): - dp[0][j] = j ##base case:当s1为空,需要插入所有s2的字符 - for i in range(1,m+1): - for j in range(1,n+1): - if word1[i-1] == word2[j-1]: ##当前字符一样 - dp[i][j] = dp[i-1][j-1] - else: - dp[i][j] = min( - dp[i-1][j]+1, - ##删除s1字符操作,可以理解为我直接把 s1[i] - ##这个字符删掉,前移 i,继续跟 j 对比,操作数加一 - dp[i][j-1]+1, - ##增加s1字符操作,可以理解为我直接在s1[i]插入一个和s2[j]一样的字符 - ##s2[j]被匹配,那么前移 j,继续跟 i 对比,操作数加一 - dp[i-1][j-1]+1 - ##修改s1字符操作,可以理解为我直接替换s1[i]为s2[j]一样的字符 - ##s2[j]被匹配,那么前移 i,j,操作数加一 - ) - - return dp[m][n] ##返回s1,s2最小的编辑距离 -``` +**_____________** -[上一篇:动态规划设计:最长递增子序列](../动态规划系列/动态规划设计:最长递增子序列.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:经典动态规划问题:高楼扔鸡蛋](../动态规划系列/高楼扔鸡蛋问题.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" index 75157d83e0..1ae5516b00 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" @@ -1,5 +1,27 @@ # 贪心算法之区间调度问题 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [如何判定括号合法性](https://labuladong.gitbook.io/algo) + * [一文解决三道区间问题](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[435. 无重叠区间](https://leetcode-cn.com/problems/non-overlapping-intervals/) + +[452.用最少数量的箭引爆气球](https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons) + +**-----------** + 什么是贪心算法呢?贪心算法可以认为是动态规划算法的一个特例,相比动态规划,使用贪心算法需要满足更多的条件(贪心选择性质),但是效率比动态规划要高。 比如说一个算法问题使用暴力解法需要指数级时间,如果能使用动态规划消除重叠子问题,就可以降到多项式级别的时间,如果满足贪心选择性质,那么可以进一步降低时间复杂度,达到线性级别的。 @@ -118,66 +140,14 @@ int findMinArrowShots(int[][] intvs) { } ``` -这么做的原因也不难理解,因为现在边界接触也算重叠,所以 `start == x_end` 时不能更新 x。 - -如果本文对你有帮助,欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚~ - -[renxiaoyao](https://github.com/tianzhongwei) 提供C++解法代码:435题 无重叠区间 -```C++ -class Solution { -public: - int eraseOverlapIntervals(vector>& intervals) { - int n = intervals.size(); - if(n <= 1) return 0; - auto myCmp = [&](const auto& a,const auto& b) { - return a[1] < b[1]; - }; - sort(intervals.begin(),intervals.end(),myCmp); - int cnt = 1; - int end = intervals[0][1]; // 区间动态历史最小值 - for(const auto interval : intervals) { - int start = interval[0]; - if(start >= end) { - cnt++; - end = interval[1]; - } - } - return n - cnt; - } -}; -``` - -[renxiaoyao](https://github.com/tianzhongwei) 提供C++解法代码:312 题 戳气球 -``` -class Solution { -public: - int findMinArrowShots(vector>& points) { - int n = points.size(); - if(n < 2) return n; - - auto myCmp = [&](const auto& a,const auto& b) { - return a[1] < b[1]; - }; - sort(points.begin(),points.end(),myCmp); - - int cnt = 1; - int end = points[0][1]; - for(const auto& point : points) { - int start = point[0]; - if(start > end) { // 若当前区间的起点在当前历史最右边界的后面 - cnt++; // 则非重叠区间个数累加一 - end = point[1]; // 更新当前历史最优边界 - } - } - return cnt; // 返回非重叠区间的个数 - } -}; -``` +**_____________** -[上一篇:动态规划之博弈问题](../动态规划系列/动态规划之博弈问题.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:动态规划之KMP字符匹配算法](../动态规划系列/动态规划之KMP字符匹配算法.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\350\277\233\351\230\266.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\350\277\233\351\230\266.md" index 50ff9efecc..3211f7cf71 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\350\277\233\351\230\266.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\350\277\233\351\230\266.md" @@ -1,5 +1,25 @@ # 经典动态规划问题:高楼扔鸡蛋(进阶) + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [手把手带你刷二叉树(第二期)](https://labuladong.gitbook.io/algo) + * [状态压缩:对动态规划进行降维打击](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[887.鸡蛋掉落](https://leetcode-cn.com/problems/super-egg-drop/) + +**-----------** + 上篇文章聊了高楼扔鸡蛋问题,讲了一种效率不是很高,但是较为容易理解的动态规划解法。后台很多读者问如何更高效地解决这个问题,今天就谈两种思路,来优化一下这个问题,分别是二分查找优化和重新定义状态转移。 如果还不知道高楼扔鸡蛋问题的读者可以看下「经典动态规划:高楼扔鸡蛋」,那篇文章详解了题目的含义和基本的动态规划解题思路,请确保理解前文,因为今天的优化都是基于这个基本解法的。 @@ -39,7 +59,9 @@ def dp(K, N): 这个 for 循环就是下面这个状态转移方程的具体代码实现: -$$ dp(K, N) = \min_{0 <= i <= N}\{\max\{dp(K - 1, i - 1), dp(K, N - i)\} + 1\}$$ + + +![](../pic/../pictures/扔鸡蛋/formula1.png) 如果能够理解这个状态转移方程,那么就很容易理解二分查找的优化思路。 @@ -263,13 +285,12 @@ while (lo < hi) { 本文终,希望对你有一点启发。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:经典动态规划问题:高楼扔鸡蛋](../动态规划系列/高楼扔鸡蛋问题.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:动态规划之子序列问题解题模板](../动态规划系列/子序列问题模板.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" index 9bcb6cce43..8b80075c8d 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" @@ -1,5 +1,26 @@ # 经典动态规划问题:高楼扔鸡蛋 +**学好算法全靠套路,认准 labuladong 就够了**。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [如何判断回文链表](https://labuladong.gitbook.io/algo) + * [SQL进阶技巧](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[887.鸡蛋掉落](https://leetcode-cn.com/problems/super-egg-drop/) + +**-----------** + 今天要聊一个很经典的算法问题,若干层楼,若干个鸡蛋,让你算出最少的尝试次数,找到鸡蛋恰好摔不碎的那层楼。国内大厂以及谷歌脸书面试都经常考察这道题,只不过他们觉得扔鸡蛋太浪费,改成扔杯子,扔破碗什么的。 具体的问题等会再说,但是这道题的解法技巧很多,光动态规划就好几种效率不同的思路,最后还有一种极其高效数学解法。秉承咱们号一贯的作风,拒绝奇技淫巧,拒绝过于诡异的技巧,因为这些技巧无法举一反三,学了也不划算。 @@ -169,7 +190,9 @@ def dp(K, N): 这个 for 循环就是下面这个状态转移方程的具体代码实现: -$$ dp(K, N) = \min_{0 <= i <= N}\{\max\{dp(K - 1, i - 1), dp(K, N - i)\} + 1\}$$ + + +![](../pic/../pictures/扔鸡蛋/formula1.png) 首先我们根据 `dp(K, N)` 数组的定义(有 `K` 个鸡蛋面对 `N` 层楼,最少需要扔几次),**很容易知道 `K` 固定时,这个函数一定是单调递增的**,无论你策略多聪明,楼层增加测试次数一定要增加。 @@ -220,53 +243,20 @@ def superEggDrop(self, K: int, N: int) -> int: return dp(K, N) ``` -这里就不展开其他解法了,留在下一篇文章 [高楼扔鸡蛋进阶](高楼扔鸡蛋进阶.md) +这里就不展开其他解法了,留在下一篇文章 [高楼扔鸡蛋进阶](../动态规划系列/高楼扔鸡蛋进阶.md) 我觉得吧,我们这种解法就够了:找状态,做选择,足够清晰易懂,可流程化,可举一反三。掌握这套框架学有余力的话,再去考虑那些奇技淫巧也不迟。 最后预告一下,《动态规划详解(修订版)》和《回溯算法详解(修订版)》已经动笔了,教大家用模板的力量来对抗变化无穷的算法题,敬请期待。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[renxiaoyao](https://github.com/tianzhongwei) 提供C++解法代码: -1) 状态定义一:动态规划 + 二分查找 -```C++ -class Solution { -public: - int superEggDrop(int K,int N) { - vector> dp(N + 1,vector(K + 1,0)); - for(int j = 1 ; j <= K ; ++j) - dp[1][j] = 1; - - for(int i = 1 ; i <= N ; ++i) - dp[i][1] = i; - for(int i = 2 ; i <= N ; ++i) - for(int j = 2 ; j <= K ; ++j) - dp[i][j] = binary_Valley(i,j,dp); - return dp[N][K]; - } -private: - int binary_Valley(int floors,int eggs,vector>& dp) { - int l = 1; - int r = floors; - while(l < r) { - int LMid = l + (r - l) / 2; - int broken = dp[LMid - 1][eggs - 1]; - int not_broken = dp[floors - LMid][eggs]; - if(not_broken > broken) - l = LMid + 1; - else - r = LMid; - } - return max(dp[r - 1][eggs - 1],dp[floors - r][eggs]) + 1; - } -}; -``` +**_____________** -[上一篇:编辑距离](../动态规划系列/编辑距离.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:经典动态规划问题:高楼扔鸡蛋(进阶)](../动态规划系列/高楼扔鸡蛋进阶.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\346\212\200\346\234\257/linuxshell.md" "b/\346\212\200\346\234\257/linuxshell.md" index 470cbf7c9f..2f3f3a64c6 100644 --- "a/\346\212\200\346\234\257/linuxshell.md" +++ "b/\346\212\200\346\234\257/linuxshell.md" @@ -1,3 +1,21 @@ +# 关于 Linux shell 你必须知道的技巧 + + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [状态压缩:对动态规划进行降维打击](https://labuladong.gitbook.io/algo) + * [我用四个命令概括了 Git 的所有套路](https://labuladong.gitbook.io/algo) + +**-----------** + 我个人很喜欢使用 Linux 系统,虽然说 Windows 的图形化界面做的确实比 Linux 好,但是对脚本的支持太差了。一开始有点不习惯命令行操作,但是熟悉了之后反而发现移动鼠标点点点才是浪费时间的罪魁祸首。。。 **那么对于 Linux 命令行,本文不是介绍某些命令的用法,而是说明一些简单却特别容易让人迷惑的细节问题**。 @@ -85,10 +103,10 @@ $ logout 类似的,还有一种后台运行常用的做法是这样: ```shell -$ nohup some_cmd & +$ nohub some_cmd & ``` -`nohup`命令也是类似的原理,不过通过我的测试,还是`(cmd &)`这种形式更加稳定。 +`nohub`命令也是类似的原理,不过通过我的测试,还是`(cmd &)`这种形式更加稳定。 ### 三、单引号和双引号的区别 @@ -104,7 +122,7 @@ shell 的行为可以测试,使用`set -x`命令,会开启 shell 的命令 ### 四、sudo 找不到命令 -有时候我们普通用户可以用的命令,用`sudo`加权限之后却报错 command not found: +有时候我们普通用户可以用的命令,用 `sudo` 加权限之后却报错 command not found: ```shell $ connect.sh @@ -114,28 +132,27 @@ $ sudo connect.sh sudo: command not found ``` -原因在于,`connect.sh`这个脚本仅存在于该用户的环境变量中: +原因在于,`connect.sh` 这个脚本仅存在于该用户的环境变量中: ```shell $ where connect.sh /home/fdl/bin/connect.sh ``` -**当使用`sudo`时,系统认为是 root 用户在执行命令,所以会去搜索 root 用户的环境变量**,而这个脚本在 root 的环境变量目录中当然是找不到的。 +**当使用 `sudo` 时,系统会使用 `/etc/sudoers` 这个文件中规定的该用户的权限和环境变量**,而这个脚本在 `/etc/sudoers` 环境变量目录中当然是找不到的。 -解决方法是使用脚本文件的绝对路径,而不是用脚本的相对路径: +解决方法是使用脚本文件的路径,而不是仅仅通过脚本名称: ```shell $ sudo /home/fdl/bin/connect.sh ``` -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:一文看懂 session 和 cookie](../技术/session和cookie.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:加密算法的前身今世](../技术/密码技术.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\346\212\200\346\234\257/linux\350\277\233\347\250\213.md" "b/\346\212\200\346\234\257/linux\350\277\233\347\250\213.md" index 2d6c6ba879..7b400c1d36 100644 --- "a/\346\212\200\346\234\257/linux\350\277\233\347\250\213.md" +++ "b/\346\212\200\346\234\257/linux\350\277\233\347\250\213.md" @@ -1,5 +1,21 @@ # Linux的进程、线程、文件描述符是什么 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [一文解决三道区间问题](https://labuladong.gitbook.io/algo) + * [Union-Find算法详解](https://labuladong.gitbook.io/algo) + +**-----------** + 说到进程,恐怕面试中最常见的问题就是线程和进程的关系了,那么先说一下答案:**在 Linux 系统中,进程和线程几乎没有区别**。 Linux 中的进程就是一个数据结构,看明白就可以理解文件描述符、重定向、管道命令的底层工作原理,最后我们从操作系统的角度看看为什么说线程和进程基本没有区别。 @@ -115,13 +131,12 @@ $ cmd1 | cmd2 | cmd3 在 Linux 中新建线程和进程的效率都是很高的,对于新建进程时内存区域拷贝的问题,Linux 采用了 copy-on-write 的策略优化,也就是并不真正复制父进程的内存空间,而是等到需要写操作时才去复制。**所以 Linux 中新建进程和新建线程都是很迅速的**。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:双指针技巧解题框架](../算法思维系列/双指针技巧.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:Git/SQL/正则表达式的在线练习平台](../技术/在线练习平台.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\346\212\200\346\234\257/redis\345\205\245\344\276\265.md" "b/\346\212\200\346\234\257/redis\345\205\245\344\276\265.md" index e971a88586..67cb321871 100644 --- "a/\346\212\200\346\234\257/redis\345\205\245\344\276\265.md" +++ "b/\346\212\200\346\234\257/redis\345\205\245\344\276\265.md" @@ -1,3 +1,21 @@ +# Redis 入侵 + + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [烧饼排序](https://labuladong.gitbook.io/algo) + * [动态规划之正则表达](https://labuladong.gitbook.io/algo) + +**-----------** + 好吧,我也做了回标题党,像我这么细心的同学,怎么可能让服务器被入侵呢? 其实是这样的,昨天我和一个朋友聊天,他说他自己有一台云服务器运行了 Redis 数据库,有一天突然发现数据库里的**数据全没了**,只剩下一个奇奇怪怪的键值对,其中值看起来像一个 RSA 公钥的字符串,他以为是误操作删库了,幸好自己的服务器里没啥重要的数据,也就没在意。 @@ -78,6 +96,12 @@ Redis 监听的默认端口是 6379,我们设置它接收网卡 127.0.0.1 的 3、利用 rename 功能伪装 flushall 这种危险命令,以防被删库,丢失数据。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: +**_____________** + +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 + +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -![labuladong](../pictures/labuladong.jpg) +

+ +

\ No newline at end of file diff --git "a/\346\212\200\346\234\257/session\345\222\214cookie.md" "b/\346\212\200\346\234\257/session\345\222\214cookie.md" index bc7b994e88..85b46266dc 100644 --- "a/\346\212\200\346\234\257/session\345\222\214cookie.md" +++ "b/\346\212\200\346\234\257/session\345\222\214cookie.md" @@ -1,3 +1,21 @@ +# 一文读懂 session 和 cookie + + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [如何计算完全二叉树的节点数](https://labuladong.gitbook.io/algo) + * [Linux的进程、线程、文件描述符是什么](https://labuladong.gitbook.io/algo) + +**-----------** + cookie 大家应该都熟悉,比如说登录某些网站一段时间后,就要求你重新登录;再比如有的同学很喜欢玩爬虫技术,有时候网站就是可以拦截住你的爬虫,这些都和 cookie 有关。如果你明白了服务器后端对于 cookie 和 session 的处理逻辑,就可以解释这些现象,甚至钻一些空子无限白嫖,待我慢慢道来。 ### 一、session 和 cookie 简介 @@ -127,13 +145,12 @@ https://github.com/alexedwards/scs https://github.com/astaxie/build-web-application-with-golang -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:Linux的进程、线程、文件描述符是什么](../技术/linux进程.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:关于 Linux shell 你必须知道的](../技术/linuxshell.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

\ No newline at end of file diff --git "a/\346\212\200\346\234\257/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" "b/\346\212\200\346\234\257/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" index f05434b0bd..7aaaf46295 100644 --- "a/\346\212\200\346\234\257/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" +++ "b/\346\212\200\346\234\257/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" @@ -1,3 +1,21 @@ +# 在线刷题学习平台 + + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [区间调度之区间合并问题](https://labuladong.gitbook.io/algo) + * [别再说你不懂Linux内存管理了,10张图给你安排的明明白白](https://labuladong.gitbook.io/algo) + +**-----------** + 虽说我没事就喜欢喷应试教育,但我也从应试教育中发现了一个窍门:如果能够以刷题的形式学习某项技能,效率和效果是最佳的。对于技术的学习,我经常面临的困境是,**理论知识知道的不少,但是有的场景实在无法模拟,缺少亲自动手实践的机会**,如果能有一本带标准答案的习题册让我刷刷就好了。 所以在学习新技术时,我首先会去搜索是否有在线刷题平台,你还别说,有的大神真就做了很不错的在线练习平台,下面就介绍几个平台,分别是学习 Git、SQL、正则表达式的在线练习平台。 @@ -91,13 +109,12 @@ SQLZOO 是一款很好用的 SQL 练习平台,英文不难理解,可以直 https://sqlzoo.net/ -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:Linux的进程、线程、文件描述符是什么](../技术/linux进程.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:动态规划详解](../动态规划系列/动态规划详解进阶.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

\ No newline at end of file diff --git "a/\346\212\200\346\234\257/\345\257\206\347\240\201\346\212\200\346\234\257.md" "b/\346\212\200\346\234\257/\345\257\206\347\240\201\346\212\200\346\234\257.md" index 6203408d24..8a88315300 100644 --- "a/\346\212\200\346\234\257/\345\257\206\347\240\201\346\212\200\346\234\257.md" +++ "b/\346\212\200\346\234\257/\345\257\206\347\240\201\346\212\200\346\234\257.md" @@ -1,3 +1,21 @@ +# 密码算法的前世今生 + + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [我用四个命令概括了 Git 的所有套路](https://labuladong.gitbook.io/algo) + * [一个方法团灭 nSum 问题](https://labuladong.gitbook.io/algo) + +**-----------** + 说到密码,我们第一个想到的就是登陆账户的密码,但是从密码学的角度来看,这种根本就不算合格的密码。 为什么呢,因为我们的账户密码,是依靠隐蔽性来达到加密作用:密码藏在我心里,你不知道,所以你登不上我的账户。 @@ -174,13 +192,14 @@ HTTPS 协议中的 SSL/TLS 安全层会组合使用以上几种加密方式,** 密码技术只是安全的一小部分,即便是通过正规机构认证的 HTTPS 站点,也不意味着可信任,只能说明其数据传输是安全的。技术永远不可能真正保护你,最重要的还是得提高个人的安全防范意识,多留心眼儿,谨慎处理敏感数据。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) +**_____________** +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[上一篇:关于 Linux shell 你必须知道的](../技术/linuxshell.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[下一篇:Git/SQL/正则表达式的在线练习平台](../技术/在线练习平台.md) +

+ +

-[目录](../README.md#目录) \ No newline at end of file +[test ad](https://labuladong.gitbook.io/algo) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/README.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/README.md" index 736832579e..4b64bc4743 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/README.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/README.md" @@ -2,6 +2,6 @@ 这一章主要是一些特殊的数据结构设计,比如单调栈解决 Next Greater Number,单调队列解决滑动窗口问题;还有常用数据结构的操作,比如链表、树、二叉堆。 -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: +欢迎关注我的公众号 labuladong,查看全部文章: ![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" index bb3262bc2d..99e0b060f6 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" @@ -1,5 +1,21 @@ # 二叉堆详解实现优先级队列 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [算法就像搭乐高:带你手撸 LRU 算法](https://labuladong.gitbook.io/algo) + * [拆解复杂问题:实现计算器](https://labuladong.gitbook.io/algo) + +**-----------** + 二叉堆(Binary Heap)没什么神秘,性质比二叉搜索树 BST 还简单。其主要操作就两个,`sink`(下沉)和 `swim`(上浮),用以维护二叉堆的性质。其主要应用有两个,首先是一种排序方法「堆排序」,第二是一种很有用的数据结构「优先级队列」。 本文就以实现优先级队列(Priority Queue)为例,通过图片和人类的语言来描述一下二叉堆怎么运作的。 @@ -198,7 +214,7 @@ public Key delMax() { ![5](../pictures/heap/delete.gif) -至此,一个优先级队列就实现了,插入和删除元素的时间复杂度为 $O(logK)$,$K$ 为当前二叉堆(优先级队列)中的元素总数。因为我们时间复杂度主要花费在 `sink` 或者 `swim` 上,而不管上浮还是下沉,最多也就树(堆)的高度,也就是 log 级别。 +至此,一个优先级队列就实现了,插入和删除元素的时间复杂度为 `O(logK)`,`K` 为当前二叉堆(优先级队列)中的元素总数。因为我们时间复杂度主要花费在 `sink` 或者 `swim` 上,而不管上浮还是下沉,最多也就树(堆)的高度,也就是 log 级别。 ### 五、最后总结 @@ -211,12 +227,14 @@ public Key delMax() { 也许这就是数据结构的威力,简单的操作就能实现巧妙的功能,真心佩服发明二叉堆算法的人! -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[上一篇:学习数据结构和算法读什么书](../算法思维系列/为什么推荐算法4.md) +**_____________** + +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:LRU算法详解](../高频面试系列/LRU算法.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\346\223\215\344\275\234\351\233\206\351\224\246.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\346\223\215\344\275\234\351\233\206\351\224\246.md" index 3fd21e6d74..6c056262d7 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\346\223\215\344\275\234\351\233\206\351\224\246.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\346\223\215\344\275\234\351\233\206\351\224\246.md" @@ -1,6 +1,34 @@ # 二叉搜索树操作集锦 -通过之前的文章[框架思维](../算法思维系列/学习数据结构和算法的高效方法.md),二叉树的遍历框架应该已经印到你的脑子里了,这篇文章就来实操一下,看看框架思维是怎么灵活运用,秒杀一切二叉树问题的。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [特殊数据结构:单调队列](https://labuladong.gitbook.io/algo) + * [一行代码就能解决的算法题](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[100.相同的树](https://leetcode-cn.com/problems/same-tree) + +[450.删除二叉搜索树中的节点](https://leetcode-cn.com/problems/delete-node-in-a-bst) + +[701.二叉搜索树中的插入操作](https://leetcode-cn.com/problems/insert-into-a-binary-search-tree) + +[700.二叉搜索树中的搜索](https://leetcode-cn.com/problems/search-in-a-binary-search-tree) + +[98.验证二叉搜索树](https://leetcode-cn.com/problems/validate-binary-search-tree) + +**-----------** + +通过之前的文章[框架思维](https://labuladong.gitbook.io/algo),二叉树的遍历框架应该已经印到你的脑子里了,这篇文章就来实操一下,看看框架思维是怎么灵活运用,秒杀一切二叉树问题的。 二叉树算法的设计的总路线:明确一个节点要做的事情,然后剩下的事抛给框架。 @@ -272,13 +300,12 @@ void BST(TreeNode root, int target) { -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:LRU算法详解](../高频面试系列/LRU算法.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:特殊数据结构:单调栈](../数据结构系列/单调栈.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" index e91c439012..19600ded46 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" @@ -1,4 +1,28 @@ -### 如何使用单调栈解题 +# 如何使用单调栈解题 + + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [回溯算法解题套路框架](https://labuladong.gitbook.io/algo) + * [动态规划解题套路框架](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[496.下一个更大元素I](https://leetcode-cn.com/problems/next-greater-element-i) + +[503.下一个更大元素II](https://leetcode-cn.com/problems/next-greater-element-ii) + +[1118.一月有多少天](https://leetcode-cn.com/problems/number-of-days-in-a-month) + +**-----------** 栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。 @@ -6,80 +30,104 @@ 听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一种典型的问题,叫做 Next Greater Element。本文用讲解单调队列的算法模版解决这类问题,并且探讨处理「循环数组」的策略。 -首先,讲解 Next Greater Number 的原始问题:给你一个数组,返回一个等长的数组,对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。不好用语言解释清楚,直接上一个例子: +### 单调栈模板 + +首先,看一下 Next Greater Number 的原始问题,这是力扣第 496 题「下一个更大元素 I」: + +给你一个数组,返回一个等长的数组,对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。 + +函数签名如下: + +```cpp +vector nextGreaterElement(vector& nums); +``` -给你一个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,-1]。 +比如说,输入一个数组 `nums = [2,1,2,4,3]`,你返回数组 `[4,2,4,-1,-1]`。 解释:第一个 2 后面比 2 大的数是 4; 1 后面比 1 大的数是 2;第二个 2 后面比 2 大的数是 4; 4 后面没有比 4 大的数,填 -1;3 后面没有比 3 大的数,填 -1。 -这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是 O(n^2)。 +这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是 `O(n^2)`。 这个问题可以这样抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的 Next Greater Number 呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的 Next Greater Number,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。 -![ink-image](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/1.png) +![](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/1.jpeg) 这个情景很好理解吧?带着这个抽象的情景,先来看下代码。 ```cpp vector nextGreaterElement(vector& nums) { - vector ans(nums.size()); // 存放答案的数组 + vector res(nums.size()); // 存放答案的数组 stack s; - for (int i = nums.size() - 1; i >= 0; i--) { // 倒着往栈里放 - while (!s.empty() && s.top() <= nums[i]) { // 判定个子高矮 - s.pop(); // 矮个起开,反正也被挡着了。。。 + // 倒着往栈里放 + for (int i = nums.size() - 1; i >= 0; i--) { + // 判定个子高矮 + while (!s.empty() && s.top() <= nums[i]) { + // 矮个起开,反正也被挡着了。。。 + s.pop(); } - ans[i] = s.empty() ? -1 : s.top(); // 这个元素身后的第一个高个 - s.push(nums[i]); // 进队,接受之后的身高判定吧! + // nums[i] 身后的 next great number + res[i] = s.empty() ? -1 : s.top(); + // + s.push(nums[i]); } - return ans; + return res; } ``` -这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个“高个”元素之间的元素排除,因为他们的存在没有意义,前面挡着个“更高”的元素,所以他们不可能被作为后续进来的元素的 Next Great Number 了。 +这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个「个子高」元素之间的元素排除,因为他们的存在没有意义,前面挡着个「更高」的元素,所以他们不可能被作为后续进来的元素的 Next Great Number 了。 + +这个算法的时间复杂度不是那么直观,如果你看到 for 循环嵌套 while 循环,可能认为这个算法的复杂度也是 `O(n^2)`,但是实际上这个算法的复杂度只有 `O(n)`。 -这个算法的时间复杂度不是那么直观,如果你看到 for 循环嵌套 while 循环,可能认为这个算法的复杂度也是 O(n^2),但是实际上这个算法的复杂度只有 O(n)。 +分析它的时间复杂度,要从整体来看:总共有 `n` 个元素,每个元素都被 `push` 入栈了一次,而最多会被 `pop` 一次,没有任何冗余操作。所以总的计算规模是和元素规模 `n` 成正比的,也就是 `O(n)` 的复杂度。 -分析它的时间复杂度,要从整体来看:总共有 n 个元素,每个元素都被 push 入栈了一次,而最多会被 pop 一次,没有任何冗余操作。所以总的计算规模是和元素规模 n 成正比的,也就是 O(n) 的复杂度。 +### 问题变形 -现在,你已经掌握了单调栈的使用技巧,来一个简单的变形来加深一下理解。 +单调栈的使用技巧差不多了,来一个简单的变形,力扣第 1118 题「一月有多少天」: -给你一个数组 T = [73, 74, 75, 71, 69, 72, 76, 73],这个数组存放的是近几天的天气气温(这气温是铁板烧?不是的,这里用的华氏度)。你返回一个数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0 。 +给你一个数组 `T`,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:**对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0**。 + +函数签名如下: + +```cpp +vector dailyTemperatures(vector& T); +``` -举例:给你 T = [73, 74, 75, 71, 69, 72, 76, 73],你返回 [1, 1, 4, 2, 1, 1, 0, 0]。 +比如说给你输入 `T = [73,74,75,71,69,76]`,你返回 `[1,1,3,2,1,0]`。 -解释:第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温。后面的同理。 +解释:第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温,后面的同理。 -你已经对 Next Greater Number 类型问题有些敏感了,这个问题本质上也是找 Next Greater Number,只不过现在不是问你 Next Greater Number 是多少,而是问你当前距离 Next Greater Number 的距离而已。 +这个问题本质上也是找 Next Greater Number,只不过现在不是问你 Next Greater Number 是多少,而是问你当前距离 Next Greater Number 的距离而已。 -相同类型的问题,相同的思路,直接调用单调栈的算法模板,稍作改动就可以啦,直接上代码把。 +相同的思路,直接调用单调栈的算法模板,稍作改动就可以,直接上代码吧: ```cpp vector dailyTemperatures(vector& T) { - vector ans(T.size()); - stack s; // 这里放元素索引,而不是元素 + vector res(T.size()); + // 这里放元素索引,而不是元素 + stack s; + /* 单调栈模板 */ for (int i = T.size() - 1; i >= 0; i--) { while (!s.empty() && T[s.top()] <= T[i]) { s.pop(); } - ans[i] = s.empty() ? 0 : (s.top() - i); // 得到索引间距 - s.push(i); // 加入索引,而不是元素 + // 得到索引间距 + res[i] = s.empty() ? 0 : (s.top() - i); + // 将索引入栈,而不是元素 + s.push(i); } - return ans; + return res; } ``` +单调栈讲解完毕,下面开始另一个重点:如何处理「循环数组」。 +### 如何处理环形数组 -单调栈讲解完毕。下面开始另一个重点:如何处理「循环数组」。 - -同样是 Next Greater Number,现在假设给你的数组是个环形的,如何处理? - -给你一个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,4]。拥有了环形属性,最后一个元素 3 绕了一圈后找到了比自己大的元素 4 。 +同样是 Next Greater Number,现在假设给你的数组是个环形的,如何处理?力扣第 503 题「下一个更大元素 II」就是这个问题: -![ink-image](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/2.png) +比如输入一个数组 `[2,1,2,4,3]`,你返回数组 `[4,2,4,-1,4]`。拥有了环形属性,**最后一个元素 3 绕了一圈后找到了比自己大的元素 4**。 - -首先,计算机的内存都是线性的,没有真正意义上的环形数组,但是我们可以模拟出环形数组的效果,一般是通过 % 运算符求模(余数),获得环形特效: +一般是通过 % 运算符求模(余数),来获得环形特效: ```java int[] arr = {1,2,3,4,5}; @@ -90,21 +138,26 @@ while (true) { } ``` -回到 Next Greater Number 的问题,增加了环形属性后,问题的难点在于:这个 Next 的意义不仅仅是当前元素的右边了,有可能出现在当前元素的左边(如上例)。 +这个问题肯定还是要用单调栈的解题模板,但难点在于,比如输入是 `[2,1,2,4,3]`,对于最后一个元素 3,如何找到元素 4 作为 Next Greater Number。 + +**对于这种需求,常用套路就是将数组长度翻倍**: + +![](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/2.jpeg) -明确问题,问题就已经解决了一半了。我们可以考虑这样的思路:将原始数组“翻倍”,就是在后面再接一个原始数组,这样的话,按照之前“比身高”的流程,每个元素不仅可以比较自己右边的元素,而且也可以和左边的元素比较了。 +这样,元素 3 就可以找到元素 4 作为 Next Greater Number 了,而且其他的元素都可以被正确地计算。 -![ink-image (2)](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/3.png) +有了思路,最简单的实现方式当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,**我们可以不用构造新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果**。 -怎么实现呢?你当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,我们可以不用构造新数组,而是利用循环数组的技巧来模拟。直接看代码吧: +直接看代码吧: ```cpp vector nextGreaterElements(vector& nums) { int n = nums.size(); - vector res(n); // 存放结果 + vector res(n); stack s; // 假装这个数组长度翻倍了 for (int i = 2 * n - 1; i >= 0; i--) { + // 索引要求模,其他的和模板一样 while (!s.empty() && s.top() <= nums[i % n]) s.pop(); res[i % n] = s.empty() ? -1 : s.top(); @@ -114,13 +167,16 @@ vector nextGreaterElements(vector& nums) { } ``` -至此,你已经掌握了单调栈的设计方法及代码模板,学会了解决 Next Greater Number,并能够处理循环数组了。 +这样,就可以巧妙解决环形数组的问题,时间复杂度 `O(N)`。 -你的在看,是对我的鼓励。关注公众号:labuladong +如果本文对你有帮助,请三连,这次一定。 +**_____________** -[上一篇:二叉搜索树操作集锦](../数据结构系列/二叉搜索树操作集锦.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:特殊数据结构:单调队列](../数据结构系列/单调队列.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

\ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" index 5c466f07cc..e30ddda4cb 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" @@ -1,5 +1,25 @@ # 特殊数据结构:单调队列 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [几个反直觉的概率问题](https://labuladong.gitbook.io/algo) + * [Git/SQL/正则表达式的在线练习平台](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[239.滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum) + +**-----------** + 前文讲了一种特殊的数据结构「单调栈」monotonic stack,解决了一类问题「Next Greater Number」,本文写一个类似的数据结构「单调队列」。 也许这种数据结构的名字你没听过,其实没啥难的,就是一个「队列」,只是使用了一点巧妙的方法,使得队列中的元素单调递增(或递减)。这个数据结构有什么用?可以解决滑动窗口的一系列问题。 @@ -180,13 +200,12 @@ vector maxSlidingWindow(vector& nums, int k) { 赶紧去拿下 LeetCode 第 239 道题吧~ -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:特殊数据结构:单调栈](../数据结构系列/单调栈.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:设计Twitter](../数据结构系列/设计Twitter.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" index dfc9094a4a..dcebc0391d 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" @@ -1,8 +1,33 @@ # 拆解复杂问题:实现计算器 +**学好算法全靠套路,认准 labuladong 就够了**。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [特殊数据结构:单调队列](https://labuladong.gitbook.io/algo) + * [一行代码就能解决的算法题](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[224.基本计算器](https://leetcode-cn.com/problems/basic-calculator) + +[227.基本计算器II](https://leetcode-cn.com/problems/basic-calculator-ii) + +[772.基本计算器III](https://leetcode-cn.com/problems/basic-calculator-iii) + +**-----------** + 我们最终要实现的计算器功能如下: -1、输入一个字符串,可以包含`+ - * /`、数字、括号以及空格,你的算法返回运算结构。 +1、输入一个字符串,可以包含`+ - * /`、数字、括号以及空格,你的算法返回运算结果。 2、要符合运算法则,括号的优先级最高,先乘除后加减。 @@ -270,12 +295,13 @@ def calculate(s: str) -> int: **退而求其次是一种很聪明策略**。你想想啊,假设这是一道考试题,你不会实现这个计算器,但是你写了字符串转整数的算法并指出了容易溢出的陷阱,那起码可以得 20 分吧;如果你能够处理加减法,那可以得 40 分吧;如果你能处理加减乘除四则运算,那起码够 70 分了;再加上处理空格字符,80 有了吧。我就是不会处理括号,那就算了,80 已经很 OK 了好不好。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:常用的位操作](../算法思维系列/常用的位操作.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:烧饼排序](../算法思维系列/烧饼排序.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" index 1fb45750b1..e5cee44c3e 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" @@ -1,5 +1,25 @@ # 设计Twitter + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [面试官:你说对MySQL事务很熟?那我问你10个问题](https://labuladong.gitbook.io/algo) + * [一行代码就能解决的算法题](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[355.设计推特](https://leetcode-cn.com/problems/design-twitter) + +**-----------** + 「design Twitter」是 LeetCode 上第 355 道题目,不仅题目本身很有意思,而且把合并多个有序链表的算法和面向对象设计(OO design)结合起来了,很有实际意义,本文就带大家来看看这道题。 至于 Twitter 的什么功能跟算法有关系,等我们描述一下题目要求就知道了。 @@ -268,17 +288,16 @@ public List getNewsFeed(int userId) { 我们解决的问题应该只能算 Timeline Service 模块的一小部分,功能越多,系统的复杂性可能是指数级增长的。所以说合理的顶层设计十分重要,其作用是远超某一个算法的。 -最后,Github 上有一个优秀的开源项目叫 [system-design-primer](https://github.com/donnemartin/system-design-primer),专门收集了很多大型系统设计的案例和解析,而且有中文版本,上面这个图也出自该项目。对系统设计感兴趣的读者可以点击链接查看。 +最后,Github 上有一个优秀的开源项目,专门收集了很多大型系统设计的案例和解析,而且有中文版本,上面这个图也出自该项目。对系统设计感兴趣的读者可以点击 [这里](https://github.com/donnemartin/system-design-primer) 查看。 PS:本文前两张图片和 GIF 是我第一次尝试用平板的绘图软件制作的,花了很多时间,尤其是 GIF 图,需要一帧一帧制作。如果本文内容对你有帮助,点个赞分个享,鼓励一下我呗! -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:特殊数据结构:单调队列](../数据结构系列/单调队列.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:递归反转链表的一部分](../数据结构系列/递归反转链表的一部分.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" index 1a20d37ea1..7b68d6f7ee 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" @@ -1,5 +1,25 @@ # 递归反转链表的一部分 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [特殊数据结构:单调队列](https://labuladong.gitbook.io/algo) + * [回溯算法最佳实践:括号生成](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[92.反转链表II](https://leetcode-cn.com/problems/reverse-linked-list-ii/) + +**-----------** + 反转单链表的迭代实现不是一个困难的事情,但是递归实现就有点难度了,如果再加一点难度,让你仅仅反转单链表中的一部分,你是否能**够递归实现**呢? 本文就来由浅入深,step by step 地解决这个问题。如果你还不会递归地反转单链表也没关系,**本文会从递归反转整个单链表开始拓展**,只要你明白单链表的结构,相信你能够有所收获。 @@ -188,13 +208,12 @@ ListNode reverseBetween(ListNode head, int m, int n) { 值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。所以递归操作链表可以作为对递归算法的练习或者拿去和小伙伴装逼,但是考虑效率的话还是使用迭代算法更好。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:设计Twitter](../数据结构系列/设计Twitter.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:队列实现栈\|栈实现队列](../数据结构系列/队列实现栈栈实现队列.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" index e361f61dac..dd3b37ccc7 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" @@ -1,5 +1,27 @@ # 队列实现栈|栈实现队列 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [手把手带你刷二叉树(第三期)](https://labuladong.gitbook.io/algo) + * [高性能短链设计](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[232.用栈实现队列](https://leetcode-cn.com/problems/implement-queue-using-stacks) + +[225.用队列实现栈](https://leetcode-cn.com/problems/implement-stack-using-queues) + +**-----------** + 队列是一种先进先出的数据结构,栈是一种先进后出的数据结构,形象一点就是这样: ![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/1.jpg) @@ -198,109 +220,12 @@ public boolean empty() { 希望本文对你有帮助。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[Xingsheng Qi](https://github.com/ziggy7) 提供 用栈实现队列 C++解法代码: - -```CPP -class MyQueue { -private: - stack s1; - stack s2; - -public: - MyQueue() { - } - - /** 添加元素到队尾 */ - void push(int x) { - s1.push(x); - } - - /** 删除队头的元素并返回 */ - int pop() { - // 先调用 peek 保证 s2 非空 - peek(); - //保存 s2 的栈顶元素用于返回 - int tmp = s2.top(); - s2.pop(); - return tmp; - } - - /** 返回队头元素 */ - int peek() { - if (s2.empty()) - // 把 s1 元素压入 s2 - while (!s1.empty()){ - s2.push(s1.top()); - s1.pop(); - } - return s2.top(); - } - - /** 判断队列是否为空 */ - bool empty() { - return s1.empty()&& s2.empty(); - } -}; -``` - -[Xingsheng Qi](https://github.com/ziggy7) 提供 用队列实现栈 C++解法代码: - -```CPP -class MyStack { -private: - queueq; - int top_elem = 0; - -public: - MyStack() { - - } - - /** 添加元素到栈顶 */ - void push(int x) { - // x 是队列的队尾,是栈的栈顶 - q.push(x); - top_elem = x; - } - - /** 删除栈顶的元素并返回 */ - int pop() { - int size = q.size(); - // 留下队尾 2 个元素 - while (size > 2) { - q.push(q.front()); - q.pop(); - size--; - } - // 记录新的队尾元素 - top_elem = q.front(); - q.push(q.front()); - q.pop(); - // 删除之前的队尾元素 - int tmp = q.front(); - q.pop(); - return tmp; - } - - /** 返回栈顶元素 */ - int top() { - return top_elem; - } - - /** 判断栈是否为空 */ - bool empty() { - return q.empty(); - } -}; -``` +**_____________** -[上一篇:递归反转链表的一部分](../数据结构系列/递归反转链表的一部分.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:算法学习之路](../算法思维系列/算法学习之路.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" index 1d737cf70a..79c5ac09cd 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" @@ -1,5 +1,25 @@ # FloodFill算法详解及应用 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [如何高效进行模幂运算](https://labuladong.gitbook.io/algo) + * [经典动态规划:0-1 背包问题](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[733.图像渲染](https://leetcode-cn.com/problems/flood-fill) + +**-----------** + 啥是 FloodFill 算法呢,最直接的一个应用就是「颜色填充」,就是 Windows 绘画本中那个小油漆桶的标志,可以把一块被圈起来的区域全部染色。 ![floodfill](../pictures/floodfill/floodfill.gif) @@ -106,7 +126,7 @@ image[x][y] = newColor; 完全 OK,这也是处理「图」的一种常用手段。不过对于此题,不用开数组,我们有一种更好的方法,那就是回溯算法。 -前文「回溯算法详解」讲过,这里不再赘述,直接套回溯算法框架: +前文 [回溯算法框架套路](https://labuladong.gitbook.io/algo)讲过,这里不再赘述,直接套回溯算法框架: ```java void fill(int[][] image, int x, int y, @@ -209,20 +229,12 @@ int fill(int[][] image, int x, int y, 以上详细讲解了 FloodFill 算法的框架设计,**二维矩阵中的搜索问题,都逃不出这个算法框架**。 +**_____________** +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 - - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - -[上一篇:字符串乘法](../算法思维系列/字符串乘法.md) - -[下一篇:区间调度之区间合并问题](../算法思维系列/区间调度问题之区间合并.md) - -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/README.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/README.md" index 02fde7ad2c..d1a6268bc7 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/README.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/README.md" @@ -2,6 +2,7 @@ 本章包含一些常用的算法技巧,比如前缀和、回溯思想、位操作、双指针、如何正确书写二分查找等等。 -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: +欢迎关注我的公众号 labuladong,查看全部文章: +![labuladong二维码](../pictures/table_qr2.jpg) ![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" index 7f250a12e7..c4ebebcc46 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" @@ -1,5 +1,27 @@ # Union-Find算法应用 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [手把手带你刷二叉树(第一期)](https://labuladong.gitbook.io/algo) + * [二分查找详解](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[130.被围绕的区域](https://leetcode-cn.com/problems/surrounded-regions) + +[990.等式方程的可满足性](https://leetcode-cn.com/problems/surrounded-regions) + +**-----------** + 上篇文章很多读者对于 Union-Find 算法的应用表示很感兴趣,这篇文章就拿几道 LeetCode 题目来讲讲这个算法的巧妙用法。 首先,复习一下,Union-Find 算法解决的是图的动态连通性问题,这个算法本身不难,能不能应用出来主要是看你抽象问题的能力,是否能够把原始问题抽象成一个有关图论的问题。 @@ -218,13 +240,12 @@ boolean equationsPossible(String[] equations) { -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:Union-Find算法详解](../算法思维系列/UnionFind算法详解.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:一行代码就能解决的算法题](../高频面试系列/一行代码解决的智力题.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" index 3ff8cf4c81..5ea238fa9f 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" @@ -1,5 +1,21 @@ # Union-Find算法详解 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [一文秒杀四道原地修改数组的算法题](https://labuladong.gitbook.io/algo) + * [学习算法和数据结构的思路指南](https://labuladong.gitbook.io/algo) + +**-----------** + 今天讲讲 Union-Find 算法,也就是常说的并查集算法,主要是解决图论中「动态连通性」问题的。名词很高端,其实特别好理解,等会解释,另外这个算法的应用都非常有趣。 说起这个 Union-Find,应该算是我的「启蒙算法」了,因为《算法4》的开头就介绍了这款算法,可是把我秀翻了,感觉好精妙啊!后来刷了 LeetCode,并查集相关的算法题目都非常有意思,而且《算法4》给的解法竟然还可以进一步优化,只要加一个微小的修改就可以把时间复杂度降到 O(1)。 @@ -289,12 +305,14 @@ class UF { Union-Find 算法的复杂度可以这样分析:构造函数初始化数据结构需要 O(N) 的时间和空间复杂度;连通两个节点`union`、判断两个节点的连通性`connected`、计算连通分量`count`所需的时间复杂度均为 O(1)。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[上一篇:如何调度考生的座位](../高频面试系列/座位调度.md) +**_____________** + +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:Union-Find算法应用](../算法思维系列/UnionFind算法应用.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" index 1fd3b72f57..1d2832b21f 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" @@ -1,5 +1,27 @@ # twoSum问题的核心思想 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [我写了首诗,让你闭着眼睛也能写对二分搜索](https://labuladong.gitbook.io/algo) + * [经典动态规划:完全背包问题](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[1.两数之和](https://leetcode-cn.com/problems/two-sum) + +[170.两数之和 III - 数据结构设计](https://leetcode-cn.com/problems/two-sum-iii-data-structure-design) + +**-----------** + Two Sum 系列问题在 LeetCode 上有好几道,这篇文章就挑出有代表性的几道,介绍一下这种问题怎么解决。 ### TwoSum I @@ -153,112 +175,12 @@ int[] twoSum(int[] nums, int target) { -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - -[labuladong](https://github.com/labuladong) 提供TwoSum I JAVA解法代码: - -```JAVA -int[] twoSum(int[] nums, int target) { - int n = nums.length; - index index = new HashMap<>(); - // 构造一个哈希表:元素映射到相应的索引 - for (int i = 0; i < n; i++) - index.put(nums[i], i); - - for (int i = 0; i < n; i++) { - int other = target - nums[i]; - // 如果 other 存在且不是 nums[i] 本身 - if (index.containsKey(other) && index.get(other) != i) - return new int[] {i, index.get(other)}; - } - - return new int[] {-1, -1}; -} -``` - -[Jinglun Zhou](https://github.com/Jasper-Joe) 提供TwoSum I C++解法代码: - -```CPP -class Solution { -public: - vector twoSum(vector& nums, int target) { - int n=nums.size(); - unordered_map index; - // 构造一个哈希表: 元素映射到相应的索引 - for(int i=0;i freq = new HashMap<>(); - - public void add(int number) { - // 记录 number 出现的次数 - freq.put(number, freq.getOrDefault(number, 0) + 1); - } - - public boolean find(int value) { - for (Integer key : freq.keySet()) { - int other = value - key; - // 情况一 - if (other == key && freq.get(key) > 1) - return true; - // 情况二 - if (other != key && freq.containsKey(other)) - return true; - } - return false; - } -} -``` -[Jinglun Zhou](https://github.com/Jasper-Joe) 提供TwoSum II C++解法代码: - -```CPP -class TwoSum { -public: - unordered_map freq; // key为当前加入的元素,value为当前加入元素一共出现的频率 - TwoSum() {} // constructor - - void add(int number) { - // 记录number出现的次数 - freq[number]++; - } - - bool find(int value) { - for(auto& cur:freq) - { - int other=value-cur.first; - // 情况一: other和当前这个元素一样大,所以需要两个这个的元素才能构成value - if(other==cur.first && cur.second>1) - return true; - // 情况二: other和当前这个元素不一样,other在freq中需要至少出现一次,与twoSum I道理一样 - if(other!=cur.first && freq.count(other)) - return true; - } - return false; - } -}; -``` - +**_____________** -[上一篇:滑动窗口技巧](../算法思维系列/滑动窗口技巧.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:常用的位操作](../算法思维系列/常用的位操作.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" index c400bd528c..3798b389a2 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" @@ -1,5 +1,23 @@ # 为什么我推荐《算法4》 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [递归反转链表的一部分](https://labuladong.gitbook.io/algo) + * [25 张图解:键入网址后,到网页显示,其间发生了什么](https://labuladong.gitbook.io/algo) + +**-----------** + +通知:如果本站对你学习算法有帮助,**请收藏网址,并推荐给你的朋友**。由于 **labuladong** 的算法套路太火,很多人直接拿我的 GitHub 文章去开付费专栏,价格还不便宜。我这免费写给你看,**多宣传原创作者是你唯一能做的**,谁也不希望劣币驱逐良币对吧? + 咱们的公众号有很多硬核的算法文章,今天就聊点轻松的,就具体聊聊我非常“鼓吹”的《算法4》。这本书我在之前的文章多次推荐过,但是没有具体的介绍,今天就来正式介绍一下。。 我的推荐不会直接甩一大堆书目,而是会联系实际生活,讲一些书中有趣有用的知识,无论你最后会不会去看这本书,本文都会给你带来一些收获。 @@ -58,7 +76,7 @@ 图论中有一个经典算法叫做 **Bellman-Ford 算法,可以用于寻找负权重环**。对于我们说的套汇问题,可以先把所有边的权重 w 替换成 -ln(w),这样「寻找权重乘积大于 1 的环」就转化成了「寻找权重和小于 0 的环」,就可以使用 Bellman-Ford 算法在 O(EV) 的时间内寻找负权重环,也就是寻找套汇机会。 -《算法4》就介绍到这里,关于上面两个例子的具体内容,可以自己去看书,公众号后台回复关键词「算法4」就有 PDF。 +《算法4》就介绍到这里,关于上面两个例子的具体内容,可以自己去看书,**公众号后台回复关键词「算法4」就有 PDF**。 ### 三、最后说几句 @@ -71,12 +89,14 @@ 另外,读书在精不在多。你花时间《算法4》过个大半(最后小半部分有点困难),同时刷点题,看看咱们的公众号文章,算法这块真就够了,别对细节问题太较真。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**,公众号后台回复关键词「算法4」可以获得 PDF 下载: -![labuladong](../pictures/labuladong.png) -[上一篇:学习算法和刷题的框架思维](../算法思维系列/学习数据结构和算法的高效方法.md) +**_____________** + +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:动态规划解题框架](../动态规划系列/动态规划详解进阶.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" index 6aa5431273..f055e006b7 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" @@ -1,5 +1,27 @@ # 二分查找详解 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [手把手带你刷二叉树(第三期)](https://labuladong.gitbook.io/algo) + * [回溯算法解题套路框架](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[704.二分查找](https://leetcode-cn.com/problems/binary-search) + +[34.在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/) + +**-----------** + 先给大家讲个笑话乐呵一下: 有一天阿东到图书馆借了 N 本书,出图书馆的时候,警报响了,于是保安把阿东拦下,要检查一下哪本书没有登记出借。阿东正准备把每一本书在报警器下过一下,以找出引发警报的书,但是保安露出不屑的眼神:你连二分查找都不会吗?于是保安把书分成两堆,让第一堆过一下报警器,报警器响;于是再把这堆书分成两堆…… 最终,检测了 logN 次之后,保安成功的找到了那本引起警报的书,露出了得意和嘲讽的笑容。于是阿东背着剩下的书走了。 @@ -474,12 +496,14 @@ int right_bound(int[] nums, int target) { 4、如果将「搜索区间」全都统一成两端都闭,好记,只要稍改 `nums[mid] == target` 条件处的代码和返回的逻辑即可,**推荐拿小本本记下,作为二分搜索模板**。 -呵呵,此文对二分查找的问题无敌好吧!**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[上一篇:回溯算法解题框架](../算法思维系列/回溯算法详解修订版.md) +**_____________** + +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:滑动窗口解题框架](../算法思维系列/滑动窗口技巧.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" index 4f5e678b40..eed4b72c24 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" @@ -1,8 +1,28 @@ # 信封嵌套问题 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [讲两道常考的阶乘算法题](https://labuladong.gitbook.io/algo) + * [状态压缩:对动态规划进行降维打击](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[354.俄罗斯套娃信封问题](https://leetcode-cn.com/problems/russian-doll-envelopes) + +**-----------** + 很多算法问题都需要排序技巧,其难点不在于排序本身,而是需要巧妙地排序进行预处理,将算法问题进行转换,为之后的操作打下基础。 -信封嵌套问题就需要先按特定的规则排序,之后就转换为一个 [最长递增子序列问题](../动态规划系列/动态规划设计:最长递增子序列.md),可以用前文 [二分查找详解](二分查找详解.md) 的技巧来解决了。 +信封嵌套问题就需要先按特定的规则排序,之后就转换为一个 [最长递增子序列问题](https://labuladong.gitbook.io/algo),可以用前文 [二分查找详解](https://labuladong.gitbook.io/algo) 的技巧来解决了。 ### 一、题目概述 @@ -89,9 +109,9 @@ public int lengthOfLIS(int[] nums) { 为了清晰,我将代码分为了两个函数, 你也可以合并,这样可以节省下 `height` 数组的空间。 -此算法的时间复杂度为 $O(NlogN)$,因为排序和计算 LIS 各需要 $O(NlogN)$ 的时间。 +此算法的时间复杂度为 `O(NlogN)`,因为排序和计算 LIS 各需要 `O(NlogN)` 的时间。 -空间复杂度为 $O(N)$,因为计算 LIS 的函数中需要一个 `top` 数组。 +空间复杂度为 `O(N)`,因为计算 LIS 的函数中需要一个 `top` 数组。 ### 三、总结 @@ -105,13 +125,12 @@ public int lengthOfLIS(int[] nums) { 有很多算法问题都需要排序后进行处理,阿东正在进行整理总结。希望本文对你有帮助。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:区间调度之区间交集问题](../算法思维系列/区间交集问题.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:几个反直觉的概率问题](../算法思维系列/几个反直觉的概率问题.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md" index 33e5426f32..185e7a2504 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md" @@ -1,6 +1,22 @@ # 几个反直觉的概率问题 -上篇文章 [洗牌算法详解](./洗牌算法.md) 讲到了验证概率算法的蒙特卡罗方法,今天聊点轻松的内容:几个和概率相关的有趣问题。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [学习算法和数据结构的思路指南](https://labuladong.gitbook.io/algo) + * [我写了首诗,让你闭着眼睛也能写对二分搜索](https://labuladong.gitbook.io/algo) + +**-----------** + +上篇文章 [洗牌算法详解](https://labuladong.gitbook.io/algo) 讲到了验证概率算法的蒙特卡罗方法,今天聊点轻松的内容:几个和概率相关的有趣问题。 计算概率有下面两个最简单的原则: @@ -64,11 +80,12 @@ 不是的,就像中奖率 50% 的游戏,你玩两次的中奖率就是 100% 吗?显然不是,你玩两次的中奖率是 75%: -$P(两次能中奖) = P(第一次就中了) + P(第一次没中但第二次中了) = 1/2 + 1/2*1/2 = 75\%$ +`P(两次能中奖) = P(第一次就中了) + P(第一次没中但第二次中了) = 1/2 + 1/2*1/2 = 75%` 那么换到生日悖论也是一个道理,概率不是简单叠加,而要考虑一个连续的过程,所以这个结论并没有什么不合常理之处。 -那为什么只要 23 个人出现相同生日的概率就能大于 50% 了呢?我们先计算 23 个人生日都唯一(不重复)的概率。只有 1 个人的时候,生日唯一的概率是 $365/365$,2 个人时,生日唯一的概率是 $365/365 × 364/365$,以此类推可知 23 人的生日都唯一的概率: +那为什么只要 23 个人出现相同生日的概率就能大于 50% 了呢?我们先计算 23 个人生日都唯一(不重复)的概率。只有 1 个人的时候,生日唯一的概率是 `365/365`,2 个人时,生日唯一的概率是 `365/365 × 364/365`,以此类推可知 23 人的生日都唯一的概率: + ![](../pictures/概率问题/p.png) @@ -114,13 +131,12 @@ $P(两次能中奖) = P(第一次就中了) + P(第一次没中但第二次中 当然,运用此策略蒙题的前提是你真的抓瞎,真的随机乱选答案,这样概率才能作为最后的杀手锏。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:信封嵌套问题](../算法思维系列/信封嵌套问题.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:洗牌算法](../算法思维系列/洗牌算法.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" index d77d0f93c8..a5de61800e 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" @@ -1,10 +1,30 @@ # 前缀和技巧 -今天来聊一道简单却十分巧妙的算法问题:算出一共有几个和为 k 的子数组。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [如何去除有序数组的重复元素](https://labuladong.gitbook.io/algo) + * [区间调度之区间合并问题](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[560.和为K的子数组](https://leetcode-cn.com/problems/subarray-sum-equals-k) + +**-----------** + +今天来聊一道简单却十分巧妙的算法问题:算出一共有几个和为 `k` 的子数组。 ![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/title.png) -那我把所有子数组都穷举出来,算它们的和,看看谁的和等于 k 不就行了。 +那我把所有子数组都穷举出来,算它们的和,看看谁的和等于 `k` 不就行了。 关键是,**如何快速得到某个子数组的和呢**,比如说给你一个数组 `nums`,让你实现一个接口 `sum(i, j)`,这个接口要返回 `nums[i..j]` 的和,而且会被多次调用,你怎么实现这个接口呢? @@ -50,7 +70,7 @@ int subarraySum(int[] nums, int k) { } ``` -这个解法的时间复杂度 $O(N^2)$ 空间复杂度 $O(N)$,并不是最优的解法。不过通过这个解法理解了前缀和数组的工作原理之后,可以使用一些巧妙的办法把时间复杂度进一步降低。 +这个解法的时间复杂度 `O(N^2)` 空间复杂度 `O(N)`,并不是最优的解法。不过通过这个解法理解了前缀和数组的工作原理之后,可以使用一些巧妙的办法把时间复杂度进一步降低。 ### 二、优化解法 @@ -103,7 +123,7 @@ int subarraySum(int[] nums, int k) { ![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/2.jpg) -这样,就把时间复杂度降到了 $O(N)$,是最优解法了。 +这样,就把时间复杂度降到了 `O(N)`,是最优解法了。 ### 三、总结 @@ -129,66 +149,12 @@ for (int i = 1; i < count.length; i++) 希望本文对你有帮助。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - -[labuladong](https://github.com/labuladong) 提供JAVA解法代码: - -```JAVA -int subarraySum(int[] nums, int k) { - int n = nums.length; - // map:前缀和 -> 该前缀和出现的次数 - HashMap - preSum = new HashMap<>(); - // base case - preSum.put(0, 1); - - int ans = 0, sum0_i = 0; - for (int i = 0; i < n; i++) { - sum0_i += nums[i]; - // 这是我们想找的前缀和 nums[0..j] - int sum0_j = sum0_i - k; - // 如果前面有这个前缀和,则直接更新答案 - if (preSum.containsKey(sum0_j)) - ans += preSum.get(sum0_j); - // 把前缀和 nums[0..i] 加入并记录出现次数 - preSum.put(sum0_i, - preSum.getOrDefault(sum0_i, 0) + 1); - } - return ans; -} -``` - -[Jinglun Zhou](https://github.com/Jasper-Joe) 提供C++解法代码: - -```CPP -class Solution { -public: - int subarraySum(vector& nums, int k) { - int n=nums.size(); - unordered_map preSum; - // map: 前缀和 -> 该前缀和出现的次数 - preSum[0]=1; // base case: 例如当数组中只有一个元素, 而k恰好等于这个元素 - int ans=0, sum0_i=0; // sum0_i 表示前缀和 nums[0...i] - for(int i=0;i + +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" index d66ba25f65..d40a4ea62f 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" @@ -1,5 +1,25 @@ # 区间交集问题 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [经典动态规划:编辑距离](https://labuladong.gitbook.io/algo) + * [经典动态规划:高楼扔鸡蛋(进阶)](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[986.区间列表的交集](https://leetcode-cn.com/problems/interval-list-intersections) + +**-----------** + 本文是区间系列问题的第三篇,前两篇分别讲了区间的最大不相交子集和重叠区间的合并,今天再写一个算法,可以快速找出两组区间的交集。 先看下题目,LeetCode 第 986 题就是这个问题: @@ -101,51 +121,14 @@ def intervalIntersection(A, B): 总结一下,区间类问题看起来都比较复杂,情况很多难以处理,但实际上通过观察各种不同情况之间的共性可以发现规律,用简洁的代码就能处理。 -另外,区间问题没啥特别厉害的奇技淫巧,其操作也朴实无华,但其应用却十分广泛,接之前的几篇文章: - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - -[kingkong1111](https://github.com/kingkong1111)提供 Java 代码: - -```java -public int[][] intervalIntersection(int[][] A, int[][] B) { - List res = new ArrayList<>(); - if (A == null || B == null) { - return res.toArray(new int[0][]); - } - // 定义两个指针遍历两个数组 - int i =0; - int j =0; - while (i < A.length && j < B.length) { - int a1 = A[i][0]; - int a2 = A[i][1]; - int b1 = B[j][0]; - int b2 = B[j][1]; - //1 说明有重合 - if (a1 <= b2 && a2 >= b1) { - //2 重合区间:前面的最大值 到 后面的最小值 - int start = Math.max(a1, b1); - int end = Math.min(a2, b2); - res.add(new int[]{start, end}); - } - //3 哪个区间大,哪个不动,另一个指针动,看是否能扩大重合区间 - if (a2 > b2) { - j++; - } else { - i++; - } - } - return res.toArray(new int[0][]); - } -``` - -[上一篇:区间调度之区间合并问题](../算法思维系列/区间调度问题之区间合并.md) - -[下一篇:信封嵌套问题](../算法思维系列/信封嵌套问题.md) +另外,区间问题没啥特别厉害的奇技淫巧,其操作也朴实无华,但其应用却十分广泛。 -[目录](../README.md#目录) +**_____________** +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230\344\271\213\345\214\272\351\227\264\345\220\210\345\271\266.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230\344\271\213\345\214\272\351\227\264\345\220\210\345\271\266.md" index bc0fd37c12..3a12c58626 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230\344\271\213\345\214\272\351\227\264\345\220\210\345\271\266.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230\344\271\213\345\214\272\351\227\264\345\220\210\345\271\266.md" @@ -1,5 +1,25 @@ # 区间调度问题之区间合并 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [Git原理之最近公共祖先](https://labuladong.gitbook.io/algo) + * [洗牌算法](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[56.合并区间](https://leetcode-cn.com/problems/merge-intervals) + +**-----------** + 上篇文章用贪心算法解决了区间调度问题:给你很多区间,让你求其中的最大不重叠子集。 其实对于区间相关的问题,还有很多其他类型,本文就来讲讲区间合并问题(Merge Interval)。 @@ -61,13 +81,12 @@ def merge(intervals): 本文终,希望对你有帮助。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:FloodFill算法详解及应用](../算法思维系列/FloodFill算法详解及应用.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:区间调度之区间交集问题](../算法思维系列/区间交集问题.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" index e3b308629f..9859ea314f 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" @@ -1,5 +1,29 @@ # 双指针技巧总结 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [一文秒杀四道原地修改数组的算法题](https://labuladong.gitbook.io/algo) + * [Linux的进程、线程、文件描述符是什么](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[141.环形链表](https://leetcode-cn.com/problems/linked-list-cycle) + +[141.环形链表II](https://leetcode-cn.com/problems/linked-list-cycle-ii) + +[167.两数之和 II - 输入有序数组](https://leetcode-cn.com/problems/two-sum) + +**-----------** + 我把双指针技巧再分为两类,一类是「快慢指针」,一类是「左右指针」。前者解决主要解决链表中的问题,比如典型的判定链表中是否包含环;后者主要解决数组(或者字符串)中的问题,比如二分查找。 ### 一、快慢指针的常见算法 @@ -67,7 +91,7 @@ ListNode detectCycle(ListNode head) { 可以看到,当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。这是为什么呢? -第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(环长度的倍数)。 +第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。 ![2](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/2.png) @@ -192,14 +216,16 @@ void reverse(int[] nums) { 这也许是双指针技巧的最高境界了,如果掌握了此算法,可以解决一大类子字符串匹配的问题,不过「滑动窗口」稍微比上述的这些算法复杂些。 -幸运的是,这类算法是有框架模板的,而且[这篇文章](滑动窗口技巧.md)就讲解了「滑动窗口」算法模板,帮大家秒杀几道 LeetCode 子串匹配的问题。 +幸运的是,这类算法是有框架模板的,而且[这篇文章](https://labuladong.gitbook.io/algo)就讲解了「滑动窗口」算法模板,帮大家秒杀几道 LeetCode 子串匹配的问题。 + -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:滑动窗口解题框架](../算法思维系列/滑动窗口技巧.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:Linux的进程、线程、文件描述符是什么](../技术/linux进程.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" index 23dbaadc74..a614f1f8f9 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" @@ -1,5 +1,28 @@ # 回溯算法详解 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [我写了首诗,把滑动窗口算法算法变成了默写题](https://labuladong.gitbook.io/algo) + * [经典动态规划:高楼扔鸡蛋(进阶)](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[46.全排列](https://leetcode-cn.com/problems/permutations) + +[51.N皇后](https://leetcode-cn.com/problems/n-queens) + +**-----------** + + 这篇文章是很久之前的一篇《回溯算法详解》的进阶版,之前那篇不够清楚,就不必看了,看这篇就行。把框架给你讲清楚,你会发现回溯算法问题都是一个套路。 废话不多说,直接上回溯算法框架。**解决一个回溯问题,实际上就是一个决策树的遍历过程**。你只需要思考 3 个问题: @@ -138,7 +161,7 @@ void backtrack(int[] nums, LinkedList track) { ![](../pictures/backtracking/6.jpg) -至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是很高效,因为对链表使用 `contains` 方法需要 O(N) 的时间复杂度。有更好的方法通过交换元素达到目的,但是难理解一些,这里就不写了,有兴趣可以自行搜索一下。 +至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是很高效,应为对链表使用 `contains` 方法需要 O(N) 的时间复杂度。有更好的方法通过交换元素达到目的,但是难理解一些,这里就不写了,有兴趣可以自行搜索一下。 但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。**这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高**。 @@ -274,34 +297,14 @@ def backtrack(...): 某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[Zongshuai](https://github.com/zongshuai818) 提供全排列 Python3解法代码: - -```python -class Solution: - def permute(self, nums: List[int]) -> List[List[int]]: - # 回溯算法 - result = [] - track = [] # 可行路径 - def trackBack(nums_, track_): - if len(track_) == len(nums_): # 满足终止条件 - result.append(track_[:]) - return - for i in nums_: #所有可选项 - if i in track_: # 判断是否可选 - continue - track.append(i) # 选择 - trackBack(nums_, track_) # 递归 - track.pop() # 回溯 - trackBack(nums, track) - return result -``` +**_____________** -[上一篇:动态规划答疑篇](../动态规划系列/最优子结构.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:二分查找解题框架](../算法思维系列/二分查找详解.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" index 9638127110..26ccfe489f 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" @@ -1,7 +1,27 @@ # 字符串乘法 -对于比较小的数字,做运算可以直接使用编程语言提供的运算符,但是如果相乘的两个因数非常大,语言提供的数据类型可能就会溢出。一种替代方案就是,运算数以字符串的形式输入,然后模仿我们小学学习的乘法算术过程计算出结果,并且也用字符串表示。 +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [关于 Linux shell 你必须知道的](https://labuladong.gitbook.io/algo) + * [回溯算法解题套路框架](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[43.字符串相乘](https://leetcode-cn.com/problems/multiply-strings) + +**-----------** + +对于比较小的数字,做运算可以直接使用编程语言提供的运算符,但是如果相乘的两个因数非常大,语言提供的数据类型可能就会溢出。一种替代方案就是,运算数以字符串的形式输入,然后模仿我们小学学习的乘法算术过程计算出结果,并且也用字符串表示。 + ![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/title.png) 需要注意的是,`num1` 和 `num2` 可以非常长,所以不可以把他们直接转成整型然后运算,唯一的思路就是模仿我们手算乘法。 @@ -71,13 +91,12 @@ string multiply(string num1, string num2) { 也许算法就是一种**寻找思维定式的思维**吧,希望本文对你有帮助。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:前缀和技巧](../算法思维系列/前缀和技巧.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:FloodFill算法详解及应用](../算法思维系列/FloodFill算法详解及应用.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

\ No newline at end of file diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" index 7b7fcfd594..2ee5b6bc6d 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" @@ -1,4 +1,22 @@ -# 学习数据结构和算法的框架思维 +# 学习算法和刷题的思路指南 + + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [twoSum问题的核心思想](https://labuladong.gitbook.io/algo) + * [经典动态规划:高楼扔鸡蛋(进阶)](https://labuladong.gitbook.io/algo) + +**-----------** + +通知:如果本站对你学习算法有帮助,**请收藏网址,并推荐给你的朋友**。由于 **labuladong** 的算法套路太火,很多人直接拿我的 GitHub 文章去开付费专栏,价格还不便宜。我这免费写给你看,**多宣传原创作者是你唯一能做的**,谁也不希望劣币驱逐良币对吧? 这是好久之前的一篇文章「学习数据结构和算法的框架思维」的修订版。之前那篇文章收到广泛好评,没看过也没关系,这篇文章会涵盖之前的所有内容,并且会举很多代码的实例,教你如何使用框架思维。 @@ -100,7 +118,7 @@ class TreeNode { void traverse(TreeNode root) { for (TreeNode child : root.children) - traverse(child) + traverse(child); } ``` @@ -112,7 +130,7 @@ N 叉树的遍历又可以扩展为图的遍历,因为图就是好几 N 叉棵 首先要明确的是,**数据结构是工具,算法是通过合适的工具解决特定问题的方法**。也就是说,学习算法之前,最起码得了解那些常用的数据结构,了解它们的特性和缺陷。 -那么该如何在 LeetCode 刷题呢?之前的文章[算法学习之路](算法学习之路.md)写过一些,什么按标签刷,坚持下去云云。现在距那篇文章已经过去将近一年了,我不说那些不痛不痒的话,直接说具体的建议: +那么该如何在 LeetCode 刷题呢?之前的文章[算法学习之路](https://labuladong.gitbook.io/algo)写过一些,什么按标签刷,坚持下去云云。现在距那篇文章已经过去将近一年了,我不说那些不痛不痒的话,直接说具体的建议: **先刷二叉树,先刷二叉树,先刷二叉树**! @@ -196,7 +214,7 @@ void traverse(TreeNode* node) { 再举例吧,说几道我们之前文章写过的问题。 -[动态规划详解](../动态规划系列/动态规划详解进阶.md)说过凑零钱问题,暴力解法就是遍历一棵 N 叉树: +[动态规划详解](https://labuladong.gitbook.io/algo)说过凑零钱问题,暴力解法就是遍历一棵 N 叉树: ![](../pictures/动态规划详解进阶/5.jpg) @@ -229,7 +247,7 @@ def dp(n): 其实很多动态规划问题就是在遍历一棵树,你如果对树的遍历操作烂熟于心,起码知道怎么把思路转化成代码,也知道如何提取别人解法的核心思路。 -再看看回溯算法,前文[回溯算法详解](回溯算法详解修订版.md)干脆直接说了,回溯算法就是个 N 叉树的前后序遍历问题,没有例外。 +再看看回溯算法,前文[回溯算法详解](https://labuladong.gitbook.io/algo)干脆直接说了,回溯算法就是个 N 叉树的前后序遍历问题,没有例外。 比如 N 皇后问题吧,主要代码如下: @@ -268,7 +286,7 @@ N 叉树的遍历框架,找出来了把~你说,树这种结构重不重要 但是,你要是心中没有框架,那么你根本无法解题,给了你答案,你也不会发现这就是个树的遍历问题。 -这种思维是很重要的,[动态规划详解](../动态规划系列/动态规划详解进阶.md)中总结的找状态转移方程的几步流程,有时候按照流程写出解法,说实话我自己都不知道为啥是对的,反正它就是对了。。。 +这种思维是很重要的,[动态规划详解](https://labuladong.gitbook.io/algo)中总结的找状态转移方程的几步流程,有时候按照流程写出解法,说实话我自己都不知道为啥是对的,反正它就是对了。。。 **这就是框架的力量,能够保证你在快睡着的时候,依然能写出正确的程序;就算你啥都不会,都能比别人高一个级别。** @@ -279,12 +297,14 @@ N 叉树的遍历框架,找出来了把~你说,树这种结构重不重要 刷算法题建议从「树」分类开始刷,结合框架思维,把这几十道题刷完,对于树结构的理解应该就到位了。这时候去看回溯、动规、分治等算法专题,对思路的理解可能会更加深刻一些。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[上一篇:最长公共子序列](../动态规划系列/最长公共子序列.md) +**_____________** + +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:学习数据结构和算法读什么书](../算法思维系列/为什么推荐算法4.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" index f239d1b516..a35a3a39f5 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" @@ -1,37 +1,59 @@ # 常用的位操作 -本文分两部分,第一部分列举几个有趣的位操作,第二部分讲解算法中常用的 n & (n - 1) 操作,顺便把用到这个技巧的算法题列出来讲解一下。因为位操作很简单,所以假设读者已经了解与、或、异或这三种基本操作。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [40张图解:TCP三次握手和四次挥手面试题](https://labuladong.gitbook.io/algo) + * [动态规划答疑篇](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[191.位1的个数](https://leetcode-cn.com/problems/number-of-1-bits) + +[231.2的幂](https://leetcode-cn.com/problems/power-of-two/) + +**-----------** + +本文分两部分,第一部分列举几个有趣的位操作,第二部分讲解算法中常用的 `n & (n - 1)` 操作,顺便把用到这个技巧的算法题列出来讲解一下。因为位操作很简单,所以假设读者已经了解与、或、异或这三种基本操作。 位操作(Bit Manipulation)可以玩出很多奇技淫巧,但是这些技巧大部分都过于晦涩,没必要深究,读者只要记住一些有用的操作即可。 ### 一、几个有趣的位操作 -1. 利用或操作 `|` 和空格将英文字符转换为小写 +1. **利用或操作 `|` 和空格将英文字符转换为小写** ```c ('a' | ' ') = 'a' ('A' | ' ') = 'a' ``` -2. 利用与操作 `&` 和下划线将英文字符转换为大写 +2. **利用与操作 `&` 和下划线将英文字符转换为大写** ```c ('b' & '_') = 'B' ('B' & '_') = 'B' ``` -3. 利用异或操作 `^` 和空格进行英文字符大小写互换 +3. **利用异或操作 `^` 和空格进行英文字符大小写互换** ```c ('d' ^ ' ') = 'D' ('D' ^ ' ') = 'd' ``` -PS:以上操作能够产生奇特效果的原因在于 ASCII 编码。字符其实就是数字,恰巧这些字符对应的数字通过位运算就能得到正确的结果,有兴趣的读者可以查 ASCII 码表自己算算,本文就不展开讲了。 +以上操作能够产生奇特效果的原因在于 ASCII 编码。字符其实就是数字,恰巧这些字符对应的数字通过位运算就能得到正确的结果,有兴趣的读者可以查 ASCII 码表自己算算,本文就不展开讲了。 -4. 判断两个数是否异号 +4. **判断两个数是否异号** -```c +```cpp int x = -1, y = 2; bool f = ((x ^ y) < 0); // true @@ -39,9 +61,9 @@ int x = 3, y = 2; bool f = ((x ^ y) < 0); // false ``` -PS:这个技巧还是很实用的,利用的是补码编码的符号位。如果不用位运算来判断是否异号,需要使用 if else 分支,还挺麻烦的。读者可能想利用乘积或者商来判断两个数是否异号,但是这种处理方式可能造成溢出,从而出现错误。(关于补码编码和溢出,参见前文) +这个技巧还是很实用的,利用的是补码编码的符号位。如果不用位运算来判断是否异号,需要使用 if else 分支,还挺麻烦的。读者可能想利用乘积或者商来判断两个数是否异号,但是这种处理方式可能造成溢出,从而出现错误。 -5. 交换两个数 +5. **不用临时变量交换两个数** ```c int a = 1, b = 2; @@ -51,7 +73,7 @@ a ^= b; // 现在 a = 2, b = 1 ``` -6. 加一 +6. **加一** ```c int n = 1; @@ -59,7 +81,7 @@ n = -~n; // 现在 n = 2 ``` -7. 减一 +7. **减一** ```c int n = 2; @@ -69,17 +91,19 @@ n = ~-n; PS:上面这三个操作就纯属装逼用的,没啥实际用处,大家了解了解乐呵一下就行。 -### 二、算法常用操作 n&(n-1) +### 二、算法常用操作 -这个操作是算法中常见的,作用是消除数字 n 的二进制表示中的最后一个 1。 + `n&(n-1)` 这个操作是算法中常见的,作用是消除数字 `n` 的二进制表示中的最后一个 1。 看个图就很容易理解了: -![n](../pictures/%E4%BD%8D%E6%93%8D%E4%BD%9C/1.png) +![](../pictures/%E4%BD%8D%E6%93%8D%E4%BD%9C/1.png) + +其核心逻辑就是,`n - 1` 一定可以消除最后一个 1,同时把其后的 0 都变成 1,这样再和 `n` 做一次 `&` 运算,就可以仅仅把最后一个 1 变成 0 了。 -1. 计算汉明权重(Hamming Weight) +1. **计算汉明权重(Hamming Weight)** -![title](../pictures/%E4%BD%8D%E6%93%8D%E4%BD%9C/title.png) +![](../pictures/%E4%BD%8D%E6%93%8D%E4%BD%9C/title.png) 就是让你返回 n 的二进制表示中有几个 1。因为 n & (n - 1) 可以消除最后一个 1,所以可以用一个循环不停地消除 1 同时计数,直到 n 变成 0 为止。 @@ -94,7 +118,7 @@ int hammingWeight(uint32_t n) { } ``` -1. 判断一个数是不是 2 的指数 +2. **判断一个数是不是 2 的指数** 一个数如果是 2 的指数,那么它的二进制表示一定只含有一个 1: @@ -104,7 +128,7 @@ int hammingWeight(uint32_t n) { 2^2 = 4 = 0b0100 ``` -如果使用位运算技巧就很简单了(注意运算符优先级,括号不可以省略): +如果使用 `n&(n-1)` 的技巧就很简单了(注意运算符优先级,括号不可以省略): ```cpp bool isPowerOfTwo(int n) { @@ -113,15 +137,37 @@ bool isPowerOfTwo(int n) { } ``` -以上便是一些有趣/常用的位操作。其实位操作的技巧很多,有一个叫做 Bit Twiddling Hacks 的外国网站收集了几乎所有位操作的黑科技玩法,感兴趣的读者可以点击「阅读原文」按钮查看。 +**3、查找只出现一次的元素** + +![](../pictures/位操作/title1.png) + +这里就可以运用异或运算的性质: + +一个数和它本身做异或运算结果为 0,即 `a ^ a = 0`;一个数和 0 做异或运算的结果为它本身,即 `a ^ 0 = a`。 + +对于这道题目,我们只要把所有数字进行异或,成对儿的数字就会变成 0,落单的数字和 0 做异或还是它本身,所以最后异或的结果就是只出现一次的元素: + +```cpp +int singleNumber(vector& nums) { + int res = 0; + for (int n : nums) { + res ^= n; + } + return res; +} +``` + -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: +以上便是一些有趣/常用的位操作。其实位操作的技巧很多,有一个叫做 Bit Twiddling Hacks 的外国网站收集了几乎所有位操作的黑科技玩法,感兴趣的读者可以查看: -![labuladong](../pictures/labuladong.jpg) +http://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel +**_____________** -[上一篇:twoSum问题的核心思想](../算法思维系列/twoSum问题的核心思想.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:拆解复杂问题:实现计算器](../数据结构系列/实现计算器.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" index 5207da39ad..5aa1768584 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" @@ -1,5 +1,25 @@ # 洗牌算法 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [二叉搜索树操作集锦](https://labuladong.gitbook.io/algo) + * [动态规划解题套路框架](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[384.打乱数组](https://leetcode-cn.com/problems/shuffle-an-array) + +**-----------** + 我知道大家会各种花式排序算法,但是如果叫你打乱一个数组,你是否能做到胸有成竹?即便你拍脑袋想出一个算法,怎么证明你的算法就是正确的呢?乱序算法不像排序算法,结果唯一可以很容易检验,因为「乱」可以有很多种,你怎么能证明你的算法是「真的乱」呢? 所以我们面临两个问题: @@ -102,9 +122,9 @@ void shuffle(int[] arr) { } ``` -现在你应该明白这种写法为什么会错误了。因为这种写法得到的所有可能结果有 $n^n$ 种,而不是 $n!$ 种,而且 $n^n$ 不可能是 $n!$ 的整数倍。 +现在你应该明白这种写法为什么会错误了。因为这种写法得到的所有可能结果有 `n^n` 种,而不是 `n!` 种,而且 `n^n` 不可能是 `n!` 的整数倍。 -比如说 `arr = {1,2,3}`,正确的结果应该有 $3!= 6$ 种可能,而这种写法总共有 $3^3 = 27$ 种可能结果。因为 27 不能被 6 整除,所以一定有某些情况被「偏袒」了,也就是说某些情况出现的概率会大一些,所以这种打乱结果不算「真的乱」。 +比如说 `arr = {1,2,3}`,正确的结果应该有 `3!= 6` 种可能,而这种写法总共有 `3^3 = 27` 种可能结果。因为 27 不能被 6 整除,所以一定有某些情况被「偏袒」了,也就是说某些情况出现的概率会大一些,所以这种打乱结果不算「真的乱」。 上面我们从直觉上简单解释了洗牌算法正确的准则,没有数学证明,我想大家也懒得证明。对于概率问题我们可以使用「蒙特卡罗方法」进行简单验证。 @@ -181,13 +201,12 @@ for (int feq : count) 第二部分写了洗牌算法正确性的衡量标准,即每种随机结果出现的概率必须相等。如果我们不用严格的数学证明,可以通过蒙特卡罗方法大力出奇迹,粗略验证算法的正确性。蒙特卡罗方法也有不同的思路,不过要求不必太严格,因为我们只是寻求一个简单的验证。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:几个反直觉的概率问题](../算法思维系列/几个反直觉的概率问题.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:递归详解](../算法思维系列/递归详解.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" index 7aac23fdf0..581aca5c98 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" @@ -334,8 +334,8 @@ class Solution: return s[start:start+min_len] if min_len != float("Inf") else "" ``` -[上一篇:二分查找解题框架](../算法思维系列/二分查找详解.md) +[上一篇:二分查找解题框架](https://labuladong.gitbook.io/algo) -[下一篇:双指针技巧解题框架](../算法思维系列/双指针技巧.md) +[下一篇:双指针技巧解题框架](https://labuladong.gitbook.io/algo) -[目录](../README.md#目录) +[目录](https://labuladong.gitbook.io/algo#目录) diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" index f910b44fa2..5e4e4df029 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" @@ -1,5 +1,25 @@ # 烧饼排序 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [手把手带你刷二叉树(第三期)](https://labuladong.gitbook.io/algo) + * [Union-Find算法应用](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[969.煎饼排序](https://leetcode-cn.com/problems/pancake-sorting) + +**-----------** + 烧饼排序是个很有意思的实际问题:假设盘子上有 `n` 块**面积大小不一**的烧饼,你如何用一把锅铲进行若干次翻转,让这些烧饼的大小有序(小的在上,大的在下)? ![](../pictures/pancakeSort/1.jpg) @@ -14,7 +34,7 @@ ![](../pictures/pancakeSort/title.png) -如何解决这个问题呢?其实类似上篇文章 [递归反转链表的一部分](../数据结构系列/递归反转链表的一部分.md),这也是需要**递归思想**的。 +如何解决这个问题呢?其实类似上篇文章 [递归反转链表的一部分](https://labuladong.gitbook.io/algo),这也是需要**递归思想**的。 ### 一、思路分析 @@ -119,49 +139,12 @@ void reverse(int[] arr, int i, int j) { 不妨分享一下你的思考。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - -[AkiJoey](https://github.com/AkiJoey) 提供 C++ 解法代码: -```c++ -class Solution { -public: - vector pancakeSort(vector& A) { - sort(A, A.size()); - return res; - } -private: - vector res; - void sort(vector& arr, int n) { - // base case - if (n == 1) - return; - - // 寻找最大饼的索引 - int max = 0, index = 0; - for(int i = 0;i < n;i++) - if (arr[i] > max) { - max = arr[i]; - index = i; - } - - // 第一次翻转,将最大饼翻到最上面 - reverse(arr.begin(), arr.begin() + index + 1); - res.emplace_back(index + 1); - - // 第二次翻转,将最大饼翻到最下面 - reverse(arr.begin(), arr.begin() + n); - res.emplace_back(n); - - // 递归调用 - sort(arr, n - 1); - } -}; -``` +**_____________** -[上一篇:拆解复杂问题:实现计算器](../数据结构系列/实现计算器.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:前缀和技巧](../算法思维系列/前缀和技巧.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" index accfede4c4..cd26588f3a 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" @@ -1,5 +1,21 @@ # 算法学习之路 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [Git/SQL/正则表达式的在线练习平台](https://labuladong.gitbook.io/algo) + * [回溯算法团灭子集、排列、组合问题](https://labuladong.gitbook.io/algo) + +**-----------** + 之前发的那篇关于框架性思维的文章,我也发到了不少其他圈子,受到了大家的普遍好评,这一点我真的没想到,首先感谢大家的认可,我会更加努力,写出通俗易懂的算法文章。 有很多朋友问我数据结构和算法到底该怎么学,尤其是很多朋友说自己是「小白」,感觉这些东西好难啊,就算看了之前的「框架思维」,也感觉自己刷题乏力,希望我能聊聊我从一个非科班小白一路是怎么学过来的。 @@ -76,12 +92,14 @@ PS:**如果有的英文题目实在看不懂,有个小技巧**,你在题 以上,不光是坚持刷算法题吧,很多场景都适用。执行力是要靠「欲望」支撑的,我也是一凡人,只有那些看得见摸得着的东西才能使我快乐呀。读者不妨也尝试把刷题学习和自己的切身利益联系起来,这恐怕是坚持下去最简单直白的理由了。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) -[上一篇:队列实现栈\|栈实现队列](../数据结构系列/队列实现栈栈实现队列.md) +**_____________** + +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:回溯算法详解](../算法思维系列/回溯算法详解修订版.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\200\222\345\275\222\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\200\222\345\275\222\350\257\246\350\247\243.md" index 1e49bc9ac9..4a0b09fb9d 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\200\222\345\275\222\350\257\246\350\247\243.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\200\222\345\275\222\350\257\246\350\247\243.md" @@ -1,5 +1,21 @@ # 递归详解 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [特殊数据结构:单调队列](https://labuladong.gitbook.io/algo) + * [设计Twitter](https://labuladong.gitbook.io/algo) + +**-----------** + 首先说明一个问题,简单阐述一下递归,分治算法,动态规划,贪心算法这几个东西的区别和联系,心里有个印象就好。 递归是一种编程技巧,一种解决问题的思维方式;分治算法和动态规划很大程度上是递归思想基础上的(虽然动态规划的最终版本大都不是递归了,但解题思想还是离不开递归),解决更具体问题的两类算法思想;贪心算法是动态规划算法的一个子集,可以更高效解决一部分更特殊的问题。 @@ -249,13 +265,12 @@ https://leetcode.com/tag/divide-and-conquer/ -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:洗牌算法](../算法思维系列/洗牌算法.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何实现LRU算法](../高频面试系列/LRU算法.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" index e39c60b3d7..8bdcc69245 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" @@ -1,8 +1,26 @@ -# LRU算法详解 +# 层层拆解,带你手写 LRU 算法 -### 一、什么是 LRU 算法 -就是一种缓存淘汰策略。 +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [25 张图解:键入网址后,到网页显示,其间发生了什么](https://labuladong.gitbook.io/algo) + * [如何在无限序列中随机抽取元素](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[146.LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/) + +**-----------** + +LRU 算法就是一种缓存淘汰策略,原理不难,但是面试中写出没有 bug 的算法比较有技巧,需要对数据结构进行层层抽象和拆解,本文 labuladong 就给你写一手漂亮的代码。 计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。那么,什么样的数据,我们判定为「有用的」的数据呢? @@ -10,25 +28,27 @@ LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently 举个简单的例子,安卓手机都可以把软件放到后台运行,比如我先后打开了「设置」「手机管家」「日历」,那么现在他们在后台排列的顺序是这样的: -![jietu](../pictures/LRU%E7%AE%97%E6%B3%95/1.jpg) +![](../pictures/LRU%E7%AE%97%E6%B3%95/1.jpg) 但是这时候如果我访问了一下「设置」界面,那么「设置」就会被提前到第一个,变成这样: -![jietu](../pictures/LRU%E7%AE%97%E6%B3%95/2.jpg) +![](../pictures/LRU%E7%AE%97%E6%B3%95/2.jpg) 假设我的手机只允许我同时开 3 个应用程序,现在已经满了。那么如果我新开了一个应用「时钟」,就必须关闭一个应用为「时钟」腾出一个位置,关那个呢? 按照 LRU 的策略,就关最底下的「手机管家」,因为那是最久未使用的,然后把新开的应用放到最上面: -![jietu](../pictures/LRU%E7%AE%97%E6%B3%95/3.jpg) +![](../pictures/LRU%E7%AE%97%E6%B3%95/3.jpg) 现在你应该理解 LRU(Least Recently Used)策略了。当然还有其他缓存淘汰策略,比如不要按访问的时序来淘汰,而是按访问频率(LFU 策略)来淘汰等等,各有应用场景。本文讲解 LRU 算法策略。 -### 二、LRU 算法描述 +### 一、LRU 算法描述 -LRU 算法实际上是让你设计数据结构:首先要接收一个 capacity 参数作为缓存的最大容量,然后实现两个 API,一个是 put(key, val) 方法存入键值对,另一个是 get(key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1。 +力扣第 146 题「LRU缓存机制」就是让你设计数据结构: -注意哦,get 和 put 方法必须都是 $O(1)$ 的时间复杂度,我们举个具体例子来看看 LRU 算法怎么工作。 +首先要接收一个 `capacity` 参数作为缓存的最大容量,然后实现两个 API,一个是 `put(key, val)` 方法存入键值对,另一个是 `get(key)` 方法获取 `key` 对应的 `val`,如果 `key` 不存在则返回 -1。 + +注意哦,`get` 和 `put` 方法必须都是 `O(1)` 的时间复杂度,我们举个具体例子来看看 LRU 算法怎么工作。 ```cpp /* 缓存容量为 2 */ @@ -40,49 +60,64 @@ LRUCache cache = new LRUCache(2); cache.put(1, 1); // cache = [(1, 1)] + cache.put(2, 2); // cache = [(2, 2), (1, 1)] + cache.get(1); // 返回 1 // cache = [(1, 1), (2, 2)] // 解释:因为最近访问了键 1,所以提前至队头 // 返回键 1 对应的值 1 + cache.put(3, 3); // cache = [(3, 3), (1, 1)] // 解释:缓存容量已满,需要删除内容空出位置 // 优先删除久未使用的数据,也就是队尾的数据 // 然后把新的数据插入队头 + cache.get(2); // 返回 -1 (未找到) // cache = [(3, 3), (1, 1)] // 解释:cache 中不存在键为 2 的数据 + cache.put(1, 4); // cache = [(1, 4), (3, 3)] // 解释:键 1 已存在,把原始值 1 覆盖为 4 // 不要忘了也要将键值对提前到队头 ``` -### 三、LRU 算法设计 +### 二、LRU 算法设计 + +分析上面的操作过程,要让 `put` 和 `get` 方法的时间复杂度为 O(1),我们可以总结出 `cache` 这个数据结构必要的条件: + +1、显然 `cache` 中的元素必须有时序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置。 -分析上面的操作过程,要让 put 和 get 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分。 +2、我们要在 `cache` 中快速找某个 `key` 是否已存在并得到对应的 `val`; -因为显然 cache 必须有顺序之分,以区分最近使用的和久未使用的数据;而且我们要在 cache 中查找键是否已存在;如果容量满了要删除最后一个数据;每次访问还要把数据插入到队头。 +3、每次访问 `cache` 中的某个 `key`,需要将这个元素变为最近使用的,也就是说 `cache` 要支持在任意位置快速插入和删除元素。 -那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。 +那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表 `LinkedHashMap`。 LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样: ![HashLinkedList](../pictures/LRU%E7%AE%97%E6%B3%95/4.jpg) -思想很简单,就是借助哈希表赋予了链表快速查找的特性嘛:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。回想刚才的例子,这种数据结构是不是完美解决了 LRU 缓存的需求? +借助这个结构,我们来逐一分析上面的 3 个条件: + +1、如果我们每次默认从链表尾部添加元素,那么显然越靠尾部的元素就是最近使用的,越靠头部的元素就是最久未使用的。 + +2、对于某一个 `key`,我们可以通过哈希表快速定位到链表中的节点,从而取得对应 `val`。 + +3、链表显然是支持在任意位置快速插入和删除的,改改指针就行。只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助哈希表,可以通过 `key` 快速映射到任意一个链表节点,然后进行插入和删除。 -也许读者会问,为什么要是双向链表,单链表行不行?另外,既然哈希表中已经存了 key,为什么链表中还要存键值对呢,只存值不就行了? +**也许读者会问,为什么要是双向链表,单链表行不行?另外,既然哈希表中已经存了 `key`,为什么链表中还要存 `key` 和 `val` 呢,只存 `val` 不就行了**? 想的时候都是问题,只有做的时候才有答案。这样设计的原因,必须等我们亲自实现 LRU 算法之后才能理解,所以我们开始看代码吧~ -### 四、代码实现 +### 三、代码实现 -很多编程语言都有内置的哈希链表或者类似 LRU 功能的库函数,但是为了帮大家理解算法的细节,我们用 Java 自己造轮子实现一遍 LRU 算法。 +很多编程语言都有内置的哈希链表或者类似 LRU 功能的库函数,但是为了帮大家理解算法的细节,我们先自己造轮子实现一遍 LRU 算法,然后再使用 Java 内置的 `LinkedHashMap` 来实现一遍。 -首先,我们把双链表的节点类写出来,为了简化,key 和 val 都认为是 int 类型: +首先,我们把双链表的节点类写出来,为了简化,`key` 和 `val` 都认为是 int 类型: ```java class Node { @@ -95,63 +130,61 @@ class Node { } ``` -然后依靠我们的 Node 类型构建一个双链表,实现几个需要的 API(这些操作的时间复杂度均为 $O(1)$): +然后依靠我们的 `Node` 类型构建一个双链表,实现几个 LRU 算法必须的 API: ```java class DoubleList { - // 在链表头部添加节点 x,时间 O(1) - public void addFirst(Node x); + // 头尾虚节点 + private Node head, tail; + // 链表元素数 + private int size; + + public DoubleList() { + // 初始化双向链表的数据 + head = new Node(0, 0); + tail = new Node(0, 0); + head.next = tail; + tail.prev = head; + size = 0; + } + + // 在链表尾部添加节点 x,时间 O(1) + public void addLast(Node x) { + x.prev = tail.prev; + x.next = tail; + tail.prev.next = x; + tail.prev = x; + size++; + } // 删除链表中的 x 节点(x 一定存在) // 由于是双链表且给的是目标 Node 节点,时间 O(1) - public void remove(Node x); - - // 删除链表中最后一个节点,并返回该节点,时间 O(1) - public Node removeLast(); + public void remove(Node x) { + x.prev.next = x.next; + x.next.prev = x.prev; + size--; + } + // 删除链表中第一个节点,并返回该节点,时间 O(1) + public Node removeFirst() { + if (head.next == tail) + return null; + Node first = head.next; + remove(first); + return first; + } + // 返回链表长度,时间 O(1) - public int size(); + public int size() { return size; } + } ``` -PS:这就是普通双向链表的实现,为了让读者集中精力理解 LRU 算法的逻辑,就省略链表的具体代码。 - -到这里就能回答刚才“为什么必须要用双向链表”的问题了,因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 $O(1)$。 - -有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可。我们先把逻辑理清楚: - -```java -// key 映射到 Node(key, val) -HashMap map; -// Node(k1, v1) <-> Node(k2, v2)... -DoubleList cache; - -int get(int key) { - if (key 不存在) { - return -1; - } else { - 将数据 (key, val) 提到开头; - return val; - } -} +到这里就能回答刚才「为什么必须要用双向链表」的问题了,因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。 -void put(int key, int val) { - Node x = new Node(key, val); - if (key 已存在) { - 把旧的数据删除; - 将新节点 x 插入到开头; - } else { - if (cache 已满) { - 删除链表的最后一个数据腾位置; - 删除 map 中映射到该数据的键; - } - 将新节点 x 插入到开头; - map 中新建 key 对新节点 x 的映射; - } -} -``` +**注意我们实现的双链表 API 只能从尾部插入,也就是说靠尾部的数据是最近使用的,靠头部的数据是最久为使用的**。 -如果能够看懂上述逻辑,翻译成代码就很容易理解了: +有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可,先搭出代码框架: ```java class LRUCache { @@ -167,186 +200,148 @@ class LRUCache { map = new HashMap<>(); cache = new DoubleList(); } - - public int get(int key) { - if (!map.containsKey(key)) - return -1; - int val = map.get(key).val; - // 利用 put 方法把该数据提前 - put(key, val); - return val; - } - - public void put(int key, int val) { - // 先把新节点 x 做出来 - Node x = new Node(key, val); - - if (map.containsKey(key)) { - // 删除旧的节点,新的插到头部 - cache.remove(map.get(key)); - cache.addFirst(x); - // 更新 map 中对应的数据 - map.put(key, x); - } else { - if (cap == cache.size()) { - // 删除链表最后一个数据 - Node last = cache.removeLast(); - map.remove(last.key); - } - // 直接添加到头部 - cache.addFirst(x); - map.put(key, x); - } - } +``` + +先不慌去实现 LRU 算法的 `get` 和 `put` 方法。由于我们要同时维护一个双链表 `cache` 和一个哈希表 `map`,很容易漏掉一些操作,比如说删除某个 `key` 时,在 `cache` 中删除了对应的 `Node`,但是却忘记在 `map` 中删除 `key`。 + +**解决这种问题的有效方法是:在这两种数据结构之上提供一层抽象 API**。 + +说的有点玄幻,实际上很简单,就是尽量让 LRU 的主方法 `get` 和 `put` 避免直接操作 `map` 和 `cache` 的细节。我们可以先实现下面几个函数: + +```java +/* 将某个 key 提升为最近使用的 */ +private void makeRecently(int key) { + Node x = map.get(key); + // 先从链表中删除这个节点 + cache.remove(x); + // 重新插到队尾 + cache.addLast(x); +} + +/* 添加最近使用的元素 */ +private void addRecently(int key, int val) { + Node x = new Node(key, val); + // 链表尾部就是最近使用的元素 + cache.addLast(x); + // 别忘了在 map 中添加 key 的映射 + map.put(key, x); +} + +/* 删除某一个 key */ +private void deleteKey(int key) { + Node x = map.get(key); + // 从链表中删除 + cache.remove(x); + // 从 map 中删除 + map.remove(key); +} + +/* 删除最久未使用的元素 */ +private void removeLeastRecently() { + // 链表头部的第一个元素就是最久未使用的 + Node deletedNode = cache.removeFirst(); + // 同时别忘了从 map 中删除它的 key + int deletedKey = deletedNode.key; + map.remove(deletedKey); } ``` -这里就能回答之前的问答题“为什么要在链表中同时存储 key 和 val,而不是只存储 val”,注意这段代码: +这里就能回答之前的问答题「为什么要在链表中同时存储 key 和 val,而不是只存储 val」,注意 `removeLeastRecently` 函数中,我们需要用 `deletedNode` 得到 `deletedKey`。 + +也就是说,当缓存容量已满,我们不仅仅要删除最后一个 `Node` 节点,还要把 `map` 中映射到该节点的 `key` 同时删除,而这个 `key` 只能由 `Node` 得到。如果 `Node` 结构中只存储 `val`,那么我们就无法得知 `key` 是什么,就无法删除 `map` 中的键,造成错误。 + +上述方法就是简单的操作封装,调用这些函数可以避免直接操作 `cache` 链表和 `map` 哈希表,下面我先来实现 LRU 算法的 `get` 方法: ```java -if (cap == cache.size()) { - // 删除链表最后一个数据 - Node last = cache.removeLast(); - map.remove(last.key); +public int get(int key) { + if (!map.containsKey(key)) { + return -1; + } + // 将该数据提升为最近使用的 + makeRecently(key); + return map.get(key).val; } ``` -当缓存容量已满,我们不仅仅要删除最后一个 Node 节点,还要把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 Node 得到。如果 Node 结构中只存储 val,那么我们就无法得知 key 是什么,就无法删除 map 中的键,造成错误。 +`put` 方法稍微复杂一些,我们先来画个图搞清楚它的逻辑: -至此,你应该已经掌握 LRU 算法的思想和实现了,很容易犯错的一点是:处理链表节点的同时不要忘了更新哈希表中对节点的映射。 +![](../pictures/LRU算法/put.jpg) -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: +这样我们可以轻松写出 `put` 方法的代码: -![labuladong](../pictures/labuladong.png) +```java +public void put(int key, int val) { + if (map.containsKey(key)) { + // 删除旧的数据 + deleteKey(key); + // 新插入的数据为最近使用的数据 + addRecently(key, val); + return; + } + + if (cap == cache.size()) { + // 删除最久未使用的元素 + removeLeastRecently(); + } + // 添加为最近使用的元素 + addRecently(key, val); +} +``` -[yuxiang-zhang](https://github.com/yuxiang-zhang) 提供 C++ 代码: +至此,你应该已经完全掌握 LRU 算法的原理和实现了,我们最后用 Java 的内置类型 `LinkedHashMap` 来实现 LRU 算法,逻辑和之前完全一致,我就不过多解释了: -```cpp +```java class LRUCache { - // key -> iterator to pair(key, val) in the list - unordered_map>::iterator> map; - - // pair(k1, v1) <-> pair(k2, v2)... - list> cache; - - // 最大容量 int cap; -public: - LRUCache(int capacity) : cap(capacity) {} + LinkedHashMap cache = new LinkedHashMap<>(); + public LRUCache(int capacity) { + this.cap = capacity; + } - int get(int key) { - if(map.find(key) == map.end()) { + public int get(int key) { + if (!cache.containsKey(key)) { return -1; } - - int val = map[key]->second; - - // 利用 put 方法把该数据提前 - put(key, val); - - return val; + // 将 key 变为最近使用 + makeRecently(key); + return cache.get(key); } - void put(int key, int value) { - pair x = {key, value}; + public void put(int key, int val) { + if (cache.containsKey(key)) { + // 修改 key 的值 + cache.put(key, val); + // 将 key 变为最近使用 + makeRecently(key); + return; + } - if(map.find(key) != map.end()) { - - // 删除旧的节点 - cache.erase(map[key]); - // 新的插到头部 - cache.emplace_front(x); - - // 更新 map 中对应的数据 - map[key] = cache.begin(); - - } else { - - if(cap == cache.size()) { - // 删除链表最后一个数据 - pair last = cache.back(); cache.pop_back(); - - map.erase(last.first); - } - - // 直接添加到头部 - cache.emplace_front(x); - map[key] = cache.begin(); - + if (cache.size() >= this.cap) { + // 链表头部就是最久未使用的 key + int oldestKey = cache.keySet().iterator().next(); + cache.remove(oldestKey); } + // 将新的 key 添加链表尾部 + cache.put(key, val); } -}; + + private void makeRecently(int key) { + int val = cache.get(key); + // 删除 key,重新插入到队尾 + cache.remove(key); + cache.put(key, val); + } +} ``` -[eric wang](https://www.github.com/eric496) 提供 Python3 代码 - -```python -class ListNode: - def __init__(self, key: int, val: int): - self.key = key - self.val = val - self.prev = None - self.next = None - - -class LRUCache: - def __init__(self, capacity: int): - # 最大容量 - self.cap = capacity - self.cache = {} - # 哨兵节点 - self.sentinel = ListNode(None, None) - # 尾节点: 用于链表容量超过最大容量是快速定位、删除尾节点 - self.tail = ListNode(None, None) - # 初始化双向链表 - self.sentinel.next = self.tail - self.tail.prev = self.sentinel - - def get(self, key: int) -> int: - if key in self.cache: - node = self.cache[key] - # 从链表中删除该节点 - self.remove_node_from_list(node) - # 把该节点添加到链表头部 - self.push_node_to_front(node) - return node.val - else: - return -1 - - def put(self, key: int, value: int) -> None: - # 如果该节点已经存在那么删除该节点 - if key in self.cache: - self.remove_node_from_list(self.cache[key]) - - # 把该节点添加到链表头部 - node = ListNode(key, value) - self.cache[key] = node - self.push_node_to_front(node) - - # 如果链表超过最大容量,删除链表尾部节点 - if len(self.cache) > self.cap: - last_node = self.tail.prev - self.remove_node_from_list(last_node) - self.cache.pop(last_node.key) - - # 从链表中删除节点 - def remove_node_from_list(self, node: "ListNode") -> None: - prev = node.prev - nxt = node.next - prev.next = nxt - nxt.prev = prev - - # 添加节点到链表头部 - def push_node_to_front(self, node: "ListNode") -> None: - nxt = self.sentinel.next - self.sentinel.next = node - node.next = nxt - node.prev = self.sentinel - nxt.prev = node -``` +至此,LRU 算法就没有什么神秘的了,**敬请期待下文:LFU 算法拆解与实现**。 + +**_____________** -[上一篇:二叉堆详解实现优先级队列](../数据结构系列/二叉堆详解实现优先级队列.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:二叉搜索树操作集锦](../数据结构系列/二叉搜索树操作集锦.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/README.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/README.md" index 5c31a12770..3e9823392d 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/README.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/README.md" @@ -2,6 +2,6 @@ 8 说了,本章都是高频面试题,配合前面的动态规划系列,祝各位马到成功! -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: +欢迎关注我的公众号 labuladong,查看全部文章: ![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/koko\345\201\267\351\246\231\350\225\211.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/koko\345\201\267\351\246\231\350\225\211.md" index 13b32db912..c4cb83085e 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/koko\345\201\267\351\246\231\350\225\211.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/koko\345\201\267\351\246\231\350\225\211.md" @@ -1,5 +1,27 @@ # 如何运用二分查找算法 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [如何运用贪心思想玩跳跃游戏](https://labuladong.gitbook.io/algo) + * [如何寻找最长回文子串](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[875.爱吃香蕉的珂珂](https://leetcode-cn.com/problems/koko-eating-bananas) + +[1011.在D天内送达包裹的能力](https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days) + +**-----------** + 二分查找到底有能运用在哪里? 最常见的就是教科书上的例子,在**有序数组**中搜索给定的某个目标值的索引。再推广一点,如果目标值存在重复,修改版的二分查找可以返回目标值的左侧边界索引或者右侧边界索引。 @@ -137,13 +159,12 @@ for (int i = 0; i < n; i++) return ans; ``` -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:如何计算编辑距离](../动态规划系列/编辑距离.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何高效解决接雨水问题](../高频面试系列/接雨水.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" index a637312896..7509480a39 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" @@ -1,5 +1,25 @@ # 如何k个一组反转链表 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [区间调度之区间交集问题](https://labuladong.gitbook.io/algo) + * [动态规划和回溯算法到底谁是谁爹?](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[25.K个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group) + +**-----------** + 之前的文章「递归反转链表的一部分」讲了如何递归地反转一部分链表,有读者就问如何迭代地反转链表,这篇文章解决的问题也需要反转链表的函数,我们不妨就用迭代方式来解决。 本文要解决「K 个一组反转链表」,不难理解: @@ -12,7 +32,7 @@ ### 一、分析问题 -首先,前文[学习数据结构的框架思维](../算法思维系列/学习数据结构和算法的框架思维.md)提到过,链表是一种兼具递归和迭代性质的数据结构,认真思考一下可以发现**这个问题具有递归性质**。 +首先,前文[学习数据结构的框架思维](https://labuladong.gitbook.io/algo)提到过,链表是一种兼具递归和迭代性质的数据结构,认真思考一下可以发现**这个问题具有递归性质**。 什么叫递归性质?直接上图理解,比如说我们对这个链表调用 `reverseKGroup(head, 2)`,即以 2 个节点为一组反转链表: @@ -71,7 +91,7 @@ ListNode reverse(ListNode a) { 「反转以 `a` 为头结点的链表」其实就是「反转 `a` 到 null 之间的结点」,那么如果让你「反转 `a` 到 `b` 之间的结点」,你会不会? 只要更改函数签名,并把上面的代码中 `null` 改成 `b` 即可: -[labuladong](https://github.com/labuladong) 提供Java解法代码: + ```java /** 反转区间 [a, b) 的元素,注意是左闭右开 */ ListNode reverse(ListNode a, ListNode b) { @@ -88,23 +108,9 @@ ListNode reverse(ListNode a, ListNode b) { return pre; } ``` -[renxiaoyao](https://github.com/tianzhongwei) 提供C++解法代码: -```C++ -ListNode* reverse(ListNode* begin,ListNode* end) { - ListNode* newHead = nullptr; - ListNode* cur = begin; - while(cur != end) { - ListNode* next = cur->next; - cur->next = newHead; - newHead = cur; - cur = next; - } - return newHead; -} -``` 现在我们迭代实现了反转部分链表的功能,接下来就按照之前的逻辑编写 `reverseKGroup` 函数即可: -[labuladong](https://github.com/labuladong) 提供Java解法代码: + ```java ListNode reverseKGroup(ListNode head, int k) { if (head == null) return null; @@ -123,37 +129,7 @@ ListNode reverseKGroup(ListNode head, int k) { return newHead; } ``` -[renxiaoyao](https://github.com/tianzhongwei) 提供C++解法代码: -```C++ -class Solution { -public: - ListNode* reverseKGroup(ListNode* head, int k) { - if(!head) return head; - ListNode* begin = head; - ListNode* end = head; - for(int i = 0 ; i < k ; ++i) { - if(!end) - return head; - end = end->next; - } - ListNode* newHead = reverse(begin,end); - begin->next = reverseKGroup(end,k); - return newHead; - } -private: - ListNode* reverse(ListNode* begin,ListNode* end) { - ListNode* newHead = nullptr; - ListNode* cur = begin; - while(cur != end) { - ListNode* next = cur->next; - cur->next = newHead; - newHead = cur; - cur = next; - } - return newHead; - } -}; -``` + 解释一下 `for` 循环之后的几句代码,注意 `reverse` 函数是反转区间 `[a, b)`,所以情形是这样的: ![](../pictures/kgroup/6.jpg) @@ -170,56 +146,12 @@ private: 那么如何分解问题、发现递归性质呢?这个只能多练习,也许后续可以专门写一篇文章来探讨一下,本文就到此为止吧,希望对大家有帮助! -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - - - -[KAGAWA317](https://github.com/KAGAWA317) 提供Python3解法代码: - -```python -# 反转区间 [a, b) 的元素 -def reverse(a, b): - pre = None - cur = a - while cur != b: - cur.next, pre, cur = pre, cur, cur.next - return pre -``` - -[KAGAWA317](https://github.com/KAGAWA317) 提供Python3解法代码: - -```python -class Solution: - def reverseKGroup(self, head: ListNode, k: int) -> ListNode: - if not head: - return - # 区间 [a, b) 包含 k 个待反转元素 - a = b = head - for _ in range(k): - # 不足 k 个,不需要反转,base case - if not b: - return head - b = b.next - - # 反转区间 [a, b) 的元素 - def reverse(a, b): - pre = None - cur = a - while cur != b: - cur.next, pre, cur = pre, cur, cur.next - return pre - - # 反转前 k 个元素 - newHead = reverse(a, b) - # 递归反转后续链表并连接起来 - a.next = self.reverseKGroup(b, k) - return newHead -``` +**_____________** -[上一篇:如何寻找最长回文子串](../高频面试系列/最长回文子串.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何判定括号合法性](../高频面试系列/合法括号判定.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" index 5f70d39d78..4ca0b6de4d 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" @@ -1,5 +1,29 @@ # 一行代码就能解决的算法题 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [学习算法和数据结构的思路指南](https://labuladong.gitbook.io/algo) + * [我用四个命令概括了 Git 的所有套路](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[292.Nim游戏](https://leetcode-cn.com/problems/nim-game) + +[877.石子游戏](https://leetcode-cn.com/problems/stone-game) + +[319.灯泡开关](https://leetcode-cn.com/problems/bulb-switcher) + +**-----------** + 下文是我在 LeetCode 刷题过程中总结的三道有趣的「脑筋急转弯」题目,可以使用算法编程解决,但只要稍加思考,就能找到规律,直接想出答案。 ### 一、Nim 游戏 @@ -101,27 +125,26 @@ int bulbSwitch(int n) { 我们假设只有 6 盏灯,而且我们只看第 6 盏灯。需要进行 6 轮操作对吧,请问对于第 6 盏灯,会被按下几次开关呢?这不难得出,第 1 轮会被按,第 2 轮,第 3 轮,第 6 轮都会被按。 -为什么第 1、2、3、6 轮会被按呢?因为 `6=1x6=2x3`。一般情况下,因子都是成对出现的,也就是说开关被按的次数一般是偶数次。但是有特殊情况,比如说总共有 16 盏灯,那么第 16 盏灯会被按几次? +为什么第 1、2、3、6 轮会被按呢?因为 `6=1*6=2*3`。一般情况下,因子都是成对出现的,也就是说开关被按的次数一般是偶数次。但是有特殊情况,比如说总共有 16 盏灯,那么第 16 盏灯会被按几次? -`16=1x16=2x8=4x4` +`16=1*16=2*8=4*4` 其中因子 4 重复出现,所以第 16 盏灯会被按 5 次,奇数次。现在你应该理解这个问题为什么和平方根有关了吧? 不过,我们不是要算最后有几盏灯亮着吗,这样直接平方根一下是啥意思呢?稍微思考一下就能理解了。 -就假设现在总共有 16 盏灯,我们求 16 的平方根,等于 4,这就说明最后会有 4 盏灯亮着,它们分别是第 `1x1=1` 盏、第 `2x2=4` 盏、第 `3x3=9` 盏和第 `4x4=16` 盏。 +就假设现在总共有 16 盏灯,我们求 16 的平方根,等于 4,这就说明最后会有 4 盏灯亮着,它们分别是第 `1*1=1` 盏、第 `2*2=4` 盏、第 `3*3=9` 盏和第 `4*4=16` 盏。 就算有的 n 平方根结果是小数,强转成 int 型,也相当于一个最大整数上界,比这个上界小的所有整数,平方后的索引都是最后亮着的灯的索引。所以说我们直接把平方根转成整数,就是这个问题的答案。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:Union-Find算法应用](../算法思维系列/UnionFind算法应用.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:二分查找高效判定子序列](../高频面试系列/二分查找判定子序列.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" index 4c18690380..797c09836a 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" @@ -1,6 +1,26 @@ # 二分查找高效判定子序列 -二分查找本身不难理解,难在巧妙地运用二分查找技巧。对于一个问题,你可能都很难想到它跟二分查找有关,比如前文 [最长递增子序列](../动态规划系列/动态规划设计:最长递增子序列.md) 就借助一个纸牌游戏衍生出二分查找解法。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [twoSum问题的核心思想](https://labuladong.gitbook.io/algo) + * [经典动态规划:子集背包问题](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[392.判断子序列](https://leetcode-cn.com/problems/is-subsequence) + +**-----------** + +二分查找本身不难理解,难在巧妙地运用二分查找技巧。对于一个问题,你可能都很难想到它跟二分查找有关,比如前文 [最长递增子序列](https://labuladong.gitbook.io/algo) 就借助一个纸牌游戏衍生出二分查找解法。 今天再讲一道巧用二分查找的算法问题:如何判定字符串 `s` 是否是字符串 `t` 的子序列(可以假定 `s` 长度比较小,且 `t` 的长度非常大)。举两个例子: @@ -75,7 +95,7 @@ for (int i = 0; i < n; i++) { ### 三、再谈二分查找 -在前文 [二分查找详解](../算法思维系列/二分查找详解.md) 中,详解了如何正确写出三种二分查找算法的细节。二分查找返回目标值 `val` 的索引,对于搜索**左侧边界**的二分查找,有一个特殊性质: +在前文 [二分查找详解](https://labuladong.gitbook.io/algo) 中,详解了如何正确写出三种二分查找算法的细节。二分查找返回目标值 `val` 的索引,对于搜索**左侧边界**的二分查找,有一个特殊性质: **当 `val` 不存在时,得到的索引恰好是比 `val` 大的最小元素索引**。 @@ -138,13 +158,12 @@ boolean isSubsequence(String s, String t) { 可见借助二分查找,算法的效率是可以大幅提升的。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:一行代码就能解决的算法题](../高频面试系列/一行代码解决的智力题.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:Linux的进程、线程、文件描述符是什么](../技术/linux进程.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" index 5a280b0d5d..d25823dd5d 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" @@ -1,3 +1,25 @@ +# 如何高效判断回文链表 + + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [如何高效进行模幂运算](https://labuladong.gitbook.io/algo) + * [一文学会递归解题](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[234.回文链表](https://leetcode-cn.com/problems/palindrome-linked-list) + +**-----------** + 我们之前有两篇文章写了回文串和回文序列相关的问题。 **寻找**回文串的核心思想是从中心向两端扩展: @@ -205,13 +227,12 @@ p.next = reverse(q); 具体到回文链表的判断问题,由于回文的特殊性,可以不完全反转链表,而是仅仅反转部分链表,将空间复杂度降到 O(1)。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:如何寻找缺失和重复的元素](../高频面试系列/缺失和重复的元素.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何在无限序列中随机抽取元素](../高频面试系列/水塘抽样.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" index f56d9fb1cd..ef9cd34f4c 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" @@ -1,5 +1,25 @@ # 如何判定括号合法性 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [回溯算法解题套路框架](https://labuladong.gitbook.io/algo) + * [Linux shell 的实用小技巧](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[20.有效的括号](https://leetcode-cn.com/problems/valid-parentheses) + +**-----------** + 对括号的合法性判断是一个很常见且实用的问题,比如说我们写的代码,编辑器和编译器都会检查括号是否正确闭合。而且我们的代码可能会包含三种括号 `[](){}`,判断起来有一点难度。 本文就来聊一道关于括号合法性判断的算法题,相信能加深你对**栈**这种数据结构的理解。 @@ -45,6 +65,7 @@ bool isValid(string str) { return left == 0; } ``` + 如果只有圆括号,这样就能正确判断合法性。对于三种括号的情况,我一开始想模仿这个思路,定义三个变量 `left1`,`left2`,`left3` 分别处理每种括号,虽然要多写不少 if else 分支,但是似乎可以解决问题。 但实际上直接照搬这种思路是不行的,比如说只有一个括号的情况下 `(())` 是合法的,但是多种括号的情况下, `[(])` 显然是不合法的。 @@ -81,50 +102,12 @@ char leftOf(char c) { } ``` -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - -[张三](any_link_you_want) 提供 Java 代码: - -```java -public boolean isValid(String str) { - // ... -} - -private char leftOf(char c) { - // ... -} -``` - -[kalok87](https://github.com/kalok87) 提供 Python3 代码: - -```python -def isValid(self, s: str): - left = [] # 定义一个左栈,记录所有的左括号 - match = {'}':'{', ']':'[', ')':'('} # 定义一个字典,检查当前str是否是右括号 - right = {'}', ']', ')'} # 定义一个右括号集合,方便快速检查 - - # 进行循环,如果当前str是左括号,则入栈;如果是右括号,则检查左栈的最后一个元素是不是 - # 与其对应。 - for x in s: - if x in right: - if len(left) == 0 or match[x] != left[-1]: - return(False) # 如果对应的左栈元素不符(括号种类不同或左栈为空),返回False - else: - left.pop() # 移除左栈最顶端的元素 - else: - left.append(x) # 当前str是左括号,入左栈 - - if len(left) == 0: - return(True) # 如果左栈为空(左右括号数相等),返回True - else: - return(False) -``` - +**_____________** -[上一篇:如何k个一组反转链表](../高频面试系列/k个一组反转链表.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何寻找消失的元素](../高频面试系列/消失的元素.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\246\202\344\275\225\345\216\273\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\246\202\344\275\225\345\216\273\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" index 3e1cd14721..e159228312 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\246\202\344\275\225\345\216\273\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\246\202\344\275\225\345\216\273\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" @@ -108,8 +108,8 @@ def deleteDuplicates(self, head: ListNode) -> ListNode: return head ``` -[上一篇:如何高效解决接雨水问题](../高频面试系列/接雨水.md) +[上一篇:如何高效解决接雨水问题](https://labuladong.gitbook.io/algo) -[下一篇:如何寻找最长回文子串](../高频面试系列/最长回文子串.md) +[下一篇:如何寻找最长回文子串](https://labuladong.gitbook.io/algo) -[目录](../README.md#目录) +[目录](https://labuladong.gitbook.io/algo#目录) diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" index 41bff4bcae..0d5f905d4e 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" @@ -1,3 +1,30 @@ +# 回溯算法团灭子集、排列、组合问题 + +**学好算法全靠套路,认准 labuladong 就够了**。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [回溯算法解题套路框架](https://labuladong.gitbook.io/algo) + * [twoSum问题的核心思想](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[78.子集](https://leetcode-cn.com/problems/subsets) + +[46.全排列](https://leetcode-cn.com/problems/permutations) + +[77.组合](https://leetcode-cn.com/problems/combinations) + +**-----------** + 今天就来聊三道考察频率高,而且容易让人搞混的算法问题,分别是求子集(subset),求排列(permutation),求组合(combination)。 这几个问题都可以用回溯算法模板解决,同时子集问题还可以用数学归纳思想解决。读者可以记住这几个问题的回溯套路,就不怕搞不清了。 @@ -199,7 +226,7 @@ vector> permute(vector& nums); ![](../pictures/子集/3.jpg) 我们当时使用 Java 代码写的解法: -[labuladong](https://github.com/labuladong) 提供Java解法代码: + ```java List> res = new LinkedList<>(); @@ -231,44 +258,6 @@ void backtrack(int[] nums, LinkedList track) { } } ``` -[renxiaoyao](https://github.com/tianzhongwei) 提供C++解法代码: -```C++ -class Solution { -public: - vector> permute(vector& nums) { - paths.clear(); - path.clear(); - - vector used(nums.size(),false); - - helper(nums,used); - - return paths; - } -private: - void helper(vector& nums,vector& used) { - if(path.size() == nums.size()) { - paths.push_back(path); - return ; - } - - for(int i = 0 ; i < nums.size() ; ++i) { - if(used[i]) continue; - - used[i] = true; - path.push_back(nums[i]); - - helper(nums,used); - - path.pop_back(); - used[i] = false; - } - } -private: - vector> paths; - vector path; -}; -``` 回溯模板依然没有变,但是根据排列问题和组合问题画出的树来看,排列问题的树比较对称,而组合问题的树越靠右节点越少。 @@ -284,13 +273,13 @@ private: 记住这几种树的形状,就足以应对大部分回溯算法问题了,无非就是 `start` 或者 `contains` 剪枝,也没啥别的技巧了。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) +**_____________** -[上一篇:回溯算法详解](../算法思维系列/回溯算法详解修订版.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:二分查找详解](../算法思维系列/二分查找详解.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" index 8360e672d7..f2b0306ac9 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" @@ -1,5 +1,25 @@ # 如何调度考生的座位 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [一个方法团灭 LeetCode 股票买卖问题](https://labuladong.gitbook.io/algo) + * [Linux shell 的实用小技巧](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[855.考场就座](https://leetcode-cn.com/problems/exam-room) + +**-----------** + 这是 LeetCode 第 855 题,有趣且具有一定技巧性。这种题目并不像动态规划这类算法拼智商,而是看你对常用数据结构的理解和写代码的水平,个人认为值得重视和学习。 另外说句题外话,很多读者都问,算法框架是如何总结出来的,其实框架反而是慢慢从细节里抠出来的。希望大家看了我们的文章之后,最好能抽时间把相关的问题亲自做一做,纸上得来终觉浅,绝知此事要躬行嘛。 @@ -203,13 +223,12 @@ private int distance(int[] intv) { 希望本文对大家有帮助。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:如何在无限序列中随机抽取元素](../高频面试系列/水塘抽样.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:Union-Find算法详解](../算法思维系列/UnionFind算法详解.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" index 11420b478e..6ecb9c42f8 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" @@ -1,5 +1,25 @@ # 如何高效寻找素数 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [算法就像搭乐高:带你手撸 LRU 算法](https://labuladong.gitbook.io/algo) + * [区间调度之区间合并问题](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[204.计数质数](https://leetcode-cn.com/problems/count-primes) + +**-----------** + 素数的定义看起来很简单,如果一个数如果只能被 1 和它本身整除,那么这个数就是素数。 不要觉得素数的定义简单,恐怕没多少人真的能把素数相关的算法写得高效。比如让你写这样一个函数: @@ -146,35 +166,14 @@ int countPrimes(int n) { 以上就是素数算法相关的全部内容。怎么样,是不是看似简单的问题却有不少细节可以打磨呀? -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) - -[ruicore](https://github.com/ruicore/algorithm) 提供 Python3 代码: -```py -class Solution: - def countPrimes(self, n: int): - # 前 2 个数不是素数 - if n < 3: - return 0 - # isprime 数组 - isprime = [1] * n - for i in range(2, int(n ** 0.5) + 1): - if isprime[i]: - # 从 i*i(包括自己) 到 n(不包括n),每步增加 i 的所有数的个数 - tmp = ((n - 1 - i * i) // i + 1) - # 这种方式赋值比用 for 循环更高效 - isprime[i * i: n: i] = [0] * tmp - - # 前 2 个数不是素数,去掉 - count = sum(isprime[2:]) - return count -``` +**_____________** -[上一篇:如何实现LRU算法](../高频面试系列/LRU算法.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何计算编辑距离](../动态规划系列/编辑距离.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" index 3104e2198c..0b3e1e0515 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" @@ -1,5 +1,25 @@ # 接雨水问题详解 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [手把手带你刷二叉树(第三期)](https://labuladong.gitbook.io/algo) + * [45张图解:IP基础知识全家桶](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[42.接雨水](https://leetcode-cn.com/problems/trapping-rain-water) + +**-----------** + 接雨水这道题目挺有意思,在面试题中出现频率还挺高的,本文就来步步优化,讲解一下这道题。 先看一下题目: @@ -16,17 +36,17 @@ int trap(int[] height); ### 一、核心思路 -我第一次看到这个问题,无计可施,完全没有思路,相信很多朋友跟我一样。所以对于这种问题,我们不要想整体,而应该去想局部;就像之前的文章处理字符串问题,不要考虑如何处理整个字符串,而是去思考应该如何处理每一个字符。 +所以对于这种问题,我们不要想整体,而应该去想局部;就像之前的文章写的动态规划问题处理字符串问题,不要考虑如何处理整个字符串,而是去思考应该如何处理每一个字符。 -这么一想,可以发现这道题的思路其实很简单。具体来说,仅仅对于位置 i,能装下多少水呢? +这么一想,可以发现这道题的思路其实很简单。具体来说,仅仅对于位置 `i`,能装下多少水呢? ![](../pictures/接雨水/0.jpg) -能装 2 格水。为什么恰好是两格水呢?因为 height[i] 的高度为 0,而这里最多能盛 2 格水,2-0=2。 +能装 2 格水,因为 `height[i]` 的高度为 0,而这里最多能盛 2 格水,2-0=2。 -为什么位置 i 最多能盛 2 格水呢?因为,位置 i 能达到的水柱高度和其左边的最高柱子、右边的最高柱子有关,我们分别称这两个柱子高度为 `l_max` 和 `r_max`;**位置 i 最大的水柱高度就是 `min(l_max, r_max)`。** +为什么位置 `i` 最多能盛 2 格水呢?因为,位置 `i` 能达到的水柱高度和其左边的最高柱子、右边的最高柱子有关,我们分别称这两个柱子高度为 `l_max` 和 `r_max`;**位置 i 最大的水柱高度就是 `min(l_max, r_max)`。** -更进一步,对于位置 i,能够装的水为: +更进一步,对于位置 `i`,能够装的水为: ```python water[i] = min( @@ -47,7 +67,7 @@ water[i] = min( ```cpp int trap(vector& height) { int n = height.size(); - int ans = 0; + int res = 0; for (int i = 1; i < n - 1; i++) { int l_max = 0, r_max = 0; // 找右边最高的柱子 @@ -58,9 +78,9 @@ int trap(vector& height) { l_max = max(l_max, height[j]); // 如果自己就是最高的话, // l_max == r_max == height[i] - ans += min(l_max, r_max) - height[i]; + res += min(l_max, r_max) - height[i]; } - return ans; + return res; } ``` @@ -68,15 +88,15 @@ int trap(vector& height) { ### 二、备忘录优化 -之前的暴力解法,不是在每个位置 i 都要计算 `r_max` 和 `l_max` 吗?我们直接把结果都缓存下来,别傻不拉几的每次都遍历,这时间复杂度不就降下来了嘛。 +之前的暴力解法,不是在每个位置 `i` 都要计算 `r_max` 和 `l_max` 吗?我们直接把结果都提前计算出来,别傻不拉几的每次都遍历,这时间复杂度不就降下来了嘛。 -我们开两个**数组** `r_max` 和 `l_max` 充当备忘录,`l_max[i]` 表示位置 i 左边最高的柱子高度,`r_max[i]` 表示位置 i 右边最高的柱子高度。预先把这两个数组计算好,避免重复计算: +**我们开两个数组 `r_max` 和 `l_max` 充当备忘录,`l_max[i]` 表示位置 `i` 左边最高的柱子高度,`r_max[i]` 表示位置 `i` 右边最高的柱子高度**。预先把这两个数组计算好,避免重复计算: ```cpp int trap(vector& height) { if (height.empty()) return 0; int n = height.size(); - int ans = 0; + int res = 0; // 数组充当备忘录 vector l_max(n), r_max(n); // 初始化 base case @@ -90,12 +110,12 @@ int trap(vector& height) { r_max[i] = max(height[i], r_max[i + 1]); // 计算答案 for (int i = 1; i < n - 1; i++) - ans += min(l_max[i], r_max[i]) - height[i]; - return ans; + res += min(l_max[i], r_max[i]) - height[i]; + return res; } ``` -这个优化其实和暴力解法差不多,就是避免了重复计算,把时间复杂度降低为 O(N),已经是最优了,但是空间复杂度是 O(N)。下面来看一个精妙一些的解法,能够把空间复杂度降低到 O(1)。 +这个优化其实和暴力解法思路差不多,就是避免了重复计算,把时间复杂度降低为 O(N),已经是最优了,但是空间复杂度是 O(N)。下面来看一个精妙一些的解法,能够把空间复杂度降低到 O(1)。 ### 三、双指针解法 @@ -130,7 +150,7 @@ int trap(vector& height) { if (height.empty()) return 0; int n = height.size(); int left = 0, right = n - 1; - int ans = 0; + int res = 0; int l_max = height[0]; int r_max = height[n - 1]; @@ -139,25 +159,25 @@ int trap(vector& height) { l_max = max(l_max, height[left]); r_max = max(r_max, height[right]); - // ans += min(l_max, r_max) - height[i] + // res += min(l_max, r_max) - height[i] if (l_max < r_max) { - ans += l_max - height[left]; + res += l_max - height[left]; left++; } else { - ans += r_max - height[right]; + res += r_max - height[right]; right--; } } - return ans; + return res; } ``` 你看,其中的核心思想和之前一模一样,换汤不换药。但是细心的读者可能会发现次解法还是有点细节差异: -之前的备忘录解法,`l_max[i]` 和 `r_max[i]` 代表的是 `height[0..i]` 和 `height[i..end]` 的最高柱子高度。 +之前的备忘录解法,`l_max[i]` 和 `r_max[i]` 分别代表 `height[0..i]` 和 `height[i..end]` 的最高柱子高度。 ```cpp -ans += min(l_max[i], r_max[i]) - height[i]; +res += min(l_max[i], r_max[i]) - height[i]; ``` ![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/3.jpg) @@ -166,7 +186,7 @@ ans += min(l_max[i], r_max[i]) - height[i]; ```cpp if (l_max < r_max) { - ans += l_max - height[left]; + res += l_max - height[left]; left++; } ``` @@ -175,161 +195,18 @@ if (l_max < r_max) { 此时的 `l_max` 是 `left` 指针左边的最高柱子,但是 `r_max` 并不一定是 `left` 指针右边最高的柱子,这真的可以得到正确答案吗? -其实这个问题要这么思考,我们只在乎 `min(l_max, r_max)`。对于上图的情况,我们已经知道 `l_max < r_max` 了,至于这个 `r_max` 是不是右边最大的,不重要,重要的是 `height[i]` 能够装的水只和 `l_max` 有关。 +其实这个问题要这么思考,我们只在乎 `min(l_max, r_max)`。**对于上图的情况,我们已经知道 `l_max < r_max` 了,至于这个 `r_max` 是不是右边最大的,不重要。重要的是 `height[i]` 能够装的水只和较低的 `l_max` 之差有关**: ![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/5.jpg) -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) +这样,接雨水问题就解决了。 -[newler](https://me.csdn.net/qq_28749335)提供java代码: - -暴力解法 - -```java -public int trap(int[] height) { - int ans = 0; - for (int i = 1; i < height.length - 1; i++) { - int leftMax = 0, rightMax = 0; - // 找右边最高的柱子 - for (int j = i; j < height.length; j++) { - rightMax = Math.max(height[j], rightMax); - } - // 找左边最高的柱子 - for (int j = i; j >= 0; j--) { - leftMax = Math.max(height[j], leftMax); - } - // 如果自己就是最高的话, - // leftMax == rightMax == height[i] - ans += Math.min(leftMax, rightMax) - height[i]; - } - return ans; -} -``` - -备忘录优化解法 - -```java -public int trap(int[] height) { - if (height == null || height.length == 0) return 0; - int ans = 0; - // 数组充当备忘录 - int[] leftMax = new int[height.length]; - int[] rightMax = new int[height.length]; - // 初始化base case - leftMax[0] = height[0]; - rightMax[height.length - 1] = height[height.length - 1]; - // 从左到右计算leftMax - for (int i = 1; i < height.length; i++) { - leftMax[i] = Math.max(height[i], leftMax[i-1]); - } - // 从右到左计算rightMax - for (int i = height.length - 2; i >= 0; i--) { - rightMax[i] = Math.max(height[i], rightMax[i + 1]); - } - // 计算结果 - for (int i = 1; i < height.length - 1; i++) { - ans += Math.min(leftMax[i], rightMax[i]) - height[i]; - } - return ans; -} -``` - -双指针解法 - -```java -public int trap(int[] height) { - if (height == null || height.length == 0) return 0; - - int ans = 0; - int leftMax, rightMax; - // 左右指针 - int left = 0, right = height.length - 1; - // 初始化 - leftMax = height[0]; - rightMax = height[height.length - 1]; - - while (left < right) { - // 更新左右两边柱子最大值 - leftMax = Math.max(height[left], leftMax); - rightMax = Math.max(height[right], rightMax); - - // 相当于ans += Math.min(leftMax, rightMax) - height[i] - if (leftMax < rightMax) { - ans += leftMax - height[left]; - left++; - } else { - ans += rightMax - height[right]; - right--; - } - } - return ans; -} -``` - -[eric wang](https://www.github.com/eric496) 提供 Java 代码 - -```java -public int trap(int[] height) { - if (height.length == 0) { - return 0; - } - - int n = height.length; - int left = 0, right = n - 1; - int ans = 0; - - int l_max = height[0]; - int r_max = height[n - 1]; - - while (left <= right) { - l_max = Math.max(l_max, height[left]); - r_max = Math.max(r_max, height[right]); - - if (l_max < r_max) { - ans += l_max - height[left]; - left++; - } else { - ans += r_max - height[right]; - right--; - } - } - - return ans; -} -``` - -[eric wang](https://www.github.com/eric496) 提供 Python3 代码 - -```python -def trap(self, height: List[int]) -> int: - if not height: - return 0 - - n = len(height) - left, right = 0, n - 1 - ans = 0 - - l_max = height[0] - r_max = height[n - 1] - - while left <= right: - l_max = max(l_max, height[left]) - r_max = max(r_max, height[right]) - - if l_max < r_max: - ans += l_max - height[left] - left += 1 - else: - ans += r_max - height[right] - right -= 1 - - return ans -``` +**_____________** -[上一篇:如何运用二分查找算法](../高频面试系列/koko偷香蕉.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何去除有序数组的重复元素](../高频面试系列/如何去除有序数组的重复元素.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" index b4ea0c5d8e..5fd2691130 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" @@ -1,5 +1,26 @@ # 如何寻找最长回文子串 +**学好算法全靠套路,认准 labuladong 就够了**。 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [如何运用二分查找算法](https://labuladong.gitbook.io/algo) + * [动态规划答疑篇](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[5.最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring) + +**-----------** + 回文串是面试常常遇到的问题(虽然问题本身没啥意义),本文就告诉你回文串问题的核心思想是什么。 首先,明确一下什:**回文串就是正着读和反着读都一样的字符串**。 @@ -99,12 +120,13 @@ string longestPalindrome(string s) { 另外,这个问题还有一个巧妙的解法,时间复杂度只需要 O(N),不过该解法比较复杂,我个人认为没必要掌握。该算法的名字叫 Manacher's Algorithm(马拉车算法),有兴趣的读者可以自行搜索一下。 -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: -![labuladong](../pictures/labuladong.png) +**_____________** -[上一篇:如何去除有序数组的重复元素](../高频面试系列/如何去除有序数组的重复元素.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何k个一组反转链表](../高频面试系列/k个一组反转链表.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" index 0897e48f59..a9cb9ecc8c 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" @@ -1,3 +1,27 @@ +# 随机算法之水塘抽样算法 + + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [一文看懂 session 和 cookie](https://labuladong.gitbook.io/algo) + * [算法就像搭乐高:带你手撸 LFU 算法](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[382.链表随机节点](https://leetcode-cn.com/problems/linked-list-random-node) + +[398.随机数索引](https://leetcode-cn.com/problems/random-pick-index) + +**-----------** + 我最近在 LeetCode 上做到两道非常有意思的题目,382 和 398 题,关于水塘抽样算法(Reservoir Sampling),本质上是一种随机概率算法,解法应该说会者不难,难者不会。 我第一次见到这个算法问题是谷歌的一道算法题:给你一个**未知长度**的链表,请你设计一个算法,**只能遍历一次**,随机地返回链表中的一个节点。 @@ -37,13 +61,15 @@ int getRandom(ListNode head) { **证明**:假设总共有 `n` 个元素,我们要的随机性无非就是每个元素被选择的概率都是 `1/n` 对吧,那么对于第 `i` 个元素,它被选择的概率就是: -$$ + + +![](../pictures/水塘抽样/formula1.png) 第 `i` 个元素被选择的概率是 `1/i`,第 `i+1` 次不被替换的概率是 `1 - 1/(i+1)`,以此类推,相乘就是第 `i` 个元素最终被选中的概率,就是 `1/n`。 @@ -81,14 +107,16 @@ int[] getRandom(ListNode head, int k) { 对于数学证明,和上面区别不大: -$$ + + +![](../pictures/水塘抽样/formula2.png) 因为虽然每次更新选择的概率增大了 `k` 倍,但是选到具体第 `i` 个元素的概率还是要乘 `1/k`,也就回到了上一个推导。 @@ -110,13 +138,12 @@ $$ 这两个问题都是比较困难的,以后有时间我会写一写相关的文章。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:如何判断回文链表](../高频面试系列/判断回文链表.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何调度考生的座位](../高频面试系列/座位调度.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" index 01bdd63856..843e1153c3 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" @@ -1,5 +1,25 @@ # 如何寻找消失的元素 + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [学习算法和数据结构的思路指南](https://labuladong.gitbook.io/algo) + * [回溯算法最佳实践:括号生成](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[448.找到所有数组中消失的数字](https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array) + +**-----------** + 之前也有文章写过几个有趣的智力题,今天再聊一道巧妙的题目。 题目非常简单: @@ -102,13 +122,12 @@ public int missingNumber(int[] nums) { 至此这道算法题目经历九曲十八弯,终于再也没有什么坑了。 -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:如何判定括号合法性](../高频面试系列/合法括号判定.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何寻找缺失和重复的元素](../高频面试系列/缺失和重复的元素.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +

diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" index d541e839a2..117578856d 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" @@ -1,3 +1,25 @@ +# 如何寻找缺失和重复的元素 + + +

+GitHub + + + +

+ +![](../pictures/souyisou.png) + +相关推荐: + * [手把手带你刷二叉树(第三期)](https://labuladong.gitbook.io/algo) + * [25 张图解:键入网址后,到网页显示,其间发生了什么](https://labuladong.gitbook.io/algo) + +读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目: + +[645.错误的集合](https://leetcode-cn.com/problems/set-mismatch) + +**-----------** + 今天就聊一道很看起来简单却十分巧妙的问题,寻找缺失和重复的元素。之前的一篇文章「寻找缺失元素」也写过类似的问题,不过这次的和上次的问题使用的技巧不同。 这是 LeetCode 645 题,我来描述一下这个题目: @@ -107,13 +129,12 @@ vector findErrorNums(vector& nums) { -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) - +**_____________** -[上一篇:如何寻找消失的元素](../高频面试系列/消失的元素.md) +**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitbook.io/algo) 持续更新最新文章**。 -[下一篇:如何判断回文链表](../高频面试系列/判断回文链表.md) +**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 -[目录](../README.md#目录) \ No newline at end of file +

+ +