diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index df5b188691..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: 发现问题 -about: 我发现了某处链接或者知识点的错误 -title: 'bug ' -labels: bug -assignees: '' - ---- - - - -你好,我发现如下文章有 bug(点击文字可跳转到相关文章): - -[动态规划系列/抢房子.md](https://github.com/labuladong/fucking-algorithm/blob/master/动态规划系列/抢房子.md) - -问题描述: - -某章图片链接失效/其中的 XXX 内容有误/等等。 diff --git a/.github/ISSUE_TEMPLATE/translate.md b/.github/ISSUE_TEMPLATE/translate.md deleted file mode 100644 index f633c9fd9a..0000000000 --- a/.github/ISSUE_TEMPLATE/translate.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: 参与翻译 -about: 我想参与仓库中文章的翻译工作 -title: 'translate ' -labels: documentation -assignees: '' - ---- - - - - -我将开始翻译如下文章(点击可查看目标文章): - -[动态规划系列/抢房子.md](https://github.com/labuladong/fucking-algorithm/blob/master/动态规划系列/抢房子.md) - -我准备将它翻译成:**英文** - -**预计 X 天内翻译完成**,若由于种种原因没有完成,如果你愿意,你可以接替我的工作翻译这篇文章。 diff --git a/README.md b/README.md index 4e8962e4ff..2907001f55 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,93 @@ -感谢各位老铁前来参与翻译! - -请查看最新的 `english` 分支,保证你准备翻译的文章暂时没有英文版本。 - -翻译完成后,请删除文末的公众号二维码。对于第一个提交的翻译版本,你可以在文章开头的**一级标题下方**添加作者和翻译者: - -**Translator: [YourName](https://github.com/YourName)** - -**Author: [labuladong](https://github.com/labuladong)** - -你的链接可以指向任何你希望的地方。 - -### 翻译约定 - -1、翻译尽可能表达中文原意,你对基本的专业术语应该做到正确地使用,诸如 Queue, stack, binary tree 等词语。这种词语用错会让人很迷惑。基本的语法不能出错,建议搜索一些英语语法检查的在线网站,**或者最简单的,翻译后将你的文本粘贴到 Word 中,查看是否有基本的语法错误**。 - -2、**所有内容应以 `master` 分支为准**,因为 `english` 分支仅作为翻译,不会更新文章。所以如果你发现 `master` 中的某一篇文章在 `english` 分支中不存在或有冲突,以 `master` 分支中的 md 文件为准进行翻译,别忘了把相关图片文件夹加入 `english` 分支。 - -3、**加粗等信息需要保留,同时鼓励扩展自己的知识**,增加参考文献,将重要知识点添加粗体或使用英语(或其他语言)特有的表达形式来表达某些思想。 - -4、对于图片,很少包含汉字,如果不影响理解,比如图片右下角的公众号水印,就不必修改了。**如果汉字涉及算法理解,需要把图片一同修改了**,把汉字抹掉换成英文,或者汉字比较少的话,在汉字旁添加对应英文。**对于一些描述题目的图片**,都是我在中文版 LeetCode 上截的图,你可以去英文版 LeetCode 上寻找对应题目截图替换,如果不知道是哪一题,可以要求我给你找。 - -5、**保持原有的目录结构,但文件和文件夹的名称应改为英文**,md 文件的名称根据具体文章内容修改成恰当的英文,文章引用的图片路径有时也会包含中文,需要你将装有该图片的文件夹改成适当的英文。 - -6、**只处理在 issue 中约定的文章(和相关的图片),不要动其他任何的内容**,否则后续你对主仓库提交成果的时候,容易出现冲突。如果出现冲突,你需要先想办法使用 Git 工具解决本地仓库和主仓库的版本冲突才能提交 pull request,练习 Git 的使用是非常重要的。 - -其实咱们刷的算法题都没有什么特别生僻的英文单词,而且很多歪果仁母语也不一定是英文。Google Translator 翻译带点术语(栈、队列这种)的文章效果很差,甚至代码都给你翻译,所以不要害怕,勇敢地翻就行了,我们会在一次次迭代中慢慢变好的~ - -Github 具体的协作方式我在仓库置顶的 [issue](https://github.com/labuladong/fucking-algorithm/issues/9) 中有写,很简单,如果你之前没有协作过,这次翻译工作更是你对新事物的尝试和学习机会。不要害怕,Git 仓库的一切都是可以恢复的,不会出现操作不熟练而搞砸,**放开手干就完事儿了**。 - -PS:另外再强调一下,不要修改 issue 中约定的之外的文章,以便你的仓库后续合并进主仓库,提交你的分支也需要提交到 `english` 分支,翻译工作不要向 `master` 分支提交任何修改。 - -**Become a contributor, 奥利给**! +English translation is still processing... Some articles are still in Chinese, but most are completed. Please **star** this repo, when you come back soon, translation will finish perfectly. Just enjoy. + +These articles are somehow kinds of **Algorithmic Thinking**. All based on LeetCode problems, but **NOT ONLY the code of solution, but also WHY writing code this way, HOW we figure it out.** + +I don't like one liner and confusing, I like clear and easy-understanding. + +**Gitbook** has deployed, will sync with this branch of the repo: https://labuladong.gitbook.io/algo-en/ + +If you want to clone this repo, please use following command: + +```shell +git clone --depth 1 --branch english https://github.com/labuladong/fucking-algorithm.git +``` +This command specifies the `english` branch and limit the depth of clone, get rid of the Git commit history, which can be faster to clone. + +# Table of Content + +* I. High Frequency Interview Problem + * [How to Implement LRU Cache](interview/LRU_algorithm.md) + * [How to Find Prime Number Efficiently](interview/Print_PrimeNumbers.md) + * [How to Calculate Minimium Edit Distance](dynamic_programming/EditDistance.md) + * [How to Solve Drop Water Problem](interview/Trapping_Rain_Water.md) + * [How to Remove Duplicate From Sorted Sequence](interview/RemoveDuplicatesfromSortedArray.md) + * [How to Find Longest Palindromic Substring](interview/TheLongestPalindromicSubstring.md) + * [How to Reverse Linked List in K Group](interview/reverse-nodes-in-k-group.md) + * [How to Check the Validation of Parenthesis](interview/valid-parentheses.md) + * [How to Find Missing Element](interview/missing_elements.md) + * [How to Pick Elements From a Arbitrary Sequence](interview/ReservoirSampling.md) + * [How to use Binary Search](interview/UsingBinarySearchAlgorithm.md) + * [How to Scheduling Seats](interview/Seatscheduling.md) + * [Union-Find Algorithm in Detail](think_like_computer/Union-find-Explanation.md) + * [Union-Find Application](think_like_computer/Union-Find-Application.md) + * [Find Sebesquence With Binary Search](interview/findSebesquenceWithBinarySearch.md) + * [Problems can be sloved by one line](interview/one-line-code-puzzles.md) + * [How to Find Dup and Missing Element](interview/Find-Duplicate-and-Missing-Element.md) + * [How to Check Palindrom LinkedList](interview/check_palindromic_linkedlist.md) + +* II. Data Structure + * [Binary Head and Priority Queue](data_structure/binary_heap_implements_priority_queues.md) + * [LRU Cache Strategy in Detial](interview/LRU_algorithm.md) + * [Collections of Binary Search Operations](data_structure/The_Manipulation_Collection_of_Binary_Search_Tree.md) + * [Special Data Structure: Monotonic Stack](data_structure/MonotonicStack.md) + * [Special Data Structure: Monotonic Stack](data_structure/Monotonic_queue.md) + * [Design Twitter](data_structure/design_Twitter.md) + * [Reverse Part of Linked List via Recursion](data_structure/reverse_part_of_a_linked_list_via_recursion.md) + * [What's the Best Algo Book](think_like_computer/why_i_recommend_algs4.md) + * [Queue Implement Stack/Stack implement Queue](data_structure/ImplementQueueUsingStacksImplementStackUsingQueues.md) + * [学习算法和刷题的思路指南](think_like_computer/学习数据结构和算法的高效方法.md) + +* III. Algorithmic thinking + * [My Way to Learn Algorithm](think_like_computer/ThewaytoAlgorithmlearning.md) + * [The Framwork of Backtracking Algorithm](think_like_computer/DetailsaboutBacktracking.md) + * [Binary Search in Detial](think_like_computer/DetailedBinarySearch.md) + * [The Tech of Double Pointer](think_like_computer/double_pointer.md) + * [The Key Concept of TowSum Problems](think_like_computer/The_key_to_resolving_TwoSum_problems.md) + * [Divide Complicated Problem: Implement a Calculator](data_structure/Implementing_the_functions_of_a_calculator.md) + * [Prefix Sum Skill](think_like_computer/prefix_sum.md) + * [FloodFill Algorithm in Detail](think_like_computer/flood_fill.md) + * [Interval Scheduling: Interval Merging](think_like_computer/IntervalMerging.md) + * [Interval Scheduling: Intersections of Intervals](think_like_computer/IntervalIntersection.md) + * [String Multiplication](think_like_computer/string_multiplication.md) + * [Pancake Soring Algorithm](think_like_computer/PancakesSorting.md) + * [Sliding Window Algorithm](think_like_computer/SlidingWindowTechnique.md) + * [Some Useful Bit Manipulations](think_like_computer/CommonBitManipulation.md) + * [Russian Doll Envelopes Problem](think_like_computer/RussianDollEnvelopes.md) + * [回溯算法团灭排列、组合、子集问题](interview/子集排列组合.md) + * [几个反直觉的概率问题](think_like_computer/几个反直觉的概率问题.md) + * [洗牌算法](think_like_computer/洗牌算法.md) + * [递归详解](think_like_computer/递归详解.md) + +* IV. Dynamic Programming + * [Classic DP: Edit Distance](dynamic_programming/EditDistance.md) + * [Classic DP: Super Egg](dynamic_programming/ThrowingEggsinHighBuildings.md) + * [Classic DP: Super Egg(Advanced Solution)](dynamic_programming/SuperEggDropAdvanced.md) + * [Class DP: Longest Common Subsequence](dynamic_programming/LongestCommonSubsequence.md) + * [Classis DP: Game Problems](dynamic_programming/GameProblemsInDynamicProgramming.md) + * [Regular Expression](dynamic_programming/RegularExpression.md) + * [The Strategies of Subsequence Problem](dynamic_programming/StrategiesForSubsequenceProblem.md) + * [Greedy: Interval Scheduling](dynamic_programming/IntervalScheduling.md) + * [4 Keys Keyboard](dynamic_programming/FourKeysKeyboard.md) + * [What is DP Optimal Substructure](dynamic_programming/OptimalSubstructure.md) + * [动态规划详解](dynamic_programming/动态规划详解进阶.md) + * [动态规划设计:最长递增子序列](dynamic_programming/动态规划设计:最长递增子序列.md) + * [动态规划之KMP字符匹配算法](dynamic_programming/动态规划之KMP字符匹配算法.md) + * [团灭 LeetCode 股票买卖问题](dynamic_programming/团灭股票问题.md) + * [团灭 LeetCode 打家劫舍问题](dynamic_programming/抢房子.md) + +* V. Common Knowledge + * [Difference Between Process and Thread in Linux](common_knowledge/linuxProcess.md) + * [You Must Know About Linux Shell](common_knowledge/linuxshell.md) + * [You Must Know About Cookie and Session](common_knowledge/SessionAndCookie.md) + * [Cryptology Algorithm](common_knowledge/Cryptology.md) + * [Some Good Online Pratice Platforms](common_knowledge/OnlinePraticePlatform.md) \ No newline at end of file diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000000..5f26de5788 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,80 @@ +# Summary + +* [Introduction](README.md) + +* I. High Frequency Interview Problem + * [How to Implement LRU Cache](interview/LRU_algorithm.md) + * [How to Find Prime Number Efficiently](interview/Print_PrimeNumbers.md) + * [How to Calculate Minimium Edit Distance](dynamic_programming/EditDistance.md) + * [How to Solve Drop Water Problem](interview/Trapping_Rain_Water.md) + * [How to Remove Duplicate From Sorted Sequence](interview/RemoveDuplicatesfromSortedArray.md) + * [How to Find Longest Palindromic Substring](interview/TheLongestPalindromicSubstring.md) + * [How to Reverse Linked List in K Group](interview/reverse-nodes-in-k-group.md) + * [How to Check the Validation of Parenthesis](interview/valid-parentheses.md) + * [How to Find Missing Element](interview/missing_elements.md) + * [How to Pick Elements From a Arbitrary Sequence](interview/ReservoirSampling.md) + * [How to use Binary Search](interview/UsingBinarySearchAlgorithm.md) + * [How to Scheduling Seats](interview/Seatscheduling.md) + * [Union-Find Algorithm in Detail](think_like_computer/Union-find-Explanation.md) + * [Union-Find Application](think_like_computer/Union-Find-Application.md) + * [Find Sebesquence With Binary Search](interview/findSebesquenceWithBinarySearch.md) + * [Problems can be sloved by one line](interview/one-line-code-puzzles.md) + * [How to Find Dup and Missing Element](interview/Find-Duplicate-and-Missing-Element.md) + * [How to Check Palindrom LinkedList](interview/check_palindromic_linkedlist.md) + +* II. Data Structure + * [Binary Head and Priority Queue](data_structure/binary_heap_implements_priority_queues.md) + * [LRU Cache Strategy in Detial](interview/LRU_algorithm.md) + * [Collections of Binary Search Operations](data_structure/The_Manipulation_Collection_of_Binary_Search_Tree.md) + * [Special Data Structure: Monotonic Stack](data_structure/MonotonicStack.md) + * [Special Data Structure: Monotonic Stack](data_structure/Monotonic_queue.md) + * [Design Twitter](data_structure/design_Twitter.md) + * [Reverse Part of Linked List via Recursion](data_structure/reverse_part_of_a_linked_list_via_recursion.md) + * [What's the Best Algo Book](think_like_computer/why_i_recommend_algs4.md) + * [Queue Implement Stack/Stack implement Queue](data_structure/ImplementQueueUsingStacksImplementStackUsingQueues.md) + * [学习算法和刷题的思路指南](think_like_computer/学习数据结构和算法的高效方法.md) + +* III. Algorithmic thinking + * [My Way to Learn Algorithm](think_like_computer/ThewaytoAlgorithmlearning.md) + * [The Framwork of Backtracking Algorithm](think_like_computer/DetailsaboutBacktracking.md) + * [Binary Search in Detial](think_like_computer/DetailedBinarySearch.md) + * [The Tech of Double Pointer](think_like_computer/double_pointer.md) + * [The Key Concept of TowSum Problems](think_like_computer/The_key_to_resolving_TwoSum_problems.md) + * [Divide Complicated Problem: Implement a Calculator](data_structure/Implementing_the_functions_of_a_calculator.md) + * [Prefix Sum Skill](think_like_computer/prefix_sum.md) + * [FloodFill Algorithm in Detail](think_like_computer/flood_fill.md) + * [Interval Scheduling: Interval Merging](think_like_computer/IntervalMerging.md) + * [Interval Scheduling: Intersections of Intervals](think_like_computer/IntervalIntersection.md) + * [String Multiplication](think_like_computer/string_multiplication.md) + * [Pancake Soring Algorithm](think_like_computer/PancakesSorting.md) + * [Sliding Window Algorithm](think_like_computer/SlidingWindowTechnique.md) + * [Some Useful Bit Manipulations](think_like_computer/CommonBitManipulation.md) + * [Russian Doll Envelopes Problem](think_like_computer/RussianDollEnvelopes.md) + * [回溯算法团灭排列、组合、子集问题](interview/子集排列组合.md) + * [几个反直觉的概率问题](think_like_computer/几个反直觉的概率问题.md) + * [洗牌算法](think_like_computer/洗牌算法.md) + * [递归详解](think_like_computer/递归详解.md) + +* IV. Dynamic Programming + * [Classic DP: Edit Distance](dynamic_programming/EditDistance.md) + * [Classic DP: Super Egg](dynamic_programming/ThrowingEggsinHighBuildings.md) + * [Classic DP: Super Egg(Advanced Solution)](dynamic_programming/SuperEggDropAdvanced.md) + * [Class DP: Longest Common Subsequence](dynamic_programming/LongestCommonSubsequence.md) + * [Classis DP: Game Problems](dynamic_programming/GameProblemsInDynamicProgramming.md) + * [Regular Expression](dynamic_programming/RegularExpression.md) + * [The Strategies of Subsequence Problem](dynamic_programming/StrategiesForSubsequenceProblem.md) + * [Greedy: Interval Scheduling](dynamic_programming/IntervalScheduling.md) + * [4 Keys Keyboard](dynamic_programming/FourKeysKeyboard.md) + * [What is DP Optimal Substructure](dynamic_programming/OptimalSubstructure.md) + * [动态规划详解](dynamic_programming/动态规划详解进阶.md) + * [动态规划设计:最长递增子序列](dynamic_programming/动态规划设计:最长递增子序列.md) + * [动态规划之KMP字符匹配算法](dynamic_programming/动态规划之KMP字符匹配算法.md) + * [团灭 LeetCode 股票买卖问题](dynamic_programming/团灭股票问题.md) + * [团灭 LeetCode 打家劫舍问题](dynamic_programming/抢房子.md) + +* V. Common Knowledge + * [Difference Between Process and Thread in Linux](common_knowledge/linuxProcess.md) + * [You Must Know About Linux Shell](common_knowledge/linuxshell.md) + * [You Must Know About Cookie and Session](common_knowledge/SessionAndCookie.md) + * [Cryptology Algorithm](common_knowledge/Cryptology.md) + * [Some Good Online Pratice Platforms](common_knowledge/OnlinePraticePlatform.md) \ No newline at end of file diff --git a/common_knowledge/Cryptology.md b/common_knowledge/Cryptology.md new file mode 100644 index 0000000000..88e0925e7b --- /dev/null +++ b/common_knowledge/Cryptology.md @@ -0,0 +1,177 @@ +Speaking of cipher, the first thing that comes to our mind is the password to login, but from the point of cryptography, it is the unqualified cipher. + +Why? Because the password of our account depends on crypticity, that is I keep the password in my mind and do not let you know,so you can not login in with my account. + +However,cryptography says confidential information will be revealed some day, so encryption algorithm should not keep the secret to become confidential, Conversely, even you know the encryption algorithm, you can do nothing. Magically speaking,I tell you my ciphers, but you still do not know my ciphers. + +The most metaphysical algorithm is Diffie-Hellman key exchange algorithm.I am so surprised in such a case that two people exchange some numbers in the front of you and they can share a common secret,but you can not figure out what it is.The following will focus on the algorithm. + +In the article,cryptography mainly solve the problem of encrypt and decrypt in the process of information exchange.Assume that the process of information exchange is insecurity and all the information is being eavesdropped,so sender encrypt the information before sending and receiver must know how to decrypt after receiving.The interesting thing is that +if the receiver know the way to decrypt,the eavesdropper should also know. + +In the following **we will introduce symmetric encryption algorithm、key exchange algorithm、asymmetrical encryption algorithm、digital signature、public-key certificate** to explain how we solve the problem of secure transmission. + +### First、Symmetric Encryption + +symmetric ciphers,also called shared key cipher,as the name suggests,it uses the same key to encrypt and decrypt. + +First of all,we know information consists of 0/1 bits sequence and the xor of two identical bits sequence is 0. + +For example,one of the simplest symmetric encryption is that we can generate a random bits key that is the same length as original word ,then get the xor of key and origin as encrypted words. +Conversely,we can xor this encrypted words with the key to recovery the origin words. + +This simple example has some problems because of its simplicity.For example,if the original words is large,the key is just as large and the overhead of generating large random bits sequence is also high. + +Of course,there are many sophisticated and excellent symmetric encryption algorithm that solves these problems.For example, Rijndael cryptographic algorithm,triple DES algorithm and so on. +**These algorithm is invulnerable,that is,they have huge key space,are impossible to brute force and encryption process is relatively fast**. + +**However,the weakness of these symmetric encryption algorithm is the distribution of keys**.when using the same key to encrypt and decrypt,sender must send the key to receiver.If the hacker can overhead the encrypted text and the key,these +invulnerable algorithm is broken. + +Therefore, the two most common algorithms to solve the key distribution problem are diffie-hellman key exchange algorithm and asymmetric encryption algorithm. + +### Second、Key exchange algorithm + +The secret key as we say is a big number, the algorithm use this number to encrypt and decrypt. The problem is that transition is insecure and the data can be eavesdropped.In other words, is there a way that it can let two people exchange the key in the front of others? + +Diffie-Hellman key exchange algorithm can achieve it. **Precisely speaking, this algorithm do not send the secret to the receiver, but by some sharing number, both sides generate the same secret in their mind and the secret can not be generated by the third party eavesdropper.** + +Perhaps this is the legend of the heart has a sharp touch of it. + +This algorithm is not complicated, you can try it with your friends and I will show the flow.In the beginning, we should define one thing: **Not all operations have inverses.** + +The simplest case is the well-known one-way hash function.That is given a number `a` and a hash function `f`, you can calculate `f(a)`,but if given `f(a)` and `f`,finding `a` is basically impossible.The reason why the key exchange algorithm looks so mysterious is that it takes advantage of this irreversible property. + +Below, let me show the flow of exchange key algorithm. Follow naming conventions, the parties who are going to perform the key exchange algorithm are called Alice and Bob,the bad guy who is trying to steal their communication in the Internet is called hack. + +First, Alice and Bob negotiate two number `N` and `G` as the origin.Of course the negotiation can be eavesdropped by Hack,so I put these two numbers in the middle presenting all three parties know. + +![](../pictures/密码技术/1.jpg) + +Now Alice 和 Bob **in mind** generate a number separately `A` and `B`: + +![](../pictures/密码技术/2.jpg) + +Now Alice do some calculation using `A` and `G` and get the result `AG`, then send it to Bob; Bob also do some calculation using `B` and `G` and get the result `BG`,then send it to Alice: + +![](../pictures/密码技术/3.jpg) + +Now the situation is as following: + +![](../pictures/密码技术/4.jpg) + +Note that,as the hash function case above, knowing `AG` and `G` can not figure out `A`, the same as `BG`. + +So, Alice can use `BG` and his `A` generate a number `ABG` by some calculation. Bob also can use `AG` and his `B` get the number `ABG` by some calculation. This number is the shared secret of Alice and Bob. + +As for Hack, He can eavesdrop `G`,`AG`,`BG`, but because the calculation is irreversible, he can not calculate `ABG`. + +![](../pictures/密码技术/5.jpg) + +Above is the basic flow, as for picking which number, it is exquisite and I won't write specifically for the space. + +Under the premise of the third party's eavesdropping, the algorithm can calculate a secret which can not be calculated by others as the key of symmetric encryption algorithm and start the communication of symmetric encryption. + +About this algorithm, Hack come up with a crack way, not to eavesdrop Alice and Bob's communication, but to pretend to be Alice and Bob at the same time, that is man-in-middle attack. + +![](../pictures/密码技术/6.jpg) + +In this way, both parties can't realize that they are sharing secrets with Hack. As a result, Hack can decrypt or even modify data. + +**So, key exchange algorithm can not perfectly resolve the distribution of the key, the weakness lies in not being able to verify the identity of the receiver**.So, before using exchange key algorithm, we must verify the identity. For example, using the digital signature. + +### Third、Asymmetrical encryption + +The thinking of the asymmetrical encryption is that don't sneak around with the transmission key. I separate the encryption key from the decryption key and use the public key to encrypt and the private to decrypt.Only send the public key to the receivers and they can send me encrypted data that I can use my private key to decrypt.About the eavesdropper,it is useless to get the data and the public key, because only the private key can be used to decrypt. + +You can think like this, **the private key is the key and the public key is the lock.We can public the lock and let others lock the data before send to me,but the key is in my hand to unlock.** Our common RSA algorithm is a typical asymmetric encryption algorithm, the implementation is complicated,I will skip here and you can find in google. + +In practice, the speed of asymmetrical encryption is slow compared to the symmetric encryption, so when it comes to transit huge amount of data,we do not use the public key to encrypt the data,but encrypt the symmetric encryption key and send it to receiver and then use symmetric encryption algorithm to encrypt data. + +It should be noted that, like Diffie-Hellman algorithm, **asymmetrical encryption algorithm can not verify the identity of sender and receiver,is still be cracked by man-in-middle attack.** For example, Hack block public key released by Bob, then send Alice her public key as Bob.So, without knowing it, Alice will encrypt the private data with Hack's public key, which can be decrypted and eavesdropped by Hack. + +So, the Diffie-Hellman algorithm and the RSA asymmetrical encryption algorithm can partially resolve the distribution of the key and both have the same weakness.What is the different scenario of applying them? + +Simply speaking, according to the basic principles of the two algorithms, we can see that: + +If both sides have a symmetric encryption plan and hope to encrypt the communication and not let others get the key,they can use Diffie-Hellman key exchange algorithm. +If you hope everyone can encrypt the data,but only you can decrypt, you can use RSA asymmetrical encryption algorithm and release the public key. + +In the following, we try to resolve the problem of verifying the identity of the sender. + +## Fourth、Digital signature + +About asymmetrical encryption algorithm as said,it release the public key so that others can send the encrypted date to you and it can only be decrypted by the private key kept by you. Actually, **private key can be used to encrypt the data,as RSA algorithm,the data encrypted by the private key can open be decrypted by the public key.** + +Digital signature takes the advantage of asymmetrical encryption,but reverses the process of public key encrypting. **Releasing the public key,but you use the private key to encrypt data and then public the encrypted data,this is the digital signature.** + +You may ask what is the purpose? The public key can decrypt the data,but I still encrypt the data and release.Isn't that a superfluous act? + +Yes,but ** the digital signature is not used to ensure the confidentiality of the data,but is used to verify your identity,** to prove that the data comes from you. + +You can image the data encrypted by you private key can only be decrypted by you public key,so if the encrypted data can be decrypted by you public key,can't it prove that the data comes from yourself? + +Of course,the encrypted data is just a signature that should be released with the data at the same time.The specific process is: + +1 Bob generates the public key and the secret key and releases the public key and keep the secret key himself. + +2 **making the data encrypted by the private key as signature and sends the data with this signature.** + +3 Alice receives the data the the signature and needs to check does the data come from Bob.So he use the public key released by Bob to decrypt and compare the decrypted data with the received data.If they are the same,it proves that the data is origin and comes from Bob. + +Why Alice can conclude? After all,the data and the signature,either can be exchanged.The reason is as follows: +1 If someone modify the data,Alice will know after he decrypt the data and find the difference. + +2 If someone exchange the signature,Alice will get the wrong code after decryption and it is obviously different from the original data. + +3 someone may tend to modify the data and regenerate the signature so that Alice can not find the difference;but he can not generate the signature because he do not have the private key of Bob. + +In Summary,**digital signature can verify the origin of the data to some degree**.The reason is that it can be cracked by man-in-middle attack. Once it comes to the distribution of the public key, the receiver may receive the fake public key and make the wrong verification, Which can not be avoided. + +Ridiculously, digital signature is a way verify the identity of others with the assumption that the identity of others is real.It seems like a dead cycle.**There must exist a trusted origin to verify the identity of others,Or no matter how many processes are used, they are just transferring problems, not really solving them.** + +### Fifth、Public-key certificate + +**The public-key certificate is the public key plus the signature,issued by a trusted third party certification authority**。Introducing the trusted third part is one of the feasible solution of dependency cycle. + +The process of certificate is as follows: + +1 Bob goes to the trusted certification authority to verify the identity of himself and provide his public key. + +2 Alice who wants to communicate with Bob, request the public key of Bob from the certification authority and then certification authority will give the certificate of Bob(it contains Bob's public key and the signature of his public key) to Alice. + +3 Alice check the signature adn verify that the public key comes from the certificate authority and not tampered in halfway. + +4 Alice encrypts the data through this public key and starts to communicate with Bob. + +![图片来自《图解密码技术》](../pictures/密码技术/7.jpg) + +PS: the above is for description.In real,certificate is only installed once instead of request from certificate authority every time and it is the server sends the certificate to client not the certificate authority. + +Some people may ask if Alice want to verify the validity of the certificate, he must have the public key of the authority.Isn't it the dead cycle mentioned just now? + +The regular browser we pre-installed contains trusted certificate to verity the identity of certificate authority,so the certificate is credible. + +when Bob provide the public key, he must provide many personal information to strictly verify his identity,so it can be considered trusted. + +Except for the trusted public key of Bob, the communication of Alice and Bob is protected by the secure algorithm and is invulnerable. + +Most of the regular websites nowadays apply HTTPS protocol, that adds a SSL/TLS secure layer between the HTTP protocol and the TCP protocol. After the TCP handshake, SSL protocol layer also handshake to exchange secure information including the certificate of the website, so that the browser can verify the website. After SSL layer finish the verification, the data in the HTTP protocol is encrypted to guarantee secure transmission. + +### Sixth、Summary + +Symmetric encryption algorithm use the same secret key to encrypt and decrypt, is hard to crack, encrypt quickly, but has the problem of secret key transmission. + +Diffie-Hellman key exchange algorithm can it like a Vulcan mind meld,partially solve the problem of the distribution of the secret key,but it can not verify the identity of senders and receivers and can be cracked by man-in-middle attack. + +asymmetrical encryption algorithm generates a pair of secrete keys, separate encryption and decryption. + +RSA algorithm is a classical asymmetrical encryption algorithm and it has two purpose:One is to encrypt,that is releasing a public key to encrypt and hold a secret key itself to decrypt,for the confidentiality of the data.Another one is for digital signature,that is +releasing the public key and generate the digital signature by private key, to prove that the data comes from the secret key holder.But either way can not avoid man-in-middle attack because of the distribution of the public key. + +The public-key certificate is the public key plus the signature,issued by a trusted third party certification authority.Because regular browsers pre-install the public key of the trusted certification authority, it can avoid man-in-middle attack. + +The SSL/TLS secure layer in HTTPS protocol includes these encryption methods above.**So do not install irregular browser and certificate of unknown source**. + +Cryptography is a little part of the security.Even though the HTTPS websites certified by a formal authority are not totally trusted,it only indicates the transition of the data is safe.Technology can not protect you.The most important thing is to improve personal safety awareness,pay more attention and handle sensitive data carefully. + diff --git a/common_knowledge/OnlinePraticePlatform.md b/common_knowledge/OnlinePraticePlatform.md new file mode 100644 index 0000000000..f27d8756b1 --- /dev/null +++ b/common_knowledge/OnlinePraticePlatform.md @@ -0,0 +1,97 @@ +# Git/SQL/Regular Expressions Online Practice Platform + +**Translator: [Dong Wang](https://github.com/Coder2Programmer)** + +**Author: [labuladong](https://github.com/labuladong)** + +Although I like to take exam-oriented education when I'm fine, I also found a trick from the exam-oriented education: If I can learn a certain skill in the form of a question, the efficiency and effectiveness are the best. For technical learning, I often face a dilemma: **I know a lot of theoretical knowledge, but some scenarios can’t be simulated, and I lack the opportunity to do it myself**. It's useful there is a workbook with standard answers. + +So when learning new technologies, I will first search for online problem solving platforms. Do n’t mention that some gods have done very good online practice platforms. Here are a few platforms, which are learning Git, SQL, regular expression online training platform. + +### 1. Git exercises + +This is a project called Learning Git Branching, which I must recommend: + +![](../pictures/online/1_english.png) + +As in the welcome dialog box, this is indeed one of the **best** Git animation tutorials I have found so far, none of them. + +When I used Git, I would `add .`,` clone`, `push`,` pull`, `commit`. The other commands would not be at all. Git is a downloader, and Github is a resource website, of course the execution of git commands depend on luck. I don't understand what version control is, and I don't bother to read the messy documents. + +The tutorial on this site is not to give you detailed examples of modifying files, but to abstract each `commit` into nodes of the tree, **in the form of animation to break through the levels**, and let you use Git commands to complete your goals freely: + +![](../pictures/online/2_english.png) + +All Git branches are visualized. You just enter the Git command on the command line on the left, and the branch will change accordingly. As long as the task goal is reached, you will pass the level! The website will also record the number of your orders, try to pass the minimum number of orders! + +![](../pictures/online/3_english.png) + +At first I thought that this tutorial only included version management of the local Git repository, but **later I was surprised to find that it also has a remote repository operation tutorial**! + +![](../pictures/online/4_english.png) + +![](../pictures/online/5_english.png) + +It ’s really the same as playing a game. The difficulty design is reasonable and the fluency is very good. I can’t stop playing it anymore, and I get through in a few hours, cooool! + +![](../pictures/online/6_english.png) + +In short, this tutorial is very suitable for beginners and advanced. If you feel that you don't have a good grasp of Git, using Git commands or relying on luck, you can play this tutorial, I believe it will make you more proficient in using Git. + +It is an open source project, Github project address: + +https://github.com/pcottle/learnGitBranching + +Tutorial website address: + +https://learngitbranching.js.org + +### 2. regular expression exercises + +**Regular expression is a very powerful tool**. It can be said that all data in the computer are characters. With the help of pattern matching tools such as regular expressions, operating the computer can be said to be even more powerful. + +Here are two websites recommended, one is an exercise platform, and the other is a platform for testing regular expressions. + +The exercise platform called RegexOne: + +![](../pictures/online/9.png) + +There are basic tutorials in front, and some common regular expression topics in the back, such as judging mailboxes, URLs, phone numbers, or extracting key information from logs. + +As long as you write a regular expression that meets the requirements, you can go to the next question. The key is that each question has a standard answer. You can click the solution button below to view it: + +![](../pictures/online/10.png) + +RegexOne URL: + +https://regexone.com/ + +The test tool is a Github project called RegExr and its website: + +![](../pictures/online/11.png) + +It can be seen that after entering text and regular pattern strings, **the website will add nice and easy-to-recognize styles to regular expressions, automatically search for pattern strings in text, highlight matching strings, and also display each group captured string**. + +This website can be used in conjunction with the previous regular exercise platform. Here you can try various expressions and paste them after successful matching. + +RegExr URL: + +https://regexr.com/ + +### 3. SQL exercises + +This is a website called SQLZOO with all the exercises on the left: + +![](../pictures/online/7.png) + +SQLZOO is a very useful SQL practice platform. English is not difficult to understand. You can read the English version directly, but you can also switch to Traditional Chinese, which is friendly. + +Here are the more commonly used SQL commands, to give you a need, you write SQL statements to achieve correct query results. **The most important thing is that there is not only a detailed explanation of the usage of each command here, there is a multiple choice question (quiz) at the end of each topic, and there is a judgment system, and even some difficult questions have video explanations**: + +![](../pictures/online/8.png) + +As for the difficulty, it's gradual, even friendly to novices, and the later questions are really more technical. I believe this is what people who love to think and challenge like! LeetCode also has SQL-related topics, but the difficulty is generally relatively large. I think it is more appropriate to brush the basic SQL commands in SQLZOO and then to LeetCode. + +Website address: + +https://sqlzoo.net/ diff --git a/common_knowledge/SessionAndCookie.md b/common_knowledge/SessionAndCookie.md new file mode 100644 index 0000000000..356acb5f67 --- /dev/null +++ b/common_knowledge/SessionAndCookie.md @@ -0,0 +1,135 @@ +# Session and Cookie + +**Translator: [Funnyyanne](https://github.com/Funnyyanne)** + +**Author: [labuladong](https://github.com/labuladong)** + +Everyone should be familiar with cookies. For example after logging on the website, you will be asked to log in again. Or some guys play with python, but websites just block your crawlers. These are all related to cookies. If you understand the server backend's processing logic for cookies and sessions, you can explain these phenomena, and even drill some holes indefinitely, let me talk it slowly. + +### 1.Introduction to session and cookie + +The emergence of cookie because HTTP is a stateless protocol, In other words, the server can't remember you, and every time you refresh the web page, you have to re-enter your account password to log in. It's hard to accept. Cookie is like the server tagged you, and the server recognizes you every time you make a request to the server. + +To summarize it abstractly:**A cookie can be considered a「variable」,such as `name=value`,stored in the browser; One session can be understood as a data structure ,for the most part is the 「mapping」(Key-value data),and stored on the server**. + +Note that I said is 「a」cookie can be thought of as a variable,but the server can be set at a time more than one cookie. So it sometimes makes sense to say that cookies are「a set」of key-value pairs. + +Cookie can be set on the sever through the “SetCookie” field of HTTP, such as s simple service I wrote in Go: + +```go +func cookie(w http.ResponseWriter, r *http.Request) { + // 设置了两个 cookie + http.SetCookie(w, &http.Cookie{ + Name: "name1", + Value: "value1", + }) + + http.SetCookie(w, &http.Cookie{ + Name: "name2", + Value: "value2", + }) + // 将字符串写入网页 + fmt.Fprintln(w, "页面内容") +} +``` + +When the browser accesses the corresponding URL, check the details of the HTTP communication through the browser‘s develop tools,and you can see that the server’s response issued the `SetCookie` command twice: + +![](../pictures/session/1.png) + +After that,the `Cookie` field in the browser’s request carries two cookies: + +![](../pictures/session/2.png) + +**So, what cookie does is it's very simple, it's nothing more than the server tagging every client (browser)** to make it easier for the server to recognize them. Of course, HTTP also has a number of parameters that can be used to set cookies, such as expiration time, or to make a cookie available only to a specific path, and so on. + +But the problem is that we also know that many websites now have complex functions and involve a lot of data interaction. For example, the shopping cart function of the e-commerce website has a large amount of information and a complicated structure, not by a simple cookie mechanism to pass so much information. Also, know that the cookie field is stored in the HTTP header. Even if it can carry this information, it will consume a lot of bandwidth and consume more network resources. + +Session can work with cookies to solve this problem. For example, a cookie stores such a variable `sessionID=xxxx`, and just passes this cookie to the server, and then the server finds the corresponding session by this ID. This session is a data structure that stores the user ’s shopping cart and other detailed information. The server can use this information to return to the user's customized web page, effectively solving the problem of tracking users. + +**Session is a data structure designed by the website developer, so it can carry various data** , as long as the client's cookie sends a unique session ID, the server can find the corresponding session, recognize this client. + +Because session is stored in the server in the form of memory. When many users occupy the session, it will take up server resources, so the session pool management plan must be done. Due to the session will generally have an expiration time. The server will regularly check and delete the expired session. If the user to access the server again later, may go into log back in and so on, the server will create a new session, the session ID sends to the client through the form of a cookie. + +So, we know the principle of cookies and sessions, what are the practical benefits? **In addition to dealing with interviews, I will tell you the usefulness of a chicken thieves, that is use these services without paying.** + +Some website, the services you use it for the first time. It allows you to try it for free directly, but after using it once, let you log in and pay to continue using the service. And you find that the website seems to remember your computer by some means, unless you change the computer or change a browser to do it for free again. + +So the question is, how does the web server remember you when you're not logged in? Obviously, the server must have sent cookies to your browser, and a corresponding session was set up in the background to record your status. Every time your browser visits the website, it will obediently carry cookies. Server checks the session that the browser has been free to use, have to let it log in pay, can't let it continue to pay for nothing. + +If I don't let the browser sends the cookie, every time I pretend to be a little cute newcomer to try it out, can I keep no flower playing? The browser will store the website's cookies as files in some places (different browser configurations are different), so you just find them and delete them. But for Firefox and Chrome browsers, there are many plugins that can directly edit cookies.For example, my Chrome browser with a plug-in called “EditThisCookie”, this is their website: + +![http://www.editthiscookie.com/](../pictures/session/3.png) + +This type of plugin can read the browser's cookies on the current web page, open the plugin can edit and delete cookies at will. **Of course, occasionally get a free job is okay, but discouraged it all time. If you want to use it, pay for it. Otherwise, That's all the website can say:“No buck, No bang!”** + +The above is a brief introduction to cookies and sessions. Cookie is a part of the HTTP protocol and are not complicated. So let's take a look at the code architecture to implement session management in detail. + +### 2.Implementation of session + +The principle of session is not difficult, but it is very skillful to implement it. Generally, three components are required to complete it. They respectively are`Manager`,`Provider` and `Session` three classes (interface). + +![](../pictures/session/4.jpg) + +1.The browser requests the page resource of the path `/content` rom the server over the HTTP protocol, there is a Handler function on the corresponding path to receive the request, parses the cookie in the HTTP header, and gets the session ID stored in it,then send this ID to the `Manager`. + +2.`Manager`acts as a session manager, mainly storing some configuration information, such as the lifetime of the session, the name of the cookie, and so on. All sessions are stored in a `Provider` inside the `Manager`.So `Manager` passes the `Sid` (session ID) to the `Provider` to find out which session that ID corresponds to. + +3.`Provider` is a container, most commonly a hash table that maps each `Sid` to its session. After receiving the `Sid` passed by the `Manager`, it finds the session structure corresponding to the `Sid`, which is the session structure, and returns it. + +4.`Session` stores the user's specific information. The logic in the Handler function takes out this information, generates the user's HTML page, and returns it to the client. + +So you might ask, why make such a trouble, why not directly in the Handler function to get a hash table, and then store the `Sid` and `Session` structure mapping ? + +**That's the design trick!** Let's talk about why it is divided into `Manager`、`Provider` and `Session`。 + + +Let's start with `Session` at the bottom. Since session is a key-value pair, why not use a hash table directly, but abstract such a data structure? + +First, because the `Session` structure may not only store a hash table, but also some auxiliary data, such as `Sid`, number of accesses, expiration time, or last access time, which is easy to implement algorithms like LRU and LFU. + +Second, because sessions can be stored in different ways. If you use the built-in programming language hash table, then the session data is stored in memory, if the amount of data, it is likely to cause the program to crash, but once the program ends, all session data is lost. So we can have a variety of session storage, such as cached database Redis, or stored in MySQL and so on. + +Therefore, `Session` structure provides a layer of abstraction to shield the differences between different storage methods, as long as a set of common interfaces are provided to manipulate key-value pairs: + +```go +type Session interface { + // 设置键值对 + Set(key, val interface{}) + // 获取 key 对应的值 + Get(key interface{}) interface{} + // 删除键 key + Delete(key interface{}) +} +``` + +Besides, why `Provider` should be abstracted. `Provider` in our figure above is a hash table that holds the mapping of `Sid` to `Session`, but it will definitely be more complicated in practice. We need to delete some sessions from time to time. In addition to setting the survival time, we can also adopt some other strategies, such as LRU cache elimination algorithm, which requires the `Provider` to use the data structure of hash list to store the session. + +PS: For the mystery of the LRU algorithm, please refer to the [LRU_algorithm](https://github.com/Funnyyanne/fucking-algorithm/blob/english/interview/LRU_algorithm.md) above. + +Therefore, `Provider` as a container is to shield algorithm details and organize the mapping relationship between `Sid` and `Session` with a reasonable data structure and algorithm.You only need to implement the following methods to add, delete, modify and check sessions: + +```go +type Provider interface { + // 新增并返回一个 session + SessionCreate(sid string) (Session, error) + // 删除一个 session + SessionDestroy(sid string) + // 查找一个 session + SessionRead(sid string) (Session, error) + // 修改一个session + SessionUpdate(sid string) + // 通过类似 LRU 的算法回收过期的 session + SessionGC(maxLifeTime int64) +} +``` + + +Finally, `Manager`, most of the specific work is delegated to `Session` and the `Provider`, `Manager` is mainly a set of parameters, such as the survival time of the session, the strategy to clean up expired sessions, and the session's available storage methods. `Manager` blocks the specific details of the operation, and we can flexibly configure the session mechanism through `Manager`. + +In summary, the main reason for the session mechanism to be divided into several parts is decoupling and customization. I have seen several use Go to implement session services on Github, the source code is very simple, if you are interested you can learn: + +https://github.com/alexedwards/scs + +https://github.com/astaxie/build-web-application-with-golang + diff --git a/common_knowledge/linuxProcess.md b/common_knowledge/linuxProcess.md new file mode 100644 index 0000000000..05e99853c2 --- /dev/null +++ b/common_knowledge/linuxProcess.md @@ -0,0 +1,123 @@ +# What are Process, Thread, and File Descriptor in Linux? + +**Translator: [Seaworth](https://github.com/Seaworth)** + +**Author: [labuladong](https://github.com/labuladong)** + +Speaking of process, I am afraid that the most common problem of interviews is the relationship between thread and process. The answer is: **In Linux systems, there is almost no difference between process and thread**. + +A process of Linux is a data structure. You can clearly understand the underlying working principle of file descriptors, redirection, and pipeline commands. Finally, from the perspective of operating system, we can see why there is basically no difference between thread and process. + +### 一、What is a process? + +First, abstractly, our computer is this thing as follows: + +![](../pictures/linuxProcess/1.jpg) + +This large rectangle represents the computer's **memory space**, where the small rectangle represents **process**, the circle in the lower left corner represents **disk**, and the graph in the lower right corner represents some **input and output devices** , such as mouse, keyboard, monitor, etc. In addition, it is noted that the memory space is divided into two parts, the upper part represents **user space**, and the lower part represents **kernel space**. + +User space holds the resources that the user process needs to use. For example, if you create an array in the program, this array must exist in user space. Kernel space stores system resources that the kernel process needs to load. These resources are generally not allowed to be accessed by users. But some user processes can share some kernel space resources, such as some dynamic link libraries and so on. + +We write a hello program in C language, compile it to get an executable file, run it on the command line to display Hello World on the screen, and then exit the program. At the operating system level, a new process is created, which reads the executable file into memory space, executes it, and finally exits. + +**The executable program you compiled is just a file**, not a process. The executable file must be loaded into memory and packed into a process to really run. Processes are created by the operating system. Each process has its inherent attributes, such as process ID (PID), process status, open files, etc. After the process is created, ti reads into your program and your program will be executed by the system. + +So, how does the operating system create processes? **For the operating system, a process is a data structure**. Let's look directly at the Linux source code: + +```cpp +struct task_struct { + // Process status + /* -1 unrunnable, 0 runnable, >0 stopped: */ + long state; + // Virtual memory structure + struct mm_struct *mm; + // Process number + pid_t pid; + // Pointer to parent process + struct task_struct __rcu *parent; + // Children form the list of natural children: + struct list_head children; + // Pointer to filesystem information: + struct fs_struct *fs; + // Open file information: + struct files_struct *files; +}; +``` + +`task_struct` is the description of a process by the Linux kernel, which can also be called `process descriptor`. The [source code](https://github.com/torvalds/linux/blob/master/include/linux/sched.h) is more complicated. So I only intercepted a few common ones here. + +The interesting ones are the `mm` pointer and the `files` pointer. The `mm` pointer refers to the virtual memory of the process, which is where the resources and executable files are loaded. The `files` pointer points to an array containing pointers to all files opened by the process. + +### 二、What is a file descriptor? + +Let's start with `files`, which is an array of file pointers. Generally, a process will read input from `files[0]`, write output to `files[1]`, and write error information to `files[2]`. + +For example, from our perspective, the `printf` function in C is to print characters to the command line, but from the process perspective, it is to write data to `files[1]`. Similarly, the `scanf` function is that the process reads data from `files[0]`. + +**When each process is created, the first three bits of `files` are filled with default values, which point to standard input stream, standard output stream, and standard error stream, respectively. We often say `file descriptor` refers to the index of this file pointer array **. So the file descriptor of the program by default : 0 represents standard input (stdin), 1 is standard output (stdout), 2 is standard error (stderr). + +We can redraw a picture as follows: + +![](../pictures/linuxProcess/2.jpg) + +For general computers, input stream is the keyboard, output stream and error stream are both displays. So now this process is connected to the kernel with three wires. Because hardware resources are managed by the kernel, our process needs to let the kernel process access hardware resources through **system calls**. + +PS: Don't forget, everything is abstracted into files in Linux. And devices are also files, which can be read and written. + +If the program we wrote needs other resources, such as opening a file for reading and writing, this is also very simple. Make a system call and let the kernel open the file, and this file will be placed in the 4th position of `files`: + +![](../pictures/linuxProcess/3.jpg) + +Understand this principle, **input redirection** is easy to understand. When the program wants to read data, it will read `files[0]`. So we just point `files[0]` to a file. Then the program will read the data from this file instead of the keyboard. the **less-than character <** is used to redirect the input of a command. + +```shell +$ command < file.txt +``` + +![](../pictures/linuxProcess/5.jpg) + +Similarly, **output redirection** is to point `files[1]` to a file. So the output of the program will not be written to the display, but to this file. The **greater-than character >** is used for output redirection. + +```shell +$ command > file.txt +``` + +![](../pictures/linuxProcess/4.jpg) + +Error redirection is the same, so I will not go into details. + +**Pipe symbol** is actually the same. It connects the output stream of one process with the input stream of another process, and the data is passed in it. I have to say that this design idea is really beautiful. + +```shell +$ cmd1 | cmd2 | cmd3 +``` + +![](../pictures/linuxProcess/6.jpg) + +At this point, you may also see the clever design idea of **Everything is a file in Linux**. Whether it is a device, a process, a socket, or a real file, all of them can be read and written. And they are loaded into a simple `files` array. The specific details are delivered to the operating system, which is effectively decoupled, beautiful and efficient. + +### 三、What is a thread? + +The first thing to be clear is that multi-process and multi-thread can achieve concurrency to improve the utilization efficiency of the processor. So the key now is what's the difference between multi-thread and multi-process. + +Why is there basically no difference between thread and process in Linux? From the perspective of the Linux kernel, thread and process are not treated differently. + +We know that the system call `fork()` function can create a new child process. And the function `pthread()` can create a new thread. **But both thread and process are represented by the `task_struct` structure. The only difference is the shared data area**. + +In other words, threads look no different from processes. It's just that some data areas of a thread are shared with its parent process. However, a child process is a copy, not a share. For example, the `mm` structure and the ` files` structure are shared across threads, I drew two pictures and you will understand. + +![](../pictures/linuxProcess/7.jpg) + +![](../pictures/linuxProcess/8.jpg) + +Therefore, our multi-thread program should use the lock mechanism to avoid multiple threads writing data to the same area at the same time. Otherwise, data may be disordered. + +Then you may ask, **Since processes and threads are similar, and multi-process data is not shared, that is, there is no data disorder problem. Why is multi-thread use more common than multi-process?** + +Because in reality the concurrency of data sharing is more common. For example, ten people take ten yuan from one account at the same time. What we hope is that the balance of this shared account will be reduced by exactly one hundred yuan. Instead, each person gets a copy of the account, and each copy account is reduced by ten yuan. + +Of course, it must be explained that only Linux systems treat thread as process that shares data, and do not treat them specifically, do not treat thread and process differently. Many other operating systems treat thread and process differently. Threads have their own unique data structures. I personally think that this design is not as concise as Linux and increases the complexity of the system. + +Creating threads and processes are very efficient in Linux. For the problem of memory area copy, Linux uses the copy-on-write optimization strategy when creating a process. The memory space of parent process is not actually copied, but only copied during the write operation. **So creating processes and threads in Linux are very fast**. + +Stick to original high-quality articles, committed to making algorithmic problems clear. Welcome to follow us on WeChat public account **labuladong** for latest articles. \ No newline at end of file diff --git a/common_knowledge/linuxshell.md b/common_knowledge/linuxshell.md index 7805ebf834..ebc7837261 100644 --- a/common_knowledge/linuxshell.md +++ b/common_knowledge/linuxshell.md @@ -1,45 +1,51 @@ -我个人很喜欢使用 Linux 系统,虽然说 Windows 的图形化界面做的确实比 Linux 好,但是对脚本的支持太差了。一开始有点不习惯命令行操作,但是熟悉了之后反而发现移动鼠标点点点才是浪费时间的罪魁祸首。。。 +# Things you must know about Linux Shell -**那么对于 Linux 命令行,本文不是介绍某些命令的用法,而是说明一些简单却特别容易让人迷惑的细节问题**。 +**Translator: [Tianhao Zhou](https://github.com/tianhaoz95)** -1、标准输入和命令参数的区别。 +**Author: [labuladong](https://github.com/labuladong)** -2、在后台运行命令在退出终端后也全部退出了。 +Although Windows has advantages over Linux in terms of graphical interfaces, due to Windows' limited support for terminal scripts, I still prefer Linux. Using terminal may, at first, seem counterintuitive, but with more familiarity, it can become a timer saver over graphical interfaces. -3、单引号和双引号表示字符串的区别。 +**Instead of demonstrating the usage of Linux terminal, the post focuses on the basic yet confusing gotchas:** -4、有的命令和`sudo`一起用就 command not found。 +1. The difference between standard input and variables. -### 一、标准输入和参数的区别 +2. Why processes running on the background exit upon terminal termination? -这个问题一定是最容易让人迷惑的,具体来说,就是搞不清什么时候用管道符`|`和文件重定向`>`,`<`,什么时候用变量`$`。 +3. Single-quotes vs. double-quotes. -比如说,我现在有个自动连接宽带的 shell 脚本`connect.sh`,存在我的家目录: +4. How does `sudo` make commands `not found`? + +### 1. The difference between standard input and variables + +The difference between standard input and variables boils down to the question of when to use pipe `|` and redirecting `>`, `<` vs. when to use variables `$`. + +For example, if a shell script to automate ethernet connection locates in my home directory: ```shell $ where connect.sh /home/fdl/bin/connect.sh ``` -如果我想删除这个脚本,而且想少敲几次键盘,应该怎么操作呢?我曾经这样尝试过: +To remove the script with minimal effort, I tried: ```shell $ where connect.sh | rm ``` -实际上,这样操作是错误的,正确的做法应该是这样的: +However, the command above is incorrect. The proper way is: ```shell $ rm $(where connect.sh) ``` -前者试图将`where`的结果连接到`rm`的标准输入,后者试图将结果作为命令行参数传入。 +The former attempts to pipe the output from `where` into the standard input of `rm`, whereas the latter passes it in as an variable. -**标准输入就是编程语言中诸如`scanf`或者`readline`这种命令;而参数是指程序的`main`函数传入的`args`字符数组**。 +**Typically standard inputs appear in programming languages as `scanf` and `readline`; Variables refer to the literal arguments, `args`, the `main` program consumes.** -前文「Linux文件描述符」说过,管道符和重定向符是将数据作为程序的标准输入,而`$(cmd)`是读取`cmd`命令输出的数据作为参数。 +As mentioned in「Linux file descriptor」, Pipe and redirecting aim to use data as standard input. By contrast, `$(cmd)` reads the output from `cmd` as variables. -用刚才的例子说,`rm`命令源代码中肯定不接受标准输入,而是接收命令行参数,删除相应的文件。作为对比,`cat`命令是既接受标准输入,又接受命令行参数: +Revisiting the previous example, the source code of `rm` will certainly prefer receiving variable arguments over standard input to remove a file. In comparison, the `cat` command accepts both standard input and variables. ```shell $ cat filename @@ -52,22 +58,22 @@ $ echo 'hello world' | cat hello world ``` -**如果命令能够让终端阻塞,说明该命令接收标准输入,反之就是不接受**,比如你只运行`cat`命令不加任何参数,终端就会阻塞,等待你输入字符串并回显相同的字符串。 +**If a command can clog the terminal, then it accepts standard input and vice versa.** For example, running "cat" without arguments will suspend (intentionally clog) the terminal to wait for user input and print back the same content. -### 二、后台运行程序 +### 2. Why processes running on the background exit upon terminal termination? -比如说你远程登录到服务器上,运行一个 Django web 程序: +For example, we want to spin up a Django web server on a remote server: ```shell $ python manager.py runserver 0.0.0.0 Listening on 0.0.0.0:8080... ``` -现在你可以通过服务器的 IP 地址测试 Django 服务,但是终端此时就阻塞了,你输入什么都不响应,除非输入 Ctrl-C 或者 Ctrl-/ 终止 python 进程。 +With the server up and running, we can test it through the server's IP address. However, at the same time, the terminal will suspend, not responding to any input, until it detects `Ctrl-C`/`Ctrl-/` and kills the Python process. -可以在命令之后加一个`&`符号,这样命令行不会阻塞,可以响应你后续输入的命令,但是如果你退出服务器的登录,就不能访问该网页了。 +With a tailing `&`, the command won't clog the terminal and will continue to respond to incoming commands. However, the website becomes unavailable once you log out of the server. -如果你想在退出服务器之后仍然能够访问 web 服务,应该这样写命令 `(cmd &)`: +To keep the web service available after logging out of the server, consider using this command `(cmd &)`: ```shell $ (python manager.py runserver 0.0.0.0 &) @@ -76,35 +82,35 @@ Listening on 0.0.0.0:8080... $ logout ``` -**底层原理是这样的**: +**Under the hood:**: -每一个命令行终端都是一个 shell 进程,你在这个终端里执行的程序实际上都是这个 shell 进程分出来的子进程。正常情况下,shell 进程会阻塞,等待子进程退出才重新接收你输入的新的命令。加上`&`号,只是让 shell 进程不再阻塞,可以继续响应你的新命令。但是无论如何,你如果关掉了这个 shell 命令行端口,依附于它的所有子进程都会退出。 +Every terminal is a shell process, and it forks itself to provide child processes to execute commands. Usually, the shell process clogs while waiting for the child processes to exit, not accepting new commands. With a tailing `&`, the shell process allows issuing new commands. However, when the shell process exits upon the termination of the terminal window, all its child processes will exit. -而`(cmd &)`这样运行命令,则是将`cmd`命令挂到一个`systemd`系统守护进程名下,认`systemd`做爸爸,这样当你退出当前终端时,对于刚才的`cmd`命令就完全没有影响了。 +Nevertheless, commands like `(cmd &)` move the process under `systemd`, an OS guarded process that prevents the process from exiting when we close the current terminal. -类似的,还有一种后台运行常用的做法是这样: +An alternative approach to background execution is: ```shell $ nohub some_cmd & ``` -`nohub`命令也是类似的原理,不过通过我的测试,还是`(cmd &)`这种形式更加稳定。 +`nohub` functions similarly, but with extensive testing, `(cmd &)` appears to be more stable. -### 三、单引号和双引号的区别 +### 3. Single-quotes vs. double-quotes -不同的 shell 行为会有细微区别,但有一点是确定的,**对于`$`,`(`,`)`这几个符号,单引号包围的字符串不会做任何转义,双引号包围的字符串会转义**。 +Shells with different flavors behave differently, but with one invariant: **for `$`,`(`,`)`, single-quote won't trigger evaluation, but double-quote will.** -shell 的行为可以测试,使用`set -x`命令,会开启 shell 的命令回显,你可以通过回显观察 shell 到底在执行什么命令: +The shell behavior is observable through `set -x`, which triggers playback: ![](../pictures/linuxshell/1.png) -可见 `echo $(cmd)` 和 `echo "$(cmd)"`,结果差不多,但是仍然有区别。注意观察,双引号转义完成的结果会自动增加单引号,而前者不会。 +As shown above, `echo $(cmd)` and `echo "$(cmd)"` differ slightly. Look closely, double-quote adds single-quote after evaluation whereas single-quote doesn't. -**也就是说,如果 `$` 读取出的参数字符串包含空格,应该用双引号括起来,否则就会出错**。 +**As a result, if the literal value from `$` contains space, we should use double-quote to avoid errors.** -### 四、sudo 找不到命令 +### 4. How does `sudo` make commands `not found`? -有时候我们普通用户可以用的命令,用`sudo`加权限之后却报错 command not found: +Under certain situations, a command that non-privileged users can execute becomes "not found" when privileged users try to run with `sudo`: ```shell $ connect.sh @@ -114,21 +120,17 @@ $ sudo connect.sh sudo: command not found ``` -原因在于,`connect.sh`这个脚本仅存在于该用户的环境变量中: +The root cause is that the `connect.sh` script only exists in the user's environment variables. ```shell $ where connect.sh /home/fdl/bin/connect.sh ``` -**当使用`sudo`时,系统认为是 root 用户在执行命令,所以会去搜索 root 用户的环境变量**,而这个脚本在 root 的环境变量目录中当然是找不到的。 +**When prefixing `sudo`, we tell the OS that the `sudoer` is executing the command, so the OS will search the environment variables of the `sudoer`(defined in `/etc/sudoer`)** where the `connect.sh` script doesn't exist. -解决方法是使用脚本文件的路径,而不是仅仅通过脚本名称: +The solution is to locate the script with a path instead of a name: ```shell $ sudo /home/fdl/bin/connect.sh ``` - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/common_knowledge/linux\350\277\233\347\250\213.md" "b/common_knowledge/linux\350\277\233\347\250\213.md" deleted file mode 100644 index 836e44cb81..0000000000 --- "a/common_knowledge/linux\350\277\233\347\250\213.md" +++ /dev/null @@ -1,120 +0,0 @@ -# Linux的进程、线程、文件描述符是什么 - -说到进程,恐怕面试中最常见的问题就是线程和进程的关系了,那么先说一下答案:**在 Linux 系统中,进程和线程几乎没有区别**。 - -Linux 中的进程就是一个数据结构,看明白就可以理解文件描述符、重定向、管道命令的底层工作原理,最后我们从操作系统的角度看看为什么说线程和进程基本没有区别。 - -### 一、进程是什么 - -首先,抽象地来说,我们的计算机就是这个东西: - -![](../pictures/linuxProcess/1.jpg) - -这个大的矩形表示计算机的**内存空间**,其中的小矩形代表**进程**,左下角的圆形表示**磁盘**,右下角的图形表示一些**输入输出设备**,比如鼠标键盘显示器等等。另外,注意到内存空间被划分为了两块,上半部分表示**用户空间**,下半部分表示**内核空间**。 - -用户空间装着用户进程需要使用的资源,比如你在程序代码里开一个数组,这个数组肯定存在用户空间;内核空间存放内核进程需要加载的系统资源,这一些资源一般是不允许用户访问的。但是注意有的用户进程会共享一些内核空间的资源,比如一些动态链接库等等。 - -我们用 C 语言写一个 hello 程序,编译后得到一个可执行文件,在命令行运行就可以打印出一句 hello world,然后程序退出。在操作系统层面,就是新建了一个进程,这个进程将我们编译出来的可执行文件读入内存空间,然后执行,最后退出。 - -**你编译好的那个可执行程序只是一个文件**,不是进程,可执行文件必须要载入内存,包装成一个进程才能真正跑起来。进程是要依靠操作系统创建的,每个进程都有它的固有属性,比如进程号(PID)、进程状态、打开的文件等等,进程创建好之后,读入你的程序,你的程序才被系统执行。 - -那么,操作系统是如何创建进程的呢?**对于操作系统,进程就是一个数据结构**,我们直接来看 Linux 的源码: - -```cpp -struct task_struct { - // 进程状态 - long state; - // 虚拟内存结构体 - struct mm_struct *mm; - // 进程号 - pid_t pid; - // 指向父进程的指针 - struct task_struct __rcu *parent; - // 子进程列表 - struct list_head children; - // 存放文件系统信息的指针 - struct fs_struct *fs; - // 一个数组,包含该进程打开的文件指针 - struct files_struct *files; -}; -``` - -`task_struct`就是 Linux 内核对于一个进程的描述,也可以称为「进程描述符」。源码比较复杂,我这里就截取了一小部分比较常见的。 - -其中比较有意思的是`mm`指针和`files`指针。`mm`指向的是进程的虚拟内存,也就是载入资源和可执行文件的地方;`files`指针指向一个数组,这个数组里装着所有该进程打开的文件的指针。 - -### 二、文件描述符是什么 - -先说`files`,它是一个文件指针数组。一般来说,一个进程会从`files[0]`读取输入,将输出写入`files[1]`,将错误信息写入`files[2]`。 - -举个例子,以我们的角度 C 语言的`printf`函数是向命令行打印字符,但是从进程的角度来看,就是向`files[1]`写入数据;同理,`scanf`函数就是进程试图从`files[0]`这个文件中读取数据。 - -**每个进程被创建时,`files`的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件指针数组的索引**,所以程序的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。 - -我们可以重新画一幅图: - -![](../pictures/linuxProcess/2.jpg) - -对于一般的计算机,输入流是键盘,输出流是显示器,错误流也是显示器,所以现在这个进程和内核连了三根线。因为硬件都是由内核管理的,我们的进程需要通过「系统调用」让内核进程访问硬件资源。 - -PS:不要忘了,Linux 中一切都被抽象成文件,设备也是文件,可以进行读和写。 - -如果我们写的程序需要其他资源,比如打开一个文件进行读写,这也很简单,进行系统调用,让内核把文件打开,这个文件就会被放到`files`的第 4 个位置: - -![](../pictures/linuxProcess/3.jpg) - -明白了这个原理,**输入重定向**就很好理解了,程序想读取数据的时候就会去`files[0]`读取,所以我们只要把`files[0]`指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘: - -```shell -$ command < file.txt -``` - -![](../pictures/linuxProcess/5.jpg) - -同理,**输出重定向**就是把`files[1]`指向一个文件,那么程序的输出就不会写入到显示器,而是写入到这个文件中: - -```shell -$ command > file.txt -``` - -![](../pictures/linuxProcess/4.jpg) - -错误重定向也是一样的,就不再赘述。 - -**管道符**其实也是异曲同工,把一个进程的输出流和另一个进程的输入流接起一条「管道」,数据就在其中传递,不得不说这种设计思想真的很优美: - -```shell -$ cmd1 | cmd2 | cmd3 -``` - -![](../pictures/linuxProcess/6.jpg) - -到这里,你可能也看出「Linux 中一切皆文件」设计思路的高明了,不管是设备、另一个进程、socket 套接字还是真正的文件,全部都可以读写,统一装进一个简单的`files`数组,进程通过简单的文件描述符访问相应资源,具体细节交于操作系统,有效解耦,优美高效。 - -### 三、线程是什么 - -首先要明确的是,多进程和多线程都是并发,都可以提高处理器的利用效率,所以现在的关键是,多线程和多进程有啥区别。 - -为什么说 Linux 中线程和进程基本没有区别呢,因为从 Linux 内核的角度来看,并没有把线程和进程区别对待。 - -我们知道系统调用`fork()`可以新建一个子进程,函数`pthread()`可以新建一个线程。**但无论线程还是进程,都是用`task_struct`结构表示的,唯一的区别就是共享的数据区域不同**。 - -换句话说,线程看起来跟进程没有区别,只是线程的某些数据区域和其父进程是共享的,而子进程是拷贝副本,而不是共享。就比如说,`mm`结构和`files`结构在线程中都是共享的,我画两张图你就明白了: - -![](../pictures/linuxProcess/7.jpg) - -![](../pictures/linuxProcess/8.jpg) - -所以说,我们的多线程程序要利用锁机制,避免多个线程同时往同一区域写入数据,否则可能造成数据错乱。 - -那么你可能问,**既然进程和线程差不多,而且多进程数据不共享,即不存在数据错乱的问题,为什么多线程的使用比多进程普遍得多呢**? - -因为现实中数据共享的并发更普遍呀,比如十个人同时从一个账户取十元,我们希望的是这个共享账户的余额正确减少一百元,而不是希望每人获得一个账户的拷贝,每个拷贝账户减少十元。 - -当然,必须要说明的是,只有 Linux 系统将线程看做共享数据的进程,不对其做特殊看待,其他的很多操作系统是对线程和进程区别对待的,线程有其特有的数据结构,我个人认为不如 Linux 的这种设计简洁,增加了系统的复杂度。 - -在 Linux 中新建线程和进程的效率都是很高的,对于新建进程时内存区域拷贝的问题,Linux 采用了 copy-on-write 的策略优化,也就是并不真正复制父进程的内存空间,而是等到需要写操作时才去复制。**所以 Linux 中新建进程和新建线程都是很迅速的**。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/common_knowledge/session\345\222\214cookie.md" "b/common_knowledge/session\345\222\214cookie.md" deleted file mode 100644 index ccabf1b222..0000000000 --- "a/common_knowledge/session\345\222\214cookie.md" +++ /dev/null @@ -1,132 +0,0 @@ -cookie 大家应该都熟悉,比如说登录某些网站一段时间后,就要求你重新登录;再比如有的同学很喜欢玩爬虫技术,有时候网站就是可以拦截住你的爬虫,这些都和 cookie 有关。如果你明白了服务器后端对于 cookie 和 session 的处理逻辑,就可以解释这些现象,甚至钻一些空子无限白嫖,待我慢慢道来。 - -### 一、session 和 cookie 简介 - -cookie 的出现是因为 HTTP 是无状态的一种协议,换句话说,服务器记不住你,可能你每刷新一次网页,就要重新输入一次账号密码进行登录。这显然是让人无法接受的,cookie 的作用就好比服务器给你贴个标签,然后你每次向服务器再发请求时,服务器就能够 cookie 认出你。 - -抽象地概括一下:**一个 cookie 可以认为是一个「变量」,形如 `name=value`,存储在浏览器;一个 session 可以理解为一种数据结构,多数情况是「映射」(键值对),存储在服务器上**。 - -注意,我说的是「一个」cookie 可以认为是一个变量,但是服务器可以一次设置多个 cookie,所以有时候说 cookie 是「一组」键值对儿,这也可以说得通。 - -cookie 可以在服务器端通过 HTTP 的 SetCookie 字段设置 cookie,比如我用 Go 语言写的一个简单服务: - -```go -func cookie(w http.ResponseWriter, r *http.Request) { - // 设置了两个 cookie - http.SetCookie(w, &http.Cookie{ - Name: "name1", - Value: "value1", - }) - - http.SetCookie(w, &http.Cookie{ - Name: "name2", - Value: "value2", - }) - // 将字符串写入网页 - fmt.Fprintln(w, "页面内容") -} -``` - -当浏览器访问对应网址时,通过浏览器的开发者工具查看此次 HTTP 通信的细节,可以看见服务器的回应发出了两次 `SetCookie` 命令: - -![](../pictures/session/1.png) - -在这之后,浏览器的请求中的 `Cookie` 字段就带上了这两个 cookie: - -![](../pictures/session/2.png) - -**cookie 的作用其实就是这么简单,无非就是服务器给每个客户端(浏览器)打的标签**,方便服务器辨认而已。当然,HTTP 还有很多参数可以设置 cookie,比如过期时间,或者让某个 cookie 只有某个特定路径才能使用等等。 - -但问题是,我们也知道现在的很多网站功能很复杂,而且涉及很多的数据交互,比如说电商网站的购物车功能,信息量大,而且结构也比较复杂,无法通过简单的 cookie 机制传递这么多信息,而且要知道 cookie 字段是存储在 HTTP header 中的,就算能够承载这些信息,也会消耗很多的带宽,比较消耗网络资源。 - -session 就可以配合 cookie 解决这一问题,比如说一个 cookie 存储这样一个变量 `sessionID=xxxx`,仅仅把这一个 cookie 传给服务器,然后服务器通过这个 ID 找到对应的 session,这个 session 是一个数据结构,里面存储着该用户的购物车等详细信息,服务器可以通过这些信息返回该用户的定制化网页,有效解决了追踪用户的问题。 - -**session 是一个数据结构,由网站的开发者设计,所以可以承载各种数据**,只要客户端的 cookie 传来一个唯一的 session ID,服务器就可以找到对应的 session,认出这个客户。 - -当然,由于 session 存储在服务器中,肯定会消耗服务器的资源,所以 session 一般都会有一个过期时间,服务器一般会定期检查并删除过期的 session,如果后来该用户再次访问服务器,可能就会面临重新登录等等措施,然后服务器新建一个 session,将 session ID 通过 cookie 的形式传送给客户端。 - -那么,我们知道 cookie 和 session 的原理,有什么切实的好处呢?**除了应对面试,我给你说一个鸡贼的用处,就是可以白嫖某些服务**。 - -有些网站,你第一次使用它的服务,它直接免费让你试用,但是用一次之后,就让你登录然后付费继续使用该服务。而且你发现网站似乎通过某些手段记住了你的电脑,除非你换个电脑或者换个浏览器才能再白嫖一次。 - -那么问题来了,你试用的时候没有登录,网站服务器是怎么记住你的呢?这就很显然了,服务器一定是给你的浏览器打了 cookie,后台建立了对应的 session 记录你的状态。你的浏览器在每次访问该网站的时候都会听话地带着 cookie,服务器一查 session 就知道这个浏览器已经免费使用过了,得让它登录付费,不能让它继续白嫖了。 - -那如果我不让浏览器发送 cookie,每次都伪装成一个第一次来试用的小萌新,不就可以不断白嫖了么?浏览器会把网站的 cookie 以文件的形式存在某些地方(不同的浏览器配置不同),你把他们找到然后删除就行了。但是对于 Firefox 和 Chrome 浏览器,有很多插件可以直接编辑 cookie,比如我的 Chrome 浏览器就用的一款叫做 EditThisCookie 的插件,这是他们官网: - -![http://www.editthiscookie.com/](../pictures/session/3.png) - -这类插件可以读取浏览器在当前网页的 cookie,点开插件可以任意编辑和删除 cookie。**当然,偶尔白嫖一两次还行,不鼓励高频率白嫖,想常用还是掏钱吧,否则网站赚不到钱,就只能取消免费试用这个机制了**。 - -以上就是关于 cookie 和 session 的简单介绍,cookie 是 HTTP 协议的一部分,不算复杂,而 session 是可以定制的,所以下面详细看一下实现 session 管理的代码架构吧。 - -### 二、session 的实现 - -session 的原理不难,但是具体实现它可是很有技巧的,一般需要三个组件配合完成,它们分别是 `Manager`、`Provider` 和 `Session` 三个类(接口)。 - -![](../pictures/session/4.jpg) - -1、浏览器通过 HTTP 协议向服务器请求路径 `/content` 的网页资源,对应路径上有一个 Handler 函数接收请求,解析 HTTP header 中的 cookie,得到其中存储的 sessionID,然后把这个 ID 发给 `Manager`。 - -2、`Manager` 充当一个 session 管理器的角色,主要存储一些配置信息,比如 session 的存活时间,cookie 的名字等等。而所有的 session 存在 `Manager` 内部的一个 `Provider` 中。所以 `Manager` 会把 `sid`(sessionID)传递给 `Provider`,让它去找这个 ID 对应的具体是哪个 session。 - -3、`Provider` 就是一个容器,最常见的应该就是一个散列表,将每个 `sid` 和对应的 session 一一映射起来。收到 `Manager` 传递的 `sid` 之后,它就找到 `sid` 对应的 session 结构,也就是 `Session` 结构,然后返回它。 - -4、`Session` 中存储着用户的具体信息,由 Handler 函数中的逻辑拿出这些信息,生成该用户的 HTML 网页,返回给客户端。 - -那么你也许会问,为什么搞这么麻烦,直接在 Handler 函数中搞一个哈希表,然后存储 `sid` 和 `Session` 结构的映射不就完事儿了? - -**这就是设计层面的技巧了**,下面就来说说,为什么分成 `Manager`、`Provider` 和 `Session`。 - - -先从最底层的 `Session` 说。既然 session 就是键值对,为啥不直接用哈希表,而是要抽象出这么一个数据结构呢? - -第一,因为 `Session` 结构可能不止存储了一个哈希表,还可以存储一些辅助数据,比如 `sid`,访问次数,过期时间或者最后一次的访问时间,这样便于实现想 LRU、LFU 这样的算法。 - -第二,因为 session 可以有不同的存储方式。如果用编程语言内置的哈希表,那么 session 数据就是存储在内存中,如果数据量大,很容易造成程序崩溃,而且一旦程序结束,所有 session 数据都会丢失。所以可以有很多种 session 的存储方式,比如存入缓存数据库 Redis,或者存入 MySQL 等等。 - -因此,`Session` 结构提供一层抽象,屏蔽不同存储方式的差异,只要提供一组通用接口操纵键值对: - -```go -type Session interface { - // 设置键值对 - Set(key, val interface{}) - // 获取 key 对应的值 - Get(key interface{}) interface{} - // 删除键 key - Delete(key interface{}) -} -``` - -再说 `Provider` 为啥要抽象出来。我们上面那个图的 `Provider` 就是一个散列表,保存 `sid` 到 `Session` 的映射,但是实际中肯定会更加复杂。我们不是要时不时删除一些 session 吗,除了设置存活时间之外,还可以采用一些其他策略,比如 LRU 缓存淘汰算法,这样就需要 `Provider` 内部使用哈希链表这种数据结构来存储 session。 - -PS:关于 LRU 算法的奥妙,参见前文「LRU 算法详解」。 - -因此,`Provider` 作为一个容器,就是要屏蔽算法细节,以合理的数据结构和算法组织 `sid` 和 `Session` 的映射关系,只需要实现下面这几个方法实现对 session 的增删查改: - -```go -type Provider interface { - // 新增并返回一个 session - SessionCreate(sid string) (Session, error) - // 删除一个 session - SessionDestroy(sid string) - // 查找一个 session - SessionRead(sid string) (Session, error) - // 修改一个session - SessionUpdate(sid string) - // 通过类似 LRU 的算法回收过期的 session - SessionGC(maxLifeTime int64) -} -``` - - -最后说 `Manager`,大部分具体工作都委托给 `Session` 和 `Provider` 承担了,`Manager` 主要就是一个参数集合,比如 session 的存活时间,清理过期 session 的策略,以及 session 的可用存储方式。`Manager` 屏蔽了操作的具体细节,我们可以通过 `Manager` 灵活地配置 session 机制。 - -综上,session 机制分成几部分的最主要原因就是解耦,实现定制化。我在 Github 上看过几个 Go 语言实现的 session 服务,源码都很简单,有兴趣的朋友可以学习学习: - -https://github.com/alexedwards/scs - -https://github.com/astaxie/build-web-application-with-golang - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/common_knowledge/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" "b/common_knowledge/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" deleted file mode 100644 index 079037ba74..0000000000 --- "a/common_knowledge/\345\234\250\347\272\277\347\273\203\344\271\240\345\271\263\345\217\260.md" +++ /dev/null @@ -1,96 +0,0 @@ -虽说我没事就喜欢喷应试教育,但我也从应试教育中发现了一个窍门:如果能够以刷题的形式学习某项技能,效率和效果是最佳的。对于技术的学习,我经常面临的困境是,**理论知识知道的不少,但是有的场景实在无法模拟,缺少亲自动手实践的机会**,如果能有一本带标准答案的习题册让我刷刷就好了。 - -所以在学习新技术时,我首先会去搜索是否有在线刷题平台,你还别说,有的大神真就做了很不错的在线练习平台,下面就介绍几个平台,分别是学习 Git、SQL、正则表达式的在线练习平台。 - -### 一、练习 Git - -这是个叫做 Learning Git Branching 的项目,是我一定要推荐的: - -![](../pictures/online/1.png) - -正如对话框中的自我介绍,这确实也是我至今发现的**最好**的 Git 动画教程,没有之一。 - -想当年我用 Git 就会 `add .`,`clone`,`push`,`pull`,`commit` 几个命令,其他的命令完全不会,Git 就是一个下载器,Github 就是个资源网站加免费图床,命令能不能达成目的都是靠运气。什么版本控制,我根本搞不懂,也懒得去看那一堆乱七八糟的文档。 - -这个网站的教程不是给你举那种修改文件的细节例子,而是将每次 `commit` 都抽象成树的节点,**用动画闯关的形式**,让你自由使用 Git 命令完成目标: - -![](../pictures/online/2.png) - -所有 Git 分支都被可视化了,你只要在左侧的命令行输入 Git 命令,分支会进行相应的变化,只要达成任务目标,你就过关啦!网站还会记录你的命令数,试试能不能以最少的命令数过关! - -![](../pictures/online/3.png) - -我一开始以为这个教程只包含本地 Git 仓库的版本管理,**后来我惊奇地发现它还有远程仓库的操作教程**! - -![](../pictures/online/4.png) - -![](../pictures/online/5.png) - -真的跟玩游戏一样,难度设计合理,流畅度很好,我一玩都停不下来了,几小时就打通了,哈哈哈! - -![](../pictures/online/6.png) - -总之,这个教程很适合初学和进阶,如果你觉得自己对 Git 的掌握还不太好,用 Git 命令还是靠碰运气,就可以玩玩这个教程,相信能够让你更熟练地使用 Git。 - -它是一个开源项目,Github 项目地址: - -https://github.com/pcottle/learnGitBranching - -教程网站地址: - -https://learngitbranching.js.org - -### 二、练习正则表达式 - -**正则表达式是个非常强有力的工具**,可以说计算机中的一切数据都是字符,借助正则表达式这种模式匹配工具,操作计算机可以说是如虎添翼。 - -我这里要推荐两个网站,一个是练习平台,一个是测试正则表达式的平台。 - -先说练习平台,叫做 RegexOne: - -![](../pictures/online/9.png) - -前面有基本教程,后面有一些常见的正则表达式题目,比如判断邮箱、URL、电话号,或者抽取日志的关键信息等等。 - -只要写出符合要求的正则表达式,就可以进入下一个问题,关键是每道题还有标准答案,可以点击下面的 solution 按钮查看: - -![](../pictures/online/10.png) - -RegexOne 网址: - -https://regexone.com/ - -再说测试工具,是个叫做 RegExr 的 Github 项目,这是它的网站: - -![](../pictures/online/11.png) - -可以看见,输入文本和正则模式串后,**网站会给正则表达式添加好看且容易辨认的样式,自动在文本中搜索模式串,高亮显示匹配的字符串,并且还会显示每个分组捕获的字符串**。 - -这个网站可以配合前面的正则练习平台使用,在这里尝试各种表达式,成功匹配之后粘贴过去。 - -RegExr 网址: - -https://regexr.com/ - -### 三、练习 SQL - -这是一个叫做 SQLZOO 的网站,左侧是所有的练习内容: - -![](../pictures/online/7.png) - -SQLZOO 是一款很好用的 SQL 练习平台,英文不难理解,可以直接看英文版,但是也可以切换繁体中文,比较友好。 - -这里都是比较常用的 SQL 命令,给你一个需求,你写 SQL 语句实现正确的查询结果。**最重要的是,这里不仅对每个命令的用法有详细解释,每个专题后面还有选择题(quiz),而且有判题系统,甚至有的比较难的题目还有视频讲解**: - -![](../pictures/online/8.png) - -至于难度,循序渐进,即便对新手也很友好,靠后的问题确实比较有技巧性,相信这是热爱思维挑战的人喜欢的!LeetCode 也有 SQL 相关的题目,不过难度一般比较大,我觉得 SQLZOO 刷完基础 SQL 命令再去 LeetCode 刷比较合适。 - -网站地址: - -https://sqlzoo.net/ - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/common_knowledge/\345\257\206\347\240\201\346\212\200\346\234\257.md" "b/common_knowledge/\345\257\206\347\240\201\346\212\200\346\234\257.md" deleted file mode 100644 index a8ade205d3..0000000000 --- "a/common_knowledge/\345\257\206\347\240\201\346\212\200\346\234\257.md" +++ /dev/null @@ -1,179 +0,0 @@ -说到密码,我们第一个想到的就是登陆账户的密码,但是从密码学的角度来看,这种根本就不算合格的密码。 - -为什么呢,因为我们的账户密码,是依靠隐蔽性来达到加密作用:密码藏在我心里,你不知道,所以你登不上我的账户。 - -然而密码技术认为,「保密」信息总有一天会被扒出来,所以加密算法不应该依靠「保密」来保证机密性,而应该做到:即便知道了加密算法,依然无计可施。说的魔幻一点就是,告诉你我的密码,你依然不知道我的密码。 - -最玄学的就是 Diffie-Hellman 密钥交换算法,我当初就觉得很惊奇,两个人当着你的面互相报几个数字,他们就可以拥有一个共同的秘密,而你却根本不可能算出来这个秘密。下文会着重介绍一下这个算法。 - -本文讨论的密码技术要解决的主要是信息传输中的加密和解密问题。要假设数据传输过程是不安全的,所有信息都在被窃听的,所以发送端要把信息加密,接收方收到信息之后,肯定得知道如何解密。有意思的是,如果你能够让接收者知道如何解密,那么窃听者不是也能够知道如何解密了吗? - -下面,**我们会介绍对称加密算法、密钥交换算法、非对称加密算法、数字签名、公钥证书**,看看解决安全传输问题的一路坎坷波折。 - -### 一、对称性加密 - -对称性密码,也叫共享密钥密码,顾名思义,这种加密方式用相同的密钥进行加密和解密。 - -比如我说一种最简单的对称加密的方法。首先我们知道信息都可以表示成 0/1 比特序列,也知道相同的两个比特序列做异或运算的结果为 0。 - -那么我们就可以生成一个长度和原始信息一样的随机比特序列作为密钥,然后用它对原始信息做异或运算,就生成了密文。反之,再用该密钥对密文做一次异或运算,就可以恢复原始信息。 - -这是一个简单例子,不过有些过于简单,有很多问题。比如密钥的长度和原始信息完全一致,如果原始信息很大,密钥也会一样大,而且生成大量真随机比特序列的计算开销也比较大。 - -当然,有很多更复杂优秀的对称加密算法解决了这些问题,比如 Rijndael 算法、三重 DES 算法等等。**它们从算法上是无懈可击的,也就是拥有巨大的密钥空间,基本无法暴力破解,而且加密过程相对快速**。 - -**但是,一切对称加密算法的软肋在于密钥的配送**。加密和解密用同一个密钥,发送方必须设法把密钥发送给接收方。如果窃听者有能力窃取密文,肯定也可以窃取密钥,那么再无懈可击的算法依然不攻自破。 - -所以,下面介绍两种解决密钥配送问题最常见的算法,分别是 Diffie-Hellman 密钥交换算法和非对称加密算法。 - -### 二、密钥交换算法 - -我们所说的密钥一般就是一个很大的数字,算法用这个数加密、解密。问题在于,信道是不安全的,所有发出的数据都会被窃取。换句话说,有没有一种办法,能够让两个人在众目睽睽之下,光明正大地交换一个秘密,把对称性密钥安全地送到接收方的手中? - -Diffie-Hellman 密钥交换算法可以做到。**准确的说,该算法并不是把一个秘密安全地「送给」对方,而是通过一些共享的数字,双方「心中」各自「生成」了一个相同的秘密,而且双方的这个秘密,是第三方窃听者无法生成的**。 - -也许这就是传说中的心有灵犀一点通吧。 - -这个算法规则不算复杂,你甚至都可以找个朋友尝试一下共享秘密,等会我会简单画出它的基本流程。在此之前,需要明确一个问题:**并不是所有运算都有逆运算**。 - -最简单的例子就是我们熟知的单向散列函数,给一个数字 `a` 和一个散列函数 `f`,你可以很快计算出 `f(a)`,但是如果给你 `f(a)` 和 `f`,推出 `a` 是一件基本做不到的事。密钥交换算法之所以看起来如此玄幻,就是利用了这种不可逆的性质。 - -下面,看下密钥交换算法的流程是什么,按照命名惯例,准备执行密钥交换算法的双方称为 Alice 和 Bob,在网络中企图窃取他俩通信内容的坏人称为 Hack 吧。 - -首先,Alice 和 Bob 协商出两个数字 `N` 和 `G` 作为生成元,当然协商过程可以被窃听者 Hack 窃取,所以我把这两个数画到中间,代表三方都知道: - -![](../pictures/密码技术/1.jpg) - -现在 Alice 和 Bob **心中**各自想一个数字出来,分别称为 `A` 和 `B` 吧: - -![](../pictures/密码技术/2.jpg) - -现在 Alice 将自己心里的这个数字 `A` 和 `G` 通过某些运算得出一个数 `AG`,然后发给 Bob;Bob 将自己心里的数 `B` 和 `G` 通过相同的运算得出一个数 `BG`,然后发给 Alice: - -![](../pictures/密码技术/3.jpg) - -现在的情况变成这样了: - -![](../pictures/密码技术/4.jpg) - -注意,类似刚才举的散列函数的例子,知道 `AG` 和 `G`,并不能反推出 `A` 是多少,`BG` 同理。 - -那么,Alice 可以通过 `BG` 和自己的 `A` 通过某些运算得到一个数 `ABG`,Bob 也可以通过 `AG` 和自己的 `B` 通过某些运算得到 `ABG`,这个数就是 Alice 和 Bob 共有的秘密。 - -而对于 Hack,可以窃取传输过程中的 `G`,`AG`,`BG`,但是由于计算不可逆,怎么都无法结合出 `ABG` 这个数字。 - -![](../pictures/密码技术/5.jpg) - -以上就是基本流程,至于具体的数字取值是有讲究的,运算方法在百度上很容易找到,限于篇幅我就不具体写了。 - -该算法可以在第三者窃听的前提下,算出一个别人无法算出的秘密作为对称性加密算法的密钥,开始对称加密的通信。 - -对于该算法,Hack 又想到一种破解方法,不是窃听 Alice 和 Bob 的通信数据,而是直接同时冒充 Alice 和 Bob 的身份,也就是我们说的「**中间人攻击**」: - -![](../pictures/密码技术/6.jpg) - -这样,双方根本无法察觉在和 Hack 共享秘密,后果就是 Hack 可以解密甚至修改数据。 - -**可见,密钥交换算法也不算完全解决了密钥配送问题,缺陷在于无法核实对方身份**。所以密钥交换算法之前一般要核实对方身份,比如使用数字签名。 - -### 三、非对称加密 - -非对称加密的思路就是,干脆别偷偷摸摸传输密钥了,我把加密密钥和解密密钥分开,公钥用于加密,私钥用于解密。只把公钥传送给对方,然后对方开始给我发送加密的数据,我用私钥就可以解密。至于窃听者,拿到公钥和加密数据也没用,因为只有我手上的私钥才能解密。 - -可以这样想,**私钥是钥匙,而公钥是锁,可以把锁公开出去,让别人把数据锁起来发给我;而钥匙一定要留在自己手里,用于解锁**。我们常见的 RSA 算法就是典型的非对称加密算法,具体实现比较复杂,我就不写了,网上很多资料。 - -在实际应用中,非对称性加密的运算速度要比对称性加密慢很多的,所以传输大量数据时,一般不会用公钥直接加密数据,而是加密对称性加密的密钥,传输给对方,然后双方使用对称性加密算法传输数据。 - -需要注意的是,类似 Diffie-Hellman 算法,**非对称加密算法也无法确定通信双方的身份,依然会遭到中间人攻击**。比如 Hack 拦截 Bob 发出的公钥,然后冒充 Bob 的身份给 Alice 发送自己的公钥,那么不知情的 Alice 就会把私密数据用 Hack 的公钥加密,Hack 可以通过私钥解密窃取。 - -那么,Diffie-Hellman 算法和 RSA 非对称加密算法都可以一定程度上解决密钥配送的问题,也具有相同的缺陷,二者的应用场景有什么区别呢? - -简单来说,根据两种算法的基本原理就可以看出来: - -如果双方有一个对称加密方案,希望加密通信,而且不能让别人得到钥匙,那么可以使用 Diffie-Hellman 算法交换密钥。 - -如果你希望任何人都可以对信息加密,而只有你能够解密,那么就使用 RSA 非对称加密算法,公布公钥。 - -下面,我们尝试着解决认证发送方身份的问题。 - -### 四、数字签名 - -刚才说非对称加密,把公钥公开用于他人对数据加密然后发给你,只有用你手上对应的私钥才能将密文解密。其实,**私钥也可用用来加密数据的,对于 RSA 算法,私钥加密的数据只有公钥才能解开**。 - -数字签名也是利用了非对称性密钥的特性,但是和公钥加密完全颠倒过来:**仍然公布公钥,但是用你的私钥加密数据,然后把加密的数据公布出去,这就是数字签名**。 - -你可能问,这有什么用,公钥可以解开私钥加密,我还加密发出去,不是多此一举吗? - -是的,但是**数字签名的作用本来就不是保证数据的机密性,而是证明你的身份**,证明这些数据确实是由你本人发出的。 - -你想想,你的私钥加密的数据,只有你的公钥才能解开,那么如果一份加密数据能够被你的公钥解开,不就说明这份数据是你(私钥持有者)本人发布的吗? - -当然,加密数据仅仅是一个签名,签名应该和数据一同发出,具体流程应该是: - -1、Bob 生成公钥和私钥,然后把公钥公布出去,私钥自己保留。 - -2、**用私钥加密数据作为签名,然后将数据附带着签名一同发布出去**。 - -3、Alice 收到数据和签名,需要检查此份数据是否是 Bob 所发出,于是用 Bob 之前发出的公钥尝试解密签名,将收到的数据和签名解密后的结果作对比,如果完全相同,说明数据没被篡改,且确实由 Bob 发出。 - -为什么 Alice 这么肯定呢,毕竟数据和签名是两部分,都可以被掉包呀?原因如下: - -1、如果有人修改了数据,那么 Alice 解密签名之后,对比发现二者不一致,察觉出异常。 - -2、如果有人替换了签名,那么 Alice 用 Bob 的公钥只能解出一串乱码,显然和数据不一致。 - -3、也许有人企图修改数据,然后将修改之后的数据制成签名,使得 Alice 的对比无法发现不一致;但是一旦解开签名,就不可能再重新生成 Bob 的签名了,因为没有 Bob 的私钥。 - -综上,**数字签名可以一定程度上认证数据的来源**。之所以说是一定程度上,是因为这种方式依然可能受到中间人攻击。一旦涉及公钥的发布,接收方就可能收到中间人的假公钥,进行错误的认证,这个问题始终避免不了。 - -说来可笑,数字签名就是验证对方身份的一种方式,但是前提是对方的身份必须是真的... 这似乎陷入一个先有鸡还是先有蛋的死循环,**要想确定对方的身份,必须有一个信任的源头,否则的话,再多的流程也只是在转移问题,而不是真正解决问题**。 - -### 五、公钥证书 - -**证书其实就是公钥 + 签名,由第三方认证机构颁发**。引入可信任的第三方,是终结信任循环的一种可行方案。 - -证书认证的流程大致如下: - -1、Bob 去可信任的认证机构证实本人真实身份,并提供自己的公钥。 - -2、Alice 想跟 Bob 通信,首先向认证机构请求 Bob 的公钥,认证机构会把一张证书(Bob 的公钥以及自己对其公钥的签名)发送给 Alice。 - -3、Alice 检查签名,确定该公钥确实由这家认证机构发送,中途未被篡改。 - -4、Alice 通过这个公钥加密数据,开始和 Bob 通信。 - -![图片来自《图解密码技术》](../pictures/密码技术/7.jpg) - -PS:以上只是为了说明,证书只需要安装一次,并不需要每次都向认证机构请求;一般是服务器直接给客户端发送证书,而不是认证机构。 - -也许有人问,Alice 要想通过数字签名确定证书的有效性,前提是要有该机构的(认证)公钥,这不是又回到刚才的死循环了吗? - -我们安装的正规浏览器中都预存了正规认证机构的证书(包含其公钥),用于确认机构身份,所以说证书的认证是可信的。 - -Bob 向机构提供公钥的过程中,需要提供很多个人信息进行身份验证,比较严格,所以说也算是可靠的。 - -获得了 Bob 的可信公钥,Alice 和 Bob 之间的通信基于加密算法的保护,是完全无懈可击的。 - -现在的正规网站,大都使用 HTTPS 协议,就是在 HTTP 协议和 TCP 协议之间加了一个 SSL/TLS 安全层。在你的浏览器和网站服务器完成 TCP 握手后,SSL 协议层也会进行 SSL 握手交换安全参数,其中就包含该网站的证书,以便浏览器验证站点身份。SSL 安全层验证完成之后,上层的 HTTP 协议内容都会被加密,保证数据的安全传输。 - -这样一来,传统的中间人攻击就几乎没有了生存空间,攻击手段只能由技术缺陷转变为坑蒙拐骗。事实上,这种手段的效果反而更高效,比如我就发现**网上不少下载网站发布的浏览器,不仅包含乱七八糟的导航和收藏网址,还包含一些不正规的认证机构证书。任何人都可以申请证书,这些不正规证书很可能造成安全隐患**。 - -### 六、最后总结 - -对称性加密算法使用同一个密钥加密和解密,难以破解,加密速度较快,但是存在密钥配送问题。 - -Diffie-Hellman 密钥交换算法可以让双方「心有灵犀一点通」,一定程度解决密钥配送问题,但是无法验证通信方的身份,所以可能受到中间人攻击。 - -非对称性加密算法生成一对儿密钥,把加密和解密的工作分开了。 - -RSA 算法作为经典的非对称加密算法,有两种用途:如果用于加密,可以把公钥发布出去用于加密,只有自己的私钥可以解密,保证了数据的机密性;如果用于数字签名,把公钥发布出去后,用私钥加密数据作为签名,以证明该数据由私钥持有者所发送。但是无论那种用法,涉及公钥的发布,都无法避免中间人攻击。 - -公钥证书就是公钥 + 签名,由可信任的第三方认证机构颁发。由于正规浏览器都预装了可信的认证机构的公钥,所以可以有效防止中间人攻击。 - -HTTPS 协议中的 SSL/TLS 安全层会组合使用以上几种加密方式,**所以说不要安装非正规的浏览器,不要乱安装未知来源的证书**。 - -密码技术只是安全的一小部分,即便是通过正规机构认证的 HTTPS 站点,也不意味着可信任,只能说明其数据传输是安全的。技术永远不可能真正保护你,最重要的还是得提高个人的安全防范意识,多留心眼儿,谨慎处理敏感数据。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git a/data_structure/ImplementQueueUsingStacksImplementStackUsingQueues.md b/data_structure/ImplementQueueUsingStacksImplementStackUsingQueues.md index e847db4870..5baf2e3c64 100644 --- a/data_structure/ImplementQueueUsingStacksImplementStackUsingQueues.md +++ b/data_structure/ImplementQueueUsingStacksImplementStackUsingQueues.md @@ -1,8 +1,8 @@ # Implement Queue using Stacks |Implement Stack using Queues -**Translator**:[walsvid](https://github.com/walsvid) +**Translator:[walsvid](https://github.com/walsvid)** -**Author**:[labuladong](https://github.com/labuladong) +**Author:[labuladong](https://github.com/labuladong)** Queue is a FIFO (first-in-first-out) strategy data structure, while Stack is a FILO (first-in-last-out) data structure. The visual description of these data structures is shown in the figure: diff --git a/data_structure/Implementing the functions of a calculator.md b/data_structure/Implementing_the_functions_of_a_calculator.md similarity index 100% rename from data_structure/Implementing the functions of a calculator.md rename to data_structure/Implementing_the_functions_of_a_calculator.md diff --git a/data_structure/MonotonicStack.md b/data_structure/MonotonicStack.md new file mode 100644 index 0000000000..155dcc7014 --- /dev/null +++ b/data_structure/MonotonicStack.md @@ -0,0 +1,125 @@ +### How to use Monotonic Stack to solve problems[](#如何使用单调栈解题) + +> 原文地址:[https://github.com/labuladong/fucking-algorithm/blob/master/数据结构系列/单调栈.md](https://github.com/labuladong/fucking-algorithm/blob/master/数据结构系列/单调栈.md) + +**Translator: [miaoxiaozui2017](https://github.com/miaoxiaozui2017)** + +**Author: [labuladong](https://github.com/labuladong)** + +`Stack` is a very simple data structure. The logical sequence of first in and last out conforms to the characteristics of some problems, such as function call stack. + +`Monotonic stack` is actually a stack. It just uses some ingenious logic to keep the elements in the stack orderly (monotone increasing or monotone decreasing) after each new element putting into the stack. + +Well,sounds like a heap? No, monotonic stack is not widely used. It only deals with one typical problem, which is called `Next Greater Element`. In this paper, the algorithm template of monotonic queue is used to solve this kind of problem, and the strategy of dealing with "cyclic array" will be discussed. + +First, explain the original problem of `Next Greater Number`: give you an array,and return an array of equal length.The corresponding index stores the next larger element, if there is no larger element, store `-1`. It's not easy to explain clearly in words. Let's take a direct example: + +Give you an array `[2,1,2,4,3]`,and you return an array `[4,2,4,-1,-1]`. + +### Explanation + +The number that is larger than `2` after `the first 2` is `4`.The number that is larger than `1` after `the first 1` is `2`. The number that is larger than `2` after `the second 2` is `4`.There is no number that is larger than `4` after `the fourth`,so fill in `-1`.There is no number that is larger than `3` after `the third`,so fill in `-1`. + +It's a good idea for the violent solution of this problem.It scans the back of each element to find the first larger element. But the time complexity of the violent solution is `O (n^2)`. + +This problem can be thought abstractly: think of the elements in the array as people standing side by side, and the size of the elements as the height of an adult. These people stand in line before you. How to find the `Next Greater Number` of element `"2"`? Very simply, if you can see the element `"2"`, then the first person you can see behind him is the `Next Greater Number` of `"2"`. Because the element smaller than `"2"` is not tall enough and it is blocked by `"2"`,the first one not being blocked is the answer. + + +![ink-image](../pictures/MonotonicStack/1.png) + +This is a very understandable situation,huh? With this abstract scenario in mind, let's look at the code first. + +```cpp +vector nextGreaterElement(vector& nums) { + vector ans(nums.size()); // array to store answer + stack s; + for (int i = nums.size() - 1; i >= 0; i--) { // put it into the stack back to front + while (!s.empty() && s.top() <= nums[i]) { // determine by height + s.pop(); // short one go away while blocked + } + ans[i] = s.empty() ? -1 : s.top(); // the first tall behind this element + s.push(nums[i]); // get into the queue and wait for later height determination + } + return ans; +} +``` + +This is the template for monotonic queue solving problem. The `for` loop scans elements from the back to the front,and while we use the stack structure and enter the stack back to front, we are actually going to exit the stack front to back. The `while` loop is to rule out the elements between the two "tall" elements.Their existence has no meaning, because there is a "taller" element in front of them and they cannot be considered as the `Next Great Number` of the subsequent elements. + +The time complexity of this algorithm is not so intuitive. If you see `for` loop nesting with `while` loop, you may think that the complexity of this algorithm is `O(n^2)`, but in fact the complexity of this algorithm is only `O(n)`. + +To analyze its time complexity, we need to look at it on a whole: There are `n` elements in total, each element is pushed into the stack once, and it will be pop once at most, without any redundant operation. So the total calculation scale is proportional to the element scale `n`, which is the complexity of `O(n)`. + +Now that you have mastered the technique of using monotonic stack, and take a simple transformation to deepen your understanding. + +Give you an array `T = [73, 74, 75, 71, 69, 72, 76, 73]`, which stores the weather temperature in recent days(Is it in teppanyaki? No, it's in Fahrenheit). You return an array to calculate: for each day, how many days do you have to wait for a warmer temperature;and if you can't wait for that day, fill in `0`. + +### Example + +Give you `T = [73, 74, 75, 71, 69, 72, 76, 73]`, and you return `[1, 1, 4, 2, 1, 1, 0, 0]`. +**Explanation** +The first day is 73 degrees Fahrenheit, and the next day is 74 degrees Fahrenheit, which is higher than 73 degrees Fahrenheit.So for the first day, you can wait for a warmer temperature just one day. The same goes for the latter. + +You are already sensitive to the typical problem like `Next Greater Number`.In essence, this problem is also to find the `Next Greater Number`. Instead of just answering what the `Next Greater Number` is, now you need to know the current distance from the `Next Greater Number`. + +For the same type of problem using the same idea, directly call the algorithm template of monotonic stack with a little change.Directly go to the code. + +```cpp +vector dailyTemperatures(vector& T) { + vector ans(T.size()); + stack s; // here for element index,not element + 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); // get index spacing + s.push(i); // add index,not element + } + return ans; +} +``` + +The monotonic stack is explained. Let's start with another important point: how to deal with "circular array". + +It's also `Next Greater Number`. Now suppose the array given to you is a ring and how to deal with it? + +Give you an array `[2,1,2,4,3]`,and you return an array `[4,2,4,-1,4]`. With the ring attribute, the last element `3` goes around and finds the element `4` larger than itself. + +![ink-image](../pictures/MonotonicStack/2.png) + +First of all, the memory of the computer is linear, and there is no real ring array. However, we can simulate the effect of ring array. Generally, we use the `%` operator to calculate the modulus (remainder) to get the ring effect: + +```java +int[] arr = {1,2,3,4,5}; +int n = arr.length, index = 0; +while (true) { + print(arr[index % n]); + index++; +} +``` + +Back to the problem of `Next Greater Number`. After adding the ring attribute, the difficulty lies in that the meaning of `Next` is not only the right side of the current element, but also the left side of the current element (as shown in the above example). + +If we are clear about the problem, it will be half solved. We can think about like this: "Double" the original array,or in another word,to connect another original array at the back. In this way, according to the previous "height comparison" process, each element can not only compare with the elements on its right, but also the elements on its left. + +![ink-image (2)](../pictures/MonotonicStack/3.png) + +How do you achieve it? Of course, you can construct this double length array and apply the algorithm template. However, instead of constructing a new array, we can use the technique of `circular array` to simulate. Just look at the code: + +```cpp +vector nextGreaterElements(vector& nums) { + int n = nums.size(); + vector res(n); // store result + stack s; + // pretend that this array is doubled in length + 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(); + s.push(nums[i % n]); + } + return res; +} +``` + +Till now,you have caught up with the design method and code template for `Monotonic Stack`,learned to solve the problem of `Next Greater Number` ,and can deal with `circular array`. diff --git a/data_structure/Monotonic_queue.md b/data_structure/Monotonic_queue.md new file mode 100644 index 0000000000..e8f18aa6eb --- /dev/null +++ b/data_structure/Monotonic_queue.md @@ -0,0 +1,184 @@ +# special data structure: monotonic queue + +**Author:[labuladong](https://github.com/labuladong)** + +**Translator:[warmingkkk](https://github.com/warmingkkk)** + +The previous article talked about a special data structure "monotonic stack"a type of problem "Next Greater Number" is solved. This article writes a similar data structure "monotonic queue". + +Maybe you haven't heard of the name of this data structure. In fact, it is not difficult. It is a "queue", but it uses a clever method to make the elements in the queue monotonically increase (or decrease). What's the use of this data structure? Can solve a series of problems with sliding windows. + +See a LeetCode title, 239 question,difficulty is hard: + +![](../pictures/monotonic_queue/title.png) + +### 1, build a problem solving framewor + +This problem is not complicated. The difficulty is how to calculate the maximum value in each "window" at O(1) time, so that the entire algorithm is completed in linear time.We discussed similar scenarios before and came to a conclusion: + +In a bunch of numbers,the best value is known,If you add a number to this bunch of numbers,you can quickly calculate the most value by comparing them,but if you reduce one number,you may not get the maximum vaue quickly,but you can have to go through all the numbers and find the maximum value again. + +Back to the scenario of this problem,as each window advances,you need to add a number and decrease one number,so if you want to get a new maximum value in O(1) time,you need a special "monotonic queue" data structure to assist. + +An ordinary queue must have these two operations: + +```java +class Queue { + void push(int n); + // or enqueue, adding element n to the end of the line + void pop(); + // or dequeue, remove the leader element +} +``` + +The operation of a "monotonic queue" is similar: + +```java +class MonotonicQueue { + // add element n to the end of the line + void push(int n); + // returns the maximum value in the current queue + int max(); + // if the head element is n, delete it + void pop(int n); +} +``` +Of course, the implementation methods of these APIs are definitely different from the general Queue, but we leave them alone, and think that the time complexity of these operations is O (1), first answer this "sliding window" problem Frame out: + +```cpp +vector maxSlidingWindow(vector& nums, int k) { + MonotonicQueue window; + vector res; + for (int i = 0; i < nums.size(); i++) { + if (i < k - 1) { // fill the first k-1 of the window first + window.push(nums[i]); + } else { // the window begins to slide forward + window.push(nums[i]); + res.push_back(window.max()); + window.pop(nums[i - k + 1]); + // nums[i - k + 1] is the last element of the window + } + } + return res; +} +``` + +![图示](../pictures/monotonic_queue/1.png) + +The idea is simple, understand? Below we start the highlight, the implementation of monotonic queues. + +### 2, Implementing a monotonic queue data structure + +First we need to know another data structure: deque, which is a double-ended queue. It's simple: + +```java +class deque { + // insert element n at the head of the team + void push_front(int n); + // insert element n at the end of the line + void push_back(int n); + // remove elements at the head of the team + void pop_front(); + // remove element at the end of the line + void pop_back(); + // returns the team head element + int front(); + // returns the tail element + int back(); +} +``` + +Moreover, the complexity of these operations is O (1). This is actually not a rare data structure. If you use a linked list as the underlying structure, it is easy to implement these functions. + +The core idea of "monotonic queue" is similar to "monotonic stack". The push method of the monotonic queue still adds elements to the end of the queue, but deletes the previous elements smaller than the new element: + +```cpp +class MonotonicQueue { +private: + deque data; +public: + void push(int n) { + while (!data.empty() && data.back() < n) + data.pop_back(); + data.push_back(n); + } +}; +``` + +As you can imagine, adding the size of the number represents the weight of the person, squashing the underweight in front, and stopping until it encounters a larger magnitude. + +![](../pictures/monotonic_queue/2.png) + +If every element is added like this, the size of the elements in the monotonic queue will eventually decrease in a monotonic order, so our max () API can be written like this: + +```cpp +int max() { + return data.front(); +} +``` + +The pop () API deletes element n at the head of the queue, which is also very easy to write: + +```cpp +void pop(int n) { + if (!data.empty() && data.front() == n) + data.pop_front(); +} +``` + +The reason to judge `data.front () == n` is because the queue head element n we want to delete may have been" squashed ", so we don't need to delete it at this time: + +![](../pictures/monotonic_queue/3.png) + +At this point, the monotonous queue design is complete, look at the complete problem-solving code: + +```cpp +class MonotonicQueue { +private: + deque data; +public: + void push(int n) { + while (!data.empty() && data.back() < n) + data.pop_back(); + data.push_back(n); + } + + int max() { return data.front(); } + + void pop(int n) { + if (!data.empty() && data.front() == n) + data.pop_front(); + } +}; + +vector maxSlidingWindow(vector& nums, int k) { + MonotonicQueue window; + vector res; + for (int i = 0; i < nums.size(); i++) { + if (i < k - 1) { // fill the first k-1 of the window first + window.push(nums[i]); + } else { // window slide forward + window.push(nums[i]); + res.push_back(window.max()); + window.pop(nums[i - k + 1]); + } + } + return res; +} +``` + +### 3, Algorithm complexity analysis + +Readers may be wondering, while the push operation contains a while loop, the time complexity is not O (1), so the time complexity of this algorithm should not be linear time, right? + +The complexity of the push operation alone is not O (1), but the overall complexity of the algorithm is still O (N) linear time. To think of it this way, each element in nums is pushed_back and pop_back at most once, without any redundant operations, so the overall complexity is still O (N). + +The space complexity is very simple, which is the size of the window O (k). + +### 4, Final conclusion + +Some readers may think that "monotonic queues" and "priority queues" are more similar, but they are actually very different. + +The monotonic queue maintains the monotonicity of the queue by deleting elements when adding elements, which is equivalent to extracting the monotonically increasing (or decreasing) part of a function; while the priority queue (binary heap) is equivalent to automatic sorting, the difference is large went. + +Hurry up and get LeetCode's Question 239 ~ diff --git a/data_structure/README.md b/data_structure/README.md deleted file mode 100644 index 736832579e..0000000000 --- a/data_structure/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# 数据结构系列 - -这一章主要是一些特殊的数据结构设计,比如单调栈解决 Next Greater Number,单调队列解决滑动窗口问题;还有常用数据结构的操作,比如链表、树、二叉堆。 - -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: - -![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git a/data_structure/The_Manipulation_Collection_of_Binary_Search_Tree.md b/data_structure/The_Manipulation_Collection_of_Binary_Search_Tree.md new file mode 100644 index 0000000000..a1b03a56eb --- /dev/null +++ b/data_structure/The_Manipulation_Collection_of_Binary_Search_Tree.md @@ -0,0 +1,249 @@ +# The manipulation collection of binary search tree + +**Translator**: [Fulin Li](https://fulinli.github.io/) + +**Author**:[labuladong](https://github.com/labuladong) + +In the previous article about [framework thinking](../algorithmic_thinking/学习数据结构和算法的高效方法.md), we introduced the traverse framework of the binary tree. There should be a deep impression of this framework left in your mind. In this article, we will put the framework into practice and illustrate how does it flexible resolve all issues about the binary tree. + +The basic idea of binary tree algorithm design: Defining the manipulation in the current node and the last things are thrown to the framework. + +```java +void traverse(TreeNode root) { + // The manipulation required in the root node should be written here. + // Other things will be resolved by the framework. + traverse(root.left); + traverse(root.right); +} +``` + +There are two simple examples to illustrate such an idea, and you can warm up first. + +**1. How to add an integer to every node of binary tree?** + +```java +void plusOne(TreeNode root) { + if (root == null) return; + root.val += 1; + + plusOne(root.left); + plusOne(root.right); +} +``` + +**2. How to determine whether two binary trees are identical?** + +```java +boolean isSameTree(TreeNode root1, TreeNode root2) { + // If they are null, they are identical obviously + if (root1 == null && root2 == null) return true; + // If one of the nodes is void, but the other is not null, they are not identical + if (root1 == null || root2 == null) return false; + // If they are all not void, but their values are not equal, they are not identical + if (root1.val != root2.val) return false; + + // To recursively compare every pair of the node + return isSameTree(root1.left, root2.left) + && isSameTree(root1.right, root2.right); +} +``` + +It is straightforward to understand the two above examples with the help of the traverse framework of the binary tree. If you can understand it, now you can handle all the problems with the binary tree. + +Binary Search Tree (BST), is a common type of binary. The tree additionally satisfies the binary search property, which states that the key in each node must be greater than or equal to any key stored in the left sub-tree, and less than or equal to any key stored in the right sub-tree. + +An example corresponding to the definition is shown as: + +![BST](../pictures/BST/BST_example.png) + +Next, we will realize basic operations with BST, including compliance checking of BST, addition, deletion, and search. The process of deletion and compliance checking may be slightly more complicated. + +**0. Compliance checking of BST** + +This operation sometimes is error-prone. Following the framework mentioned above, the manipulation of every node in the binary tree is to compare the key in the left child with the right child, and it seems that the codes should be written like this: + +```java +boolean isValidBST(TreeNode root) { + if (root == null) return true; + if (root.left != null && root.val <= root.left.val) return false; + if (root.right != null && root.val >= root.right.val) return false; + + return isValidBST(root.left) + && isValidBST(root.right); +} +``` + +But such algorithm is an error. Because the key in each node must be greater than or equal to any key stored in the left sub-tree, and less than or equal to any key stored in the right sub-tree. For example, the following binary tree is not a BST, but our algorithm will make the wrong decision. + +![notBST](../pictures/BST/假BST.png) + +Don't panic though the algorithm is wrong. Our framework is still correct, and we didn't notice some details information. Let's refresh the definition of BST: The manipulations in root node should not only include the comparison between left and right child, but it also require a comparison of the whole left and right sub-tree. What should do? It is beyond the reach of the root node. + +In this situation, we can use an auxiliary function to add parameters in the parameter list, which can carry out more useful information. The correct algorithm is as follows: + +```java +boolean isValidBST(TreeNode root) { + return isValidBST(root, null, null); +} + +boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) { + if (root == null) return true; + if (min != null && root.val <= min.val) return false; + if (max != null && root.val >= max.val) return false; + return isValidBST(root.left, min, root) + && isValidBST(root.right, root, max); +} +``` + +**1. Lookup function in BST** + +According to the framework, we can write the codes like this: + +```java +boolean isInBST(TreeNode root, int target) { + if (root == null) return false; + if (root.val == target) return true; + + return isInBST(root.left, target) + || isInBST(root.right, target); +} +``` + +It is entirely right. If you can write like this, you have remembered the framework. Now you can attempt to take some details into account: How to leverage the property of BST to facilitate us to search efficiently. + +It is effortless! We don't have to search both of nodes recursively. Similar to the binary search, we can exclude the impossible child node by comparing the target value and root value. We can modify the codes slightly: + +```java +boolean isInBST(TreeNode root, int target) { + if (root == null) return false; + if (root.val == target) + return true; + if (root.val < target) + return isInBST(root.right, target); + if (root.val > target) + return isInBST(root.left, target); + // The manipulations in the root node are finished, and the framework is done, great! +``` + +Therefore, we can modify the original framework to abstract a new framework for traversing BST. + +```java +void BST(TreeNode root, int target) { + if (root.val == target) + // When you find the target, your manipulation should be written here + if (root.val < target) + BST(root.right, target); + if (root.val > target) + BST(root.left, target); +} +``` + +**3. Deletion function in BST** + +This problem is slightly complicated. But you can handle it with the help of the framework! Similar to the insert function, we should find it before modification. Let's write it first: + +```java +TreeNode deleteNode(TreeNode root, int key) { + if (root.val == key) { + // When you find it, you can delete it here. + } else if (root.val > key) { + root.left = deleteNode(root.left, key); + } else if (root.val < key) { + root.right = deleteNode(root.right, key); + } + return root; +} +``` + +When you find the target, for example, node A. It isn't effortless for us to delete it. Because we can't destroy the property of BST when we realize the Deletion function. There are three situations, and we will illustrate in the following three pictures: + +Case 1: Node A is just the leaf node, and it's child nodes are all null. In this way, we can delete it directly. + +The picture is excerpted from LeetCode + +![1](../pictures/BST/bst_deletion_case_1.png) + +```java +if (root.left == null && root.right == null) + return null; +``` + +Case 2: The node A has only one child node, then we can change its child node to replace its place. + +The picture is excerpted from LeetCode + +![2](../pictures/BST/bst_deletion_case_2.png) + +```java +// After excluding the Situation 1 +if (root.left == null) return root.right; +if (root.right == null) return root.left; +``` + +Case 3: Node A has two child nodes. To avoid destroying the property of BST, node A must find the maximum node in left sub-tree or the minimum node in the right sub-tree to replace its place. We use the minimum node in the right sub-tree to illustrate it. + +The picture is excerpted from LeetCode + +![2](../pictures/BST/bst_deletion_case_3.png) + +```java +if (root.left != null && root.right != null) { + // Find the minimum node in right sub-tree + TreeNode minNode = getMin(root.right); + // replace root node to minNode + root.val = minNode.val; + // Delete the root node subsequently + root.right = deleteNode(root.right, minNode.val); +} +``` + +The three situations are analyzed, and we can fill them into the framework and simplify the codes: + +```java +TreeNode deleteNode(TreeNode root, int key) { + if (root == null) return null; + if (root.val == key) { + // These two IF function handle the situation 1 and situation 2 + if (root.left == null) return root.right; + if (root.right == null) return root.left; + // Deal with situation 3 + TreeNode minNode = getMin(root.right); + root.val = minNode.val; + root.right = deleteNode(root.right, minNode.val); + } else if (root.val > key) { + root.left = deleteNode(root.left, key); + } else if (root.val < key) { + root.right = deleteNode(root.right, key); + } + return root; +} + +TreeNode getMin(TreeNode node) { + // The left child node is the minimum + while (node.left != null) node = node.left; + return node; +} +``` + +In this way, we can finish the deletion function. Note that such an algorithm is not perfect because we wouldn't exchange the two nodes by 'root.val = minNode.val'. Generally, we will exchange the root and minNode by a series of slightly complicated linked list operations. Because the value of Val may be tremendous in the specific application, it's time-consuming to modify the value of the node. Still, the linked list operations only require to change the pointer and don't modify values. + +**Summary** + +In this article, you can learn the following skills: + +1. The basic idea of designing a binary tree algorithm: Defining the manipulations in the current node and the last things are thrown to the framework. +2. If the manipulations in the current node have influence in its sub-tree, we can add additional parameters to the parameter list by adding auxiliary function. +3. On the foundation of the framework of the binary tree, we abstract the traverse framework of BST: + +```java +void BST(TreeNode root, int target) { + if (root.val == target) + // When you find the target, your manipulation should be written here + if (root.val < target) + BST(root.right, target); + if (root.val > target) + BST(root.left, target); +} +``` + +4. We grasp the basic operations of BST. \ No newline at end of file diff --git a/data_structure/binary_heap_implements_priority_queues.md b/data_structure/binary_heap_implements_priority_queues.md index 640bbbde4d..f74571c113 100644 --- a/data_structure/binary_heap_implements_priority_queues.md +++ b/data_structure/binary_heap_implements_priority_queues.md @@ -211,3 +211,5 @@ Binary heap operation is very simple, mainly floating up and down, to maintain t Priority queues are implemented based on binary heap, with the main operations being insert and delete. Insert is to insert to the end first and then float up to the correct position; Deletion is to reverse the position and then delete, and then sink to the correct position. The core code is only ten lines. Perhaps this is the power of data structure, simple operation can achieve clever functions, really admire the invention of binary heap algorithm people! + +Addition, here is an elegant implementation of Heap with python lambda from [vancanhuit](https://github.com/labuladong/fucking-algorithm/issues/157#issue-576237519). \ No newline at end of file diff --git a/data_structure/design_Twitter.md b/data_structure/design_Twitter.md new file mode 100644 index 0000000000..b35aa1e702 --- /dev/null +++ b/data_structure/design_Twitter.md @@ -0,0 +1,278 @@ +# Design Twitter + +**Translator: [youyun](https://github.com/youyun)** + +**Author: [labuladong](https://github.com/labuladong)** + +[Design Twitter](https://leetcode.com/problems/design-twitter/) is question 355 on LeetCode. This question is both interesting and practical. It combines both algorithms about ordered linked lists and Object Oriented (OO) design principles. We'll be able to link Twitter functions with algorithms when we look at the requirements. + +### 1. The Question and Use Cases + +Twitter is similar to Weibo. We'll focus on the APIs below: + +```java +class Twitter { + + /** user post a tweet */ + public void postTweet(int userId, int tweetId) {} + + /** return the list of IDs of recent tweets, + from the users that the current user follows (including him/herself), + maximum 10 tweets with updated time sorted in descending order */ + public List getNewsFeed(int userId) {} + + /** follower will follow the followee, + create the ID if it doesn't exist */ + public void follow(int followerId, int followeeId) {} + + /** follower unfollows the followee, + do nothing if the ID does not exist */ + public void unfollow(int followerId, int followeeId) {} +} +``` + +Let's look at an user story to understand how to use these APIs: + +```java +Twitter twitter = new Twitter(); + +twitter.postTweet(1, 5); +// user 1 posts a tweet with ID 5 + +twitter.getNewsFeed(1); +// return [5] +// Remarks: because each user follows him/herself + +twitter.follow(1, 2); +// user 1 starts to follow user 2 + +twitter.postTweet(2, 6); +// user 2 posted a tweet with ID 6 + +twitter.getNewsFeed(1); +// return [6, 5] +// Remarks: user 1 follows both user 1 and user 2, +// return the recent tweets from both users, +// with tweet 6 in front of tweet 5 as tweet 6 is more recent + +twitter.unfollow(1, 2); +// user 1 unfollows user 2 + +twitter.getNewsFeed(1); +// return [5] +``` + +This is a common case in our daily life. Take Facebook as an example, when I just added my dream girl as friend on Facebook, I'll see her recent posts in my refreshed feeds, sorted in descending order. The difference is Twitter is uni-directional, while Facebook friends are bi-directional. + +Most of these APIs are easy to implement. The most functionally difficult part could be `getNewsFeed`, as we have to sort by time in descending. However, the list of followees are dynamic, which makes these hard to keep track of. + +__Algorithm helps here__: Imagine we store each user's own tweets in a linked list sorted by timestamp, with each node representing the tweet's ID and timestamp (datetime of creation). If a user follows k followees, we can combine these k ordered linked lists, and apply an algorithm to get the correct `getNewsFeed`. + +Let's put the algorithm aside first and discuss in details later. There is another question: how should we use code to represent users and tweets to apply the algorithm? __This involves OO design__. Let's break into parts and tackle them one step at a time. + +### 2. OO Design + +Based on the analysis just now, we need a `User` class to store information about users, and a `Tweet` class to store information of tweets. The Tweet class will also be nodes in linked lists. Let's put up the frameworks: + +```java +class Twitter { + private static int timestamp = 0; + private static class Tweet {} + private static class User {} + + /* the APIs skeleton */ + public void postTweet(int userId, int tweetId) {} + public List getNewsFeed(int userId) {} + public void follow(int followerId, int followeeId) {} + public void unfollow(int followerId, int followeeId) {} +} +``` + +Because `Tweet` class needs to store timestamp, and `User` class needs to use `Tweet` class to store the tweets posted by a user, we put `Tweet` class and `User` class in `Twitter` class as inner class. For clarity and simplicity, we'll define them one by one. + +**1、Implementation of Tweet Class** + +Based on the previous analysis, it is easy to implement `Tweet` class. Each `Tweet` instance just needs to store its own `tweetId` and posted timestamp `time`. As node in linked list, it also needs to have a point `next` pointing to the next node. + +```java +class Tweet { + private int id; + private int time; + private Tweet next; + + // initialize with tweet ID and post timestamp + public Tweet(int id, int time) { + this.id = id; + this.time = time; + this.next = null; + } +} +``` + +![tweet](../pictures/design_Twitter/tweet.jpg) + +**2、Implementation of User Class** + +Let's think about the real use cases. A user needs to store his/her `userId`, list of followees, and list of posted tweets. The list of followees can use Hash Set to store data, to avoid duplication and search fast. The list of posted tweets should be stored in a linked list to merge with order. Refer to the diagram below: + +![User](../pictures/design_Twitter/user.jpg) + +Besides, based on OO design principles, since the list of followees and the list of tweets are stored in `User`, actions such as "follow", "unfollow", and "post" should be `User`'s actions. Let's define these as `User`'s APIs: + +```java +// static int timestamp = 0 +class User { + private int id; + public Set followed; + // The head of the linked list of posted tweets by the user + public Tweet head; + + public User(int userId) { + followed = new HashSet<>(); + this.id = userId; + this.head = null; + // follow the user him/herself + follow(id); + } + + public void follow(int userId) { + followed.add(userId); + } + + public void unfollow(int userId) { + // a user is not allowed to unfollow him/herself + if (userId != this.id) + followed.remove(userId); + } + + public void post(int tweetId) { + Tweet twt = new Tweet(tweetId, timestamp); + timestamp++; + // insert the new tweet to the head of the linked list + // the closer a tweet is to the head, the larger the value of time + twt.next = head; + head = twt; + } +} +``` + +**3、 Implementation of Several APIs** + +```java +class Twitter { + private static int timestamp = 0; + private static class Tweet {...} + private static class User {...} + + // we need a mapping to associate userId and User + private HashMap userMap = new HashMap<>(); + + /** user posts a tweet */ + public void postTweet(int userId, int tweetId) { + // instantiate an instance if userId does not exist + if (!userMap.containsKey(userId)) + userMap.put(userId, new User(userId)); + User u = userMap.get(userId); + u.post(tweetId); + } + + /** follower follows the followee */ + public void follow(int followerId, int followeeId) { + // instantiate if the follower does not exist + if(!userMap.containsKey(followerId)){ + User u = new User(followerId); + userMap.put(followerId, u); + } + // instantiate if the followee does not exist + if(!userMap.containsKey(followeeId)){ + User u = new User(followeeId); + userMap.put(followeeId, u); + } + userMap.get(followerId).follow(followeeId); + } + + /** follower unfollows the followee, do nothing if follower does not exists */ + public void unfollow(int followerId, int followeeId) { + if (userMap.containsKey(followerId)) { + User flwer = userMap.get(followerId); + flwer.unfollow(followeeId); + } + } + + /** return the list of IDs of recent tweets, + from the users that the current user follows (including him/herself), + maximum 10 tweets with updated time sorted in descending order */ + public List getNewsFeed(int userId) { + // see below as we need to understand the algorithm + } +} +``` + +### 3. Design of The Algorithm + +The algorithm which combines k ordered linked list is implemented using Priority Queue. This data structure is an important application of Binary Heap. All inserted elements are auto sorted. When some random elements are inserted, we can easily take them out in ascending or descending order. + +```python +PriorityQueue pq +# insert with random elements +for i in {2,4,1,9,6}: + pq.add(i) +while pq not empty: + # pop out the first (smallest) element each time + print(pq.pop()) + +# Sorted Output:1,2,4,6,9 +``` + +Based on this cool data structure, we can easily implement the core function. Note that we use Priority Queue to sort `time` in __descending order__, because the larger the value of `time`, the more recent it is, and hence, the close to the head it should be placed: + +```java +public List getNewsFeed(int userId) { + List res = new ArrayList<>(); + if (!userMap.containsKey(userId)) return res; + // IDs of followees + Set users = userMap.get(userId).followed; + // auto sorted by time property in descending order + // the size will be equivalent to users + PriorityQueue pq = + new PriorityQueue<>(users.size(), (a, b)->(b.time - a.time)); + + // first, insert all heads of linked list into the priority queue + for (int id : users) { + Tweet twt = userMap.get(id).head; + if (twt == null) continue; + pq.add(twt); + } + + while (!pq.isEmpty()) { + // return only 10 records + if (res.size() == 10) break; + // pop the tweet with the largest time (the most recent) + Tweet twt = pq.poll(); + res.add(twt.id); + // insert the next tweet, which will be sorted automatically + if (twt.next != null) + pq.add(twt.next); + } + return res; +} +``` + +Here is a GIF I created to describe the process of combining linked lists. Assume there are 3 linked lists of tweets, sorted by `time` property in descending order, we'll combine them in `res` in descending order. Note that the numbers in the nodes are `time` property, not `id`: + +![gif](../pictures/design_Twitter/merge.gif) + +As of now, the design of a simple Twitter timeline function is completed. + + +### 4. Summary + +In this article, we designed a simple timeline function using OO design principles and an algorithm which combines k sorted linked lists. This functionality is widely used in many social applications. + +Firstly, we design the two classes, `User` and `Tweet`. On top of these, we used an algorithm to resolve the most important function. From this example, we can see that algorithms are not used alone in real applications. Algorithms need to be integrated with other knowledge to show their value. + +However, our simple design may not cope with large throughput. In fact, the amount of data in real social applications is tremendous. There are a lot more aspects to take into consideration, including read and write performance to Database, the limit of memory cache, etc. Real applications are big and complicated engineering projects. For instance, the diagram below is a high-level system architecture diagram of a social network such as Twitter: + +![design](../pictures/design_Twitter/design.png) + +The problem we resolved is only a small part of the Timeline Service component. As the number of functions increases, the degree of complexity grows exponentially. Having one algorithm is not enough. It is more important to have a proper high-level design. diff --git a/data_structure/monotonic_stack.md b/data_structure/monotonic_stack.md new file mode 100644 index 0000000000..d4ec158149 --- /dev/null +++ b/data_structure/monotonic_stack.md @@ -0,0 +1,123 @@ +### How to solve problems with a monotonic stack + +**Translator: [nettee](https://github.com/nettee)** + +**Author: [labuladong](https://github.com/labuladong)** + +Stack is a very simple data structure, with a logical order of last-in-first-out (LIFO). Stack conform to the characteristics of some problems, such as function call stacks. + +A monotonic stack is just a stack essentially. However, with some tricks, it keeps the elements in the stack orderly (either increasing or decreasing) whenever new elements are pushed on. + +Sounds a bit like a heap? No, it's not a heap. Monotonic stack has restricted applications. It deals with a typical problem called *Next Greater Element* only. This article is going to solve such problems using the algorithm template that solves monotonic queue problems, and discuss the strategy to deal with "circular arrays". + +First, let's talk about the original problem of Next Greater Element. You are given an array of integers. Find the next greater elements for each number in the array. You should return an array with the same size containing the next greater elements. If there is no greater elements, output -1 for this number. For example: + ++ Input: `[2,1,2,4,3]` ++ Output: `[4,2,4,-1,-1]` ++ Explanation: + + For number 2, the next greater number is 4. + + For number 1, the next greater number is 2. + + For the second 2, the next greater number is 4. + + For number 4, there is no greater numbers, so output -1. + + For number 3, there is no greater numbers after it, so output -1. + +It is easy to come up with a naive solution. For each number in the array, scan the elements after it, and find the first larger element. However, the time complexity of this naive solution is O(n^2). + +You can think of this problem in such an abstract way: imagine the elements of the array as people standing in a line, and the value of the elements as the height of each person. You stand facing this line of people. How to find the Next Greater Number of element "2"? It's easy. If you could see the element "2", then the fist person visible behind him is the Next Greater Number of "2", as the elements less than "2" are not tall enough and are blocked by "2". + +![ink-image](../pictures/monotonic_stack/1.png) + +Easy to understand! With this abstract scenario, let's take a look at the code. + +```cpp +vector nextGreaterElement(vector& nums) { + vector ans(nums.size()); // the result array + stack s; + for (int i = nums.size() - 1; i >= 0; i--) { // push onto the stack backward + while (!s.empty() && s.top() <= nums[i]) { // comparing the height + s.pop(); // fuck off the shorter ones, you are already blocked... + } + ans[i] = s.empty() ? -1 : s.top(); // the next greater taller element + s.push(nums[i]); // join the line and accept the height comparison! + } + return ans; +} +``` + +THIS is the template for monotonic stacks to solve problem. The for loop should scan elements from back to front, because as we are using a stack, pushing onto the stack backwards means popping from the stack forwards. The while loop is to emit the elements between two "tall people", because they have no meaning to exist. With a "taller" element standing in front, they can never become the Next Greater Number of the subsequent coming elements. + +The time complexity of this algorithm is not so intuitive. You may think the algorithm to run in O(n^2), as a for loop is nested in a while loop. However, this algorithm takes only O(n) time actually. + +To analyze its time complexity, we need to consider in a whole. There are n elements in total, and each elements is pushed onto the stack once, and popped at most once, without any redundant operations. Thus, the total computing scale is proportional to the element scale n, which is the complexity of O(n). + +Now, you have mastered the use of monotonic stacks. Let's take a simple variant of this problem to deepen your understanding. + +You are given an array T = [73, 74, 75, 71, 69, 72, 76, 73], the list of daily temperatures. You should return an array that, for each day in the input, how many days you would have to wait until a warmer temperature. If there is no such future day, put 0 instead. + +For example, given T = [73, 74, 75, 71, 69, 72, 76, 73], your output should be [1, 1, 4, 2, 1, 1, 0, 0]. + +Explanation: The temperature is 73 at the first day, and 74 at the second day, which is larger than 73. So for the first day, you only need to wait for one day for a warmer temperature. It is similar for the other days. + +You are already a little sensitive to this kind of Next Greater Number problem. This problem is essentially looking for Next Greater Numbers, but instead of what the Next Greater Number is, you are asked the distance from the Next Greater Number. + +So this is a problem with the same type and the same idea. You can use the template of monotonic stack directly with some slight change in code. Let's show the code. + +```cpp +vector dailyTemperatures(vector& T) { + vector ans(T.size()); + stack s; // store index of elements, instead of element itself + 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); // calculate the distance of indexes + s.push(i); // push the index instead of element + } + return ans; +} +``` + +That's all for the explanation of monotonic stack. Next, we will talk about another important topic: how to deal with "circular arrays". + +Now suppose the same Next Greater Number problem, with the array arranging in a ring. How to deal with it? + +Given an array [2,1,2,4,3], you should output array [4,2,4,-1,4]. On a ring, the last element 3 travels a round and find an element 4 larger than itself. + +![ink-image](../pictures/monotonic_stack/2.png) + +First, the storage in computer is linear, and there are actually no circular array. But we can simulate the effect of circular arrays. Usually we obtain this effect by the modulus (remainder) operation and the % operator: + +```java +int[] arr = {1,2,3,4,5}; +int n = arr.length, index = 0; +while (true) { + print(arr[index % n]); + index++; +} +``` + +Let's back to the Next Greater Number problem. In the case of ring, the difficulty of the problem is that, "next" not only refer to the right side of the current element, but also the left side of the current element, as shown in the example above. + +The problem is half solved by identifying it. We can consider the idea of "doubling" the original array, that is, putting a same array right behind it. In this way, following the previous process of "comparing the height", each element can be compared to not only the elements on its right, but also the elements on its left. + +![ink-image (2)](../pictures/monotonic_stack/3.png) + +How to implement this idea? You can of course construct this double-length array and then apply the algorithm template. However, we can just use the tricks of circular array to simulate the double-length array. Let's look at the code: + +```cpp +vector nextGreaterElements(vector& nums) { + int n = nums.size(); + vector res(n); // the result array + stack s; + // pretend that the length of the array is doubled + 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(); + s.push(nums[i % n]); + } + return res; +} +``` + +Now you have mastered the design and code template of monotonic stacks, the solutions to Next Greater Number, and how to deal with circular arrays. \ No newline at end of file diff --git "a/data_structure/\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/data_structure/\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" deleted file mode 100644 index 2143acd79d..0000000000 --- "a/data_structure/\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" +++ /dev/null @@ -1,277 +0,0 @@ -# 二叉搜索树操作集锦 - -通过之前的文章[框架思维](../算法思维系列/学习数据结构和算法的高效方法.md),二叉树的遍历框架应该已经印到你的脑子里了,这篇文章就来实操一下,看看框架思维是怎么灵活运用,秒杀一切二叉树问题的。 - -二叉树算法的设计的总路线:明确一个节点要做的事情,然后剩下的事抛给框架。 - -```java -void traverse(TreeNode root) { - // root 需要做什么?在这做。 - // 其他的不用 root 操心,抛给框架 - traverse(root.left); - traverse(root.right); -} -``` - -举两个简单的例子体会一下这个思路,热热身。 - -**1. 如何把二叉树所有的节点中的值加一?** - -```java -void plusOne(TreeNode root) { - if (root == null) return; - root.val += 1; - - plusOne(root.left); - plusOne(root.right); -} -``` - -**2. 如何判断两棵二叉树是否完全相同?** - -```java -boolean isSameTree(TreeNode root1, TreeNode root2) { - // 都为空的话,显然相同 - if (root1 == null && root2 == null) return true; - // 一个为空,一个非空,显然不同 - if (root1 == null || root2 == null) return false; - // 两个都非空,但 val 不一样也不行 - if (root1.val != root2.val) return false; - - // root1 和 root2 该比的都比完了 - return isSameTree(root1.left, root2.left) - && isSameTree(root1.right, root2.right); -} -``` - -借助框架,上面这两个例子不难理解吧?如果可以理解,那么所有二叉树算法你都能解决。 - - - -二叉搜索树(Binary Search Tree,简称 BST)是一种很常用的的二叉树。它的定义是:一个二叉树中,任意节点的值要大于等于左子树所有节点的值,且要小于等于右边子树的所有节点的值。 - -如下就是一个符合定义的 BST: - -![BST](../pictures/BST/BST_example.png) - - -下面实现 BST 的基础操作:判断 BST 的合法性、增、删、查。其中“删”和“判断合法性”略微复杂。 - -**零、判断 BST 的合法性** - -这里是有坑的哦,我们按照刚才的思路,每个节点自己要做的事不就是比较自己和左右孩子吗?看起来应该这样写代码: -```java -boolean isValidBST(TreeNode root) { - if (root == null) return true; - if (root.left != null && root.val <= root.left.val) return false; - if (root.right != null && root.val >= root.right.val) return false; - - return isValidBST(root.left) - && isValidBST(root.right); -} -``` - -但是这个算法出现了错误,BST 的每个节点应该要小于右边子树的所有节点,下面这个二叉树显然不是 BST,但是我们的算法会把它判定为 BST。 - -![notBST](../pictures/BST/假BST.png) - -出现错误,不要慌张,框架没有错,一定是某个细节问题没注意到。我们重新看一下 BST 的定义,root 需要做的不只是和左右子节点比较,而是要整个左子树和右子树所有节点比较。怎么办,鞭长莫及啊! - -这种情况,我们可以使用辅助函数,增加函数参数列表,在参数中携带额外信息,请看正确的代码: - -```java -boolean isValidBST(TreeNode root) { - return isValidBST(root, null, null); -} - -boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) { - if (root == null) return true; - if (min != null && root.val <= min.val) return false; - if (max != null && root.val >= max.val) return false; - return isValidBST(root.left, min, root) - && isValidBST(root.right, root, max); -} -``` - - -**一、在 BST 中查找一个数是否存在** - -根据我们的指导思想,可以这样写代码: - -```java -boolean isInBST(TreeNode root, int target) { - if (root == null) return false; - if (root.val == target) return true; - - return isInBST(root.left, target) - || isInBST(root.right, target); -} -``` - -这样写完全正确,充分证明了你的框架性思维已经养成。现在你可以考虑一点细节问题了:如何充分利用信息,把 BST 这个“左小右大”的特性用上? - -很简单,其实不需要递归地搜索两边,类似二分查找思想,根据 target 和 root.val 的大小比较,就能排除一边。我们把上面的思路稍稍改动: - -```java -boolean isInBST(TreeNode root, int target) { - if (root == null) return false; - if (root.val == target) - return true; - if (root.val < target) - return isInBST(root.right, target); - if (root.val > target) - return isInBST(root.left, target); - // root 该做的事做完了,顺带把框架也完成了,妙 -} -``` - -于是,我们对原始框架进行改造,抽象出一套**针对 BST 的遍历框架**: - -```java -void BST(TreeNode root, int target) { - if (root.val == target) - // 找到目标,做点什么 - if (root.val < target) - BST(root.right, target); - if (root.val > target) - BST(root.left, target); -} -``` - - -**二、在 BST 中插入一个数** - -对数据结构的操作无非遍历 + 访问,遍历就是“找”,访问就是“改”。具体到这个问题,插入一个数,就是先找到插入位置,然后进行插入操作。 - -上一个问题,我们总结了 BST 中的遍历框架,就是“找”的问题。直接套框架,加上“改”的操作即可。一旦涉及“改”,函数就要返回 TreeNode 类型,并且对递归调用的返回值进行接收。 - -```java -TreeNode insertIntoBST(TreeNode root, int val) { - // 找到空位置插入新节点 - if (root == null) return new TreeNode(val); - // if (root.val == val) - // BST 中一般不会插入已存在元素 - if (root.val < val) - root.right = insertIntoBST(root.right, val); - if (root.val > val) - root.left = insertIntoBST(root.left, val); - return root; -} -``` - - -**三、在 BST 中删除一个数** - -这个问题稍微复杂,不过你有框架指导,难不住你。跟插入操作类似,先“找”再“改”,先把框架写出来再说: - -```java -TreeNode deleteNode(TreeNode root, int key) { - if (root.val == key) { - // 找到啦,进行删除 - } else if (root.val > key) { - root.left = deleteNode(root.left, key); - } else if (root.val < key) { - root.right = deleteNode(root.right, key); - } - return root; -} -``` - -找到目标节点了,比方说是节点 A,如何删除这个节点,这是难点。因为删除节点的同时不能破坏 BST 的性质。有三种情况,用图片来说明。 - -情况 1:A 恰好是末端节点,两个子节点都为空,那么它可以当场去世了。 - -图片来自 LeetCode -![1](../pictures/BST/bst_deletion_case_1.png) - -```java -if (root.left == null && root.right == null) - return null; -``` - -情况 2:A 只有一个非空子节点,那么它要让这个孩子接替自己的位置。 - -图片来自 LeetCode -![2](../pictures/BST/bst_deletion_case_2.png) - -```java -// 排除了情况 1 之后 -if (root.left == null) return root.right; -if (root.right == null) return root.left; -``` - -情况 3:A 有两个子节点,麻烦了,为了不破坏 BST 的性质,A 必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。我们以第二种方式讲解。 - -图片来自 LeetCode -![2](../pictures/BST/bst_deletion_case_3.png) - -```java -if (root.left != null && root.right != null) { - // 找到右子树的最小节点 - TreeNode minNode = getMin(root.right); - // 把 root 改成 minNode - root.val = minNode.val; - // 转而去删除 minNode - root.right = deleteNode(root.right, minNode.val); -} -``` - -三种情况分析完毕,填入框架,简化一下代码: - -```java -TreeNode deleteNode(TreeNode root, int key) { - if (root == null) return null; - if (root.val == key) { - // 这两个 if 把情况 1 和 2 都正确处理了 - if (root.left == null) return root.right; - if (root.right == null) return root.left; - // 处理情况 3 - TreeNode minNode = getMin(root.right); - root.val = minNode.val; - root.right = deleteNode(root.right, minNode.val); - } else if (root.val > key) { - root.left = deleteNode(root.left, key); - } else if (root.val < key) { - root.right = deleteNode(root.right, key); - } - return root; -} - -TreeNode getMin(TreeNode node) { - // BST 最左边的就是最小的 - while (node.left != null) node = node.left; - return node; -} -``` - -删除操作就完成了。注意一下,这个删除操作并不完美,因为我们一般不会通过 root.val = minNode.val 修改节点内部的值来交换节点,而是通过一系列略微复杂的链表操作交换 root 和 minNode 两个节点。因为具体应用中,val 域可能会很大,修改起来很耗时,而链表操作无非改一改指针,而不会去碰内部数据。 - -但这里忽略这个细节,旨在突出 BST 基本操作的共性,以及借助框架逐层细化问题的思维方式。 - -**四、最后总结** - -通过这篇文章,你学会了如下几个技巧: - -1. 二叉树算法设计的总路线:把当前节点要做的事做好,其他的交给递归框架,不用当前节点操心。 - -2. 如果当前节点会对下面的子节点有整体影响,可以通过辅助函数增长参数列表,借助参数传递信息。 - -3. 在二叉树框架之上,扩展出一套 BST 遍历框架: -```java -void BST(TreeNode root, int target) { - if (root.val == target) - // 找到目标,做点什么 - if (root.val < target) - BST(root.right, target); - if (root.val > target) - BST(root.left, target); -} -``` - -4. 掌握了 BST 的基本操作。 - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/data_structure/\345\215\225\350\260\203\346\240\210.md" "b/data_structure/\345\215\225\350\260\203\346\240\210.md" deleted file mode 100644 index 66f37242e5..0000000000 --- "a/data_structure/\345\215\225\350\260\203\346\240\210.md" +++ /dev/null @@ -1,119 +0,0 @@ -### 如何使用单调栈解题 - -栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。 - -单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。 - -听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一种典型的问题,叫做 Next Greater Element。本文用讲解单调队列的算法模版解决这类问题,并且探讨处理「循环数组」的策略。 - -首先,讲解 Next Greater Number 的原始问题:给你一个数组,返回一个等长的数组,对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。不好用语言解释清楚,直接上一个例子: - -给你一个数组 [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)。 - -这个问题可以这样抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「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) - -这个情景很好理解吧?带着这个抽象的情景,先来看下代码。 - -```cpp -vector nextGreaterElement(vector& nums) { - vector ans(nums.size()); // 存放答案的数组 - stack s; - 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]); // 进队,接受之后的身高判定吧! - } - return ans; -} -``` - -这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个“高个”元素之间的元素排除,因为他们的存在没有意义,前面挡着个“更高”的元素,所以他们不可能被作为后续进来的元素的 Next Great Number 了。 - -这个算法的时间复杂度不是那么直观,如果你看到 for 循环嵌套 while 循环,可能认为这个算法的复杂度也是 O(n^2),但是实际上这个算法的复杂度只有 O(n)。 - -分析它的时间复杂度,要从整体来看:总共有 n 个元素,每个元素都被 push 入栈了一次,而最多会被 pop 一次,没有任何冗余操作。所以总的计算规模是和元素规模 n 成正比的,也就是 O(n) 的复杂度。 - -现在,你已经掌握了单调栈的使用技巧,来一个简单的变形来加深一下理解。 - -给你一个数组 T = [73, 74, 75, 71, 69, 72, 76, 73],这个数组存放的是近几天的天气气温(这气温是铁板烧?不是的,这里用的华氏度)。你返回一个数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0 。 - -举例:给你 T = [73, 74, 75, 71, 69, 72, 76, 73],你返回 [1, 1, 4, 2, 1, 1, 0, 0]。 - -解释:第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温。后面的同理。 - -你已经对 Next Greater Number 类型问题有些敏感了,这个问题本质上也是找 Next Greater Number,只不过现在不是问你 Next Greater Number 是多少,而是问你当前距离 Next Greater Number 的距离而已。 - -相同类型的问题,相同的思路,直接调用单调栈的算法模板,稍作改动就可以啦,直接上代码把。 - -```cpp -vector dailyTemperatures(vector& T) { - vector ans(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); // 加入索引,而不是元素 - } - return ans; -} -``` - - - -单调栈讲解完毕。下面开始另一个重点:如何处理「循环数组」。 - -同样是 Next Greater Number,现在假设给你的数组是个环形的,如何处理? - -给你一个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,4]。拥有了环形属性,最后一个元素 3 绕了一圈后找到了比自己大的元素 4 。 - -![ink-image](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/2.png) - - -首先,计算机的内存都是线性的,没有真正意义上的环形数组,但是我们可以模拟出环形数组的效果,一般是通过 % 运算符求模(余数),获得环形特效: - -```java -int[] arr = {1,2,3,4,5}; -int n = arr.length, index = 0; -while (true) { - print(arr[index % n]); - index++; -} -``` - -回到 Next Greater Number 的问题,增加了环形属性后,问题的难点在于:这个 Next 的意义不仅仅是当前元素的右边了,有可能出现在当前元素的左边(如上例)。 - -明确问题,问题就已经解决了一半了。我们可以考虑这样的思路:将原始数组“翻倍”,就是在后面再接一个原始数组,这样的话,按照之前“比身高”的流程,每个元素不仅可以比较自己右边的元素,而且也可以和左边的元素比较了。 - -![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); // 存放结果 - 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(); - s.push(nums[i % n]); - } - return res; -} -``` - -至此,你已经掌握了单调栈的设计方法及代码模板,学会了解决 Next Greater Number,并能够处理循环数组了。 - -你的在看,是对我的鼓励。关注公众号:labuladong diff --git "a/data_structure/\345\215\225\350\260\203\351\230\237\345\210\227.md" "b/data_structure/\345\215\225\350\260\203\351\230\237\345\210\227.md" deleted file mode 100644 index bb7e85230d..0000000000 --- "a/data_structure/\345\215\225\350\260\203\351\230\237\345\210\227.md" +++ /dev/null @@ -1,185 +0,0 @@ -# 特殊数据结构:单调队列 - -前文讲了一种特殊的数据结构「单调栈」monotonic stack,解决了一类问题「Next Greater Number」,本文写一个类似的数据结构「单调队列」。 - -也许这种数据结构的名字你没听过,其实没啥难的,就是一个「队列」,只是使用了一点巧妙的方法,使得队列中的元素单调递增(或递减)。这个数据结构有什么用?可以解决滑动窗口的一系列问题。 - -看一道 LeetCode 题目,难度 hard: - -![](../pictures/单调队列/title.png) - -### 一、搭建解题框架 - -这道题不复杂,难点在于如何在 O(1) 时间算出每个「窗口」中的最大值,使得整个算法在线性时间完成。在之前我们探讨过类似的场景,得到一个结论: - -在一堆数字中,已知最值,如果给这堆数添加一个数,那么比较一下就可以很快算出最值;但如果减少一个数,就不一定能很快得到最值了,而要遍历所有数重新找最值。 - -回到这道题的场景,每个窗口前进的时候,要添加一个数同时减少一个数,所以想在 O(1) 的时间得出新的最值,就需要「单调队列」这种特殊的数据结构来辅助了。 - -一个普通的队列一定有这两个操作: - -```java -class Queue { - void push(int n); - // 或 enqueue,在队尾加入元素 n - void pop(); - // 或 dequeue,删除队头元素 -} -``` - -一个「单调队列」的操作也差不多: - -```java -class MonotonicQueue { - // 在队尾添加元素 n - void push(int n); - // 返回当前队列中的最大值 - int max(); - // 队头元素如果是 n,删除它 - void pop(int n); -} -``` - -当然,这几个 API 的实现方法肯定跟一般的 Queue 不一样,不过我们暂且不管,而且认为这几个操作的时间复杂度都是 O(1),先把这道「滑动窗口」问题的解答框架搭出来: - -```cpp -vector maxSlidingWindow(vector& nums, int k) { - MonotonicQueue window; - vector res; - for (int i = 0; i < nums.size(); i++) { - if (i < k - 1) { //先把窗口的前 k - 1 填满 - window.push(nums[i]); - } else { // 窗口开始向前滑动 - window.push(nums[i]); - res.push_back(window.max()); - window.pop(nums[i - k + 1]); - // nums[i - k + 1] 就是窗口最后的元素 - } - } - return res; -} -``` - -![图示](../pictures/单调队列/1.png) - -这个思路很简单,能理解吧?下面我们开始重头戏,单调队列的实现。 - -### 二、实现单调队列数据结构 - -首先我们要认识另一种数据结构:deque,即双端队列。很简单: - -```java -class deque { - // 在队头插入元素 n - void push_front(int n); - // 在队尾插入元素 n - void push_back(int n); - // 在队头删除元素 - void pop_front(); - // 在队尾删除元素 - void pop_back(); - // 返回队头元素 - int front(); - // 返回队尾元素 - int back(); -} -``` - -而且,这些操作的复杂度都是 O(1)。这其实不是啥稀奇的数据结构,用链表作为底层结构的话,很容易实现这些功能。 - -「单调队列」的核心思路和「单调栈」类似。单调队列的 push 方法依然在队尾添加元素,但是要把前面比新元素小的元素都删掉: - -```cpp -class MonotonicQueue { -private: - deque data; -public: - void push(int n) { - while (!data.empty() && data.back() < n) - data.pop_back(); - data.push_back(n); - } -}; -``` - -你可以想象,加入数字的大小代表人的体重,把前面体重不足的都压扁了,直到遇到更大的量级才停住。 - -![](../pictures/单调队列/2.png) - -如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个单调递减的顺序,因此我们的 max() API 可以可以这样写: - -```cpp -int max() { - return data.front(); -} -``` - -pop() API 在队头删除元素 n,也很好写: - -```cpp -void pop(int n) { - if (!data.empty() && data.front() == n) - data.pop_front(); -} -``` - -之所以要判断 `data.front() == n`,是因为我们想删除的队头元素 n 可能已经被「压扁」了,这时候就不用删除了: - -![](../pictures/单调队列/3.png) - -至此,单调队列设计完毕,看下完整的解题代码: - -```cpp -class MonotonicQueue { -private: - deque data; -public: - void push(int n) { - while (!data.empty() && data.back() < n) - data.pop_back(); - data.push_back(n); - } - - int max() { return data.front(); } - - void pop(int n) { - if (!data.empty() && data.front() == n) - data.pop_front(); - } -}; - -vector maxSlidingWindow(vector& nums, int k) { - MonotonicQueue window; - vector res; - for (int i = 0; i < nums.size(); i++) { - if (i < k - 1) { //先填满窗口的前 k - 1 - window.push(nums[i]); - } else { // 窗口向前滑动 - window.push(nums[i]); - res.push_back(window.max()); - window.pop(nums[i - k + 1]); - } - } - return res; -} -``` - -**三、算法复杂度分析** - -读者可能疑惑,push 操作中含有 while 循环,时间复杂度不是 O(1) 呀,那么本算法的时间复杂度应该不是线性时间吧? - -单独看 push 操作的复杂度确实不是 O(1),但是算法整体的复杂度依然是 O(N) 线性时间。要这样想,nums 中的每个元素最多被 push_back 和 pop_back 一次,没有任何多余操作,所以整体的复杂度还是 O(N)。 - -空间复杂度就很简单了,就是窗口的大小 O(k)。 - -**四、最后总结** - -有的读者可能觉得「单调队列」和「优先级队列」比较像,实际上差别很大的。 - -单调队列在添加元素的时候靠删除元素保持队列的单调性,相当于抽取出某个函数中单调递增(或递减)的部分;而优先级队列(二叉堆)相当于自动排序,差别大了去了。 - -赶紧去拿下 LeetCode 第 239 道题吧~ - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/data_structure/\350\256\276\350\256\241Twitter.md" "b/data_structure/\350\256\276\350\256\241Twitter.md" deleted file mode 100644 index 9f53d2a4c3..0000000000 --- "a/data_structure/\350\256\276\350\256\241Twitter.md" +++ /dev/null @@ -1,277 +0,0 @@ -# 设计Twitter - -「design Twitter」是 LeetCode 上第 335 道题目,不仅题目本身很有意思,而且把合并多个有序链表的算法和面向对象设计(OO design)结合起来了,很有实际意义,本文就带大家来看看这道题。 - -至于 Twitter 的什么功能跟算法有关系,等我们描述一下题目要求就知道了。 - -### 一、题目及应用场景简介 - -Twitter 和微博功能差不多,我们主要要实现这样几个 API: - -```java -class Twitter { - - /** user 发表一条 tweet 动态 */ - public void postTweet(int userId, int tweetId) {} - - /** 返回该 user 关注的人(包括他自己)最近的动态 id, - 最多 10 条,而且这些动态必须按从新到旧的时间线顺序排列。*/ - public List getNewsFeed(int userId) {} - - /** follower 关注 followee,如果 Id 不存在则新建 */ - public void follow(int followerId, int followeeId) {} - - /** follower 取关 followee,如果 Id 不存在则什么都不做 */ - public void unfollow(int followerId, int followeeId) {} -} -``` - -举个具体的例子,方便大家理解 API 的具体用法: - -```java -Twitter twitter = new Twitter(); - -twitter.postTweet(1, 5); -// 用户 1 发送了一条新推文 5 - -twitter.getNewsFeed(1); -// return [5],因为自己是关注自己的 - -twitter.follow(1, 2); -// 用户 1 关注了用户 2 - -twitter.postTweet(2, 6); -// 用户2发送了一个新推文 (id = 6) - -twitter.getNewsFeed(1); -// return [6, 5] -// 解释:用户 1 关注了自己和用户 2,所以返回他们的最近推文 -// 而且 6 必须在 5 之前,因为 6 是最近发送的 - -twitter.unfollow(1, 2); -// 用户 1 取消关注了用户 2 - -twitter.getNewsFeed(1); -// return [5] -``` - -这个场景在我们的现实生活中非常常见。拿朋友圈举例,比如我刚加到女神的微信,然后我去刷新一下我的朋友圈动态,那么女神的动态就会出现在我的动态列表,而且会和其他动态按时间排好序。只不过 Twitter 是单向关注,微信好友相当于双向关注。除非,被屏蔽... - -这几个 API 中大部分都很好实现,最核心的功能难点应该是 `getNewsFeed`,因为返回的结果必须在时间上有序,但问题是用户的关注是动态变化的,怎么办? - -**这里就涉及到算法了**:如果我们把每个用户各自的推文存储在链表里,每个链表节点存储文章 id 和一个时间戳 time(记录发帖时间以便比较),而且这个链表是按 time 有序的,那么如果某个用户关注了 k 个用户,我们就可以用合并 k 个有序链表的算法合并出有序的推文列表,正确地 `getNewsFeed` 了! - -具体的算法等会讲解。不过,就算我们掌握了算法,应该如何编程表示用户 user 和推文动态 tweet 才能把算法流畅地用出来呢?**这就涉及简单的面向对象设计了**,下面我们来由浅入深,一步一步进行设计。 - -### 二、面向对象设计 - -根据刚才的分析,我们需要一个 User 类,储存 user 信息,还需要一个 Tweet 类,储存推文信息,并且要作为链表的节点。所以我们先搭建一下整体的框架: - -```java -class Twitter { - private static int timestamp = 0; - private static class Tweet {} - private static class User {} - - /* 还有那几个 API 方法 */ - public void postTweet(int userId, int tweetId) {} - public List getNewsFeed(int userId) {} - public void follow(int followerId, int followeeId) {} - public void unfollow(int followerId, int followeeId) {} -} -``` - -之所以要把 Tweet 和 User 类放到 Twitter 类里面,是因为 Tweet 类必须要用到一个全局时间戳 timestamp,而 User 类又需要用到 Tweet 类记录用户发送的推文,所以它们都作为内部类。不过为了清晰和简洁,下文会把每个内部类和 API 方法单独拿出来实现。 - -**1、Tweet 类的实现** - -根据前面的分析,Tweet 类很容易实现:每个 Tweet 实例需要记录自己的 tweetId 和发表时间 time,而且作为链表节点,要有一个指向下一个节点的 next 指针。 - -```java -class Tweet { - private int id; - private int time; - private Tweet next; - - // 需要传入推文内容(id)和发文时间 - public Tweet(int id, int time) { - this.id = id; - this.time = time; - this.next = null; - } -} -``` - -![tweet](../pictures/设计Twitter/tweet.jpg) - -**2、User 类的实现** - -我们根据实际场景想一想,一个用户需要存储的信息有 userId,关注列表,以及该用户发过的推文列表。其中关注列表应该用集合(Hash Set)这种数据结构来存,因为不能重复,而且需要快速查找;推文列表应该由链表这种数据结构储存,以便于进行有序合并的操作。画个图理解一下: - -![User](../pictures/设计Twitter/user.jpg) - -除此之外,根据面向对象的设计原则,「关注」「取关」和「发文」应该是 User 的行为,况且关注列表和推文列表也存储在 User 类中,所以我们也应该给 User 添加 follow,unfollow 和 post 这几个方法: - -```java -// static int timestamp = 0 -class User { - private int id; - public Set followed; - // 用户发表的推文链表头结点 - public Tweet head; - - public User(int userId) { - followed = new HashSet<>(); - this.id = userId; - this.head = null; - // 关注一下自己 - follow(id); - } - - public void follow(int userId) { - followed.add(userId); - } - - public void unfollow(int userId) { - // 不可以取关自己 - if (userId != this.id) - followed.remove(userId); - } - - public void post(int tweetId) { - Tweet twt = new Tweet(tweetId, timestamp); - timestamp++; - // 将新建的推文插入链表头 - // 越靠前的推文 time 值越大 - twt.next = head; - head = twt; - } -} -``` - -**3、几个 API 方法的实现** - -```java -class Twitter { - private static int timestamp = 0; - private static class Tweet {...} - private static class User {...} - - // 我们需要一个映射将 userId 和 User 对象对应起来 - private HashMap userMap = new HashMap<>(); - - /** user 发表一条 tweet 动态 */ - public void postTweet(int userId, int tweetId) { - // 若 userId 不存在,则新建 - if (!userMap.containsKey(userId)) - userMap.put(userId, new User(userId)); - User u = userMap.get(userId); - u.post(tweetId); - } - - /** follower 关注 followee */ - public void follow(int followerId, int followeeId) { - // 若 follower 不存在,则新建 - if(!userMap.containsKey(followerId)){ - User u = new User(followerId); - userMap.put(followerId, u); - } - // 若 followee 不存在,则新建 - if(!userMap.containsKey(followeeId)){ - User u = new User(followeeId); - userMap.put(followeeId, u); - } - userMap.get(followerId).follow(followeeId); - } - - /** follower 取关 followee,如果 Id 不存在则什么都不做 */ - public void unfollow(int followerId, int followeeId) { - if (userMap.containsKey(followerId)) { - User flwer = userMap.get(followerId); - flwer.unfollow(followeeId); - } - } - - /** 返回该 user 关注的人(包括他自己)最近的动态 id, - 最多 10 条,而且这些动态必须按从新到旧的时间线顺序排列。*/ - public List getNewsFeed(int userId) { - // 需要理解算法,见下文 - } -} -``` - -### 三、算法设计 - -实现合并 k 个有序链表的算法需要用到优先级队列(Priority Queue),这种数据结构是「二叉堆」最重要的应用,你可以理解为它可以对插入的元素自动排序。乱序的元素插入其中就被放到了正确的位置,可以按照从小到大(或从大到小)有序地取出元素。 - -```python -PriorityQueue pq -# 乱序插入 -for i in {2,4,1,9,6}: - pq.add(i) -while pq not empty: - # 每次取出第一个(最小)元素 - print(pq.pop()) - -# 输出有序:1,2,4,6,9 -``` - -借助这种牛逼的数据结构支持,我们就很容易实现这个核心功能了。注意我们把优先级队列设为按 time 属性**从大到小降序排列**,因为 time 越大意味着时间越近,应该排在前面: - -```java -public List getNewsFeed(int userId) { - List res = new ArrayList<>(); - if (!userMap.containsKey(userId)) return res; - // 关注列表的用户 Id - Set users = userMap.get(userId).followed; - // 自动通过 time 属性从大到小排序,容量为 users 的大小 - PriorityQueue pq = - new PriorityQueue<>(users.size(), (a, b)->(b.time - a.time)); - - // 先将所有链表头节点插入优先级队列 - for (int id : users) { - Tweet twt = userMap.get(id).head; - if (twt == null) continue; - pq.add(twt); - } - - while (!pq.isEmpty()) { - // 最多返回 10 条就够了 - if (res.size() == 10) break; - // 弹出 time 值最大的(最近发表的) - Tweet twt = pq.poll(); - res.add(twt.id); - // 将下一篇 Tweet 插入进行排序 - if (twt.next != null) - pq.add(twt.next); - } - return res; -} -``` - -这个过程是这样的,下面是我制作的一个 GIF 图描述合并链表的过程。假设有三个 Tweet 链表按 time 属性降序排列,我们把他们降序合并添加到 res 中。注意图中链表节点中的数字是 time 属性,不是 id 属性: - -![gif](../pictures/设计Twitter/merge.gif) - -至此,这道一个极其简化的 Twitter 时间线功能就设计完毕了。 - - -### 四、最后总结 - -本文运用简单的面向对象技巧和合并 k 个有序链表的算法设计了一套简化的时间线功能,这个功能其实广泛地运用在许多社交应用中。 - -我们先合理地设计出 User 和 Tweet 两个类,然后基于这个设计之上运用算法解决了最重要的一个功能。可见实际应用中的算法并不是孤立存在的,需要和其他知识混合运用,才能发挥实际价值。 - -当然,实际应用中的社交 App 数据量是巨大的,考虑到数据库的读写性能,我们的设计可能承受不住流量压力,还是有些太简化了。而且实际的应用都是一个极其庞大的工程,比如下图,是 Twitter 这样的社交网站大致的系统结构: - -![design](../pictures/设计Twitter/design.png) - -我们解决的问题应该只能算 Timeline Service 模块的一小部分,功能越多,系统的复杂性可能是指数级增长的。所以说合理的顶层设计十分重要,其作用是远超某一个算法的。 - -最后,Github 上有一个优秀的开源项目,专门收集了很多大型系统设计的案例和解析,而且有中文版本,上面这个图也出自该项目。对系统设计感兴趣的读者可以点击「阅读原文」查看。 - -PS:本文前两张图片和 GIF 是我第一次尝试用平板的绘图软件制作的,花了很多时间,尤其是 GIF 图,需要一帧一帧制作。如果本文内容对你有帮助,点个赞分个享,鼓励一下我呗! - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/data_structure/\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/data_structure/\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" deleted file mode 100644 index d2500e6051..0000000000 --- "a/data_structure/\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" +++ /dev/null @@ -1,193 +0,0 @@ -# 递归反转链表的一部分 - -反转单链表的迭代实现不是一个困难的事情,但是递归实现就有点难度了,如果再加一点难度,让你仅仅反转单链表中的一部分,你是否能**够递归实现**呢? - -本文就来由浅入深,step by step 地解决这个问题。如果你还不会递归地反转单链表也没关系,**本文会从递归反转整个单链表开始拓展**,只要你明白单链表的结构,相信你能够有所收获。 - -```java -// 单链表节点的结构 -public class ListNode { - int val; - ListNode next; - ListNode(int x) { val = x; } -} -``` - -什么叫反转单链表的一部分呢,就是给你一个索引区间,让你把单链表中这部分元素反转,其他部分不变: - -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/title.png) - -**注意这里的索引是从 1 开始的**。迭代的思路大概是:先用一个 for 循环找到第 `m` 个位置,然后再用一个 for 循环将 `m` 和 `n` 之间的元素反转。但是我们的递归解法不用一个 for 循环,纯递归实现反转。 - -迭代实现思路看起来虽然简单,但是细节问题很多的,反而不容易写对。相反,递归实现就很简洁优美,下面就由浅入深,先从反转整个单链表说起。 - -### 一、递归反转整个链表 - -这个算法可能很多读者都听说过,这里详细介绍一下,先直接看实现代码: - -```java -ListNode reverse(ListNode head) { - if (head.next == null) return head; - ListNode last = reverse(head.next); - head.next.next = head; - head.next = null; - return last; -} -``` - -看起来是不是感觉不知所云,完全不能理解这样为什么能够反转链表?这就对了,这个算法常常拿来显示递归的巧妙和优美,我们下面来详细解释一下这段代码。 - -**对于递归算法,最重要的就是明确递归函数的定义**。具体来说,我们的 `reverse` 函数定义是这样的: - -**输入一个节点 `head`,将「以 `head` 为起点」的链表反转,并返回反转之后的头结点**。 - -明白了函数的定义,在来看这个问题。比如说我们想反转这个链表: - -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/1.jpg) - -那么输入 `reverse(head)` 后,会在这里进行递归: - -```java -ListNode last = reverse(head.next); -``` - -不要跳进递归(你的脑袋能压几个栈呀?),而是要根据刚才的函数定义,来弄清楚这段代码会产生什么结果: - -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/2.jpg) - -这个 `reverse(head.next)` 执行完成后,整个链表就成了这样: - -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/3.jpg) - -并且根据函数定义,`reverse` 函数会返回反转之后的头结点,我们用变量 `last` 接收了。 - -现在再来看下面的代码: - -```java -head.next.next = head; -``` - -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/4.jpg) - -接下来: - -```java -head.next = null; -return last; -``` - -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/5.jpg) - -神不神奇,这样整个链表就反转过来了!递归代码就是这么简洁优雅,不过其中有两个地方需要注意: - -1、递归函数要有 base case,也就是这句: - -```java -if (head.next == null) return head; -``` - -意思是如果链表只有一个节点的时候反转也是它自己,直接返回即可。 - -2、当链表递归反转之后,新的头结点是 `last`,而之前的 `head` 变成了最后一个节点,别忘了链表的末尾要指向 null: - -```java -head.next = null; -``` - -理解了这两点后,我们就可以进一步深入了,接下来的问题其实都是在这个算法上的扩展。 - -### 二、反转链表前 N 个节点 - -这次我们实现一个这样的函数: - -```java -// 将链表的前 n 个节点反转(n <= 链表长度) -ListNode reverseN(ListNode head, int n) -``` - -比如说对于下图链表,执行 `reverseN(head, 3)`: - -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/6.jpg) - -解决思路和反转整个链表差不多,只要稍加修改即可: - -```java -ListNode successor = null; // 后驱节点 - -// 反转以 head 为起点的 n 个节点,返回新的头结点 -ListNode reverseN(ListNode head, int n) { - if (n == 1) { - // 记录第 n + 1 个节点 - successor = head.next; - return head; - } - // 以 head.next 为起点,需要反转前 n - 1 个节点 - ListNode last = reverseN(head.next, n - 1); - - head.next.next = head; - // 让反转之后的 head 节点和后面的节点连起来 - head.next = successor; - return last; -} -``` - -具体的区别: - -1、base case 变为 `n == 1`,反转一个元素,就是它本身,同时**要记录后驱节点**。 - -2、刚才我们直接把 `head.next` 设置为 null,因为整个链表反转后原来的 `head` 变成了整个链表的最后一个节点。但现在 `head` 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 `successor`(第 n + 1 个节点),反转之后将 `head` 连接上。 - -![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/7.jpg) - -OK,如果这个函数你也能看懂,就离实现「反转一部分链表」不远了。 - -### 三、反转链表的一部分 - -现在解决我们最开始提出的问题,给一个索引区间 `[m,n]`(索引从 1 开始),仅仅反转区间中的链表元素。 - -```java -ListNode reverseBetween(ListNode head, int m, int n) -``` - -首先,如果 `m == 1`,就相当于反转链表开头的 `n` 个元素嘛,也就是我们刚才实现的功能: - -```java -ListNode reverseBetween(ListNode head, int m, int n) { - // base case - if (m == 1) { - // 相当于反转前 n 个元素 - return reverseN(head, n); - } - // ... -} -``` - -如果 `m != 1` 怎么办?如果我们把 `head` 的索引视为 1,那么我们是想从第 `m` 个元素开始反转对吧;如果把 `head.next` 的索引视为 1 呢?那么相对于 `head.next`,反转的区间应该是从第 `m - 1` 个元素开始的;那么对于 `head.next.next` 呢…… - -区别于迭代思想,这就是递归思想,所以我们可以完成代码: - -```java -ListNode reverseBetween(ListNode head, int m, int n) { - // base case - if (m == 1) { - return reverseN(head, n); - } - // 前进到反转的起点触发 base case - head.next = reverseBetween(head.next, m - 1, n - 1); - return head; -} -``` - -至此,我们的最终大 BOSS 就被解决了。 - -### 四、最后总结 - -递归的思想相对迭代思想,稍微有点难以理解,处理的技巧是:不要跳进递归,而是利用明确的定义来实现算法逻辑。 - -处理看起来比较困难的问题,可以尝试化整为零,把一些简单的解法进行修改,解决困难的问题。 - -值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。所以递归操作链表可以作为对递归算法的练习或者拿去和小伙伴装逼,但是考虑效率的话还是使用迭代算法更好。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git a/dynamic_programming/EditDistance.md b/dynamic_programming/EditDistance.md new file mode 100644 index 0000000000..b2579e69bb --- /dev/null +++ b/dynamic_programming/EditDistance.md @@ -0,0 +1,271 @@ +# Editing Distance + +**Translator: [Master-cai](https://github.com/Master-cai)** + +**Author: [labuladong](https://github.com/labuladong)** + +few days ago, I saw an interview paper of Tencent. In this paper, most of the algorithm problems are Dynamic programming. The last question is that writing a function to calculate the shortest editing Distance. Today I wrote an article specifically to discuss this problem. + +I personally like this problem because it looks very hard, the solution is Surprisingly simple and beautiful and it`s a rare algorithm which is not very useful.(yech, I recognized that many algorithm problems are not very useful.)Following is the problem: + +![](../pictures/editDistance/title.png) + +Why I say this problem is hard? Obviously, it`s just hard, making people Helpless and frightened. + +And why I say this problem is useful? Because days ago I used the algorithm in my daily life. I had a article in my ‎Wechat Official Account and I wrote some words out of place by mistake. So I decided to modify this part to make the logic suitable. However, the Wechat Official Account article can only be modified 20 words at most, and it only supports addition, deletion and replacement(exactly same as the editing distance problem.) So I used the algorithm to find a best way to solve the problem in just 16 steps. + +Another advanced example is that the edit distance can be used to measure the similarity of two DNA sequences. The DNA sequence is a sequence included of A, G, C and T, which is similar to a string. The less editing distance is, The more similar the two DNA are. Maybe the owner of these DNAs were ancient relatives. + +Let's get to the point, I will explain you how to edit the distance in detail, and I hope you could obtain something fruitful. + + + +### 1. train of thought + +The editing distance is a problem that give us two strings `s1` and `s2` with only three operations and let\`s change `s1` to `s2` in least steps. The first thing to be sure of is that the result of `s1` to `s2` and `s2` to `s1` is the same. So we will use `s1` to `s2` as an example. + +Mentioned in the early paper "The longest common subsequence", **I said that to solve the dynamic programming problem of two strings, We normally use two pointers `i`, `j` to point to the end of the two strings, and then go forward step by step to reduce the size of the problem.** + +Assuming that the two strings are "rad" and "apple", in order to change `s1` to` s2`, the algorithm works like this: + +![](../pictures/editDistance/edit.gif) +![](../pictures/editDistance/1.jpg) + +Remember this gif in order to solve the editing distance problem. The key is how to make + +the right operation which I will discuss later. + +According to the above gif, we can figure out that there are not only three operations, in fact there is the fourth operation which is skip. For example: + +![](../pictures/editDistance/2.jpg) + +As the two strings are same, obviously there should be no operation to minimize the distance. Just move `i`, `j`. + +Another simple situation is when `j` has finished `s2`, if `i` has not finished `s1`, then you can only delete `s1` to make them the same. For example: + + + +![](../pictures/editDistance/3.jpg) + +Similarly, if `i` finished `s1` and `j` has not finished `s2`, you can only insert all the remaining characters of `s2` into `s1` by inserting. As you see, the two cases are the **base case** of the algorithm. + +Let\`s look at how to change your ideas into code. Sit tight, it's time to go. + +### 2. code in detail + +First we sort out our ideas: + +The base case is when `i` finished `s1` or `j` finished `s2`, we can return the remaining length of another string directly. + +For each pair characters, `s1[i]` and `s2[j]`, there are four operations: + +```python +if s1[i] == s2[j]: + skip + i, j move forward +else: + chose: + insert + delete + replace +``` + +With this framework, the problem has been solved. Maybe you will ask, how to chose the "three choices"? It\`s very simple, try it all, and chose the smallest one. we need some recursive skills here.Look at the code: + +```python +def minDistance(s1, s2) -> int: + + def dp(i, j): + # base case + if i == -1: return j + 1 + if j == -1: return i + 1 + + if s1[i] == s2[j]: + return dp(i - 1, j - 1) # skip + else: + return min( + dp(i, j - 1) + 1, # insert + dp(i - 1, j) + 1, # delete + dp(i - 1, j - 1) + 1 # replace + ) + + # i,j initialize to the last index + return dp(len(s1) - 1, len(s2) - 1) +``` + +Let\`s explain this recursive code in detail. There is no need to explain the base case, so I mainly explain the recursive part. + +It is said that recursive code is very interpretable. It does make sense. As long as you understand the definition of a function, you can clearly understand the logic of the algorithm. The function dp(i, j) is defined like this: + +```python +def dp(i, j) -> int +# return the least editing distance s1[0..i] and s2[0..j] +``` + +**Remember this definition**, let\`s look at the code: + +```python +if s1[i] == s2[j]: + return dp(i - 1, j - 1) # skip +# explain: +# already the same, no need any operation +# the least editing distance of s1[0..i] and s2[0..j] equals +# the least distance of s1[0..i-1] 和 s2[0..j-1] +# It means that dp(i, j) equals dp(i-1, j-1) +``` + +if `s1[i]!=s2[j]`, we should recurse the three operations which needs a bit of thing: + +```python +dp(i, j - 1) + 1, # insert +# explain: +# I Directly insert a character same as s2[j] at s1[i] +# then s2[j] are matched,move forward j,and continue compareed with i +# Don`t forget to add one to the operation number +``` + +![](../pictures/editDistance/insert.gif) + +```python +dp(i - 1, j) + 1, # delete +# explain: +# I directly delete s[i] +# move forward i,continue to compared with j +# add one to the operation number +``` + +![](../pictures/editDistance/delete.gif) + +```python +dp(i - 1, j - 1) + 1 # replace +# explain: +# I directly replace s1[i] with s2[j], then they are matched +# move forward i,j and continue to compare +# add one to operation number +``` + +![](../pictures/editDistance/replace.gif) + +Now, you should fully understand this short and clever code. Another small problem is that this is a violent solution. There are many overlapping subproblems, which should to be optimized by dynamic programming techniques. + +**How can we see the overlapping subproblems at a glance?** As mentioned in the previous article "Regular Expressions for Dynamic Programming", we need to abstract the recursive framework of the algorithm in this article: + +```python +def dp(i, j): + dp(i - 1, j - 1) #1 + dp(i, j - 1) #2 + dp(i - 1, j) #3 +``` + +For the subproblem `dp(i-1, j-1)`, how can we get it from the original question `dp(i, j)`? Once we found a repetitive path, it means that there is a huge number of repetitive paths, which is the overlapping subproblem. For example: `dp(i, j)-> #1` and `dp(i, j)->#2->#3`. + +### 3. Optimized by Dynamic programming + +For the overlapping subproblems, we introduced in the previous article "Detailed Explanation of Dynamic Programming" in detailed. The optimization is nothing more than a memo or a DP table. + +The memo is easy to append, just modified the original code slightly. + +```python +def minDistance(s1, s2) -> int: + + memo = dict() # memo + def dp(i, j): + if (i, j) in memo: + return memo[(i, j)] + ... + + if s1[i] == s2[j]: + memo[(i, j)] = ... + else: + memo[(i, j)] = ... + return memo[(i, j)] + + return dp(len(s1) - 1, len(s2) - 1) +``` + +**We mainly explain the DP table solution.** + +First, we declare the meaning of the dp array. The dp array is a two-dimensional array, which looks like this: + +![](../pictures/editDistance/dp.jpg) + +With the foundation of the previous recursive solution, it\`s easy to understand. `dp [..][0]` and `dp [0][..]` correspond to the base case. The meaning of `dp [i][j]` is similar to the previous dp function: + +```python +def dp(i, j) -> int +# return the least editing distance of s1[0..i] and s2[0..j] + +dp[i-1][j-1] +# storage the least editing distance of s1[0..i] and s2[0..j] +``` + +The base case of the dp function is that `i, j` is equal to -1. However the array index is at least 0, the dp array is offset by one position. + +Since the dp array has the same meaning as the recursive dp function, you can directly apply the previous ideas to write code. **The only difference is that the DP table is solved from the bottom to up, and the recursive solution is solved from the top to down**: + +```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; + // from the bottom to up + 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 + ); + // storage the least editing distance of s1 and s2 + return dp[m][n]; +} + +int min(int a, int b, int c) { + return Math.min(a, Math.min(b, c)); +} +``` + +### 4. Extension + +Generally speaking, when dealing with the dynamic programming of two strings, we just follow the ideas of this article, making the DP table. Why? Because it\`s easy to find out the relationship of the state transitions, such as the DP table of the edit distance: + +![](../pictures/editDistance/4.jpg) + +There is another detail: since every `dp[i][j]` is only related to the three status, the space complexity can be reduced to $O(min(M, N))$ (M, N is the length of the two strings). It\`s not very difficult but the code is harder to read. You can try to optimize it by yourself. + +Maybe you will also ask, **As we only found the minimum editing distance, how can we know the every step?** In the example of modifying the article you mentioned earlier, only a editing distance is definitely not enough. You must know how to modify it. + +Actually, it\`s very simple, just slightly modified the code and add additional information to the dp array: + +```java +// int[][] dp; +Node[][] dp; + +class Node { + int val; + int choice; + // 0 skip + // 1 insert + // 2 delete + // 3 replace +} +``` + +The `val` attribute is the value of the previous dp array, and the` choice` attribute represents the operation. When making the best choice, record the operation and then infer the specific operation from the result. + +Our final result is `dp [m] [n]`, where `val` holds the minimum edit distance, and` choice` holds the last operation, such as the insert operation, then you can move one space to the left: + +![](../pictures/editDistance/5.jpg) + +Repeat this process, you can return to the starting point `dp [0] [0]` step by step to form a path. Editing according to the operations on this path is the best solution. + +![](../pictures/editDistance/6.jpg) + +The above is the entire content of the edit distance algorithm. diff --git a/dynamic_programming/FourKeysKeyboard.md b/dynamic_programming/FourKeysKeyboard.md new file mode 100644 index 0000000000..9690c58a02 --- /dev/null +++ b/dynamic_programming/FourKeysKeyboard.md @@ -0,0 +1,184 @@ +# 4 Keys Keyboard + +**Translator: [upbin](https://github.com/upbin)** + +**Author: [labuladong](https://github.com/labuladong)** + +The problem of *4 keys keyboard* is very interesting and can broaden one's horizon. This problem can make you obviously feel that different definitions of dp arrays need completely different logic to think about, and this logic can produce completely different solutions. + +We can't wait to solve this problem: + +![](../pictures/4keyboard/title.png) + +After reading the question, think about how to get the maximum number of characters 'A' after typing `N` times on the keyboard? We are more familiar with trying questions using enumeration. Whenever we want to press the keyboard (and can press it), there are `4 buttons` for us to choose from, we can enumerate every possible operation It is obvious that this is a dynamic programming problem. + +### Discuss the first method + +This kind of problem-solving idea is easy to understand, but the efficiency is not high. We follow the routine directly: **for dynamic programming problems, we must first understand which are [ states ] and which are [ choices ].** + +Specific to this problem, what **choices** are obvious for each keystroke: four types are the **4** keys mentioned in the title, which are `A`, `Ctrl-A`, `Ctrl-C`, and `Ctrl-V`. + +Next, let's think about what are the **states** of this problem? **In other words, what information do we need to know to break down the original problem into smaller subproblems?** + +Now you think about it, Is it correct for me to define the status of this problem as follows? + +- The first state is the remaining number of times the key can be pressed, we use `n` to represent it. +- The second state is the number of characters 'A' on the current screen, we use `a_num`. +- The third state is the number of characters 'A' still in the clipboard, represented by `copy`. + +By defining **state** in this way, we can know the *base case*: when the number of remaining `n` is `0`, `a_num` is the answer we want. + +Combining the four **choices** just mentioned, we can express these kinds of choices through state transitions: + +```python +dp(n - 1, a_num + 1, copy) # [ A ] +# comment: Press the 'A' key to add a character to the screen. +# Subtract 1 at the same time the number of times you are allowed to press the keyboard. + +dp(n - 1, a_num + copy, copy) # [Ctrl-V] +# comment: Press C-V to paste, the characters in the clipboard are added to the screen. +# Subtract 1 at the same time the number of times you are allowed to press the keyboard. + +dp(n - 2, a_num, a_num) # [Ctrl-A] & [Ctrl-C] +# comment: Ctrl + A and Ctrl + C can obviously be used together. +# The number of 'A' in the clipboard becomes the number of 'A' on the screen. +# Subtract 2 at the same time the number of times you are allowed to press the keyboard. +``` + +By describing this, we can see that the scale of the problem `n` is constantly decreasing, and finally we can reach the *base case* of `n == 0`. So this idea is correct: (Do you think so?) + +```python +def maxA(N: int) -> int: + + # It can be verified that for the initial (n, a_num, copy) state, + # there can be at most dp (n, a_num, copy) 'A' on the screen. + def dp(n, a_num, copy): + # base case + if n <= 0: return a_num; + # Let ’s try all three options and choose the largest one. + return max( + dp(n - 1, a_num + 1, copy), # [ A ] + dp(n - 1, a_num + copy, copy), # [Ctrl-V] + dp(n - 2, a_num, a_num) # [Ctrl-A] & [Ctrl-C] + ) + + # You can press the key n times, then there is no 'A' in the screen + # and the clipboard. + return dp(N, 0, 0) +``` + +This solution should be well understood because it is semantically explicit. + +Below we continue to follow the routine and use memorized search to eliminate those overlapping sub-problems: + +```python +def maxA(N: int) -> int: + # memorandum + memo = dict() + def dp(n, a_num, copy): + if n <= 0: return a_num; + # Avoid overlapping subproblems being recalculated + if (n, a_num, copy) in memo: + return memo[(n, a_num, copy)] + + memo[(n, a_num, copy)] = max( + # These options are still the same + ) + return memo[(n, a_num, copy)] + + return dp(N, 0, 0) +``` + +After we optimized our code in this way, although the sub-problem was repeatedly solved, the number of searches was still very large (*if we submit to LeetCode it will definitely time out*). + +Now let's try to analyze the time complexity of the algorithm just now. The challenge is that this analysis is not easy. No matter what it is, now we write this *dp function* as a *dp array*: + +```c++ +dp[n][a_num][copy] +// The total number of states (spatial complexity) of this problem +// is the volume of this three-dimensional array. +``` + +We know that the maximum value of the variable `n` is `N`, but it is difficult to calculate the maximum number of `a_num` and `copy`. The lowest complexity is *O(N^3)​*. So the algorithm just now is not good, the complexity is too high, and it can no longer be optimized. + +The more embarrassing thing is that this also shows that I used to define **state** as it is not very good. Let's change the idea of defining this dp problem. + +### Exploration of the second approach + +Next, our thinking is a little more complicated, but it is very efficient. + +Continue to follow our routine, **choice** has been defined before, or the `4`. But this time we only need to define **a state**, which is the remaining number of available keyboard presses `n`. + +This algorithm is based on the following fact. There must be only two cases of the key sequence corresponding to the optimal answer: + +- Either keep pressing A: `A`, ` A`, ... , `A` (more when `N` is smaller). +- Either this is the form: `A`, `A`, ..., `Ctrl-A`, `Ctrl-C`, `Ctrl-V`, `Ctrl-V`, ..., `Ctrl-V` (mostly when `N` is larger). *(Here you can find some mathematical rules, you can study if you are interested)* + +Because when the number of characters to be printed is relatively small (`N` is small), "`Ctrl-A`, `Ctrl-C`, `Ctrl-V`" consumes a relatively high number of operations, so we might as well keep pressing `A`. When `N` is relatively large, the gain of `Ctrl-V` in the later period is definitely relatively large. In this case, the entire operation sequence is roughly like this: at the beginning, press several 'A's, then `Ctrl-A`, `Ctrl-C`, and finally several `Ctrl-V`, and then continue `Ctrl-A -> Ctrl-C -> Ctrl-V` Such a loop operation. + +In other words, the last keystroke was either `A` or `Ctrl-V`. As long as we are clear on this, we can design the algorithm through these **two situations**: + +```java +int[] dp = new int[N + 1]; +// Definition: dp[i] indicates the maximum number of 'A' that can be displayed after the // first operation. +for (int i = 0; i <= N; i++) + dp[i] = max( + // Press [ A ] this time, +         // This time press [Ctrl-V]. + ) +``` + +Think about it. For the case of [pressing the `A` key], it is actually a new 'A' printed on the screen of **state i-1**, so it is easy to get the result: + +```java +// If we press the [ A ] key, it's just one more 'A' than the last time. +dp[i] = dp[i - 1] + 1; +``` + +However, if we want to press `Ctrl-V`, we also need to consider where we did `Ctrl-A` and `Ctrl-C`. + +Earlier we said that the optimal sequence of operations must be `Ctrl-A`, `Ctrl-C` followed by several `Ctrl-V`, so we use a variable `j` as the starting point for these `Ctrl-V` operations. Then the two operations before `j` should be `Ctrl-A` and `Ctrl-C`: + +```java +public int maxA(int N) { + int[] dp = new int[N + 1]; + dp[0] = 0; + for (int i = 1; i <= N; i++) { + // press [ A ] + dp[i] = dp[i - 1] + 1; + for (int j = 2; j < i; j++) { + // [Ctrl-A] & [Ctrl-C] -> dp[j-2], Paste i-j times + // There are { dp[j-2] * (i-j+1) }number of 'A' on the screen + dp[i] = Math.max(dp[i], dp[j - 2] * (i - j + 1)); + } + } + // What is the maximum number of 'A' after N keystrokes? + return dp[N]; +} +``` + +The `j` variable `minus 2` is used to save the number of operations available to `Ctrl-A`, `Ctrl-C`. See the description picture to understand: + +![](../pictures/4keyboard/1.jpg) + +We have just completed this algorithm. The time complexity of the algorithm is *O(N^2)​* and the space complexity is ​*O(N)*​, so this solution seems to be very efficient. + +### Review our algorithmic ideas + +Dynamic programming is difficult to find the state transition. The different definitions we set will produce different state transition logic. Although we can all get the correct results in the end, the efficiency of the program may have amazing differences. + +Let's review the method we tried for the first time. Although the overlapping sub-problem has been eliminated, the efficiency of the program is still low, but where is the low? Let's abstract the recursive framework to find out: + +```python +def dp(n, a_num, copy): + dp(n - 1, a_num + 1, copy), # [ A ] + dp(n - 1, a_num + copy, copy), # [Ctrl-V] + dp(n - 2, a_num, a_num) # [Ctrl-A] & [Ctrl-C] +``` + +Let's analyze the logic of this exhaustive scheme. Obviously, it is possible to have such a sequence of operations `Ctrl-A`, `Ctrl+C`, `Ctrl-A`, `Ctrl-C`, ... , or `Ctrl-V`, `Ctrl-V`, ... . However, the result of the operation sequence produced by this method is not optimal, even if we have not figured out a way to circumvent these situations, thereby adding a lot of calculations of unnecessary sub-problems. + +After we review the second solution, we only need to think a little bit before we can think that the operation sequence of the optimal answer should be this form: `A`, `A`, ..., `Ctrl-A`, `Ctrl-C`, `Ctrl-V`, `Ctrl-V`, ..., `Ctrl-V`. + +Based on the findings we found, we redefined state and re-searched for state transition, which logically reduced the number of invalid sub-problems, and ultimately optimized the program's operating efficiency. + diff --git a/dynamic_programming/GameProblemsInDynamicProgramming.md b/dynamic_programming/GameProblemsInDynamicProgramming.md new file mode 100644 index 0000000000..5dc7e3dcff --- /dev/null +++ b/dynamic_programming/GameProblemsInDynamicProgramming.md @@ -0,0 +1,193 @@ +# Game Problems In Dynamic Programming + +**Translator: [wadegrc](https://github.com/wadegrc)** + +**Author: [labuladong](https://github.com/labuladong)** + +In the last article,we discussed a fun「stone game 」in [several puzzles](../高频面试系列/一行代码解决的智力题.md),By the constraints +of the problem, the game is first to win.But intelligence questions are always intelligence questions,Real algorithmic problems are +not solved by cutting corners. So this paper is going to talk about the stone game and assuming that both of these guys are smart enough, who's going to win in the end how do you solve this problem with dynamic programming. + +Game problems follow a similar pattern,The core idea is to use tuples to store the game results of two people on the basis of two-dimensional dp array.Once you're mastered this technique,if someone asks you a similar question again,you can take it in stride. + +We changed the stone game to be more general: + +There is a pile of stones in front of you and your friends,it's represented by an array of piles,and piles[i] is how many stones are there in the ith heap.You take turns with the stones,one pile at a time,but you can only take the left or the right piles.After all the stones have been taken away, the last one who has more stones wins. + +The heap number of stones can be any positive integer,and the total number of stones can be any positive integer,That would break the situation in which one must win first.Let's say I have three piles of rocks: `piles = [1, 100, 3]`,Whether it's a 1or a 3,the 100 that's going to make the difference is going to be taken away by the back hand,and the back hand is going to win. + +**Assuming they are both smart**,design an algorithm that returns the difference between the final score of the first hand and the last hand,As in the example above,the first hand gets 4 points,the second hand gets 100 points, and you should return -96. + + +With this design,this problem is a Hard dynamic programming problem.**The difficuty with gaming is that two people have to take turns choosing,and they're both smart.How do we program?** + +It's the approach that's been emphasized many times,The first step is to define the array,and then,like the stock buying and selling +series,once you find the「status」and the「selection」,and then it's easy. + +### 1.Define the meaning of the dp array: + +Defining what a dp array means is very tachnical,The dp array of the same problem can be defined in several ways.Different definitions +lead to different state transition equations,But as long as there's no logic problem,you end up with the same answer.I recommend that you don't get caught up in what looks like a great short technique,and that you end up with something that's a little bit more stable, something that's the most interpretable, and something that's the easiest to generalize,This paper gives a general design framework of game problem. + +Before we introduce what a dp array means,let's take a look at what it ultimately looks like: + +![1](../pictures/GameProblems/1.png) + +As explained below,tupels are considered to be a calss containing first and second atrributes,And to save space,these two atrributes are abbreviated to fir and sec.As shown in the figure above,we think `dp[1][3].fir = 10`,`dp[0][1].sec = 3`. + +Start by answering a few questions that readers might ask: + +This is a two-dimensional dp table that stores tuples.How do you represent that?Half of this array is useless,How do you optimize it?Very simple, do not care,first to think out the way to solve the problem again. + +**Here's an explanation of what a dp array means:** + +```python +dp[i][j].fir represents the highest score the first hand can get for this section of the pile piles[i...j] +dp[i][j].sec represents the highest score the back hand can get for this section of the pile piles[i...j] + +Just to give you an example,Assuming piles = [3, 9, 1, 2],The inedx starts at 0 +dp[0][1].fir = 9 means:Facing the pile of stones [3, 9],The first player eventually gets 9 points. +dp[1][3].sec = 2 means:Facing the pile of stones [9, 1, 2],The second player eventually gets 2 points. +``` + +The answer we want is the difference between the final score of the first hand and the final score of the second hand,By thisdefinition, that is $dp[0][n-1].fir - dp[0][n-1].sec$ That is,facing the whole piles,the difference between the best score of the first hand and the best score of the second hand. + +### 2.state transition equation: + +It's easy to write the transition equation,The first step is to find all the states and the choices you can make for each state,and then pick the best. + +From the previous definition of the dp array,**there are obviously three states:the starting index i,the ending index j,and the person whose turn it is.** + +```python +dp[i][j][fir or sec] +range: +0 <= i < piles.length +i <= j < piles.length +``` + +For each state of the problem,**there are two choices you can make :Choose the pile to the left,or the pile to the right**.We can do all the states like this : + +```python +n = piles.length +for 0 <= i < n: + for j <= i < n: + for who in {fir, sec}: + dp[i][j][who] = max(left, right) + +``` + +The pseudocode above is a rough framework for dynamic programming,and there is a similar pseudocode in the stock series problem.The difficulty of this problem is that two people choose alternately,that is to say,the choice of the first hand has effect on the second hand,how can we express this? + +According to our definition of dp array,it is easy to solve this difficulty and **write down the state transition equation**: + +```python +dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec) +dp[i][j].fir = max( Select the rock pile on the far left , Select the rock pile on the far right ) +# explanation:I,as a first hand,faced piles[i...j],I had two choices: +# If I choose the pile of rocks on the far left,and I will face piles[i+1...j] +# But when it came to the other side,I became the back hand. +# If I choose the pile of rocks on the far right,and I will face piles[i...j-1] +# But when it came to the other side,I became the back hand. + +if the first hand select the left: + dp[i][j].sec = dp[i+1][j].fir +if the first hand select the right: + dp[i][j].sec = dp[i][j-1].fir +# explanation:I,as a back hand ,have to wait for the first hand to choose,There are two condition: +# If the first hand choose the pile of rocks on the far left,I will face piles[i+1...j] +# then it's my turn, and i become the first hand. +# If the first hand choose the pile of rocks on the far right,I will face piles[i...j-1] +# then it's my turn, and i become the first hand. +``` + +According to the definition of the dp array, we can also find the **base case**,which is the simplest case: + +```python +dp[i][j].fir = piles[i] +dp[i][j].sec = 0 +range: 0 <= i == j < n +# explanation:i==j which means just a bunch of rocks piles[i] in the front of us +# So obviously the first hand can get piles[i], +# there are no stones int the back,so his score is 0 +``` + +![2](../pictures/GameProblems/2.png) + +One thing to note here is that we found that the base case is tilted in the table,and we need dp[i+1][j] and dp[i][j-1] to compute dp[i][j]: + +![3](../pictures/GameProblems/3.png) + +So the algorithm can not simply traverse the dp array row by row,but **traverse the array diagonally**. + +![4](../pictures/GameProblems/4.png) + +To be honest,traversing a two-dimensional array diagonally is easier said than done. + + +### 3.code implementation + +How do you implement this fir and sec tuple?You can either use python,with its own tuple type,or use the c++pair container,or use a three-dimensional array,the last dimension being the tuple,or we can write a pair class ourselves. + +```java +class Pair { + int fir, sec; + Pair(int fir, int sec) { + this.fir = fir; + this.sec = sec; + } +} +``` + +Then we can directly translate our state transition equation into code,and we can pay attention to technique of traversing through array diagonally: + +```java +/* Returns the difference between the last first hand and last hand */ +int stoneGame(int[] piles) { + int n = piles.length; + //Initializes the dp array + Pair[][] dp = new Pair[n][n]; + for (int i = 0; i < n; i++) + for (int j = i; j < n; j++) + dp[i][j] = new Pair(0, 0); + // base case + for (int i = 0; i < n; i++) { + dp[i][i].fir = piles[i]; + dp[i][i].sec = 0; + } + // traverse the array diagonally + for (int l = 2; l <= n; l++) { + for (int i = 0; i <= n - l; i++) { + int j = l + i - 1; + // The first hand select the left- or right-most pile. + int left = piles[i] + dp[i+1][j].sec; + int right = piles[j] + dp[i][j-1].sec; + // Refer to the state transition equation. + if (left > right) { + dp[i][j].fir = left; + dp[i][j].sec = dp[i+1][j].fir; + } else { + dp[i][j].fir = right; + dp[i][j].sec = dp[i][j-1].fir; + } + } + } + Pair res = dp[0][n-1]; + return res.fir - res.sec; +} +``` + +Dynamic programming ,the most important is to understand the state transition equation,based on the previous detailed explanation,the reader should be able to clearly understand the meaning of this large piece of code. + +And notice that the calculation of 'dp[i][j]' only depends on the left and the bottom elements,so there must be room for optimization, for one-dimensional dp,But one-dimensional dp is a little bit more complicated,it's less interpretable,so you don't have to waste time trying to understand it. + +### 4.summary: + +This paper presents a dynamic programming method to solve the game problem. The premise of game problems is usually between two smart people. The common way to describe such games is a one-dimensional array of dp, in which tuples represent the optimal decision of two people. + +The reason for this design is that when the first hand makes a choice, it becomes the second hand, and when the second hand makes a choice, it becomes the first hand. This role reversal allows us to reuse the previous results, typical dynamic programming flags. + + +Those of you who have read this should understand how algorithms solve game problems. Learning algorithms, must pay attention to the template framework of the algorithm,rather than some seemingly awesome ideas, do not bend to write an optimal solution.Don't be afraid to use more space,don't try optimization too early, and don't be afraid of multidimensional arrays.A dp array is a way to store information and avoid double counting. + + +I hope this article has been helpful. diff --git a/dynamic_programming/IntervalScheduling.md b/dynamic_programming/IntervalScheduling.md new file mode 100644 index 0000000000..6725002578 --- /dev/null +++ b/dynamic_programming/IntervalScheduling.md @@ -0,0 +1,191 @@ +# Interval Problem (I): Interval Scheduling + +**Translator: [GYHHAHA](https://github.com/GYHHAHA)** + +**Author: [labuladong](https://github.com/labuladong)** + +What is the greedy algorithm? It can be regarded as a special case of dynamic programming (DP). Compared with DP, using greedy algorithm need to meet more conditions, such as the greedy choosing property, but show more efficiency. + +For example, supposed that a certain algorithm using enumeration method needs exponential time, if the overlapping subproblems can be solved by DP, then polynomial time is available. Furthermore, if it meets the greedy choosing property, the time complexity can be reduced to a linear level. + +So what is the greedy choosing property? Easily speaking, if the final global optimum can be satisfied by several local-optimal steps, then we call the algorithm have the greedy choosing property. And also we should remind that it's a special property, only a part of problem have characteristic like this. + +For example, if you can choose ten banknotes from 100 given banknotes, how to get banknotes with the highest values in total? Clearly, choosing the banknote with the highest value in the rest each time would bring about the global optimum. + +### First Part: Problem Restatement + +This article will solve a classical greedy algorithm problem: Interval Scheduling. Given a series of closed intervals `[start, end]` , you should design an algorithm to compute the number of maximum subsets without any overlapping. + +```java +int intervalSchedule(int[][] intvs) {} +``` + +For example,`intvs = [[1,3], [2,4], [3,6]]`, the interval set have 2 subsets without any overlapping at most, `[[1,3], [3,6]]` , so your algorithm should return 2 as the result. Note that intervals with the same border doesn't meet the condition. + +This problem is widely used in our daily life. For example, you get several activities today, each activity can be represented by its starting time and its ending time with interval`[start, end]` . Clearly you can't attend 2 activities at the same time, so this problem can be change into a question about how to find the maximum subsets without any time overlapping. + +### Second Part: Greedy Algorithm + +For this problem, there are some potential thought, but none of them could lead to the correct answer. + +① Choosing the interval with the earliest starting time. + +There maybe exists some intervals appear very early, but they can also be with long duration, which make us missing up some short intervals. + +② Choosing the interval with the shortest duration. + +③ Choosing the interval with the shortest duration. + +It's easy to raise counterexample to these solution. + +The correct thought can be very easy, which can be devided into three parts: + +① Choosing a interval 'x', which has the earliest ending time among all the current intervals, from the interval set 'intvs'. + +② Delete all invertals intersecting with 'x'. + +③ Repecting ① and ②, until intvs gets empty. These 'x' selected before are the subsets meeting the conditions. + +Now when we change this thought into algorithm, it's more convenient to implement ① and ② with a ascending sorting by `end` for each interval. + +【Explanations for the chinese in the picture】 + +【索引:index】【按end排序,sorting by end】【选择区间x:choosing the interval x】 + +【更新x:updating x】【去除x的重叠区间:delecting the overlapping intervals with x】 + +【得到结果:achieve the results】 + + + +![1](../pictures/interval/1.gif) + +Now we implement our algorithm. For the step one, since we ordered `end` in advance, then it's easy to choose the 'x'. The key point is how to delect the intervals intersecting with 'x' and choose the new 'x' for the next loop. + +Thanks to the ordering, it's not difficult to find out all the interval intersecting with 'x' will contain the `end` of 'x'. Namely, if a interval doesn't contain the ending point of 'x', then its `start` must bigger or equal to the `end` of 'x'. + +【Comments for the chinese in the picture】 + +![2](../pictures/interval/2.jpg) + +Here is the code: + +```java +public int intervalSchedule(int[][] intvs) { + if (intvs.length == 0) return 0; + // ascending sorting by end + Arrays.sort(intvs, new Comparator() { + public int compare(int[] a, int[] b) { + return a[1] - b[1]; + } + }); + // at least have one interval without intersection + int count = 1; + // after sorting, the first interval is x + int x_end = intvs[0][1]; + for (int[] interval : intvs) { + int start = interval[0]; + if (start >= x_end) { + // get the next selected interval + count++; + x_end = interval[1]; + } + } + return count; +} +``` + +### Third Part: Example for the Application + +Now we will take some problem from leetcode to apply the interval scheduling algorithm. + +【Leetcode 435】Given a collection of intervals, find the minimum number of intervals you need to remove to make the rest of the intervals non-overlapping. + +**Example 1:** + +``` +Input: [[1,2],[2,3],[3,4],[1,3]] +Output: 1 +Explanation: [1,3] can be removed and the rest of intervals are non-overlapping. +``` + +**Example 2:** + +``` +Input: [[1,2],[1,2],[1,2]] +Output: 2 +Explanation: You need to remove two [1,2] to make the rest of intervals non-overlapping. +``` + +**Example 3:** + +``` +Input: [[1,2],[2,3]] +Output: 0 +Explanation: You don't need to remove any of the intervals since they're already non-overlapping. +``` + +**Note:** + +1. You may assume the interval's end point is always bigger than its start point. +2. Intervals like [1,2] and [2,3] have borders "touching" but they don't overlap each other. + +Since we are able to compute the original case, it's easy to finish this case by achieving the intervals which need to be removed. + +```java +int eraseOverlapIntervals(int[][] intervals) { + int n = intervals.length; + return n - intervalSchedule(intervals); +} +``` + +【Leetcode 452】Minimum Number of Arrows to Burst Balloons + +There are a number of spherical balloons spread in two-dimensional space. For each balloon, provided input is the start and end coordinates of the horizontal diameter. Since it's horizontal, y-coordinates don't matter and hence the x-coordinates of start and end of the diameter suffice. Start is always smaller than end. There will be at most 104 balloons. + +An arrow can be shot up exactly vertically from different points along the x-axis. A balloon with xstart and xend bursts by an arrow shot at x if xstart ≤ x ≤ xend. There is no limit to the number of arrows that can be shot. An arrow once shot keeps travelling up infinitely. The problem is to find the minimum number of arrows that must be shot to burst all balloons. + +**Example:** + +``` +Input: +[[10,16], [2,8], [1,6], [7,12]] + +Output: +2 + +Explanation: +One way is to shoot one arrow for example at x = 6 (bursting the balloons [2,8] and [1,6]) and another arrow at x = 11 (bursting the other two balloons). +``` + +Actually, it's not difficult to find that this question is the same as the interval scheduling algorithm. If there are n intervals without overlapping at most, then at least n arrows which get throw all the intervals are needed. + +![3](../pictures/interval/3.jpg) + +There still a little difference: in the interval schedule, the same border will not be regarded as overlapping, but it counts in this problem. + +【Explanations for the chinese in the picture】 + +【射气球:shooting the balballoon】 + +![4](../pictures/interval/4.jpg) + +Therefore, we can get the answer to this problem with only a little change. + +```java +int findMinArrowShots(int[][] intvs) { + // ... + + for (int[] interval : intvs) { + int start = interval[0]; + // Change >= into > + if (start > x_end) { + count++; + x_end = interval[1]; + } + } + return count; +} +``` + +It's not difficult to understand why it ought be done like that: 'x' should not be updated when `start == x_end` , since the same border is also regarded as overlapping. \ No newline at end of file diff --git a/dynamic_programming/OptimalSubstructure.md b/dynamic_programming/OptimalSubstructure.md new file mode 100644 index 0000000000..b07adb467f --- /dev/null +++ b/dynamic_programming/OptimalSubstructure.md @@ -0,0 +1,134 @@ +# Q&A on Dynamic Programming + +**Translator: [qy-yang](https://github.com/qy-yang)** + +**Author: [labuladong](https://github.com/labuladong)** + +This article will answer two questions: + +1. What exactly is called "optimal substructure" and what is the relationship with dynamic programming? + +2. Why does dynamic programming have various ways to traverse `dp` arrays, some are traversing fowards, some are traversing backwards, and some are traversing diagonally. + +### 1. Optimal substructure + +"Optimal substructure" is a specific property of some problems and is not exclusive to dynamic programming. In other words, many problems actually have optimal substructures, but most of them do not have overlapping subproblems, so we cannot classify them dynamic programming problems. + +Here is a trivial example: supposed your school has 10 classes, and you have already calculated the highest test score for each class. So if I ask you to calculate the highest grade in the school, how would you do it? Obviously, it is not necessary to re-traverse the scores of all the students'. You only need to take the maxium score among the 10x highest (from each class). + +This example **exhibits optimal substructure**: the optimal solution of a problem can be derived from the optimal solultions of subproblems. Calculating the highest score of **each class** is the subproblem. Once you know the answers to all the subprbolems, you can use this to derive the solution of the original problem which calculating the highest score across the school. + +Although this problem has optimal-substructure property, it cannot be solved by dynamic programming due to lack of overlapping-subproblem property. + +Here is an another example: Supposed your school has 10 classes, and you have known the maximum score difference (the difference between the highest score and lowest score) of each class. And now you want to calculate the maximum score difference among the students in the school, how would you do it? You should note that it can't be calculated by taking the maximum score difference of these 10 classes. As the maximum score difference of the 10 classes does not necessarily result in the maximum score difference of the entire school. More concretely, the maximum score difference of the school may be the derived by the highest score of class 3 and the lowest score of class 6. + +This problem does **not exhibit optimal substructure**, you cannot get the optimal solution of school through optimal solutions of each class. As mentioned in [Detailed Explanation of Dynamic Programming](https://github.com/labuladong/fucking-algorithm/blob/english/dynamic_programming/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E8%AF%A6%E8%A7%A3%E8%BF%9B%E9%98%B6.md), the subproblems must be independent of each other to satisfy the optimal-substructure requirement. In this example, the maximum score difference of the whole school may be derived from two classes. In other words, the subproblems are not independent, so this problem does not have the optimal substructure. + +**So what should be done when it lacks of optimal substructure? The trick is: recontructing the problem**. For the problem of maximum score difference, as we can't use score differences of each class, we can write a piece of brute-force code like this: + +```java +int result = 0; +for (Student a : school) { + for (Student b : school) { + if (a is b) continue; + result = max(result, |a.score - b.score|); + } +} +return result; +``` + +Next we can transform it to an equivalent problem and we should realise that: the maximum score difference is equivalent to the difference between the highest score and the lowest score; the highest and lowest score is required. So it is back to first question that has optimal substructure, and we can apply the optimal substructure to solve the highest-score & lowest-score problem, then solve the problem of maximum score difference. This is more efficient, isn't? + +The examples above are quite simple, but readers should notice that we are constantly seeking the maximum/minimum values for dynamic programming problems. This is exactly the same with the examples. Once again, dynamic programming is nothing more than solving the overlapping subproblems. + +Previous sections "different definitions with different solutions" and ["throwing eggs in high building throwing (advanced)"](https://github.com/labuladong/fucking-algorithm/blob/english/dynamic_programming/SuperEggDropAdvanced.md) showed readers on how to transform the problem. Reader should understant that different optimal substructures may lead to different solutions and efficiency. + +Here is another common and simple example: find the maximum value of a binary tree (for simplicity, assume that all the values in the nodes are non-negative): + +```java +int maxVal(TreeNode root) { + if (root == null) + return -1; + int left = maxVal(root.left); + int right = maxVal(root.right); + return max(root.val, left, right); +} +``` + +Reader can observe that this problem also exhibits optimal substructure. The maximum value of the tree rooted at "root" node can be calculated from the maximum value of the subtrees (subproblem) at left side and right side. This is similar to the example of the highest score in school before. + +The tree example above is not a dynamic programming problem. And readers should notice that the optimal-substructure property is not unique for dynamic programming. Most of the problems with optimal values have this property. **However, the optimal substructure is a necessary condition for dynamic programming problems.** So in the future, if you encounter the problem of optimal value. The dynamic programming is one of the right idea. This is the trick. + +Dynamic programming is to induce the optimal solution starting from trivial base case. And it can be viewed as a chain reaction. Only the problems with optimal substructure have the chain reaction. + +The process of finding the optimal substructure is actually the process of verifying correctness of state transition equation. There exists a brute-force solution, if the state transition exhibits the optimal substructure. Next is to check if there are overlapping subproblems. If so, some optimization can be done. This is another trick. + +We are not giving the examples of non-classical dynamic programming here. Readers can read more about how state transition follows the optimal substructure from previous articles. Next, let ’s look at another confusing issue with dynamic programming. + +### 2. Traversal order of the `dp` array + +I believe that some readers will definitely be confused with the traversal orders of `dp` arrays when doing dynamic programming problems . Taking a two-dimensional `dp` array as an example, sometimes we traverse forward: + +```java +int[][] dp = new int[m][n]; +for (int i = 0; i < m; i++) + for (int j = 0; j < n; j++) + // Calculate dp[i][j] +``` + +sometimes we traverse backward: + +```java +for (int i = m - 1; i >= 0; i--) + for (int j = n - 1; j >= 0; j--) + // Calculate dp[i][j] +``` + +and sometimes it may traverse diagonally: + +```java +// Traverse the array diagonally +for (int l = 2; l <= n; l++) { + for (int i = 0; i <= n - l; i++) { + int j = l + i - 1; + // Calculate dp[i][j] + } +} +``` + +Even more confusing, a correct answer can be obtained by traversing either forward or backward sometimes. For example, we can do both forward and backward in some parts of the problem ["best time to buy and sell stock".](https://github.com/labuladong/fucking-algorithm/blob/english/dynamic_programming/%E5%9B%A2%E7%81%AD%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98.md) + +If you look it closely, you should find out the reason. There are two rules you should take note: + +**1. During traversal, all the required states must have been calculated**. + +**2. The final point of the traversal must be the point where the result is stored**. + +Let's explain the two principles in detail. + +For example, the classic problem "edit distance" that explained in the previous article [Edit Distance](https://github.com/labuladong/fucking-algorithm/blob/english/dynamic_programming/EditDistance.md), by definition of `dp`, we know that the base case is `dp[..][0]` and `dp[0][..]`; and the final answer is `dp[m][n]`. We also know from the state transition equation that `dp[i][j]` is derived from `dp[i-1][j]`, `dp[i] [j-1]`, `dp [i-1] [j-1]`, as shown below: + +![](../pictures/optimal_substructure/1.jpg) + +So, referring to the two principles, how would you traverse the `dp` array? It should be a forward traversal: + +```java +for (int i = 1; i < m; i++) + for (int j = 1; j < n; j++) + // First calculate dp[i-1][j], dp[i][j-1], dp[i-1][j-1] + // Then calculate dp[i][j] +``` + +In this way, the left, top, and top left of each iteration will be either base cases or states calculated before, and finally it will end up with the answer we want `dp[m][n]`. + +Another example, the palindrome subsequence problem, refer to [Strategies For Subsequence Problem](https://github.com/labuladong/fucking-algorithm/blob/english/dynamic_programming/StrategiesForSubsequenceProblem.md) for details, from the definition of `dp` array, we know the base case is in the middle of diagonal of the array. `dp[i][j]` is derived from `dp[i+1][j]`, `dp[i][j-1]`, and `dp[i+1][j-1]`, and the final answer to be calculated is `dp[0][n-1]`, as shown below: + +![](../pictures/subsequence/4.jpg) + +In this case, there are two correct traversal orders: + +![](../pictures/subsequence/5.jpg) + +Either traverse obliquely from left to right, or traverse from bottom to top, left to right, so that to ensure the left, bottom, and bottom left of `dp[i][j]` have been calculated. + +Now, readers should understand these two principles, which are mainly dependent on the base case and the location of final result. Just to ensure that the intermediate results used during traversal have been calculated. There are multiple ways to get the correct answer sometimes, and readers can choose one based on your preference. diff --git a/dynamic_programming/README.md b/dynamic_programming/README.md deleted file mode 100644 index 5075ab2b62..0000000000 --- a/dynamic_programming/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# 动态规划系列 - -我们公众号最火的就是动态规划系列的文章,也许是动态规划问题有难度而且有意思,也许因为它是面试常考题型。不管你之前是否害怕动态规划系列的问题,相信这一章的内容足以帮助你消除对动态规划算法的恐惧。 - -具体来说,动态规划的一般流程就是三步:**暴力的递归解法 -> 带备忘录的递归解法 -> 迭代的动态规划解法**。 - -就思考流程来说,就分为一下几步:**找到状态和选择 -> 明确 dp 数组/函数的定义 -> 寻找状态之间的关系**。 - -这就是思维模式的框架,**本章都会按照以上的模式来解决问题,辅助读者养成这种模式思维**,有了方向遇到问题就不会抓瞎,足以解决一般的动态规划问题。 - -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: - -![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git a/dynamic_programming/RegularExpression.md b/dynamic_programming/RegularExpression.md index 7f7cce6e4d..3aaa2e4b4d 100644 --- a/dynamic_programming/RegularExpression.md +++ b/dynamic_programming/RegularExpression.md @@ -205,10 +205,4 @@ In this article, you have gained a deep insight into the algorithmic implementat Reviewing the whole process, you should be able to understand the process of algorithm design: from similar simple problems to the basic framework of the gradual assembly of new logic, eventually become a more complex, sophisticated algorithm. So, you guys don't be afraid of some more complex algorithm problems. No matter how big the algorithm in your eyes is just a piece of cake. -If this article is helpful to you, welcome to pay attention to my wechat official account **labuladong**, I'm committed to make the algorithm problem more clear ~ - - - - - - +If this article is helpful to you, welcome to pay attention to my wechat official account **labuladong**, I'm committed to make the algorithm problem more clear ~ \ No newline at end of file diff --git a/dynamic_programming/StrategiesForSubsequenceProblem.md b/dynamic_programming/StrategiesForSubsequenceProblem.md new file mode 100644 index 0000000000..7c130233f5 --- /dev/null +++ b/dynamic_programming/StrategiesForSubsequenceProblem.md @@ -0,0 +1,151 @@ +# The Strategies of Subsequence Problem + +**Translator: [sunqiuming526](https://github.com/sunqiuming526)** + +**Author: [labuladong](https://github.com/labuladong)** + +Subsequence Problem is one of the most common algorithm problem, which is not easy to figure out. + +First of all, the subsequence problem itself is more difficult than those for substring and subarray, since the former needs to deal with discontinuous sequence, while the latter two are continuous. It is hard enough to simply enumerate the subsequences, let alone solve related algorithm problems. + +Moreover, the subsequence problem is likely to involve two strings, such as the "Longest Common Subsequence (LCS)" problem in the previous article. Without some processing experience, it is really not easy to figure out. Therefore, this article will come up with a routine for the subsequence related problems. In fact, there are only two types of strategies. As long as these two problem-solving strategies in your mind, it's highly possible to ace the problem. + +Generally speaking, this kind of question would ask you to find a **longest subsequence **. Since the shortest subsequence, on the other hand, is just a character, which is not worth asking. Once it comes to subsequences or extreme value problems, it is almost certain that **we need to use dynamic programming techniques, and the time complexity is generally O(n^2)**. + +The reason is quite simple. Just think about a string. How many possibilities are there for its subsequence? The answer is at least exponential, right? Thus, we have no reason not to use DP. + +Since dynamic programming is used, it is necessary to define the DP array and find the state transition relation. The two strategies we mentioned above are actually the ideas of defining DP arrays. Different problems may require different DP array definitions to solve. + +### 1. Two Strategies + +**1.1 The first strategy is using a one-dimensional DP array** + +```java +int n = array.length; +int[] dp = new int[n]; + +for (int i = 1; i < n; i++) { + for (int j = 0; j < i; j++) { + dp[i] = max|min(dp[i], dp[j] + ...) + } +} +``` + +Take an example we used before -- "the Longest Increasing Subsequence (LIS)". The definition of DP array in this case is as below: + +**We define `dp[i]` as the length of the required subsequence (the longest increasing subsequence) within the subarray `array [0..i]`**. + +Why does the LIS problem require this strategy? The foregoing is clear enough -- because this strategy is in line with the induction method, and the state transition relation can be found. We are not going to discuss this in details further. + +**1.2 The second strategy is using a two-dimensional DP array** + +```java +int n = arr.length; +int[][] dp = new dp[n][n]; + +for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + if (arr[i] == arr[j]) + dp[i][j] = dp[i][j] + ... + else + dp[i][j] = max|min(...) + } +} +``` + +This strategy is used relatively more, especially for the subsequences problems involving two strings / arrays, such as the **"Longest Common Subsequence"** we mentioned before. The definition of the DP array in this strategy is further divided into two cases: "Only one string is involved" and "Two strings are involved". + +**a) In the case where two strings are involved** (e.g. LCS), the definition of DP array is as follows: + +**We define `dp[i][j]` as the length of the required subsequence (longest common subsequence) within the subarray `arr1[0..i]` and the subarray `arr2[0..j]`**. + +**b) In the case where only one string is involved** (such as the Longest Palindrome Subsequence (LPS) which will be discussed in this article later), the definition of DP array is as follows: + +**We define `dp[i][j]` as the length of the required subsequence (the longest palindrome subsequence) within the subarray `array [i..j]`**. + + + +For the first case, you can refer to these two articles: "Editing distance", "Common Subsequence". + +Now let's talk about the Longest Palindrome Subsequence (LPS) problem to explain how to solve DP in the second case in details. + +### 2. The Longest Palindrome Subsequence + +We have solve the "Longest Palindrome Substring" problem before. This time, the difficulty is increased by finding the length of the Longest Palindrome Subsequence instead of substring: + +![](../pictures/subsequence/1.jpg) + +In this question, **we define `dp[i][j]` as the length of the longest palindrome subsequence within the substring `s[i..j]`**. Please remember this definition so as to understand the algorithm. + +Why do we define a two-dimensional DP array like this? We mentioned many times before that **finding state transition relation requires inductive thinking. To put it plainly, it is how we derive unknown parts from known results**, which makes it easy to generalize and discover the state transition relation. + +Specifically, if we want to find `dp[i][j]`, suppose you have already got the result of the subproblem `dp[i+1][j-1]` (the length of the longest palindrome subsequence in`s[i+1..j-1]`), can you find a way to calculate the value of` dp[i][j] `(the length of the longest palindrome subsequence in` s[i..j] `) ? + + + +![](../pictures/subsequence/1.jpg) + +The answer is yes! It depends on the characters of `s[i]` and `s[j]`: + +**If they are equal**, then the longest palindrome subsequence in `s[i+1..j-1]` would be these two characters plus the longest palindrome subsequence in `s[i..j]`: + + + +![](../pictures/subsequence/2.jpg) + +**If they are not equal**, it means that they **cannot appear at the same time** in the longest palindrome subsequence of `s[i..j]`. Therefore, we add them **separately** to `s[i+1..j-1] ` to see which substring produces a longer palindrome subsequence: + +![](../pictures/subsequence/3.jpg) + +The code of the above two cases can be written like this: + +```java +if (s[i] == s[j]) + // These two chars must be in the longest palindrome sequence + dp[i][j] = dp[i + 1][j - 1] + 2; +else + // Choose the longer palindrome subsequence from s[i+1..j] and s[i..j-1] + dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); +``` + +At this point, the state transition equation is derived. According to the definition of the DP array, what we require is `dp[0][n-1]`, which is the length of the longest palindrome subsequence of the entire `s`. + +### 3. Code Implementation + +Let's begin with defining the base case. If there is only one character, the longest palindrome subsequence length is 1, which can be represented as `dp[i][j] = 1 (i == j)`. + +Since `i`must be less than or equal to `j`, for those locations where `i > j`, there are no subsequences at all and thus should be initialized to 0. + +In addition, look at the state transition equation we just got. To find `dp[i][j]`, you need to know `dp[i+1][j-1]`, `dp[i+1][j]` and`dp[i][j -1]` these three values. And look at the base case we determined, this is how the DP array looks like after being filled: + +![](../pictures/subsequence/4.jpg) + +**In order to guarantee that before each calculation of `dp[i][j]`, the values in the left, down and right direction have been calculated, we can only traverse it diagonally or reversely**:![](../pictures/subsequence/5.jpg) + +Here I choose to traverse reversely. The code is as follows: + +```cpp +int longestPalindromeSubseq(string s) { + int n = s.size(); + // DP arrays are all initialized to 0 + vector> dp(n, vector(n, 0)); + // base case + for (int i = 0; i < n; i++) + dp[i][i] = 1; + // Reverse traversal to ensure correct state transition + for (int i = n - 1; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + // State transition equation + if (s[i] == s[j]) + dp[i][j] = dp[i + 1][j - 1] + 2; + else + dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); + } + } + // return the length of LPS + return dp[0][n - 1]; +} +``` + +So far, the longest palindrome subsequence problem has been solved. + diff --git a/dynamic_programming/SuperEggDropAdvanced.md b/dynamic_programming/SuperEggDropAdvanced.md new file mode 100644 index 0000000000..0c004ab41a --- /dev/null +++ b/dynamic_programming/SuperEggDropAdvanced.md @@ -0,0 +1,263 @@ +# Super Egg Drop(Advanced) + +**Translator: [Jieyixia](https://github.com/Jieyixia)** + +**Author: [labuladong](https://github.com/labuladong)** + +The Super Egg Drop problem (Leetcode 887) has been discussed in the last article using the classic dynamic programming method. If you are not very familiar with this problem and the classic method, please read「Super Egg Drop」, which is the basic of following contents. + +In this article, we will optimize this problem with other two more efficient methods. One is adding binary search into the classic dynamic programming method, the other one is redefining state transition equation. + +### Binary Search Optimization +We want to find the floor `F` for a building with `N` floors using **minimum** number of moves (Each move means dropping an egg from a certain floor). Any egg dropped at a floor higher than `F` will break, and any egg dropped at or below floor `F` will not break. First, let's review the classic dynamic programming method: + +1、To know `F`, we should traverse the situations that we drop an egg from floor `i`, `1 <= i <= N` and find the situation that costs minimum number of moves; + +2、Anytime we drop an egg, there are two possible outcomes: the egg is broken or not broken; + +3、If the egg is broken, `F` <= `i`; else, `F` > `i`; + +4. Whether the egg is broken or not depends on which outcome causes **more** moves, since the goal is to know with certainty what the value of `F` is, regardless of its initial value. + +The code for state transition: + +```python +# current state: K eggs, N floors +# return the optimal results under current state +def dp(K, N): + for 1 <= i <= N: + # the mininum moves + res = min(res, + max( + dp(K - 1, i - 1), # the egg is broken + dp(K, N - i) # the egg is not broken + ) + 1 # drop an egg at floor i + ) + return res +``` + +The above code reflects the following state transition equation: + +$$ dp(K, N) = \min_{0 <= i <= N}\{\max\{dp(K - 1, i - 1), dp(K, N - i)\} + 1\}$$ + +If you can understand the state transition equation, it is not difficult to understand how to use binary search to optimize the process. + +From the definition of `dp(K, N)` array (the minimum number of moves with `K` eggs and `N` floors), we know that when `K` is fixed, `dp(K, N)` will increase monotonically as `N` increases. In the above state transition equation, `dp(K - 1, i - 1)` will increase monotonically and `dp(K, N - i)` will decrease monotonically as `i` increases from 1 to `N`. + +![](../pictures/SuperEggDrop/2.jpg) + +We need to find the maximum between `dp(K - 1, i - 1)` and `dp(K, N - i)`, and then choose the minimum one among those maximum values. This means that we should get the intersection of the two straight lines (the lowest points of the red polyline). + +In other article, we have mentioned that binary search is widely used in many cases, for example: + +```java +for (int i = 0; i < n; i++) { + if (isOK(i)) + return i; +} +``` + +In the above case, it is likely to use binary search to optimize the complexity of linear search. Review the two `dp` functions, the lowest point satisfies following condition: + +```java +for (int i = 1; i <= N; i++) { + if (dp(K - 1, i - 1) == dp(K, N - i)) + return dp(K, N - i); +} +``` + +If you are familiar with binary search, it is easy to know that what we need to search is the valley value. Let's look at the following code: + +```python +def superEggDrop(self, K: int, N: int) -> int: + + memo = dict() + def dp(K, N): + if K == 1: return N + if N == 0: return 0 + if (K, N) in memo: + return memo[(K, N)] + + # for 1 <= i <= N: + # res = min(res, + # max( + # dp(K - 1, i - 1), + # dp(K, N - i) + # ) + 1 + # ) + + res = float('INF') + # use binary search to replace linear search + lo, hi = 1, N + while lo <= hi: + mid = (lo + hi) // 2 + broken = dp(K - 1, mid - 1) # the egg is broken + not_broken = dp(K, N - mid) # the egg is not broken + # res = min(max(broken, not broken) + 1) + if broken > not_broken: + hi = mid - 1 + res = min(res, broken + 1) + else: + lo = mid + 1 + res = min(res, not_broken + 1) + + memo[(K, N)] = res + return res + + return dp(K, N) +``` +The time complexity for dynamic programming problems is **the number of sub-problems × the complexity of function**. + +Regardless of the recursive part, the complexity of `dp` function is O(logN), since binary search is used. + +The number of sub-problems equals to the number of different states, which is O(KN). + +Therefore, the time complexity of the improved method is O(K\*N\*logN), which is more efficient than O(KN^2) of the classic dynamic programming method. The space complexity is O(KN). + + +### Redefine State Transition Equation + +It has been mentioned in other article that the state transition equation for the same problem is not unique, resulting in different methods with different complexity. + +Review the definition of the `dp` function: + +```python +def dp(k, n) -> int +# current state: k eggs, n floors +# return the optimal results under current state +``` + +Or the `dp` array: + +```python +dp[k][n] = m +# current state: k eggs, n floors +# return the optimal results under current state +``` + +Based on this definition, the expected answer is `dp(K, N)`. The method of exhaustion is necessary, we have to compare the results under different situations `1<=i<=N` to find the minimum. Binary search helps to reduce the search space. + +Now, we make some modifications to the definition of `dp` array, current states are `k` eggs and allowed maximum number of moves `m`. `dp[k][m] = n` represents that we can accurately determine a floor `F` for a building with at most `n` floors. More specifically: + +```python +dp[k][m] = n +# current state: k eggs, at most m moves +# `F` can be determined for a building with at most n floors + +# For example: dp[1][7] = 7 represents:; +# one egg is given and you can drop an egg at certain floor 7 times; +# you can determine floor `F` for a building with at most 7 floors; +# any egg dropped at a floor higher than `F` will break; +# any egg dropped at or below floor `F` will not break. +# (search linearly from the first floor) +``` + +This is actually a reverse version of our original definition. We want to know the number of moves at last. But under this new definition, it is one state of the `dp` array instead of the result. This is how we deal with this problem: + +```java +int superEggDrop(int K, int N) { + + int m = 0; + while (dp[K][m] < N) { + m++; + // state transition... + } + return m; +} +``` + +The `while` loop ends when `dp[K][m] == N`, which means that given `K` eggs and at most `m` moves, floor `F` can be accurately determined for a building with `N` floors. This is exactly the same as before. + +Then how to find the state transition equation? Let's start from the initial idea: + +![](../pictures/SuperEggDrop/1.jpg) + +You have to traverse `1<=i<=N` to find the minimum. But these are not necessary under the new definition of `dp` array. This is based on the following two facts: + +**1、There are only two possible outcomes when you drop an egg at any floor: the egg is broken or not broken. If the egg is broken, go downstairs. If the egg is not broken, go upstairs**。 + +**2、No matter which outcome, total number of floors = the number of floors upstairs + the number of floors downstairs + 1(current floor)**。 + +Base on the two facts, we can write the following state transition equation: + +`dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1` + +**`dp[k][m - 1]` is the number of floors upstairs**. `k` keeps unchanged since the egg is not broken, `m` minus one; + +**`dp[k - 1][m - 1]` is the number of floors downstairs**. `k` minus one since the egg is broken, `m` minus one. + +PS: why `m` minus one instead of plus one? According to the definition, `m` is the upper bound of the number of moves, instead of the number of moves。 + +![](../pictures/SuperEggDrop/3.jpg) + +The code is: + +```java +int superEggDrop(int K, int N) { + // m will not exceed N (linear search) + int[][] dp = new int[K + 1][N + 1]; + // base case: + // dp[0][..] = 0 + // dp[..][0] = 0 + // Java intializes the array to be all 0 by default + int m = 0; + while (dp[K][m] < N) { + m++; + for (int k = 1; k <= K; k++) + dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; + } + return m; +} +``` + +which equals to: + +```java +for (int m = 1; dp[K][m] < N; m++) + for (int k = 1; k <= K; k++) + dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; +``` + +It seems more familiar. Since we need to get a certain index `m` of the `dp` array, `while` loop is used. + +The time complexity of this algorithm is apparently O(KN), two nested `for` loop。 + +Moreover, `dp[m][k]` only relates to the left and left-top states, it is easy to simplify `dp` array to one dimension. + +### More Optimization + +In this section, we will introduce some mathematical methods without specific details. + +Based on the `dp` definition in the previous section, **`dp(k, m)` increases monotonically when `m` increases. When `k` is fixed, a bigger `m` will cause a bigger `N`**。 + +We can also use binary search to optimize the process, `dp[K][m] == N` is the stop criterion. Time complexity further decreases to O(KlogN), we can assume `g(k, m) =`…… + +All right, let's stop. I think it's enough to understand the binary search method with O(K\*N\*logN) time complexity. + +It is certain that we should change the for loop to find `m`: + +```java +// change the linear search to binary search +// for (int m = 1; dp[K][m] < N; m++) +int lo = 1, hi = N; +while (lo < hi) { + int mid = (lo + hi) / 2; + if (... < N) { + lo = ... + } else { + hi = ... + } + + for (int k = 1; k <= K; k++) + // state transition equation +} +``` +In conclusion, the first optimization using binary search is based on the monotonicity of the `dp` function; the second optimization modifies the state transition function. For most of us, it is easier to understand the idea of binary search instead of different forms of `dp` array. + +If you have grasped the basic methods well, the methods in the last section are good challenges for you. + + + + + + diff --git a/dynamic_programming/ThrowingEggsinHighBuildings.md b/dynamic_programming/ThrowingEggsinHighBuildings.md new file mode 100644 index 0000000000..545e5834eb --- /dev/null +++ b/dynamic_programming/ThrowingEggsinHighBuildings.md @@ -0,0 +1,230 @@ +# Classic Dynamic Programming Problem: Throwing Eggs in High Buildings + +**Translator: [timmmGZ](https://github.com/timmmGZ)** + +**Author: [labuladong](https://github.com/labuladong)** + +Today I am going to talk about a very classic algorithm problem. Suppose there are several floors in a high building and several eggs in your hands, lets calculate the minimum number of attempts and find out the floor where the eggs just won’t be broken. Many famous Chinese large enterprises, Google and Facebook often examine this question in a job interview, but they throw cups, broken bowls or something else instead of eggs, because they think throwing eggs is too wasteful. + +Specific problems will be discussed later, but there are many solutions to this problem, Dynamic Programming has already had several ideas with different efficiency, moreover, there is an extremely efficient mathematical solution. Anyway, let's throw the tricky and weird skills away, because these skills can‘t be inferior to each other, it is not cost effective to learn. + +Let's use the general idea of Dynamic Programming that we always emphasized to study this problem. + +### First, analyze the problem + +Question: There is a `N`-storey building indexed from 1 to `N` in front of you, you get `K` eggs (`K` >= 1). It is determined that this building has floor F (`0 <= F <= N`), you drop an egg down from this floor and the egg **just won’t be broken** (the floors above `F` will break, and the floors below `F` won't break). Now, in **the worst** case, how many times **at least** do you need to throw the eggs to **determine** what floor is this floor `F` on? + +In other words, you need to find the highest floor `F` where you can't break the eggs, but what does it mean how many times "at least" to throw "in the worst"? We will understand by giving an example. + +For example, **regardless of the number of eggs**, there are 7 floors, how do you find the floor where the eggs are just broken? + +The most primitive way is linear search: Let's throw it on the first floor and it isn't broken, then we throw it on the second floor, not broken, then we go to the third floor...... + +With this strategy, the **worst** case would be that I try to the 7th floor without breaking the eggs (`F` = 7), that is, I threw the eggs 7 times. + +Now you may understand what is called "the worst case", **eggs breaking must happen when the search interval is exhausted (from 0 till N)**, if you break the eggs on the first floor, this is your luck, not the worst case. + +Now let’s figure out what it means how many times “at least” to throw? Regardless of the number of eggs, it is still 7 floors, we can optimize the strategy. + +The best strategy is to use the Binary Search idea. first, we go to `(1 + 7) / 2 = 4th` floor and throw an egg: + +If it is broken, then it means `F` is less than 4, therefore I will go to `(1 + 3) / 2 = 2th` floor to try again... + +If it isn’t broken, then it means `F` is greater than or equal to 4, therefore I will go to `(5 + 7) / 2 = 6th` floor to try again... + +In this strategy, the **worst** case is that you try to the 7th floor without breaking the eggs (`F = 7`), or the eggs were broken all the way to the 1st floor (`F = 0`). However, no matter what the worst case is, you only need to try `log2(7)` rounding up equal to 3 times, which is less than 7 times you just tried. This is the so called how many times **at least** to throw. + +PS: This is a bit like Big O notation which is for calculating the complexity of algorithm. + +In fact, if the number of eggs is not limited, the binary search method can obviously get the least number of attempts, but the problem is that **now the number of eggs is limited by `K`, and you can't use the binary search directly.** + +For example, you just get 1 egg, 7 floors, are you sure to use binary search? You just go to the 4th floor and throw it, if the eggs are not broken, it is okay, but if they are broken, you will not have the eggs to continue the test, then you can’t be sure the floor `F` on which the eggs won't be broken. In this case, only linear search can be used, and the algorithm should return a result of 7. + +Some readers may have this idea: binary search is undoubtedly the fastest way to eliminate floors, then use binary search first, and then use linear search when there is only 1 egg left, is the result the least number of eggs thrown? + +Unfortunately, it’s not, for example, make the floor higher, there are 100 floors and 2 eggs, if you throw it on the 50th floor and it is broken, you can only search from 1st to 49th floor linearly, in the worst case, you have to throw 50 times. + +If you don't use 「binary search」, but 「quinary search」 and 「decimal search」, it will greatly reduce the number of the worst case attempts. Let's say the first egg is thrown every ten floors, where the egg is broken, then where you search linearly for the second egg, it won't be more than 20 times in total. + +Actually, the optimal solution is 14 times. There are many optimal strategies, and there is no regularity at all. + +I talk so much nonsense in order to make sure everyone understands the meaning of the topic, and realize that this topic is really complicated, it is even not easy to calculate by hand, so how to solve it with an algorithm? + +### Second, analysis of ideas + +For the dynamic programming problem, we can directly set the framework we have emphasized many times before: what is the 「state」 of this problem, what are 「choices」, and then use exhaustive method. + +**The 「status」 is obviously the number of eggs `K` currently possessed and the number of floors `N` to be tested.** As the test progresses, the number of eggs may decrease, and the search range of floors will decrease. This is the change of state. + +**The 「choice」 is actually choosing which floor to throw eggs on.** Looking back at the linear search and binary search idea, the binary search selects to throw the eggs in the middle of the floor interval each time, and the linear search chooses to test floor by floor, different choices will cause a state transition. + +Now the 「state」 and 「choice」 are clear, **the basic idea of dynamic programming is formed**: it must be a two dimensional `DP` array or a `DP` function with two state parameters to represent the state transition; and a for loop to traverse all the choices , choose the best option to update the status: + +```python +# Current state is K eggs and N floors +# Returns the optimal result in this state +def dp(K, N): + int res + for 1 <= i <= N: + res = min(res, Throw eggs on the i-th floor this time) + return res +``` +This pseudo code has not shown recursion and state transition yet, but the general algorithm framework has been completed. + +After we choose to throw a egg on the `i`-th floor, two situations could happen: the egg is broken and the egg is not broken. **Note that the state transition is now here**: + +**If the egg is broken**, then the number of eggs `K` should be reduced by one, and the search floor interval should be changed from`[1..N]`to`[1..i-1]`, `i-1` floors in total. + +**If the egg is not broken**, then the number of eggs `K` will not change, and the searched floor interval should be changed from`[1..N]`to`[i+1..N]`,`N-i` floors in total. + +![](../pictures/SuperEggDrop/1.jpg) + +PS: Attentive readers may ask, if throwing a egg on the i-th floor is not broken, the search range of the floor is narrowed down to the upper floors, should it include the i-th floor? No, because it is included. As I said at the beginning that F can be equal to 0, after recursing upwards, the i-th floor is actually equivalent to the 0th floor, so there is nothing wrong. + +Because we are asking the number of eggs to be thrown in **the worst case**, so whether the egg is broken on the `i` floor, it depends on which situation's result is **larger**: + +```python +def dp(K, N): + for 1 <= i <= N: + # Minimum number of eggs throwing in the worst case + res = min(res, + max( + dp(K - 1, i - 1), # broken + dp(K, N - i) # not broken + ) + 1 # throw once on the i-th floor + ) + return res +``` + +The recursive base case is easy to understand: when the number of floors `N` is 0, obviously no eggs need to be thrown; when the number of eggs `K` is 1, obviously all floors can only be searched linearly: + +```python +def dp(K, N): + if K == 1: return N + if N == 0: return 0 + ... +``` + +Now, this problem is actually solved! Just add a memo to eliminate overlapping subproblems: + +```python +def superEggDrop(K: int, N: int): + + memo = dict() + def dp(K, N) -> int: + # base case + if K == 1: return N + if N == 0: return 0 + # avoid calculating again + if (K, N) in memo: + return memo[(K, N)] + + res = float('INF') + # Exhaust all possible choices + for i in range(1, N + 1): + res = min(res, + max( + dp(K, N - i), + dp(K - 1, i - 1) + ) + 1 + ) + # Record into memo + memo[(K, N)] = res + return res + + return dp(K, N) +``` + +What is the time complexity of this algorithm? **The time complexity of the dynamic programming algorithm is the number of subproblems × the complexity of the function itself**. + +The complexity of the function itself is the complexity of itself without the recursive part. Here the `dp` function has a for loop, so the complexity of the function itself is O(N). + +The number of subproblems is the total number of combinations of the different states, which is obviously the Cartesian product of the two states, and it is O(KN). + +So the total time complexity of the algorithm is O(K*N^2) and the space complexity is O(KN). + +### Third, troubleshooting + +This problem is very complicated, but the algorithm code is very simple, This is the characteristic of dynamic programming, exhaustive method plus memo/ DP table optimization. + +First of all, some readers may not understand why the code uses a for loop to traverse the floors `[1..N]`, and may confuse this logic with the linear search discussed before. Actually not like so, **this is just making a 「choice」**. + +Let's say you have 2 eggs and you are facing 10 floors, which floor do you choose **this time**? Don't know, so just try all 10 floors. As for how to choose next time, you don't need to worry about it, There is a correct state transition, recursion will calculate the cost of each choice, the best one is the optimal solution. + +In addition, there are better solutions to this problem, such as modifying the for loop in the code to binary search, which can reduce the time complexity to O(K\*N\*logN); and then improving the dynamic programming solution can be further reduced to O(KN); use mathematical methods to solve, the time complexity reaches the optimal O(K*logN), and the space complexity reaches O(1). + +But such binary search above is also a bit misleading, you may think that it is similar to the binary search we discussed earlier, actually it is not the same at all. Above binary search can be used because the function graph of the state transition equation is monotonic, and the extreme value can be found quickly. + +Let me briefly introduce the optimization of binary search, In fact, it is just optimizing this code: + +```python +def dp(K, N): + for 1 <= i <= N: + # Minimum number of eggs throwing in the worst case + res = min(res, + max( + dp(K - 1, i - 1), # broken + dp(K, N - i) # not broken + ) + 1 # throw once on the i-th floor + ) + return res +``` + +This for loop is the code implementation of the following state transition equation: + + +![equation](http://latex.codecogs.com/gif.latex?%24%24%20dp%28K%2C%20N%29%20%3D%20%5Cmin_%7B0%20%3C%3D%20i%20%3C%3D%20N%7D%5C%7B%5Cmax%5C%7Bdp%28K%20-%201%2C%20i%20-%201%29%2C%20dp%28K%2C%20N%20-%20i%29%5C%7D%20+%201%5C%7D%24%24) + +First of all, according to the definition of the `dp(K, N)` array (there are `K` eggs and `N` floors, how many times at least do we need to throw the eggs?). **It is easy to know that when `K` is fixed, this function must be It is a monotonically increasing**, no matter how smart your strategy is, the number of tests must increase if the number of floors increases. + +Then notice the two functions `dp(K-1, i-1)` and `dp(K, N-i)`, where `i` is increasing from 1 to `N`, if we fix `K`and `N`, **treat these two functions as function with only one variable `i`, the former function should also increase monotonically with the increase of `i`, and the latter function should decrease monotonically with the increase of `i`**: + +![](../pictures/扔鸡蛋/2.jpg) + +Now find the larger value of these two functions, and then find the minimum of these larger values, it is actually to find the intersection as above figure, readers who are familiar with binary search must have already noticed that this is equivalent to finding the Valley value, we can use binary search to quickly find this point. + +Let's post the code directly, the idea is exactly the same: + +```python +def superEggDrop(self, K: int, N: int) -> int: + + memo = dict() + def dp(K, N): + if K == 1: return N + if N == 0: return 0 + if (K, N) in memo: + return memo[(K, N)] + + # for 1 <= i <= N: + # res = min(res, + # max( + # dp(K - 1, i - 1), + # dp(K, N - i) + # ) + 1 + # ) + + res = float('INF') + # use binary search instead of for loop(linear search) + lo, hi = 1, N + while lo <= hi: + mid = (lo + hi) // 2 + broken = dp(K - 1, mid - 1) # broken + not_broken = dp(K, N - mid) # not broken + # res = min(max(broken,not broken) + 1) + if broken > not_broken: + hi = mid - 1 + res = min(res, broken + 1) + else: + lo = mid + 1 + res = min(res, not_broken + 1) + + memo[(K, N)] = res + return res + + return dp(K, N) +``` + +I won’t discuss about other solutions here, I will just leave them in the next article. + + +I think our solution is enough: find the states, make the choices, it is clear and easy enough to understand, can be streamlined. If you can master this framework, then it's not too late to consider those tricky and weird skills. diff --git "a/dynamic_programming/\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/dynamic_programming/\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" deleted file mode 100644 index b67f065935..0000000000 --- "a/dynamic_programming/\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" +++ /dev/null @@ -1,188 +0,0 @@ -# 动态规划之博弈问题 - -上一篇文章 [几道智力题](../高频面试系列/一行代码解决的智力题.md) 中讨论到一个有趣的「石头游戏」,通过题目的限制条件,这个游戏是先手必胜的。但是智力题终究是智力题,真正的算法问题肯定不会是投机取巧能搞定的。所以,本文就借石头游戏来讲讲「假设两个人都足够聪明,最后谁会获胜」这一类问题该如何用动态规划算法解决。 - -博弈类问题的套路都差不多,下文举例讲解,其核心思路是在二维 dp 的基础上使用元组分别存储两个人的博弈结果。掌握了这个技巧以后,别人再问你什么俩海盗分宝石,俩人拿硬币的问题,你就告诉别人:我懒得想,直接给你写个算法算一下得了。 - -我们「石头游戏」改的更具有一般性: - -你和你的朋友面前有一排石头堆,用一个数组 piles 表示,piles[i] 表示第 i 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。 - -石头的堆数可以是任意正整数,石头的总数也可以是任意正整数,这样就能打破先手必胜的局面了。比如有三堆石头 `piles = [1, 100, 3]`,先手不管拿 1 还是 3,能够决定胜负的 100 都会被后手拿走,后手会获胜。 - -**假设两人都很聪明**,请你设计一个算法,返回先手和后手的最后得分(石头总数)之差。比如上面那个例子,先手能获得 4 分,后手会获得 100 分,你的算法应该返回 -96。 - -这样推广之后,这个问题算是一道 Hard 的动态规划问题了。**博弈问题的难点在于,两个人要轮流进行选择,而且都贼精明,应该如何编程表示这个过程呢?** - -还是强调多次的套路,首先明确 dp 数组的含义,然后和股票买卖系列问题类似,只要找到「状态」和「选择」,一切就水到渠成了。 - -### 一、定义 dp 数组的含义 - -定义 dp 数组的含义是很有技术含量的,同一问题可能有多种定义方法,不同的定义会引出不同的状态转移方程,不过只要逻辑没有问题,最终都能得到相同的答案。 - -我建议不要迷恋那些看起来很牛逼,代码很短小的奇技淫巧,最好是稳一点,采取可解释性最好,最容易推广的设计思路。本文就给出一种博弈问题的通用设计框架。 - -介绍 dp 数组的含义之前,我们先看一下 dp 数组最终的样子: - -![1](../pictures/博弈问题/1.png) - -下文讲解时,认为元组是包含 first 和 second 属性的一个类,而且为了节省篇幅,将这两个属性简写为 fir 和 sec。比如按上图的数据,我们说 `dp[1][3].fir = 10`,`dp[0][1].sec = 3`。 - -先回答几个读者可能提出的问题: - -这个二维 dp table 中存储的是元组,怎么编程表示呢?这个 dp table 有一半根本没用上,怎么优化?很简单,都不要管,先把解题的思路想明白了再谈也不迟。 - -**以下是对 dp 数组含义的解释:** - -```python -dp[i][j].fir 表示,对于 piles[i...j] 这部分石头堆,先手能获得的最高分数。 -dp[i][j].sec 表示,对于 piles[i...j] 这部分石头堆,后手能获得的最高分数。 - -举例理解一下,假设 piles = [3, 9, 1, 2],索引从 0 开始 -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 数组的定义,**状态显然有三个:开始的索引 i,结束的索引 j,当前轮到的人。** - -```python -dp[i][j][fir or sec] -其中: -0 <= i < piles.length -i <= j < piles.length -``` - -对于这个问题的每个状态,可以做的**选择有两个:选择最左边的那堆石头,或者选择最右边的那堆石头。** 我们可以这样穷举所有状态: - -```python -n = piles.length -for 0 <= i < n: - for j <= i < n: - for who in {fir, sec}: - dp[i][j][who] = max(left, right) - -``` - -上面的伪码是动态规划的一个大致的框架,股票系列问题中也有类似的伪码。这道题的难点在于,两人是交替进行选择的,也就是说先手的选择会对后手有影响,这怎么表达出来呢? - -根据我们对 dp 数组的定义,很容易解决这个难点,**写出状态转移方程:** - -```python -dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec) -dp[i][j].fir = max( 选择最左边的石头堆 , 选择最右边的石头堆 ) -# 解释:我作为先手,面对 piles[i...j] 时,有两种选择: -# 要么我选择最左边的那一堆石头,然后面对 piles[i+1...j] -# 但是此时轮到对方,相当于我变成了后手; -# 要么我选择最右边的那一堆石头,然后面对 piles[i...j-1] -# 但是此时轮到对方,相当于我变成了后手。 - -if 先手选择左边: - dp[i][j].sec = dp[i+1][j].fir -if 先手选择右边: - dp[i][j].sec = dp[i][j-1].fir -# 解释:我作为后手,要等先手先选择,有两种情况: -# 如果先手选择了最左边那堆,给我剩下了 piles[i+1...j] -# 此时轮到我,我变成了先手; -# 如果先手选择了最右边那堆,给我剩下了 piles[i...j-1] -# 此时轮到我,我变成了先手。 -``` - -根据 dp 数组的定义,我们也可以找出 **base case**,也就是最简单的情况: - -```python -dp[i][j].fir = piles[i] -dp[i][j].sec = 0 -其中 0 <= i == j < n -# 解释:i 和 j 相等就是说面前只有一堆石头 piles[i] -# 那么显然先手的得分为 piles[i] -# 后手没有石头拿了,得分为 0 -``` - -![2](../pictures/博弈问题/2.png) - -这里需要注意一点,我们发现 base case 是斜着的,而且我们推算 dp[i][j] 时需要用到 dp[i+1][j] 和 dp[i][j-1]: - -![3](../pictures/博弈问题/3.png) - -所以说算法不能简单的一行一行遍历 dp 数组,**而要斜着遍历数组:** - -![4](../pictures/博弈问题/4.png) - -说实话,斜着遍历二维数组说起来容易,你还真不一定能想出来怎么实现,不信你思考一下?这么巧妙的状态转移方程都列出来了,要是不会写代码实现,那真的很尴尬了。 - - -### 三、代码实现 - -如何实现这个 fir 和 sec 元组呢,你可以用 python,自带元组类型;或者使用 C++ 的 pair 容器;或者用一个三维数组 `dp[n][n][2]`,最后一个维度就相当于元组;或者我们自己写一个 Pair 类: - -```java -class Pair { - int fir, sec; - Pair(int fir, int sec) { - this.fir = fir; - this.sec = sec; - } -} -``` - -然后直接把我们的状态转移方程翻译成代码即可,可以注意一下斜着遍历数组的技巧: - -```java -/* 返回游戏最后先手和后手的得分之差 */ -int stoneGame(int[] piles) { - int n = piles.length; - // 初始化 dp 数组 - Pair[][] dp = new Pair[n][n]; - for (int i = 0; i < n; i++) - for (int j = i; j < n; j++) - dp[i][j] = new Pair(0, 0); - // 填入 base case - for (int i = 0; i < n; i++) { - dp[i][i].fir = piles[i]; - dp[i][i].sec = 0; - } - // 斜着遍历数组 - for (int l = 2; l <= n; l++) { - for (int i = 0; i <= n - l; i++) { - int j = l + i - 1; - // 先手选择最左边或最右边的分数 - int left = piles[i] + dp[i+1][j].sec; - int right = piles[j] + dp[i][j-1].sec; - // 套用状态转移方程 - if (left > right) { - dp[i][j].fir = left; - dp[i][j].sec = dp[i+1][j].fir; - } else { - dp[i][j].fir = right; - dp[i][j].sec = dp[i][j-1].fir; - } - } - } - Pair res = dp[0][n-1]; - return res.fir - res.sec; -} -``` - -动态规划解法,如果没有状态转移方程指导,绝对是一头雾水,但是根据前面的详细解释,读者应该可以清晰理解这一大段代码的含义。 - -而且,注意到计算 `dp[i][j]` 只依赖其左边和下边的元素,所以说肯定有优化空间,转换成一维 dp,想象一下把二维平面压扁,也就是投影到一维。但是,一维 dp 比较复杂,可解释性很差,大家就不必浪费这个时间去理解了。 - -### 四、最后总结 - -本文给出了解决博弈问题的动态规划解法。博弈问题的前提一般都是在两个聪明人之间进行,编程描述这种游戏的一般方法是二维 dp 数组,数组中通过元组分别表示两人的最优决策。 - -之所以这样设计,是因为先手在做出选择之后,就成了后手,后手在对方做完选择后,就变成了先手。这种角色转换使得我们可以重用之前的结果,典型的动态规划标志。 - -读到这里的朋友应该能理解算法解决博弈问题的套路了。学习算法,一定要注重算法的模板框架,而不是一些看起来牛逼的思路,也不要奢求上来就写一个最优的解法。不要舍不得多用空间,不要过早尝试优化,不要惧怕多维数组。dp 数组就是存储信息避免重复计算的,随便用,直到咱满意为止。 - -希望本文对你有帮助。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/dynamic_programming/\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/dynamic_programming/\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" deleted file mode 100644 index 5d289a182a..0000000000 --- "a/dynamic_programming/\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" +++ /dev/null @@ -1,173 +0,0 @@ -# 动态规划之四键键盘 - -四键键盘问题很有意思,而且可以明显感受到:对 dp 数组的不同定义需要完全不同的逻辑,从而产生完全不同的解法。 - -首先看一下题目: - -![](../pictures/4keyboard/title.png) - -如何在 N 次敲击按钮后得到最多的 A?我们穷举呗,每次有对于每次按键,我们可以穷举四种可能,很明显就是一个动态规划问题。 - -### 第一种思路 - -这种思路会很容易理解,但是效率并不高,我们直接走流程:**对于动态规划问题,首先要明白有哪些「状态」,有哪些「选择」**。 - -具体到这个问题,对于每次敲击按键,有哪些「选择」是很明显的:4 种,就是题目中提到的四个按键,分别是 `A`、`C-A`、`C-C`、`C-V`(`Ctrl` 简写为 `C`)。 - -接下来,思考一下对于这个问题有哪些「状态」?**或者换句话说,我们需要知道什么信息,才能将原问题分解为规模更小的子问题**? - -你看我这样定义三个状态行不行:第一个状态是剩余的按键次数,用 `n` 表示;第二个状态是当前屏幕上字符 A 的数量,用 `a_num` 表示;第三个状态是剪切板中字符 A 的数量,用 `copy` 表示。 - -如此定义「状态」,就可以知道 base case:当剩余次数 `n` 为 0 时,`a_num` 就是我们想要的答案。 - -结合刚才说的 4 种「选择」,我们可以把这几种选择通过状态转移表示出来: - -```python -dp(n - 1, a_num + 1, copy), # A -解释:按下 A 键,屏幕上加一个字符 -同时消耗 1 个操作数 - -dp(n - 1, a_num + copy, copy), # C-V -解释:按下 C-V 粘贴,剪切板中的字符加入屏幕 -同时消耗 1 个操作数 - -dp(n - 2, a_num, a_num) # C-A C-C -解释:全选和复制必然是联合使用的, -剪切板中 A 的数量变为屏幕上 A 的数量 -同时消耗 2 个操作数 -``` - -这样可以看到问题的规模 `n` 在不断减小,肯定可以到达 `n = 0` 的 base case,所以这个思路是正确的: - -```python -def maxA(N: int) -> int: - - # 对于 (n, a_num, copy) 这个状态, - # 屏幕上能最终最多能有 dp(n, a_num, copy) 个 A - def dp(n, a_num, copy): - # base case - if n <= 0: return a_num; - # 几种选择全试一遍,选择最大的结果 - return max( - dp(n - 1, a_num + 1, copy), # A - dp(n - 1, a_num + copy, copy), # C-V - dp(n - 2, a_num, a_num) # C-A C-C - ) - - # 可以按 N 次按键,屏幕和剪切板里都还没有 A - return dp(N, 0, 0) -``` - -这个解法应该很好理解,因为语义明确。下面就继续走流程,用备忘录消除一下重叠子问题: - -```python -def maxA(N: int) -> int: - # 备忘录 - memo = dict() - def dp(n, a_num, copy): - if n <= 0: return a_num; - # 避免计算重叠子问题 - if (n, a_num, copy) in memo: - return memo[(n, a_num, copy)] - - memo[(n, a_num, copy)] = max( - # 几种选择还是一样的 - ) - return memo[(n, a_num, copy)] - - return dp(N, 0, 0) -``` - -这样优化代码之后,子问题虽然没有重复了,但数目仍然很多,在 LeetCode 提交会超时的。 - -我们尝试分析一下这个算法的时间复杂度,就会发现不容易分析。我们可以把这个 dp 函数写成 dp 数组: - -```python -dp[n][a_num][copy] -# 状态的总数(时空复杂度)就是这个三维数组的体积 -``` - -我们知道变量 `n` 最多为 `N`,但是 `a_num` 和 `copy` 最多为多少我们很难计算,复杂度起码也有 O(N^3) 把。所以这个算法并不好,复杂度太高,且已经无法优化了。 - -这也就说明,我们这样定义「状态」是不太优秀的,下面我们换一种定义 dp 的思路。 - -### 第二种思路 - -这种思路稍微有点复杂,但是效率高。继续走流程,「选择」还是那 4 个,但是这次我们只定义一个「状态」,也就是剩余的敲击次数 `n`。 - -这个算法基于这样一个事实,**最优按键序列一定只有两种情况**: - -要么一直按 `A`:A,A,...A(当 N 比较小时)。 - -要么是这么一个形式:A,A,...C-A,C-C,C-V,C-V,...C-V(当 N 比较大时)。 - -因为字符数量少(N 比较小)时,`C-A C-C C-V` 这一套操作的代价相对比较高,可能不如一个个按 `A`;而当 N 比较大时,后期 `C-V` 的收获肯定很大。这种情况下整个操作序列大致是:**开头连按几个 `A`,然后 `C-A C-C` 组合再接若干 `C-V`,然后再 `C-A C-C` 接着若干 `C-V`,循环下去**。 - -换句话说,最后一次按键要么是 `A` 要么是 `C-V`。明确了这一点,可以通过这两种情况来设计算法: - -```java -int[] dp = new int[N + 1]; -// 定义:dp[i] 表示 i 次操作后最多能显示多少个 A -for (int i = 0; i <= N; i++) - dp[i] = max( - 这次按 A 键, - 这次按 C-V - ) -``` - -对于「按 `A` 键」这种情况,就是状态 `i - 1` 的屏幕上新增了一个 A 而已,很容易得到结果: - -```java -// 按 A 键,就比上次多一个 A 而已 -dp[i] = dp[i - 1] + 1; -``` -但是,如果要按 `C-V`,还要考虑之前是在哪里 `C-A C-C` 的。 - -**刚才说了,最优的操作序列一定是 `C-A C-C` 接着若干 `C-V`,所以我们用一个变量 `j` 作为若干 `C-V` 的起点**。那么 `j` 之前的 2 个操作就应该是 `C-A C-C` 了: - -```java -public int maxA(int N) { - int[] dp = new int[N + 1]; - dp[0] = 0; - for (int i = 1; i <= N; i++) { - // 按 A 键 - dp[i] = dp[i - 1] + 1; - for (int j = 2; j < i; j++) { - // 全选 & 复制 dp[j-2],连续粘贴 i - j 次 - // 屏幕上共 dp[j - 2] * (i - j + 1) 个 A - dp[i] = Math.max(dp[i], dp[j - 2] * (i - j + 1)); - } - } - // N 次按键之后最多有几个 A? - return dp[N]; -} -``` - -其中 `j` 变量减 2 是给 `C-A C-C` 留下操作数,看个图就明白了: - -![](../pictures/4keyboard/1.jpg) - -这样,此算法就完成了,时间复杂度 O(N^2),空间复杂度 O(N),这种解法应该是比较高效的了。 - -### 最后总结 - -动态规划难就难在寻找状态转移,不同的定义可以产生不同的状态转移逻辑,虽然最后都能得到正确的结果,但是效率可能有巨大的差异。 - -回顾第一种解法,重叠子问题已经消除了,但是效率还是低,到底低在哪里呢?抽象出递归框架: - -```python -def dp(n, a_num, copy): - dp(n - 1, a_num + 1, copy), # A - dp(n - 1, a_num + copy, copy), # C-V - dp(n - 2, a_num, a_num) # C-A C-C -``` - -看这个穷举逻辑,是有可能出现这样的操作序列 `C-A C-C,C-A C-C...` 或者 `C-V,C-V,...`。然这种操作序列的结果不是最优的,但是我们并没有想办法规避这些情况的发生,从而增加了很多没必要的子问题计算。 - -回顾第二种解法,我们稍加思考就能想到,最优的序列应该是这种形式:`A,A..C-A,C-C,C-V,C-V..C-A,C-C,C-V..`。 - -根据这个事实,我们重新定义了状态,重新寻找了状态转移,从逻辑上减少了无效的子问题个数,从而提高了算法的效率。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/dynamic_programming/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" "b/dynamic_programming/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" deleted file mode 100644 index 5af4dadafd..0000000000 --- "a/dynamic_programming/\345\255\220\345\272\217\345\210\227\351\227\256\351\242\230\346\250\241\346\235\277.md" +++ /dev/null @@ -1,146 +0,0 @@ -# 动态规划之子序列问题解题模板 - -子序列问题是常见的算法问题,而且并不好解决。 - -首先,子序列问题本身就相对子串、子数组更困难一些,因为前者是不连续的序列,而后两者是连续的,就算穷举你都不一定会,更别说求解相关的算法问题了。 - -而且,子序列问题很可能涉及到两个字符串,比如前文「最长公共子序列」,如果没有一定的处理经验,真的不容易想出来。所以本文就来扒一扒子序列问题的套路,其实就有两种模板,相关问题只要往这两种思路上想,十拿九稳。 - -一般来说,这类问题都是让你求一个**最长子序列**,因为最短子序列就是一个字符嘛,没啥可问的。一旦涉及到子序列和最值,那几乎可以肯定,**考察的是动态规划技巧,时间复杂度一般都是 O(n^2)**。 - -原因很简单,你想想一个字符串,它的子序列有多少种可能?起码是指数级的吧,这种情况下,不用动态规划技巧,还想怎么着? - -既然要用动态规划,那就要定义 dp 数组,找状态转移关系。我们说的两种思路模板,就是 dp 数组的定义思路。不同的问题可能需要不同的 dp 数组定义来解决。 - -### 一、两种思路 - -**1、第一种思路模板是一个一维的 dp 数组**: - -```java -int n = array.length; -int[] dp = new int[n]; - -for (int i = 1; i < n; i++) { - for (int j = 0; j < i; j++) { - dp[i] = 最值(dp[i], dp[j] + ...) - } -} -``` - -举个我们写过的例子「最长递增子序列」,在这个思路中 dp 数组的定义是: - -**在子数组 `array[0..i]` 中,我们要求的子序列(最长递增子序列)的长度是 `dp[i]`**。 - -为啥最长递增子序列需要这种思路呢?前文说得很清楚了,因为这样符合归纳法,可以找到状态转移的关系,这里就不具体展开了。 - -**2、第二种思路模板是一个二维的 dp 数组**: - -```java -int n = arr.length; -int[][] dp = new dp[n][n]; - -for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - if (arr[i] == arr[j]) - dp[i][j] = dp[i][j] + ... - else - dp[i][j] = 最值(...) - } -} -``` - -这种思路运用相对更多一些,尤其是涉及两个字符串/数组的子序列,比如前文讲的「最长公共子序列」。本思路中 dp 数组含义又分为「只涉及一个字符串」和「涉及两个字符串」两种情况。 - -**2.1 涉及两个字符串/数组时**(比如最长公共子序列),dp 数组的含义如下: - -**在子数组 `arr1[0..i]` 和子数组 `arr2[0..j]` 中,我们要求的子序列(最长公共子序列)长度为 `dp[i][j]`**。 - -**2.2 只涉及一个字符串/数组时**(比如本文要讲的最长回文子序列),dp 数组的含义如下: - -**在子数组 `array[i..j]` 中,我们要求的子序列(最长回文子序列)的长度为 `dp[i][j]`**。 - -第一种情况可以参考这两篇旧文:「编辑距离」「公共子序列」 - -下面就借最长回文子序列这个问题,详解一下第二种情况下如何使用动态规划。 - -### 二、最长回文子序列 - -之前解决了「最长回文子串」的问题,这次提升难度,求最长回文子序列的长度: - -![](../pictures/最长回文子序列/1.jpg) - -我们说这个问题对 dp 数组的定义是:**在子串 `s[i..j]` 中,最长回文子序列的长度为 `dp[i][j]`**。一定要记住这个定义才能理解算法。 - -为啥这个问题要这样定义二维的 dp 数组呢?我们前文多次提到,**找状态转移需要归纳思维,说白了就是如何从已知的结果推出未知的部分**,这样定义容易归纳,容易发现状态转移关系。 - -具体来说,如果我们想求 `dp[i][j]`,假设你知道了子问题 `dp[i+1][j-1]` 的结果(`s[i+1..j-1]` 中最长回文子序列的长度),你是否能想办法算出 `dp[i][j]` 的值(`s[i..j]` 中,最长回文子序列的长度)呢? - -![](../pictures/最长回文子序列/1.jpg) - -可以!这取决于 `s[i]` 和 `s[j]` 的字符: - -**如果它俩相等**,那么它俩加上 `s[i+1..j-1]` 中的最长回文子序列就是 `s[i..j]` 的最长回文子序列: - -![](../pictures/最长回文子序列/2.jpg) - -**如果它俩不相等**,说明它俩**不可能同时**出现在 `s[i..j]` 的最长回文子序列中,那么把它俩**分别**加入 `s[i+1..j-1]` 中,看看哪个子串产生的回文子序列更长即可: - -![](../pictures/最长回文子序列/3.jpg) - -以上两种情况写成代码就是这样: - -```java -if (s[i] == s[j]) - // 它俩一定在最长回文子序列中 - dp[i][j] = dp[i + 1][j - 1] + 2; -else - // s[i+1..j] 和 s[i..j-1] 谁的回文子序列更长? - dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); -``` - -至此,状态转移方程就写出来了,根据 dp 数组的定义,我们要求的就是 `dp[0][n - 1]`,也就是整个 `s` 的最长回文子序列的长度。 - -### 三、代码实现 - -首先明确一下 base case,如果只有一个字符,显然最长回文子序列长度是 1,也就是 `dp[i][j] = 1 (i == j)`。 - -因为 `i` 肯定小于等于 `j`,所以对于那些 `i > j` 的位置,根本不存在什么子序列,应该初始化为 0。 - -另外,看看刚才写的状态转移方程,想求 `dp[i][j]` 需要知道 `dp[i+1][j-1]`,`dp[i+1][j]`,`dp[i][j-1]` 这三个位置;再看看我们确定的 base case,填入 dp 数组之后是这样: - -![](../pictures/最长回文子序列/4.jpg) - -**为了保证每次计算 `dp[i][j]`,左下右方向的位置已经被计算出来,只能斜着遍历或者反着遍历**: - -![](../pictures/最长回文子序列/5.jpg) - -我选择反着遍历,代码如下: - -```cpp -int longestPalindromeSubseq(string s) { - int n = s.size(); - // dp 数组全部初始化为 0 - vector> dp(n, vector(n, 0)); - // base case - for (int i = 0; i < n; i++) - dp[i][i] = 1; - // 反着遍历保证正确的状态转移 - for (int i = n - 1; i >= 0; i--) { - for (int j = i + 1; j < n; j++) { - // 状态转移方程 - if (s[i] == s[j]) - dp[i][j] = dp[i + 1][j - 1] + 2; - else - dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); - } - } - // 整个 s 的最长回文子串长度 - return dp[0][n - 1]; -} -``` - -至此,最长回文子序列的问题就解决了。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/dynamic_programming/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" "b/dynamic_programming/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" deleted file mode 100644 index ae00e4e2c4..0000000000 --- "a/dynamic_programming/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" +++ /dev/null @@ -1,134 +0,0 @@ -# 动态规划答疑篇 - -这篇文章就给你讲明白两个问题: - -1、到底什么才叫「最优子结构」,和动态规划什么关系。 - -2、为什么动态规划遍历 `dp` 数组的方式五花八门,有的正着遍历,有的倒着遍历,有的斜着遍历。 - -### 一、最优子结构详解 - -「最优子结构」是某些问题的一种特定性质,并不是动态规划问题专有的。也就是说,很多问题其实都具有最优子结构,只是其中大部分不具有重叠子问题,所以我们不把它们归为动态规划系列问题而已。 - -我先举个很容易理解的例子:假设你们学校有 10 个班,你已经计算出了每个班的最高考试成绩。那么现在我要求你计算全校最高的成绩,你会不会算?当然会,而且你不用重新遍历全校学生的分数进行比较,而是只要在这 10 个最高成绩中取最大的就是全校的最高成绩。 - -我给你提出的这个问题就**符合最优子结构**:可以从子问题的最优结果推出更大规模问题的最优结果。让你算**每个班**的最优成绩就是子问题,你知道所有子问题的答案后,就可以借此推出**全校**学生的最优成绩这个规模更大的问题的答案。 - -你看,这么简单的问题都有最优子结构性质,只是因为显然没有重叠子问题,所以我们简单地求最值肯定用不出动态规划。 - -再举个例子:假设你们学校有 10 个班,你已知每个班的最大分数差(最高分和最低分的差值)。那么现在我让你计算全校学生中的最大分数差,你会不会算?可以想办法算,但是肯定不能通过已知的这 10 个班的最大分数差推到出来。因为这 10 个班的最大分数差不一定就包含全校学生的最大分数差,比如全校的最大分数差可能是 3 班的最高分和 6 班的最低分之差。 - -这次我给你提出的问题就**不符合最优子结构**,因为你没办通过每个班的最优值推出全校的最优值,没办法通过子问题的最优值推出规模更大的问题的最优值。前文「动态规划详解」说过,想满足最优子结,子问题之间必须互相独立。全校的最大分数差可能出现在两个班之间,显然子问题不独立,所以这个问题本身不符合最优子结构。 - -**那么遇到这种最优子结构失效情况,怎么办?策略是:改造问题**。对于最大分数差这个问题,我们不是没办法利用已知的每个班的分数差吗,那我只能这样写一段暴力代码: - -```java -int result = 0; -for (Student a : school) { - for (Student b : school) { - if (a is b) continue; - result = max(result, |a.score - b.score|); - } -} -return result; -``` - -改造问题,也就是把问题等价转化:最大分数差,不就等价于最高分数和最低分数的差么,那不就是要求最高和最低分数么,不就是我们讨论的第一个问题么,不就具有最优子结构了么?那现在改变思路,借助最优子结构解决最值问题,再回过头解决最大分数差问题,是不是就高效多了? - -当然,上面这个例子太简单了,不过请读者回顾一下,我们做动态规划问题,是不是一直在求各种最值,本质跟我们举的例子没啥区别,无非需要处理一下重叠子问题。 - -前文「不同定义不同解法」和「高楼扔鸡蛋进阶」就展示了如何改造问题,不同的最优子结构,可能导致不同的解法和效率。 - -再举个常见但也十分简单的例子,求一棵二叉树的最大值,不难吧(简单起见,假设节点中的值都是非负数): - -```java -int maxVal(TreeNode root) { - if (root == null) - return -1; - int left = maxVal(root.left); - int right = maxVal(root.right); - return max(root.val, left, right); -} -``` - -你看这个问题也符合最优子结构,以 `root` 为根的树的最大值,可以通过两边子树(子问题)的最大值推导出来,结合刚才学校和班级的例子,很容易理解吧。 - -当然这也不是动态规划问题,旨在说明,最优子结构并不是动态规划独有的一种性质,能求最值的问题大部分都具有这个性质;**但反过来,最优子结构性质作为动态规划问题的必要条件,一定是让你求最值的**,以后碰到那种恶心人的最值题,思路往动态规划想就对了,这就是套路。 - -动态规划不就是从最简单的 base case 往后推导吗,可以想象成一个链式反应,以小博大。但只有符合最优子结构的问题,才有发生这种链式反应的性质。 - -找最优子结构的过程,其实就是证明状态转移方程正确性的过程,方程符合最优子结构就可以写暴力解了,写出暴力解就可以看出有没有重叠子问题了,有则优化,无则 OK。这也是套路,经常刷题的朋友应该能体会。 - -这里就不举那些正宗动态规划的例子了,读者可以翻翻历史文章,看看状态转移是如何遵循最优子结构的,这个话题就聊到这,下面再来看另外个动态规划迷惑行为。 - -### 二、dp 数组的遍历方向 - -我相信读者做动态规问题时,肯定会对 `dp` 数组的遍历顺序有些头疼。我们拿二维 `dp` 数组来举例,有时候我们是正向遍历: - -```java -int[][] dp = new int[m][n]; -for (int i = 0; i < m; i++) - for (int j = 0; j < n; j++) - // 计算 dp[i][j] -``` - -有时候我们反向遍历: - -```java -for (int i = m - 1; i >= 0; i--) - for (int j = n - 1; j >= 0; j--) - // 计算 dp[i][j] -``` - -有时候可能会斜向遍历: - -```java -// 斜着遍历数组 -for (int l = 2; l <= n; l++) { - for (int i = 0; i <= n - l; i++) { - int j = l + i - 1; - // 计算 dp[i][j] - } -} -``` - -甚至更让人迷惑的是,有时候发现正向反向遍历都可以得到正确答案,比如我们在「团灭股票问题」中有的地方就正反皆可。 - -那么,如果仔细观察的话可以发现其中的原因的。你只要把住两点就行了: - -**1、遍历的过程中,所需的状态必须是已经计算出来的**。 - -**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]` 转移而来,如下图: - -![](../pictures/最优子结构/1.jpg) - -那么,参考刚才说的两条原则,你该怎么遍历 `dp` 数组?肯定是正向遍历: - -```java -for (int i = 1; i < m; i++) - for (int j = 1; j < n; j++) - // 通过 dp[i-1][j], dp[i][j - 1], dp[i-1][j-1] - // 计算 dp[i][j] -``` - -因为,这样每一步迭代的左边、上边、左上边的位置都是 base case 或者之前计算过的,而且最终结束在我们想要的答案 `dp[m][n]`。 - -再举一例,回文子序列问题,详见前文「子序列问题模板」,我们通过过对 `dp` 数组的定义,确定了 base case 处在中间的对角线,`dp[i][j]` 需要从 `dp[i+1][j]`, `dp[i][j-1]`, `dp[i+1][j-1]` 转移而来,想要求的最终答案是 `dp[0][n-1]`,如下图: - -![](../pictures/最长回文子序列/4.jpg) - -这种情况根据刚才的两个原则,就可以有两种正确的遍历方式: - -![](../pictures/最长回文子序列/5.jpg) - -要么从左至右斜着遍历,要么从下向上从左到右遍历,这样才能保证每次 `dp[i][j]` 的左边、下边、左下边已经计算完毕,得到正确结果。 - -现在,你应该理解了这两个原则,主要就是看 base case 和最终结果的存储位置,保证遍历过程中使用的数据都是计算完毕的就行,有时候确实存在多种方法可以得到正确答案,可根据个人口味自行选择。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/dynamic_programming/\347\274\226\350\276\221\350\267\235\347\246\273.md" "b/dynamic_programming/\347\274\226\350\276\221\350\267\235\347\246\273.md" deleted file mode 100644 index 77652b9fad..0000000000 --- "a/dynamic_programming/\347\274\226\350\276\221\350\267\235\347\246\273.md" +++ /dev/null @@ -1,263 +0,0 @@ -# 编辑距离 - -前几天看了一份鹅场的面试题,算法部分大半是动态规划,最后一题就是写一个计算编辑距离的函数,今天就专门写一篇文章来探讨一下这个问题。 - -我个人很喜欢编辑距离这个问题,因为它看起来十分困难,解法却出奇得简单漂亮,而且它是少有的比较实用的算法(是的,我承认很多算法问题都不太实用)。下面先来看下题目: - -![](../pictures/editDistance/title.png) - -为什么说这个问题难呢,因为显而易见,它就是难,让人手足无措,望而生畏。 - -为什么说它实用呢,因为前几天我就在日常生活中用到了这个算法。之前有一篇公众号文章由于疏忽,写错位了一段内容,我决定修改这部分内容让逻辑通顺。但是公众号文章最多只能修改 20 个字,且只支持增、删、替换操作(跟编辑距离问题一模一样),于是我就用算法求出了一个最优方案,只用了 16 步就完成了修改。 - -再比如高大上一点的应用,DNA 序列是由 A,G,C,T 组成的序列,可以类比成字符串。编辑距离可以衡量两个 DNA 序列的相似度,编辑距离越小,说明这两段 DNA 越相似,说不定这俩 DNA 的主人是远古近亲啥的。 - -下面言归正传,详细讲解一下编辑距离该怎么算,相信本文会让你有收获。 - -### 一、思路 - -编辑距离问题就是给我们两个字符串 `s1` 和 `s2`,只能用三种操作,让我们把 `s1` 变成 `s2`,求最少的操作数。需要明确的是,不管是把 `s1` 变成 `s2` 还是反过来,结果都是一样的,所以后文就以 `s1` 变成 `s2` 举例。 - -前文「最长公共子序列」说过,**解决两个字符串的动态规划问题,一般都是用两个指针 `i,j` 分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模**。 - -设两个字符串分别为 "rad" 和 "apple",为了把 `s1` 变成 `s2`,算法会这样进行: - -![](../pictures/editDistance/edit.gif) -![](../pictures/editDistance/1.jpg) - -请记住这个 GIF 过程,这样就能算出编辑距离。关键在于如何做出正确的操作,稍后会讲。 - -根据上面的 GIF,可以发现操作不只有三个,其实还有第四个操作,就是什么都不要做(skip)。比如这个情况: - -![](../pictures/editDistance/2.jpg) - -因为这两个字符本来就相同,为了使编辑距离最小,显然不应该对它们有任何操作,直接往前移动 `i,j` 即可。 - -还有一个很容易处理的情况,就是 `j` 走完 `s2` 时,如果 `i` 还没走完 `s1`,那么只能用删除操作把 `s1` 缩短为 `s2`。比如这个情况: - -![](../pictures/editDistance/3.jpg) - -类似的,如果 `i` 走完 `s1` 时 `j` 还没走完了 `s2`,那就只能用插入操作把 `s2` 剩下的字符全部插入 `s1`。等会会看到,这两种情况就是算法的 **base case**。 - -下面详解一下如何将思路转换成代码,坐稳,要发车了。 - -### 二、代码详解 - -先梳理一下之前的思路: - -base case 是 `i` 走完 `s1` 或 `j` 走完 `s2`,可以直接返回另一个字符串剩下的长度。 - -对于每对儿字符 `s1[i]` 和 `s2[j]`,可以有四种操作: - -```python -if s1[i] == s2[j]: - 啥都别做(skip) - i, j 同时向前移动 -else: - 三选一: - 插入(insert) - 删除(delete) - 替换(replace) -``` - -有这个框架,问题就已经解决了。读者也许会问,这个「三选一」到底该怎么选择呢?很简单,全试一遍,哪个操作最后得到的编辑距离最小,就选谁。这里需要递归技巧,理解需要点技巧,先看下代码: - -```python -def minDistance(s1, s2) -> int: - - def dp(i, j): - # base case - if i == -1: return j + 1 - if j == -1: return i + 1 - - if s1[i] == s2[j]: - return dp(i - 1, j - 1) # 啥都不做 - else: - return min( - dp(i, j - 1) + 1, # 插入 - dp(i - 1, j) + 1, # 删除 - dp(i - 1, j - 1) + 1 # 替换 - ) - - # i,j 初始化指向最后一个索引 - return dp(len(s1) - 1, len(s2) - 1) -``` - -下面来详细解释一下这段递归代码,base case 应该不用解释了,主要解释一下递归部分。 - -都说递归代码的可解释性很好,这是有道理的,只要理解函数的定义,就能很清楚地理解算法的逻辑。我们这里 dp(i, j) 函数的定义是这样的: - -```python -def dp(i, j) -> int -# 返回 s1[0..i] 和 s2[0..j] 的最小编辑距离 -``` - -**记住这个定义**之后,先来看这段代码: - -```python -if s1[i] == s2[j]: - return dp(i - 1, j - 1) # 啥都不做 -# 解释: -# 本来就相等,不需要任何操作 -# s1[0..i] 和 s2[0..j] 的最小编辑距离等于 -# s1[0..i-1] 和 s2[0..j-1] 的最小编辑距离 -# 也就是说 dp(i, j) 等于 dp(i-1, j-1) -``` - -如果 `s1[i]!=s2[j]`,就要对三个操作递归了,稍微需要点思考: - -```python -dp(i, j - 1) + 1, # 插入 -# 解释: -# 我直接在 s1[i] 插入一个和 s2[j] 一样的字符 -# 那么 s2[j] 就被匹配了,前移 j,继续跟 i 对比 -# 别忘了操作数加一 -``` - -![](../pictures/editDistance/insert.gif) - -```python -dp(i - 1, j) + 1, # 删除 -# 解释: -# 我直接把 s[i] 这个字符删掉 -# 前移 i,继续跟 j 对比 -# 操作数加一 -``` - -![](../pictures/editDistance/delete.gif) - -```python -dp(i - 1, j - 1) + 1 # 替换 -# 解释: -# 我直接把 s1[i] 替换成 s2[j],这样它俩就匹配了 -# 同时前移 i,j 继续对比 -# 操作数加一 -``` - -![](../pictures/editDistance/replace.gif) - -现在,你应该完全理解这段短小精悍的代码了。还有点小问题就是,这个解法是暴力解法,存在重叠子问题,需要用动态规划技巧来优化。 - -**怎么能一眼看出存在重叠子问题呢**?前文「动态规划之正则表达式」有提过,这里再简单提一下,需要抽象出本文算法的递归框架: - -```python -def dp(i, j): - dp(i - 1, j - 1) #1 - dp(i, j - 1) #2 - dp(i - 1, j) #3 -``` - -对于子问题 `dp(i-1, j-1)`,如何通过原问题 `dp(i, j)` 得到呢?有不止一条路径,比如 `dp(i, j) -> #1` 和 `dp(i, j) -> #2 -> #3`。一旦发现一条重复路径,就说明存在巨量重复路径,也就是重叠子问题。 - -### 三、动态规划优化 - -对于重叠子问题呢,前文「动态规划详解」详细介绍过,优化方法无非是备忘录或者 DP table。 - -备忘录很好加,原来的代码稍加修改即可: - -```python -def minDistance(s1, s2) -> int: - - memo = dict() # 备忘录 - def dp(i, j): - if (i, j) in memo: - return memo[(i, j)] - ... - - if s1[i] == s2[j]: - memo[(i, j)] = ... - else: - memo[(i, j)] = ... - return memo[(i, j)] - - return dp(len(s1) - 1, len(s2) - 1) -``` - -**主要说下 DP table 的解法**: - -首先明确 dp 数组的含义,dp 数组是一个二维数组,长这样: - -![](../pictures/editDistance/dp.jpg) - -有了之前递归解法的铺垫,应该很容易理解。`dp[..][0]` 和 `dp[0][..]` 对应 base case,`dp[i][j]` 的含义和之前的 dp 函数类似: - -```python -def dp(i, j) -> int -# 返回 s1[0..i] 和 s2[0..j] 的最小编辑距离 - -dp[i-1][j-1] -# 存储 s1[0..i] 和 s2[0..j] 的最小编辑距离 -``` - -dp 函数的 base case 是 `i,j` 等于 -1,而数组索引至少是 0,所以 dp 数组会偏移一位。 - -既然 dp 数组和递归 dp 函数含义一样,也就可以直接套用之前的思路写代码,**唯一不同的是,DP table 是自底向上求解,递归解法是自顶向下求解**: - -```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)); -} -``` - -### 三、扩展延伸 - -一般来说,处理两个字符串的动态规划问题,都是按本文的思路处理,建立 DP table。为什么呢,因为易于找出状态转移的关系,比如编辑距离的 DP table: - -![](../pictures/editDistance/4.jpg) - -还有一个细节,既然每个 `dp[i][j]` 只和它附近的三个状态有关,空间复杂度是可以压缩成 $O(min(M, N))$ 的(M,N 是两个字符串的长度)。不难,但是可解释性大大降低,读者可以自己尝试优化一下。 - -你可能还会问,**这里只求出了最小的编辑距离,那具体的操作是什么**?你之前举的修改公众号文章的例子,只有一个最小编辑距离肯定不够,还得知道具体怎么修改才行。 - -这个其实很简单,代码稍加修改,给 dp 数组增加额外的信息即可: - -```java -// int[][] dp; -Node[][] dp; - -class Node { - int val; - int choice; - // 0 代表啥都不做 - // 1 代表插入 - // 2 代表删除 - // 3 代表替换 -} -``` - -`val` 属性就是之前的 dp 数组的数值,`choice` 属性代表操作。在做最优选择时,顺便把操作记录下来,然后就从结果反推具体操作。 - -我们的最终结果不是 `dp[m][n]` 吗,这里的 `val` 存着最小编辑距离,`choice` 存着最后一个操作,比如说是插入操作,那么就可以左移一格: - -![](../pictures/editDistance/5.jpg) - -重复此过程,可以一步步回到起点 `dp[0][0]`,形成一条路径,按这条路径上的操作进行编辑,就是最佳方案。 - -![](../pictures/editDistance/6.jpg) - -以上就是编辑距离算法的全部内容,如果本文对你有帮助,**欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚**~ - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/dynamic_programming/\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/dynamic_programming/\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" deleted file mode 100644 index 103aa533ef..0000000000 --- "a/dynamic_programming/\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" +++ /dev/null @@ -1,123 +0,0 @@ -# 贪心算法之区间调度问题 - -什么是贪心算法呢?贪心算法可以认为是动态规划算法的一个特例,相比动态规划,使用贪心算法需要满足更多的条件(贪心选择性质),但是效率比动态规划要高。 - -比如说一个算法问题使用暴力解法需要指数级时间,如果能使用动态规划消除重叠子问题,就可以降到多项式级别的时间,如果满足贪心选择性质,那么可以进一步降低时间复杂度,达到线性级别的。 - -什么是贪心选择性质呢,简单说就是:每一步都做出一个局部最优的选择,最终的结果就是全局最优。注意哦,这是一种特殊性质,其实只有一部分问题拥有这个性质。 - -比如你面前放着 100 张人民币,你只能拿十张,怎么才能拿最多的面额?显然每次选择剩下钞票中面值最大的一张,最后你的选择一定是最优的。 - -然而,大部分问题明显不具有贪心选择性质。比如打斗地主,对手出对儿三,按照贪心策略,你应该出尽可能小的牌刚好压制住对方,但现实情况我们甚至可能会出王炸。这种情况就不能用贪心算法,而得使用动态规划解决,参见前文「动态规划解决博弈问题」。 - -### 一、问题概述 - -言归正传,本文解决一个很经典的贪心算法问题 Interval Scheduling(区间调度问题)。给你很多形如 `[start, end]` 的闭区间,请你设计一个算法,**算出这些区间中最多有几个互不相交的区间**。 - -```java -int intervalSchedule(int[][] intvs) {} -``` - -举个例子,`intvs = [[1,3], [2,4], [3,6]]`,这些区间最多有 2 个区间互不相交,即 `[[1,3], [3,6]]`,你的算法应该返回 2。注意边界相同并不算相交。 - -这个问题在生活中的应用广泛,比如你今天有好几个活动,每个活动都可以用区间 `[start, end]` 表示开始和结束的时间,请问你今天**最多能参加几个活动呢?**显然你一个人不能同时参加两个活动,所以说这个问题就是求这些时间区间的最大不相交子集。 - -### 二、贪心解法 - -这个问题有许多看起来不错的贪心思路,却都不能得到正确答案。比如说: - -也许我们可以每次选择可选区间中开始最早的那个?但是可能存在某些区间开始很早,但是很长,使得我们错误地错过了一些短的区间。或者我们每次选择可选区间中最短的那个?或者选择出现冲突最少的那个区间?这些方案都能很容易举出反例,不是正确的方案。 - -正确的思路其实很简单,可以分为以下三步: - -1. 从区间集合 intvs 中选择一个区间 x,这个 x 是在当前所有区间中**结束最早的**(end 最小)。 -2. 把所有与 x 区间相交的区间从区间集合 intvs 中删除。 -3. 重复步骤 1 和 2,直到 intvs 为空为止。之前选出的那些 x 就是最大不相交子集。 - -把这个思路实现成算法的话,可以按每个区间的 `end` 数值升序排序,因为这样处理之后实现步骤 1 和步骤 2 都方便很多: - -![1](../pictures/interval/1.gif) - -现在来实现算法,对于步骤 1,由于我们预先按照 `end` 排了序,所以选择 x 是很容易的。关键在于,如何去除与 x 相交的区间,选择下一轮循环的 x 呢? - -**由于我们事先排了序**,不难发现所有与 x 相交的区间必然会与 x 的 `end` 相交;如果一个区间不想与 x 的 `end` 相交,它的 `start` 必须要大于(或等于)x 的 `end`: - -![2](../pictures/interval/2.jpg) - -看下代码: - -```java -public int intervalSchedule(int[][] intvs) { - if (intvs.length == 0) return 0; - // 按 end 升序排序 - Arrays.sort(intvs, new Comparator() { - public int compare(int[] a, int[] b) { - return a[1] - b[1]; - } - }); - // 至少有一个区间不相交 - int count = 1; - // 排序后,第一个区间就是 x - int x_end = intvs[0][1]; - for (int[] interval : intvs) { - int start = interval[0]; - if (start >= x_end) { - // 找到下一个选择的区间了 - count++; - x_end = interval[1]; - } - } - return count; -} -``` - -### 三、应用举例 - -下面举例几道 LeetCode 题目应用一下区间调度算法。 - -第 435 题,无重叠区间: - -![title1](../pictures/interval/title1.png) - -我们已经会求最多有几个区间不会重叠了,那么剩下的不就是至少需要去除的区间吗? - -```java -int eraseOverlapIntervals(int[][] intervals) { - int n = intervals.length; - return n - intervalSchedule(intervals); -} -``` - -第 452 题,用最少的箭头射爆气球: - -![title2](../pictures/interval/title2.png) - -其实稍微思考一下,这个问题和区间调度算法一模一样!如果最多有 `n` 个不重叠的区间,那么就至少需要 `n` 个箭头穿透所有区间: - -![3](../pictures/interval/3.jpg) - -只是有一点不一样,在 `intervalSchedule` 算法中,如果两个区间的边界触碰,不算重叠;而按照这道题目的描述,箭头如果碰到气球的边界气球也会爆炸,所以说相当于区间的边界触碰也算重叠: - -![4](../pictures/interval/4.jpg) - -所以只要将之前的算法稍作修改,就是这道题目的答案: - -```java -int findMinArrowShots(int[][] intvs) { - // ... - - for (int[] interval : intvs) { - int start = interval[0]; - // 把 >= 改成 > 就行了 - if (start > x_end) { - count++; - x_end = interval[1]; - } - } - return count; -} -``` - -这么做的原因也不难理解,因为现在边界接触也算重叠,所以 `start == x_end` 时不能更新 x。 - -如果本文对你有帮助,欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚~ \ No newline at end of file diff --git "a/dynamic_programming/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\350\277\233\351\230\266.md" "b/dynamic_programming/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\350\277\233\351\230\266.md" deleted file mode 100644 index a75e08b4de..0000000000 --- "a/dynamic_programming/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\350\277\233\351\230\266.md" +++ /dev/null @@ -1,268 +0,0 @@ -# 经典动态规划问题:高楼扔鸡蛋(进阶) - -上篇文章聊了高楼扔鸡蛋问题,讲了一种效率不是很高,但是较为容易理解的动态规划解法。后台很多读者问如何更高效地解决这个问题,今天就谈两种思路,来优化一下这个问题,分别是二分查找优化和重新定义状态转移。 - -如果还不知道高楼扔鸡蛋问题的读者可以看下「经典动态规划:高楼扔鸡蛋」,那篇文章详解了题目的含义和基本的动态规划解题思路,请确保理解前文,因为今天的优化都是基于这个基本解法的。 - -二分搜索的优化思路也许是我们可以尽力尝试写出的,而修改状态转移的解法可能是不容易想到的,可以借此见识一下动态规划算法设计的玄妙,当做思维拓展。 - -### 二分搜索优化 - -之前提到过这个解法,核心是因为状态转移方程的单调性,这里可以具体展开看看。 - -首先简述一下原始动态规划的思路: - -1、暴力穷举尝试在所有楼层 `1 <= i <= N` 扔鸡蛋,每次选择尝试次数**最少**的那一层; - -2、每次扔鸡蛋有两种可能,要么碎,要么没碎; - -3、如果鸡蛋碎了,`F` 应该在第 `i` 层下面,否则,`F` 应该在第 `i` 层上面; - -4、鸡蛋是碎了还是没碎,取决于哪种情况下尝试次数**更多**,因为我们想求的是最坏情况下的结果。 - -核心的状态转移代码是这段: - -```python -# 当前状态为 K 个鸡蛋,面对 N 层楼 -# 返回这个状态下的最优结果 -def dp(K, N): - for 1 <= i <= N: - # 最坏情况下的最少扔鸡蛋次数 - res = min(res, - max( - dp(K - 1, i - 1), # 碎 - dp(K, N - i) # 没碎 - ) + 1 # 在第 i 楼扔了一次 - ) - return res -``` - -这个 for 循环就是下面这个状态转移方程的具体代码实现: - -$$ dp(K, N) = \min_{0 <= i <= N}\{\max\{dp(K - 1, i - 1), dp(K, N - i)\} + 1\}$$ - -如果能够理解这个状态转移方程,那么就很容易理解二分查找的优化思路。 - -首先我们根据 `dp(K, N)` 数组的定义(有 `K` 个鸡蛋面对 `N` 层楼,最少需要扔几次),**很容易知道 `K` 固定时,这个函数随着 `N` 的增加一定是单调递增的**,无论你策略多聪明,楼层增加测试次数一定要增加。 - -那么注意 `dp(K - 1, i - 1)` 和 `dp(K, N - i)` 这两个函数,其中 `i` 是从 1 到 `N` 单增的,如果我们固定 `K` 和 `N`,**把这两个函数看做关于 `i` 的函数,前者随着 `i` 的增加应该也是单调递增的,而后者随着 `i` 的增加应该是单调递减的**: - -![](../pictures/扔鸡蛋/2.jpg) - -这时候求二者的较大值,再求这些最大值之中的最小值,其实就是求这两条直线交点,也就是红色折线的最低点嘛。 - -我们前文「二分查找只能用来查找元素吗」讲过,二分查找的运用很广泛,形如下面这种形式的 for 循环代码: - -```java -for (int i = 0; i < n; i++) { - if (isOK(i)) - return i; -} -``` - -都很有可能可以运用二分查找来优化线性搜索的复杂度,回顾这两个 `dp` 函数的曲线,我们要找的最低点其实就是这种情况: - -```java -for (int i = 1; i <= N; i++) { - if (dp(K - 1, i - 1) == dp(K, N - i)) - return dp(K, N - i); -} -``` - -熟悉二分搜索的同学肯定敏感地想到了,这不就是相当于求 Valley(山谷)值嘛,可以用二分查找来快速寻找这个点的,直接看代码吧,整体的思路还是一样,只是加快了搜索速度: - -```python -def superEggDrop(self, K: int, N: int) -> int: - - memo = dict() - def dp(K, N): - if K == 1: return N - if N == 0: return 0 - if (K, N) in memo: - return memo[(K, N)] - - # for 1 <= i <= N: - # res = min(res, - # max( - # dp(K - 1, i - 1), - # dp(K, N - i) - # ) + 1 - # ) - - res = float('INF') - # 用二分搜索代替线性搜索 - lo, hi = 1, N - while lo <= hi: - mid = (lo + hi) // 2 - broken = dp(K - 1, mid - 1) # 碎 - not_broken = dp(K, N - mid) # 没碎 - # res = min(max(碎,没碎) + 1) - if broken > not_broken: - hi = mid - 1 - res = min(res, broken + 1) - else: - lo = mid + 1 - res = min(res, not_broken + 1) - - memo[(K, N)] = res - return res - - return dp(K, N) -``` - -这个算法的时间复杂度是多少呢?**动态规划算法的时间复杂度就是子问题个数 × 函数本身的复杂度**。 - -函数本身的复杂度就是忽略递归部分的复杂度,这里 `dp` 函数中用了一个二分搜索,所以函数本身的复杂度是 O(logN)。 - -子问题个数也就是不同状态组合的总数,显然是两个状态的乘积,也就是 O(KN)。 - -所以算法的总时间复杂度是 O(K\*N\*logN), 空间复杂度 O(KN)。效率上比之前的算法 O(KN^2) 要高效一些。 - -### 重新定义状态转移 - -前文「不同定义有不同解法」就提过,找动态规划的状态转移本就是见仁见智,比较玄学的事情,不同的状态定义可以衍生出不同的解法,其解法和复杂程度都可能有巨大差异。这里就是一个很好的例子。 - -再回顾一下我们之前定义的 `dp` 数组含义: - -```python -def dp(k, n) -> int -# 当前状态为 k 个鸡蛋,面对 n 层楼 -# 返回这个状态下最少的扔鸡蛋次数 -``` - -用 dp 数组表示的话也是一样的: - -```python -dp[k][n] = m -# 当前状态为 k 个鸡蛋,面对 n 层楼 -# 这个状态下最少的扔鸡蛋次数为 m -``` - -按照这个定义,就是**确定当前的鸡蛋个数和面对的楼层数,就知道最小扔鸡蛋次数**。最终我们想要的答案就是 `dp(K, N)` 的结果。 - -这种思路下,肯定要穷举所有可能的扔法的,用二分搜索优化也只是做了「剪枝」,减小了搜索空间,但本质思路没有变,还是穷举。 - -现在,我们稍微修改 `dp` 数组的定义,**确定当前的鸡蛋个数和最多允许的扔鸡蛋次数,就知道能够确定 `F` 的最高楼层数**。具体来说是这个意思: - -```python -dp[k][m] = n -# 当前有 k 个鸡蛋,可以尝试扔 m 次鸡蛋 -# 这个状态下,最坏情况下最多能确切测试一栋 n 层的楼 - -# 比如说 dp[1][7] = 7 表示: -# 现在有 1 个鸡蛋,允许你扔 7 次; -# 这个状态下最多给你 7 层楼, -# 使得你可以确定楼层 F 使得鸡蛋恰好摔不碎 -# (一层一层线性探查嘛) -``` - -这其实就是我们原始思路的一个「反向」版本,我们先不管这种思路的状态转移怎么写,先来思考一下这种定义之下,最终想求的答案是什么? - -我们最终要求的其实是扔鸡蛋次数 `m`,但是这时候 `m` 在状态之中而不是 `dp` 数组的结果,可以这样处理: - -```java -int superEggDrop(int K, int N) { - - int m = 0; - while (dp[K][m] < N) { - m++; - // 状态转移... - } - return m; -} -``` - -题目不是**给你 `K` 鸡蛋,`N` 层楼,让你求最坏情况下最少的测试次数 `m`** 吗?`while` 循环结束的条件是 `dp[K][m] == N`,也就是**给你 `K` 个鸡蛋,测试 `m` 次,最坏情况下最多能测试 `N` 层楼**。 - -注意看这两段描述,是完全一样的!所以说这样组织代码是正确的,关键就是状态转移方程怎么找呢?还得从我们原始的思路开始讲。之前的解法配了这样图帮助大家理解状态转移思路: - -![](../pictures/扔鸡蛋/1.jpg) - -这个图描述的仅仅是某一个楼层 `i`,原始解法还得线性或者二分扫描所有楼层,要求最大值、最小值。但是现在这种 `dp` 定义根本不需要这些了,基于下面两个事实: - -**1、无论你在哪层楼扔鸡蛋,鸡蛋只可能摔碎或者没摔碎,碎了的话就测楼下,没碎的话就测楼上**。 - -**2、无论你上楼还是下楼,总的楼层数 = 楼上的楼层数 + 楼下的楼层数 + 1(当前这层楼)**。 - -根据这个特点,可以写出下面的状态转移方程: - -`dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1` - -**`dp[k][m - 1]` 就是楼上的楼层数**,因为鸡蛋个数 `k` 不变,也就是鸡蛋没碎,扔鸡蛋次数 `m` 减一; - -**`dp[k - 1][m - 1]` 就是楼下的楼层数**,因为鸡蛋个数 `k` 减一,也就是鸡蛋碎了,同时扔鸡蛋次数 `m` 减一。 - -PS:这个 `m` 为什么要减一而不是加一?之前定义得很清楚,这个 `m` 是一个允许的次数上界,而不是扔了几次。 - -![](../pictures/扔鸡蛋/3.jpg) - -至此,整个思路就完成了,只要把状态转移方程填进框架即可: - -```java -int superEggDrop(int K, int N) { - // m 最多不会超过 N 次(线性扫描) - int[][] dp = new int[K + 1][N + 1]; - // base case: - // dp[0][..] = 0 - // dp[..][0] = 0 - // Java 默认初始化数组都为 0 - int m = 0; - while (dp[K][m] < N) { - m++; - for (int k = 1; k <= K; k++) - dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; - } - return m; -} -``` - -如果你还觉得这段代码有点难以理解,其实它就等同于这样写: - -```java -for (int m = 1; dp[K][m] < N; m++) - for (int k = 1; k <= K; k++) - dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; -``` - -看到这种代码形式就熟悉多了吧,因为我们要求的不是 `dp` 数组里的值,而是某个符合条件的索引 `m`,所以用 `while` 循环来找到这个 `m` 而已。 - -这个算法的时间复杂度是多少?很明显就是两个嵌套循环的复杂度 O(KN)。 - -另外注意到 `dp[m][k]` 转移只和左边和左上的两个状态有关,所以很容易优化成一维 `dp` 数组,这里就不写了。 - -### 还可以再优化 - -再往下就要用一些数学方法了,不具体展开,就简单提一下思路吧。 - -在刚才的思路之上,**注意函数 `dp(m, k)` 是随着 `m` 单增的,因为鸡蛋个数 `k` 不变时,允许的测试次数越多,可测试的楼层就越高**。 - -这里又可以借助二分搜索算法快速逼近 `dp[K][m] == N` 这个终止条件,时间复杂度进一步下降为 O(KlogN),我们可以设 `g(k, m) =`…… - -算了算了,打住吧。我觉得我们能够写出 O(K\*N\*logN) 的二分优化算法就行了,后面的这些解法呢,听个响鼓个掌就行了,把欲望限制在能力的范围之内才能拥有快乐! - -不过可以肯定的是,根据二分搜索代替线性扫描 `m` 的取值,代码的大致框架肯定是修改穷举 `m` 的 for 循环: - -```java -// 把线性搜索改成二分搜索 -// for (int m = 1; dp[K][m] < N; m++) -int lo = 1, hi = N; -while (lo < hi) { - int mid = (lo + hi) / 2; - if (... < N) { - lo = ... - } else { - hi = ... - } - - for (int k = 1; k <= K; k++) - // 状态转移方程 -} -``` - -简单总结一下吧,第一个二分优化是利用了 `dp` 函数的单调性,用二分查找技巧快速搜索答案;第二种优化是巧妙地修改了状态转移方程,简化了求解了流程,但相应的,解题逻辑比较难以想到;后续还可以用一些数学方法和二分搜索进一步优化第二种解法,不过看了看镜子中的发量,算了。 - -本文终,希望对你有一点启发。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/dynamic_programming/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" "b/dynamic_programming/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" deleted file mode 100644 index f187c75acb..0000000000 --- "a/dynamic_programming/\351\253\230\346\245\274\346\211\224\351\270\241\350\233\213\351\227\256\351\242\230.md" +++ /dev/null @@ -1,231 +0,0 @@ -# 经典动态规划问题:高楼扔鸡蛋 - -今天要聊一个很经典的算法问题,若干层楼,若干个鸡蛋,让你算出最少的尝试次数,找到鸡蛋恰好摔不碎的那层楼。国内大厂以及谷歌脸书面试都经常考察这道题,只不过他们觉得扔鸡蛋太浪费,改成扔杯子,扔破碗什么的。 - -具体的问题等会再说,但是这道题的解法技巧很多,光动态规划就好几种效率不同的思路,最后还有一种极其高效数学解法。秉承咱们号一贯的作风,拒绝奇技淫巧,拒绝过于诡异的技巧,因为这些技巧无法举一反三,学了也不划算。 - -下面就来用我们一直强调的动态规划通用思路来研究一下这道题。 - -### 一、解析题目 - -题目是这样:你面前有一栋从 1 到 `N` 共 `N` 层的楼,然后给你 `K` 个鸡蛋(`K` 至少为 1)。现在确定这栋楼存在楼层 `0 <= F <= N`,在这层楼将鸡蛋扔下去,鸡蛋**恰好没摔碎**(高于 `F` 的楼层都会碎,低于 `F` 的楼层都不会碎)。现在问你,**最坏**情况下,你**至少**要扔几次鸡蛋,才能**确定**这个楼层 `F` 呢? - -也就是让你找摔不碎鸡蛋的最高楼层 `F`,但什么叫「最坏情况」下「至少」要扔几次呢?我们分别举个例子就明白了。 - -比方说**现在先不管鸡蛋个数的限制**,有 7 层楼,你怎么去找鸡蛋恰好摔碎的那层楼? - -最原始的方式就是线性扫描:我先在 1 楼扔一下,没碎,我再去 2 楼扔一下,没碎,我再去 3 楼…… - -以这种策略,**最坏**情况应该就是我试到第 7 层鸡蛋也没碎(`F = 7`),也就是我扔了 7 次鸡蛋。 - -先在你应该理解什么叫做「最坏情况」下了,**鸡蛋破碎一定发生在搜索区间穷尽时**,不会说你在第 1 层摔一下鸡蛋就碎了,这是你运气好,不是最坏情况。 - -现在再来理解一下什么叫「至少」要扔几次。依然不考虑鸡蛋个数限制,同样是 7 层楼,我们可以优化策略。 - -最好的策略是使用二分查找思路,我先去第 `(1 + 7) / 2 = 4` 层扔一下: - -如果碎了说明 `F` 小于 4,我就去第 `(1 + 3) / 2 = 2` 层试…… - -如果没碎说明 `F` 大于等于 4,我就去第 `(5 + 7) / 2 = 6` 层试…… - -以这种策略,**最坏**情况应该是试到第 7 层鸡蛋还没碎(`F = 7`),或者鸡蛋一直碎到第 1 层(`F = 0`)。然而无论那种最坏情况,只需要试 `log7` 向上取整等于 3 次,比刚才尝试 7 次要少,这就是所谓的**至少**要扔几次。 - -PS:这有点像 Big O 表示法计算​算法的复杂度。 - -实际上,如果不限制鸡蛋个数的话,二分思路显然可以得到最少尝试的次数,但问题是,**现在给你了鸡蛋个数的限制 `K`,直接使用二分思路就不行了**。 - -比如说只给你 1 个鸡蛋,7 层楼,你敢用二分吗?你直接去第 4 层扔一下,如果鸡蛋没碎还好,但如果碎了你就没有鸡蛋继续测试了,无法确定鸡蛋恰好摔不碎的楼层 `F` 了。这种情况下只能用线性扫描的方法,算法返回结果应该是 7。 - -有的读者也许会有这种想法:二分查找排除楼层的速度无疑是最快的,那干脆先用二分查找,等到只剩 1 个鸡蛋的时候再执行线性扫描,这样得到的结果是不是就是最少的扔鸡蛋次数呢? - -很遗憾,并不是,比如说把楼层变高一些,100 层,给你 2 个鸡蛋,你在 50 层扔一下,碎了,那就只能线性扫描 1~49 层了,最坏情况下要扔 50 次。 - -如果不要「二分」,变成「五分」「十分」都会大幅减少最坏情况下的尝试次数。比方说第一个鸡蛋每隔十层楼扔,在哪里碎了第二个鸡蛋一个个线性扫描,总共不会超过 20 次​。 - -最优解其实是 14 次。最优策略非常多,而且并没有什么规律可言。 - -说了这么多废话,就是确保大家理解了题目的意思,而且认识到这个题目确实复杂,就连我们手算都不容易,如何用算法解决呢? - -### 二、思路分析 - -对动态规划问题,直接套我们以前多次强调的框架即可:这个问题有什么「状态」,有什么「选择」,然后穷举。 - -**「状态」很明显,就是当前拥有的鸡蛋数 `K` 和需要测试的楼层数 `N`**。随着测试的进行,鸡蛋个数可能减少,楼层的搜索范围会减小,这就是状态的变化。 - -**「选择」其实就是去选择哪层楼扔鸡蛋**。回顾刚才的线性扫描和二分思路,二分查找每次选择到楼层区间的中间去扔鸡蛋,而线性扫描选择一层层向上测试。不同的选择会造成状态的转移。 - -现在明确了「状态」和「选择」,**动态规划的基本思路就形成了**:肯定是个二维的 `dp` 数组或者带有两个状态参数的 `dp` 函数来表示状态转移;外加一个 for 循环来遍历所有选择,择最优的选择更新状态: - -```python -# 当前状态为 K 个鸡蛋,面对 N 层楼 -# 返回这个状态下的最优结果 -def dp(K, N): - int res - for 1 <= i <= N: - res = min(res, 这次在第 i 层楼扔鸡蛋) - return res -``` - -这段伪码还没有展示递归和状态转移,不过大致的算法框架已经完成了。 - -我们选择在第 `i` 层楼扔了鸡蛋之后,可能出现两种情况:鸡蛋碎了,鸡蛋没碎。**注意,这时候状态转移就来了**: - -**如果鸡蛋碎了**,那么鸡蛋的个数 `K` 应该减一,搜索的楼层区间应该从 `[1..N]` 变为 `[1..i-1]` 共 `i-1` 层楼; - -**如果鸡蛋没碎**,那么鸡蛋的个数 `K` 不变,搜索的楼层区间应该从 `[1..N]` 变为 `[i+1..N]` 共 `N-i` 层楼。 - -![](../pictures/扔鸡蛋/1.jpg) - -PS:细心的读者可能会问,在第i层楼扔鸡蛋如果没碎,楼层的搜索区间缩小至上面的楼层,是不是应该包含第i层楼呀?不必,因为已经包含了。开头说了 F 是可以等于 0 的,向上递归后,第i层楼其实就相当于第 0 层,可以被取到,所以说并没有错误。 - -因为我们要求的是**最坏情况**下扔鸡蛋的次数,所以鸡蛋在第 `i` 层楼碎没碎,取决于那种情况的结果**更大**: - -```python -def dp(K, N): - for 1 <= i <= N: - # 最坏情况下的最少扔鸡蛋次数 - res = min(res, - max( - dp(K - 1, i - 1), # 碎 - dp(K, N - i) # 没碎 - ) + 1 # 在第 i 楼扔了一次 - ) - return res -``` - -递归的 base case 很容易理解:当楼层数 `N` 等于 0 时,显然不需要扔鸡蛋;当鸡蛋数 `K` 为 1 时,显然只能线性扫描所有楼层: - -```python -def dp(K, N): - if K == 1: return N - if N == 0: return 0 - ... -``` - -至此,其实这道题就解决了!只要添加一个备忘录消除重叠子问题即可: - -```python -def superEggDrop(K: int, N: int): - - memo = dict() - def dp(K, N) -> int: - # base case - if K == 1: return N - if N == 0: return 0 - # 避免重复计算 - if (K, N) in memo: - return memo[(K, N)] - - res = float('INF') - # 穷举所有可能的选择 - for i in range(1, N + 1): - res = min(res, - max( - dp(K, N - i), - dp(K - 1, i - 1) - ) + 1 - ) - # 记入备忘录 - memo[(K, N)] = res - return res - - return dp(K, N) -``` - -这个算法的时间复杂度是多少呢?**动态规划算法的时间复杂度就是子问题个数 × 函数本身的复杂度**。 - -函数本身的复杂度就是忽略递归部分的复杂度,这里 `dp` 函数中有一个 for 循环,所以函数本身的复杂度是 O(N)。 - -子问题个数也就是不同状态组合的总数,显然是两个状态的乘积,也就是 O(KN)。 - -所以算法的总时间复杂度是 O(K*N^2), 空间复杂度 O(KN)。 - -### 三、疑难解答 - -这个问题很复杂,但是算法代码却十分简洁,这就是动态规划的特性,穷举加备忘录/DP table 优化,真的没啥新意。 - -首先,有读者可能不理解代码中为什么用一个 for 循环遍历楼层 `[1..N]`,也许会把这个逻辑和之前探讨的线性扫描混为一谈。其实不是的,**这只是在做一次「选择」**。 - -比方说你有 2 个鸡蛋,面对 10 层楼,你**这次**选择去哪一层楼扔呢?不知道,那就把这 10 层楼全试一遍。至于下次怎么选择不用你操心,有正确的状态转移,递归会算出每个选择的代价,我们取最优的那个就是最优解。 - -另外,这个问题还有更好的解法,比如修改代码中的 for 循环为二分搜索,可以将时间复杂度降为 O(K\*N\*logN);再改进动态规划解法可以进一步降为 O(KN);使用数学方法解决,时间复杂度达到最优 O(K*logN),空间复杂度达到 O(1)。 - -二分的解法也有点误导性,你很可能以为它跟我们之前讨论的二分思路扔鸡蛋有关系,实际上没有半毛钱关系。能用二分搜索是因为状态转移方程的函数图像具有单调性,可以快速找到最值。 - -简单介绍一下二分查找的优化吧,其实只是在优化这段代码: - -```python -def dp(K, N): - for 1 <= i <= N: - # 最坏情况下的最少扔鸡蛋次数 - res = min(res, - max( - dp(K - 1, i - 1), # 碎 - dp(K, N - i) # 没碎 - ) + 1 # 在第 i 楼扔了一次 - ) - return res -``` - -这个 for 循环就是下面这个状态转移方程的具体代码实现: - -$$ dp(K, N) = \min_{0 <= i <= N}\{\max\{dp(K - 1, i - 1), dp(K, N - i)\} + 1\}$$ - -首先我们根据 `dp(K, N)` 数组的定义(有 `K` 个鸡蛋面对 `N` 层楼,最少需要扔几次),**很容易知道 `K` 固定时,这个函数一定是单调递增的**,无论你策略多聪明,楼层增加测试次数一定要增加。 - -那么注意 `dp(K - 1, i - 1)` 和 `dp(K, N - i)` 这两个函数,其中 `i` 是从 1 到 `N` 单增的,如果我们固定 `K` 和 `N`,**把这两个函数看做关于 `i` 的函数,前者随着 `i` 的增加应该也是单调递增的,而后者随着 `i` 的增加应该是单调递减的**: - -![](../pictures/扔鸡蛋/2.jpg) - -这时候求二者的较大值,再求这些最大值之中的最小值,其实就是求这个交点嘛,熟悉二分搜索的同学肯定敏感地想到了,这不就是相当于求 Valley(山谷)值嘛,可以用二分查找来快速寻找这个点的。 - -直接贴一下代码吧,思路还是完全一样的: - -```python -def superEggDrop(self, K: int, N: int) -> int: - - memo = dict() - def dp(K, N): - if K == 1: return N - if N == 0: return 0 - if (K, N) in memo: - return memo[(K, N)] - - # for 1 <= i <= N: - # res = min(res, - # max( - # dp(K - 1, i - 1), - # dp(K, N - i) - # ) + 1 - # ) - - res = float('INF') - # 用二分搜索代替线性搜索 - lo, hi = 1, N - while lo <= hi: - mid = (lo + hi) // 2 - broken = dp(K - 1, mid - 1) # 碎 - not_broken = dp(K, N - mid) # 没碎 - # res = min(max(碎,没碎) + 1) - if broken > not_broken: - hi = mid - 1 - res = min(res, broken + 1) - else: - lo = mid + 1 - res = min(res, not_broken + 1) - - memo[(K, N)] = res - return res - - return dp(K, N) -``` - -这里就不展开其他解法了,留在下一篇文章 [高楼扔鸡蛋进阶](高楼扔鸡蛋进阶.md) - -我觉得吧,我们这种解法就够了:找状态,做选择,足够清晰易懂,可流程化,可举一反三。掌握这套框架学有余力的话,再去考虑那些奇技淫巧也不迟。 - -最后预告一下,《动态规划详解(修订版)》和《回溯算法详解(修订版)》已经动笔了,教大家用模板的力量来对抗变化无穷的算法题,敬请期待。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git a/interview/Find-Duplicate-and-Missing-Element.md b/interview/Find-Duplicate-and-Missing-Element.md new file mode 100644 index 0000000000..3aa2b61f0a --- /dev/null +++ b/interview/Find-Duplicate-and-Missing-Element.md @@ -0,0 +1,115 @@ +# How to Find Duplicate and Missing Element + +**Translator: [bryceustc](https://github.com/bryceustc)** + +**Author: [labuladong](https://github.com/labuladong)** + +Today we are going to talk about a simple but skillfull problem: find duplicate and missing element. It seems to be similar to the previous problem [How to Find Missing Elements](./missing_elements.md), but there are some difference between these two problems. + +Here is the detailed description of this problem(LeetCode 645: Set Mismatch) + +The set ``S``originally contains numbers from `1` to ``n``. But unfortunately, due to the data error, one of the numbers in the set got duplicate to **another** number in the set, which results in repetition of one number and loss of another number. + +Given an array ``nums`` representing the data status of this set after the error. Your task is to firstly find the number occurs twice and then find the number that is missing. Return them in the form of an array. + +**Example 1:** + +``` +Input: nums = [1,2,2,4] +Output: [2,3] +``` + +Actually, it's easy to solve this problem. Firstly, traverse over the whole `nums` array and use HashMap to store the number of times each element of the array. After this, we can consider every number from `1` to `n`, and check for its presence in map. + +But here's a problem. This solution requires a HashMap that means the space complexity is O(n). We check the condition again. Consider the numbers from `1` to `n`, which happens to be one duplicate element and one missing element. There must be something strange about things going wrong. + +We must traverse over the whole `nums` array of size `n` for each of the numbers from `1` to `n`. That means the time complexity is O(n). So we can think how to save the space used to reduce the space complexity to O(1). + +## Analysis + +The characteristic of this problem is that each element has a certain correspondence with the array index. + +Let's change the condition of the problem temporarily. Change the elements in ``nums`` array to ``[0..N-1]``. Therefore, each element corresponds exactly to an array index, which is easy to understand. + +We assume that there are no duplicate or missing elements in the array. Therefore, each element corresponds to a unique index value. + +But the question now is one number is repeated that results which results in loss of another number. What would happen? This will result in two elements corresponding to the same index, and there will be an index with no elements to correspond. + +If we can somehow find the duplicate corresponding index, which means we find the duplicate element. Then find the index that no element to correspond that also means we find the missing element. + +So, how do you determine how many elements of an index correspond to without using extra space? Here is the subtlety of the question. + +**By turning the element corresponding to each index into a negative number, it indicates that this index has been mapped once.** + +![](../pictures/dupmissing/1.gif) + +If we find a duplicate element `4`, the intuitive result is that the element corresponding to index `4 `is already negative. + +![](../pictures/dupmissing/2.jpg) + +For the missing element `3`, the intuitive result is that the element corresponding to index `3 `is positive. + +![](../pictures/dupmissing/3.jpg) + +Therefore, we can code as follows: +```c++ +vector findErrorNums(vector& nums) { + int n = nums.size(); + int dup = -1; + for (int i = 0; i < n; i++) { + int index = abs(nums[i]); + // nums[index] < 0 means find the duplicate element + if (nums[index] < 0) + dup = abs(nums[i]); + else + nums[index] *= -1; + } + + int missing = -1; + for (int i = 0; i < n; i++) + // nums[i] > 0 means find the missing element + if (nums[i] > 0) + missing = i; + + return {dup, missing}; +} +``` + +Now, the question is basically solved. But don't forget that we have just assumed that the elements in ``nums`` array is from `0` to `N-1`. Actually, it should be `1` to `N`. So we need to modify two places to get the right answer to the original question. + +```c++ +vector findErrorNums(vector& nums) { + int n = nums.size(); + int dup = -1; + for (int i = 0; i < n; i++) { + // Now, elements start at 1 + int index = abs(nums[i]) - 1; + if (nums[index] < 0) + dup = abs(nums[i]); + else + nums[index] *= -1; + } + + int missing = -1; + for (int i = 0; i < n; i++) + if (nums[i] > 0) + // Convert index to element + missing = i + 1; + + return {dup, missing}; +} +``` + +In fact, it makes sense for elements to start from `1`, and it must start with a non-zero number. If the element starts from `0`, the opposite number of `0` is still itself. So when the number `0` is repeated or missing, we can't deal with this situation. Our previous assumption was just to simplify the problem and make it easier to understand. + +## Summary + +**The key point is that elements and indexes appear in pairs for this kind of problems. Common methods include Sorting, XOR, and Map** + +The idea of Map is the above analysis. Mapping each index and element, and recording whether an element is mapped with a sign. + +The Sorting method is also easy to understand. For this problem, we can assume that if all elements are sorted from smallest to largest. If we find that the corresponding elements of the index didn't match, so we find duplicate and missing elements. + +XOR operation is also commonly used. The XOR operation (`^`) has a special property: the result of a number XOR itself is 0, and the result of a number with 0 is itself. For instance: ``a ^ a = 0, a ^ 0 = a``. If we take XOR of the index and element at the same time, the paired index and element can be eliminated, and the remaining are duplicate or missing elements. You can look at the previous article [Find Missing Elements](./missing_elements.md) which introduce this method. + +_We can stop by now._ \ No newline at end of file diff --git a/interview/LRU_algorithm.md b/interview/LRU_algorithm.md new file mode 100644 index 0000000000..ea1e14e238 --- /dev/null +++ b/interview/LRU_algorithm.md @@ -0,0 +1,229 @@ +# Detailed Analysis of LRU Algorithm + +**Translator: [youyun](https://github.com/youyun)** + +**Author: [labuladong](https://github.com/labuladong)** + +### 1. What is LRU Algorithm + +It is just a cache clean-up strategy. + +A computer has limited memory cache. If the cache is full, some contents need to be removed from cache to provide space for new content. However, which part of the cache should be removed? We hope to remove not so useful contents, while leaving useful contents untouched for future usage. So the question is, what are the criteria to determine if the data is _useful_ or not? + +LRU (Least Recently Used) cache clean-up algorithm is a common strategy. According to the name, the latest used data should be _useful_. Hence, when the memory cache is full, we should prioritize removing those data that haven't been used for long are not useful. + +For example, an Android phone can run apps in the background. If I opened in sequence: Settings, Phone Manager, and Calendar, their order in the background will be shown as following: + +![jietu](../pictures/LRU/1.jpg) + +If I switch to Settings now, Settings will be brought to the first: + +![jietu](../pictures/LRU/2.jpg) + +Assume that my phone only allows me to open 3 apps simultaneously, then the cache is already full by now. If I open another app, Clock, then I have to close another app to free up space for Clock. Which one should be closed? + +According to LRU strategy, the lowest app, Phone Manager, should be closed, because it is the longest unused app. Afterwards, the newly opened app will be on the top: + +![jietu](../pictures/LRU/3.jpg) + +Now you should understand LRU (Least Recently Used) strategy. There are some other strategies available, for example, LFU (Least Frequently Used) strategy, etc. Different strategies can be applied in different use cases. We'll focus on LRU in this article. + +### 2. LRU Algorithm Description + +LRU algorithm is actually about data structure design: +1. Take a parameter, `capacity`, as the maximum size; then +2. Implement two APIs: + * `put(key, val)`: to store key-value pair + * `get(key)`: return the value associated with the key; return -1 if the key doesn't exist. +3. The time complexity for both `get` and `put` should be __O(1)__. + +Let's use an example to understand how the LRU algorithm works. + +```cpp +/* Cache capacity is 2 */ +LRUCache cache = new LRUCache(2); +// Assume the cache is an queue +// The head is on the left, while the tail is on the right +// The latest used is at the head, while the longest unused is at the tail +// Bracket represents key-value pair, (key, val) + +cache.put(1, 1); +// cache = [(1, 1)] +cache.put(2, 2); +// cache = [(2, 2), (1, 1)] +cache.get(1); // return 1 +// cache = [(1, 1), (2, 2)] +// Remarks: because key 1 is visited, move it to the head +// Return the value, 1, associated with key 1 +cache.put(3, 3); +// cache = [(3, 3), (1, 1)] +// Remarks: the memory capacity is full +// We need to remove some contents to free up space +// Removal will prioritize longest unused data, which is at the tail +// Afterwards, insert the new data at the head +cache.get(2); // return -1 (not found) +// cache = [(3, 3), (1, 1)] +// Remarks: key 2 does not exist in the cache +cache.put(1, 4); +// cache = [(1, 4), (3, 3)] +// Remarks: key 1 exists +// Overwrite with new value 4 +// Don't forget to bring the key to the head +``` + +### 3. LRU Algorithm Design + +Through analysis of the above steps, if time complexity for `put` and `get` are both O(1), we can summarize features of this cache data structure: fast search, fast insertion, fast deletion, and ordered. +- _Ordered_: Obviously, the data has to be ordered to distinguish recently used and longest unused. +- _Fast Search_: We also need to be able to find if a key exists in the cache. +- _Fast Deletion_: If the cache is full, we need to delete the last element. +- _Fast Insertion_: We need to insert the data to the head upon each visit. + +Which data structure can fulfill the above requirements? Hash table can search fast, but the data is unordered. Data in linked list is ordered, and can be inserted or deleted fast, but is hard to search. Combining these two, we can come up with a new data structure: __hash linked list__. + +The core data structure of LRU cache algorithm is hash linked list, a combination of doubly linked list and hash table. Here is how the data structure looks: + +![HashLinkedList](../pictures/LRU/5.jpg) + +The idea is simple - using a hash table to provide the ability of fast search to linked list. Think again about the previous example, isn't this data structure the perfect solution for LRU cache data structure? + +Some audience may wonder, why doubly linked list? Can't single linked list work? Since key exists in hash table, why do we have to store the key-value pairs in linked list instead of values only? + +The answers only afloat when we actually do it. We can only understand the rationale behind the design after we implement the LRU algorithm ourselves. Let's look at the code. + +### 4. Implementation + +A lot of programming languages have built-in hash linked list, or LRU-alike functions. To help understand the details of the LRU algorithm, let's use Java to reinvent the wheel. + +First, define the `Node` class of doubly linked list. Assuming both `key` and `val` are of type `int`. + +```java +class Node { + public int key, val; + public Node next, prev; + public Node(int k, int v) { + this.key = k; + this.val = v; + } +} +``` + +Using our `Node` class, implement a doubly linked list with the necessary APIs (the time complexity of these functions are all O(1)): + +```java +class DoubleList { + // Add x at the head, time complexity O(1) + public void addFirst(Node x); + + // Delete node x in the linked list (x is guaranteed to exist) + // Given a node in a doubly linked list, time complexity O(1) + public void remove(Node x); + + // Delete and return the last node in the linked list, time complexity O(1) + public Node removeLast(); + + // Return the length of the linked list, time complexity O(1) + public int size(); +} +``` + +P.S. This is the typical interface of a doubly linked list. In order to focus on the LRU algorithm, we'll skip the detailed implementation of functions in this class. + +Now we can answer the question, why we have to use a doubly linked list. In order to delete a node, we not only need to get the pointer of the node itself, but also need to update the node before and the node after. Only using a doubly linked list, we can guarantee the time complexity is O(1). + +With the doubly linked list, we just need to use it in with a hash table in the LRU algorithm. Let's sort out the logic with pseudo code: + +```java +// key associated with Node(key, val) +HashMap map; +// Node(k1, v1) <-> Node(k2, v2)... +DoubleList cache; + +int get(int key) { + if (key does not exist) { + return -1; + } else { + bring (key, val) to the head; + return val; + } +} + +void put(int key, int val) { + Node x = new Node(key, val); + if (key exists) { + delete the old node; + insert the new node x to the head; + } else { + if (cache is full) { + delete the last node in the linked list; + delete the associated value in map; + } + insert the new node x to the head; + associate the new node x with key in map; + } +} +``` + +If you can understand the logic above, it's easy to translate to code: + +```java +class LRUCache { + // key -> Node(key, val) + private HashMap map; + // Node(k1, v1) <-> Node(k2, v2)... + private DoubleList cache; + // Max capacity + private int cap; + + public LRUCache(int capacity) { + this.cap = capacity; + map = new HashMap<>(); + cache = new DoubleList(); + } + + public int get(int key) { + if (!map.containsKey(key)) + return -1; + int val = map.get(key).val; + // Using put method to bring it forward to the head + put(key, val); + return val; + } + + public void put(int key, int val) { + // Initialize new node x + Node x = new Node(key, val); + + if (map.containsKey(key)) { + // Delete the old node, add to the head + cache.remove(map.get(key)); + cache.addFirst(x); + // Update the corresponding record in map + map.put(key, x); + } else { + if (cap == cache.size()) { + // Delete the last node in the linked list + Node last = cache.removeLast(); + map.remove(last.key); + } + // Add to the head + cache.addFirst(x); + map.put(key, x); + } + } +} +``` + +This can answer the previous question, why we need to store key-value pair in the linked list, instead of value only. Pay attention to the block of code below: + +```java +if (cap == cache.size()) { + // Delete the last node + Node last = cache.removeLast(); + map.remove(last.key); +} +``` + +If the cache is full, we not only need to delete the last node, but also need to delete the key in the map, where we can only get the key through the node. If we only store value in a node, we can't get the key, and hence, can't delete the key from the map. + +Till now, you should have understood the idea and implementation of LRU algorithm. One common mistake is to update associated entries in the hash table when you deal with nodes in the linked list. diff --git "a/interview/LRU\347\256\227\346\263\225.md" "b/interview/LRU\347\256\227\346\263\225.md" deleted file mode 100644 index d6d89b6d05..0000000000 --- "a/interview/LRU\347\256\227\346\263\225.md" +++ /dev/null @@ -1,220 +0,0 @@ -# LRU算法详解 - -### 一、什么是 LRU 算法 - -就是一种缓存淘汰策略。 - -计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。那么,什么样的数据,我们判定为「有用的」的数据呢? - -LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。 - -举个简单的例子,安卓手机都可以把软件放到后台运行,比如我先后打开了「设置」「手机管家」「日历」,那么现在他们在后台排列的顺序是这样的: - -![jietu](../pictures/LRU%E7%AE%97%E6%B3%95/1.jpg) - -但是这时候如果我访问了一下「设置」界面,那么「设置」就会被提前到第一个,变成这样: - -![jietu](../pictures/LRU%E7%AE%97%E6%B3%95/2.jpg) - -假设我的手机只允许我同时开 3 个应用程序,现在已经满了。那么如果我新开了一个应用「时钟」,就必须关闭一个应用为「时钟」腾出一个位置,关那个呢? - -按照 LRU 的策略,就关最底下的「手机管家」,因为那是最久未使用的,然后把新开的应用放到最上面: - -![jietu](../pictures/LRU%E7%AE%97%E6%B3%95/3.jpg) - -现在你应该理解 LRU(Least Recently Used)策略了。当然还有其他缓存淘汰策略,比如不要按访问的时序来淘汰,而是按访问频率(LFU 策略)来淘汰等等,各有应用场景。本文讲解 LRU 算法策略。 - -### 二、LRU 算法描述 - -LRU 算法实际上是让你设计数据结构:首先要接收一个 capacity 参数作为缓存的最大容量,然后实现两个 API,一个是 put(key, val) 方法存入键值对,另一个是 get(key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1。 - -注意哦,get 和 put 方法必须都是 $O(1)$ 的时间复杂度,我们举个具体例子来看看 LRU 算法怎么工作。 - -```cpp -/* 缓存容量为 2 */ -LRUCache cache = new LRUCache(2); -// 你可以把 cache 理解成一个队列 -// 假设左边是队头,右边是队尾 -// 最近使用的排在队头,久未使用的排在队尾 -// 圆括号表示键值对 (key, val) - -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 算法设计 - -分析上面的操作过程,要让 put 和 get 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分。 - -因为显然 cache 必须有顺序之分,以区分最近使用的和久未使用的数据;而且我们要在 cache 中查找键是否已存在;如果容量满了要删除最后一个数据;每次访问还要把数据插入到队头。 - -那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。 - -LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样: - -![HashLinkedList](../pictures/LRU%E7%AE%97%E6%B3%95/4.jpg) - -思想很简单,就是借助哈希表赋予了链表快速查找的特性嘛:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。回想刚才的例子,这种数据结构是不是完美解决了 LRU 缓存的需求? - -也许读者会问,为什么要是双向链表,单链表行不行?另外,既然哈希表中已经存了 key,为什么链表中还要存键值对呢,只存值不就行了? - -想的时候都是问题,只有做的时候才有答案。这样设计的原因,必须等我们亲自实现 LRU 算法之后才能理解,所以我们开始看代码吧~ - -### 四、代码实现 - -很多编程语言都有内置的哈希链表或者类似 LRU 功能的库函数,但是为了帮大家理解算法的细节,我们用 Java 自己造轮子实现一遍 LRU 算法。 - -首先,我们把双链表的节点类写出来,为了简化,key 和 val 都认为是 int 类型: - -```java -class Node { - public int key, val; - public Node next, prev; - public Node(int k, int v) { - this.key = k; - this.val = v; - } -} -``` - -然后依靠我们的 Node 类型构建一个双链表,实现几个需要的 API(这些操作的时间复杂度均为 $O(1)$): - -```java -class DoubleList { - // 在链表头部添加节点 x,时间 O(1) - public void addFirst(Node x); - - // 删除链表中的 x 节点(x 一定存在) - // 由于是双链表且给的是目标 Node 节点,时间 O(1) - public void remove(Node x); - - // 删除链表中最后一个节点,并返回该节点,时间 O(1) - public Node removeLast(); - - // 返回链表长度,时间 O(1) - public int 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; - } -} - -void put(int key, int val) { - Node x = new Node(key, val); - if (key 已存在) { - 把旧的数据删除; - 将新节点 x 插入到开头; - } else { - if (cache 已满) { - 删除链表的最后一个数据腾位置; - 删除 map 中映射到该数据的键; - } - 将新节点 x 插入到开头; - map 中新建 key 对新节点 x 的映射; - } -} -``` - -如果能够看懂上述逻辑,翻译成代码就很容易理解了: - -```java -class LRUCache { - // key -> Node(key, val) - private HashMap map; - // Node(k1, v1) <-> Node(k2, v2)... - private DoubleList cache; - // 最大容量 - private int cap; - - public LRUCache(int capacity) { - this.cap = capacity; - 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); - } - } -} -``` - -这里就能回答之前的问答题“为什么要在链表中同时存储 key 和 val,而不是只存储 val”,注意这段代码: - -```java -if (cap == cache.size()) { - // 删除链表最后一个数据 - Node last = cache.removeLast(); - map.remove(last.key); -} -``` - -当缓存容量已满,我们不仅仅要删除最后一个 Node 节点,还要把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 Node 得到。如果 Node 结构中只存储 val,那么我们就无法得知 key 是什么,就无法删除 map 中的键,造成错误。 - -至此,你应该已经掌握 LRU 算法的思想和实现了,很容易犯错的一点是:处理链表节点的同时不要忘了更新哈希表中对节点的映射。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git a/interview/Print_PrimeNumbers.md b/interview/Print_PrimeNumbers.md new file mode 100644 index 0000000000..8b5b2ad0b7 --- /dev/null +++ b/interview/Print_PrimeNumbers.md @@ -0,0 +1,151 @@ +# How to find prime Numbers efficiently + +**Translator: [shazi4399](https://github.com/shazi4399)** + +**Author: [labuladong](https://github.com/labuladong)** + +The definition of a prime number seems simple,which is said to be prime number if it can be divided by 1 and itself. + +However,don't think that the definition of prime numbers is simple. I am afraid that few people can write a prime-related algorithm that works really efficiently. Let's say you write a function like this: + +```java +// Returns several primes in the interval [2, n) +int countPrimes(int n) + +// E.g. countPrimes (10) returns 4 +// Because 2,3,5,7 is prime numbers +``` + +How would you progrma this function? I think you maybe write like this: + +```java +int countPrimes(int n) { + int count = 0; + for (int i = 2; i < n; i++) + if (isPrim(i)) count++; + return count; +} + +// Determines whether integer n is prime +boolean isPrime(int n) { + for (int i = 2; i < n; i++) + if (n % i == 0) + // There are other divisibility factors + return false; + return true; +} +``` + +The time complexity is O (n ^ 2), which is a big problem.**First of all, the idea of using the isPrime function to assist is not efficient; and even if you want to use the isPrime function, there is computational redundancy in writing the algorithm**. + +Let's briefly talk about **how to write an algorithm if you want to determine whether a number is prime or not**. Just slightly modify the for loop condition in the isPrim code above: + +```java +boolean isPrime(int n) { + for (int i = 2; i * i <= n; i++) + ... +} +``` + +In other words, `i` does not need to traverse to` n`, but only to `sqrt (n)`. Why? let's take an example, suppose `n = 12`. + +```java +12 = 2 × 6 +12 = 3 × 4 +12 = sqrt(12) × sqrt(12) +12 = 4 × 3 +12 = 6 × 2 +``` + +As you can see, the last two products are the reverse of the previous two, and the critical point of inversion is at `sqrt (n)`. + +In other words, if no divisible factor is found within the interval `[[2, sqrt (n)]`, you can directly conclude that `n` is a prime number, because in the interval `[[sqrt (n), n] ` Nor will you find a divisible factor. + +Now, the time complexity of the `isPrime` function is reduced to O (sqrt (N)), ** but we don't actually need this function to implement the` countPrimes` function. The above just hope that readers understand the meaning of `sqrt (n)`, because it will be used again later. + + +### Efficient implementation `countPrimes` + +The core idea of efficiently solving this problem is to reverse the conventional idea above: + +First from 2, we know that 2 is a prime number, then 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8 ... all are not prime numbers. + +Then we found that 3 is also a prime number, so 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12 ... are also impossible to be prime numbers. + +Seeing this, do you understand the logic of this exclusion method a bit? First look at our first version of the code: + +```java +int countPrimes(int n) { + boolean[] isPrim = new boolean[n]; + // Initialize the arrays to true + Arrays.fill(isPrim, true); + + for (int i = 2; i < n; i++) + if (isPrim[i]) + // Multiples of i cannot be prime + for (int j = 2 * i; j < n; j += i) + isPrim[j] = false; + + int count = 0; + for (int i = 2; i < n; i++) + if (isPrim[i]) count++; + + return count; +} +``` + +If you can understand the above code, then you have mastered the overall idea, but there are two subtle areas that can be optimized. + +First of all, recall the `isPrime` function that just judges whether a number is prime. Due to the symmetry of the factors, the for loop only needs to traverse` [2, sqrt (n)] `. Here is similar, our outer for loop only needs to traverse to `sqrt (n)`: + +```java +for (int i = 2; i * i < n; i++) + if (isPrim[i]) + ... +``` + +In addition, it is difficult to notice that the inner for loop can also be optimized. Our previous approach was: + +```java +for (int j = 2 * i; j < n; j += i) + isPrim[j] = false; +``` + +This can mark all integer multiples of `i` as` false`, but there is still computational redundancy. + +For example, when `n = 25` and` i = 4`, the algorithm will mark numbers such as 4 × 2 = 8, 4 × 3 = 12, and so on, but these two numbers have been marked by 2 × 4 and 3 × 4 that is `i = 2` and` i = 3`. + +We can optimize it slightly so that `j` traverses from the square of` i` instead of starting from `2 * i`: + +```java +for (int j = i * i; j < n; j += i) + isPrim[j] = false; +``` + +In this way, the algorithm for counting prime numbers is efficiently implemented. In fact, this algorithm has a name, which called Sieve of Eratosthenes. Take a look at the complete final code: + +```java +int countPrimes(int n) { + boolean[] isPrim = new boolean[n]; + Arrays.fill(isPrim, true); + for (int i = 2; i * i < n; i++) + if (isPrim[i]) + for (int j = i * i; j < n; j += i) + isPrim[j] = false; + + int count = 0; + for (int i = 2; i < n; i++) + if (isPrim[i]) count++; + + return count; +} +``` + +**The time complexity of this algorithm is difficult to calculate**.It is obvious that the time is related to these two nested for loops. The operands should be: + + n/2 + n/3 + n/5 + n/7 + ... += n × (1/2 + 1/3 + 1/5 + 1/7...) + +In parentheses, ther is the inverse of the prime number .The final result is O(N * loglogN),and readers interested in this can refer to the time complexity of the algorithm + +That is all about how to find prime Numbers.The seemingly simple problem does has a lot of details to polish \ No newline at end of file diff --git a/interview/README.md b/interview/README.md deleted file mode 100644 index 5c31a12770..0000000000 --- a/interview/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# 高频面试系列 - -8 说了,本章都是高频面试题,配合前面的动态规划系列,祝各位马到成功! - -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: - -![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git a/interview/RemoveDuplicatesfromSortedArray.md b/interview/RemoveDuplicatesfromSortedArray.md new file mode 100644 index 0000000000..a7807ed292 --- /dev/null +++ b/interview/RemoveDuplicatesfromSortedArray.md @@ -0,0 +1,69 @@ +# Remove Duplicates from Sorted Array + +**Translator: [Hi_archer](https://hiarcher.top/)** + +**Author: [labuladong](https://github.com/labuladong)** + +We know that for arrays,it is efficient to insert and delete elements at the end,with a time complexity of O(1).However, if we insert and delete elements at the middle or the beginning,it will move many data, with a time complexity of O(N). + +Therefore, for the general algorithm problems dealing with arrays, we need to operate on the elements at the end of the array as much as possible to avoid additional time complexity + +This article is on how to remove Duplicates from Sorted Array. + +![](../pictures/Remove_Duplicates_from_Sorted_Array/title1.jpg) + +Obviously, since the array is sorted, the duplicate elements must be connected together, so it's not difficult to find them, but if you delete each duplicate element as soon as you find it, you're going to delete it in the middle of the array, and the total time complexity is going to be $O(N^2)$.And the problem asking us must do this by modifying the input array in-place with O(1) extra memory. + +In fact,**for the array related algorithm problem,there is a general technique: try to avoid deleting the element in the middle, then I want to find a way to swap the element to the last**.In this way,the elements to be deleted are dragged to the end of the array and the time complexity of a single deletion is reduced to $O(1)$. + +Through this idea, we can derive a common way to solve similar requirements——the two-pointer technique.To be specific, it should be fast or slow pointer. + +We let the slow pointer `slow` go to the back of the array, and the fast pointer` fast` go ahead to find the way. If we find a unique element,let` slow` move forward. In this way, when the `fast` pointer traverses the entire array` nums`, **`nums [0..slow]` is a unique element, and all subsequent elements are repeated elements**. + +```java +int removeDuplicates(int[] nums) { + int n = nums.length; + if (n == 0) return 0; + int slow = 0, fast = 1; + while (fast < n) { + if (nums[fast] != nums[slow]) { + slow++; + // Maintain no repetition of nums[0..slow] + nums[slow] = nums[fast]; + } + fast++; + } + //The length is index + 1 + return slow + 1; +} +``` + +Look at the process of algorithm implementation: + +![](../pictures/Remove_Duplicates_from_Sorted_Array/1.gif) + +Extending it briefly,how to remove Duplicates from Sorted list.In fact, it is exactly the same as an array.The only difference is that the array assignment operation is turned into an operation pointer: + +```java +ListNode deleteDuplicates(ListNode head) { + if (head == null) return null; + ListNode slow = head, fast = head.next; + while (fast != null) { + if (fast.val != slow.val) { + // nums[slow] = nums[fast]; + slow.next = fast; + // slow++; + slow = slow.next; + } + // fast++ + fast = fast.next; + } + // The list disconnects from the following repeating elements + slow.next = null; + return head; +} +``` + +![](../pictures/Remove_Duplicates_from_Sorted_Array/2.gif) + + diff --git a/interview/Reservoir Sampling.md b/interview/ReservoirSampling.md similarity index 98% rename from interview/Reservoir Sampling.md rename to interview/ReservoirSampling.md index 9b4cc26d2b..cad10bcc79 100644 --- a/interview/Reservoir Sampling.md +++ b/interview/ReservoirSampling.md @@ -1,6 +1,8 @@ -Author: [labuladong](https://github.com/labuladong) +# Reservoir Sampling algorithm -Translator: [wsyzxxxx](https://github.com/wsyzxxxx) +**Author: [labuladong](https://github.com/labuladong)** + +**Translator: [wsyzxxxx](https://github.com/wsyzxxxx)** Recently I met with two interesting questions on LeetCode, LC382 and LC398. They are about Reservoir Sampling algorithm which basically is one kind of random possibility algorithm. If you understnd it, then such kind of questions will not be difficult. Otherwise, it may confuse you a lot. diff --git a/interview/Seatscheduling.md b/interview/Seatscheduling.md new file mode 100644 index 0000000000..0fef352f18 --- /dev/null +++ b/interview/Seatscheduling.md @@ -0,0 +1,223 @@ +# How to arrange candidates' seats + +**Translator: [SCUhzs](https://github.com/HuangZiSheng001)** + +**Author: [labuladong](https://github.com/labuladong)** + + + +This is no.885 question in LeetCode, interesting and skillful. Solving such problems is not as IQ-cost as dynamic programming, but rather depends on understanding of common data structures and capacity to write code. As far as I'm concerned, it deserves our attention and study. + +By the way, I'd like to say something. Many readers ask me, how to sum up framework of algorithm. But in fact, that's not really like that. The framework is slowly extracted from the details. I hope, after you read our articles, you'd better take some time to try solving more similar problems by yourself. As it said, "Practice goes deeper than theoretic knowledge." + +Let me first describe the subject: "suppose there is an examination room, with a row of `N` seats, respectively, their indexes are `[0.. n-1]`. The Candidates, will **successively** enter the room, will probably leave at **any time**." + +As an examiner, you should arrange the seats for students, so as to meet those requirements: **whenever a student enters, maximize the distance between him and the nearest other students; if there are more than one such seats, arrange him to the seat with the smallest index.** This is a real situation in life as we known. + +That is, you need to implement a class like this: + +```java +class ExamRoom { + // constructor, receive the N which means total number of seats + public ExamRoom(int N); + // when a candidate comes, return to the seat assigned for him + public int seat(); + // The candidate in the position P now left + // It can be considered that there must be a candidate in the position P + public void leave(int p); +} +``` + +For example, there are five seats in the room, which are `[0..4]`: + +When the candidate 1 enters (call `seat()`), it is OK for him to sit in any position, but you should arrange the position with lowest index for him, that is, return position 0. + +When the candidate 2 enters (call `seat()`), he should keep away from candidates nearby as possible, that is, return to position 4. + +When the candidate 3 enters, he should keep away from candidates nearby as possible, so he need to sit in the middle, that is, seat 2. + +If another candidate enters, he can sit in seat 1 or seat 3. Take the smaller one, index 1. + +And so on. + +In the situation just mentioned, the function `leave` doesn't be called. However, readers can definitely find the following regular: + +**If we regard every two adjacent candidates as the two endpoints of a line segment, the new arrangement is that, find the longest line segment, let this candidate 「dichotomy」 the line segment in middle, and then the middle point is the seat assigned to him. Actually, `Leave (P) ` is to remove the end point `p`, so as to merge two adjacent segments into one.** + +It's not hard to think about it, actually, this question wants to examine your understanding of data structure. To implement the above logic, which data structure should be selected ? + + + +### 1. Thinking analysis + +According to the above idea, first of all, we need to abstract the students sitting in the classroom into line segments, which can be simply represented by an array of 2 size . + +In addition, the idea requires us to find the 「longest」 line segment, removing or adding the line segment both are needed. + +**If we face with such a requirement that need to get the most value in the dynamic process, the ordered data structure should be used. Binary heap and balanced binary search tree is what we use most often.** The priority queue, which implemented by binary heap, its time complexity of getting most value is O (logN), but only the maximum value can be deleted. Balanced binary tree can not only get the most value, but also modify or delete any value, and the time complexity of them both are O (logn). + +In summary, binary heap can't finish the operation of `leave` , so balanced binary tree should be chose. And we will use a structure named `TreeSet`, which used in JAVA. It is an ordered data structure, and its bottom layer is implemented by red black tree. + +By the way, when it comes to Set or Map, some readers may take it for granted that it is a HashSet or a HashMap. There is something wrong with that. + +Because the bottom layer of Hash_Set/Map is implemented by the hash function and the array, it has the feature: its traversal order is not fixed while its operation efficiency is high, and its time complexity is O (1). + +Meanwhile, the Set/Map can also rely on other underlying data structures, The Red Black Tree (a balanced binary search tree) is the common one, which has a feature that maintaining the order of elements automatically and its efficiency is O (logn). This is commonly referred to 「ordered Set/Map」. + +The `TreeSet` we use just is an ordered set. Its purpose is to maintain the order of line length, quickly find the longest line, and quickly delete and insert. + + + +### 2. Simplify the problem + +Firstly, if there are multiple optional seats, you should choose the seat with the lowest index. **Let's simplify the problem first, this is, ignore this requirement for the moment** , and put the implement of above idea ahead. + +Another common programming trick used in this problem is to use a 「virtual line segment」, so as to let the algorithm start properly, the same as the reason why the algorithms which related to linked list algorithms need a 「virtual header」. + +```java +// Map endpoint p to the segment with P as the left endpoint +private Map startMap; +// Map endpoint p to the segment with P as the right endpoint +private Map endMap; +// According to their length, store all line segments from small to large +private TreeSet pq; +private int N; + +public ExamRoom(int N) { + this.N = N; + startMap = new HashMap<>(); + endMap = new HashMap<>(); + pq = new TreeSet<>((a, b) -> { + // Calculate the length of two line segments + int distA = distance(a); + int distB = distance(b); + // Longer means it is bigger, and put it back + return distA - distB; + }); + // Firstly, put a virtual segment in the ordered set + addInterval(new int[] {-1, N}); +} + +/* Remove a line segment */ +private void removeInterval(int[] intv) { + pq.remove(intv); + startMap.remove(intv[0]); + endMap.remove(intv[1]); +} + +/* Add a line segment */ +private void addInterval(int[] intv) { + pq.add(intv); + startMap.put(intv[0], intv); + endMap.put(intv[1], intv); +} + +/* Calculate the length of a line segment */ +private int distance(int[] intv) { + return intv[1] - intv[0] - 1; +} +``` + +「Virtual line segment 」is to represent all seats as one line segment: + +![](../pictures/seat_scheduling/9.png) + + + +With the foreshadowing, the main API `seat` and `leave` could be written: + + + +```java +public int seat() { + // Take the longest line from the ordered set + int[] longest = pq.last(); + int x = longest[0]; + int y = longest[1]; + int seat; + if (x == -1) { // case 1 + seat = 0; + } else if (y == N) { // case 2 + seat = N - 1; + } else { // case 3 + seat = (y - x) / 2 + x; + } + // Divide the longest line segment into two segments + int[] left = new int[] {x, seat}; + int[] right = new int[] {seat, y}; + removeInterval(longest); + addInterval(left); + addInterval(right); + return seat; +} + +public void leave(int p) { + // Find out the lines around p + int[] right = startMap.get(p); + int[] left = endMap.get(p); + // Merge two segments into one + int[] merged = new int[] {left[0], right[1]}; + removeInterval(left); + removeInterval(right); + addInterval(merged); +} +``` + +![three contidions](../pictures/seat_scheduling/8.png) + + + +At this point, this algorithm is basically implemented. Although a lot of code, in fact it's not difficult to think: find the longest line segment, divide it into two segments from the middle, and the midpoint is the return value of `seat()`; find the left and right line segments of `p` , merge them into one segment. Those is the logic of `leave (P)`. + + + +### 3. Advanced problem + +However, when the topic requires multiple choices, we should choose the seat with the smallest index. We just ignored that. For example, the following situation may cause errors:![](../pictures/seat_scheduling/3.jpg) + +Now there are line segments `[0,4]` and `[4,9]` in the ordered set, the longest line segment `longest` is the latter one. According to the logic of `seat`, it will split the `[4,9]`, that is, return to seat 6. However, the correct answer should be seat 2. Because both 2 and 6 meet the condition of maximizing the distance between adjacent candidates, and the smaller one should be taken. + +![](../pictures/seat_scheduling/4.jpg) + +**The solution to such requirements is to modify the sorting method of ordered data structure.** In this problem, is that, modify the logic of `treemap`'s comparison function: + +```java +pq = new TreeSet<>((a, b) -> { + int distA = distance(a); + int distB = distance(b); + // If the lengths are equal, compare the indexes + if (distA == distB) + return b[0] - a[0]; + return distA - distB; +}); +``` + +Beside that, we also need to change the `distance` function. Instead of calculating the length between two endpoints of a line segment, we need to let it calculate the length between the midpoint and endpoint of the line segment. + +```java +private int distance(int[] intv) { + int x = intv[0]; + int y = intv[1]; + if (x == -1) return y; + if (y == N) return N - 1 - x; + // Length between midpoint and endpoint + return (y - x) / 2; +} +``` + +![](../pictures/seat_scheduling/5.jpg) + +In this way, the values of `distance` , `[0,4]` and `[4,9]` are equal. The algorithm will compare the indexes of the two, and take smaller line segments for segmentation. So far, this algorithm problem has been solved perfectly. + +### 4. Final summary + +​ The problem mentioned in this article is not so difficult, although it seems that there is a lot of code. The core issue is to examine the understanding and use of ordered data structures + +​ To deal with dynamic problems, we usually use ordered data structures, such as balanced binary search tree and binary heap, which have similar time complexity. But the former supports more operations. + +​ Since balanced binary search tree is so easy to use, why use binary heap? The reason given by me, is that, the bottom layer of binary heap is array, which is easier to implement. See the old article 「detailed explanation of binary heap」 to learn more detail. Try to make a Red Black Tree? It not only has more complex operation, but also costs more space. Of course, to solve the specific problems, we should choose the appropriate data structure with specific analysis. + +​ I hope this article can be helpful for you. + + + diff --git a/interview/TheLongestPalindromicSubstring.md b/interview/TheLongestPalindromicSubstring.md new file mode 100644 index 0000000000..29b27e9c87 --- /dev/null +++ b/interview/TheLongestPalindromicSubstring.md @@ -0,0 +1,119 @@ +# How to find **The Longest Palindromic Substring** + +**Author: [labuladong](https://github.com/labuladong)** + +**Translator: [Lrc123](https://github.com/Lrc123)** + +Palindrome questions are very common in the interview, this article provides some insights about palindromic problem. + +To specific : +> A palindrome is a word, number, phrase, or other sequence of characters which reads the same backward as forward, such as madam, racecar. +[reference](https://en.wikipedia.org/wiki/Palindrome) + +For example: `aba` and `abba` are both palindromic, because they are symetric strings, that you can read each of them in reversed order, and you can just get a same string. + +Notice: palindrome string could be in either odd length or even length, a good solution would be **double pointers**. Next, I'll show you how **doulbe pointers** work in a real leetcode problem. + +![](../pictures/palindrome/example.png) + +```cpp +string longestPalindrome(string s) {} +``` + +### 1. Thinking + +Given a string s, find the longest palindromic substring in s. + +A very interesting pespective: 1. Reversing s in to s' 2. Finding the longest common substring. + +For instance, a string `abacd`, a reversed version is `dcaba`, and the longest common string is `aba`, seemingly perfect. + +However, it would be wrong when we apply to `aacxycaa`, which a reversed version would be `aacyxcaa`, then the longest common substring turns out to be `aac`. But, what we need should be `aa`. + +Although this way has its faults, **we can still get some inspirations that we can transform a problem seemingly hard into another simpler problem that we can understand easier.** + +Now, **the double pointers** + +**Core idea: start a scanner from the mid point of the string** +we represent the idea into pseudo code: + +```python +for 0 <= i < len(s): + find a palindrome that set s[i] as its mid point + update the answer +``` + +When the length of string is even, for instance: `abba`, the code above would not work. + +So, a better version here : +```python +for 0 <= i < len(s): + find a palindrome that set s[i] as its mid point + find a palindrome that set s[i] and s[i + 1] as its mid point + update the answer +``` + +PS: you may encounter some problems like : outofIndex error. Don't worry, we'll fix them later. + +### 2. Implementation + +a function implementation: + +```cpp +string palindrome(string& s, int l, int r) { + // avoid outOfIndex error + while (l >= 0 && r < s.size() + && s[l] == s[r]) { + // scanning toward both directions + l--; r++; + } + // return a palindrome that set s[l] and s[r] as mid point + return s.substr(l + 1, r - l - 1); +} +``` + +Why we need both pointer `l` and pointer `r`? **In this way, we can handle palindrome strings in odd and even length** + +```python +for 0 <= i < len(s): + # find a palindrome that set s[i] as its mid + palindrome(s, i, i) + # find a palindrome that set s[i] and s[i + 1] as its mid + palindrome(s, i, i + 1) + update the answer +``` + +Completed code solution: + +```cpp +string longestPalindrome(string s) { + string res; + for (int i = 0; i < s.size(); i++) { + // find a palindrome that set s[i] as its mid + string s1 = palindrome(s, i, i); + // find a palindrome that set s[i] and s[i + 1] as its mid + string s2 = palindrome(s, i, i + 1); + // res = longest(res, s1, s2) + res = res.size() > s1.size() ? res : s1; + res = res.size() > s2.size() ? res : s2; + } + return res; +} +``` + + +Thus, this leetcode problem is solved. Now, we get: + +Time complexity: O(N^2) + +Space complexity: O(1) + + +By the way, a dynamic programming approach can also work in this problem in a same time complexity. However, we need at least O(N^2) spaces to store DP table. +Therefore, in this problem, dp approach is not the best solution. + +In addition, **Manacher's Algorithm** requires only O(N) time complexity. You readers can search it through the Internet by your own interests. It should be very interesting. + + +**Stick to original high-quality articles, and strive to make clear the algorithm problems. Welcome to follow my Wechat official account "labuladong" for the latest articles.** + diff --git a/interview/Trapping_Rain_Water.md b/interview/Trapping_Rain_Water.md new file mode 100644 index 0000000000..d4be0c1332 --- /dev/null +++ b/interview/Trapping_Rain_Water.md @@ -0,0 +1,185 @@ +# Detailed analysis of the trapping rain water problem + +**Translator: [Iruze](https://github.com/Iruze)** + +**Author: [labuladong](https://github.com/labuladong)** + +The trapping rain water problem is very interesting and preforms frequently in interviews. So this paper will show how to solve the problem and explain how to optimize the solution step by step. + +First of all, let's have a view on the problem: + +![](../pictures/trapping_rain_water/title.jpg) + +In a word, an array represents an elevation map and hope you calculate how much rain water the elevation map can hold at most. + +```java +int trap(int[] height); +``` + +Now I will explain three approaches from shallow to deep: Brute force -> Using memorandum -> Using two pointers, and finally solve the problem with O(1) space complexity and O(N) time complexity. + +### I. Core idea + +When I saw this problem for the first time, I had no idea at all. I believe that many friends have the same experience. As for this kind of problem, we should not consider from the whole, but from the part; Just as the previous articles that talk about how to handle the string problem, don't consider how to handle the whole string. Instead, you should focus on how to handle each character among the string. + +Therefore, we find that the thought of this problem is sample. Specifically, just for the position `i` as below, how much water can it hold? + +![](../pictures/trapping_rain_water/0.jpg) + +Position `i` occupies 2 grids for holding water. Why it happens to hold 2 grids of water? Because the height of `height[i]` is 0, and `height[i]` can hold up to 2 grids of water, therefore there exists 2 - 0 = 2. + +But why the position `i` can hold 2 grids of water at most? Because the height of water column at position `i` depends on both the hightest water column on the left and the highest water column on the right. We describe the height of the two highest water columns as `l_max` and `r_max` respectively. **Thus the height at position `i` is `min(l_max, r_max)`**. + +Further more, as for the position `i`, how much water it holds can be demonstrated as: +```python +water[i] = min( + # the highest column on the left + max(height[0..i]), + # the highest column on the right + max(height[i..end]) + ) - height[i] +``` + +![](../pictures/trapping_rain_water/1.jpg) + +![](../pictures/trapping_rain_water/2.jpg) + +This is the core idea of the problem, so we can program a simple brute approach: + +```cpp +int trap(vector& height) { + int n = height.size(); + int ans = 0; + for (int i = 1; i < n - 1; i++) { + int l_max = 0, r_max = 0; + // find the highest column on the right + for (int j = i; j < n; j++) + r_max = max(r_max, height[j]); + // find the highest column on the right + for (int j = i; j >= 0; j--) + l_max = max(l_max, height[j]); + // if the position i itself is the highest column + // l_max == r_max == height[i] + ans += min(l_max, r_max) - height[i]; + } + return ans; +} +``` + +According to the previous thought, the above approach seems very direct and brute. The time complexity is O(N^2) and the space complexity is O(1). However, it is obvious that the way of calculating `r_max` and `l_max` is very clumsy, which the memorandum is generally introduced to optimize the way. + +### II. Memorandum Optimization + +In the previous brute approach, the `r_max` and `l_max` are calculated at every position `i`. So we can cache that calculation results, which avoids the stupid traversal at every time. Thus the time complexity will reasonably decline. + +Here two arrays `r_max` and `l_max` are used to act the memo. `l_max[i]` represents the highest column on the left of position `i` and `r_max[i]` represents the highest column on the right of position `i`. These two arrays are calculated in advance to avoid duplicated calculation. + +```cpp +int trap(vector& height) { + if (height.empty()) return 0; + int n = height.size(); + int ans = 0; + // arrays act the memo + vector l_max(n), r_max(n); + // initialize base case + l_max[0] = height[0]; + r_max[n - 1] = height[n - 1]; + // calculate l_max from left to right + for (int i = 1; i < n; i++) + l_max[i] = max(height[i], l_max[i - 1]); + // calculate r_max from right to left + for (int i = n - 2; i >= 0; i--) + r_max[i] = max(height[i], r_max[i + 1]); + // calculate the final result + for (int i = 1; i < n - 1; i++) + ans += min(l_max[i], r_max[i]) - height[i]; + return ans; +} +``` + +Actually, the memo optimization has not much difference from the above brute approach, except that it avoids repeat calculation and reduces the time complexity to O(N). Although time complexity O(N) is already the best, but the space complexity is still O(N). So let's look at a more subtle approach that can reduce the space complexity to O(1). + +### III. Two pointers + +The thought of this approach is exactly the same, but it is very ingenious in the way of implementation. We won't use the memo to cache calculation results in advance this time. Instead, we use two pointers to calculate during traversal and the space complexity will decline as a result. + +First, look at some of the code: + +```cpp +int trap(vector& height) { + int n = height.size(); + int left = 0, right = n - 1; + + int l_max = height[0]; + int r_max = height[n - 1]; + + while (left <= right) { + l_max = max(l_max, height[left]); + r_max = max(r_max, height[right]); + left++; right--; + } +} +``` + +In the above code, what's the meaning of `l_max` and `r_max` respectively? + +It is easy to understand that **`l_max` represents the highest column among `height[0..left]` and `r_max` represents the highest column among `height[right..end]`**. + +With that in mind, look directly at the approach: + +```cpp +int trap(vector& height) { + if (height.empty()) return 0; + int n = height.size(); + 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 = max(l_max, height[left]); + r_max = max(r_max, height[right]); + + // ans += min(l_max, r_max) - height[i] + if (l_max < r_max) { + ans += l_max - height[left]; + left++; + } else { + ans += r_max - height[right]; + right--; + } + } + return ans; +} +``` + +The core idea of the approach is the same as before, which is just like old wine in new bottle. However, a careful reader may find that the approach is slightly different in details from the previous ones: + +In the memo optimization approach, `l_max[i]` and `r_max[i]` represent the highest column of `height[0..i]` and `height[i..end]` respectively. + +```cpp +ans += min(l_max[i], r_max[i]) - height[i]; +``` + +![](../pictures/trapping_rain_water/3.jpg) + +But in two pointers approach, `l_max` and `r_max` represent the highest column of `height[0..left]` and `height[right..end]` respectively. Take the below code as an example: + +```cpp +if (l_max < r_max) { + ans += l_max - height[left]; + left++; +} +``` + +![](../pictures/trapping_rain_water/4.jpg) + +At this time, `l_max` represents the highest column on the left of `left` pointer, but `r_max` is not always the highest column on the right of `left` pointer. Under the circumstances, can this approach really get the right answer? + +In fact, we need to think about it in this way: we just focus on `min(l_max, r_max)`. In the above elevation map, we have known `l_max < r_max`, so it is doesn't matter whether the `r_max` is the highest column on the right. The key is that water capacity in `height[i]` just depends on `l_max`. + +![](../pictures/trapping_rain_water/5.jpg) + +***Tip:*** +Adhere to the original high-quality articles and strive to make the algorithm clear. Welcome to my Wechat official account: **labuladong** to get the latest articles. diff --git a/interview/UsingBinarySearchAlgorithm.md b/interview/UsingBinarySearchAlgorithm.md new file mode 100644 index 0000000000..02ce274310 --- /dev/null +++ b/interview/UsingBinarySearchAlgorithm.md @@ -0,0 +1,182 @@ +# How to use a binary search algorithm + +**Translator: [Dong Wang](https://github.com/Coder2Programmer)** + +**Author: [labuladong](https://github.com/labuladong)** + +In what scenarios can binary search be used? + +The most common example is in textbook, that is, searching for the index of a given target value in **an ordered array**. Moreover, if the target values is duplicated, the modified binary search can return the left boundary or right boundary index of the target value. + +PS: The three binary search algorithms mentioned above are explained in detail in the previous [Binary Search Detailed Explanation](../think_like_computer/BinarySearch.md). It is strongly recommended if you haven't read it. + +Putting aside the boring ordered array, how can binary search be applied to practical algorithm problems? When the search space is in order, you can perform *pruning* through binary search, greatly improving efficiency. + +Talk is cheap, show you the specific *Koko eating banana* problem. + +### 1. Problem analysis + +Koko loves to eat bananas. There are `N` piles of bananas, the `i`-th pile has `piles[i]` bananas. The guards have gone and will come back in `H` hours. + +Koko can decide her bananas-per-hour eating speed of `K`. Each hour, she chooses some pile of bananas, and eats `K` bananas from that pile. If the pile has less than `K` bananas, she eats all of them instead, and won't eat any more bananas during this hour. + +Koko likes to eat slowly, but still wants to finish eating all the bananas before the guards come back. + +Return the minimum integer `K` such that she can eat all the bananas within `H` hours. + +

Example 1:

+
+Input: piles = [3,6,7,11], H = 8
+Output: 4
+
+ +

Example 2:

+
+Input: piles = [30,11,23,4,20], H = 5
+Output: 30
+
+ +In other words, Koko eats up to a bunch of bananas every hour. +1. If she can't, she can eat them until the next hour. +2. If she has an appetite after eating this bunch, she will only eat the next bunch until the next hour. + +Under this condition, let us determine **the minimum speed** Koko eats bananas. + +Given this scenario directly, can you think of where you can use the binary search algorithm? If you haven't seen a similar problem, it's hard to associate this problem with binary search. + +So let's put aside the binary search algorithm and think about how to solve the problem violently? + +First of all, the algorithm requires *minimum speed of eating bananas in `H` hours*. We might as well call it `speed`. What is the maximum possible `speed`? What is the minimum possible `speed`? + +Obviously the minimum is 1 and the maximum is `max(piles)`, because you can only eat a bunch of bananas in an hour. Then the brute force solution is very simple. As long as it starts from 1 and exhausts to `max(piles)`, once it is found that a certain value can eat all bananas in `H` hours, this value is the minimum speed. + +```java +int minEatingSpeed(int[] piles, int H) { + // the maximum value of piles + int max = getMax(piles); + for (int speed = 1; speed < max; speed++) { + // wherher can finish eating banana in H hours at speed + if (canFinish(piles, speed, H)) + return speed; + } + return max; +} +``` + +Note that this for loop is a linear search in **continuous space, which is the flag that binary search can work**. Because we require the minimum speed, we can use a **binary search algorithm to find out the left boundary** to replace the linear search to improve efficiency. + +```java +int minEatingSpeed(int[] piles, int H) { + // apply the algorithms framework for searching the left boundary + int left = 1, right = getMax(piles) + 1; + while (left < right) { + // prevent overflow + int mid = left + (right - left) / 2; + if (canFinish(piles, mid, H)) { + right = mid; + } else { + left = mid + 1; + } + } + return left; +} +``` + +PS: If you have questions about the details of this binary search algorithm, it is recommended to look at the algorithm template on the left boundary of the search for [Binary Search Detailed Explanation](../think_like_computer/BinarySearch.md) in the previous article. + +The remaining helper functions are also very simple and can be disassembled step by step. + + +```java +// Time complexity O(N) +boolean canFinish(int[] piles, int speed, int H) { + int time = 0; + for (int n : piles) { + time += timeOf(n, speed); + } + return time <= H; +} + +int timeOf(int n, int speed) { + return (n / speed) + ((n % speed > 0) ? 1 : 0); +} + +int getMax(int[] piles) { + int max = 0; + for (int n : piles) + max = Math.max(n, max); + return max; +} +``` + +So far, with the help of the binary search, the time complexity of the algorithm is O(NlogN). + +### 2. Extension + +Similarly, look at a transportation problem again. + +The `i`-th package on the conveyor belt has a weight of `weights[i]`. Each day, we load the ship with packages on the conveyor belt (in the order given by weights). We may not load more weight than the maximum weight capacity of the ship. + +Return the least weight capacity of the ship that will result in all the packages on the conveyor belt being shipped within `D` days. + +

Example 1:

+
+Input: weights = [1,2,3,4,5,6,7,8,9,10], D = 5
+Output: 15
+Explanation: 
+A ship capacity of 15 is the minimum to ship all the packages in 5 days like this:
+1st day: 1, 2, 3, 4, 5
+2nd day: 6, 7
+3rd day: 8
+4th day: 9
+5th day: 10
+
+Note that the cargo must be shipped in the order given, so using a ship of capacity 14 and splitting the packages into parts like (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) is not allowed. 
+
+ +To transport all the goods within `D` days, the goods are inseparable. How to determine the minimum load for transportation(hereinafter referred to as `cap`)? + +In fact, it is essentially the same problem as Koko eating bananas. First, determine the minimum and maximum values of `cap` as `max(weights)` and `sum(weights)`. + +We require **minimum load**, so a binary search algorithm that searches the left boundary can be used to optimize the linear search. + +```java +// find the left boundary using binary search +int shipWithinDays(int[] weights, int D) { + // minimum possible load + int left = getMax(weights); + // maximum possible load + 1 + int right = getSum(weights) + 1; + while (left < right) { + int mid = left + (right - left) / 2; + if (canFinish(weights, D, mid)) { + right = mid; + } else { + left = mid + 1; + } + } + return left; +} + +// If the load is cap, can I ship the goods within D days? +boolean canFinish(int[] w, int D, int cap) { + int i = 0; + for (int day = 0; day < D; day++) { + int maxCap = cap; + while ((maxCap -= w[i]) >= 0) { + i++; + if (i == w.length) + return true; + } + } + return false; +} +``` + +Through these two examples, do you understand the application of binary search in practical problems? + +```java +for (int i = 0; i < n; i++) + if (isOK(i)) + return ans; +``` diff --git a/interview/check_palindromic_linkedlist.md b/interview/check_palindromic_linkedlist.md new file mode 100644 index 0000000000..89fb281047 --- /dev/null +++ b/interview/check_palindromic_linkedlist.md @@ -0,0 +1,211 @@ +**Translator: [natsunoyoru97](https://github.com/natsunoyoru97)** + +**Author: [labuladong](https://github.com/labuladong)** + +There are two previous articles mentioned the problems about palindromic strings and palindromic sequences. + +The core concept to **FIND** the palindromic strings is expanding from the middle to the edges: + +```cpp +string palindrome(string& s, int l, int r) { + // to prevent the indexes from getting out of range + while (l >= 0 && r < s.size() + && s[l] == s[r]) { + // expand to two edges + l--; r++; + } + // return the longest palindromic in which the middle + // are both s[l] and s[r] + return s.substr(l + 1, r - l - 1); +} +``` + +The length of the palindromic strings can be either odd or even: when the length is odd there is only one middle pivot, and when the length is even there are two middle pivots. So the function above needs to parse the arguments `l` and `r` in. + +But to **CHECK** a palindromic string is much easier. Regardless of its length, we only need to do the double pointers trick, and move from two edges to the middle: + +```cpp +bool isPalindrome(string s) { + int left = 0, right = s.length - 1; + while (left < right) { + if (s[left] != s[right]) + return false; + left++; right--; + } + return true; +} +``` + +So the code is cleaner and much easier to understand. **The palindromic strings are SYMMETRIC so it is same to write it in normal order as in reverse order, which is the key to solve the problems of the palindromic strings.** + +We make expansion from this simple scenario, and try to solve the problem: how to check a palindromic singly linked list. + +### 1. Check A Palindromic Singly Linked List + +Given the head node of a singly linked list, and check if the values are palindromic: + +```java +/** + * The definition of nodes in a singly linked list: + * public class ListNode { + * int val; + * ListNode next; + * } + */ + +boolean isPalindrome(ListNode head); + +Input: 1->2->null +Output: false + +Input: 1->2->2->1->null +Output: true +``` + +The two pointers DON'T do the trick because we can't traverse a singly linked list in reverse. The most straightforward way is to store the existed linked list in a new linked list REVERSELY, then to compare whether these two linked lists are the same. If you have no idea about how to reverse a linked list, you can look at _[Reverse Part of a Linked List via Recusion](https://github.com/labuladong/fucking-algorithm/blob/english/data_structure/reverse_part_of_a_linked_list_via_recursion.md)_. + +But **similar to the postorder traversal in a binary tree, we can traverse a linked list reversely without doing the actual reverse**. + +We are familiar with the ways to traverse a binary tree: + +```java +void traverse(TreeNode root) { + // code to traverse in preorder + traverse(root.left); + // code to traverse in inorder + traverse(root.right); + // code to traverse in postorder +} +``` + +As mentioned in _The Thinking Patterns in Data Structure_, the linked list is recursive and it is the derivation of the trees ADT. Thus, **the linked list also has preorder traversal and postorder traversal**: + +```java +void traverse(ListNode head) { + // code to traverse in preorder + traverse(head.next); + // code to traverse in postorder +} +``` + +How do we apply such a pattern? If I want to print the value of `val` in a linked list in normal order, I will write the code in the position of preorder traversal; Meanwhile, if I want to print the value in reverse order, I will make the code in the position of postorder traversal: + +```java +/* print the values in a linked list reversely */ +void traverse(ListNode head) { + if (head == null) return; + traverse(head.next); + // code to traverse in postorder + print(head.val); +} +``` + +We can also make a slight modification to imitate the two pointers to check the palindromes: + +```java +// The left pointer +ListNode left; + +boolean isPalindrome(ListNode head) { + left = head; + return traverse(head); +} + +boolean traverse(ListNode right) { + if (right == null) return true; + boolean res = traverse(right.next); + // code to traverse in postorder + res = res && (right.val == left.val); + left = left.next; + return res; +} +``` + +What is the essence of this way? It is all about **pushing the nodes in the linked list into a stack and then popping them out. At this time the elements are in reverse.** What we make in use is the queues and stacks in recursion. + +![](../pictures/palindromic_linkedlist/1.gif) + +Of course, both the time complexity and the space complexity are O(N) no matter you construct a reversed linked list or use postorder traversal. Can we solve it without using extra spaces? + +### 2. Optimizing the Space Complexity + +Here is how to optimize: + +**2.1 Find the node in the middle by the fast and slow pointers**: + +```java +ListNode slow, fast; +slow = fast = head; +while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; +} +// the slow pointer now points to the middle point +``` + +![](../pictures/palindromic_linkedlist/1.jpg) + +**2.2 If the `fast` pointer doesn't point to `null`, the length of this linked list is odd, which means the `slow` pointer needs to forward one more step**: + +```java +if (fast != null) + slow = slow.next; +``` + +![](../pictures/palindromic_linkedlist/2.jpg) + +**2.3 Reverse the right half of the linked list and compare palindromes**: + +```java +ListNode left = head; +ListNode right = reverse(slow); + +while (right != null) { + if (left.val != right.val) + return false; + left = left.next; + right = right.next; +} +return true; +``` + +![](../pictures/palindromic_linkedlist/3.jpg) + +Till now, we only need to merge these 3 parts of code to cope with this problem. The `reverse` function is easy to build: + +```java +ListNode reverse(ListNode head) { + ListNode pre = null, cur = head; + while (cur != null) { + ListNode next = cur.next; + cur.next = pre; + pre = cur; + cur = next; + } + return pre; +} +``` + +![](../pictures/kgroup/8.gif) + +The time complexity of this algorithm is O(N) and the space complexity is O(1), which is fully optimized. + +I know some readers may ask: It is efficient but it broke the structure of the linked list. Can we remain the structure in origin? + +It is easy to deal with, and the key is to get the positions of the pointers `p, q`: + +![](../pictures/palindromic_linkedlist/4.jpg) + +We only need to add one line before the return function to get the original order of the linked list. + +```java +p.next = reverse(q); +``` + +To avoid being wordy, my words will stop here. Readers can have a try by yourselves. + +### 3. Summing Up + +First, by extending **from the middle to the two edges** to FIND the palindromic strings, meanwhile by shrinking **from the middle to the two edges** to CHECK the palindromic strings. Traversing in reverse doesn't work for a singly linked list, the two alternatives are: to construct a new reversed linked list instead, or to apply the stack ADT. + +Due to the feature of palindromes (they are SYMMETRIC), we can optimize the space complexity to O(1) by only **reversing half of the linked list**. \ No newline at end of file diff --git a/interview/findSebesquenceWithBinarySearch.md b/interview/findSebesquenceWithBinarySearch.md new file mode 100644 index 0000000000..2fd73e0a36 --- /dev/null +++ b/interview/findSebesquenceWithBinarySearch.md @@ -0,0 +1,139 @@ +# Subsequence Using Binary Search + +**Translator: [youyun](https://github.com/youyun)** + +**Author: [labuladong](https://github.com/labuladong)** + +Binary search is not hard to understand. It is rather hard to apply. Sometimes, you can't even link a question with binary search. In another article [Longest Increasing Subsequence](../dynamic_programming/动态规划设计:最长递增子序列.md), we could even apply binary search in a poker game. + +Let's discuss another interesting question that we can use binary search: how to determine if a given string `s` is subsequence of another string `t` (assume `s` is much shorter as compared to `t`)? Look at the two examples below: + +> s = "abc", t = "**a**h**b**gd**c**", return true. + +> s = "axc", t = "ahbgdc", return false. + +This is a straightforward question which looks simple. But can you relate this with binary search? + +### 1. Problem Analysis + +Here is an intuitive solution: + +```cpp +bool isSubsequence(string s, string t) { + int i = 0, j = 0; + while (i < s.size() && j < t.size()) { + if (s[i] == t[j]) i++; + j++; + } + return i == s.size(); +} +``` + +The idea is to use two pointers `i, j` to point to `s, t` respectively. While moving forward, try to match the characters: + +![gif](../pictures/subsequence/1.gif) + +Some people may claim this is the optimal solution, given the time complexity is O(N) while N is the length of `t`. + +In fact, this solution is good enough for this problem alone. __However, there is a follow-up__: + +Given a list of string `s1,s2,...` and a string `t`, determine if each string `s` is a subsequence of `t` (assume each `s` is much shorter as compared to `t`). + +```java +boolean[] isSubsequence(String[] sn, String t); +``` + +We can still apply the same logic inside a `for` loop. However, the time complexity for each `s` is still O(N). If binary search is applied, the time complexity can be reduced to O(MlogN). Since `N >> M`, the efficiency will be improved significantly. + +### 2. Using Binary Search + +To begin with binary search, we need to pre-process `t` by storing the indices of each character in a dictionary `index`. + +```java +int m = s.length(), n = t.length(); +ArrayList[] index = new ArrayList[256]; +// record down the indices of each character in t +for (int i = 0; i < n; i++) { + char c = t.charAt(i); + if (index[c] == null) + index[c] = new ArrayList<>(); + index[c].add(i); +} +``` + +![](../pictures/subsequence/2.jpg) + +Refer to the diagram below, since we've matched "ab", the next one to be matched should be "c": + +![](../pictures/subsequence/1.jpg) + +If we apply the first solution, we need to traverse linearly using `j` to find "c". With the information in `index`, __we can use binary search to find an index that is greater than `j` in `index["c"]`__. In the diagram above, we need to find an index from `[0, 2, 6]` that is greater than 4: + +![](../pictures/subsequence/3.jpg) + +In this way, we can directly get the index of next "c". The problem becomes how to find the smallest index that is greater than 4? We can use binary search to find the left boundary. + +### 3. More about Binary Search + +In another article [Detailed Binary Search](../think_like_computer/DetailedBinarySearch.md), we discussed in details how to implement binary search in 3 different ways. When we use binary search to return the index of target `val` to find __the left boundary__, there is a special property: + +__When `val` does not exist, the index returned is the index of the smallest value which is greater than `val`__. + +It means that when we try to find element 2 in array `[0,1,3,4]`, the algorithm will return index 2, where element 3 is located. And element 3 is the smallest element that is greater than 2 in this array. Hence, we can use binary search to avoid linear traversal. + +```java +// binary search to find the left boundary +int left_bound(ArrayList arr, int tar) { + int lo = 0, hi = arr.size(); + while (lo < hi) { + int mid = lo + (hi - lo) / 2; + if (tar > arr.get(mid)) { + lo = mid + 1; + } else { + hi = mid; + } + } + return lo; +} +``` + +The binary search above is to find the left boundary. Its details can be found in [Detailed Binary Search](../think_like_computer/DetailedBinarySearch.md). Let's apply it. + +### 4. Implementation + +We take a single string `s` as an example for the case of multiple strings. The part of pre-processing can be extracted out. + +```java +boolean isSubsequence(String s, String t) { + int m = s.length(), n = t.length(); + // pre-process t + ArrayList[] index = new ArrayList[256]; + for (int i = 0; i < n; i++) { + char c = t.charAt(i); + if (index[c] == null) + index[c] = new ArrayList<>(); + index[c].add(i); + } + + // the pointer in t + int j = 0; + // find s[i] using index + for (int i = 0; i < m; i++) { + char c = s.charAt(i); + // character c does not exist in t + if (index[c] == null) return false; + int pos = left_bound(index[c], j); + // c is not found in the binary search interval + if (pos == index[c].size()) return false; + // increment pointer j + j = index[c].get(pos) + 1; + } + return true; +} +``` + +The gif below illustrates how the algorithm executes: + +![](../pictures/subsequence/2.gif) + +We can see that the efficiency can be significantly improved using binary search. diff --git "a/interview/koko\345\201\267\351\246\231\350\225\211.md" "b/interview/koko\345\201\267\351\246\231\350\225\211.md" deleted file mode 100644 index 03a4583911..0000000000 --- "a/interview/koko\345\201\267\351\246\231\350\225\211.md" +++ /dev/null @@ -1,142 +0,0 @@ -# 如何运用二分查找算法 - -二分查找到底有能运用在哪里? - -最常见的就是教科书上的例子,在**有序数组**中搜索给定的某个目标值的索引。再推广一点,如果目标值存在重复,修改版的二分查找可以返回目标值的左侧边界索引或者右侧边界索引。 - -PS:以上提到的三种二分查找算法形式在前文「二分查找详解」有代码详解,如果没看过强烈建议看看。 - -抛开有序数组这个枯燥的数据结构,二分查找如何运用到实际的算法问题中呢?当搜索空间有序的时候,就可以通过二分搜索「剪枝」,大幅提升效率。 - -说起来玄乎得很,本文先用一个具体的「Koko 吃香蕉」的问题来举个例子。 - -### 一、问题分析 - -![](../pictures/二分应用/title1.png) - -也就是说,Koko 每小时最多吃一堆香蕉,如果吃不下的话留到下一小时再吃;如果吃完了这一堆还有胃口,也只会等到下一小时才会吃下一堆。在这个条件下,让我们确定 Koko 吃香蕉的**最小速度**(根/小时)。 - -如果直接给你这个情景,你能想到哪里能用到二分查找算法吗?如果没有见过类似的问题,恐怕是很难把这个问题和二分查找联系起来的。 - -那么我们先抛开二分查找技巧,想想如何暴力解决这个问题呢? - -首先,算法要求的是「`H` 小时内吃完香蕉的最小速度」,我们不妨称为 `speed`,请问 `speed` 最大可能为多少,最少可能为多少呢? - -显然最少为 1,最大为 `max(piles)`,因为一小时最多只能吃一堆香蕉。那么暴力解法就很简单了,只要从 1 开始穷举到 `max(piles)`,一旦发现发现某个值可以在 `H` 小时内吃完所有香蕉,这个值就是最小速度: - -```java -int minEatingSpeed(int[] piles, int H) { - // piles 数组的最大值 - int max = getMax(piles); - for (int speed = 1; speed < max; speed++) { - // 以 speed 是否能在 H 小时内吃完香蕉 - if (canFinish(piles, speed, H)) - return speed; - } - return max; -} -``` - -注意这个 for 循环,就是在**连续的空间线性搜索,这就是二分查找可以发挥作用的标志**。由于我们要求的是最小速度,所以可以用一个**搜索左侧边界的二分查找**来代替线性搜索,提升效率: - -```java -int minEatingSpeed(int[] piles, int H) { - // 套用搜索左侧边界的算法框架 - int left = 1, right = getMax(piles) + 1; - while (left < right) { - // 防止溢出 - int mid = left + (right - left) / 2; - if (canFinish(piles, mid, H)) { - right = mid; - } else { - left = mid + 1; - } - } - return left; -} -``` - -PS:如果对于这个二分查找算法的细节问题有疑问,建议看下前文「二分查找详解」搜索左侧边界的算法模板,这里不展开了。 - -剩下的辅助函数也很简单,可以一步步拆解实现: - -```java -// 时间复杂度 O(N) -boolean canFinish(int[] piles, int speed, int H) { - int time = 0; - for (int n : piles) { - time += timeOf(n, speed); - } - return time <= H; -} - -int timeOf(int n, int speed) { - return (n / speed) + ((n % speed > 0) ? 1 : 0); -} - -int getMax(int[] piles) { - int max = 0; - for (int n : piles) - max = Math.max(n, max); - return max; -} -``` - -至此,借助二分查找技巧,算法的时间复杂度为 O(NlogN)。 - -### 二、扩展延伸 - -类似的,再看一道运输问题: - -![](../pictures/二分应用/title2.png) - -要在 `D` 天内运输完所有货物,货物不可分割,如何确定运输的最小载重呢(下文称为 `cap`)? - -其实本质上和 Koko 吃香蕉的问题一样的,首先确定 `cap` 的最小值和最大值分别为 `max(weights)` 和 `sum(weights)`。 - -我们要求**最小载重**,所以可以用搜索左侧边界的二分查找算法优化线性搜索: - -```java -// 寻找左侧边界的二分查找 -int shipWithinDays(int[] weights, int D) { - // 载重可能的最小值 - int left = getMax(weights); - // 载重可能的最大值 + 1 - int right = getSum(weights) + 1; - while (left < right) { - int mid = left + (right - left) / 2; - if (canFinish(weights, D, mid)) { - right = mid; - } else { - left = mid + 1; - } - } - return left; -} - -// 如果载重为 cap,是否能在 D 天内运完货物? -boolean canFinish(int[] w, int D, int cap) { - int i = 0; - for (int day = 0; day < D; day++) { - int maxCap = cap; - while ((maxCap -= w[i]) >= 0) { - i++; - if (i == w.length) - return true; - } - } - return false; -} -``` - -通过这两个例子,你是否明白了二分查找在实际问题中的应用? - -```java -for (int i = 0; i < n; i++) - if (isOK(i)) - return ans; -``` - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/interview/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" "b/interview/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" deleted file mode 100644 index 2c96477742..0000000000 --- "a/interview/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" +++ /dev/null @@ -1,131 +0,0 @@ -# 如何k个一组反转链表 - -之前的文章「递归反转链表的一部分」讲了如何递归地反转一部分链表,有读者就问如何迭代地反转链表,这篇文章解决的问题也需要反转链表的函数,我们不妨就用迭代方式来解决。 - -本文要解决「K 个一组反转链表」,不难理解: - -![](../pictures/kgroup/title.png) - -这个问题经常在面经中看到,而且 LeetCode 上难度是 Hard,它真的有那么难吗? - -对于基本数据结构的算法问题其实都不难,只要结合特点一点点拆解分析,一般都没啥难点。下面我们就来拆解一下这个问题。 - -### 一、分析问题 - -首先,前文[学习数据结构的框架思维](../算法思维系列/学习数据结构和算法的框架思维.md)提到过,链表是一种兼具递归和迭代性质的数据结构,认真思考一下可以发现**这个问题具有递归性质**。 - -什么叫递归性质?直接上图理解,比如说我们对这个链表调用 `reverseKGroup(head, 2)`,即以 2 个节点为一组反转链表: - -![](../pictures/kgroup/1.jpg) - -如果我设法把前 2 个节点反转,那么后面的那些节点怎么处理?后面的这些节点也是一条链表,而且规模(长度)比原来这条链表小,这就叫**子问题**。 - -![](../pictures/kgroup/2.jpg) - -我们可以直接递归调用 `reverseKGroup(cur, 2)`,因为子问题和原问题的结构完全相同,这就是所谓的递归性质。 - -发现了递归性质,就可以得到大致的算法流程: - -**1、先反转以 `head` 开头的 `k` 个元素**。 - -![](../pictures/kgroup/3.jpg) - -**2、将第 `k + 1` 个元素作为 `head` 递归调用 `reverseKGroup` 函数**。 - -![](../pictures/kgroup/4.jpg) - -**3、将上述两个过程的结果连接起来**。 - -![](../pictures/kgroup/5.jpg) - -整体思路就是这样了,最后一点值得注意的是,递归函数都有个 base case,对于这个问题是什么呢? - -题目说了,如果最后的元素不足 `k` 个,就保持不变。这就是 base case,待会会在代码里体现。 - -### 二、代码实现 - -首先,我们要实现一个 `reverse` 函数反转一个区间之内的元素。在此之前我们再简化一下,给定链表头结点,如何反转整个链表? - -```java -// 反转以 a 为头结点的链表 -ListNode reverse(ListNode a) { - ListNode pre, cur, nxt; - pre = null; cur = a; nxt = a; - while (cur != null) { - nxt = cur.next; - // 逐个结点反转 - cur.next = pre; - // 更新指针位置 - pre = cur; - cur = nxt; - } - // 返回反转后的头结点 - return pre; -} -``` - -![](../pictures/kgroup/8.gif) - -这次使用迭代思路来实现的,借助动画理解应该很容易。 - -「反转以 `a` 为头结点的链表」其实就是「反转 `a` 到 null 之间的结点」,那么如果让你「反转 `a` 到 `b` 之间的结点」,你会不会? - -只要更改函数签名,并把上面的代码中 `null` 改成 `b` 即可: - -```java -/** 反转区间 [a, b) 的元素,注意是左闭右开 */ -ListNode reverse(ListNode a, ListNode b) { - ListNode pre, cur, nxt; - pre = null; cur = a; nxt = a; - // while 终止的条件改一下就行了 - while (cur != b) { - nxt = cur.next; - cur.next = pre; - pre = cur; - cur = nxt; - } - // 返回反转后的头结点 - return pre; -} -``` - -现在我们迭代实现了反转部分链表的功能,接下来就按照之前的逻辑编写 `reverseKGroup` 函数即可: - -```java -ListNode reverseKGroup(ListNode head, int k) { - if (head == null) return null; - // 区间 [a, b) 包含 k 个待反转元素 - ListNode a, b; - a = b = head; - for (int i = 0; i < k; i++) { - // 不足 k 个,不需要反转,base case - if (b == null) return head; - b = b.next; - } - // 反转前 k 个元素 - ListNode newHead = reverse(a, b); - // 递归反转后续链表并连接起来 - a.next = reverseKGroup(b, k); - return newHead; -} -``` - -解释一下 `for` 循环之后的几句代码,注意 `reverse` 函数是反转区间 `[a, b)`,所以情形是这样的: - -![](../pictures/kgroup/6.jpg) - -递归部分就不展开了,整个函数递归完成之后就是这个结果,完全符合题意: - -![](../pictures/kgroup/7.jpg) - -### 三、最后说两句 - -从阅读量上看,基本数据结构相关的算法文章看的人都不多,我想说这是要吃亏的。 - -大家喜欢看动态规划相关的问题,可能因为面试很常见,但就我个人理解,很多算法思想都是源于数据结构的。我们公众号的成名之作之一,「学习数据结构的框架思维」就提过,什么动规、回溯、分治算法,其实都是树的遍历,树这种结构它不就是个多叉链表吗?你能处理基本数据结构的问题,解决一般的算法问题应该也不会太费事。 - -那么如何分解问题、发现递归性质呢?这个只能多练习,也许后续可以专门写一篇文章来探讨一下,本文就到此为止吧,希望对大家有帮助! - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git a/interview/missing_elements.md b/interview/missing_elements.md new file mode 100644 index 0000000000..bb829710bc --- /dev/null +++ b/interview/missing_elements.md @@ -0,0 +1,101 @@ +# How to Find Missing Elements + +**Translator: [youyun](https://github.com/youyun)** + +**Author: [labuladong](https://github.com/labuladong)** + +I have written several articles about mind twisters. Today, let's look at another interesting question. + +The question is simple: + +![](../pictures/missing_elements/title_en.jpg) + +Given an arry of length n, the index should be in `[0, n)`. Since we have to put `n+1` number of elements from set `[0, n]`, there must be one element which can't fit. Find the missing element. + +This question is not hard. It's easy to think aabout traversing after sorting. Alternatively, using a `HashSet` to store all the existing elements, and then go through elements in `[0, n]` and loop up in the `HashSet`. Both ways can find the correct answer. + +However, the time complexity for the sorting solution is O(NlogN). The `HashSet` solution has O(N) for time complexity, but requires O(N) space complexity to store the data. + +__Third Solution: Bit Operation__ + +The XOR operation (`^`) has a special property: the result of a number XOR itself is 0, and the result of a number with 0 is itself. + +In addition, XOR operation satisfies the Exchange Law and Communicative Law. For instance: + +2 ^ 3 ^ 2 = 3 ^ (2 ^ 2) = 3 ^ 0 = 3 + +We can using these special properties to find the missing element through a smart way. For example, `nums = [0,3,1,4]`: + +![](../pictures/missing_elements/1.jpg) + +For easier understanding, let's assume the index increments by 1 (from `[0, n)` to `[0, n]`), and let each element to be placed at the index of its value: + +![](../pictures/missing_elements/2.jpg) + +After doing so, all elements and their indices will be a pair except the missing element. If we can find out index 2 is missing, we can find out the missing element subsequently. + +How to find out the missing number? __Perform XOR operations to all elements and their indices respectively. A pair of an element and its index will become 0. Only the missing element will be left.__ + +```java +int missingNumber(int[] nums) { + int n = nums.length; + int res = 0; + // XOR with the new index first + res ^= n; + // XOR with the all elements and the other indices + for (int i = 0; i < n; i++) + res ^= i ^ nums[i]; + return res; +} +``` + +![](../pictures/missing_elements/3.jpg) + +Because XOR operation fulfills the Exchange Law and the Communicative Law, all pairs of numbers will become 0, left with the missing element. + +Till now, the time complexity is O(N), and the space complexity is O(1). This is optimal. _Should we stop now?_ + +If we think so, we have become restricted by algorithms. The more knowledge we learn, the easier we might fall into stagnant mindsets. There is actually an even easier solution: __Summation of Arithmetic Progression (AP)__. + +We can interpret the question in this way: given an arithmetic progression `0, 1, 2, ..., n` with an missing element, please find out the missing one. Consequently, the number is just `sum(0,1,..n) - sum(nums)`! + +```java +int missingNumber(int[] nums) { + int n = nums.length; + // Formula: (head + tail) * n / 2 + int expect = (0 + n) * (n + 1) / 2; + + int sum = 0; + for (int x : nums) + sum += x; + return expect - sum; +``` + +As you can see, this is the simplest solution. But honestly, even I didn't think of this way. It may be hard for an experienced programmers to think in this way, but very easy for a secondary school student to come up with such a solution. + +_Should we stop now?_ + +If we think so, we might still need to pay more attention to details. When we use the formula to calculate `except`, have you thought about __Integer overflow__? If the product is too big and overflowing, the final result must be wrong. + +In the previous implementation, we subtract two sums. To avoid overflow, why not perform subtraction while summing up? Similar to our bit operation solution just now, assume `nums = [0,3,1,4]`, add an index such that elements will be paired up with indices respectively. + +![](../pictures/missing_elements/xor.png) + +Let's subtract each element from its corresponding index, and then sum up the differences, the result will be the missing element! + +```java +public int missingNumber(int[] nums) { + int n = nums.length; + int res = 0; + // Added index + res += n - 0; + // Summing up the differences between the remaining indices and elements + for (int i = 0; i < n; i++) + res += i - nums[i]; + return res; +} +``` + +Because both addition and subtraction satisfy the Exchange Law and the Communicative Law, we can always eliminate paired numbers, left with the missing one. + +_We can stop by now._ diff --git a/interview/one-line-code-puzzles.md b/interview/one-line-code-puzzles.md new file mode 100644 index 0000000000..3a0ffc9b95 --- /dev/null +++ b/interview/one-line-code-puzzles.md @@ -0,0 +1,118 @@ +# One-line Code Puzzles + +**Translator: [tommytim0515](https://github.com/tommytim0515)** + +**Author: [labuladong](https://github.com/labuladong)** + +This is my summary of three interesting "brain teaser" puzzles from the problems I solved on LeetCode. They can all be solved by algorithmic programming. However, if you think a bit more, you may find the laws of them and figure them out directly. + +### 1. Nim Game + +The game rule is that there is a heap of stones on the table for you and friends to remove. Each of you takes turns to remove the stones and can take at least one and at most three each time. The one who takes the last stone will win the game. See more on [LeetCode page](https://leetcode.com/problems/nim-game/) and [Wikipedia](https://en.wikipedia.org/wiki/Nim). + +Suppose both of you are very clever and have optimal strategies for the game, and you are the first one to take the stone. Write an algorithm to determine whether you can win the game given the number of stones in the heap. (Input a positive integer n, output true or false depending on whether you can win the game). + +For instance, there are 4 stones in total, and the output should be false. Because no matter how many stones you take (1, 2, or 3), the opponent can always take the remaining at once including the last one. You are guaranteed to lose the game. + +First of all, dynamic programming (DP) can be implemented into this problem, because you can find the repeated sub-problems. However, this method would be very complicated as it involves games played between you and your opponent, who are both clever. + +**We usually use contrarian thinking to find a solution of this kind of problems**: + +If I win the game, I need to take the remaining stones (1\\~3 stones) at once. + +How to make this situation come into being? If there are 4 stones remaining when your opponent takes the chance to pick the stones, no matter how he takes the stones, you can always win the game because there will always be 1\~3 stones remaining. + +And how to force your opponent to face the situation when there are 4 stones left? If there are 5\~7 stones remaining by the time you take your turn, you can let your opponent face 4-stone situation. + +Then how to get into a 5\~7 stones situation when you are picking? Let your opponent face 8 stones. No matter how he plans to take the stones, we can win the game because of the remaining 5\~7 stones. + +And so on, we can find out that if n is a multiple of 4, you will fall into the trap and can never win the game. The solution to this problem is very simple: + +```cpp +bool canWinNim(int n) { + // If n is a multiple of 4, then return false + // Otherwise, return true + return n % 4 != 0; +} +``` + +### 2. Stone Game + +The game rule is that you and your friend play a game with piles of stones. The piles of stone are represented by an array, ```piles```. ```pile[i]``` refers to the number of stones in the ith pile. Each turn, a player takes the entire pile of stones from either the beginning or the end of the row. And the winner is the one who gets more stones in the end. See more on [LeetCode page](https://leetcode.com/problems/stone-game/). + +**Suppose both of you are very clever and have optimal strategies for the game**, and you start first. Write an algorithm to determine whether you can win the game given the number of stones in the heap. (Input an array, pile, output true or false depending on whether you can win the game). + +Please pay attention that the number of piles of stones should be even. So both of you get the same number of piles of stones. However, the total amount of stones is odd. So you are not able to get the same number of stones and there must be a winner. + +For instance, `piles=[2, 1, 9, 5]`, you take first, you can choose 2 or 5 and you choose 2. + +`piles=[1, 9, 5]`, your opponent's turn, he or she can choose 1 or 5, and he or she chooses 5. + +`piles=[1, 9]`, your turn, you pick 9. + +Finally, your opponent has no choice but choosing 1. + +In summary, you get $2 + 9 = 11$ stones in total, your opponents gets $5 + 1 = 6$ stones. You win, the return value is true. + +As you can see that it is not always correct to choose the one with larger number of stones. Why you should choose 2 rather than 5 at the first time? Because 9 is behind 5. You will lose the game for giving the pile of 9 stones to the opponent for chasing a moment's gain. + +And that is why we need to emphasize that both the players are clever. The algorithm is also to determine whether you can win with best decisions. + +The problem also involves playing a game by the two players. It is very complicated to use "brute force" method like dynamic programming (DP). And if we think a bit deeper we will find out that + +```java +boolean stoneGame(int[] piles) { + return true; +} +``` + +Why we can write like this? There are two important conditions about the problem: the number of the pile of stones is even, while the total number of the stone is odd. These two conditions seem to increase the fairness of the game, while they indeed let it become a "leet-cutting" game. For instance, suppose the indexes of piles of stones are 1, 2, 3, 4 from start to end sequentially when `pile=[2, 1, 9, 5]`. + +If we divide these four piles of stones into two groups according to whether the index is even or not, which equals 1, 3 in a group and 2, 4 in another group. The numbers of stones of these two groups are different as the total number of stones is odd. + +As the first one to take the stones, you can decide to take all the even group or all the odd group at once. + +In the beginning, you can choose the 1st pile or the 4th pile. If you want an even group, you can take the 4th pile. So that your opponent can only choose the 1st one or the 3rd one. No matter how he takes, you can choose the 2nd pile after that. Similarly, if you choose the 1st pile which is in the odd group, your opponent can only choose 2nd or 4th pile, no matter how he chooses, you can get the 3rd pile. + +In other words, you can observe all the strategies at the first try. You can win the game by observing which group of stones is more, even or odd. Knowing this loophole, you can play a trick on your friend who doesn't know this. + +### 3. Bulb Switcher + +The description of the problem: there are n bulbs in a room and they are initially turned off. Now we need to do n operations: + +1. Flip all the lights. + +2. Flip lights with even numbers. + +3. Flip the bulb whose number is a multiple of 3 (e.g. 3, 6, 9, ... and 3 is off while 6 is on). + +For the i-th round, you toggle every i bulb. For the n-th round, you only toggle the last bulb. + +You need to find how many bulbs are on after n rounds. See more on [LeetCode page](https://leetcode.com/problems/bulb-switcher/). + +We can simulate the condition with a boolean array, then count the result. However, this method is not smart enough. The best solution is as follows: + +```java +int bulbSwitch(int n) { + return (int)Math.sqrt(n); +} +``` + +What? What does this have to do with square roots? It's actually a pretty neat solution, and it's hard to figure out if nobody tells you how to do it. + +First, because the lights are always off at the beginning, a certain light must be flipped an odd number of times if it is turned on at the end. + +Let's say we only have six lights, and we're only looking at the sixth light and it's going to take six turns, right? How many times is the switch going to be pressed for the sixth light? It's not difficult to see that its switch will be pressed at the 1st, 2nd, 3rd and 6th round. + +Why the light will be flipped at these rounds? Because $6=1\times6=2\times3$. In general, the factors come in pairs, which means that the number of times the switch is pressed is usually even, but in a special case, if there are 16 lights, how many times will the 16th light be flipped? + +$16=1\times16=2\times8=4\times4$ + +The factor 4 repeats, so the 16th light will be flipped 5 times which is odd, and now you understand the relationships to the square root, right? + +But, we're going to figure out how many lights are on at the end, and what does that mean by square root? Just think about it a little bit. + +Suppose we have 16 lights, and we take the square root of 16, which is equal to 4, and that means we're going to end up with 4 lights on. The lights are $1\times1=1$, $2\times2=4$, $3\times3=9$, and $4\times4=16$. + +Some square root of n turns out to be a decimal. However, converting them to integers is the same thing as getting all the integers smaller than a certain integer upper bound, and the square roots of these numbers are the index of the lights on at last. so just turn the square root into an integer, that's the answer to the question. + diff --git a/interview/reverse-nodes-in-k-group.md b/interview/reverse-nodes-in-k-group.md new file mode 100644 index 0000000000..260ebca7d4 --- /dev/null +++ b/interview/reverse-nodes-in-k-group.md @@ -0,0 +1,146 @@ +# How to reverse nodes in k-group + +**Translator: [Justin](https://github.com/Justin-YGG)** + +**Author: [labuladong](https://github.com/labuladong)** + +We talked about the way how to reverse the part of linked list recursively in [previous article](..算法思维系列/学习数据结构和算法的框架思维.md). Some readers may wonder how to reverse the whole linked list. We also need to use the function of linked list reversion in this article, so we might as well use the recursive method to solve it. + +The problem we need to solve is [Reverse Nodes in k-Group](https://leetcode.com/problems/reverse-nodes-in-k-group/). It's easy to understand what that means. + +> Given a linked list, reverse the nodes of a linked list k at a time and return its modified list. + +> k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k then left-out nodes in the end should remain as it is. + +> Example: + +> Given this linked list: `1->2->3->4->5` +> For k = 2, you should return: `2->1->4->3->5` +> For k = 3, you should return: `3->2->1->4->5` + +We may often encounter this problem in interview and its difficulty is **Hard** on LeetCode. But is it really so tough? + +Actually, the problems of basic data structure are not difficult. We can solve them by splitting the big problem into the small one step by step. I will show you how to do that below. + +### Analysis + +As mentioned in the previous article [the thinking framework of learning data structure](../算法思维系列/学习数据结构和算法的框架思维.md), linked list is a kind of data structure with recursion and iteration. On second thought, we can find that this problem can be solved by recursion. + +What does recursion mean? We can try to understand it with the help of the example below. + +We call `reverseKGroup(head, 2)` on the linked list so that we can reverse the linked list with 2 nodes as a group. + +![](../pictures/kgroup/1.jpg) + +What should we do next to deal with the remaining nodes after reversing the first two nodes?The remaining nodes also form a linked list but it's shorter than origin linked list.It turns out to be a subproblem of primal problem. + +![](../pictures/kgroup/2.jpg) + +We can call `reverseKGroup(cur, 2)` recursively because there is +the same structure between primal problem and subproblem. So, this is so called recursion. + +We can find out the basic procedure of algorithm to solve the problem after understand recursion. + +**1.Reverse the first k nodes** + +![](../pictures/kgroup/3.jpg) + +**2. Reverse list with k+1 node as head by calling reverseKGroup recursively** + +![](../pictures/kgroup/4.jpg) + +**3. Merge the results of above two steps** + +![](../pictures/kgroup/5.jpg) + +Note, there usually is a base case in recursion function. The base case of this problem is **If the number of nodes is not a multiple of k then left-out nodes in the end should remain as it is**. I will emphasize it in code. + +### Coding + +First, we need to implement a `reverse` function to reverse the elements in a interval. Before that, let's simplify the problem and consider that how to reverse the linked list with a given head node. + +```java +// reverse the linked list with node a as head +ListNode reverse(ListNode a) { + ListNode pre, cur, nxt; + pre = null; cur = a; nxt = a; + while (cur != null) { + nxt = cur.next; + // reverse node one by one + cur.next = pre; + // update pointer + pre = cur; + cur = nxt; + } + // return head node of the reversed linked list + return pre; +} +``` + +![](../pictures/kgroup/8.gif) + +It's easy to understand the iteration with the help of animation above. + +When we reverse the linked list with node `a` as head, indeed, we reverse nodes between node `a` and `null`. + +How should we do to reverse nodes between node `a` and `b`? + +Just change the function signature and change `null` to` b` in the above code + +```java +/** reverse the nodes of interval [a, b), which is left-closed and right-open */ +ListNode reverse(ListNode a, ListNode b) { + ListNode pre, cur, nxt; + pre = null; cur = a; nxt = a; + // just change the condition of quit + while (cur != b) { + nxt = cur.next; + cur.next = pre; + pre = cur; + cur = nxt; + } + // return head node of the reversed linked list + return pre; +} +``` + +So far, we have finished the function of reversing the part of the linked list. Next, we will work on the function of `reverseKGroup` according to the previous design. + +```java +ListNode reverseKGroup(ListNode head, int k) { + if (head == null) return null; + // interval [a, b) includes k nodes to be reversed + ListNode a, b; + a = b = head; + for (int i = 0; i < k; i++) { + // base case + if (b == null) return head; + b = b.next; + } + // reverse first k nodes + ListNode newHead = reverse(a, b); + // merge all reversed internals + a.next = reverseKGroup(b, k); + return newHead; +} +``` + +Note that the interval of `reverse` function is `[a, b)`. + +![](../pictures/kgroup/6.jpg) + +We will not give more details about the recursive part again. The result fully meets the meaning of the question: + +![](../pictures/kgroup/7.jpg) + +### More + +Only a few people read the algorithm articles related to basic data structure according to the page view. Most of people tend to read the articles related to dynamic programming because they often show up in interview. But what I want to share is basic data structure and algorithm matter a lot and all complicated problems evolve from simple problems. + +By the way, remember that **practice makes perfect**. + +[previous:how to find the longest palindromicsubstring](../interview/The Longest Palindromic Substring.md.md) + +[next:how to valid parentheses](../interview/合法括号判定.md) + +[catalog](../README.md#目录) \ No newline at end of file diff --git a/interview/valid-parentheses.md b/interview/valid-parentheses.md new file mode 100644 index 0000000000..ce2dd2b3af --- /dev/null +++ b/interview/valid-parentheses.md @@ -0,0 +1,87 @@ +# Valid Parentheses + +**Translator: [andavid](https://github.com/andavid)** + +**Author: [labuladong](https://github.com/labuladong)** + +The valid of parentheses is a very common and practical problem. For example, the editor and the compiler would check the code we wrote whether the parentheses were correctly closed. As our code might contain the characters '(', ')', '{', '}', '[' and ']', it's a little bit difficult to determine. + +This article is on an algorithm problem about valid parentheses. I believe it will help you to come to a better understanding of stack. + +The problem is very simple. Given a string containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid. + +```text +Input: "()[]{}" +Output: true + +Input: "([)]" +Output: false + +Input: "{[]}" +Output: true +``` + +Before solving this problem, let's lower the difficulty. If the given string contains only parentheses, i.e. '(' and ')', how to check if the string is valid? + +## deal with parentheses + +As the string contains only parentheses, if the string is valid we must make sure: + +**every `)`'s left must have a corresponding `(`**. + +For example: `()))((`, the two right parenthesis in the middle have no corresponding left parenthesis, so this string is not valid. + +We can write out the algorithm according to this thought. + +```cpp +bool isValid(string str) { + // the number of left parenthesis to be matched + int left = 0; + for (char c : str) { + if (c == '(') + left++; + else // encounter right parenthesis + left--; + + if (left < 0) + return false; + } + return left == 0; +} +``` + +If the string contains only one parentheses, the algorithm above will work. As for the case with three kind of parentheses, At first I imitate this thought, define three variables `left1`, `left2` and `left3`, each handle one parentheses. Although we need to write a lot of if-else branches, it seems to solve the problem. + +But actually, it doesn't work. For example, `(())` is valid in the case with one parentheses, while `[(])` is not valid in the case with multiple parentheses. + +Only recording the number of times that left parenthesis occurs is not enough to make the right judgments. We need to increase the information we stored. We can use stack to imitate similar thoughts. + +## deal with multiple parentheses + +Stack is a FILO(first in last out) data structure. It's very useful in dealing with parentheses. + +In this problem, we use a `left` stack instead of the `left` variable before. **Having left parenthesis into stack, as for right parenthesis, find the recent left parenthesis in the stack, and then check if matched.**. + +```cpp +bool isValid(string str) { + stack left; + for (char c : str) { + if (c == '(' || c == '{' || c == '[') + left.push(c); + else // character c is right parenthesis + if (!left.empty() && leftOf(c) == left.top()) + left.pop(); + else + // not match with recent left parenthesis + return false; + } + // whether all left parenthesis are matched + return left.empty(); +} + +char leftOf(char c) { + if (c == '}') return '{'; + if (c == ')') return '('; + return '['; +} +``` diff --git "a/interview/\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/interview/\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" deleted file mode 100644 index 446fda7eb8..0000000000 --- "a/interview/\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" +++ /dev/null @@ -1,120 +0,0 @@ -# 一行代码就能解决的算法题 - -下文是我在 LeetCode 刷题过程中总结的三道有趣的「脑筋急转弯」题目,可以使用算法编程解决,但只要稍加思考,就能找到规律,直接想出答案。 - -### 一、Nim 游戏 - -游戏规则是这样的:你和你的朋友面前有一堆石子,你们轮流拿,一次至少拿一颗,最多拿三颗,谁拿走最后一颗石子谁获胜。 - -假设你们都很聪明,由你第一个开始拿,请你写一个算法,输入一个正整数 n,返回你是否能赢(true 或 false)。 - -比如现在有 4 颗石子,算法应该返回 false。因为无论你拿 1 颗 2 颗还是 3 颗,对方都能一次性拿完,拿走最后一颗石子,所以你一定会输。 - -首先,这道题肯定可以使用动态规划,因为显然原问题存在子问题,且子问题存在重复。但是因为你们都很聪明,涉及到你和对手的博弈,动态规划会比较复杂。 - -**我们解决这种问题的思路一般都是反着思考**: - -如果我能赢,那么最后轮到我取石子的时候必须要剩下 1~3 颗石子,这样我才能一把拿完。 - -如何营造这样的一个局面呢?显然,如果对手拿的时候只剩 4 颗石子,那么无论他怎么拿,总会剩下 1~3 颗石子,我就能赢。 - -如何逼迫对手面对 4 颗石子呢?要想办法,让我选择的时候还有 5~7 颗石子,这样的话我就有把握让对方不得不面对 4 颗石子。 - -如何营造 5~7 颗石子的局面呢?让对手面对 8 颗石子,无论他怎么拿,都会给我剩下 5~7 颗,我就能赢。 - -这样一直循环下去,我们发现只要踩到 4 的倍数,就落入了圈套,永远逃不出 4 的倍数,而且一定会输。所以这道题的解法非常简单: - -```cpp -bool canWinNim(int n) { - // 如果上来就踩到 4 的倍数,那就认输吧 - // 否则,可以把对方控制在 4 的倍数,必胜 - return n % 4 != 0; -} -``` - - -### 二、石头游戏 - -游戏规则是这样的:你和你的朋友面前有一排石头堆,用一个数组 piles 表示,piles[i] 表示第 i 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。 - -**假设你们都很聪明**,由你第一个开始拿,请你写一个算法,输入一个数组 piles,返回你是否能赢(true 或 false)。 - -注意,石头的堆的数量为偶数,所以你们两人拿走的堆数一定是相同的。石头的总数为奇数,也就是你们最后不可能拥有相同多的石头,一定有胜负之分。 - -举个例子,`piles=[2, 1, 9, 5]`,你先拿,可以拿 2 或者 5,你选择 2。 - -`piles=[1, 9, 5]`,轮到对手,可以拿 1 或 5,他选择 5。 - -`piles=[1, 9]` 轮到你拿,你拿 9。 - -最后,你的对手只能拿 1 了。 - -这样下来,你总共拥有 $2 + 9 = 11$ 颗石头,对手有 $5 + 1 = 6$ 颗石头,你是可以赢的,所以算法应该返回 true。 - -你看到了,并不是简单的挑数字大的选,为什么第一次选择 2 而不是 5 呢?因为 5 后面是 9,你要是贪图一时的利益,就把 9 这堆石头暴露给对手了,那你就要输了。 - -这也是强调双方都很聪明的原因,算法也是求最优决策过程下你是否能赢。 - -这道题又涉及到两人的博弈,也可以用动态规划算法暴力试,比较麻烦。但我们只要对规则深入思考,就会大惊失色:只要你足够聪明,你是必胜无疑的,因为你是先手。 - -```java -boolean stoneGame(int[] piles) { - return true; -} -``` - -这是为什么呢,因为题目有两个条件很重要:一是石头总共有偶数堆,石头的总数是奇数。这两个看似增加游戏公平性的条件,反而使该游戏成为了一个割韭菜游戏。我们以 `piles=[2, 1, 9, 5]` 讲解,假设这四堆石头从左到右的索引分别是 1,2,3,4。 - -如果我们把这四堆石头按索引的奇偶分为两组,即第 1、3 堆和第 2、4 堆,那么这两组石头的数量一定不同,也就是说一堆多一堆少。因为石头的总数是奇数,不能被平分。 - -而作为第一个拿石头的人,你可以控制自己拿到所有偶数堆,或者所有的奇数堆。 - -你最开始可以选择第 1 堆或第 4 堆。如果你想要偶数堆,你就拿第 4 堆,这样留给对手的选择只有第 1、3 堆,他不管怎么拿,第 2 堆又会暴露出来,你就可以拿。同理,如果你想拿奇数堆,你就拿第 1 堆,留给对手的只有第 2、4 堆,他不管怎么拿,第 3 堆又给你暴露出来了。 - -也就是说,你可以在第一步就观察好,奇数堆的石头总数多,还是偶数堆的石头总数多,然后步步为营,就一切尽在掌控之中了。知道了这个漏洞,可以整一整不知情的同学了。 - -### 三、电灯开关问题 - -这个问题是这样描述的:有 n 盏电灯,最开始时都是关着的。现在要进行 n 轮操作: - -第 1 轮操作是把每一盏电灯的开关按一下(全部打开)。 - -第 2 轮操作是把每两盏灯的开关按一下(就是按第 2,4,6... 盏灯的开关,它们被关闭)。 - -第 3 轮操作是把每三盏灯的开关按一下(就是按第 3,6,9... 盏灯的开关,有的被关闭,比如 3,有的被打开,比如 6)... - -如此往复,直到第 n 轮,即只按一下第 n 盏灯的开关。 - -现在给你输入一个正整数 n 代表电灯的个数,问你经过 n 轮操作后,这些电灯有多少盏是亮的? - -我们当然可以用一个布尔数组表示这些灯的开关情况,然后模拟这些操作过程,最后去数一下就能出结果。但是这样显得没有灵性,最好的解法是这样的: - -```java -int bulbSwitch(int n) { - return (int)Math.sqrt(n); -} -``` - -什么?这个问题跟平方根有什么关系?其实这个解法挺精妙,如果没人告诉你解法,还真不好想明白。 - -首先,因为电灯一开始都是关闭的,所以某一盏灯最后如果是点亮的,必然要被按奇数次开关。 - -我们假设只有 6 盏灯,而且我们只看第 6 盏灯。需要进行 6 轮操作对吧,请问对于第 6 盏灯,会被按下几次开关呢?这不难得出,第 1 轮会被按,第 2 轮,第 3 轮,第 6 轮都会被按。 - -为什么第 1、2、3、6 轮会被按呢?因为 $6=1\times6=2\times3$。一般情况下,因子都是成对出现的,也就是说开关被按的次数一般是偶数次。但是有特殊情况,比如说总共有 16 盏灯,那么第 16 盏灯会被按几次? - -$16=1\times16=2\times8=4\times4$ - -其中因子 4 重复出现,所以第 16 盏灯会被按 5 次,奇数次。现在你应该理解这个问题为什么和平方根有关了吧? - -不过,我们不是要算最后有几盏灯亮着吗,这样直接平方根一下是啥意思呢?稍微思考一下就能理解了。 - -就假设现在总共有 16 盏灯,我们求 16 的平方根,等于 4,这就说明最后会有 4 盏灯亮着,它们分别是第 $1\times1=1$ 盏、第 $2\times2=4$ 盏、第 $3\times3=9$ 盏和第 $4\times4=16$ 盏。 - -就算有的 n 平方根结果是小数,强转成 int 型,也相当于一个最大整数上界,比这个上界小的所有整数,平方后的索引都是最后亮着的灯的索引。所以说我们直接把平方根转成整数,就是这个问题的答案。 - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/interview/\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/interview/\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" deleted file mode 100644 index fb42a07e60..0000000000 --- "a/interview/\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" +++ /dev/null @@ -1,143 +0,0 @@ -# 二分查找高效判定子序列 - -二分查找本身不难理解,难在巧妙地运用二分查找技巧。对于一个问题,你可能都很难想到它跟二分查找有关,比如前文 [最长递增子序列](../动态规划系列/动态规划设计:最长递增子序列.md) 就借助一个纸牌游戏衍生出二分查找解法。 - -今天再讲一道巧用二分查找的算法问题:如何判定字符串 `s` 是否是字符串 `t` 的子序列(可以假定 `s` 长度比较小,且 `t` 的长度非常大)。举两个例子: - - -s = "abc", t = "**a**h**b**gd**c**", return true. - - -s = "axc", t = "ahbgdc", return false. - -题目很容易理解,而且看起来很简单,但很难想到这个问题跟二分查找有关吧? - -### 一、问题分析 - -首先,一个很简单的解法是这样的: - -```cpp -bool isSubsequence(string s, string t) { - int i = 0, j = 0; - while (i < s.size() && j < t.size()) { - if (s[i] == t[j]) i++; - j++; - } - return i == s.size(); -} -``` - -其思路也非常简单,利用双指针 `i, j` 分别指向 `s, t`,一边前进一边匹配子序列: - -![gif](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/1.gif) - -读者也许会问,这不就是最优解法了吗,时间复杂度只需 O(N),N 为 `t` 的长度。 - -是的,如果仅仅是这个问题,这个解法就够好了,**不过这个问题还有 follow up**: - -如果给你一系列字符串 `s1,s2,...` 和字符串 `t`,你需要判定每个串 `s` 是否是 `t` 的子序列(可以假定 `s` 较短,`t` 很长)。 - -```java -boolean[] isSubsequence(String[] sn, String t); -``` - -你也许会问,这不是很简单吗,还是刚才的逻辑,加个 for 循环不就行了? - -可以,但是此解法处理每个 `s` 时间复杂度仍然是 O(N),而如果巧妙运用二分查找,可以将时间复杂度降低,大约是 O(MlogN)。由于 N 相对 M 大很多,所以后者效率会更高。 - -### 二、二分思路 - -二分思路主要是对 `t` 进行预处理,用一个字典 `index` 将每个字符出现的索引位置按顺序存储下来: - -```java -int m = s.length(), n = t.length(); -ArrayList[] index = new ArrayList[256]; -// 先记下 t 中每个字符出现的位置 -for (int i = 0; i < n; i++) { - char c = t.charAt(i); - if (index[c] == null) - index[c] = new ArrayList<>(); - index[c].add(i); -} -``` - -![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/2.jpg) - -比如对于这个情况,匹配了 "ab",应该匹配 "c" 了: - -![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/1.jpg) - -按照之前的解法,我们需要 `j` 线性前进扫描字符 "c",但借助 `index` 中记录的信息,**可以二分搜索 `index[c]` 中比 j 大的那个索引**,在上图的例子中,就是在 `[0,2,6]` 中搜索比 4 大的那个索引: - -![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/3.jpg) - -这样就可以直接得到下一个 "c" 的索引。现在的问题就是,如何用二分查找计算那个恰好比 4 大的索引呢?答案是,寻找左侧边界的二分搜索就可以做到。 - -### 三、再谈二分查找 - -在前文 [二分查找详解](../算法思维系列/二分查找详解.md) 中,详解了如何正确写出三种二分查找算法的细节。二分查找返回目标值 `val` 的索引,对于搜索**左侧边界**的二分查找,有一个特殊性质: - -**当 `val` 不存在时,得到的索引恰好是比 `val` 大的最小元素索引**。 - -什么意思呢,就是说如果在数组 `[0,1,3,4]` 中搜索元素 2,算法会返回索引 2,也就是元素 3 的位置,元素 3 是数组中大于 2 的最小元素。所以我们可以利用二分搜索避免线性扫描。 - -```java -// 查找左侧边界的二分查找 -int left_bound(ArrayList arr, int tar) { - int lo = 0, hi = arr.size(); - while (lo < hi) { - int mid = lo + (hi - lo) / 2; - if (tar > arr.get(mid)) { - lo = mid + 1; - } else { - hi = mid; - } - } - return lo; -} -``` - -以上就是搜索左侧边界的二分查找,等会儿会用到,其中的细节可以参见前文《二分查找详解》,这里不再赘述。 - -### 四、代码实现 - -这里以单个字符串 `s` 为例,对于多个字符串 `s`,可以把预处理部分抽出来。 - -```java -boolean isSubsequence(String s, String t) { - int m = s.length(), n = t.length(); - // 对 t 进行预处理 - ArrayList[] index = new ArrayList[256]; - for (int i = 0; i < n; i++) { - char c = t.charAt(i); - if (index[c] == null) - index[c] = new ArrayList<>(); - index[c].add(i); - } - - // 串 t 上的指针 - int j = 0; - // 借助 index 查找 s[i] - for (int i = 0; i < m; i++) { - char c = s.charAt(i); - // 整个 t 压根儿没有字符 c - if (index[c] == null) return false; - int pos = left_bound(index[c], j); - // 二分搜索区间中没有找到字符 c - if (pos == index[c].size()) return false; - // 向前移动指针 j - j = index[c].get(pos) + 1; - } - return true; -} -``` - -算法执行的过程是这样的: - -![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/2.gif) - -可见借助二分查找,算法的效率是可以大幅提升的。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/interview/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" "b/interview/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" deleted file mode 100644 index 0e96e34f05..0000000000 --- "a/interview/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" +++ /dev/null @@ -1,210 +0,0 @@ -我们之前有两篇文章写了回文串和回文序列相关的问题。 - -**寻找**回文串的核心思想是从中心向两端扩展: - -```cpp -string palindrome(string& s, int l, int r) { - // 防止索引越界 - while (l >= 0 && r < s.size() - && s[l] == s[r]) { - // 向两边展开 - l--; r++; - } - // 返回以 s[l] 和 s[r] 为中心的最长回文串 - return s.substr(l + 1, r - l - 1); -} -``` - -因为回文串长度可能为奇数也可能是偶数,长度为奇数时只存在一个中心点,而长度为偶数时存在两个中心点,所以上面这个函数需要传入`l`和`r`。 - -而**判断**一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要「双指针技巧」,从两端向中间逼近即可: - -```cpp -bool isPalindrome(string s) { - int left = 0, right = s.length - 1; - while (left < right) { - if (s[left] != s[right]) - return false; - left++; right--; - } - return true; -} -``` - -以上代码很好理解吧,**因为回文串是对称的,所以正着读和倒着读应该是一样的,这一特点是解决回文串问题的关键**。 - -下面扩展这一最简单的情况,来解决:如何判断一个「单链表」是不是回文。 - -### 一、判断回文单链表 - -输入一个单链表的头结点,判断这个链表中的数字是不是回文: - -```java -/** - * 单链表节点的定义: - * public class ListNode { - * int val; - * ListNode next; - * } - */ - -boolean isPalindrome(ListNode head); - -输入: 1->2->null -输出: false - -输入: 1->2->2->1->null -输出: true -``` - -这道题的关键在于,单链表无法倒着遍历,无法使用双指针技巧。那么最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同。关于如何反转链表,可以参见前文「递归操作链表」。 - -其实,**借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表**,下面来具体聊聊。 - -对于二叉树的几种遍历方式,我们再熟悉不过了: - -```java -void traverse(TreeNode root) { - // 前序遍历代码 - traverse(root.left); - // 中序遍历代码 - traverse(root.right); - // 后序遍历代码 -} -``` - -在「学习数据结构的框架思维」中说过,链表兼具递归结构,树结构不过是链表的衍生。那么,**链表其实也可以有前序遍历和后序遍历**: - -```java -void traverse(ListNode head) { - // 前序遍历代码 - traverse(head.next); - // 后序遍历代码 -} -``` - -这个框架有什么指导意义呢?如果我想正序打印链表中的`val`值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作: - -```java -/* 倒序打印单链表中的元素值 */ -void traverse(ListNode head) { - if (head == null) return; - traverse(head.next); - // 后序遍历代码 - print(head.val); -} -``` - -说到这了,其实可以稍作修改,模仿双指针实现回文判断的功能: - -```java -// 左侧指针 -ListNode left; - -boolean isPalindrome(ListNode head) { - left = head; - return traverse(head); -} - -boolean traverse(ListNode right) { - if (right == null) return true; - boolean res = traverse(right.next); - // 后序遍历代码 - res = res && (right.val == left.val); - left = left.next; - return res; -} -``` - -这么做的核心逻辑是什么呢?**实际上就是把链表节点放入一个栈,然后再拿出来,这时候元素顺序就是反的**,只不过我们利用的是递归函数的堆栈而已。 - -![](../pictures/回文链表/1.gif) - -当然,无论造一条反转链表还是利用后续遍历,算法的时间和空间复杂度都是 O(N)。下面我们想想,能不能不用额外的空间,解决这个问题呢? - -### 二、优化空间复杂度 - -更好的思路是这样的: - -**1、先通过「双指针技巧」中的快慢指针来找到链表的中点**: - -```java -ListNode slow, fast; -slow = fast = head; -while (fast != null && fast.next != null) { - slow = slow.next; - fast = fast.next.next; -} -// slow 指针现在指向链表中点 -``` - -![](../pictures/回文链表/1.jpg) - -**2、如果`fast`指针没有指向`null`,说明链表长度为奇数,`slow`还要再前进一步**: - -```java -if (fast != null) - slow = slow.next; -``` - -![](../pictures/回文链表/2.jpg) - -**3、从`slow`开始反转后面的链表,现在就可以开始比较回文串了**: - -```java -ListNode left = head; -ListNode right = reverse(slow); - -while (right != null) { - if (left.val != right.val) - return false; - left = left.next; - right = right.next; -} -return true; -``` - -![](../pictures/回文链表/3.jpg) - -至此,把上面 3 段代码合在一起就高效地解决这个问题了,其中`reverse`函数很容易实现: - -```java -ListNode reverse(ListNode head) { - ListNode pre = null, cur = head; - while (cur != null) { - ListNode next = cur.next; - cur.next = pre; - pre = cur; - cur = next; - } - return pre; -} -``` - -![](../pictures/kgroup/8.gif) - -算法总体的时间复杂度 O(N),空间复杂度 O(1),已经是最优的了。 - -我知道肯定有读者会问:这种解法虽然高效,但破坏了输入链表的原始结构,能不能避免这个瑕疵呢? - -其实这个问题很好解决,关键在于得到`p, q`这两个指针位置: - -![](../pictures/回文链表/4.jpg) - -这样,只要在函数 return 之前加一段代码即可恢复原先链表顺序: - -```java -p.next = reverse(q); -``` - -篇幅所限,我就不写了,读者可以自己尝试一下。 - -### 三、最后总结 - -首先,寻找回文串是从中间向两端扩展,判断回文串是从两端向中间收缩。对于单链表,无法直接倒序遍历,可以造一条新的反转链表,可以利用链表的后序遍历,也可以用栈结构倒序处理单链表。 - -具体到回文链表的判断问题,由于回文的特殊性,可以不完全反转链表,而是仅仅反转部分链表,将空间复杂度降到 O(1)。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/interview/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" "b/interview/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" deleted file mode 100644 index 50e5495056..0000000000 --- "a/interview/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" +++ /dev/null @@ -1,86 +0,0 @@ -# 如何判定括号合法性 - -对括号的合法性判断是一个很常见且实用的问题,比如说我们写的代码,编辑器和编译器都会检查括号是否正确闭合。而且我们的代码可能会包含三种括号 `[](){}`,判断起来有一点难度。 - -本文就来聊一道关于括号合法性判断的算法题,相信能加深你对**栈**这种数据结构的理解。 - -题目很简单,输入一个字符串,其中包含 `[](){}` 六种括号,请你判断这个字符串组成的括号是否合法。 - -``` -Input: "()[]{}" -Output: true - -Input: "([)]" -Output: false - -Input: "{[]}" -Output: true -``` - -解决这个问题之前,我们先降低难度,思考一下,**如果只有一种括号 `()`**,应该如何判断字符串组成的括号是否合法呢? - -### 一、处理一种括号 - -字符串中只有圆括号,如果想让括号字符串合法,那么必须做到: - -**每个右括号 `)` 的左边必须有一个左括号 `(` 和它匹配**。 - -比如说字符串 `()))((` 中,中间的两个右括号**左边**就没有左括号匹配,所以这个括号组合是不合法的。 - -那么根据这个思路,我们可以写出算法: - -```cpp -bool isValid(string str) { - // 待匹配的左括号数量 - int left = 0; - for (char c : str) { - if (c == '(') - left++; - else // 遇到右括号 - left--; - - if (left < 0) - return false; - } - return left == 0; -} -``` -如果只有圆括号,这样就能正确判断合法性。对于三种括号的情况,我一开始想模仿这个思路,定义三个变量 `left1`,`left2`,`left3` 分别处理每种括号,虽然要多写不少 if else 分支,但是似乎可以解决问题。 - -但实际上直接照搬这种思路是不行的,比如说只有一个括号的情况下 `(())` 是合法的,但是多种括号的情况下, `[(])` 显然是不合法的。 - -仅仅记录每种左括号出现的次数已经不能做出正确判断了,我们要加大存储的信息量,可以利用栈来模仿类似的思路。 - -### 二、处理多种括号 - -栈是一种先进后出的数据结构,处理括号问题的时候尤其有用。 - -我们这道题就用一个名为 `left` 的栈代替之前思路中的 `left` 变量,**遇到左括号就入栈,遇到右括号就去栈中寻找最近的左括号,看是否匹配**。 - -```cpp -bool isValid(string str) { - stack left; - for (char c : str) { - if (c == '(' || c == '{' || c == '[') - left.push(c); - else // 字符 c 是右括号 - if (!left.empty() && leftOf(c) == left.top()) - left.pop(); - else - // 和最近的左括号不匹配 - return false; - } - // 是否所有的左括号都被匹配了 - return left.empty(); -} - -char leftOf(char c) { - if (c == '}') return '{'; - if (c == ')') return '('; - return '['; -} -``` - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/interview/\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/interview/\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" deleted file mode 100644 index cbdadc3c42..0000000000 --- "a/interview/\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" +++ /dev/null @@ -1,67 +0,0 @@ -# 如何去除有序数组的重复元素 - -我们知道对于数组来说,在尾部插入、删除元素是比较高效的,时间复杂度是 O(1),但是如果在中间或者开头插入、删除元素,就会涉及数据的搬移,时间复杂度为 O(N),效率较低。 - -所以对于一般处理数组的算法问题,我们要尽可能只对数组尾部的元素进行操作,以避免额外的时间复杂度。 - -这篇文章讲讲如何对一个有序数组去重,先看下题目: - -![](../pictures/%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E5%8E%BB%E9%87%8D/title.png) - -显然,由于数组已经排序,所以重复的元素一定连在一起,找出它们并不难,但如果毎找到一个重复元素就立即删除它,就是在数组中间进行删除操作,整个时间复杂度是会达到 O(N^2)。而且题目要求我们原地修改,也就是说不能用辅助数组,空间复杂度得是 O(1)。 - -其实,**对于数组相关的算法问题,有一个通用的技巧:要尽量避免在中间删除元素,那我就想先办法把这个元素换到最后去**。这样的话,最终待删除的元素都拖在数组尾部,一个一个 pop 掉就行了,每次操作的时间复杂度也就降低到 O(1) 了。 - -按照这个思路呢,又可以衍生出解决类似需求的通用方式:双指针技巧。具体一点说,应该是快慢指针。 - -我们让慢指针 `slow` 走左后面,快指针 `fast` 走在前面探路,找到一个不重复的元素就告诉 `slow` 并让 `slow` 前进一步。这样当 `fast` 指针遍历完整个数组 `nums` 后,**`nums[0..slow]` 就是不重复元素,之后的所有元素都是重复元素**。 - -```java -int removeDuplicates(int[] nums) { - int n = nums.length; - if (n == 0) return 0; - int slow = 0, fast = 1; - while (fast < n) { - if (nums[fast] != nums[slow]) { - slow++; - // 维护 nums[0..slow] 无重复 - nums[slow] = nums[fast]; - } - fast++; - } - // 长度为索引 + 1 - return slow + 1; -} -``` - -看下算法执行的过程: - -![](../pictures/%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E5%8E%BB%E9%87%8D/1.gif) - -再简单扩展一下,如果给你一个有序链表,如何去重呢?其实和数组是一模一样的,唯一的区别是把数组赋值操作变成操作指针而已: - -```java -ListNode deleteDuplicates(ListNode head) { - if (head == null) return null; - ListNode slow = head, fast = head.next; - while (fast != null) { - if (fast.val != slow.val) { - // nums[slow] = nums[fast]; - slow.next = fast; - // slow++; - slow = slow.next; - } - // fast++ - fast = fast.next; - } - // 断开与后面重复元素的连接 - slow.next = null; - return head; -} -``` - -![](../pictures/%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E5%8E%BB%E9%87%8D/2.gif) - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/interview/\345\272\247\344\275\215\350\260\203\345\272\246.md" "b/interview/\345\272\247\344\275\215\350\260\203\345\272\246.md" deleted file mode 100644 index 577d0785d8..0000000000 --- "a/interview/\345\272\247\344\275\215\350\260\203\345\272\246.md" +++ /dev/null @@ -1,208 +0,0 @@ -# 如何调度考生的座位 - -这是 LeetCode 第 885 题,有趣且具有一定技巧性。这种题目并不像动态规划这类算法拼智商,而是看你对常用数据结构的理解和写代码的水平,个人认为值得重视和学习。 - -另外说句题外话,很多读者都问,算法框架是如何总结出来的,其实框架反而是慢慢从细节里抠出来的。希望大家看了我们的文章之后,最好能抽时间把相关的问题亲自做一做,纸上得来终觉浅,绝知此事要躬行嘛。 - -先来描述一下题目:假设有一个考场,考场有一排共 `N` 个座位,索引分别是 `[0..N-1]`,考生会**陆续**进入考场考试,并且可能在**任何时候**离开考场。 - -你作为考官,要安排考生们的座位,满足:**每当一个学生进入时,你需要最大化他和最近其他人的距离;如果有多个这样的座位,安排到他到索引最小的那个座位**。这很符合实际情况对吧, - -也就是请你实现下面这样一个类: - -```java -class ExamRoom { - // 构造函数,传入座位总数 N - public ExamRoom(int N); - // 来了一名考生,返回你给他分配的座位 - public int seat(); - // 坐在 p 位置的考生离开了 - // 可以认为 p 位置一定坐有考生 - public void leave(int p); -} -``` - -比方说考场有 5 个座位,分别是 `[0..4]`: - -第一名考生进入时(调用 `seat()`),坐在任何位置都行,但是要给他安排索引最小的位置,也就是返回位置 0。 - -第二名学生进入时(再调用 `seat()`),要和旁边的人距离最远,也就是返回位置 4。 - -第三名学生进入时,要和旁边的人距离最远,应该做到中间,也就是座位 2。 - -如果再进一名学生,他可以坐在座位 1 或者 3,取较小的索引 1。 - -以此类推。 - -刚才所说的情况,没有调用 `leave` 函数,不过读者肯定能够发现规律: - -**如果将每两个相邻的考生看做线段的两端点,新安排考生就是找最长的线段,然后让该考生在中间把这个线段「二分」,中点就是给他分配的座位。`leave(p)` 其实就是去除端点 `p`,使得相邻两个线段合并为一个**。 - -核心思路很简单对吧,所以这个问题实际上实在考察你对数据结构的理解。对于上述这个逻辑,你用什么数据结构来实现呢? - -### 一、思路分析 - -根据上述思路,首先需要把坐在教室的学生抽象成线段,我们可以简单的用一个大小为 2 的数组表示。 - -另外,思路需要我们找到「最长」的线段,还需要去除线段,增加线段。 - -**但凡遇到在动态过程中取最值的要求,肯定要使用有序数据结构,我们常用的数据结构就是二叉堆和平衡二叉搜索树了**。二叉堆实现的优先级队列取最值的时间复杂度是 O(logN),但是只能删除最大值。平衡二叉树也可以取最值,也可以修改、删除任意一个值,而且时间复杂度都是 O(logN)。 - -综上,二叉堆不能满足 `leave` 操作,应该使用平衡二叉树。所以这里我们会用到 Java 的一种数据结构 `TreeSet`,这是一种有序数据结构,底层由红黑树维护有序性。 - -这里顺便提一下,一说到集合(Set)或者映射(Map),有的读者可能就想当然的认为是哈希集合(HashSet)或者哈希表(HashMap),这样理解是有点问题的。 - -因为哈希集合/映射底层是由哈希函数和数组实现的,特性是遍历无固定顺序,但是操作效率高,时间复杂度为 O(1)。 - -而集合/映射还可以依赖其他底层数据结构,常见的就是红黑树(一种平衡二叉搜索树),特性是自动维护其中元素的顺序,操作效率是 O(logN)。这种一般称为「有序集合/映射」。 - -我们使用的 `TreeSet` 就是一个有序集合,目的就是为了保持线段长度的有序性,快速查找最大线段,快速删除和插入。 - -### 二、简化问题 - -首先,如果有多个可选座位,需要选择索引最小的座位对吧?**我们先简化一下问题,暂时不管这个要求**,实现上述思路。 - -这个问题还用到一个常用的编程技巧,就是使用一个「虚拟线段」让算法正确启动,这就和链表相关的算法需要「虚拟头结点」一个道理。 - -```java -// 将端点 p 映射到以 p 为左端点的线段 -private Map startMap; -// 将端点 p 映射到以 p 为右端点的线段 -private Map endMap; -// 根据线段长度从小到大存放所有线段 -private TreeSet pq; -private int N; - -public ExamRoom(int N) { - this.N = N; - startMap = new HashMap<>(); - endMap = new HashMap<>(); - pq = new TreeSet<>((a, b) -> { - // 算出两个线段的长度 - int distA = distance(a); - int distB = distance(b); - // 长度更长的更大,排后面 - return distA - distB; - }); - // 在有序集合中先放一个虚拟线段 - addInterval(new int[] {-1, N}); -} - -/* 去除一个线段 */ -private void removeInterval(int[] intv) { - pq.remove(intv); - startMap.remove(intv[0]); - endMap.remove(intv[1]); -} - -/* 增加一个线段 */ -private void addInterval(int[] intv) { - pq.add(intv); - startMap.put(intv[0], intv); - endMap.put(intv[1], intv); -} - -/* 计算一个线段的长度 */ -private int distance(int[] intv) { - return intv[1] - intv[0] - 1; -} -``` - -「虚拟线段」其实就是为了将所有座位表示为一个线段: - -![](../pictures/座位调度/1.jpg) - -有了上述铺垫,主要 API `seat` 和 `leave` 就可以写了: - -```java -public int seat() { - // 从有序集合拿出最长的线段 - int[] longest = pq.last(); - int x = longest[0]; - int y = longest[1]; - int seat; - if (x == -1) { // 情况一 - seat = 0; - } else if (y == N) { // 情况二 - seat = N - 1; - } else { // 情况三 - seat = (y - x) / 2 + x; - } - // 将最长的线段分成两段 - int[] left = new int[] {x, seat}; - int[] right = new int[] {seat, y}; - removeInterval(longest); - addInterval(left); - addInterval(right); - return seat; -} - -public void leave(int p) { - // 将 p 左右的线段找出来 - int[] right = startMap.get(p); - int[] left = endMap.get(p); - // 合并两个线段成为一个线段 - int[] merged = new int[] {left[0], right[1]}; - removeInterval(left); - removeInterval(right); - addInterval(merged); -} -``` - -![三种情况](../pictures/座位调度/2.jpg) - -至此,算法就基本实现了,代码虽多,但思路很简单:找最长的线段,从中间分隔成两段,中点就是 `seat()` 的返回值;找 `p` 的左右线段,合并成一个线段,这就是 `leave(p)` 的逻辑。 - -### 三、进阶问题 - -但是,题目要求多个选择时选择索引最小的那个座位,我们刚才忽略了这个问题。比如下面这种情况会出错: - -![](../pictures/座位调度/3.jpg) - -现在有序集合里有线段 `[0,4]` 和 `[4,9]`,那么最长线段 `longest` 就是后者,按照 `seat` 的逻辑,就会分割 `[4,9]`,也就是返回座位 6。但正确答案应该是座位 2,因为 2 和 6 都满足最大化相邻考生距离的条件,二者应该取较小的。 - -![](../pictures/座位调度/4.jpg) - -**遇到题目的这种要求,解决方式就是修改有序数据结构的排序方式**。具体到这个问题,就是修改 `TreeMap` 的比较函数逻辑: - -```java -pq = new TreeSet<>((a, b) -> { - int distA = distance(a); - int distB = distance(b); - // 如果长度相同,就比较索引 - if (distA == distB) - return b[0] - a[0]; - return distA - distB; -}); -``` - -除此之外,还要改变 `distance` 函数,**不能简单地让它计算一个线段两个端点间的长度,而是让它计算该线段中点和端点之间的长度**。 - -```java -private int distance(int[] intv) { - int x = intv[0]; - int y = intv[1]; - if (x == -1) return y; - if (y == N) return N - 1 - x; - // 中点和端点之间的长度 - return (y - x) / 2; -} -``` - -![](../pictures/座位调度/5.jpg) - -这样,`[0,4]` 和 `[4,9]` 的 `distance` 值就相等了,算法会比较二者的索引,取较小的线段进行分割。到这里,这道算法题目算是完全解决了。 - -### 四、最后总结 - -本文聊的这个问题其实并不算难,虽然看起来代码很多。核心问题就是考察有序数据结构的理解和使用,来梳理一下。 - -处理动态问题一般都会用到有序数据结构,比如平衡二叉搜索树和二叉堆,二者的时间复杂度差不多,但前者支持的操作更多。 - -既然平衡二叉搜索树这么好用,还用二叉堆干嘛呢?因为二叉堆底层就是数组,实现简单啊,详见旧文「二叉堆详解」。你实现个红黑树试试?操作复杂,而且消耗的空间相对来说会多一些。具体问题,还是要选择恰当的数据结构来解决。 - -希望本文对大家有帮助。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/interview/\346\211\223\345\215\260\347\264\240\346\225\260.md" "b/interview/\346\211\223\345\215\260\347\264\240\346\225\260.md" deleted file mode 100644 index 3875915ade..0000000000 --- "a/interview/\346\211\223\345\215\260\347\264\240\346\225\260.md" +++ /dev/null @@ -1,151 +0,0 @@ -# 如何高效寻找素数 - -素数的定义看起来很简单,如果一个数如果只能被 1 和它本身整除,那么这个数就是素数。 - -不要觉得素数的定义简单,恐怕没多少人真的能把素数相关的算法写得高效。比如让你写这样一个函数: - -```java -// 返回区间 [2, n) 中有几个素数 -int countPrimes(int n) - -// 比如 countPrimes(10) 返回 4 -// 因为 2,3,5,7 是素数 -``` - -你会如何写这个函数?我想大家应该会这样写: - -```java -int countPrimes(int n) { - int count = 0; - for (int i = 2; i < n; i++) - if (isPrim(i)) count++; - return count; -} - -// 判断整数 n 是否是素数 -boolean isPrime(int n) { - for (int i = 2; i < n; i++) - if (n % i == 0) - // 有其他整除因子 - return false; - return true; -} -``` - -这样写的话时间复杂度 O(n^2),问题很大。**首先你用 isPrime 函数来辅助的思路就不够高效;而且就算你要用 isPrime 函数,这样写算法也是存在计算冗余的**。 - -先来简单说下**如果你要判断一个数是不是素数,应该如何写算法**。只需稍微修改一下上面的 isPrim 代码中的 for 循环条件: - -```java -boolean isPrime(int n) { - for (int i = 2; i * i <= n; i++) - ... -} -``` - -换句话说,`i` 不需要遍历到 `n`,而只需要到 `sqrt(n)` 即可。为什么呢,我们举个例子,假设 `n = 12`。 - -```java -12 = 2 × 6 -12 = 3 × 4 -12 = sqrt(12) × sqrt(12) -12 = 4 × 3 -12 = 6 × 2 -``` - -可以看到,后两个乘积就是前面两个反过来,反转临界点就在 `sqrt(n)`。 - -换句话说,如果在 `[2,sqrt(n)]` 这个区间之内没有发现可整除因子,就可以直接断定 `n` 是素数了,因为在区间 `[sqrt(n),n]` 也一定不会发现可整除因子。 - -现在,`isPrime` 函数的时间复杂度降为 O(sqrt(N)),**但是我们实现 `countPrimes` 函数其实并不需要这个函数**,以上只是希望读者明白 `sqrt(n)` 的含义,因为等会还会用到。 - - -### 高效实现 `countPrimes` - -高效解决这个问题的核心思路是和上面的常规思路反着来: - -首先从 2 开始,我们知道 2 是一个素数,那么 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8... 都不可能是素数了。 - -然后我们发现 3 也是素数,那么 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12... 也都不可能是素数了。 - -看到这里,你是否有点明白这个排除法的逻辑了呢?先看我们的第一版代码: - -```java -int countPrimes(int n) { - boolean[] isPrim = new boolean[n]; - // 将数组都初始化为 true - Arrays.fill(isPrim, true); - - for (int i = 2; i < n; i++) - if (isPrim[i]) - // i 的倍数不可能是素数了 - for (int j = 2 * i; j < n; j += i) - isPrim[j] = false; - - int count = 0; - for (int i = 2; i < n; i++) - if (isPrim[i]) count++; - - return count; -} -``` - -如果上面这段代码你能够理解,那么你已经掌握了整体思路,但是还有两个细微的地方可以优化。 - -首先,回想刚才判断一个数是否是素数的 `isPrime` 函数,由于因子的对称性,其中的 for 循环只需要遍历 `[2,sqrt(n)]` 就够了。这里也是类似的,我们外层的 for 循环也只需要遍历到 `sqrt(n)`: - -```java -for (int i = 2; i * i < n; i++) - if (isPrim[i]) - ... -``` - -除此之外,很难注意到内层的 for 循环也可以优化。我们之前的做法是: - -```java -for (int j = 2 * i; j < n; j += i) - isPrim[j] = false; -``` - -这样可以把 `i` 的整数倍都标记为 `false`,但是仍然存在计算冗余。 - -比如 `n = 25`,`i = 4` 时算法会标记 4 × 2 = 8,4 × 3 = 12 等等数字,但是这两个数字已经被 `i = 2` 和 `i = 3` 的 2 × 4 和 3 × 4 标记了。 - -我们可以稍微优化一下,让 `j` 从 `i` 的平方开始遍历,而不是从 `2 * i` 开始: - -```java -for (int j = i * i; j < n; j += i) - isPrim[j] = false; -``` - -这样,素数计数的算法就高效实现了,其实这个算法有一个名字,叫做 Sieve of Eratosthenes。看下完整的最终代码: - -```java -int countPrimes(int n) { - boolean[] isPrim = new boolean[n]; - Arrays.fill(isPrim, true); - for (int i = 2; i * i < n; i++) - if (isPrim[i]) - for (int j = i * i; j < n; j += i) - isPrim[j] = false; - - int count = 0; - for (int i = 2; i < n; i++) - if (isPrim[i]) count++; - - return count; -} -``` - -**该算法的时间复杂度比较难算**,显然时间跟这两个嵌套的 for 循环有关,其操作数应该是: - - n/2 + n/3 + n/5 + n/7 + ... -= n × (1/2 + 1/3 + 1/5 + 1/7...) - -括号中是素数的倒数。其最终结果是 O(N * loglogN),有兴趣的读者可以查一下该算法的时间复杂度证明。 - -以上就是素数算法相关的全部内容。怎么样,是不是看似简单的问题却有不少细节可以打磨呀? - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/interview/\346\216\245\351\233\250\346\260\264.md" "b/interview/\346\216\245\351\233\250\346\260\264.md" deleted file mode 100644 index f378322351..0000000000 --- "a/interview/\346\216\245\351\233\250\346\260\264.md" +++ /dev/null @@ -1,184 +0,0 @@ -# 接雨水问题详解 - -接雨水这道题目挺有意思,在面试题中出现频率还挺高的,本文就来步步优化,讲解一下这道题。 - -先看一下题目: - -![](../pictures/接雨水/title.png) - -就是用一个数组表示一个条形图,问你这个条形图最多能接多少水。 - -```java -int trap(int[] height); -``` - -下面就来由浅入深介绍暴力解法 -> 备忘录解法 -> 双指针解法,在 O(N) 时间 O(1) 空间内解决这个问题。 - -### 一、核心思路 - -我第一次看到这个问题,无计可施,完全没有思路,相信很多朋友跟我一样。所以对于这种问题,我们不要想整体,而应该去想局部;就像之前的文章处理字符串问题,不要考虑如何处理整个字符串,而是去思考应该如何处理每一个字符。 - -这么一想,可以发现这道题的思路其实很简单。具体来说,仅仅对于位置 i,能装下多少水呢? - -![](../pictures/接雨水/0.jpg) - -能装 2 格水。为什么恰好是两格水呢?因为 height[i] 的高度为 0,而这里最多能盛 2 格水,2-0=2。 - -为什么位置 i 最多能盛 2 格水呢?因为,位置 i 能达到的水柱高度和其左边的最高柱子、右边的最高柱子有关,我们分别称这两个柱子高度为 `l_max` 和 `r_max`;**位置 i 最大的水柱高度就是 `min(l_max, r_max)`。** - -更进一步,对于位置 i,能够装的水为: - -```python -water[i] = min( - # 左边最高的柱子 - max(height[0..i]), - # 右边最高的柱子 - max(height[i..end]) - ) - height[i] - -``` - -![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/1.jpg) - -![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/2.jpg) - -这就是本问题的核心思路,我们可以简单写一个暴力算法: - -```cpp -int trap(vector& height) { - int n = height.size(); - int ans = 0; - for (int i = 1; i < n - 1; i++) { - int l_max = 0, r_max = 0; - // 找右边最高的柱子 - for (int j = i; j < n; j++) - r_max = max(r_max, height[j]); - // 找左边最高的柱子 - for (int j = i; j >= 0; j--) - l_max = max(l_max, height[j]); - // 如果自己就是最高的话, - // l_max == r_max == height[i] - ans += min(l_max, r_max) - height[i]; - } - return ans; -} -``` - -有之前的思路,这个解法应该是很直接粗暴的,时间复杂度 O(N^2),空间复杂度 O(1)。但是很明显这种计算 `r_max` 和 `l_max` 的方式非常笨拙,一般的优化方法就是备忘录。 - -### 二、备忘录优化 - -之前的暴力解法,不是在每个位置 i 都要计算 `r_max` 和 `l_max` 吗?我们直接把结果都缓存下来,别傻不拉几的每次都遍历,这时间复杂度不就降下来了嘛。 - -我们开两个**数组** `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; - // 数组充当备忘录 - vector l_max(n), r_max(n); - // 初始化 base case - l_max[0] = height[0]; - r_max[n - 1] = height[n - 1]; - // 从左向右计算 l_max - for (int i = 1; i < n; i++) - l_max[i] = max(height[i], l_max[i - 1]); - // 从右向左计算 r_max - for (int i = n - 2; i >= 0; i--) - 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; -} -``` - -这个优化其实和暴力解法差不多,就是避免了重复计算,把时间复杂度降低为 O(N),已经是最优了,但是空间复杂度是 O(N)。下面来看一个精妙一些的解法,能够把空间复杂度降低到 O(1)。 - -### 三、双指针解法 - -这种解法的思路是完全相同的,但在实现手法上非常巧妙,我们这次也不要用备忘录提前计算了,而是用双指针**边走边算**,节省下空间复杂度。 - -首先,看一部分代码: - -```cpp -int trap(vector& height) { - int n = height.size(); - int left = 0, right = n - 1; - - int l_max = height[0]; - int r_max = height[n - 1]; - - while (left <= right) { - l_max = max(l_max, height[left]); - r_max = max(r_max, height[right]); - left++; right--; - } -} -``` - -对于这部分代码,请问 `l_max` 和 `r_max` 分别表示什么意义呢? - -很容易理解,**`l_max` 是 `height[0..left]` 中最高柱子的高度,`r_max` 是 `height[right..end]` 的最高柱子的高度**。 - -明白了这一点,直接看解法: - -```cpp -int trap(vector& height) { - if (height.empty()) return 0; - int n = height.size(); - 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 = max(l_max, height[left]); - r_max = max(r_max, height[right]); - - // ans += min(l_max, r_max) - height[i] - if (l_max < r_max) { - ans += l_max - height[left]; - left++; - } else { - ans += r_max - height[right]; - right--; - } - } - return ans; -} -``` - -你看,其中的核心思想和之前一模一样,换汤不换药。但是细心的读者可能会发现次解法还是有点细节差异: - -之前的备忘录解法,`l_max[i]` 和 `r_max[i]` 代表的是 `height[0..i]` 和 `height[i..end]` 的最高柱子高度。 - -```cpp -ans += min(l_max[i], r_max[i]) - height[i]; -``` - -![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/3.jpg) - -但是双指针解法中,`l_max` 和 `r_max` 代表的是 `height[0..left]` 和 `height[right..end]` 的最高柱子高度。比如这段代码: - -```cpp -if (l_max < r_max) { - ans += l_max - height[left]; - left++; -} -``` - -![](../pictures/%E6%8E%A5%E9%9B%A8%E6%B0%B4/4.jpg) - -此时的 `l_max` 是 `left` 指针左边的最高柱子,但是 `r_max` 并不一定是 `left` 指针右边最高的柱子,这真的可以得到正确答案吗? - -其实这个问题要这么思考,我们只在乎 `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) diff --git "a/interview/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" "b/interview/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" deleted file mode 100644 index 159435ccf9..0000000000 --- "a/interview/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" +++ /dev/null @@ -1,104 +0,0 @@ -# 如何寻找最长回文子串 - -回文串是面试常常遇到的问题(虽然问题本身没啥意义),本文就告诉你回文串问题的核心思想是什么。 - -首先,明确一下什:**回文串就是正着读和反着读都一样的字符串**。 - -比如说字符串 `aba` 和 `abba` 都是回文串,因为它们对称,反过来还是和本身一样。反之,字符串 `abac` 就不是回文串。 - -可以看到回文串的的长度可能是奇数,也可能是偶数,这就添加了回文串问题的难度,解决该类问题的核心是**双指针**。下面就通过一道最长回文子串的问题来具体理解一下回文串问题: - -![](../pictures/回文/title.png) - -```cpp -string longestPalindrome(string s) {} -``` - -### 一、思考 - -对于这个问题,我们首先应该思考的是,给一个字符串 `s`,如何在 `s` 中找到一个回文子串? - -有一个很有趣的思路:既然回文串是一个正着反着读都一样的字符串,那么如果我们把 `s` 反转,称为 `s'`,然后在 `s` 和 `s'` 中寻找**最长公共子串**,这样应该就能找到最长回文子串。 - -比如说字符串 `abacd`,反过来是 `dcaba`,它的最长公共子串是 `aba`,也就是最长回文子串。 - -但是这个思路是错误的,比如说字符串 `aacxycaa`,反转之后是 `aacyxcaa`,最长公共子串是 `aac`,但是最长回文子串应该是 `aa`。 - -虽然这个思路不正确,但是**这种把问题转化为其他形式的思考方式是非常值得提倡的**。 - -下面,就来说一下正确的思路,如何使用双指针。 - -**寻找回文串的问题核心思想是:从中间开始向两边扩散来判断回文串**。对于最长回文子串,就是这个意思: - -```python -for 0 <= i < len(s): - 找到以 s[i] 为中心的回文串 - 更新答案 -``` - -但是呢,我们刚才也说了,回文串的长度可能是奇数也可能是偶数,如果是 `abba`这种情况,没有一个中心字符,上面的算法就没辙了。所以我们可以修改一下: - -```python -for 0 <= i < len(s): - 找到以 s[i] 为中心的回文串 - 找到以 s[i] 和 s[i+1] 为中心的回文串 - 更新答案 -``` - -PS:读者可能发现这里的索引会越界,等会会处理。 - -### 二、代码实现 - -按照上面的思路,先要实现一个函数来寻找最长回文串,这个函数是有点技巧的: - -```cpp -string palindrome(string& s, int l, int r) { - // 防止索引越界 - while (l >= 0 && r < s.size() - && s[l] == s[r]) { - // 向两边展开 - l--; r++; - } - // 返回以 s[l] 和 s[r] 为中心的最长回文串 - return s.substr(l + 1, r - l - 1); -} -``` - -为什么要传入两个指针 `l` 和 `r` 呢?**因为这样实现可以同时处理回文串长度为奇数和偶数的情况**: - -```python -for 0 <= i < len(s): - # 找到以 s[i] 为中心的回文串 - palindrome(s, i, i) - # 找到以 s[i] 和 s[i+1] 为中心的回文串 - palindrome(s, i, i + 1) - 更新答案 -``` - -下面看下 `longestPalindrome` 的完整代码: - -```cpp -string longestPalindrome(string s) { - string res; - for (int i = 0; i < s.size(); i++) { - // 以 s[i] 为中心的最长回文子串 - string s1 = palindrome(s, i, i); - // 以 s[i] 和 s[i+1] 为中心的最长回文子串 - string s2 = palindrome(s, i, i + 1); - // res = longest(res, s1, s2) - res = res.size() > s1.size() ? res : s1; - res = res.size() > s2.size() ? res : s2; - } - return res; -} -``` - -至此,这道最长回文子串的问题就解决了,时间复杂度 O(N^2),空间复杂度 O(1)。 - -值得一提的是,这个问题可以用动态规划方法解决,时间复杂度一样,但是空间复杂度至少要 O(N^2) 来存储 DP table。这道题是少有的动态规划非最优解法的问题。 - -另外,这个问题还有一个巧妙的解法,时间复杂度只需要 O(N),不过该解法比较复杂,我个人认为没必要掌握。该算法的名字叫 Manacher's Algorithm(马拉车算法),有兴趣的读者可以自行搜索一下。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/interview/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" "b/interview/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" deleted file mode 100644 index 40c1e2855b..0000000000 --- "a/interview/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" +++ /dev/null @@ -1,107 +0,0 @@ -# 如何寻找消失的元素 - -之前也有文章写过几个有趣的智力题,今天再聊一道巧妙的题目。 - -题目非常简单: - -![](../pictures/缺失元素/title.png) - -给一个长度为 n 的数组,其索引应该在 `[0,n)`,但是现在你要装进去 n + 1 个元素 `[0,n]`,那么肯定有一个元素装不下嘛,请你找出这个缺失的元素。 - -这道题不难的,我们应该很容易想到,把这个数组排个序,然后遍历一遍,不就很容易找到缺失的那个元素了吗? - -或者说,借助数据结构的特性,用一个 HashSet 把数组里出现的数字都储存下来,再遍历 `[0,n]` 之间的数字,去 HashSet 中查询,也可以很容易查出那个缺失的元素。 - -排序解法的时间复杂度是 O(NlogN),HashSet 的解法时间复杂度是 O(N),但是还需要 O(N) 的空间复杂度存储 HashSet。 - -**第三种方法是位运算**。 - -对于异或运算(`^`),我们知道它有一个特殊性质:一个数和它本身做异或运算结果为 0,一个数和 0 做异或运算还是它本身。 - -而且异或运算满足交换律和结合律,也就是说: - -2 ^ 3 ^ 2 = 3 ^ (2 ^ 2) = 3 ^ 0 = 3 - -而这道题索就可以通过这些性质巧妙算出缺失的那个元素。比如说 `nums = [0,3,1,4]`: - -![](../pictures/缺失元素/1.jpg) - - -为了容易理解,我们假设先把索引补一位,然后让每个元素和自己相等的索引相对应: - -![](../pictures/缺失元素/2.jpg) - - -这样做了之后,就可以发现除了缺失元素之外,所有的索引和元素都组成一对儿了,现在如果把这个落单的索引 2 找出来,也就找到了缺失的那个元素。 - -如何找这个落单的数字呢,**只要把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下**,也就达到了我们的目的。 - -```java -int missingNumber(int[] nums) { - int n = nums.length; - int res = 0; - // 先和新补的索引异或一下 - res ^= n; - // 和其他的元素、索引做异或 - for (int i = 0; i < n; i++) - res ^= i ^ nums[i]; - return res; -} -``` - -![](../pictures/缺失元素/3.jpg) - -由于异或运算满足交换律和结合律,所以总是能把成对儿的数字消去,留下缺失的那个元素的。 - -至此,时间复杂度 O(N),空间复杂度 O(1),已经达到了最优,我们是否就应该打道回府了呢? - -如果这样想,说明我们受算法的毒害太深,随着我们学习的知识越来越多,反而容易陷入思维定式,这个问题其实还有一个特别简单的解法:**等差数列求和公式**。 - -题目的意思可以这样理解:现在有个等差数列 0, 1, 2,..., n,其中少了某一个数字,请你把它找出来。那这个数字不就是 `sum(0,1,..n) - sum(nums)` 嘛? - -```java -int missingNumber(int[] nums) { - int n = nums.length; - // 公式:(首项 + 末项) * 项数 / 2 - int expect = (0 + n) * (n + 1) / 2; - - int sum = 0; - for (int x : nums) - sum += x; - return expect - sum; -``` - -你看,这种解法应该是最简单的,但说实话,我自己也没想到这个解法,而且我去问了几个大佬,他们也没想到这个最简单的思路。相反,如果去问一个初中生,他也许很快就能想到。 - -做到这一步了,我们是否就应该打道回府了呢? - -如果这样想,说明我们对细节的把控还差点火候。在用求和公式计算 `expect` 时,你考虑过**整型溢出**吗?如果相乘的结果太大导致溢出,那么结果肯定是错误的。 - -刚才我们的思路是把两个和都加出来然后相减,为了避免溢出,干脆一边求和一边减算了。很类似刚才位运算解法的思路,仍然假设 `nums = [0,3,1,4]`,先补一位索引再让元素跟索引配对: - -![](../pictures/缺失元素/xor.png) - - -我们让每个索引减去其对应的元素,再把相减的结果加起来,不就是那个缺失的元素吗? - -```java -public int missingNumber(int[] nums) { - int n = nums.length; - int res = 0; - // 新补的索引 - res += n - 0; - // 剩下索引和元素的差加起来 - for (int i = 0; i < n; i++) - res += i - nums[i]; - return res; -} -``` - -由于加减法满足交换律和结合律,所以总是能把成对儿的数字消去,留下缺失的那个元素的。 - -至此这道算法题目经历九曲十八弯,终于再也没有什么坑了。 - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/interview/\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/interview/\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" deleted file mode 100644 index 451e9c7db0..0000000000 --- "a/interview/\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" +++ /dev/null @@ -1,112 +0,0 @@ -今天就聊一道很看起来简单却十分巧妙的问题,寻找缺失和重复的元素。之前的一篇文章「寻找缺失元素」也写过类似的问题,不过这次的和上次的问题使用的技巧不同。 - -这是 LeetCode 645 题,我来描述一下这个题目: - -给一个长度为 `N` 的数组 `nums`,其中本来装着 `[1..N]` 这 `N` 个元素,无序。但是现在出现了一些错误,`nums` 中的一个元素出现了重复,也就同时导致了另一个元素的缺失。请你写一个算法,找到 `nums` 中的重复元素和缺失元素的值。 - -```cpp -// 返回两个数字,分别是 {dup, missing} -vector findErrorNums(vector& nums); -``` - -比如说输入:`nums = [1,2,2,4]`,算法返回 `[2,3]`。 - -其实很容易解决这个问题,先遍历一次数组,用一个哈希表记录每个数字出现的次数,然后遍历一次 `[1..N]`,看看那个元素重复出现,那个元素没有出现,就 OK 了。 - -但问题是,这个常规解法需要一个哈希表,也就是 O(N) 的空间复杂度。你看题目给的条件那么巧,在 `[1..N]` 的几个数字中恰好有一个重复,一个缺失,**事出反常必有妖**,对吧。 - -O(N) 的时间复杂度遍历数组是无法避免的,所以我们可以想想办法如何降低空间复杂度,是否可以在 O(1) 的空间复杂度之下找到重复和确实的元素呢? - -### 思路分析 - -这个问题的特点是,每个元素和数组索引有一定的对应关系。 - -我们现在自己改造下问题,**暂且将 `nums` 中的元素变为 `[0..N-1]`,这样每个元素就和一个数组索引完全对应了,这样方便理解一些**。 - -如果说 `nums` 中不存在重复元素和缺失元素,那么每个元素就和唯一一个索引值对应,对吧? - -现在的问题是,有一个元素重复了,同时导致一个元素缺失了,这会产生什么现象呢?**会导致有两个元素对应到了同一个索引,而且会有一个索引没有元素对应过去**。 - -那么,如果我能够通过某些方法,找到这个重复对应的索引,不就是找到了那个重复元素么?找到那个没有元素对应的索引,不就是找到了那个缺失的元素了么? - -那么,如何不使用额外空间判断某个索引有多少个元素对应呢?这就是这个问题的精妙之处了: - -**通过将每个索引对应的元素变成负数,以表示这个索引被对应过一次了**: - -![](../pictures/dupmissing/1.gif) - -如果出现重复元素 `4`,直观结果就是,索引 `4` 所对应的元素已经是负数了: - -![](../pictures/dupmissing/2.jpg) - -对于缺失元素 `3`,直观结果就是,索引 `3` 所对应的元素是正数: - -![](../pictures/dupmissing/3.jpg) - -对于这个现象,我们就可以翻译成代码了: - -```cpp -vector findErrorNums(vector& nums) { - int n = nums.size(); - int dup = -1; - for (int i = 0; i < n; i++) { - int index = abs(nums[i]); - // nums[index] 小于 0 则说明重复访问 - if (nums[index] < 0) - dup = abs(nums[i]); - else - nums[index] *= -1; - } - - int missing = -1; - for (int i = 0; i < n; i++) - // nums[i] 大于 0 则说明没有访问 - if (nums[i] > 0) - missing = i; - - return {dup, missing}; -} -``` - -这个问题就基本解决了,别忘了我们刚才为了方便分析,假设元素是 `[0..N-1]`,但题目要求是 `[1..N]`,所以只要简单修改两处地方即可得到原题的答案: - -```cpp -vector findErrorNums(vector& nums) { - int n = nums.size(); - int dup = -1; - for (int i = 0; i < n; i++) { - // 现在的元素是从 1 开始的 - int index = abs(nums[i]) - 1; - if (nums[index] < 0) - dup = abs(nums[i]); - else - nums[index] *= -1; - } - - int missing = -1; - for (int i = 0; i < n; i++) - if (nums[i] > 0) - // 将索引转换成元素 - missing = i + 1; - - return {dup, missing}; -} -``` - -其实,元素从 1 开始是有道理的,也必须从一个非零数开始。因为如果元素从 0 开始,那么 0 的相反数还是自己,所以如果数字 0 出现了重复或者缺失,算法就无法判断 0 是否被访问过。我们之前的假设只是为了简化题目,更通俗易懂。 - -### 最后总结 - -对于这种数组问题,**关键点在于元素和索引是成对儿出现的,常用的方法是排序、异或、映射**。 - -映射的思路就是我们刚才的分析,将每个索引和元素映射起来,通过正负号记录某个元素是否被映射。 - -排序的方法也很好理解,对于这个问题,可以想象如果元素都被从小到大排序,如果发现索引对应的元素如果不相符,就可以找到重复和缺失的元素。 - -异或运算也是常用的,因为异或性质 `a ^ a = 0, a ^ 0 = a`,如果将索引和元素同时异或,就可以消除成对儿的索引和元素,留下的就是重复或者缺失的元素。可以看看前文「寻找缺失元素」,介绍过这种方法。 - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git a/pictures/4keyboard/title.png b/pictures/4keyboard/title.png index b5b71adb9e..35a7c5c12d 100644 Binary files a/pictures/4keyboard/title.png and b/pictures/4keyboard/title.png differ diff --git "a/pictures/\344\275\215\346\223\215\344\275\234/1.png" b/pictures/BitManipulation/1.png similarity index 100% rename from "pictures/\344\275\215\346\223\215\344\275\234/1.png" rename to pictures/BitManipulation/1.png diff --git "a/pictures/\344\275\215\346\223\215\344\275\234/title.png" b/pictures/BitManipulation/title.png similarity index 100% rename from "pictures/\344\275\215\346\223\215\344\275\234/title.png" rename to pictures/BitManipulation/title.png diff --git a/pictures/DetailedBinarySearch/1.jpg b/pictures/DetailedBinarySearch/1.jpg new file mode 100644 index 0000000000..0bc9bd3d82 Binary files /dev/null and b/pictures/DetailedBinarySearch/1.jpg differ diff --git a/pictures/DetailedBinarySearch/2.jpg b/pictures/DetailedBinarySearch/2.jpg new file mode 100644 index 0000000000..be0d71bf90 Binary files /dev/null and b/pictures/DetailedBinarySearch/2.jpg differ diff --git a/pictures/DetailedBinarySearch/3.jpg b/pictures/DetailedBinarySearch/3.jpg new file mode 100644 index 0000000000..e3d832a83d Binary files /dev/null and b/pictures/DetailedBinarySearch/3.jpg differ diff --git a/pictures/DetailedBinarySearch/4.jpg b/pictures/DetailedBinarySearch/4.jpg new file mode 100644 index 0000000000..594ccd122e Binary files /dev/null and b/pictures/DetailedBinarySearch/4.jpg differ diff --git "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch1.png" b/pictures/DetailedBinarySearch/binarySearch1.png similarity index 100% rename from "pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch1.png" rename to pictures/DetailedBinarySearch/binarySearch1.png diff --git "a/pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch2.png" b/pictures/DetailedBinarySearch/binarySearch2.png similarity index 100% rename from "pictures/\344\272\214\345\210\206\346\237\245\346\211\276/binarySearch2.png" rename to pictures/DetailedBinarySearch/binarySearch2.png diff --git a/pictures/DetailedBinarySearch/poem.png b/pictures/DetailedBinarySearch/poem.png new file mode 100644 index 0000000000..a200d10217 Binary files /dev/null and b/pictures/DetailedBinarySearch/poem.png differ diff --git a/pictures/DetailedBinarySearch/verse.jpg b/pictures/DetailedBinarySearch/verse.jpg new file mode 100644 index 0000000000..b597c63ab2 Binary files /dev/null and b/pictures/DetailedBinarySearch/verse.jpg differ diff --git "a/pictures/\345\217\214\346\214\207\351\222\210/1.png" b/pictures/DoublePointerTechnique/1.png similarity index 100% rename from "pictures/\345\217\214\346\214\207\351\222\210/1.png" rename to pictures/DoublePointerTechnique/1.png diff --git "a/pictures/\345\217\214\346\214\207\351\222\210/2.png" b/pictures/DoublePointerTechnique/2.png similarity index 100% rename from "pictures/\345\217\214\346\214\207\351\222\210/2.png" rename to pictures/DoublePointerTechnique/2.png diff --git "a/pictures/\345\217\214\346\214\207\351\222\210/3.png" b/pictures/DoublePointerTechnique/3.png similarity index 100% rename from "pictures/\345\217\214\346\214\207\351\222\210/3.png" rename to pictures/DoublePointerTechnique/3.png diff --git "a/pictures/\345\217\214\346\214\207\351\222\210/center.png" b/pictures/DoublePointerTechnique/center.png similarity index 100% rename from "pictures/\345\217\214\346\214\207\351\222\210/center.png" rename to pictures/DoublePointerTechnique/center.png diff --git "a/pictures/\345\217\214\346\214\207\351\222\210/title.png" b/pictures/DoublePointerTechnique/title.png similarity index 100% rename from "pictures/\345\217\214\346\214\207\351\222\210/title.png" rename to pictures/DoublePointerTechnique/title.png diff --git "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/1.png" b/pictures/GameProblems/1.png similarity index 100% rename from "pictures/\345\215\232\345\274\210\351\227\256\351\242\230/1.png" rename to pictures/GameProblems/1.png diff --git "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/2.png" b/pictures/GameProblems/2.png similarity index 100% rename from "pictures/\345\215\232\345\274\210\351\227\256\351\242\230/2.png" rename to pictures/GameProblems/2.png diff --git "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/3.png" b/pictures/GameProblems/3.png similarity index 100% rename from "pictures/\345\215\232\345\274\210\351\227\256\351\242\230/3.png" rename to pictures/GameProblems/3.png diff --git "a/pictures/\345\215\232\345\274\210\351\227\256\351\242\230/4.png" b/pictures/GameProblems/4.png similarity index 100% rename from "pictures/\345\215\232\345\274\210\351\227\256\351\242\230/4.png" rename to pictures/GameProblems/4.png diff --git "a/pictures/LRU\347\256\227\346\263\225/1.jpg" b/pictures/LRU/1.jpg similarity index 100% rename from "pictures/LRU\347\256\227\346\263\225/1.jpg" rename to pictures/LRU/1.jpg diff --git "a/pictures/LRU\347\256\227\346\263\225/2.jpg" b/pictures/LRU/2.jpg similarity index 100% rename from "pictures/LRU\347\256\227\346\263\225/2.jpg" rename to pictures/LRU/2.jpg diff --git "a/pictures/LRU\347\256\227\346\263\225/3.jpg" b/pictures/LRU/3.jpg similarity index 100% rename from "pictures/LRU\347\256\227\346\263\225/3.jpg" rename to pictures/LRU/3.jpg diff --git "a/pictures/LRU\347\256\227\346\263\225/4.jpg" b/pictures/LRU/4.jpg similarity index 100% rename from "pictures/LRU\347\256\227\346\263\225/4.jpg" rename to pictures/LRU/4.jpg diff --git a/pictures/LRU/5.jpg b/pictures/LRU/5.jpg new file mode 100644 index 0000000000..ff62ccb555 Binary files /dev/null and b/pictures/LRU/5.jpg differ diff --git "a/pictures/\345\215\225\350\260\203\346\240\210/1.png" b/pictures/MonotonicStack/1.png similarity index 100% rename from "pictures/\345\215\225\350\260\203\346\240\210/1.png" rename to pictures/MonotonicStack/1.png diff --git "a/pictures/\345\215\225\350\260\203\346\240\210/2.png" b/pictures/MonotonicStack/2.png similarity index 100% rename from "pictures/\345\215\225\350\260\203\346\240\210/2.png" rename to pictures/MonotonicStack/2.png diff --git "a/pictures/\345\215\225\350\260\203\346\240\210/3.png" b/pictures/MonotonicStack/3.png similarity index 100% rename from "pictures/\345\215\225\350\260\203\346\240\210/3.png" rename to pictures/MonotonicStack/3.png diff --git a/pictures/Remove_Duplicates_from_Sorted_Array/1.gif b/pictures/Remove_Duplicates_from_Sorted_Array/1.gif new file mode 100644 index 0000000000..c316a8a857 Binary files /dev/null and b/pictures/Remove_Duplicates_from_Sorted_Array/1.gif differ diff --git a/pictures/Remove_Duplicates_from_Sorted_Array/2.gif b/pictures/Remove_Duplicates_from_Sorted_Array/2.gif new file mode 100644 index 0000000000..0c846ea4ad Binary files /dev/null and b/pictures/Remove_Duplicates_from_Sorted_Array/2.gif differ diff --git a/pictures/Remove_Duplicates_from_Sorted_Array/title.png b/pictures/Remove_Duplicates_from_Sorted_Array/title.png new file mode 100644 index 0000000000..47c0ab0faf Binary files /dev/null and b/pictures/Remove_Duplicates_from_Sorted_Array/title.png differ diff --git a/pictures/Remove_Duplicates_from_Sorted_Array/title1.jpg b/pictures/Remove_Duplicates_from_Sorted_Array/title1.jpg new file mode 100644 index 0000000000..3d72bd2812 Binary files /dev/null and b/pictures/Remove_Duplicates_from_Sorted_Array/title1.jpg differ diff --git a/pictures/Sliding_window/0.png b/pictures/Sliding_window/0.png new file mode 100644 index 0000000000..766f30eb6c Binary files /dev/null and b/pictures/Sliding_window/0.png differ diff --git a/pictures/Sliding_window/1.png b/pictures/Sliding_window/1.png new file mode 100644 index 0000000000..d031c6d535 Binary files /dev/null and b/pictures/Sliding_window/1.png differ diff --git a/pictures/Sliding_window/2.png b/pictures/Sliding_window/2.png new file mode 100644 index 0000000000..98c0d6b548 Binary files /dev/null and b/pictures/Sliding_window/2.png differ diff --git a/pictures/Sliding_window/3.png b/pictures/Sliding_window/3.png new file mode 100644 index 0000000000..1d7230138f Binary files /dev/null and b/pictures/Sliding_window/3.png differ diff --git a/pictures/Sliding_window/title1.jpg b/pictures/Sliding_window/title1.jpg new file mode 100644 index 0000000000..aadf2e2127 Binary files /dev/null and b/pictures/Sliding_window/title1.jpg differ diff --git a/pictures/Sliding_window/title2.jpg b/pictures/Sliding_window/title2.jpg new file mode 100644 index 0000000000..649b4cfe75 Binary files /dev/null and b/pictures/Sliding_window/title2.jpg differ diff --git a/pictures/Sliding_window/title3.jpg b/pictures/Sliding_window/title3.jpg new file mode 100644 index 0000000000..646f4be38d Binary files /dev/null and b/pictures/Sliding_window/title3.jpg differ diff --git a/pictures/SuperEggDrop/1.jpg b/pictures/SuperEggDrop/1.jpg new file mode 100644 index 0000000000..a81c3e3669 Binary files /dev/null and b/pictures/SuperEggDrop/1.jpg differ diff --git "a/pictures/\346\211\224\351\270\241\350\233\213/2.jpg" b/pictures/SuperEggDrop/2.jpg similarity index 100% rename from "pictures/\346\211\224\351\270\241\350\233\213/2.jpg" rename to pictures/SuperEggDrop/2.jpg diff --git a/pictures/SuperEggDrop/3.jpg b/pictures/SuperEggDrop/3.jpg new file mode 100644 index 0000000000..2401a1f04c Binary files /dev/null and b/pictures/SuperEggDrop/3.jpg differ diff --git "a/pictures/\346\211\224\351\270\241\350\233\213/dp.png" b/pictures/SuperEggDrop/dp.png similarity index 100% rename from "pictures/\346\211\224\351\270\241\350\233\213/dp.png" rename to pictures/SuperEggDrop/dp.png diff --git a/pictures/algo4/1.jpg b/pictures/algo4/1.jpg index bc5c41c00e..b01e7b8cf8 100644 Binary files a/pictures/algo4/1.jpg and b/pictures/algo4/1.jpg differ diff --git a/pictures/algo4/2.jpg b/pictures/algo4/2.jpg index 533f783eb6..5a5be0f019 100644 Binary files a/pictures/algo4/2.jpg and b/pictures/algo4/2.jpg differ diff --git a/pictures/algo4/title.png b/pictures/algo4/title.png index aabe966dfb..1efbe973a4 100644 Binary files a/pictures/algo4/title.png and b/pictures/algo4/title.png differ diff --git a/pictures/backtrack/ink-image (1).png b/pictures/backtrack/ink-image (1).png deleted file mode 100644 index 1cf2957d12..0000000000 Binary files a/pictures/backtrack/ink-image (1).png and /dev/null differ diff --git a/pictures/backtrack/ink-image (2).png b/pictures/backtrack/ink-image (2).png deleted file mode 100644 index bf791a9117..0000000000 Binary files a/pictures/backtrack/ink-image (2).png and /dev/null differ diff --git a/pictures/backtrack/ink-image (3).png b/pictures/backtrack/ink-image (3).png deleted file mode 100644 index 0ffa62c1d6..0000000000 Binary files a/pictures/backtrack/ink-image (3).png and /dev/null differ diff --git a/pictures/backtrack/ink-image (4).png b/pictures/backtrack/ink-image (4).png deleted file mode 100644 index 6dcafa1beb..0000000000 Binary files a/pictures/backtrack/ink-image (4).png and /dev/null differ diff --git a/pictures/backtrack/ink-image (5).png b/pictures/backtrack/ink-image (5).png deleted file mode 100644 index e4d2130ee1..0000000000 Binary files a/pictures/backtrack/ink-image (5).png and /dev/null differ diff --git a/pictures/backtrack/ink-image (6).png b/pictures/backtrack/ink-image (6).png deleted file mode 100644 index f6fc968bea..0000000000 Binary files a/pictures/backtrack/ink-image (6).png and /dev/null differ diff --git a/pictures/backtrack/ink-image.png b/pictures/backtrack/ink-image.png deleted file mode 100644 index ec1ea64d50..0000000000 Binary files a/pictures/backtrack/ink-image.png and /dev/null differ diff --git a/pictures/backtrack/nqueens.png b/pictures/backtrack/nqueens.png deleted file mode 100644 index 4c9e01f560..0000000000 Binary files a/pictures/backtrack/nqueens.png and /dev/null differ diff --git a/pictures/backtrack/permutation.png b/pictures/backtrack/permutation.png deleted file mode 100644 index 421a36e67e..0000000000 Binary files a/pictures/backtrack/permutation.png and /dev/null differ diff --git "a/pictures/backtrack/\344\273\243\347\240\201.png" "b/pictures/backtrack/\344\273\243\347\240\201.png" deleted file mode 100644 index 85be66b34e..0000000000 Binary files "a/pictures/backtrack/\344\273\243\347\240\201.png" and /dev/null differ diff --git "a/pictures/backtrack/\344\273\243\347\240\2011.png" "b/pictures/backtrack/\344\273\243\347\240\2011.png" deleted file mode 100644 index b029f5f776..0000000000 Binary files "a/pictures/backtrack/\344\273\243\347\240\2011.png" and /dev/null differ diff --git "a/pictures/backtrack/\344\273\243\347\240\2012.png" "b/pictures/backtrack/\344\273\243\347\240\2012.png" deleted file mode 100644 index ae4a5734ee..0000000000 Binary files "a/pictures/backtrack/\344\273\243\347\240\2012.png" and /dev/null differ diff --git "a/pictures/backtrack/\344\273\243\347\240\2013.png" "b/pictures/backtrack/\344\273\243\347\240\2013.png" deleted file mode 100644 index c6d842db5e..0000000000 Binary files "a/pictures/backtrack/\344\273\243\347\240\2013.png" and /dev/null differ diff --git "a/pictures/backtrack/\345\205\250\346\216\222\345\210\227.png" "b/pictures/backtrack/\345\205\250\346\216\222\345\210\227.png" deleted file mode 100644 index 6fb4f92f97..0000000000 Binary files "a/pictures/backtrack/\345\205\250\346\216\222\345\210\227.png" and /dev/null differ diff --git a/pictures/backtracking/1.jpg b/pictures/backtracking/1.jpg deleted file mode 100644 index e54c8a3c20..0000000000 Binary files a/pictures/backtracking/1.jpg and /dev/null differ diff --git a/pictures/backtracking/1en.jpg b/pictures/backtracking/1en.jpg new file mode 100644 index 0000000000..037e80782f Binary files /dev/null and b/pictures/backtracking/1en.jpg differ diff --git a/pictures/backtracking/2.jpg b/pictures/backtracking/2.jpg deleted file mode 100644 index ff57c7fdd3..0000000000 Binary files a/pictures/backtracking/2.jpg and /dev/null differ diff --git a/pictures/backtracking/2en.jpg b/pictures/backtracking/2en.jpg new file mode 100644 index 0000000000..b6d1d8d5b9 Binary files /dev/null and b/pictures/backtracking/2en.jpg differ diff --git a/pictures/backtracking/3.jpg b/pictures/backtracking/3.jpg deleted file mode 100644 index ca97cdaa03..0000000000 Binary files a/pictures/backtracking/3.jpg and /dev/null differ diff --git a/pictures/backtracking/3en.jpg b/pictures/backtracking/3en.jpg new file mode 100644 index 0000000000..54becf8eff Binary files /dev/null and b/pictures/backtracking/3en.jpg differ diff --git a/pictures/backtracking/3en.png b/pictures/backtracking/3en.png new file mode 100644 index 0000000000..352c062f7e Binary files /dev/null and b/pictures/backtracking/3en.png differ diff --git a/pictures/backtracking/4.jpg b/pictures/backtracking/4.jpg deleted file mode 100644 index b93cafd72e..0000000000 Binary files a/pictures/backtracking/4.jpg and /dev/null differ diff --git a/pictures/backtracking/4en.jpg b/pictures/backtracking/4en.jpg new file mode 100644 index 0000000000..bbe827f4ae Binary files /dev/null and b/pictures/backtracking/4en.jpg differ diff --git a/pictures/backtracking/5.jpg b/pictures/backtracking/5.jpg deleted file mode 100644 index 893c87e682..0000000000 Binary files a/pictures/backtracking/5.jpg and /dev/null differ diff --git a/pictures/backtracking/5en.jpg b/pictures/backtracking/5en.jpg new file mode 100644 index 0000000000..5ff679b96f Binary files /dev/null and b/pictures/backtracking/5en.jpg differ diff --git a/pictures/backtracking/6.jpg b/pictures/backtracking/6.jpg deleted file mode 100644 index 0b4d68eea5..0000000000 Binary files a/pictures/backtracking/6.jpg and /dev/null differ diff --git a/pictures/backtracking/6en.jpg b/pictures/backtracking/6en.jpg new file mode 100644 index 0000000000..a63035ce59 Binary files /dev/null and b/pictures/backtracking/6en.jpg differ diff --git a/pictures/backtracking/7.jpg b/pictures/backtracking/7.jpg deleted file mode 100644 index 1eee2f8c36..0000000000 Binary files a/pictures/backtracking/7.jpg and /dev/null differ diff --git a/pictures/backtracking/7en.jpg b/pictures/backtracking/7en.jpg new file mode 100644 index 0000000000..f9ba03ed63 Binary files /dev/null and b/pictures/backtracking/7en.jpg differ diff --git a/pictures/binarySearch/binarySearch1.png b/pictures/binarySearch/binarySearch1.png new file mode 100644 index 0000000000..911bde51e7 Binary files /dev/null and b/pictures/binarySearch/binarySearch1.png differ diff --git a/pictures/binarySearch/binarySearch2.png b/pictures/binarySearch/binarySearch2.png new file mode 100644 index 0000000000..d6ca59b939 Binary files /dev/null and b/pictures/binarySearch/binarySearch2.png differ diff --git "a/pictures/\350\256\276\350\256\241Twitter/design.png" b/pictures/design_Twitter/design.png similarity index 100% rename from "pictures/\350\256\276\350\256\241Twitter/design.png" rename to pictures/design_Twitter/design.png diff --git "a/pictures/\350\256\276\350\256\241Twitter/merge.gif" b/pictures/design_Twitter/merge.gif similarity index 100% rename from "pictures/\350\256\276\350\256\241Twitter/merge.gif" rename to pictures/design_Twitter/merge.gif diff --git "a/pictures/\350\256\276\350\256\241Twitter/tweet.jpg" b/pictures/design_Twitter/tweet.jpg similarity index 100% rename from "pictures/\350\256\276\350\256\241Twitter/tweet.jpg" rename to pictures/design_Twitter/tweet.jpg diff --git "a/pictures/\350\256\276\350\256\241Twitter/user.jpg" b/pictures/design_Twitter/user.jpg similarity index 100% rename from "pictures/\350\256\276\350\256\241Twitter/user.jpg" rename to pictures/design_Twitter/user.jpg diff --git a/pictures/double_pointer/11.png b/pictures/double_pointer/11.png new file mode 100644 index 0000000000..868b0afbc4 Binary files /dev/null and b/pictures/double_pointer/11.png differ diff --git a/pictures/double_pointer/22.png b/pictures/double_pointer/22.png new file mode 100644 index 0000000000..2f4960e335 Binary files /dev/null and b/pictures/double_pointer/22.png differ diff --git a/pictures/double_pointer/33.png b/pictures/double_pointer/33.png new file mode 100644 index 0000000000..4f73176dd7 Binary files /dev/null and b/pictures/double_pointer/33.png differ diff --git a/pictures/double_pointer/cyc1.png b/pictures/double_pointer/cyc1.png new file mode 100644 index 0000000000..4ecada73c7 Binary files /dev/null and b/pictures/double_pointer/cyc1.png differ diff --git a/pictures/double_pointer/cyc2.png b/pictures/double_pointer/cyc2.png new file mode 100644 index 0000000000..7c8743ae28 Binary files /dev/null and b/pictures/double_pointer/cyc2.png differ diff --git a/pictures/editDistance/1.jpg b/pictures/editDistance/1.jpg index 7a2e3d58f4..465089f422 100644 Binary files a/pictures/editDistance/1.jpg and b/pictures/editDistance/1.jpg differ diff --git a/pictures/editDistance/3.jpg b/pictures/editDistance/3.jpg index c201c6daa2..393e3a9965 100644 Binary files a/pictures/editDistance/3.jpg and b/pictures/editDistance/3.jpg differ diff --git a/pictures/editDistance/4.jpg b/pictures/editDistance/4.jpg index 13631d1edf..6d1d9c8cd9 100644 Binary files a/pictures/editDistance/4.jpg and b/pictures/editDistance/4.jpg differ diff --git a/pictures/editDistance/5.jpg b/pictures/editDistance/5.jpg index 8f5c006323..da5a33985a 100644 Binary files a/pictures/editDistance/5.jpg and b/pictures/editDistance/5.jpg differ diff --git a/pictures/editDistance/6.jpg b/pictures/editDistance/6.jpg index 589f8aef6b..f43c5748e6 100644 Binary files a/pictures/editDistance/6.jpg and b/pictures/editDistance/6.jpg differ diff --git a/pictures/editDistance/delete.gif b/pictures/editDistance/delete.gif index 36559b37dd..9aa4c990c7 100644 Binary files a/pictures/editDistance/delete.gif and b/pictures/editDistance/delete.gif differ diff --git a/pictures/editDistance/title.png b/pictures/editDistance/title.png index 85a392fbde..e67794be02 100644 Binary files a/pictures/editDistance/title.png and b/pictures/editDistance/title.png differ diff --git "a/pictures/floodfill/\346\212\240\345\233\276.jpeg" b/pictures/floodfill/cutout.jpeg similarity index 100% rename from "pictures/floodfill/\346\212\240\345\233\276.jpeg" rename to pictures/floodfill/cutout.jpeg diff --git "a/pictures/floodfill/\346\212\240\345\233\276.jpg" b/pictures/floodfill/cutout.jpg similarity index 100% rename from "pictures/floodfill/\346\212\240\345\233\276.jpg" rename to pictures/floodfill/cutout.jpg diff --git a/pictures/floodfill/leetcode_en.jpg b/pictures/floodfill/leetcode_en.jpg new file mode 100644 index 0000000000..9a25920211 Binary files /dev/null and b/pictures/floodfill/leetcode_en.jpg differ diff --git "a/pictures/floodfill/\346\211\253\351\233\267.png" b/pictures/floodfill/minesweeper.png similarity index 100% rename from "pictures/floodfill/\346\211\253\351\233\267.png" rename to pictures/floodfill/minesweeper.png diff --git a/pictures/floodfill/ppt1.PNG b/pictures/floodfill/ppt1.PNG index 22046455d0..10f6f15da7 100644 Binary files a/pictures/floodfill/ppt1.PNG and b/pictures/floodfill/ppt1.PNG differ diff --git a/pictures/floodfill/ppt2.PNG b/pictures/floodfill/ppt2.PNG index 28a220d614..bc1550da90 100644 Binary files a/pictures/floodfill/ppt2.PNG and b/pictures/floodfill/ppt2.PNG differ diff --git a/pictures/floodfill/ppt3.PNG b/pictures/floodfill/ppt3.PNG index 6957ffe4ae..1f1a173947 100644 Binary files a/pictures/floodfill/ppt3.PNG and b/pictures/floodfill/ppt3.PNG differ diff --git a/pictures/floodfill/ppt5.PNG b/pictures/floodfill/ppt5.PNG index b2583eef11..437e3ba331 100644 Binary files a/pictures/floodfill/ppt5.PNG and b/pictures/floodfill/ppt5.PNG differ diff --git a/pictures/linuxProcess/1.jpg b/pictures/linuxProcess/1.jpg index a3b4383b8a..5fb67bab40 100644 Binary files a/pictures/linuxProcess/1.jpg and b/pictures/linuxProcess/1.jpg differ diff --git a/pictures/linuxProcess/2.jpg b/pictures/linuxProcess/2.jpg index 821ff340fa..b79b089b61 100644 Binary files a/pictures/linuxProcess/2.jpg and b/pictures/linuxProcess/2.jpg differ diff --git a/pictures/linuxProcess/3.jpg b/pictures/linuxProcess/3.jpg index d7d3ae97ff..933c68c1d7 100644 Binary files a/pictures/linuxProcess/3.jpg and b/pictures/linuxProcess/3.jpg differ diff --git a/pictures/linuxProcess/4.jpg b/pictures/linuxProcess/4.jpg index 6ada0ef14e..f743584c77 100644 Binary files a/pictures/linuxProcess/4.jpg and b/pictures/linuxProcess/4.jpg differ diff --git a/pictures/linuxProcess/5.jpg b/pictures/linuxProcess/5.jpg index c4c51ca759..60c93dce0d 100644 Binary files a/pictures/linuxProcess/5.jpg and b/pictures/linuxProcess/5.jpg differ diff --git a/pictures/linuxProcess/6.jpg b/pictures/linuxProcess/6.jpg index c2637d131b..4508e95c95 100644 Binary files a/pictures/linuxProcess/6.jpg and b/pictures/linuxProcess/6.jpg differ diff --git a/pictures/linuxProcess/7.jpg b/pictures/linuxProcess/7.jpg index 6abd83436f..d50fb35204 100644 Binary files a/pictures/linuxProcess/7.jpg and b/pictures/linuxProcess/7.jpg differ diff --git a/pictures/linuxProcess/8.jpg b/pictures/linuxProcess/8.jpg index eafae087d3..32b8babf0e 100644 Binary files a/pictures/linuxProcess/8.jpg and b/pictures/linuxProcess/8.jpg differ diff --git "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/1.jpg" b/pictures/missing_elements/1.jpg similarity index 100% rename from "pictures/\347\274\272\345\244\261\345\205\203\347\264\240/1.jpg" rename to pictures/missing_elements/1.jpg diff --git "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/2.jpg" b/pictures/missing_elements/2.jpg similarity index 100% rename from "pictures/\347\274\272\345\244\261\345\205\203\347\264\240/2.jpg" rename to pictures/missing_elements/2.jpg diff --git "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/3.jpg" b/pictures/missing_elements/3.jpg similarity index 100% rename from "pictures/\347\274\272\345\244\261\345\205\203\347\264\240/3.jpg" rename to pictures/missing_elements/3.jpg diff --git "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/title.png" b/pictures/missing_elements/title.png similarity index 100% rename from "pictures/\347\274\272\345\244\261\345\205\203\347\264\240/title.png" rename to pictures/missing_elements/title.png diff --git a/pictures/missing_elements/title_en.jpg b/pictures/missing_elements/title_en.jpg new file mode 100644 index 0000000000..ac1e021802 Binary files /dev/null and b/pictures/missing_elements/title_en.jpg differ diff --git "a/pictures/\347\274\272\345\244\261\345\205\203\347\264\240/xor.png" b/pictures/missing_elements/xor.png similarity index 100% rename from "pictures/\347\274\272\345\244\261\345\205\203\347\264\240/xor.png" rename to pictures/missing_elements/xor.png diff --git "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/1.png" b/pictures/monotonic_queue/1.png similarity index 100% rename from "pictures/\345\215\225\350\260\203\351\230\237\345\210\227/1.png" rename to pictures/monotonic_queue/1.png diff --git "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/2.png" b/pictures/monotonic_queue/2.png similarity index 100% rename from "pictures/\345\215\225\350\260\203\351\230\237\345\210\227/2.png" rename to pictures/monotonic_queue/2.png diff --git "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/3.png" b/pictures/monotonic_queue/3.png similarity index 100% rename from "pictures/\345\215\225\350\260\203\351\230\237\345\210\227/3.png" rename to pictures/monotonic_queue/3.png diff --git a/pictures/monotonic_queue/title.png b/pictures/monotonic_queue/title.png new file mode 100644 index 0000000000..01ad8a5d03 Binary files /dev/null and b/pictures/monotonic_queue/title.png differ diff --git a/pictures/monotonic_stack/1.png b/pictures/monotonic_stack/1.png new file mode 100644 index 0000000000..a3729004bb Binary files /dev/null and b/pictures/monotonic_stack/1.png differ diff --git a/pictures/monotonic_stack/2.png b/pictures/monotonic_stack/2.png new file mode 100644 index 0000000000..bdc7ea6b48 Binary files /dev/null and b/pictures/monotonic_stack/2.png differ diff --git a/pictures/monotonic_stack/3.png b/pictures/monotonic_stack/3.png new file mode 100644 index 0000000000..71d78b84ea Binary files /dev/null and b/pictures/monotonic_stack/3.png differ diff --git a/pictures/online/1_english.png b/pictures/online/1_english.png new file mode 100644 index 0000000000..e80d6b5ed1 Binary files /dev/null and b/pictures/online/1_english.png differ diff --git a/pictures/online/2_english.png b/pictures/online/2_english.png new file mode 100644 index 0000000000..700366e7db Binary files /dev/null and b/pictures/online/2_english.png differ diff --git a/pictures/online/3_english.png b/pictures/online/3_english.png new file mode 100644 index 0000000000..5733191842 Binary files /dev/null and b/pictures/online/3_english.png differ diff --git a/pictures/online/4_english.png b/pictures/online/4_english.png new file mode 100644 index 0000000000..fe102eb7cb Binary files /dev/null and b/pictures/online/4_english.png differ diff --git a/pictures/online/5_english.png b/pictures/online/5_english.png new file mode 100644 index 0000000000..6a42f4f461 Binary files /dev/null and b/pictures/online/5_english.png differ diff --git a/pictures/online/6_english.png b/pictures/online/6_english.png new file mode 100644 index 0000000000..ac633b4817 Binary files /dev/null and b/pictures/online/6_english.png differ diff --git "a/pictures/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204/1.jpg" b/pictures/optimal_substructure/1.jpg similarity index 100% rename from "pictures/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204/1.jpg" rename to pictures/optimal_substructure/1.jpg diff --git a/pictures/palindrome/example.png b/pictures/palindrome/example.png new file mode 100644 index 0000000000..1a9f0562d7 Binary files /dev/null and b/pictures/palindrome/example.png differ diff --git "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/1.gif" b/pictures/palindromic_linkedlist/1.gif similarity index 100% rename from "pictures/\345\233\236\346\226\207\351\223\276\350\241\250/1.gif" rename to pictures/palindromic_linkedlist/1.gif diff --git a/pictures/palindromic_linkedlist/1.jpg b/pictures/palindromic_linkedlist/1.jpg new file mode 100644 index 0000000000..32ce16294b Binary files /dev/null and b/pictures/palindromic_linkedlist/1.jpg differ diff --git a/pictures/palindromic_linkedlist/2.jpg b/pictures/palindromic_linkedlist/2.jpg new file mode 100644 index 0000000000..f44d32a680 Binary files /dev/null and b/pictures/palindromic_linkedlist/2.jpg differ diff --git a/pictures/palindromic_linkedlist/3.jpg b/pictures/palindromic_linkedlist/3.jpg new file mode 100644 index 0000000000..5fc2188ebe Binary files /dev/null and b/pictures/palindromic_linkedlist/3.jpg differ diff --git "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/4.jpg" b/pictures/palindromic_linkedlist/4.jpg similarity index 100% rename from "pictures/\345\233\236\346\226\207\351\223\276\350\241\250/4.jpg" rename to pictures/palindromic_linkedlist/4.jpg diff --git "a/pictures/\345\211\215\347\274\200\345\222\214/1.jpg" b/pictures/prefix_sum/1.jpg similarity index 100% rename from "pictures/\345\211\215\347\274\200\345\222\214/1.jpg" rename to pictures/prefix_sum/1.jpg diff --git a/pictures/prefix_sum/2.jpg b/pictures/prefix_sum/2.jpg new file mode 100644 index 0000000000..e0afbbd5ba Binary files /dev/null and b/pictures/prefix_sum/2.jpg differ diff --git "a/pictures/\345\211\215\347\274\200\345\222\214/title.png" b/pictures/prefix_sum/title.png similarity index 100% rename from "pictures/\345\211\215\347\274\200\345\222\214/title.png" rename to pictures/prefix_sum/title.png diff --git a/pictures/prefix_sum/title_en.jpg b/pictures/prefix_sum/title_en.jpg new file mode 100644 index 0000000000..cd6177b5a5 Binary files /dev/null and b/pictures/prefix_sum/title_en.jpg differ diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/1.jpg" b/pictures/seat_scheduling/1.jpg similarity index 100% rename from "pictures/\345\272\247\344\275\215\350\260\203\345\272\246/1.jpg" rename to pictures/seat_scheduling/1.jpg diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/2.jpg" b/pictures/seat_scheduling/2.jpg similarity index 100% rename from "pictures/\345\272\247\344\275\215\350\260\203\345\272\246/2.jpg" rename to pictures/seat_scheduling/2.jpg diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/3.jpg" b/pictures/seat_scheduling/3.jpg similarity index 100% rename from "pictures/\345\272\247\344\275\215\350\260\203\345\272\246/3.jpg" rename to pictures/seat_scheduling/3.jpg diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/4.jpg" b/pictures/seat_scheduling/4.jpg similarity index 100% rename from "pictures/\345\272\247\344\275\215\350\260\203\345\272\246/4.jpg" rename to pictures/seat_scheduling/4.jpg diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/5.jpg" b/pictures/seat_scheduling/5.jpg similarity index 100% rename from "pictures/\345\272\247\344\275\215\350\260\203\345\272\246/5.jpg" rename to pictures/seat_scheduling/5.jpg diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/6.jpg" b/pictures/seat_scheduling/6.jpg similarity index 100% rename from "pictures/\345\272\247\344\275\215\350\260\203\345\272\246/6.jpg" rename to pictures/seat_scheduling/6.jpg diff --git "a/pictures/\345\272\247\344\275\215\350\260\203\345\272\246/7.jpg" b/pictures/seat_scheduling/7.jpg similarity index 100% rename from "pictures/\345\272\247\344\275\215\350\260\203\345\272\246/7.jpg" rename to pictures/seat_scheduling/7.jpg diff --git a/pictures/seat_scheduling/8.png b/pictures/seat_scheduling/8.png new file mode 100644 index 0000000000..ae0e883939 Binary files /dev/null and b/pictures/seat_scheduling/8.png differ diff --git a/pictures/seat_scheduling/9.png b/pictures/seat_scheduling/9.png new file mode 100644 index 0000000000..2c950f73d6 Binary files /dev/null and b/pictures/seat_scheduling/9.png differ diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/1.jpg" b/pictures/string_multiplication/1.jpg similarity index 100% rename from "pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/1.jpg" rename to pictures/string_multiplication/1.jpg diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/2.jpg" b/pictures/string_multiplication/2.jpg similarity index 100% rename from "pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/2.jpg" rename to pictures/string_multiplication/2.jpg diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/3.jpg" b/pictures/string_multiplication/3.jpg similarity index 100% rename from "pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/3.jpg" rename to pictures/string_multiplication/3.jpg diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/4.gif" b/pictures/string_multiplication/4.gif similarity index 100% rename from "pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/4.gif" rename to pictures/string_multiplication/4.gif diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/6.jpg" b/pictures/string_multiplication/6.jpg similarity index 100% rename from "pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/6.jpg" rename to pictures/string_multiplication/6.jpg diff --git "a/pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/title.png" b/pictures/string_multiplication/title.png similarity index 100% rename from "pictures/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225/title.png" rename to pictures/string_multiplication/title.png diff --git a/pictures/string_multiplication/title_en.jpg b/pictures/string_multiplication/title_en.jpg new file mode 100644 index 0000000000..4ac1c623a0 Binary files /dev/null and b/pictures/string_multiplication/title_en.jpg differ diff --git "a/pictures/\345\255\220\345\272\217\345\210\227/1.gif" b/pictures/subsequence/1.gif similarity index 100% rename from "pictures/\345\255\220\345\272\217\345\210\227/1.gif" rename to pictures/subsequence/1.gif diff --git "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/1.jpg" b/pictures/subsequence/1.jpg similarity index 100% rename from "pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/1.jpg" rename to pictures/subsequence/1.jpg diff --git "a/pictures/\345\255\220\345\272\217\345\210\227/2.gif" b/pictures/subsequence/2.gif similarity index 100% rename from "pictures/\345\255\220\345\272\217\345\210\227/2.gif" rename to pictures/subsequence/2.gif diff --git "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/2.jpg" b/pictures/subsequence/2.jpg similarity index 100% rename from "pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/2.jpg" rename to pictures/subsequence/2.jpg diff --git "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/3.jpg" b/pictures/subsequence/3.jpg similarity index 100% rename from "pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/3.jpg" rename to pictures/subsequence/3.jpg diff --git "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/4.jpg" b/pictures/subsequence/4.jpg similarity index 100% rename from "pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/4.jpg" rename to pictures/subsequence/4.jpg diff --git "a/pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/5.jpg" b/pictures/subsequence/5.jpg similarity index 100% rename from "pictures/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227/5.jpg" rename to pictures/subsequence/5.jpg diff --git a/pictures/trapping_rain_water/0.jpg b/pictures/trapping_rain_water/0.jpg new file mode 100644 index 0000000000..2d2e1cb645 Binary files /dev/null and b/pictures/trapping_rain_water/0.jpg differ diff --git a/pictures/trapping_rain_water/1.jpg b/pictures/trapping_rain_water/1.jpg new file mode 100644 index 0000000000..842c5582d3 Binary files /dev/null and b/pictures/trapping_rain_water/1.jpg differ diff --git a/pictures/trapping_rain_water/2.jpg b/pictures/trapping_rain_water/2.jpg new file mode 100644 index 0000000000..7fc3635c47 Binary files /dev/null and b/pictures/trapping_rain_water/2.jpg differ diff --git a/pictures/trapping_rain_water/3.jpg b/pictures/trapping_rain_water/3.jpg new file mode 100644 index 0000000000..b9403de6e5 Binary files /dev/null and b/pictures/trapping_rain_water/3.jpg differ diff --git a/pictures/trapping_rain_water/4.jpg b/pictures/trapping_rain_water/4.jpg new file mode 100644 index 0000000000..9906e07537 Binary files /dev/null and b/pictures/trapping_rain_water/4.jpg differ diff --git a/pictures/trapping_rain_water/5.jpg b/pictures/trapping_rain_water/5.jpg new file mode 100644 index 0000000000..3a6ffb56b2 Binary files /dev/null and b/pictures/trapping_rain_water/5.jpg differ diff --git a/pictures/trapping_rain_water/title.jpg b/pictures/trapping_rain_water/title.jpg new file mode 100644 index 0000000000..7268c37352 Binary files /dev/null and b/pictures/trapping_rain_water/title.jpg differ diff --git "a/pictures/unionfind\345\272\224\347\224\250/1.jpg" b/pictures/unionfind-application/1.jpg similarity index 100% rename from "pictures/unionfind\345\272\224\347\224\250/1.jpg" rename to pictures/unionfind-application/1.jpg diff --git "a/pictures/unionfind\345\272\224\347\224\250/2.jpg" b/pictures/unionfind-application/2.jpg similarity index 100% rename from "pictures/unionfind\345\272\224\347\224\250/2.jpg" rename to pictures/unionfind-application/2.jpg diff --git "a/pictures/unionfind\345\272\224\347\224\250/3.jpg" b/pictures/unionfind-application/3.jpg similarity index 100% rename from "pictures/unionfind\345\272\224\347\224\250/3.jpg" rename to pictures/unionfind-application/3.jpg diff --git "a/pictures/\345\211\215\347\274\200\345\222\214/2.jpg" "b/pictures/\345\211\215\347\274\200\345\222\214/2.jpg" deleted file mode 100644 index b0e9993d39..0000000000 Binary files "a/pictures/\345\211\215\347\274\200\345\222\214/2.jpg" and /dev/null differ diff --git "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/title.png" "b/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/title.png" deleted file mode 100644 index 62b1f9d26a..0000000000 Binary files "a/pictures/\345\215\225\350\260\203\351\230\237\345\210\227/title.png" and /dev/null differ diff --git "a/pictures/\345\233\236\346\226\207/title.png" "b/pictures/\345\233\236\346\226\207/title.png" deleted file mode 100644 index 5c6e77d9e6..0000000000 Binary files "a/pictures/\345\233\236\346\226\207/title.png" and /dev/null differ diff --git "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/1.jpg" "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/1.jpg" deleted file mode 100644 index be6f1d165f..0000000000 Binary files "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/1.jpg" and /dev/null differ diff --git "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/2.jpg" "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/2.jpg" deleted file mode 100644 index 217a7c9f4c..0000000000 Binary files "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/2.jpg" and /dev/null differ diff --git "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/3.jpg" "b/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/3.jpg" deleted file mode 100644 index fbe86512b8..0000000000 Binary files "a/pictures/\345\233\236\346\226\207\351\223\276\350\241\250/3.jpg" and /dev/null differ diff --git "a/pictures/\345\255\220\345\272\217\345\210\227/1.jpg" "b/pictures/\345\255\220\345\272\217\345\210\227/1.jpg" deleted file mode 100644 index a936d379e3..0000000000 Binary files "a/pictures/\345\255\220\345\272\217\345\210\227/1.jpg" and /dev/null differ diff --git "a/pictures/\345\255\220\345\272\217\345\210\227/2.jpg" "b/pictures/\345\255\220\345\272\217\345\210\227/2.jpg" deleted file mode 100644 index 21a0244d0b..0000000000 Binary files "a/pictures/\345\255\220\345\272\217\345\210\227/2.jpg" and /dev/null differ diff --git "a/pictures/\345\255\220\345\272\217\345\210\227/3.jpg" "b/pictures/\345\255\220\345\272\217\345\210\227/3.jpg" deleted file mode 100644 index b46eb2f158..0000000000 Binary files "a/pictures/\345\255\220\345\272\217\345\210\227/3.jpg" and /dev/null differ diff --git "a/pictures/\346\211\224\351\270\241\350\233\213/1.jpg" "b/pictures/\346\211\224\351\270\241\350\233\213/1.jpg" deleted file mode 100644 index d711bb29af..0000000000 Binary files "a/pictures/\346\211\224\351\270\241\350\233\213/1.jpg" and /dev/null differ diff --git "a/pictures/\346\211\224\351\270\241\350\233\213/3.jpg" "b/pictures/\346\211\224\351\270\241\350\233\213/3.jpg" deleted file mode 100644 index 5ba41f1e4f..0000000000 Binary files "a/pictures/\346\211\224\351\270\241\350\233\213/3.jpg" and /dev/null differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/0.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/0.png" deleted file mode 100644 index ad4ff28c15..0000000000 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/0.png" and /dev/null differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/1.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/1.png" deleted file mode 100644 index 0825d34608..0000000000 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/1.png" and /dev/null differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/2.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/2.png" deleted file mode 100644 index 04d583c605..0000000000 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/2.png" and /dev/null differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/3.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/3.png" deleted file mode 100644 index e07f12455e..0000000000 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/3.png" and /dev/null differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title1.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title1.png" deleted file mode 100644 index c84081f9c5..0000000000 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title1.png" and /dev/null differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title2.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title2.png" deleted file mode 100644 index cb33c71545..0000000000 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title2.png" and /dev/null differ diff --git "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title3.png" "b/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title3.png" deleted file mode 100644 index 9a1ad10542..0000000000 Binary files "a/pictures/\346\273\221\345\212\250\347\252\227\345\217\243/title3.png" and /dev/null differ diff --git a/think_like_computer/BinarySearch.md b/think_like_computer/BinarySearch.md new file mode 100644 index 0000000000..55acd98eca --- /dev/null +++ b/think_like_computer/BinarySearch.md @@ -0,0 +1,301 @@ +# Binary Search + +Translator: [sinjoywong](https://blog.csdn.net/SinjoyWong) + +Author: [labuladong](https://github.com/labuladong) + + +Here is a joke: + +One day Mr.Don went to library and borrowed N books. When he left the library, the alarm rang, so the security stopped Mr.Don to check if there are any book haven't been registered. + +Mr.Don was about to check every book under the alertor, which was despised by the security, and he said: Don't you even know binary search? And the security split books in two parts, then put the first part under the alertor, it rang. So he split the first part books in to two parts again ..., finaly, after checked logN times, the security found the book which is not been registered, and he smiled sardonically. Then let Mr.Don took remainning books out of the library. + +Since then, the library lost N - 1 books. + +Is Binary search really a simple algorighm? Not really. Let's see what Knuth(the one who invented KMP algorithm) said: + +> Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly trickey... + +This article is going to discuss several the most commonly used binary search scenes: to find a number, to find its left boundary, to find its right boundary. +And that we are going to discuss details, such as if inequality sign should with the equal sign, if mid should plus one, etc. +After analysing the difference of these details and the reason why them come out, you can write binary search code flexibly and accuratly. + +### Part zero: The Framework of Binary Search + +```java +int binarySearch(int[] nums,int target){ + int left = 0,right = ...; + while(...){ + int mid = (right + left) / 2; + if(nums[mid] == target){ + ... + }else if(nums[mid] < target){ + left = ... + }else if(nums[mid] > target){ + right = ... + } + } + return ...; +} +``` + +**A technique to analize binary search is: use `else if`, rather than using `else`, then we can manage all the details.** + +In order to make it more simplier to understand, this article will use `else if` all along, you can optimize it after you truly understand it. + +Hint: the `...` part is where we need focus to. When you implement binary search, pay attention to these parts firstly. We are going to analyze how it changes under sepecific circumastance. + +Noted: when we calculate `mid`, we need to prevent it overflowing. You can see previous article, and here we assume you can handle it. + +### 1. Find a number (Basic Binary Search) + +This is the simpliest scene, we are going to search a number in a array. If it exists, return its index, otherwise return `-1`. + +```java +int binarySearch(int[] nums,int target){ + int left = 0; + int right = nums.length - 1; //pay attention! + + while(left <= right){ + int mid = (right + left) / 2; + if(nums[mid] == target){ + return mid; + }else if(nums[mid] < target){ + left = mid + 1; + }else if(nums[mid] > target){ + right = mid - 1; + } + } + return -1; +} +``` + +#### Q1.Why using `<=` in `while` loop rather than `<`? + +>A1: Because when we initialize `right`, we set it to `nums.length - 1`, which is index of the last element, not `nums.length`. + +Both of them may show up in different binary search implementions, here is diffenences: With the former, both ends are closed, like `[left,right]`, and the later is left open right close interval, like `[left,right)`, so when we use index `nums.length`, it will out of bounds. + +We will use the former `[left,right]` implemention, which both ends are closed. **This is actually the interval we search every time**. + +So when we should stop searching? + +Of course we can stop when we find the target number in the array: + +```java + if(nums[mid] == target){ + return mid; + } +``` + +But if we havn't find it, we'll need to terminate `while` loop and return `-1`. +So when we should terminal `while` loop? That's simple, **when the search inverval is empty, we should stop searching**, which means we have search all items and have nothing left, we just can't find target number in the array. + +The terminal condition of `while(left <= right)` is `left == right + 1`, we can write it as inverval `[right + 1,right]`, or we can just put a specific number into it, like `[3,2]`. It's obvious that **the inverval is empty**, since there is no number which is larger than 3 and less-and-equal to 2. So we should terminate `while` loop and return -1; + +The terminal condition of `while(wlft < right)` is `left == right`, we can write is as interval `[left,right]`, or we can also put a specific number into it, like `[2,2]`, **the interval is NOT empty**, there is still a number `2`, but the `while` loop is terminated, which means the interval `[2,2]` is missed, index 2 is not been searched, it's wrong when we return -1 directly. + +It is allright if you want to use `while(left < right)` anyway. Since we know how the mistake occurred, we can fix it with a patch: + +```java + //... + while(left < right){ + //... + } + return nums[left] == target ? left : -1; +``` + +#### Q2: Why we implement it as `left = mid + 1`,`right = mid - 1`? I read others' code and they are implenting it as `right = mid` or `left = mid`, there is not so plus or minus, what's the difference? + +>A2: This is also a difficulty of Binary Search implemention. But you can handle it if you can understand previous content. + +We are aware of the concept of 'Search Interval' now, and in our implementation, the search intarval is both end closed, like `[left, right]`. So when we find index `mid` isn't the `target` we want, how to determine next search interval? + +It is obviously that we will use `[left,mid - 1]` or `[mid + 1, right]`: we have just searched `mid`, so it should be removed from search interval. + +#### Q3: What's the defects of this algorithm? + +>A3: Since then, you should have already mastered all details of Binary Search, along with the reason why it works that way. However, there are some defects still. + +For example, there is a sorted array `nums = [1,2,2,2,3]`, `targe = 2`, after processed with Binary Search Algorithm, we will get result `index = 2`. But if we want to get left boundary of `target`, which is `index = 1`, or if we want to get right boundary of `target`, which is `index = 3`, we cannot handle it with this algorithm. + +It's a quite normal demand. Perhaps you would say, can't I find a target, then I search it from target to left(or right)? Sure you can, but it's not so good, since we cannt guarantee the time complexity with O(logn). + +Here we will discuss this two kind of Binary Search Alghrithm. + +### Part 2. Binary Search to look for left border + +See codes below, and pay attention to marked details: + +```java +int left_bound(int[] nums,int target){ + if(nums.lengh == 0) return -1; + int left = 0; + int right = nums.length; // Attention! + + while(left < right){ // Attention + int mid = (left + right) / 2; + if(nums[mid] == target){ + right = mid; + }else if(nums[mid] < target){ + left = mid + 1; + }else if(nums[mid] > target){ + right = mid; // Attention + } + } + return left; +} +``` +#### Q1: Why we use `while(left < right)`, rather than `<=`? + +>A1: Analyze in the same way, since `right = nums.length` rather than `nums.length - 1`, the search interval is `[left, right)`, which is left closed right open. + +>The terminal condition of `while(left < right)` is `left == right`. At this time search interval `[left,right)` is empty, so it can be terminated correctly. + +#### Q2: Why there is no `return -1`? what if there is no `target` in `nums`? + +>A2: Before this, let's think about what's meaning of `left border` is: + +![](../pictures/binarySearch/binarySearch1.png) + +For this array, the algorithm will get result `1`. The result `1` can be interpreted this way: there is 1 element in `nums` which element is less than 2. + +For example, a sorted array `nums = [2,3,5,7]`, `target = 1`, the alghrithm will return 0, which means there is 0 element in `nums` which element is less than 1. + +For example, we have same sorted array as described above, and this time we have `target = 8`, the algorithm will get result `4`, which means there is 4 element in `nums` which element is less than `8`. + +In summary, we can see the interval of return value using the alghrithm (which is the value of `left`) is closed interval `[0,nums.length]`, so we can simply add two line of codes to get `-1` result in proper time. + +```java +while(left < right){ + //... +} +//target is larger than all nums +if(left == nums.length) return -1; +//just like the way previously implenented +return nums[left] == target ? left : -1; +``` + +#### Q1: Why `left = mid + 1, right = mid`? It's kind of different with previous implement. + +>A1: It's easy to explain. Since our search interval is [left,right), which is left closed right open, so when `nums[mid]` has been detected, in then next move, the search interval should remove `mid` and slit it to two intervals, which is `[left,mid)` and `[mid + 1, right)`. + +#### Q4: Why this algorithm can be used for search left border? + +>A4: The key is the solution when we meet `nums[mid] == target`: +```java + if (nums[mid] == target){ + right = mid; + } +``` + +>It's obviously that we don't return it immediatly when we find `target`, in the further we continuly search in interval `[left,mid)`, which is search towarding left and contract, then we can get left border. + +#### Q5: Why return `left`, rather than `right`? + +>A5: It's same way, because the terminal condition of `while` is `left == right`. + +### Part Three: BINARY SEARCH TO FIND RIGHT BORDER + +It's almost same with part two: binary search to find left border, there is only two differences, which is marked below: + +```java +int right_bound(int[] nums,int target){ + if(nums.length == 0) return -1; + int left = 0, right = nums.length; + + while(left < right){ + int mid = (left + right) / 2; + if(nums[mid] == target){ + left = mid + 1; // Attention! + }else if(nums[mid] < target){ + left = mid + 1; + }else if(nums[mid] > target){ + right = mid; + } + } + return left - 1; //Attention! +} +``` + +#### Q1: Why this alghrithm can be used to find right border? + +>A1: Similarly, key point is: + +```java + if(nums[mid] == target){ + left = mid + 1; + } +``` + +>When `nums[mid] == target`, we don't return immediately. On the contrary we enlarge the lower bound of search interval, to make serach interval move to right rapidlly, and finally we can get right border. + +#### Q2: Why we return `left -1`, unlike when we process with left border algorithm and return `left`? In addition I think since we are searching right border, shouldn't we return `right` instead? + +>A2: First of all, the terminal condition of `while` loop is `left == right`, so it's right to use both of them. You can return `right - 1` if you want to reflect `right`. + +>As for why we should minus `1` here, it's a special point, let's see the condition judgement: + +```java + if(nums[mid] == target){ + left = mid + 1; + //Thinking this way: mid = left - 1 + } +``` +![](../pictures/binarySearch/binarySearch2.png) + +When we update the value of `left`, we must do it this way: `left = mid + 1`, which means when `while` is terminated, `nums[left]` must not equal to `target`, but `nums[left-1]` could be equal to `target`. + +As for why `left = mid + 1`, it's same as part two. + +#### Q3: Why there is no `return -1`? what if there is no `target` in `nums`? + +>A3: Like left border search, because the terminal condition of `while` is `left == right`, which means value interval of `left` is `[0,nums.length]`, so we can add some codes and `return -1` apprapoly: + +```java +while(left < right){ + // ... +} +if (lef == 0) return -1; +return nums[left -1] == target ? (left -1) : -1; +``` + +### Part Four: Summary + +Let's tease out the causal logic of these detailed differences. + +#### Firstly, we implement a basic binary search alghrithm: + +Because we initialize `right = nums.length - 1`, it decided our search interval is `[left,right]`, and it also decided `left = mid + 1` and `right = mid - 1`. + +Since we only need to find a index of `target`, so when `nums[mid] == target`, we can return immediately. + +#### Secondly, we implement binary search to find left border: + +Because we initialize `right = nums.length`, it decided our search interval is `[left,right)`, and it also decided `while (left < right)` ,and `left = mid + 1` and `right = mid`. + +Since we need to find the left border, so when `nums[mid] == target`, we shouldn't return immediately, we need to tighten the right border to lock the left border. + +#### Thirdly, we implement binary search to find right border: + +Because we initialize `right = nums.length`, it decided our search interval is `[left,right)`, + +it also decided `while(left < right)`, +`left = mid + 1` and `right = mid`. + +Since we need to find the left border, so when `nums[mid] == target`, we shouldn't return immediately, we need to tighten the left border to lock the right border. + +For further consideration, we must set `left = mid + 1` when we tighten left border, so no matter we return `left` or `right`, we must `minus 1` with the result. + +If you can understand all above, then congratulations, binary search alghrithm won't borther you any more! + +According to this article, you will learn: + +1. When we write binary search code, we don't use `else`, we will use `else if` instead to make our mind clear. + +2. Pay attention to search interval and terminal condition of `while`. If there are any element missed, check it before we return the result. + +3. If we need to search left/right border, we can get proper result when `nums[mid] == target`, and when we search right border, we should minus 1 to get result. + +4. If we close both sides of border, we can only change the code in `nums[mid] == target` and return logic to get right answer. **Put it on your notes, it can be a template for binary search implementation!** diff --git a/think_like_computer/CommonBitManipulation.md b/think_like_computer/CommonBitManipulation.md new file mode 100644 index 0000000000..5b15a9d200 --- /dev/null +++ b/think_like_computer/CommonBitManipulation.md @@ -0,0 +1,121 @@ +# Common Bit manipulation + +Translator: [Funnyyanne](https://github.com/Funnyyanne) + +Author: [labuladong](https://github.com/labuladong) + +This article is divided into two parts. The first part lists a few interesting bitwise operations, second part explains n&(n-1) trick commonly used in algorithm. By the way, I’m going to show you the algorithm for this trick. Because Bit manipulation is simple, it is assumed that the reader already knows the three basic operations of AND, OR, XOR. + +Bit Manipulation can play a lot of fucking trick, but most of these tricks are too obscure, there is no need to dig in. We just need to remember some useful operations. + +### Ⅰ.Interesting Bit manipulations + + +1. Use OR '|' and space bar coverts English characters to lowercase + +```c +('a' | ' ') = 'a' +('A' | ' ') = 'a' +``` + +2. Use AND '&' and underline coverts English to uppercase. + +```c +('b' & '_') = 'B' +('B' & '_') = 'B' +``` + +3. Use XOR '^' and space bar for English characters case exchange. + +```c +('d' ^ ' ') = 'D' +('D' ^ ' ') = 'd' +``` + +PS:The reason why the operation can produce strange effects is ASCII encoding. Characters are actually Numbers, it happens that the Numbers corresponding to these characters can get the correct result through bit manipulations, if you interested in it, you can check ASCII table, this article does not expand it. + +4. Determine if two numbers are different + +```c +int x = -1, y = 2; +bool f = ((x ^ y) < 0); // true + +int x = 3, y = 2; +bool f = ((x ^ y) < 0); // false +``` + +PS:This technique is very practical, is the use of the sign bit complement encoding. If you don't use the bit operation to determine whether the sign is different, you need to use if else branch, which is quite troublesome. Readers may want to use products or quotients to determine whether two numbers have different signs, but this processing method may cause overflow and cause errors. (For complement coding and overflow, see the previous article.) + +5. Swap two Numbers + +```c +int a = 1, b = 2; +a ^= b; +b ^= a; +a ^= b; +// 现在 a = 2, b = 1 +``` + +6. Plus one + +```c +int n = 1; +n = -~n; +// 现在 n = 2 +``` + +7. Minus one + +```c +int n = 2; +n = ~-n; +// 现在 n = 1 +``` + +PS:These three operations just Show off, No practical use, we just know it.。 + +### Ⅱ.Algorithm common operations n&(n-1) + +This operation is the common algorithm, the function is eliminated the number n of the binary representation of the last 1. + +It is easy to understand by looking at the picture: + +![n](../pictures/BitManipulation/1.png) + +1. Count Hamming Weight(Hamming Weight) + +![title](../pictures/BitManipulation/title.png) + +Be to let you return several ones in the binary representation of n's one. Because n & (n-1) can eliminate the last one, you can use a loop to eliminate 1 and count at the same time until n becomes 0. + +```cpp +int hammingWeight(uint32_t n) { + int res = 0; + while (n != 0) { + n = n & (n - 1); + res++; + } + return res; +} +``` + +1. Determine if a number is an exponent of 2 + +If a number is an exponent of 2, its binary representation must contain only one 1: + +```cpp +2^0 = 1 = 0b0001 +2^1 = 2 = 0b0010 +2^2 = 4 = 0b0100 +``` + +If you use the bit operation technique, it is very simple (note the precedence of the operator, the parentheses cannot be omitted) : + +```cpp +bool isPowerOfTwo(int n) { + if (n <= 0) return false; + return (n & (n - 1)) == 0; +} +``` + +The above are some interesting / common bit manipulation. In fact, there are many bit manipulation techniques. There is a foreign website called Bit Twiddling Hacks which collects almost all black technology gameplays of bit manipulation. Interested readers can search to view. diff --git a/think_like_computer/DetailedBinarySearch.md b/think_like_computer/DetailedBinarySearch.md new file mode 100644 index 0000000000..6309372973 --- /dev/null +++ b/think_like_computer/DetailedBinarySearch.md @@ -0,0 +1,467 @@ +# Detailed Binary Search + +**Translator: [Kevin](https://github.com/Kevin-free)** + +**Author: [labuladong](https://github.com/labuladong)** + +First, let me tell you a joke cheerful: + +One day Adong went to the library and borrowed N books. When he went out of the library, the alarm went off, so the security guard stopped Adong to check which books were not registered for loan. ADong is going to go through each book under the alarm to find the book that caused the alarm, but the security guard's disdainful look: Can't you even do a binary search? Then the security divided the books into two piles, let the first pile pass the alarm, the alarm sounded; then divided the pile into two piles ... Finally, after checking logN times, the security successfully found the one that caused the alarm The book showed a smug and ridiculous smile. So Adong left with the remaining books. + +Since then, the library has lost N-1 books. + +Binary search is not easy. The mogul Knuth (the one who invented the KMP algorithm) said that binary search: **Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky...** Many people like to talk about integer overflow bugs, but the real pit of binary search is not the detail problem at all, but whether to add one to or subtract one from `mid`, whether to use` <= `in while `<`. + +If you don't understand these details correctly, writing dichotomy is definitely metaphysical programming, and if there is a bug, you can only rely on bodhisattva to bless it. **I deliberately wrote a poem to celebrate the algorithm, summarize the main content of this article, and suggest to save:** + +![](../pictures/DetailedBinarySearch/verse.jpg) + +This article explores some of the most commonly used binary search scenarios: finding a number, finding the left boundary, and finding the right boundary. Moreover, we are going to go into details, such as whether the inequality sign should be accompanied by an equal sign, whether mid should be increased by one, and so on. Analyze the differences in these details and the reasons for these differences to ensure that you can write the correct binary search algorithm flexibly and accurately. + +### Zero, binary search framework + +```java +int binarySearch(int[] nums, int target) { + int left = 0, right = ...; + + while(...) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + ... + } else if (nums[mid] < target) { + left = ... + } else if (nums[mid] > target) { + right = ... + } + } + return ...; +} +``` + +**A technique for analyzing binary search is: do not appear else, but write everything clearly with else if, so that all details can be clearly displayed**. This article will use else if to make it clear, and readers can simplify it after understanding. + +The section marked with `...`, is the place where details may occur. When you see a binary search code, pay attention to these places first. The following sections use examples to analyze what changes can be made in these places. + +In addition, it is necessary to prevent overflow when calculating mid. `left + (right-left) / 2` is the same as` (left + right) / 2` in the code, but it effectively prevents `left` and` right`. Too large a direct addition causes an overflow. + + +### First, find a number (basic binary search) + +This scenario is the simplest and certainly the most familiar to everyone, that is, searching for a number, if it exists, returns its index, otherwise it returns -1. + +```java +int binarySearch(int[] nums, int target) { + int left = 0; + int right = nums.length - 1; // attention + + while(left <= right) { + int mid = left + (right - left) / 2; + if(nums[mid] == target) + return mid; + else if (nums[mid] < target) + left = mid + 1; // attention + else if (nums[mid] > target) + right = mid - 1; // attention + } + return -1; +} +``` + +**1. Why is <= instead of < in the condition of the while loop?** + +Answer: Because the initial assignment of `right` is` nums.length-1`, which is the index of the last element, not `nums.length`. + +These two may appear in binary search with different functions. The difference is that the former is equivalent to the both closed interval `[left, right]`, and the latter is equivalent to the left closed right opening interval `[left, right)`, because An index size of `nums.length` is out of bounds. + +In our algorithm, we use the interval where `[left, right]`is closed at both ends. **This interval is actually the interval for each search**. + +When should you stop searching? Of course, you can terminate when the target value is found: + +```java + if(nums[mid] == target) + return mid; +``` + +But if not found, you need to terminate the while loop and return -1. When should the while loop terminate? **It should be terminated when the search interval is empty**, which means that if you don't have to find it, it means you haven't found it. + +The termination condition of `while (left <= right)` is `left == right + 1`, written in the form of an interval is` [right + 1, right] `, or with a specific number in it `[3, 2] `, It can be seen that **the interval is empty at this time**, because no number is greater than or equal to 3 and less than or equal to 2. So the termination of the while loop is correct at this time, just return -1. + +The termination condition of `while (left target) { + right = mid; // attention + } + } + return left; +} +``` + +**1.Why is `<` instead of `<=` in while?** + +Answer: Use the same method, because `right = nums.length` instead of` nums.length-1`. So the "search interval" of each loop is `[left, right)` + +The condition of `while (left target) { + // search interval is [left, mid-1] + right = mid - 1; +} else if (nums[mid] == target) { + // shrink right border + right = mid - 1; +} +``` + +Since the exit condition of while is `left == right + 1`, when` target` is larger than all the elements in `nums`, the following conditions exist to make the index out of bounds: + +![](../pictures/DetailedBinarySearch/2.jpg) + +Therefore, the code that finally returns the result should check for out of bounds: + +```java +if (left >= nums.length || nums[left] != target) + return -1; +return left; +``` + +At this point, the entire algorithm has been written. The complete code is as follows: + +```java +int left_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + // search interval is [left, right] + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + // search interval is [mid+1, right] + left = mid + 1; + } else if (nums[mid] > target) { + // search interval is [left, mid-1] + right = mid - 1; + } else if (nums[mid] == target) { + // shrink right border + right = mid - 1; + } + } + // check out of bounds + if (left >= nums.length || nums[left] != target) + return -1; + return left; +} +``` + +This is unified with the first binary search algorithm, which are both "search intervals" with both ends closed, and the value of the `left` variable is also returned at the end. As long as you hold the logic of binary search, let's see which one you like and which one you like. + +### Third, binary search to find the right border + +Similar to the algorithm for finding the left boundary, there are two ways to write it, or the common left-close and right-open method is written first. There are only two differences from the search of the left boundary, which are marked: + +```java +int right_bound(int[] nums, int target) { + if (nums.length == 0) return -1; + int left = 0, right = nums.length; + + while (left < right) { + int mid = (left + right) / 2; + if (nums[mid] == target) { + left = mid + 1; // attention + } else if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid; + } + } + return left - 1; // attention +} +``` + +**1. Why can this algorithm find the right border**? + +Answer: Similarly, the key point is here: + +```java +if (nums[mid] == target) { + left = mid + 1; +``` + +When `nums [mid] == target`, do not return immediately, but increase the lower bound of the“ search interval ”` left`, so that the interval continuously shrinks to the right to achieve the purpose of locking the right boundary. + +**2. Why does it return `left-1` instead of` left`? And I think that since it is searching for the right border, it should return `right` only**. + +Answer: First, the termination condition of the while loop is `left == right`, so` left` and `right` are the same. You have to embody the characteristics of the right side and return` right-1`.Answer: First, the termination condition of the while loop is `left == right`, so` left` and `right` are the same. You have to embody the characteristics of the right side and return` right-1`. + +As for why it should be reduced by one, this is a special point in the search for the right border. The key is to judge in this condition: + +```java +if (nums[mid] == target) { + left = mid + 1; + // think it: mid = left - 1 +``` + +![](../pictures/DetailedBinarySearch/3.jpg) + +Because our update to `left` must be` left = mid + 1`, which means that at the end of the while loop, `nums [left]` must not be equal to `target`, and` nums [left-1] `may be `target`. + +As for why the update of `left` must be` left = mid + 1`, the search is the same as the left border, so I won't go into details. + +**3. Why is there no operation that returns -1? What if the value of `target` does not exist in` nums`?** + +A: Similar to the previous search of the left boundary, because the termination condition of while is `left == right`, that is, the range of` left` is `[0, nums.length]`, so you can add two lines of code returns -1 correctly: + +```java +while (left < right) { + // ... +} +if (left == 0) return -1; +return nums[left-1] == target ? (left-1) : -1; +``` + +**4. Is it also possible to unify the "search interval" of this algorithm into a form with both ends closed? In this way, the three writing methods are completely unified, and they can be written with closed eyes later**. + +Answer: Of course, it is similar to searching for the unified writing on the left border. In fact, you only need to change two places: + +```java +int right_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] == target) { + // here~ change to shrink left bounds + left = mid + 1; + } + } + // here~ change to check right out of bounds, see below + if (right < 0 || nums[right] != target) + return -1; + return right; +} +``` + +When `target` is smaller than all elements,` right` will be reduced to -1, so you need to prevent it from going out of bounds at the end: + +![](../pictures/DetailedBinarySearch/4.jpg) + +At this point, the two ways of searching for the binary search on the right side of the boundary have also been completed. In fact, it is easier to remember the unification of the "search interval" with both ends closed, right? + +### Fourth, unified logic + +Let's tease out the causal logic of these detailed differences: + +#### Firstly, we implement a basic binary search algorithm: + +```python +Because we initialize `right = nums.length - 1`, it decided our search interval is `[left,right]`, and it also decided `left = mid + 1` and `right = mid - 1`. + +Since we only need to find a index of `target`, so when `nums[mid] == target`, we can return immediately. +``` + +#### Secondly, we implement binary search to find left border: + +```python +Because we initialize `right = nums.length`, it decided our search interval is `[left,right)`, and it also decided `while (left < right)` ,and `left = mid + 1` and `right = mid`. + +Since we need to find the left border, so when `nums[mid] == target`, we shouldn't return immediately, we need to tighten the right border to lock the left border. +``` + +#### Thirdly, we implement binary search to find right border: + +```python +Because we initialize `right = nums.length`, it decided our search interval is `[left,right)`, + +it also decided `while(left < right)`, +`left = mid + 1` and `right = mid`. + +Since we need to find the left border, so when `nums[mid] == target`, we shouldn't return immediately, we need to tighten the left border to lock the right border. +``` + +For the binary search to find the left and right boundaries, the common method is to use the left and right open "search intervals". **We also unified the "search intervals" into closed ends on the basis of logic, which is easy to remember. Just modify two places. There are three ways of writing**: + +```java +int binary_search(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while(left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if(nums[mid] == target) { + // Return directly + return mid; + } + } + // Return directly + return -1; +} + +int left_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] == target) { + // Don't return! Lock left border + right = mid - 1; + } + } + // Check whether left border out of bounds lastly + if (left >= nums.length || nums[left] != target) + return -1; + return left; +} + + +int right_bound(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] == target) { + // Don't return! Lock right border + left = mid + 1; + } + } + // Check whether right border out of bounds lastly + if (right < 0 || nums[right] != target) + return -1; + return right; +} +``` + +If you can understand the above, then congratulations, the details of the binary search algorithm are nothing more than that. + +Through this article, you learned: + +1. When analyzing the binary search code, do not appear else, expand all into else if for easy understanding. + +2. Pay attention to the termination conditions of "search interval" and while. If there are missing elements, remember to check at the end. + +3. If you need to define the left and right "search interval" to search the left and right boundaries, you only need to modify it when `nums [mid] == target`, and you need to subtract one when searching the right side. + +4. If the "search interval" is unified to be closed at both ends, it is easy to remember, as long as you slightly change the code and return logic at the condition of `nums [mid] == target`, **it is recommended to take a small book As a binary search template**. + + diff --git a/think_like_computer/DetailsaboutBacktracking.md b/think_like_computer/DetailsaboutBacktracking.md new file mode 100644 index 0000000000..00dce6f65d --- /dev/null +++ b/think_like_computer/DetailsaboutBacktracking.md @@ -0,0 +1,277 @@ +# Details about Backtracking + +**Translator**: [xiaodp](https://github.com/xiaodp) + +**Author**: [labuladong](https://github.com/labuladong) + +This article is an advanced version of "Details of Backtracking Algorithms" before. The previous one isn't clear enough, so you don't need to read it and just read this article. + +Ponder carefully and you will find that the backtracking problems follow the same pattern, that is, have the same framework. + +Let's go straight to the framework backtracking follows. **Solving a backtracking problem is actually a traversal process of a decision tree.** Now you only need to think about 3 terms: + +1. **Path**: the selection that have been made. + +2. **Selection List**: the selection you can currently make. + +3. **End Condition**: the condition under which you reach the bottom of the decision tree, and can no longer make a selection. + +It doesn’t matter if you don’t understand the explanation of the 3 terms. I will use the two classic backtracking algorithm problems,`Permutation` and `N Queen Problem` to help you understand what they mean. Before this, you just keep them in mind. + +Here shows the pseudocode of the framework: + +``` +result = [] +def backtrack(Path, Seletion List ): + if meet the End Conditon: + result.add(Path) + return + + for seletion in Seletion List: + select + backtrack(Path, Seletion List) + deselect +``` + +**The core is the recursion in the for loop. It `makes a selection` before the recursive call and `undoes the selection` after the recursive call**, which is especially simple. + +Then what `makes a selection` and `undo the selection` means? and what is the underlying principle of this framework? Let's use `Permutation` to solve your questions and explore the underlying principle in detail. + +### Permutation + +You must have learned the permutations and combinations. As we know, for $N$ unique numbers, the number of full permutations is $N!$. + +`note`: For simplicity and clarity, **the full permutation problem we are discussing this time does not contain duplicate numbers**. + +Think about how we find out all the permutations. If you are given three numbers `[1,2,3]` , you may follow these steps: + +1. Fix the first number to 1; +2. Then the second number can be 2; +3. If the second number is 2, then the third number can only be 3; +4. Then you can change the second number to 3 and the third number can only be 2; +5. Then you can only change the first place,and repeat 2-4. + +In fact, this is the ''backtracking''. You can use it even without a teacher! The following figure shows the backtracking tree: + +![](../pictures/backtracking/1en.jpg) + +Just traverse this tree from the root to the leaves and record the numbers on the paths, and you will get all the permutations. **We might as well call this tree a “decision tree” for backtracking** for you're actually making decisions on each node. For instance, if you are now at the red node, you will making a decision between the "1" branch and "3" branch. Why only 1 and 3? Because the "2" branch is behind you, you have made this selection before, and the full permutation is not allowed to reuse numbers. + +**Now you can understand the terms mentioned before more specifically: `[2]` is the “Path”, which records the selections you have made; `[1,3]` is the “Selection List”, which means the current selections you can make; `End Condition` is to traverse to the bottom of the decision tree(here is when the Selection List is empty)**. + +If you understand these terms, **you can use the "Path" and "Selection List" as attributes of each node in the decision tree**. For example, the following figure lists the attributes of several nodes + +![](../pictures/backtracking/3en.png) + +**The function ```backtrack()``` we defined is actually like a pointer. It is necessary to walk on the tree and maintain the attributes of each node correctly. Whenever it reaches the bottom of the tree, its “Path” is a full permutation**. + +Furthermore, how to traverse a tree? it should not be difficult. Recall from the previous article *Framework Thinking of Learning Data Structures*, various search problems are actually tree traversal problems, and the multi-tree traversal framework is: + +```java +void traverse(TreeNode root) { + for (TreeNode child : root.childern) + // Operations needed for preorder traversal + traverse(child); + // Operations needed for postorder traversal +} +``` + +The so-called preorder traversal and postorder traversal are just two very useful time points. The following picture will make you more clear: + +![](../pictures/backtracking/4en.jpg) + +**Preorder travers is executed at the time point before entering a node, and postorder traversal is executed at the time point after leaving a node**. + +Recalling what we just said:"Path" and "Selection List" are attributes of each node. If want the function to maintain the attributes of the node correctly, we must do something at these two special time points: + +![](../pictures/backtracking/5en.jpg) + +Now, do you understand the core framework of backtracking? + +```python +for seletion in Seletion List: + # select + Remove this seletion from the Seletion List + Path.add(seletion) + backtrack(Path, Seletion List) + # deselect + Path.remove(seletion) + Add the seletion to the Seletion List +``` + +**As long as we make a selection before recursion and undo the previous selection after recursion**, we can get the Selection List and Path of each node correctly. + +Here shows the code for the full permutation: + +```java +List> res = new LinkedList<>(); + +/* The main method, enter a set of unique numbers and return their full permutations */ +List> permute(int[] nums) { + // record Path + LinkedList track = new LinkedList<>(); + backtrack(nums, track); + return res; +} + +// Path: recorded in track +// Seletion List: those elements in nums that do not exist in track +// End Condition: all elements in nums appear in track +void backtrack(int[] nums, LinkedList track) { + // trigger the End Condition + if (track.size() == nums.length) { + res.add(new LinkedList(track)); + return; + } + + for (int i = 0; i < nums.length; i++) { + // exclude illegal seletions + if (track.contains(nums[i])) + continue; + // select + track.add(nums[i]); + // enter the next level decision tree + backtrack(nums, track); + // deselect + track.removeLast(); + } +} +``` + +We made a few changes here: instead of explicitly recording the "selection List", we use `nums` and `track` to deduce the current selection list: + +![](../pictures/backtracking/6en.jpg) + +So far, we have explained the underlying principle of the backtracking through the full permutation problem. Of course, this algorithm is not very efficient, and using the `contains` method for linked list requires $O(N)$ time complexity. There are better ways to achieve the purpose by exchanging elements which are more difficult to understand. I won't discuss them in this article. If you are interested, you can google related knowledge by yourself. + +However, it must be noted that no matter how optimized, it conforms to the backtracking framework, and the time complexity cannot be lower than $O (N!)$.Because exhaustion of the entire decision tree is unavoidable. **This is also a feature of backtracking. Unlike dynamic programming having overlapping subproblems which can be optimized, backtracking is purely violent exhaustion, and time complexity is generally high**. + +After understanding the full permutation problem, you can directly use the backtracking framework to solve some problems. Let's take a brief look at the `N Queen`problem. + +### N Queen Problem + +This is a classical problem: place $N$ non-attacking queens on an $N{\times}N$ chessboard. Thus, a solution requires that no two queens share the same row, column, or diagonal. + +This problem is essentially similar to the full permutation problem. If we build a decision tree, each layer of the decision tree represents each row on the chessboard. And the selection that each node can make is to place a queen on any column of the row. + +Apply the backtracking framework directly: + +```cpp +vector> res; + +/* Enter board length n, return all legal placements */ +vector> solveNQueens(int n) { + // '.' Means empty, and 'Q' means queen, initializing the empty board. + vector board(n, string(n, '.')); + backtrack(board, 0); + return res; +} + +// Path:The rows smaller than row in the board have been successfully placed the queens +// Seletion List: all columns in 'rowth' row are queen's seletions +// End condition: row meets the last line of board(n) +void backtrack(vector& board, int row) { + // trigger the End Condition + if (row == board.size()) { + res.push_back(board); + return; + } + + int n = board[row].size(); + for (int col = 0; col < n; col++) { + // exclude illegal seletions + if (!isValid(board, row, col)) + continue; + // select + board[row][col] = 'Q'; + // enter next row decision + backtrack(board, row + 1); + // deselect + board[row][col] = '.'; + } +} +``` + +This part of the code is actually similar to the full permutation problem. The implementation of the ```isValid()``` is also very simple.: + +```cpp +/*Is it possible to place a queen on board [row] [col]? */ +bool isValid(vector& board, int row, int col) { + int n = board.size(); + // Check if share the same column + for (int i = 0; i < n; i++) { + if (board[i][col] == 'Q') + return false; + } + // Check if share the same right diagonal + for (int i = row - 1, j = col + 1; + i >= 0 && j < n; i--, j++) { + if (board[i][j] == 'Q') + return false; + } + // Check if share the same left diagonal + for (int i = row - 1, j = col - 1; + i >= 0 && j >= 0; i--, j--) { + if (board[i][j] == 'Q') + return false; + } + return true; +} +``` + +The function ```backtrack()``` still looks like a pointer walking in the decision tree. The position traversed by the ```backtrack()```can be represented by` row` and `col`, and the unqualified condition can be pruned by the ```isValid()``` : + +![](../pictures/backtracking/7en.jpg) + +![](https://upload.wikimedia.org/wikipedia/commons/1/1f/Eight-queens-animation.gif) + +If you are facing such a chunk of solution code directly, you may feel very puzzled. But if you understand the framework of backtracking, it is not difficult to understand the solution code. Based on the framework, the changes are just the way of making selection and excluding illegal selections. As long as you keep the framework in mind, you are left with only minor issues. + +When $N = 8$, it is the eight queens problem. Gauss, the mathematics prince , spent his whole life not counting all possible ways to place, but our algorithm only needs one second .But don't blame Gauss, the complexity of this problem is indeed very high. Look at our decision tree, although there is a pruning by the ```isValid()``` , the worst time complexity is still $O (N ^ {N + 1})$.And it cannot be optimized. If $N = 10$, the calculation is already rather time consuming. + +**When we don't want to get all legal answers but only one answer, what should we do ?** For example, the algorithm for solving Sudoku is too complicated to find all the solutions and one solution is enough. + +In fact, it is very simple. Just modify the code of the backtracking slightly: + +```cpp +// Returns true after finding an answer +bool backtrack(vector& board, int row) { + // Trigger End Condition + if (row == board.size()) { + res.push_back(board); + return true; + } + ... + for (int col = 0; col < n; col++) { + ... + board[row][col] = 'Q'; + + if (backtrack(board, row + 1)) + return true; + + board[row][col] = '.'; + } + + return false; +} +``` + +After this modification, as long as an answer is found, subsequent recursion of the for loop will be blocked. Maybe you can slightly modify the code of the N queen problem and write an algorithm to solve Sudoku? + +### Conclusion + +Backtracking is a multi-tree traversal problem. The key is to do some operations at the positions of pre-order traversal and postorder traversal. The algorithm framework is as follows: + +```python +def backtrack(...): + for seletion in seletions List: + select + backtrack(...) + deselect +``` + +**When writing the `backtrack()` function, you need to maintain the “Path” you have traveled and the "selection List” you currently have. When the “End Condition” is triggered, record the “Path” in the result set**. + +Think carefully, is the backtracking and dynamic programming somehow similar? We have repeatedly emphasized in the series of articles about dynamic planning that the three points that need to be clear in dynamic programming are "State", "selection" and "Base Case". Do they correspond to the "Path" that has passed, and the current "selection List" And "End Condition "? + +To some extent, the brute-force solution phase of dynamic programming is a backtracking. When some problems have overlapping sub-problems, you can use dp table or memo to greatly prune the recursive tree, which becomes dynamic programming. However, today's two problems do not have overlapping subproblems, that is, the problem of backtracking, and the high complexity is inevitable. diff --git a/think_like_computer/DoublePointerTechnique.md b/think_like_computer/DoublePointerTechnique.md new file mode 100644 index 0000000000..f295316adb --- /dev/null +++ b/think_like_computer/DoublePointerTechnique.md @@ -0,0 +1,199 @@ +# Summary of double pointer technique[](#双指针技巧总结) + +> 原文地址:[https://github.com/labuladong/fucking-algorithm/blob/master/算法思维系列/双指针技巧.md](https://github.com/labuladong/fucking-algorithm/blob/master/算法思维系列/双指针技巧.md) + +**Translator: [miaoxiaozui2017](https://github.com/miaoxiaozui2017)** + +**Author: [labuladong](https://github.com/labuladong)** + +I divide the double pointer technique into two categories.One is `fast-and-slow pointer`,and the other is `left-and-right pointer`. The former mainly solves the problems in the linked list, such as the typical problem of `determination of whether a ring is included in the linked list`.And the latter mainly solves the problems in the array (or string), such as `binary search`. + +### Part 1. Common algorithms of fast-and-slow pointer[](#快慢指针的常见算法) + +The fast-and-slow pointers are usually initialized to point to the head node of the linked list. When moving forward, the `fast` pointer is in the front and the `slow` pointer is in the back, which ingeniously solves some problems in the linked list. + +**1. Determine whether there is a ring in the linked list**[](#判定链表中是否含有环) + +This should be the most basic operation of linked list. *If the reader already knows this skill, this part can be skipped.* + +The feature of single linked list is that each node only knows the next node, so a pointer can't judge whether there is a ring in the linked list. + +If there is no ring in the linked list, then this pointer will eventually encounter a null pointer indicating that the linked list is at the end.This is a good situation that it can be judged directly that the linked list does not contain a ring. + +```java + +boolean hasCycle(ListNode head) { + while (head != null) + head = head.next; + return false; +} +``` + +While if the linked list contains a ring, the pointer will fall into `a dead loop` because there is no `null` pointer as the tail node in the ring array. + +The classic solution is to use two pointers---one is fast,and the other is slow. If there is no ring, the fast pointer will eventually encounter `null` indicating that the linked list does not contain a ring.Or if there is a ring, the fast pointer will eventually exceed the slow pointer by a circle indicating that the linked list contains a ring. + +```java +boolean hasCycle(ListNode head) { + ListNode fast, slow; + fast = slow = head; + while (fast != null && fast.next != null) { + fast = fast.next.next; + slow = slow.next; + + if (fast == slow) return true; + } + return false; +} +``` + +**2.A ring is known to exist in the linked list.Return the starting position of this ring**[](#已知链表中含有环,返回这个环的起始位置) + +![1](../pictures/DoublePointerTechnique/1.png) + +This problem is not difficult at all. It's a bit like a brain teaser. First, look at the code directly: + +```java +ListNode detectCycle(ListNode head) { + ListNode fast, slow; + fast = slow = head; + while (fast != null && fast.next != null) { + fast = fast.next.next; + slow = slow.next; + if (fast == slow) break; + } + //The above code is similar to the hascycle function + slow = head; + while (slow != fast) { + fast = fast.next; + slow = slow.next; + } + return slow; +} +``` + +It can be seen that when the fast and slow pointers meet, let any of them points to the head node, and then let them advance at the same speed. When they meet again, the node position is the starting position of the ring. Why is that? + +`At the first meeting`, if the `slow` pointer takes `k` steps, then the `fast` pointer must take `2k` steps, that is to say, it takes `k` steps more than the `slow` pointer (or in another word,the length of the ring). + +![2](../pictures/DoublePointerTechnique/2.png) + +If the distance between the meeting point and the starting point of the ring is `m`, then the distance between the starting point of the ring and the `head` node is `k - m`. That is to say, if we advance `k - m` steps from the `head` node, we can reach the starting point of the ring. + +Coincidentally, if we continue to move `k - m` steps from the meeting point, we will also arrive at the starting point of the ring. + +![3](../pictures/DoublePointerTechnique/3.png) + +So, as long as we point any one of the fast and slow pointers back to `head`, and then the two pointers move at the same speed after `k - m` steps they will meet at the starting point of the ring. + +**3. Find the midpoint of the linked list**[](#寻找链表的中点) + +Similar to the above idea, we can also make the fast pointer advance two steps at a time and the slow pointer advance one step at a time. When the fast pointer reaches the end of the linked list, the slow pointer is exactly in the middle of the linked list. + +```java +while (fast != null && fast.next != null) { + fast = fast.next.next; + slow = slow.next; +} +//slow is in the middle +return slow; +``` + +When the length of the linked list is odd, `slow` happens to stop at the midpoint.If the length is even, the final position of `slow` is right in the middle: + +![center](../pictures/DoublePointerTechnique/center.png) + +An important role in finding the midpoint of a linked list is to merge and sort the linked list. + +Recall the `merging and sorting` of arrays: find the midpoint index to divide the arrays recursively, and finally merge the two ordered arrays. For linked list, it is very simple to merge two ordered linked lists, and the difficulty lies in dichotomy. + +But now that you have learned `finding the midpoint of a linked list`, you can achieve the dichotomy of a linked list. For the details of `merging and sorting`, this paper will not expand specifically. + +**4. Looking for the last k element of the linked list**[](#寻找链表的倒数第 k 个元素) +Our idea is still to use the `fast-and-slow pointer`.Let the `fast` pointer go `k` steps first, and then the `fast` and `slow` pointer starts to move at the same speed. In this way, when the `fast` pointer goes to `null` at the end of the linked list, the position of the `slow` pointer is the last `k` list node (for simplification, suppose `k` not exceed the length of the linked list): + +```java +ListNode slow, fast; +slow = fast = head; +while (k-- > 0) + fast = fast.next; + +while (fast != null) { + slow = slow.next; + fast = fast.next; +} +return slow; +``` + +### Part 2.Common algorithms of left-and-right pointer[](#左右指针的常用算法) + +The left-and-right pointer in the array actually refer to two index values, which are usually initialized as `left = 0` and `right = nums.length - 1`. + +**1. Binary search**[](#二分查找) + +The previous paper `binary search` is explained in detail. Here only the simplest binary algorithm is written to stick out its double pointer feature: + +```java +int binarySearch(int[] nums, int target) { + int left = 0; + int right = nums.length - 1; + while(left <= right) { + int mid = (right + left) / 2; + if(nums[mid] == target) + return mid; + else if (nums[mid] < target) + left = mid + 1; + else if (nums[mid] > target) + right = mid - 1; + } + return -1; +} +``` + +**2. Sum of two numbers**[](#两数之和) + +Let's take a look at a leetcode question: + +![title](../pictures/DoublePointerTechnique/title.png) + +As long as the array is ordered, you should think of the double pointer technique. The solution of this problem is similar to binary search. The size of `sum` can be adjusted by adjusting `left` and `right`: + +```java +int[] twoSum(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left < right) { + int sum = nums[left] + nums[right]; + if (sum == target) { + //The index required by the title starts from 1 + return new int[]{left + 1, right + 1}; + } else if (sum < target) { + left++; // make sum bigger + } else if (sum > target) { + right--; // make sum smaller + } + } + return new int[]{-1, -1}; +} +``` + +**3. Invert array**[](#反转数组) + +```java +void reverse(int[] nums) { + int left = 0; + int right = nums.length - 1; + while (left < right) { + // swap(nums[left], nums[right]) + int temp = nums[left]; + nums[left] = nums[right]; + nums[right] = temp; + left++; right--; + } +} +``` + +**4. Sliding window algorithm**[](#滑动窗口算法) + +This may be the highest level of the double pointer technique. If you master this algorithm, you can solve a large class of `substring matching` problems, but the `sliding window` is slightly more complex than the algorithms metioned above. + +Fortunately, there are framework templates for this kind of algorithm, and [this article](https://github.com/labuladong/fucking-algorithm/blob/master/算法思维系列/滑动窗口技巧.md) explains the `sliding window` algorithm template, which helps you to "kill" several `substrings matching` problems in leetcode. diff --git "a/think_like_computer/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" "b/think_like_computer/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" deleted file mode 100644 index c55ac66728..0000000000 --- "a/think_like_computer/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" +++ /dev/null @@ -1,221 +0,0 @@ -# FloodFill算法详解及应用 - -啥是 FloodFill 算法呢,最直接的一个应用就是「颜色填充」,就是 Windows 绘画本中那个小油漆桶的标志,可以把一块被圈起来的区域全部染色。 - -![floodfill](../pictures/floodfill/floodfill.gif) - -这种算法思想还在许多其他地方有应用。比如说扫雷游戏,有时候你点一个方格,会一下子展开一片区域,这个展开过程,就是 FloodFill 算法实现的。 - -![扫雷](../pictures/floodfill/扫雷.png) - -类似的,像消消乐这类游戏,相同方块积累到一定数量,就全部消除,也是 FloodFill 算法的功劳。 - -![xiaoxiaole](../pictures/floodfill/xiaoxiaole.jpg) - -通过以上的几个例子,你应该对 FloodFill 算法有个概念了,现在我们要抽象问题,提取共同点。 - -### 一、构建框架 - -以上几个例子,都可以抽象成一个二维矩阵(图片其实就是像素点矩阵),然后从某个点开始向四周扩展,直到无法再扩展为止。 - -矩阵,可以抽象为一幅「图」,这就是一个图的遍历问题,也就类似一个 N 叉树遍历的问题。几行代码就能解决,直接上框架吧: - -```java -// (x, y) 为坐标位置 -void fill(int x, int y) { - fill(x - 1, y); // 上 - fill(x + 1, y); // 下 - fill(x, y - 1); // 左 - fill(x, y + 1); // 右 -} -``` - -这个框架可以解决所有在二维矩阵中遍历的问题,说得高端一点,这就叫深度优先搜索(Depth First Search,简称 DFS),说得简单一点,这就叫四叉树遍历框架。坐标 (x, y) 就是 root,四个方向就是 root 的四个子节点。 - -下面看一道 LeetCode 题目,其实就是让我们来实现一个「颜色填充」功能。 - -![title](../pictures/floodfill/leetcode.png) - -根据上篇文章,我们讲了「树」算法设计的一个总路线,今天就可以用到: - -```java -int[][] floodFill(int[][] image, - int sr, int sc, int newColor) { - - int origColor = image[sr][sc]; - fill(image, sr, sc, origColor, newColor); - return image; -} - -void fill(int[][] image, int x, int y, - int origColor, int newColor) { - // 出界:超出边界索引 - if (!inArea(image, x, y)) return; - // 碰壁:遇到其他颜色,超出 origColor 区域 - if (image[x][y] != origColor) return; - image[x][y] = newColor; - - fill(image, x, y + 1, origColor, newColor); - fill(image, x, y - 1, origColor, newColor); - fill(image, x - 1, y, origColor, newColor); - fill(image, x + 1, y, origColor, newColor); -} - -boolean inArea(int[][] image, int x, int y) { - return x >= 0 && x < image.length - && y >= 0 && y < image[0].length; -} -``` - -只要你能够理解这段代码,一定要给你鼓掌,给你 99 分,因为你对「框架思维」的掌控已经炉火纯青,此算法已经 cover 了 99% 的情况,仅有一个细节问题没有解决,就是当 origColor 和 newColor 相同时,会陷入无限递归。 - -### 二、研究细节 - -为什么会陷入无限递归呢,很好理解,因为每个坐标都要搜索上下左右,那么对于一个坐标,一定会被上下左右的坐标搜索。**被重复搜索时,必须保证递归函数能够能正确地退出,否则就会陷入死循环。** - -为什么 newColor 和 origColor 不同时可以正常退出呢?把算法流程画个图理解一下: - -![ppt1](../pictures/floodfill/ppt1.PNG) - -可以看到,fill(1, 1) 被重复搜索了,我们用 fill(1, 1)* 表示这次重复搜索。fill(1, 1)* 执行时,(1, 1) 已经被换成了 newColor,所以 fill(1, 1)* 会在这个 if 语句被怼回去,正确退出了。 - -```java -// 碰壁:遇到其他颜色,超出 origColor 区域 -if (image[x][y] != origColor) return; -``` -![ppt2](../pictures/floodfill/ppt2.PNG) - -但是,如果说 origColor 和 newColor 一样,这个 if 语句就无法让 fill(1, 1)* 正确退出,而是开启了下面的重复递归,形成了死循环。 - -![ppt3](../pictures/floodfill/ppt3.PNG) - -### 三、处理细节 - -如何避免上述问题的发生,最容易想到的就是用一个和 image 一样大小的二维 bool 数组记录走过的地方,一旦发现重复立即 return。 - -```java - // 出界:超出边界索引 -if (!inArea(image, x, y)) return; -// 碰壁:遇到其他颜色,超出 origColor 区域 -if (image[x][y] != origColor) return; -// 不走回头路 -if (visited[x][y]) return; -visited[x][y] = true; -image[x][y] = newColor; -``` - -完全 OK,这也是处理「图」的一种常用手段。不过对于此题,不用开数组,我们有一种更好的方法,那就是回溯算法。 - -前文「回溯算法详解」讲过,这里不再赘述,直接套回溯算法框架: - -```java -void fill(int[][] image, int x, int y, - int origColor, int newColor) { - // 出界:超出数组边界 - if (!inArea(image, x, y)) return; - // 碰壁:遇到其他颜色,超出 origColor 区域 - if (image[x][y] != origColor) return; - // 已探索过的 origColor 区域 - if (image[x][y] == -1) return; - - // choose:打标记,以免重复 - image[x][y] = -1; - fill(image, x, y + 1, origColor, newColor); - fill(image, x, y - 1, origColor, newColor); - fill(image, x - 1, y, origColor, newColor); - fill(image, x + 1, y, origColor, newColor); - // unchoose:将标记替换为 newColor - image[x][y] = newColor; -} -``` - -这种解决方法是最常用的,相当于使用一个特殊值 -1 代替 visited 数组的作用,达到不走回头路的效果。为什么是 -1,因为题目中说了颜色取值在 0 - 65535 之间,所以 -1 足够特殊,能和颜色区分开。 - - -### 四、拓展延伸:自动魔棒工具和扫雷 - -大部分图片编辑软件一定有「自动魔棒工具」这个功能:点击一个地方,帮你自动选中相近颜色的部分。如下图,我想选中老鹰,可以先用自动魔棒选中蓝天背景,然后反向选择,就选中了老鹰。我们来分析一下自动魔棒工具的原理。 - -![抠图](../pictures/floodfill/抠图.jpg) - -显然,这个算法肯定是基于 FloodFill 算法的,但有两点不同:首先,背景色是蓝色,但不能保证都是相同的蓝色,毕竟是像素点,可能存在肉眼无法分辨的深浅差异,而我们希望能够忽略这种细微差异。第二,FloodFill 算法是「区域填充」,这里更像「边界填充」。 - -对于第一个问题,很好解决,可以设置一个阈值 threshold,在阈值范围内波动的颜色都视为 origColor: - -```java -if (Math.abs(image[x][y] - origColor) > threshold) - return; -``` - -对于第二个问题,我们首先明确问题:不要把区域内所有 origColor 的都染色,而是只给区域最外圈染色。然后,我们分析,如何才能仅给外围染色,即如何才能找到最外围坐标,最外围坐标有什么特点? - -![ppt4](../pictures/floodfill/ppt4.PNG) - -可以发现,区域边界上的坐标,至少有一个方向不是 origColor,而区域内部的坐标,四面都是 origColor,这就是解决问题的关键。保持框架不变,使用 visited 数组记录已搜索坐标,主要代码如下: - -```java -int fill(int[][] image, int x, int y, - int origColor, int newColor) { - // 出界:超出数组边界 - if (!inArea(image, x, y)) return 0; - // 已探索过的 origColor 区域 - if (visited[x][y]) return 1; - // 碰壁:遇到其他颜色,超出 origColor 区域 - if (image[x][y] != origColor) return 0; - - visited[x][y] = true; - - int surround = - fill(image, x - 1, y, origColor, newColor) - + fill(image, x + 1, y, origColor, newColor) - + fill(image, x, y - 1, origColor, newColor) - + fill(image, x, y + 1, origColor, newColor); - - if (surround < 4) - image[x][y] = newColor; - - return 1; -} -``` - -这样,区域内部的坐标探索四周后得到的 surround 是 4,而边界的坐标会遇到其他颜色,或超出边界索引,surround 会小于 4。如果你对这句话不理解,我们把逻辑框架抽象出来看: - -```java -int fill(int[][] image, int x, int y, - int origColor, int newColor) { - // 出界:超出数组边界 - if (!inArea(image, x, y)) return 0; - // 已探索过的 origColor 区域 - if (visited[x][y]) return 1; - // 碰壁:遇到其他颜色,超出 origColor 区域 - if (image[x][y] != origColor) return 0; - // 未探索且属于 origColor 区域 - if (image[x][y] == origColor) { - // ... - return 1; - } -} -``` - -这 4 个 if 判断涵盖了 (x, y) 的所有可能情况,surround 的值由四个递归函数相加得到,而每个递归函数的返回值就这四种情况的一种。借助这个逻辑框架,你一定能理解上面那句话了。 - -这样就实现了仅对 origColor 区域边界坐标染色的目的,等同于完成了魔棒工具选定区域边界的功能。 - -这个算法有两个细节问题,一是必须借助 visited 来记录已探索的坐标,而无法使用回溯算法;二是开头几个 if 顺序不可打乱。读者可以思考一下原因。 - -同理,思考扫雷游戏,应用 FloodFill 算法展开空白区域的同时,也需要计算并显示边界上雷的个数,如何实现的?其实也是相同的思路,遇到雷就返回 true,这样 surround 变量存储的就是雷的个数。当然,扫雷的 FloodFill 算法不能只检查上下左右,还得加上四个斜向。 - -![](../pictures/floodfill/ppt5.PNG) - -以上详细讲解了 FloodFill 算法的框架设计,**二维矩阵中的搜索问题,都逃不出这个算法框架**。 - - - - - - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git a/think_like_computer/IntervalIntersection.md b/think_like_computer/IntervalIntersection.md new file mode 100644 index 0000000000..4b88df3268 --- /dev/null +++ b/think_like_computer/IntervalIntersection.md @@ -0,0 +1,131 @@ +# Interval Problem (III): Interval Intersection + + +**Translator: [GYHHAHA](https://github.com/GYHHAHA)** + +**Author: [labuladong](https://github.com/labuladong)** + +This is the third article about the interval problem, and the last two articles respectively introduce the interval scheduling problem and the interval merging problem. Now, we will talk about the topic about how to find out interval intersection from two set of intervals efficiently. + +【Leetcode 986】Interval List Intersections + +Given two lists of **closed** intervals, each list of intervals is pairwise disjoint and in sorted order. + +Return the intersection of these two interval lists. + +*(Formally, a closed interval [a, b] (with a <= b) denotes the set of real numbers xwith a <= x <= b. The intersection of two closed intervals is a set of real numbers that is either empty, or can be represented as a closed interval. For example, the intersection of [1, 3] and [2, 4] is [2, 3].)* + +**Example 1:** + +**![img](https://assets.leetcode.com/uploads/2019/01/30/interval1.png)** + +``` +Input: A = [[0,2],[5,10],[13,23],[24,25]], B = [[1,5],[8,12],[15,24],[25,26]] +Output: [[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]] +Reminder: The inputs and the desired output are lists of Interval objects, and not arrays or lists. +``` + +**Note:** + +1. `0 <= A.length < 1000` +2. `0 <= B.length < 1000` +3. `0 <= A[i].start, A[i].end, B[i].start, B[i].end < 10^9` + +**NOTE:** input types have been changed on April 15, 2019. Please reset to default code definition to get new method signature. + +### Part One: Thought + +The general thought for interval problems is sorting first. Since question states that it has been ordered, then we can use two pointers to find out the intersections. + +Here is the code: + +```python +# A, B like [[0,2],[5,10]...] +def intervalIntersection(A, B): + i, j = 0, 0 + res = [] + while i < len(A) and j < len(B): + # ... + j += 1 + i += 1 + return res +``` + +Next, we will analyze all the situations or cases. + +First, for two intervals, we use `[a1,a2]` and `[b1,b2]` to represent two intervals in the `A` and `B` respectively. So, let us find out how to make these two intervals don't have intersections. + +![](../pictures/intersection/1.jpg) + +It can be written in code like this: + +``` +if b2 < a1 or a2 < b1: + [a1,a2] and [b1,b2] don't exist intersection +``` + +Then, what conditions should be met when two intervals exist intersection? + +The negative proposition of the above logic is the condition. + +```python +# get a inverse direction of the sign of inequality, and change 'or' into 'and' +if b2 >= a1 and a2 >= b1: + [a1,a2] and [b1,b2] exist intersection +``` + +Then, we enumerate all the situation that two intervals exist intersection. + +![](../pictures/intersection/2.jpg) + +It seems very simple: only four situation. exist. Then we should think about what's the common feather among these situations. + +![](../pictures/intersection/3.jpg) + +We surprisingly observe that the intersection of intervals get regular pattern. If the intersection is `[c1,c2]` then `c1=max(a1,b1)`,`c2=min(a2,b2)`! Thus this observation is the key point of finding out the interaction. Now we make our code get further. + +```python +while i < len(A) and j < len(B): + a1, a2 = A[i][0], A[i][1] + b1, b2 = B[j][0], B[j][1] + if b2 >= a1 and a2 >= b1: + res.append([max(a1, b1), min(a2, b2)]) + # ... +``` + +Last step, it's surely that the pointer `i` and `j` will go forward, but when? + +![](../pictures/intersection/4.gif) + +It's more understandable throught the gif that whether going forward only depends on the relationship between `a2` and`b2`. + +```python +while i < len(A) and j < len(B): + # ... + if b2 < a2: + j += 1 + else: + i += 1 +``` + +### Second Part: Code + +```python +# A, B like [[0,2],[5,10]...] +def intervalIntersection(A, B): + i, j = 0, 0 # double pointers + res = [] + while i < len(A) and j < len(B): + a1, a2 = A[i][0], A[i][1] + b1, b2 = B[j][0], B[j][1] + # two intervals have intersection + if b2 >= a1 and a2 >= b1: + # compute the intersection and add it into res + res.append([max(a1, b1), min(a2, b2)]) + # Pointer go forward + if b2 < a2: j += 1 + else: i += 1 + return res +``` + +To give a brief summary, although the problem concerning intervals seems to be complicated, we can still use simple code to finish the task by observe common features between different situation. diff --git a/think_like_computer/IntervalMerging.md b/think_like_computer/IntervalMerging.md new file mode 100644 index 0000000000..d493c323ec --- /dev/null +++ b/think_like_computer/IntervalMerging.md @@ -0,0 +1,88 @@ +# Interval Problem (II): Interval Merging + +**Translator: [GYHHAHA](https://github.com/GYHHAHA)** + +**Author: [labuladong](https://github.com/labuladong)** + +In the "Interval Scheduling: Greedy Algorithm", we use greedy algorithm to solve the interval scheduling problem, which means, given a lot of intervals, finding out the maximum subset without any overlapping. + +Actually, there are many other relating problems about interval itself. Now, we will talk about the "Merge Interval Problem". + +【Leetcode 56】Merge Intervals + +Given a collection of intervals, merge all overlapping intervals. + +**Example 1:** + +``` +Input: [[1,3],[2,6],[8,10],[15,18]] +Output: [[1,6],[8,10],[15,18]] +Explanation: Since intervals [1,3] and [2,6] overlaps, merge them into [1,6]. +``` + +**Example 2:** + +``` +Input: [[1,4],[4,5]] +Output: [[1,5]] +Explanation: Intervals [1,4] and [4,5] are considered overlapping. +``` + +**NOTE:** input types have been changed on April 15, 2019. Please reset to default code definition to get new method signature. + +The general thought for solving interval problems is observing regular patterns after the sorting process. + +### First Part: Thought + +A certain interval can be defined as`[start, end]`, the interval scheduling in the last article states the sorting process need to be done by `end`. But for the merging problem, both sorting with the `end` or `start` are acceptable. For the clear purpose, we choose sorting by `start` . + +【Explanations for chinese in the picture】 + +【按start排序:sorting by start】【索引:index】 + +![1](../pictures/mergeInterval/1.jpg) + +Clearly, for the merging result `x`, `x.start`must have the smallest `start` in these intersected intervals, and `x.end` must have the largest `end` in these intersected intervals as well. + +![2](../pictures/mergeInterval/2.jpg) + +Since ordered, `x.start` is easy to achieve, and computing `x.end` is also not difficult as well, which can take an analogy of searching the max number in a certain array. + +```java +int max_ele = arr[0]; +for (int i = 1; i < arr.length; i++) + max_ele = max(max_ele, arr[i]); +return max_ele; +``` + +### Second Part: Code + +```python +# intervals like [[1,3],[2,6]...] +def merge(intervals): + if not intervals: return [] + # ascending sorting by start + intervals.sort(key=lambda intv: intv[0]) + res = [] + res.append(intervals[0]) + + for i in range(1, len(intervals)): + curr = intervals[i] + # quote of the last element in res + last = res[-1] + if curr[0] <= last[1]: + # find the biggest end + last[1] = max(last[1], curr[1]) + else: + # address next interval need to be merged + res.append(curr) + return res +``` + +It will be illustrated more clearly by the follow gif. + +![3](../pictures/mergeInterval/3.gif) + +So far, the Interval Merging Problem have been solved. + +The End. Hope this article can help you! diff --git a/think_like_computer/PancakesSorting.md b/think_like_computer/PancakesSorting.md new file mode 100644 index 0000000000..0ccda0f2a0 --- /dev/null +++ b/think_like_computer/PancakesSorting.md @@ -0,0 +1,121 @@ +# Pancakes Sorting + +**Translator: [Dong Wang](https://github.com/Coder2Programmer)** + +**Author: [labuladong](https://github.com/labuladong)** + +The pancake sorting is a very interesting practical problem: assuming there are `n` pieces of pancakes of **different sizes** on the plate, how do you turn it several times with a spatula to make these pancakes in order(small up, big down)? + +![](../pictures/pancakeSort/1.jpg) + +Imagine using a spatula to flip a pile of pancakes. There are actually a few restrictions that we can only flip the top cakes at a time: + +![](../pictures/pancakeSort/2.png) + +Our question is, **how do you use an algorithm to get a sequence of flips to make the cake pile order**? + +First, we need to abstract this problem and use an array to represent the pancakes heap: + +![](../pictures/pancakeSort/title.png) + +How to solve this problem? In fact, it is similar to the previous article [Part of a Recursive Reverse Linked List](../data_structure/reverse_part_of_a_linked_list_via_recursion.md), which also requires **recursive thinking**. + +### 1. Analysis of idea + +Why is this problem recursive? For example, we need to implement a function like this: + +```java +// cakes is a bunch of pancakes, the function will sort the first n pancakes +void sort(int[] cakes, int n); +``` + +If we find the largest of the first `n` pancakes, then we try to flip this pancake to the bottom: + +![](../pictures/pancakeSort/3.jpg) + +Then, the scale of the original problem can be reduced, recursively calling `pancakeSort (A, n-1)`: + +![](../pictures/pancakeSort/4.jpg) + +Next, how to sort the `n-1` pancakes above? Still find the largest piece of pancakes from it, then place this piece of pancake to the bottom, and then recursively call `pancakeSort (A, n-1-1)` ... + +You see, this is the nature of recursion. To summarize, the idea is: + +1. Find the largest of the `n` pancakes. +2. Move this largest pancake to the bottom. +3. Recursively call `pancakeSort(A, n-1)`. + +Base case: When `n == 1`, there is no need to flip when sorting 1 pancake. + +So, the last question left, **how do you manage to turn a piece of pancake to the end**? + +In fact, it is very simple. For example, the third pancake is the largest, and we want to change it to the end, that is, to the `n` block. You can do this: + +1. Use a spatula to turn the first 3 pieces of pancakes, so that the largest pancake turns to the top. +2. Use a spatula to flip all the first `n` cakes, so that the largest pancake turns to the `n`-th pancake, which is the last pancake. + +After the above two processes are understood, the solution can be basically written, but the title requires us to write a specific sequence of inversion operations, which is also very simple, as long as it is recorded each time the pancake is turned. + +### 2. Code implementation + +As long as the above ideas are implemented in code, the only thing to note is that the array index starts from 0, and the results we want to return are calculated from 1. + +```java +// record the reverse operation sequence +LinkedList res = new LinkedList<>(); + +List pancakeSort(int[] cakes) { + sort(cakes, cakes.length); + return res; +} + +void sort(int[] cakes, int n) { + // base case + if (n == 1) return; + + // find the index of the largest pancake + int maxCake = 0; + int maxCakeIndex = 0; + for (int i = 0; i < n; i++) + if (cakes[i] > maxCake) { + maxCakeIndex = i; + maxCake = cakes[i]; + } + + // first flip, turn the largest pancake to the top + reverse(cakes, 0, maxCakeIndex); + res.add(maxCakeIndex + 1); + // second flip, turn the largest pancake to the bottom + reverse(cakes, 0, n - 1); + res.add(n); + + // recursive + sort(cakes, n - 1); +} + +void reverse(int[] arr, int i, int j) { + while (i < j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; j--; + } +} +``` + +hrough the detailed explanation just now, this code should be very clear. + +The time complexity of the algorithm is easy to calculate, because the number of recursive calls is `n`, each recursive call requires a for loop, the time complexity is O(n), so the total complexity is O(n^2). + +**Finally, we can think about a problem.**: According to our thinking, the length of the operation sequence should be `2(n-1)`, because each recursion needs to perform 2 flips and record operations and there are always `n` layers of recursion, but since the base case returns the result directly without inversion, the length of the final operation sequence should be fixed `2(n-1) `. + +Obviously, this result is not optimal (shortest). For example, a bunch of pancakes `[3,2,4,1]`. The flip sequence obtained by our algorithm is `[3,4,2,3,1,2]`, but the fastest way to flip should be ` [2,3,4] `: + +* Initial state: `[3,2,4,1]` +* Turn over the first two: `[2,3,4,1]` +* Turn over the first three: `[4,3,2,1]` +* Turn over the first 4: `[1,2,3,4]` + +If your algorithm is required to calculate the **shortest** operation sequence for sorting biscuits, how do you calculate it? In other words, what is the core idea and what algorithm skills must be used to solve the problem of finding the optimal solution? + +May wish to share your thoughts. diff --git a/think_like_computer/README.md b/think_like_computer/README.md deleted file mode 100644 index 02fde7ad2c..0000000000 --- a/think_like_computer/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# 算法思维系列 - -本章包含一些常用的算法技巧,比如前缀和、回溯思想、位操作、双指针、如何正确书写二分查找等等。 - -欢迎关注我的公众号 labuladong,方便获得最新的优质文章: - -![labuladong二维码](../pictures/qrcode.jpg) \ No newline at end of file diff --git a/think_like_computer/RussianDollEnvelopes.md b/think_like_computer/RussianDollEnvelopes.md new file mode 100644 index 0000000000..ae2d1dd65a --- /dev/null +++ b/think_like_computer/RussianDollEnvelopes.md @@ -0,0 +1,122 @@ +# Russian Doll Envelopes + +**Translator: [Dong Wang](https://github.com/Coder2Programmer)** + +**Author: [labuladong](https://github.com/labuladong)** + +Many algorithm problems require sorting skills. The difficulty is not in the sort itself, but ingenious sorting for preprocessing, transforming the algorithm problems, and laying the foundation for subsequent operations. + +The russian doll envelopes needs to be sorted according to specific rules, and then converted into a [Longest Incremental Subsequence Problem](../dynamic_programming/LongestIncrementalSubsequence.md). You can use the trick of previous text [Binary Search Detailed Explanation](binarySearch.md) to solve the problem. + +### 1. Overview + +Russian doll envelopes is a very interesting and often occurring problem in life. Let's look at the problem first: + +You have a number of envelopes with widths and heights given as a pair of integers `(w, h)`. One envelope can fit into another if and only if both the width and height of one envelope is greater than the width and height of the other envelope. + +What is the maximum number of envelopes can you Russian doll? (put one inside other) + +Note: + +Rotation is not allowed. + +Example: +
+Input: [[5,4],[6,4],[6,7],[2,3]]
+Output: 3 
+Explanation: The maximum number of envelopes you can Russian doll is 3 ([2,3] => [5,4] => [6,7]).
+
+ +This question is actually a variant of Longes Increasing Subsequence(LIS), because it is clear that each legal nesting is a large set of small, which is equivalent to finding a longest increasing subsequence , and its length is the maximum number of envelopes that can be nested. + +But the difficulty is that the standard LIS algorithm can only find the longest subsequence in the array, and our envelope is represented by a two-dimensional number pair like `(w, h)`. How can we apply the LIS algorithm? + +![0](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/0.jpg) + +The reader may calculate the area by `w × h`, and then perform the standard LIS algorithm on the area. However, if you think about it a little, you will find that this is not possible. For example, `1 × 10` is greater than` 3 × 3`, but obviously such two envelopes cannot be nested inside each other. + +### 2. Solution + +The solution to this problem is relatively clever: + +**First sort the width `w` in ascending order. If you encounter the same situation with` w`, sort in descending order by height `h`. Then use all `h` as an array, and calculate the length of LIS on this array is the answer.** + +Draw a picture to understand, first sort these number pairs: + +![1](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/1.jpg) + +Then look for the longest increasing subsequence on `h`: + +![2](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/2.jpg) + +This subsequence is the optimal nesting scheme. + +The key to this solution is that for pairs of the same width `w`, the height` h` is sorted in descending order. Because two envelopes of the same width cannot contain each other, reverse ordering guarantees that at most one of the pairs of the same `w` is selected. + +The code as follow: + +```java +// envelopes = [[w, h], [w, h]...] +public int maxEnvelopes(int[][] envelopes) { + int n = envelopes.length; + // sort by ascending width, and sort by descending height if the width are the same + Arrays.sort(envelopes, new Comparator() + { + public int compare(int[] a, int[] b) { + return a[0] == b[0] ? + b[1] - a[1] : a[0] - b[0]; + } + }); + // find LIS on the height array + int[] height = new int[n]; + for (int i = 0; i < n; i++) + height[i] = envelopes[i][1]; + + return lengthOfLIS(height); +} +``` + +Regarding the search method for the longest increasing subsequence, the dynamic programming solution was introduced in detail in the previous article, and the binary search solution was explained using a poker game. This article will not expand and directly apply the algorithm template: + +```java +/* returns the length of LIS in nums */ +public int lengthOfLIS(int[] nums) { + int piles = 0, n = nums.length; + int[] top = new int[n]; + for (int i = 0; i < n; i++) { + // playing card to process + int poker = nums[i]; + int left = 0, right = piles; + // position to insert for binary search + while (left < right) { + int mid = (left + right) / 2; + if (top[mid] >= poker) + right = mid; + else + left = mid + 1; + } + if (left == piles) piles++; + // put this playing cart on top of the pile + top[left] = poker; + } + // the number of cards is the LIS length + return piles; +} +``` +For clarity, I divided the code into two functions. You can also merge them to save space in the `height` array. + +The time complexity of this algorithm is *O(NlogN)*, because sorting and calculating LIS each takes *O(NlogN)*. + +The space complexity is *O(N)*, because a `top` array is needed in the function to calculate LIS. + +### 3. Conclusion + +This problem is a hard-level problem, and its difficult lies in sorting. The problem is transformed into a standard LIS problem after correct sorting, which is easy to solve. + +In fact, this problem can also be extended to three dimensions. For example, instead of letting you nest envelopes, you need to nest boxes. Each box has three dimensions: length, width, and height. Can you count how many boxes can be nested? + +We may think so, first find the nested sequence according to the idea of envelope nesting in the first two dimensions(length and width), and finally find LIS in the third dimension(height) of this sequence, and we should be able to calculate the answer. + +In fact, this idea is wrong. This type of problem is called a *partial order problem*. Ascending to three dimensions will greatly increase the difficulty. An advanced data structure called *Binary Index Tree* is needed, and interested readers can search by themselves. + +There are many algorithmic problems that need to be sorted and processed, and author is collating and summarizing. Hope this article is helpful to you. diff --git a/think_like_computer/SlidingWindowTechnique.md b/think_like_computer/SlidingWindowTechnique.md new file mode 100644 index 0000000000..760a7f63c2 --- /dev/null +++ b/think_like_computer/SlidingWindowTechnique.md @@ -0,0 +1,295 @@ +# Sliding Window Technique + +**Translator: [floatLig](https://github.com/floatLig)** + +**Author: [labuladong](https://github.com/labuladong)** + +This article shows you the magic template for "sliding window" with two pointers: the left and right of the window. With this, you can easily solve several difficult substring matching problems. + +There are at least 9 problems in LeetCode that can be solved efficiently using this method. In this article, we choose three problems with the most votes, more classical to explain. The first question, in order for the reader to master the algorithm template, the last two questions are easy to answer according to the template. + +This article code for C++ implementation, will not use any programming quirks, but still briefly introduce some of the data structure used, in case some readers because of the language details of the problem hindering the understanding of the algorithm idea: + +`unordered_map` is `hashmap`, one of its methods, `count(key)`, corresponds to `containsKey(key)` to determine whether the key exists or not. + +`Map [key]` can be used to access the corresponding `value` of the `key`. Note that if the key does not exist, C++ automatically creates the key and assigns the `map[key]` value to 0. + +`map[key]++`, which appears many times in the code, is equivalent to `map.put(key, map.getordefault (key, 0) + 1)` in Java. + +Now let's get to the point. + +### 1. Minimum Window Substring + +![description](../pictures/Sliding_window/title1.jpg) + +The question asks us to return the minimum substring from the string S (Source) which has all the characters of the string T (Target). Let us call a substring desirable if it has all the characters from T. + +If you don't use any optimization, the code would look like this: + +```java +for (int i = 0; i < s.size(); i++) + for (int j = i + 1; j < s.size(); j++) + if s[i:j] contains all letters of t: + update answer +``` + +Although the idea is very straightforward, but the *time complexity* of this algorithm is O(N^2). + +We can solve it with sliding window. The sliding window algorithm idea is like this: + +1. We start with two pointers, *left and right* initially pointing to the first element of the string S. + +2. We use the right pointer to expand the window [left, right] until we get a desirable window that contains all of the characters of T. + +3. Once we have a window with all the characters, we can move the left pointer ahead one by one. If the window is still a desirable one we keep on updating the minimum window size. + +4. If the window is not desirable any more, we repeat step 2 onwards. + +This idea actually not difficult. **Move right pointer to find a valid window. When a valid window is found, move left pointer to find a smaller window (optimal solution)**. + +Now let's graph it. `needs` and `window` act as counters. `needs` record the number of occurrences of characters in T, and `window` record the number of occurrences of the corresponding character. + +Initial State: + +![0](../pictures/Sliding_window/0.png) + +Moving the right pointer until the window has all the elements from string T. + +![0](../pictures/Sliding_window/1.png) + +Now move the left pointer. Notice the window is still desirable and smaller than the previous window. + +![0](../pictures/Sliding_window/2.png) + +After moving left pointer again, the window is no more desirable. + +![0](../pictures/Sliding_window/3.png) + +We need to increment the right pointer and left pointer to look for another desirable window until the right pointer reaches the end of the string S (the algorithm ends). + +If you can understand the above process, congratulations, you have fully mastered the idea of the sliding window algorithm. + +Here comes the simple pseudocode. + +```cpp +string s, t; +// Looking for the "minimum substring" of t in s +int left = 0, right = 0; +string res = s; + +while(right < s.size()) { + window.add(s[right]); + right++; + // When we found a valid window, move left to find smaller window. + while (found a valid window) { + // If the window's substring is shorter, update the res + res = minLen(res, window); + window.remove(s[left]); + left++; + } +} +return res; +``` + +If you can understand the code above, you are one step closer to solving the problem. Now comes the tricky question: how do you tell if the window (substring s[left...right]) meets the requirements (contains all characters of t)? + +A general way is to use two hashmap as counters. To check if a window is valid, we use a map `needs` to store `(char, count)` for chars in t. And use counter `window` for the number of chars of t to be found in s. If `window` contains all the keys in `needs`, and the value of these keys is greater than or equal to the value in `needs`, we know that `window` meets the requirements and can start moving the left pointer. + +Refinement pseudocode above. + +```cpp +string s, t; +// Two pointers +int left = 0, right = 0; +string res = s; + +// Initialize the map +unordered_map window; +unordered_map needs; +for (char c : t) needs[c]++; + +// The number of characters that meet the requirement +int match = 0; + +while (right < s.size()) { + char c1 = s[right]; + if (needs.count(c1)) { + window[c1]++; // Add to window + if (window[c1] == needs[c1]) + // The number of occurrences of the character c1 meets the requirement + match++; + } + right++; + + // When we found a valid window + while (match == needs.size()) { + // Update res here if finding minimum + res = minLen(res, window); + // Increase left pointer to make it invalid/valid again + char c2 = s[left]; + if (needs.count(c2)) { + window[c2]--; // Remove from window + if (window[c2] < needs[c2]) + // The number of occurrences of the character c2 no longer meets the requirement + match--; + } + left++; + } +} +return res; +``` + +The above code already has complete logic, only a pseudo-code, that is, update `res`, but this problem is too easy to solve, directly see the solution! + +The code of solving this problem is below. + +```cpp +string minWindow(string s, string t) { + // Records the starting position and length of the shortest substring + int start = 0, minLen = INT_MAX; + int left = 0, right = 0; + + unordered_map window; + unordered_map needs; + for (char c : t) needs[c]++; + + int match = 0; + + while (right < s.size()) { + char c1 = s[right]; + if (needs.count(c1)) { + window[c1]++; + if (window[c1] == needs[c1]) + match++; + } + right++; + + while (match == needs.size()) { + if (right - left < minLen) { + // Updates the position and length of the smallest string + start = left; + minLen = right - left; + } + char c2 = s[left]; + if (needs.count(c2)) { + window[c2]--; + if (window[c2] < needs[c2]) + match--; + } + left++; + } + } + return minLen == INT_MAX ? + "" : s.substr(start, minLen); +} +``` + +I think it would be hard for you to understand if you were presented with a large piece of code, but can you understand the logic of the algorithm by following up? Can you see clearly the structure of the algorithm? + +**Time Complexity**: O(|S| + |T|) where |S| and |T| represent the lengths of strings S and T. In the worst case we might end up visiting every element of string S twice, once by left pointer and once by right pointer. ∣T∣ represents the length of string T. + +The reader might think that the nested while loop complexity should be a square, but you can think of it this way, the number of while executions is the total distance that the double pointer left and right traveled, which is at most 2 meters. + +### 2. Find All Anagrams in a String + +![description](../pictures/Sliding_window/title2.jpg) + +The difficulty of this problem is medium, but using the above template, it should be easy. + +If you update the res of the original code, you can get the answer to this problem. + +```cpp +vector findAnagrams(string s, string t) { + // Init a collection to save the result + vector res; + int left = 0, right = 0; + // Create a map to save the Characters of the target substring. + unordered_map needs; + unordered_map window; + for (char c : t) needs[c]++; + // Maintain a counter to check whether match the target string. + int match = 0; + + while (right < s.size()) { + char c1 = s[right]; + if (needs.count(c1)) { + window[c1]++; + if (window[c1] == needs[c1]) + match++; + } + right++; + + while (match == needs.size()) { + // Update the result if find a target + if (right - left == t.size()) { + res.push_back(left); + } + char c2 = s[left]; + if (needs.count(c2)) { + window[c2]--; + if (window[c2] < needs[c2]) + match--; + } + left++; + } + } + return res; +} +``` + +Since this problem is similar to the previous one, the `window` also needs to contain all the characters of the string t, but the last problem is to find the shortest substring. This problem is to find a substring of the same length. + +### 3. Longest Substring Without Repeating Characters + +![description](../pictures/Sliding_window/title3.jpg) + +When you encounter substring problems, the first thing that comes to mind is the sliding window technique. + +Similar to the previous idea, use `window` as a counter to record the number of occurrences of characters in the window. Then move the right pointer to scan through the string. If the character is already in `window`, move the left pointer to the right of the same character last found. + +```cpp +int lengthOfLongestSubstring(string s) { + int left = 0, right = 0; + unordered_map window; + int res = 0; // Record maximum length + + while (right < s.size()) { + char c1 = s[right]; + window[c1]++; + right++; + // If a duplicate character appears in the window + // Move the left pointer + while (window[c1] > 1) { + char c2 = s[left]; + window[c2]--; + left++; + } + res = max(res, right - left); + } + return res; +} +``` + +One thing needs to be mentioned is that when asked to find maximum substring, we should update maximum after the inner while loop to guarantee that the substring is valid. On the other hand, when asked to find minimum substring, we should update minimum inside the inner while loop. + +### Summarize + +Through the above three questions, we can summarize the abstract idea of sliding window algorithm: + +```java +int left = 0, right = 0; + +while (right < s.size()) { + window.add(s[right]); + right++; + + while (valid) { + window.remove(s[left]); + left++; + } +} +``` + +The data type of the window can vary depending on the situation, such as using the hash table as the counter, or you can use an array to do the same, since we only deal with English letters. + +The slightly tricky part is the `valid` condition, and we might have to write a lot of code to get this updated in real time. For example, the first two problems, it seems that the solution is so long, in fact, the idea is still very simple, but most of the code is dealing with this problem. \ No newline at end of file diff --git a/think_like_computer/The_key_to_resolving_TwoSum_problems.md b/think_like_computer/The_key_to_resolving_TwoSum_problems.md new file mode 100644 index 0000000000..eab3368ffd --- /dev/null +++ b/think_like_computer/The_key_to_resolving_TwoSum_problems.md @@ -0,0 +1,158 @@ +# The key to resolving Two Sum problems + +**Translator**: [Fulin Li](https://fulinli.github.io/) + +**Author**:[labuladong](https://github.com/labuladong) + +There are a series of problems with Two Sum in LeetCode, and this article will pick out some representative problems to demonstrate how to resolve the Two Sum problems. + +### TwoSum I + +**The most basic form** of Two Sum problems is like this: Given an array of integers `nums`, and a specific integer `target`. Return indices of the two numbers such that they add up to `target`. You may assume that each input would have **exactly** one solution. + +For example, given `nums = [3,1,3,6], target = 6`, the program should return an array `[0,2]` because 3 + 3 = 6. + +So, how to solve this problem? First, the simplest method, of course, is the exhaustive search. + +```java +int[] twoSum(int[] nums, int target) { + + for (int i = 0; i < nums.length; i++) + for (int j = i + 1; j < nums.length; j++) + if (nums[j] == target - nums[i]) + return new int[] { i, j }; + + // If no such two numbers exists + return new int[] {-1, -1}; +} +``` + +This method is straightforward. The time complexity is O(n^2)​ and space complexity is O(1)​. + +We can use a hash table to reduce the time complexity: + +```java +int[] twoSum(int[] nums, int target) { + int n = nums.length; + index index = new HashMap<>(); + // Constructing a hash table: Elements are mapped to their corresponding indices + for (int i = 0; i < n; i++) + index.put(nums[i], i); + + for (int i = 0; i < n; i++) { + int other = target - nums[i]; + // IF 'other' exists and it is not nums[i]. + if (index.containsKey(other) && index.get(other) != i) + return new int[] {i, index.get(other)}; + } + + return new int[] {-1, -1}; +} +``` + +In this way, because the query time of a hash table is O(1), the time complexity of the algorithm is reduced to O(N). However, the space complexity is increased to O(N) for storing the hash table. Generally, it is more efficient than the exhaustive search method. + +**I think the objective of the two sum problems is to tell us how to use the hash table.** Let's go on to the next. + +### TwoSum II + +We can modify the last script slightly to design a class with two functions: + +```java +class TwoSum { + // Add a 'number' to data structure + public void add(int number); + // Find out whether there exist two numbers and their sum is equal to 'value'. + public boolean find(int value); +} +``` + +So how to implement these two functions? We can follow the last problem and use a hash table to realize the 'find' function. + +```java +class TwoSum { + Map freq = new HashMap<>(); + + public void add(int number) { + // Recording the number of times that number has occurred + freq.put(number, freq.getOrDefault(number, 0) + 1); + } + + public boolean find(int value) { + for (Integer key : freq.keySet()) { + int other = value - key; + // Situation 1 + if (other == key && freq.get(key) > 1) + return true; + // Situation 2 + if (other != key && freq.containsKey(other)) + return true; + } + return false; + } +} +``` + +When diving into `find` function, there are two situations, for example: + +Situation 1: After `[3,3,2,5]` is inputted in `add` function, `find(6)` is executed. There are two `3` exists and 3 + 3 = 6, thus, it will return true. + +Situation 2: After `[3,3,2,5]` is inputted in `add` function, `find(7)` is executed. Only when `key` is equal to 2 and `other` is equal to 5, it will return true. + +Except for the two situations mentioned above, `find` function will only return false. + +What's the time complexity of this algorithm? The time complexity of `add` function and `find` function is O(1) and O(N) respectively. The space complexity is O(N), which is similar to the last problem. + +**However, we should take realities of the situation into account in API design.** For example, in our class, the function `find` is used very frequently, and each time it requires O(N) times. It is a massive waste of time. Can we optimize the algorithm given in this situation? + +Of course, we can optimize the algorithm when `find` function is used frequently. We can refer to the brute force method in the last problem and utilize a hash set to optimize `find` function pertinently. + +```java +class TwoSum { + Set sum = new HashSet<>(); + List nums = new ArrayList<>(); + + public void add(int number) { + // Recording all possible sum of two numbers + for (int n : nums) + sum.add(n + number); + nums.add(number); + } + + public boolean find(int value) { + return sum.contains(value); + } +} +``` + +In this way, all possible sum of two numbers is stored in `sum`. Every time `find` function takes O(1) time to search whether the target exists in the collection. Obviously, it is very suitable for frequent use of find function. + +### Summary + +For TwoSum problems, one of the difficulties is that the given array is **unordered**. For an unordered array, it seems that we don't have any efficient methods, and an exhaustive search method may be the only way. + +**In ordinary circumstances, we will sort the unordered array first and then consider applying the dual-pointer method.** TwoSum problems make us aware that HashMap or HashSet could help us to resolve unordered array problems. + +Remarkably, the essence of such method is to trade time for space, using different data structures to improve the algorithm performance pertinently. + +Finally, if the given array in TwoSum I is ordered, how do we design the algorithm? It's very easy and you can refer to the previous article「Summary of usage of dual-pointer」: + +```java +int[] twoSum(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left < right) { + int sum = nums[left] + nums[right]; + if (sum == target) { + return new int[]{left, right}; + } else if (sum < target) { + left++; // Make sum bigger + } else if (sum > target) { + right--; // Make sum smaller + } + } + // If no such two numbers exists + return new int[]{-1, -1}; +} +``` + +**Mission**: Stick to original high-quality articles, and work hard to make algorithmic problems clear. Welcome to subscribe my Wechat public account `ID:labuladong` for latest articles. \ No newline at end of file diff --git a/think_like_computer/ThewaytoAlgorithmlearning.md b/think_like_computer/ThewaytoAlgorithmlearning.md new file mode 100644 index 0000000000..0d513ac284 --- /dev/null +++ b/think_like_computer/ThewaytoAlgorithmlearning.md @@ -0,0 +1,94 @@ +# The way to Algorithm learning + +**Translator**: [ShuozheLi](https://github.com/ShuoZheLi/) +**Author**: [labuladong](https://github.com/labuladong) + +I have published an article about the ideal framework. People from the community have give praised me. I have never thought so many people will be agree with me. I will work harder to write more easily understanding Algorithm articles. + +Many friends asked me how should I learn Data Structure and Algorithm. Especially those who are beginner feel exhausted when going through all the LeetCode questions even after reading my article about the ideal framework. So, they hope to explain and tell them the way how I begin. + +First of all, I want to congrats my friends who are asking me for it because you have learned that you need external help from experienced people. And, you already start to practice Algorithm problems. Remember, there are not many people made to this step. + +For the ideal framework, a beginner may not be easy to understand. But if you are able to understand it, you are not a beginner anymore :D! It just like software engineering. People like me who never lead a group project feel so bored. But, for someone who has led a team. He/she will treat every sentence in software engineering as a treasure. (if you do not understand this, it is fine for now) + +Now, I will go through my experience. +**if you have read many articles such as "how to practice LeetCode" and "how to study Algorithm", but you still cannot keep up. Then, this article is for you.** + +When I begin to learn Data Structure and Algorithm, I am always having knowledge gaps in my mind. +If we summary them into two questions: +1.what is it? +2.what is that for? + +For example, if you learned the word stack, your teacher may tell you "first in last out" or "function stack." However, all these are like literature words. They cannot answer your question. +Here is how to answer them: +1.what is it? Go read the textbook or description of its basic elements +2.what is that for? practice some coding questions + +**1.what the hell is it?** + +This question is easy to solve. You just need to read a chapter of the book. And then make your own Queue or Stack. +If you can understand the first half of the ideal framework: Data structure is just the combination of array and linked-list. All the operations are just add, remove, search, modify. + +For example, Queue is just made by an array or linked-list. For enqueue and dequeue operations are just add and remove function in these data type. You do not even need to writer a new operation for them. + +**2.what is that for?** + +This problem covers the design of Algorithm. This will take a long time to make through. You need to practice a lot of questions and train yourself to think like a computer. + +The previous article has said. Algorithm is just about how to use data structure. The frequent Algorithm question are just a few types. many problems just changed a few conditions. Practice problems just help to see the pattern in questions. With the pattern in your head, you can solve problems with your own framework. Feels like plug in numbers into an equation. + +For example, if you need to escape a maze, you need to treat the problem abstractly. +maze -> graph search -> tree search -> binary tree search +then you just apply your own framework + +You need to abstract and categorize the problem when you are practicing LeetCode. This will help you to find the pattern. + +**3. how to read a book** + +Let me just recommend a book to you. +Algorithms, 4th Edition by Robert Sedgewick and Kevin Wayne +If you can need 50% of the book, you are at the average level. Do think the thickness of the book because you only need to read two-third of the book. + +Now let's talk about how to read it. +Reading the book using recursively: from the top to bottom, step by step to divide the problem. + +This is book has a good structure for beginners, so you can read from the first chapter to the end. **Make sure you typed down and run all the code in the book** Because these are really just the basic elements of Algorithm. Do not just think you can understand them without doing it. But, if you know the basics, you can jump the beginning. You can also jump the math proving part and practice part. This shall make the book less thick. + +There is a trick to read a book. You should not be stoped by details. You need to build a big knowledge framework first. +**keep move on, do not get stuck by details** + +Well, the ending part of the book is very hard. There are some famous Algorithms such as Knuth–Morris–Pratt algorithm (KMP) and Regular expression. These are not useful for doing LeetCode so you do not have to know them. + +**4. how to practice problem** +1.there is not a linearly relationship between Algorithm and Math ability and program language does not really matter. Algorithm is just a way to think. To think like a computer is just like riding a bike. You have to think you are walking with two wheels, not two feet. + +LeetCode problem does not like the classic Algorithm we talked about before. Leetcode problems are more like brain teasers. +For example, you need to use a queue to make a stack or oppositely make a stack with a queue. Add two numbers without using add sign. + +Although these questions are useless, to solve them you still need a solid understanding of data structure and basic knowledge. This is why the companys ask them. + +For the beginner, **you should go to the "Explore" in menu and start on "learn"** +This will help you to go through the basic data structure and Algorithm. There are lectures and corresponding practice problems. + +Recently, the "learn" part added something new like Ruby and Machine learning. You have no need to worry about that. You just need to finish the basic part. Then, you can just go directly to the "Interview" problem part. + +No matter you start with "Explore" or "Problems". You better practice problems by types. For example, you can finish all the problem in the linked-list then jump to binary tree problem. This helps you find the framework and pattern and practice applying them. + +**5. I know what you said, but I cannot keep up** + +This is all bout what you really want. You need to activate your desire! +!! what I am saying is not a hobby but strong desire!! +Let me take myself as an example + +Half-year ago I start to practice problems for a job after graduation. But, most of the people start it when they almost graduate. + +I know I am not smart so I start early. I have a strong desire for a decent job. I have girl who I want to be in love and I have made boasts in front of my friend. So, I have to achieve it to earn the money the fame I want. The desire for money and for fame has made to work harder and harder. + +But, I am the kind of person who does not good at doing things quickly right before deadline. I understand things slowly. Therefore, I decide to wake up early and start everything ahead. In fact, if you keep focusing on something for only just a month. You can see your improvement by your eye. + +Also, as a person who likes to share, I find out what I said actually helps others. This gives me recognition too! This is also what I want!! So, I decided to write more about what I experienced and share them on WeChat and internet. + +Above, it is not only about Algorithm learning. We, as a human being, are driven by our desires. There must be a thing that is tangible to help us to keep up. We have to benefit directly form it! This should be a simple reason to keep up for what we want to achieve. + +**You can find me on Wechat official account labuladong**: +All my friend, labuladong loves you. diff --git a/think_like_computer/Union-Find-Application.md b/think_like_computer/Union-Find-Application.md new file mode 100644 index 0000000000..8594eaa9db --- /dev/null +++ b/think_like_computer/Union-Find-Application.md @@ -0,0 +1,218 @@ +# Application of Union-Find + +**Translator: [Ziming](https://github.com/ML-ZimingMeng/LeetCode-Python3)** + +**Author: [labuladong](https://github.com/labuladong)** + +Many readers in the previous article expressed interest in the Union-Find algorithm, so in this article, I will take a few LeetCode problems to talk about the ingenious use of this algorithm. + +First, let's recall that the Union-Find algorithm solves the problem of dynamic connectivity of the graph, but the algorithm itself is not difficult. Your ability which is to abstract the original problem into a question about graph theory to abstract the problem determines whether you can solve it. + +First let us review the algorithm code written in the previous article and answer a few questions: + +```java +class UF { + // Record the number of connected components + private int count; + // Store several trees + private int[] parent; + // Record the "weight" of the tree + private int[] size; + + public UF(int n) { + this.count = n; + parent = new int[n]; + size = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + /* Connect p and q */ + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // The small tree is more balanced under the big tree + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + count--; + } + + /* Determine whether p and q are connected to each other */ + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + // Nodes on the same tree are interconnected + return rootP == rootQ; + } + + /* Returns the root node of node x */ + private int find(int x) { + while (parent[x] != x) { + // Path compression + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + public int count() { + return count; + } +} +``` + +The algorithm has three key points: + +1. Use the `parent` array to record the parent node of each node, which is equivalent to a pointer to the parent node, so the` parent` array actually stores a forest (several multi-trees). + +2. Use the `size` array to record the weight of each tree. The purpose is to keep the` union` tree still balanced without degrading it into a linked list, which affects the operation efficiency. + +3. Path compression is performed in the `find` function to ensure that the height of any tree is kept constant, so that the time complexity of the` union` and `connected` API is O (1). + +Some readers may ask, **Since the path compression, does the weight balance of the `size` array still need**? This problem is very interesting, because path compression guarantees that the tree height is constant (not more than 3), even if the tree is unbalanced, the height is also constant, which basically has little effect. + +In my opinion, when it comes to time complexity, indeed, it is also O (1) without the need for weight balance. However, if the size array is added, the efficiency is still slightly higher, such as the following: + +![](../pictures/unionfind-application/1.jpg) + +If weight balance optimization is carried out, case 1 will surely be obtained, without weight optimization, case 2 may occur. The `while` loop of path compression is triggered only when the height is 3, so case 1 will not trigger path compression at all, while case 2 will perform path compression many times to compress the nodes in the third layer to the second layer. + +In other words, removing the weight balance, although the time complexity is still O (1) for a single `find` function call, the efficiency of the API call will decrease to some extent. Of course, the advantage is that it reduces some space, but for Big O notation, the space-time complexity has not changed. + +Let's get down to business and see what practical applications this algorithm has. + +### Ⅰ. DFS Alternatives + +Many problems solved by the DFS depth-first algorithm can also be solved by the Union-Find algorithm. + +For instance, Surrounded Regions of question 130: Given a 2D board containing `X` and `O` (the letter O), **capture all regions** surrounded by `X`. + +```java +void solve(char[][] board); +``` + +Note that `O` must be surrounded by four sides in order to be replaced with `X`, that is, `O` on the corner must not be enclosed, and further, `O` connected to `O` on the corner Will not be surrounded by `X` and will not be replaced. + +![](../pictures/unionfind-application/2.jpg) + +PS: This reminds me of the chess game "Othello" when I was a kid. As long as you use two pieces to sandwich each other's pieces, the opponent's pieces will be replaced with yours. Similarly, the pieces occupying the four corners are invincible, and the side pieces connected to it are also invincible (cannot be clipped). + +The traditional method of solving this problem is not difficult. First use the for loop to traverse the four sides of the chessboard, and use the DFS algorithm to replace those `O` connected to the border with a special character, such as `#`; Second, traverse the entire chessboard, replace the remaining `O` Into `X` and restore `#` to `O`. This can complete the requirements of the problem, time complexity O (MN). + +This problem can also be solved with the Union-Find algorithm. Although the implementation is more complicated and even less efficient, this is a general idea using the Union-Find algorithm and it is worth learning. + +**Those `O` which do not need to be replaced have a common ancestor called` dummy`. These `O` and` dummy` are connected to each other,however, those `O` that need to be replaced are not connected to` dummy`**. + +![](../pictures/unionfind-application/3.jpg) + +This is the core idea of Union-Find and it is easy to understand the code if you understand this diagram. + +Firstly, according to our implementation, the bottom layer of Union-Find is a one-dimensional array. The constructor needs to pass in the size of the array, and the title is a two-dimensional chessboard. + +Which is simple, that the two-dimensional coordinates `(x, y)` can be converted to the number `x * n + y` **This is a common technique for mapping two-dimensional coordinates to one dimension**. + +Secondly, the "patriarch" we described earlier is fictitious and we need to leave a place for his elderly. The index `[0 .. m * n-1]` is a one-dimensional mapping of the coordinates in the chessboard, so let this dummy `dummy` node occupy the index` m * n`. + +```java +void solve(char[][] board) { + if (board.length == 0) return; + + int m = board.length; + int n = board[0].length; + // Leave an extra room for dummy + UF uf = new UF(m * n + 1); + int dummy = m * n; + // Connect the first and last columns of O and dummy + for (int i = 0; i < m; i++) { + if (board[i][0] == 'O') + uf.union(i * n, dummy); + if (board[i][n - 1] == 'O') + uf.union(i * n + n - 1, dummy); + } + // Connect O and dummy in the first and last rows + for (int j = 0; j < n; j++) { + if (board[0][j] == 'O') + uf.union(j, dummy); + if (board[m - 1][j] == 'O') + uf.union(n * (m - 1) + j, dummy); + } + // Direction array d is a common method for searching up, down, left and right + int[][] d = new int[][]{{1,0}, {0,1}, {0,-1}, {-1,0}}; + for (int i = 1; i < m - 1; i++) + for (int j = 1; j < n - 1; j++) + if (board[i][j] == 'O') + // Connect this O with up, down, left and right O + for (int k = 0; k < 4; k++) { + int x = i + d[k][0]; + int y = j + d[k][1]; + if (board[x][y] == 'O') + uf.union(x * n + y, i * n + j); + } + // All O not connected to dummy shall be replaced + for (int i = 1; i < m - 1; i++) + for (int j = 1; j < n - 1; j++) + if (!uf.connected(dummy, i * n + j)) + board[i][j] = 'X'; +} +``` + +This code is very long. In fact, it is just the realization of the previous idea. Only the `O` connected to the boundary `O` have the connectivity with `dummy` and they will not be replaced. + +To be honest, the Union-Find algorithm solves this simple problem. It can be a bit of a killer. It can solve more complex and more technical problems. **The main idea is to add virtual nodes in a timely manner. Dynamic connectivity**. + +### Ⅱ. Satisfiability of Equality Equations + +This problem can be solved using the Union-Find algorithm, that is: + +Given an array equations of strings that represent relationships between variables, each string `equations[i]` has length `4` and takes one of two different forms: `"a==b"` or `"a!=b"`.  Here, `a` and `b` are lowercase letters (not necessarily different) that represent one-letter variable names. + +Return true if and only if it is possible to assign integers to variable names so as to satisfy all the given equations. + +The core idea of solving the problem is that **divide the expressions in `equations` into two parts according to `==` and `!=`, First process the expressions of `==`, so that they are connected. `!=` Expression to check if the inequality relationship breaks the connectivity of the equality relationship**. + +```java +boolean equationsPossible(String[] equations) { + // 26 letters + UF uf = new UF(26); + // Let equal letters form connected components first + for (String eq : equations) { + if (eq.charAt(1) == '=') { + char x = eq.charAt(0); + char y = eq.charAt(3); + uf.union(x - 'a', y - 'a'); + } + } + // Check if inequality relationship breaks connectivity of equal relationship + for (String eq : equations) { + if (eq.charAt(1) == '!') { + char x = eq.charAt(0); + char y = eq.charAt(3); + // If the equality relationship holds, it is a logical conflict + if (uf.connected(x - 'a', y - 'a')) + return false; + } + } + return true; +} +``` + +At this point, the problem of judging the validity of the expression is solved. Is it easy to use the Union-Find algorithm? + +### Ⅲ. Summery + +The Union-Find algorithm is a dynamic connectivity problem, that is, how to transform the original problem into a graph. For the legitimacy of the formula, the equivalent relationship can be directly used, and for the checkerboard envelopment problem, a virtual node is used to create the dynamic connectivity. + +In addition, you can use the directional array `d` to map a two-dimensional array to a one-dimensional array to simplify the amount of code. + +Many more complex DFS algorithm problems can be solved using the Union-Find algorithm. There are more than 20 Union-Find related questions on LeetCode, and you can go and try! + diff --git a/think_like_computer/Union-find-Explanation.md b/think_like_computer/Union-find-Explanation.md index ae1b9065a2..2bcf00b104 100644 --- a/think_like_computer/Union-find-Explanation.md +++ b/think_like_computer/Union-find-Explanation.md @@ -1,9 +1,9 @@ +# Detailed Explanation of Union-Find + **Translator: [Ziming](https://github.com/ML-ZimingMeng/LeetCode-Python3)** **Author: [labuladong](https://github.com/labuladong)** -# Detailed Explanation of Union-Find - Today I will talk about the Union-Find algorithm, which is often referred to as the Disjoint-Set algorithm, mainly to solve the problem of "dynamic connectivity" in graph theory. Nouns look a little confusing, but they ’re really easy to understand. We will explain it later. Moreover, the application of this algorithm is also very interesting. Speaking of this Union-Find, it should be my "Enlightenment Algorithm", this algorithm was introduced at the beginning of *Algorithms 4th edition*, I have to say that this algorithm shocked me! Later I discovered that leetcode also has related topics and is very interesting. Moreover, the solution given in *Algorithms 4th edition* can be further optimized. With only a small modification, the time complexity can be reduced to O (1). diff --git "a/think_like_computer/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" "b/think_like_computer/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" deleted file mode 100644 index 1f9e8ce284..0000000000 --- "a/think_like_computer/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" +++ /dev/null @@ -1,223 +0,0 @@ -# Union-Find算法应用 - -上篇文章很多读者对于 Union-Find 算法的应用表示很感兴趣,这篇文章就拿几道 LeetCode 题目来讲讲这个算法的巧妙用法。 - -首先,复习一下,Union-Find 算法解决的是图的动态连通性问题,这个算法本身不难,能不能应用出来主要是看你抽象问题的能力,是否能够把原始问题抽象成一个有关图论的问题。 - -先复习一下上篇文章写的算法代码,回答读者提出的几个问题: - -```java -class UF { - // 记录连通分量个数 - private int count; - // 存储若干棵树 - private int[] parent; - // 记录树的“重量” - private int[] size; - - public UF(int n) { - this.count = n; - parent = new int[n]; - size = new int[n]; - for (int i = 0; i < n; i++) { - parent[i] = i; - size[i] = 1; - } - } - - /* 将 p 和 q 连通 */ - public void union(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - if (rootP == rootQ) - return; - - // 小树接到大树下面,较平衡 - if (size[rootP] > size[rootQ]) { - parent[rootQ] = rootP; - size[rootP] += size[rootQ]; - } else { - parent[rootP] = rootQ; - size[rootQ] += size[rootP]; - } - count--; - } - - /* 判断 p 和 q 是否互相连通 */ - public boolean connected(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - // 处于同一棵树上的节点,相互连通 - return rootP == rootQ; - } - - /* 返回节点 x 的根节点 */ - private int find(int x) { - while (parent[x] != x) { - // 进行路径压缩 - parent[x] = parent[parent[x]]; - x = parent[x]; - } - return x; - } - - public int count() { - return count; - } -} -``` - -算法的关键点有 3 个: - -1、用 `parent` 数组记录每个节点的父节点,相当于指向父节点的指针,所以 `parent` 数组内实际存储着一个森林(若干棵多叉树)。 - -2、用 `size` 数组记录着每棵树的重量,目的是让 `union` 后树依然拥有平衡性,而不会退化成链表,影响操作效率。 - -3、在 `find` 函数中进行路径压缩,保证任意树的高度保持在常数,使得 `union` 和 `connected` API 时间复杂度为 O(1)。 - -有的读者问,**既然有了路径压缩,`size` 数组的重量平衡还需要吗**?这个问题很有意思,因为路径压缩保证了树高为常数(不超过 3),那么树就算不平衡,高度也是常数,基本没什么影响。 - -我认为,论时间复杂度的话,确实,不需要重量平衡也是 O(1)。但是如果加上 `size` 数组辅助,效率还是略微高一些,比如下面这种情况: - -![](../pictures/unionfind应用/1.jpg) - -如果带有重量平衡优化,一定会得到情况一,而不带重量优化,可能出现情况二。高度为 3 时才会触发路径压缩那个 `while` 循环,所以情况一根本不会触发路径压缩,而情况二会多执行很多次路径压缩,将第三层节点压缩到第二层。 - -也就是说,去掉重量平衡,虽然对于单个的 `find` 函数调用,时间复杂度依然是 O(1),但是对于 API 调用的整个过程,效率会有一定的下降。当然,好处就是减少了一些空间,不过对于 Big O 表示法来说,时空复杂度都没变。 - -下面言归正传,来看看这个算法有什么实际应用。 - -### 一、DFS 的替代方案 - -很多使用 DFS 深度优先算法解决的问题,也可以用 Union-Find 算法解决。 - -比如第 130 题,被围绕的区域:给你一个 M×N 的二维矩阵,其中包含字符 `X` 和 `O`,让你找到矩阵中**四面**被 `X` 围住的 `O`,并且把它们替换成 `X`。 - -```java -void solve(char[][] board); -``` - -注意哦,必须是四面被围的 `O` 才能被换成 `X`,也就是说边角上的 `O` 一定不会被围,进一步,与边角上的 `O` 相连的 `O` 也不会被 `X` 围四面,也不会被替换。 - -![](../pictures/unionfind应用/2.jpg) - -PS:这让我想起小时候玩的棋类游戏「黑白棋」,只要你用两个棋子把对方的棋子夹在中间,对方的子就被替换成你的子。可见,占据四角的棋子是无敌的,与其相连的边棋子也是无敌的(无法被夹掉)。 - -解决这个问题的传统方法也不困难,先用 for 循环遍历棋盘的**四边**,用 DFS 算法把那些与边界相连的 `O` 换成一个特殊字符,比如 `#`;然后再遍历整个棋盘,把剩下的 `O` 换成 `X`,把 `#` 恢复成 `O`。这样就能完成题目的要求,时间复杂度 O(MN)。 - -这个问题也可以用 Union-Find 算法解决,虽然实现复杂一些,甚至效率也略低,但这是使用 Union-Find 算法的通用思想,值得一学。 - -**你可以把那些不需要被替换的 `O` 看成一个拥有独门绝技的门派,它们有一个共同祖师爷叫 `dummy`,这些 `O` 和 `dummy` 互相连通,而那些需要被替换的 `O` 与 `dummy` 不连通**。 - -![](../pictures/unionfind应用/3.jpg) - -这就是 Union-Find 的核心思路,明白这个图,就很容易看懂代码了。 - -首先要解决的是,根据我们的实现,Union-Find 底层用的是一维数组,构造函数需要传入这个数组的大小,而题目给的是一个二维棋盘。 - -这个很简单,二维坐标 `(x,y)` 可以转换成 `x * n + y` 这个数(`m` 是棋盘的行数,`n` 是棋盘的列数)。敲黑板,**这是将二维坐标映射到一维的常用技巧**。 - -其次,我们之前描述的「祖师爷」是虚构的,需要给他老人家留个位置。索引 `[0.. m*n-1]` 都是棋盘内坐标的一维映射,那就让这个虚拟的 `dummy` 节点占据索引 `m * n` 好了。 - -```java -void solve(char[][] board) { - if (board.length == 0) return; - - int m = board.length; - int n = board[0].length; - // 给 dummy 留一个额外位置 - UF uf = new UF(m * n + 1); - int dummy = m * n; - // 将首列和末列的 O 与 dummy 连通 - for (int i = 0; i < m; i++) { - if (board[i][0] == 'O') - uf.union(i * n, dummy); - if (board[i][n - 1] == 'O') - uf.union(i * n + n - 1, dummy); - } - // 将首行和末行的 O 与 dummy 连通 - for (int j = 0; j < n; j++) { - if (board[0][j] == 'O') - uf.union(j, dummy); - if (board[m - 1][j] == 'O') - uf.union(n * (m - 1) + j, dummy); - } - // 方向数组 d 是上下左右搜索的常用手法 - int[][] d = new int[][]{{1,0}, {0,1}, {0,-1}, {-1,0}}; - for (int i = 1; i < m - 1; i++) - for (int j = 1; j < n - 1; j++) - if (board[i][j] == 'O') - // 将此 O 与上下左右的 O 连通 - for (int k = 0; k < 4; k++) { - int x = i + d[k][0]; - int y = j + d[k][1]; - if (board[x][y] == 'O') - uf.union(x * n + y, i * n + j); - } - // 所有不和 dummy 连通的 O,都要被替换 - for (int i = 1; i < m - 1; i++) - for (int j = 1; j < n - 1; j++) - if (!uf.connected(dummy, i * n + j)) - board[i][j] = 'X'; -} -``` - -这段代码很长,其实就是刚才的思路实现,只有和边界 `O` 相连的 `O` 才具有和 `dummy` 的连通性,他们不会被替换。 - -说实话,Union-Find 算法解决这个简单的问题有点杀鸡用牛刀,它可以解决更复杂,更具有技巧性的问题,**主要思路是适时增加虚拟节点,想办法让元素「分门别类」,建立动态连通关系**。 - -### 二、判定合法等式 - -这个问题用 Union-Find 算法就显得十分优美了。题目是这样: - -给你一个数组 `equations`,装着若干字符串表示的算式。每个算式 `equations[i]` 长度都是 4,而且只有这两种情况:`a==b` 或者 `a!=b`,其中 `a,b` 可以是任意小写字母。你写一个算法,如果 `equations` 中所有算式都不会互相冲突,返回 true,否则返回 false。 - -比如说,输入 `["a==b","b!=c","c==a"]`,算法返回 false,因为这三个算式不可能同时正确。 - -再比如,输入 `["c==c","b==d","x!=z"]`,算法返回 true,因为这三个算式并不会造成逻辑冲突。 - -我们前文说过,动态连通性其实就是一种等价关系,具有「自反性」「传递性」和「对称性」,其实 `==` 关系也是一种等价关系,具有这些性质。所以这个问题用 Union-Find 算法就很自然。 - -核心思想是,**将 `equations` 中的算式根据 `==` 和 `!=` 分成两部分,先处理 `==` 算式,使得他们通过相等关系各自勾结成门派;然后处理 `!=` 算式,检查不等关系是否破坏了相等关系的连通性**。 - -```java -boolean equationsPossible(String[] equations) { - // 26 个英文字母 - UF uf = new UF(26); - // 先让相等的字母形成连通分量 - for (String eq : equations) { - if (eq.charAt(1) == '=') { - char x = eq.charAt(0); - char y = eq.charAt(3); - uf.union(x - 'a', y - 'a'); - } - } - // 检查不等关系是否打破相等关系的连通性 - for (String eq : equations) { - if (eq.charAt(1) == '!') { - char x = eq.charAt(0); - char y = eq.charAt(3); - // 如果相等关系成立,就是逻辑冲突 - if (uf.connected(x - 'a', y - 'a')) - return false; - } - } - return true; -} -``` - -至此,这道判断算式合法性的问题就解决了,借助 Union-Find 算法,是不是很简单呢? - -### 三、简单总结 - -使用 Union-Find 算法,主要是如何把原问题转化成图的动态连通性问题。对于算式合法性问题,可以直接利用等价关系,对于棋盘包围问题,则是利用一个虚拟节点,营造出动态连通特性。 - -另外,将二维数组映射到一维数组,利用方向数组 `d` 来简化代码量,都是在写算法时常用的一些小技巧,如果没见过可以注意一下。 - -很多更复杂的 DFS 算法问题,都可以利用 Union-Find 算法更漂亮的解决。LeetCode 上 Union-Find 相关的问题也就二十多道,有兴趣的读者可以去做一做。 - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git a/think_like_computer/double_pointer.md b/think_like_computer/double_pointer.md new file mode 100644 index 0000000000..c1127cdba4 --- /dev/null +++ b/think_like_computer/double_pointer.md @@ -0,0 +1,191 @@ +### Summary of Double Pointer skills + +**Translator: [lriy](https://github.com/lriy)** + +**Author: [labuladong](https://github.com/labuladong)** + +I divided the double pointer technique into two categories, one is "fast and slow pointer" and the other is "left and right pointer". The former solution mainly solves the problems in the linked list, such as determining whether the linked list contains a ring; the latter mainly solves the problems in the array (or string), such as binary search. + +### First, the common algorithm of fast and slow pointers +The fast and slow pointers are usually initialized to point to the head node of the linked list. When moving forward, the fast pointer is fast first, and the slow pointer is slow. + +**1. Determine whether the linked list contains a ring.** + +This should be the most basic operation of the linked list. If you already know this trick, you can skip it. + +The characteristic of a single linked list is that each node only knows the next node, so if a pointer is used, it cannot be judged whether the linked list contains a ring. + +If the linked list does not contain a ring, then this pointer will eventually encounter a null pointer null to indicate that the linked list is over. It is good to say that you can determine that the linked list does not contain a ring. + +``` +boolean hasCycle(ListNode head) { + while (head != null) + head = head.next; + return false; +} +``` +But if the linked list contains a ring, then the pointer will end up in an endless loop, because there is no null pointer in the circular array as the tail node. + +The classic solution is to use two pointers, one running fast and one running slowly. If there is no ring, the pointer that runs fast will eventually encounter null, indicating that the linked list does not contain a ring; if it contains a ring, the fast pointer will eventually end up with a super slow pointer and meet the slow pointer, indicating that the linked list contains a ring. + +``` +boolean hasCycle(ListNode head) { + ListNode fast, slow; + fast = slow = head; + while (fast != null && fast.next != null) { + fast = fast.next.next; + slow = slow.next; + + if (fast == slow) return true; + } + return false; +} +``` +**2. Knowing that the linked list contains a ring, return to the starting position of the ring** + +![1](../pictures/double_pointer/11.png) + +This problem is not difficult at all, look directly at the code: + +``` +ListNode detectCycle(ListNode head) { + ListNode fast, slow; + fast = slow = head; + while (fast != null && fast.next != null) { + fast = fast.next.next; + slow = slow.next; + if (fast == slow) break; + } + // The above code is similar to the hasCycle function + slow = head; + while (slow != fast) { + fast = fast.next; + slow = slow.next; + } + return slow; +} +``` +It can be seen that when the "fast" and "slow" pointers meet, let any one of them point to the head node, and then let them advance at the same speed, and the node position when they meet again is the position where the ring starts. Why is this? + +For the first encounter, suppose the slow pointer "slow" moves k steps, then the fast pointer "fast" must move 2k steps, which means that "fast" moves k steps more than "slow" (The length of the ring) + +![4](../pictures/double_pointer/cyc1.png) + +Suppose the distance between the meeting point and the start point of the ring is m, then the distance between the start point of the ring and the head node "head" is k-m. + +Coincidentally, if we continue to k-m steps from the meeting point, we also reach the starting point of the loop. + +![5](../pictures/double_pointer/cyc2.png) + +So, as long as we repoint one of the fast and slow pointers to "head", and then the two pointers move at the same speed, we will meet after k-m steps. The place where we meet is the beginning of the ring. + +**3.Find the midpoint of the linked list** + +Similar to the above idea, we can also make the fast pointer advance two steps at a time, and the slow pointer advance one step at a time. When the fast pointer reaches the end of the list, the slow pointer is in the middle of the list. + +``` +while (fast != null && fast.next != null) { + fast = fast.next.next; + slow = slow.next; +} +// "slow" is in the middle +return slow; +``` +When the length of the linked list is odd, "slow" happens to stop at the midpoint; if the length is even, the final position of "slow" is right to the middle: + +![2](../pictures/double_pointer/22.png) + +An important role in finding the midpoint of a linked list is to "merge sort" the linked list. + +Recall the "merge sort" of arrays: find the midpoint index recursively divide the array into two, and finally merge the two ordered arrays. For linked lists, merging two ordered linked lists is simple, and the difficulty is dichotomy. + +But now that you have learned to find the midpoint of the linked list, you can achieve the dichotomy of the linked list. The specific content of the "merge sort" is not described in this article, you can find it online by yourself. + +**4.Find the k-th element from the bottom of the linked list** + +Our idea is still to use the fast and slow pointers, so that the fast pointer take k steps first, and then the fast and slow pointers start moving at the same speed. In this way, when the fast pointer reaches null at the end of the linked list, the position of the slow pointer is the kth penultimate linked list node (for simplicity, it is assumed that k does not exceed the length of the linked list): + +``` +ListNode slow, fast; +slow = fast = head; +while (k-- > 0) + fast = fast.next; + +while (fast != null) { + slow = slow.next; + fast = fast.next; +} +return slow; +``` + +### Second, the common algorithm of left and right pointer +The left and right pointers actually refer to two index values in the array, and are generally initialized to left = 0, right = nums.length-1. + +**1.Binary Search** + +The previous "Binary Search" has been explained in detail, only the simplest binary algorithm is written here, in order to highlight its dual pointer characteristics: + +``` +int binarySearch(int[] nums, int target) { + int left = 0; + int right = nums.length - 1; + while(left <= right) { + int mid = (right + left) / 2; + if(nums[mid] == target) + return mid; + else if (nums[mid] < target) + left = mid + 1; + else if (nums[mid] > target) + right = mid - 1; + } + return -1; +} +``` +**2.Two sum** + +Look directly at a LeetCode topic: + +![3](../pictures/double_pointer/33.png) + + +As long as the array is ordered, you should think of the two pointer technique. The solution of this problem is similar to binary search. You can adjust the size of "sum" by adjusting "left" and "right": + +``` +int[] twoSum(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left < right) { + int sum = nums[left] + nums[right]; + if (sum == target) { + //The index required for the question starts at 1 + return new int[]{left + 1, right + 1}; + } else if (sum < target) { + left++; //Make "sum" bigger + } else if (sum > target) { + right--; // Make "sum" smaller + } + } + return new int[]{-1, -1}; +} +``` +**3.Reverse the array** + +``` +void reverse(int[] nums) { + int left = 0; + int right = nums.length - 1; + while (left < right) { + // swap(nums[left], nums[right]) + int temp = nums[left]; + nums[left] = nums[right]; + nums[right] = temp; + left++; right--; + } +} +``` +**4.Sliding window algorithm** + +This may be the highest state of the double pointer technique. If you master this algorithm, you can solve a large class of substring matching problems, but the "sliding window" is slightly more complicated than the above algorithms. + +Fortunately, this type of algorithm has a frame template, and this article explains the "sliding window" algorithm template to help everyone kill a few LeetCode substring matching problems. + +Thanks for reading! diff --git a/think_like_computer/flood_fill.md b/think_like_computer/flood_fill.md new file mode 100644 index 0000000000..dfd3aca8e7 --- /dev/null +++ b/think_like_computer/flood_fill.md @@ -0,0 +1,217 @@ +# Analysis and Application of FloodFill Algorithm + +**Translator: [youyun](https://github.com/youyun)** + +**Author: [labuladong](https://github.com/labuladong)** + +What is the FloodFill algorithm? A real-life example is color filling. In the default Windows application _Paint_, using the bucket icon, we can fill the selected area with a color. + +![floodfill](../pictures/floodfill/floodfill.gif) + +There are other applications of the FloodFill algorithm. Another example would be Minesweeper. Sometimes when you click on a tile, an area will expand out. The process of expansion is implemented through the FloodFill algorithm. + +![Minesweeper](../pictures/floodfill/minesweeper.png) + +Similarly, those puzzle-matching games such as Candy Crush also use the FloodFill algorithm to remove blocks of the same color. + +![xiaoxiaole](../pictures/floodfill/xiaoxiaole.jpg) + +Now you should have some idea about the FloodFill algorithm. Let's abstract out the problems and find out what is common. + +### 1. Build Framework + +All above examples can be abstract as a 2D array. In fact, a picture is an array of pixels. We take an element as the starting point and expand till the end. + +An array can be further abstracted as a graph. Hence, the problem becomes about traversing a graph, similar to traversing an N-ary tree. A few lines of code are enough to resolve the problem. Here is the framework: + +```java +// (x, y) represents the coordinate +void fill(int x, int y) { + fill(x - 1, y); // up + fill(x + 1, y); // down + fill(x, y - 1); // left + fill(x, y + 1); // right +} +``` + +Using this framework, we can resolve all problems about traversing a 2D array. The concept is also called Depth First Search (DFS), or quaternary (4-ary) tree traversal. The root node is coordinate (x, y). Its four child nodes are at root's four directions. + +Let's take a look at [a LeetCode problem](https://leetcode.com/problems/flood-fill/). It's actually just a color fill function. + +![title](../pictures/floodfill/leetcode_en.jpg) + +In [another article](), we discussed a generic design of tree related algorithms. We can apply the concept here: + +```java +int[][] floodFill(int[][] image, + int sr, int sc, int newColor) { + + int origColor = image[sr][sc]; + fill(image, sr, sc, origColor, newColor); + return image; +} + +void fill(int[][] image, int x, int y, + int origColor, int newColor) { + // OUT: out of index + if (!inArea(image, x, y)) return; + // CLASH: meet other colors, beyond the area of origColor + if (image[x][y] != origColor) return; + image[x][y] = newColor; + + fill(image, x, y + 1, origColor, newColor); + fill(image, x, y - 1, origColor, newColor); + fill(image, x - 1, y, origColor, newColor); + fill(image, x + 1, y, origColor, newColor); +} + +boolean inArea(int[][] image, int x, int y) { + return x >= 0 && x < image.length + && y >= 0 && y < image[0].length; +} +``` + +If you can understand this block of code, you are almost there! It means that you have honed the mindset of framework. This block of code can cover 99% of cases. There is only one tiny problem to be resolved: an infinite loop will happen if `origColor` is the same as `newColor`. + +### 2. Pay Attention to Details + +Why is there infinite loop? Each coordinate needs to go through its 4 neighbors. Consequently, each coordinate will also be traversed 4 times by its 4 neighbors. __When we visit an visited coordinate, we must guarantee to identify the situation and exit. If not, we'll go into infinite loop.__ + +Why can the code exit properly when `newColr` and `origColor` are different? Let's draw an diagram of the algorithm execution: + +![ppt1](../pictures/floodfill/ppt1.PNG) + +As we can see from the diagram, `fill(1, 1)` is visited twice. Let's use `fill(1, 1)*` to represent this duplicated visit. When `fill(1, 1)*` is executed, `(1, 1)` has already been replaced with `newColor`. So `fill(1, 1)*` will return the control directly at the _CLASH_, i.e. exit as expected. + +```java +// CLASH: meet other colors, beyond the area of origColor +if (image[x][y] != origColor) return; +``` +![ppt2](../pictures/floodfill/ppt2.PNG) + +However, if `origColor` is the same as `newCOlor`, `fill(1, 1)*` will not exit at the _CLASH_. Instead, an infinite loop will start as shown below. + +![ppt3](../pictures/floodfill/ppt3.PNG) + +### 3. Handling Details + +How to avoid the case of infinite loop? The most intuitive answer is to use a boolean 2D array of the same size as image, to record whether a coordinate has been traversed or not. If visited, return immediately. + +```java + // OUT: out of index +if (!inArea(image, x, y)) return; +// CLASH: meet other colors, beyond the area of origColor +if (image[x][y] != origColor) return; +// VISITED: don't visit a coordinate twice +if (visited[x][y]) return; +visited[x][y] = true; +image[x][y] = newColor; +``` + +This is a common technique to handle graph related problems. For this particular problem, there is actually a better way: backtracking algorithm. + +Refer to the article [Backtracking Algorithm in Depth]() for details. We directly apply the backtracking algorithm framework here: + +```java +void fill(int[][] image, int x, int y, + int origColor, int newColor) { + // OUT: out of index + if (!inArea(image, x, y)) return; + // CLASH: meet other colors, beyond the area of origColor + if (image[x][y] != origColor) return; + // VISITED: visited origColor + if (image[x][y] == -1) return; + + // choose: mark a flag as visited + image[x][y] = -1; + fill(image, x, y + 1, origColor, newColor); + fill(image, x, y - 1, origColor, newColor); + fill(image, x - 1, y, origColor, newColor); + fill(image, x + 1, y, origColor, newColor); + // unchoose: replace the mark with newColor + image[x][y] = newColor; +} +``` + +This is a typical way, using a special value -1 to replace the visited 2D array, to achieve the same purpose. Because the range of color is `[0, 65535]`, -1 is special enough to differentiate with actual colors. + +### 4. Extension: Magic Wand Tool and Minesweeper + +Most picture editing softwares have the function "Magic Wand Tool". When you click a point, the application will help you choose a region of similar colors automatically. Refer to the picture below, if we want to select the eagle, we can use the Magic Wand Tool to select the blue sky, and perform inverse selection. Let's analyze the mechanism of the Magic Wand Tool. + +![CutOut](../pictures/floodfill/cutout.jpg) + +Obviously, the algorithm must be based on the FloodFill algorithm. However, there are two differences: +1. Though the background color is blue, we can't guarantee all the blue pixels are exactly the same. There could be minor differences that can be told by our eyes. But we still want to ignore these minor differences. +2. FloodFill is to fill regions. Magic Wand Tool is more about filling the edges. + +It's easy to resolve the first problem by setting a `threshold`. All colors within the threshold from the `origColor` can be recognized as `origColor`. + +```java +if (Math.abs(image[x][y] - origColor) > threshold) + return; +``` + +As for the second problem, let's first define the problem clearly: _"do not color all `origColor` coordinates in the region; only care about the edges."_. Next, let's analyze how to only color edges. i.e. How to find out the coordinates at the edges? What special properties do coordinates at the edges hold? + +![ppt4](../pictures/floodfill/ppt4.PNG) + +From the diagram above, we can see that for all coordinates at the edges, there is at least one direction that is not `origColor`. For all inner coordinates, all 4 directions are `origColor`. This is the key to the solution. Using the same framework, using `visited` array to represent traversed coordinates: + +```java +int fill(int[][] image, int x, int y, + int origColor, int newColor) { + // OUT: out of index + if (!inArea(image, x, y)) return 0; + // VISITED: visited origColor + if (visited[x][y]) return 1; + // CLASH: meet other colors, beyond the area of origColor + if (image[x][y] != origColor) return 0; + + visited[x][y] = true; + + int surround = + fill(image, x - 1, y, origColor, newColor) + + fill(image, x + 1, y, origColor, newColor) + + fill(image, x, y - 1, origColor, newColor) + + fill(image, x, y + 1, origColor, newColor); + + if (surround < 4) + image[x][y] = newColor; + + return 1; +} +``` + +In this way, all inner coordinates will have `surround` equal to 4 after traversing the four directions; all edge coordinates will be either OUT or CLASH, resulting `surround` less than 4. If you are still not clear, let's only look at the framework's logic flow: + +```java +int fill(int[][] image, int x, int y, + int origColor, int newColor) { + // OUT: out of index + if (!inArea(image, x, y)) return 0; + // VISITED: visited origColor + if (visited[x][y]) return 1; + // CLASH: meet other colors, beyond the area of origColor + if (image[x][y] != origColor) return 0; + // UNKNOWN: unvisited area that is origColor + if (image[x][y] == origColor) { + // ... + return 1; + } +} +``` + +These 4 `if`s cover all possible scenarios of (x, y). The value of `surround` is the sum of the return values of the 4 recursive functions. And each recursive function will fall into one of the 4 scenarios. You should be much clearer now after looking at this framework. + +This implementation colors all edge coordinates only for the `origColor` region, which is what the Magic Wand TOol does. + +Pay attention to 2 details in this algorithm: +1. We must use `visited` to record traversed coordinates instead of backtracking algorithm. +2. The order of the `if` clauses can't be modified. (Why?) + +Similarly, for Minesweeper, when we use the FloodFill algorithm to expand empty areas, we also need to show the number of mines nearby. How to implement it? Following the same idea, return `true` when we meet mine. Thus, `surround` will store the number of mines nearby. Of course, in Minesweeper, there are 8 directions instead of 4, including diagonals. + +![](../pictures/floodfill/ppt5.PNG) + +We've discussed the design and framework of the FloodFill algorithm. __All searching problems in a 2D array can be fit into this framework.__ diff --git a/think_like_computer/prefix_sum.md b/think_like_computer/prefix_sum.md new file mode 100644 index 0000000000..6c8478c5d4 --- /dev/null +++ b/think_like_computer/prefix_sum.md @@ -0,0 +1,132 @@ +# Prefix Sum + +**Translator: [youyun](https://github.com/youyun)** + +**Author: [labuladong](https://github.com/labuladong)** + +Let's talk about a simple but interesting algorithm problem today. Find the number of subarrays which sums to k. + +![](../pictures/prefix_sum/title_en.jpg) + +The most intuitive way is using brute force - find all the subarrays, sum up and compare with k. + +The tricky part is, __how to find the sum of a subarray fast?__ For example, you're given an array `nums`, and asked to implement API `sum(i, j)` which returns the sum of `nums[i..j]`. Furthermore, the API will be very frequently used. How do you plan to implement this API? + +Due to the high frequency, it is very inefficient to traverse through `nums[i..j]` each time. Is there a quick method which find the sum in time complexity of O(1)? There is a technique called __Prefix Sum__. + +### 1. What is Prefix Sum + +The idea of Prefix SUm goes like this: for a given array `nums`, create another array to store the sum of prefix for pre-processing: + +```java +int n = nums.length; +// array of prefix sum +int[] preSum = new int[n + 1]; +preSum[0] = 0; +for (int i = 0; i < n; i++) + preSum[i + 1] = preSum[i] + nums[i]; +``` + +![](../pictures/prefix_sum/1.jpg) + +The meaning of `preSum` is easy to understand. `preSum[i]` is the sum of `nums[0..i-1]`. If we want to calculate the sum of `nums[i..j]`, we just need to perform `preSum[j+1] - preSum[i]` instead of traversing the whole subarray. + +Coming back to the original problem. If we want to find the number of subarrays which sums to k respectively, it's straightforward to implement using Prefix Sum technique: + +```java +int subarraySum(int[] nums, int k) { + int n = nums.length; + // initialize prefix sum + int[] sum = new int[n + 1]; + sum[0] = 0; + for (int i = 0; i < n; i++) + sum[i + 1] = sum[i] + nums[i]; + + int ans = 0; + // loop through all subarrays by brute force + for (int i = 1; i <= n; i++) + for (int j = 0; j < i; j++) + // sum of nums[j..i-1] + if (sum[i] - sum[j] == k) + ans++; + + return ans; +} +``` + +The time complexity of this solution is O(N^2), while the space complexity is O(N). This is not the optimal solution yet. However, we can apply some cool techniques to reduce the time complexity further, after understanding how Prefix Sum and arrays can work together through this solution. + +### 2. Optimized Solution + +The solution in part 1 has nested `for` loop: + +```java +for (int i = 1; i <= n; i++) + for (int j = 0; j < i; j++) + if (sum[i] - sum[j] == k) + ans++; +``` + +What does the inner `for` loop actually do? Well, it is used __to calculate how many `j` can make the difference of `sum[i]` and `sum[j]` to be k.__ Whenever we find such `j`, we'll increment the result by 1. + +We can reorganize the condition of `if` clause: + +```java +if (sum[j] == sum[i] - k) + ans++; +``` + +The idea of optimization is, __to record down how many `sum[j]` equal to `sum[i] - k` such that we can update the result directly instead of having inner loop.__ We can utilize hash table to record both prefix sums and the frequency of each prefix sum. + +```java +int subarraySum(int[] nums, int k) { + int n = nums.length; + // map:prefix sum -> frequency + 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]; + // this is the prefix sum we want to find nums[0..j] + int sum0_j = sum0_i - k; + // if it exists, we'll just update the result + if (preSum.containsKey(sum0_j)) + ans += preSum.get(sum0_j); + // record the prefix sum nums[0..i] and its frequency + preSum.put(sum0_i, + preSum.getOrDefault(sum0_i, 0) + 1); + } + return ans; +} +``` + +In the following case, we just need prefix sum of 8 to find subarrays with sum of k. By brute force solution in part 1, we need to traverse arrays to find how many 8 there are. Using the optimal solution, we can directly get the answer through hash table. + +![](../pictures/prefix_sum/2.jpg) + +This is the optimal solution with time complexity of O(N). + +### 3. Summary + +Prefix Sum is not hard, yet very useful, especially in dealing with differences of array intervals. + +For example, if we were asked to calculate the percentage of each score interval among all students in the class, we can apply Prefix Sum technique: + +```java +int[] scores; // to store all students' scores +// the full score is 150 points +int[] count = new int[150 + 1] +// to record how many students at each score +for (int score : scores) + count[score]++ +// construct prefix sum +for (int i = 1; i < count.length; i++) + count[i] = count[i] + count[i-1]; +``` + +Afterwards, for any given score interval, we can find how many students fall in this interval by calculating the difference of prefix sums quickly. Hence, the percentage will be calculated easily. + +However, for more complex problems, simple Prefix Sum technique is not enough. Even the original question we discussed in this article requires one step further to optimize. We used hash table to eliminate an unnecessary loop. We can see that if we want to achieve the optimal solution, it is indeed important to understand a problem thoroughly and analyze into details. diff --git a/think_like_computer/string_multiplication.md b/think_like_computer/string_multiplication.md new file mode 100644 index 0000000000..10852de243 --- /dev/null +++ b/think_like_computer/string_multiplication.md @@ -0,0 +1,76 @@ +# String Multiplication + +**Translator: [youyun](https://github.com/youyun)** + +**Author: [labuladong](https://github.com/labuladong)** + +For relatively small numbers, you can calculate directly using the operators provided by a programming language. When the numbers become very big, the default data types might overflow. An alternative way is to use string to represent the numbers, perform the multiplication in the primary school way, and produce the result as string as well. Take [this question](https://leetcode.com/problems/multiply-strings/) as an example. + +![](../pictures/string_multiplication/title_en.jpg) + +Note that both `num1` and `num2` can be very long. We can't directly calculate by transforming them to integers. We can learn from the process multiplying by hand. + +For example, when we multiply `123 × 45` by hand, the process is shown in the following diagram: + +![](../pictures/string_multiplication/1.jpg) + +Firstly, calculate `123 × 5`. Then calculate `123 × 4`. In the end, add them together by shifting one digit. We learned this method in primary school. Can we __generalize the steps in this process__, such that a computer can understand? + +This simple process actually involves a lot of knowledge - carry of multiplication, carry of addition, and adding numbers by shifting digits. Another not so obvious issue is the number of digits of the final result. When two two-digit numbers multiply, the result can be either four-digit or three-digit. How to generalize this? Without the mindset of a computer, we can't even automate simple problems. This is the beauty of algorithms. + +Well, this process is still too high-level. Let's try something at a lower level. The processes of `123 × 5` and `123 × 4` can be further broken into parts and add together: + +![](../pictures/string_multiplication/2.jpg) + +`123` is pretty small. If the number is large, we can't get the product directly. An array can help to store the result of addition: + +![](../pictures/string_multiplication/3.jpg) + +Here is the rough process of calculation. __Two pointers `i, j` moves at `num1` and `num2` to multiply, adding the products to the correct positions of `res`__: + +![](../pictures/string_multiplication/4.gif) + +There is a key question now. How to add products to the correct positions of `res`? In other words, how to use `i, j` to calculate the corresponding indices in `res`? + +With careful observation, __the product of `num1[i]` and `num2[j]` corresponds to `res[i+j]` and `res[i+j+1]`__. + +![](../pictures/string_multiplication/6.jpg) + +If we understand the above, we should be able to translate the process into code: + +```java +string multiply(string num1, string num2) { + int m = num1.size(), n = num2.size(); + // the max number of digits in result is m + n + vector res(m + n, 0); + // multiply from the rightmost digit + for (int i = m - 1; i >= 0; i--) + for (int j = n - 1; j >= 0; j--) { + int mul = (num1[i]-'0') * (num2[j]-'0'); + // the corresponding index of product in res + int p1 = i + j, p2 = i + j + 1; + // add to res + int sum = mul + res[p2]; + res[p2] = sum % 10; + res[p1] += sum / 10; + } + // the result may have prefix of 0 (which is unused) + int i = 0; + while (i < res.size() && res[i] == 0) + i++; + // transform the result into string + string str; + for (; i < res.size(); i++) + str.push_back('0' + res[i]); + + return str.size() == 0 ? "0" : str; +} +``` + +We have just completed the string multiplication. + +__In summary__, some of our common ways of think may be hard to achieve by computer. For instance, the process of our calculation is not that complicated. But it is not easy to translate this process into code. Our algorithm needs to simplify the calculation process, achieve the result by adding while multiplying at the same time. + +People usually say that we need to think out of the box, be creative, and be different. But systematic thinking can be a good thing. It can improve the efficiency and reduce the error rate. Algorithms are based on systematic thinking, and can help us to resolve complex problems. + +Maybe algorithms are a kind of __mindset to find a systematic thinking__. Hope this article helps. diff --git "a/think_like_computer/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" "b/think_like_computer/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" deleted file mode 100644 index 9229fbc0d3..0000000000 --- "a/think_like_computer/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" +++ /dev/null @@ -1,158 +0,0 @@ -# twoSum问题的核心思想 - -Two Sum 系列问题在 LeetCode 上有好几道,这篇文章就挑出有代表性的几道,介绍一下这种问题怎么解决。 - -### TwoSum I - -这个问题的**最基本形式**是这样:给你一个数组和一个整数 `target`,可以保证数组中**存在**两个数的和为 `target`,请你返回这两个数的索引。 - -比如输入 `nums = [3,1,3,6], target = 6`,算法应该返回数组 `[0,2]`,因为 3 + 3 = 6。 - -这个问题如何解决呢?首先最简单粗暴的办法当然是穷举了: - -```java -int[] twoSum(int[] nums, int target) { - - for (int i = 0; i < nums.length; i++) - for (int j = i + 1; j < nums.length; j++) - if (nums[j] == target - nums[i]) - return new int[] { i, j }; - - // 不存在这么两个数 - return new int[] {-1, -1}; -} -``` - -这个解法非常直接,时间复杂度 O(N^2),空间复杂度 O(1)。 - -可以通过一个哈希表减少时间复杂度: - -```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}; -} -``` - -这样,由于哈希表的查询时间为 O(1),算法的时间复杂度降低到 O(N),但是需要 O(N) 的空间复杂度来存储哈希表。不过综合来看,是要比暴力解法高效的。 - -**我觉得 Two Sum 系列问题就是想教我们如何使用哈希表处理问题**。我们接着往后看。 - -### TwoSum II - -这里我们稍微修改一下上面的问题。我们设计一个类,拥有两个 API: - -```java -class TwoSum { - // 向数据结构中添加一个数 number - public void add(int number); - // 寻找当前数据结构中是否存在两个数的和为 value - public boolean find(int value); -} -``` - -如何实现这两个 API 呢,我们可以仿照上一道题目,使用一个哈希表辅助 `find` 方法: - -```java -class TwoSum { - Map 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; - } -} -``` - -进行 `find` 的时候有两种情况,举个例子: - -情况一:`add` 了 `[3,3,2,5]` 之后,执行 `find(6)`,由于 3 出现了两次,3 + 3 = 6,所以返回 true。 - -情况二:`add` 了 `[3,3,2,5]` 之后,执行 `find(7)`,那么 `key` 为 2,`other` 为 5 时算法可以返回 true。 - -除了上述两种情况外,`find` 只能返回 false 了。 - -对于这个解法的时间复杂度呢,`add` 方法是 O(1),`find` 方法是 O(N),空间复杂度为 O(N),和上一道题目比较类似。 - -**但是对于 API 的设计,是需要考虑现实情况的**。比如说,我们设计的这个类,使用 `find` 方法非常频繁,那么每次都要 O(N) 的时间,岂不是很浪费费时间吗?对于这种情况,我们是否可以做些优化呢? - -是的,对于频繁使用 `find` 方法的场景,我们可以进行优化。我们可以参考上一道题目的暴力解法,借助**哈希集合**来针对性优化 `find` 方法: - -```java -class TwoSum { - Set sum = new HashSet<>(); - List nums = new ArrayList<>(); - - public void add(int number) { - // 记录所有可能组成的和 - for (int n : nums) - sum.add(n + number); - nums.add(number); - } - - public boolean find(int value) { - return sum.contains(value); - } -} -``` - -这样 `sum` 中就储存了所有加入数字可能组成的和,每次 `find` 只要花费 O(1) 的时间在集合中判断一下是否存在就行了,显然非常适合频繁使用 `find` 的场景。 - -### 三、总结 - -对于 TwoSum 问题,一个难点就是给的数组**无序**。对于一个无序的数组,我们似乎什么技巧也没有,只能暴力穷举所有可能。 - -**一般情况下,我们会首先把数组排序再考虑双指针技巧**。TwoSum 启发我们,HashMap 或者 HashSet 也可以帮助我们处理无序数组相关的简单问题。 - -另外,设计的核心在于权衡,利用不同的数据结构,可以得到一些针对性的加强。 - -最后,如果 TwoSum I 中给的数组是有序的,应该如何编写算法呢?答案很简单,前文「双指针技巧汇总」写过: - -```java -int[] twoSum(int[] nums, int target) { - int left = 0, right = nums.length - 1; - while (left < right) { - int sum = nums[left] + nums[right]; - if (sum == target) { - return new int[]{left, right}; - } else if (sum < target) { - left++; // 让 sum 大一点 - } else if (sum > target) { - right--; // 让 sum 小一点 - } - } - // 不存在这样两个数 - return new int[]{-1, -1}; -} -``` - - - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git a/think_like_computer/why_i_recommend_algs4.md b/think_like_computer/why_i_recommend_algs4.md new file mode 100644 index 0000000000..aa6ec7b9a0 --- /dev/null +++ b/think_like_computer/why_i_recommend_algs4.md @@ -0,0 +1,77 @@ +# Why I Recommend _Algorithms, 4th Edition_ + +**Translator: [natsunoyoru97](https://github.com/natsunoyoru97)** + +**Author: [labuladong](https://github.com/labuladong)** + +My previous articles are mainly about hardcore algorithms, but I'll no more talk about them today. I have highly recommended _Algorithms, 4th Edition_ (alias algs4) before in my previous articles but without explanation in details, so I'll introduce it to you. + +I prefer to combine with applications rather than to throw out a pile of bibliography. I'll show you something fun and useful so you could learn something no matter you will read the book or not. + +**_Algorithms, the 4th Edition_ IS FRIENDLY WITH NEWBIES**. There are so many readers asking questions like can they read algs4 with only C programming basics, and what is the best programming language to learn algorithms. + + +Algorithms are patterns of thinking and it is nothing about what programming language you use. My codes in the articles are not in only one programming language, and it is more about by what programming language is easier for you to understand. Plus, you can get a digital copy (say, a PDF file) and then you will know it is suitable for you or not. + +algs4 is thick but the first chapter is about Java basics, practices and mathematical proofs are also take up lots of pages. All the things left are basic knowledge and answers for things hard to understand, which are of great value. If you write up the codes of basic knowledge, you will be good in algorithms. + +I think this book is highly rated because of its in-detail explanation and combination between algorithms and applications in life. Not only do you know how to construct algorithms, but also you know how you can apply them to solve other problems in real life. Then I'll introduce two simple applications of graph algorithms: + + +### 1. Applications of Bipartite Graph + +The first example is **bipartite Graph**. In brief, the bipartite graph is a special kind of graph: you can paint all nodes with two colors, and the colors are different between ANY two nodes in one edge. + +![](../pictures/algo4/1.jpg) + +As we know what is a bipartite graph, what practical problems can it deal with? **In algorithms, it is usual to check a graph is a bipartite graph or not**. For example, here is a practice in LeetCode: + +![](../pictures/algo4/title.png) + +If we regard every person as a node and use the edges represent if two persons hate with each other, we can make a graph. Thus, according to the definition of the bipartite graph mentioned before, if the graph is bipartite, these persons can be split into two groups. + +This is one application of algorithms in determining a bipartite graph, and bipartite graph can do more of this. bipartite graph, as a kind of data structure, also has its special application. + +For example, we need a kind of data structure to store the relations between movies and actors: a movie has many actors, and an actor is possible to star in many movies. What kind of data structure do you apply to store the relation? + +As what we do is to store the mapping, the most simple way is to apply a hash table, so we could use the `HashMap>` to store the mapping from movies to the actor list. Given the name of a movie, we will get all the actors starred in this movie quickly. + +But what do we do if we would like to get all the movies an actor stars in given his or her name? We need to apply inverted index, make some modification of the hash table, to build another hash table using the actors as the key and movie list as the value. + +For this example, we can apply the bipartite graph instead of the hash table. Relation between movies and actors is like a bipartite graph: we regard movies and actors as nodes in the graph, relation of acting the role as edges, thus there is only connections between actors and movies, and there is no case that an actor node connects to another actor node or a movie node connects to another movie node. + +By the definition of the bipartite graph, if we paint actor nodes and movie nodes, there must be a bipartite graph: + +![](../pictures/algo4/2.jpg) + +If the graph is constructed, there is no need to do an inverted index. One actor node only connects to a movie node, and a movie node only connects to an actor node. + +algs4 also mentioned some other fun applications, such as degrees of separation in the social network (maybe you have heard about Six Degrees of Separation), it is a problem of applying BFS searching to find the shortest path, I just skip the code here. + +### 2. Arbitrage with Bellman-Ford Algorithm + +Given the exchange rate from a currency A to another currency B is 10, which means 1 unit of currency A can exchange 10 units of currency B. If we regard every currency as a node of the graph and the exchange rate between two currencies is weighted directed edge, the whole foreign exchange rate market is a complete weighted directed graph. + +Once we abstract scenarios in real life to a graph, it is possible to solve some problems using algorithms. For example, such a situation may exist in the graph: + +![](../pictures/algo4/3.jpg) + +The weighted directed edges in the graph represent the exchange rate, we can find that if we exchange 100 units of currency A to currency B, continue exchanging to currency C, and exchange back to currency A, we could get 100×0.9×0.8×1.4 = 100.8 units of currency A! Provided the amount in a deal is larger, we could get a great amount of money. Such behavior getting the most bang for the buck is so-called arbitrage. + +There are lots of restrictions in the reality, the market is also changing rapidly, but the profit of arbitrage is high. The key is how to find such chances **RAPIDLY**. + +With help of the graph ADT, we found that the chance of arbitrage is a cycle, and the multiples of all weights in the cycle are above 1, we could make the most bang for the buck if we make the deal in the cycle. + +There is a typical graph algorithm called **the Bellman-Ford algorithm**, which can be applied **to find cycles with negative weights**. To deal with the problem of arbitrage, we can substitute the weights of all edges w to -ln(w), then the target to find the cycle in which the multiples of weights is above 1 becomes to find the cycle in which the sum of the addition of weights, so we can apply the Bellman-Ford algorithm to find cycle including negative weights in O(EV) time, thus to find the chance of arbitrage. + +The explanation of contents of algs4 is over here. You can read the relevant content in algs4. + +### 3. The Final Word + +First things first, I have mentioned we can skip the mathematical proofs and practices behind the chapters. Someone may ask: Are these practices and proofs NOT IMPORTANT? + +I would like to say, they ARE not important at least for most people. In my opinion, it is better to learn with goals. For most people, they learn algorithms just out of reviewing the computer basics and coping with questions in interviews. **If it is your goal**, what you have to do is to learn some basic data structures and typical algorithms, to make sense of their time complexity and practice. Why you have to struggle with practices and proofs? + +This is the reason why I NEVER recommend you to read CLRS (alias _Introduction to Algorithms_). If someone recommends this book for you, it is only out of two reasons: he/she is a big cheese or he/she is pretending to be a big cheese. There are tons of mathematical proofs and many ADTs mentioned are rarely used, which you can only make it a reference. Just forgive yourself and stop to learn that useless stuff. + +Plus, it is more important to master the essence rather than to scan through pages. Spending your time on reading algs4 and make through of most chapters (things are rather difficult in the last), reading the articles in this repository. If you did all of these, it is enough. Don't be torn on trivial things. diff --git "a/think_like_computer/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" "b/think_like_computer/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" deleted file mode 100644 index 49ea23126b..0000000000 --- "a/think_like_computer/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" +++ /dev/null @@ -1,76 +0,0 @@ -# 为什么我推荐《算法4》 - -咱们的公众号有很多硬核的算法文章,今天就聊点轻松的,就具体聊聊我非常“鼓吹”的《算法4》。这本书我在之前的文章多次推荐过,但是没有具体的介绍,今天就来正式介绍一下。。 - -我的推荐不会直接甩一大堆书目,而是会联系实际生活,讲一些书中有趣有用的知识,无论你最后会不会去看这本书,本文都会给你带来一些收获。 - -**首先这本书是适合初学者的**。总是有很多读者问,我只会 C 语言,能不能看《算法4》?学算法最好用什么语言?诸如此类的问题。 - -经常看咱们公众号的读者应该体会到了,算法其实是一种思维模式,和你用什么语言没啥关系。我们的文章也不会固定用某一种语言,而是什么语言写出来容易理解就用什么语言。再退一步说,到底适不适合你,网上找个 PDF 亲自看一下不就知道了? - -《算法4》看起来挺厚的,但是前面几十页是教你 Java 的;每章后面还有习题,占了不少页数;每章还有一些数学证明,这些都可以忽略。这样算下来,剩下的就是基础知识和疑难解答之类的内容,含金量很高,把这些基础知识动手实践一遍,真的就可以达到不错的水平了。 - -我觉得这本书之所以能有这么高的评分,一个是因为讲解详细,还有大量配图,另一个原因就是书中把一些算法和现实生活中的使用场景联系起来,你不仅知道某个算法怎么实现,也知道它大概能运用到什么场景,下面我就来介绍两个图算法的简单应用。 - -### 一、二分图的应用 - -我想举的第一个例子是**二分图**。简单来说,二分图就是一幅拥有特殊性质的图:能够用两种颜色为所有顶点着色,使得任何一条边的两个顶点颜色不同。 - -![](../pictures/algo4/1.jpg) - -明白了二分图是什么,能解决什么实际问题呢?**算法方面,常见的操作是如何判定一幅图是不是二分图**。比如说下面这道 LeetCode 题目: - -![](../pictures/algo4/title.png) - -你想想,如果我们把每个人视为一个顶点,边代表讨厌;相互讨厌的两个人之间连接一条边,就可以形成一幅图。那么根据刚才二分图的定义,如果这幅图是一幅二分图,就说明这些人可以被分为两组,否则的话就不行。 - -这是判定二分图算法的一个应用,**其实二分图在数据结构方面也有一些不错的特性**。 - -比如说我们需要一种数据结构来储存电影和演员之间的关系:某一部电影肯定是由多位演员出演的,且某一位演员可能会出演多部电影。你使用什么数据结构来存储这种关系呢? - -既然是存储映射关系,最简单的不就是使用哈希表嘛,我们可以使用一个 `HashMap>` 来存储电影到演员列表的映射,如果给一部电影的名字,就能快速得到出演该电影的演员。 - -但是如果给出一个演员的名字,我们想快速得到该演员演出的所有电影,怎么办呢?这就需要「反向索引」,对之前的哈希表进行一些操作,新建另一个哈希表,把演员作为键,把电影列表作为值。 - -对于上面这个例子,可以使用二分图来取代哈希表。电影和演员是具有二分图性质的:如果把电影和演员视为图中的顶点,出演关系作为边,那么与电影顶点相连的一定是演员,与演员相邻的一定是电影,不存在演员和演员相连,电影和电影相连的情况。 - -回顾二分图的定义,如果对演员和电影顶点着色,肯定就是一幅二分图: - -![](../pictures/algo4/2.jpg) - -如果这幅图构建完成,就不需要反向索引,对于演员顶点,其直接连接的顶点就是他出演的电影,对于电影顶点,其直接连接的顶点就是出演演员。 - -当然,对于这个问题,书中还提到了一些其他有趣的玩法,比如说社交网络中「间隔度数」的计算(六度空间理论应该听说过)等等,其实就是一个 BFS 广度优先搜索寻找最短路径的问题,具体代码实现这里就不展开了。 - -### 二、套汇的算法 - -如果我们说货币 A 到货币 B 的汇率是 10,意思就是 1 单位的货币 A 可以换 10 单位货币 B。如果我们把每种货币视为一幅图的顶点,货币之间的汇率视为加权有向边,那么整个汇率市场就是一幅「完全加权有向图」。 - -一旦把现实生活中的情景抽象成图,就有可能运用算法解决一些问题。比如说图中可能存在下面的情况: - -![](../pictures/algo4/3.jpg) - -图中的加权有向边代表汇率,我们可以发现如果把 100 单位的货币 A 换成 B,再换成 C,最后换回 A,就可以得到 100×0.9×0.8×1.4 = 100.8 单位的 A!如果交易的金额大一些的话,赚的钱是很可观的,这种空手套白狼的操作就是套汇。 - -现实中交易会有种种限制,而且市场瞬息万变,但是套汇的利润还是很高的,关键就在于如何**快速**找到这种套汇机会呢? - -借助图的抽象,我们发现套汇机会其实就是一个环,且这个环上的权重之积大于 1,只要在顺着这个环交易一圈就能空手套白狼。 - -图论中有一个经典算法叫做 **Bellman-Ford 算法,可以用于寻找负权重环**。对于我们说的套汇问题,可以先把所有边的权重 w 替换成 -ln(w),这样「寻找权重乘积大于 1 的环」就转化成了「寻找权重和小于 0 的环」,就可以使用 Bellman-Ford 算法在 O(EV) 的时间内寻找负权重环,也就是寻找套汇机会。 - -《算法4》就介绍到这里,关于上面两个例子的具体内容,可以自己去看书,公众号后台回复关键词「算法4」就有 PDF。 - - -### 三、最后说几句 - -首先,前文说对于数学证明、章后习题可以忽略,可能有人要抬杠了:难道习题和数学证明不重要吗? - -那我想说,就是不重要,起码对大多数人来说不重要。我觉得吧,学习就要带着目的性去学,大部分人学算法不就是巩固计算机知识,对付面试题目吗?**如果是这个目的**,那就学些基本的数据结构和经典算法,明白它们的时间复杂度,然后去刷题就好了,何必和习题、证明过不去? - -这也是我从来不推荐《算法导论》这本书的原因。如果有人给你推荐这本书,只可能有两个原因,要么他是真大佬,要么他在装大佬。《算法导论》中充斥大量数学证明,而且很多数据结构是很少用到的,顶多当个字典用。你说你学了那些有啥用呢,饶过自己呗。 - -另外,读书在精不在多。你花时间《算法4》过个大半(最后小半部分有点困难),同时刷点题,看看咱们的公众号文章,算法这块真就够了,别对细节问题太较真。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**,公众号后台回复关键词「算法4」可以获得 PDF 下载: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/think_like_computer/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" "b/think_like_computer/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" deleted file mode 100644 index 678d5de65a..0000000000 --- "a/think_like_computer/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" +++ /dev/null @@ -1,313 +0,0 @@ -# 二分查找详解 - -先给大家讲个笑话乐呵一下: - -有一天阿东到图书馆借了 N 本书,出图书馆的时候,警报响了,于是保安把阿东拦下,要检查一下哪本书没有登记出借。阿东正准备把每一本书在报警器下过一下,以找出引发警报的书,但是保安露出不屑的眼神:你连二分查找都不会吗?于是保安把书分成两堆,让第一堆过一下报警器,报警器响;于是再把这堆书分成两堆…… 最终,检测了 logN 次之后,保安成功的找到了那本引起警报的书,露出了得意和嘲讽的笑容。于是阿东背着剩下的书走了。 - -从此,图书馆丢了 N - 1 本书。 - -二分查找真的很简单吗?并不简单。看看 Knuth 大佬(发明 KMP 算法的那位)怎么说的: - -Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky..、 - -这句话可以这样理解:**思路很简单,细节是魔鬼。** - -本文就来探究几个最常用的二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界。而且,我们就是要深入细节,比如不等号是否应该带等号,mid 是否应该加一等等。分析这些细节的差异以及出现这些差异的原因,保证你能灵活准确地写出正确的二分查找算法。 - -### 零、二分查找框架 - -```java -int binarySearch(int[] nums, int target) { - int left = 0, right = ...; - - while(...) { - int mid = (right + left) / 2; - if (nums[mid] == target) { - ... - } else if (nums[mid] < target) { - left = ... - } else if (nums[mid] > target) { - right = ... - } - } - return ...; -} -``` - -**分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节**。本文都会使用 else if,旨在讲清楚,读者理解后可自行简化。 - -其中 `...` 标记的部分,就是可能出现细节问题的地方,当你见到一个二分查找的代码时,首先注意这几个地方。后文用实例分析这些地方能有什么样的变化。 - -另外声明一下,计算 mid 时需要技巧防止溢出,可以「参见前文」,本文暂时忽略这个问题。 - - -### 一、寻找一个数(基本的二分搜索) - -这个场景是最简单的,肯能也是大家最熟悉的,即搜索一个数,如果存在,返回其索引,否则返回 -1。 - -```java -int binarySearch(int[] nums, int target) { - int left = 0; - int right = nums.length - 1; // 注意 - - while(left <= right) { - int mid = (right + left) / 2; - if(nums[mid] == target) - return mid; - else if (nums[mid] < target) - left = mid + 1; // 注意 - else if (nums[mid] > target) - right = mid - 1; // 注意 - } - return -1; -} -``` - -1、为什么 while 循环的条件中是 <=,而不是 < ? - -答:因为初始化 right 的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length。 - -这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。 - -我们这个算法中使用的是前者 [left, right] 两端都闭的区间。**这个区间其实就是每次进行搜索的区间**。 - -什么时候应该停止搜索呢?当然,找到了目标值的时候可以终止: - -```java - if(nums[mid] == target) - return mid; -``` - -但如果没找到,就需要 while 循环终止,然后返回 -1。那 while 循环什么时候应该终止?**搜索区间为空的时候应该终止**,意味着你没得找了,就等于没找到嘛。 - -`while(left <= right)` 的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],可见**这时候区间为空**,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。 - -`while(left < right)` 的终止条件是 left == right,写成区间的形式就是 [left, right],或者带个具体的数字进去 [2, 2],**这时候区间非空**,还有一个数 2,但此时 while 循环终止了。也就是说这区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。 - -当然,如果你非要用 while(left < right) 也可以,我们已经知道了出错的原因,就打个补丁好了: - -```java - //... - while(left < right) { - // ... - } - return nums[left] == target ? left : -1; -``` - - -2、为什么 left = mid + 1,right = mid - 1?我看有的代码是 right = mid 或者 left = mid,没有这些加加减减,到底怎么回事,怎么判断? - -答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。 - -刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 [left, right]。那么当我们发现索引 mid 不是要找的 target 时,如何确定下一步的搜索区间呢? - -当然是 [left, mid - 1] 或者 [mid + 1, right] 对不对?因为 mid 已经搜索过,应该从搜索区间中去除。 - -3、此算法有什么缺陷? - -答:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。 - -比如说给你有序数组 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。 - -这样的需求很常见。你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。 - -我们后续的算法就来讨论这两种二分查找的算法。 - - -### 二、寻找左侧边界的二分搜索 - -直接看代码,其中的标记是需要注意的细节: - -```java -int left_bound(int[] nums, int target) { - if (nums.length == 0) return -1; - int left = 0; - int right = nums.length; // 注意 - - while (left < right) { // 注意 - int mid = (left + right) / 2; - if (nums[mid] == target) { - right = mid; - } else if (nums[mid] < target) { - left = mid + 1; - } else if (nums[mid] > target) { - right = mid; // 注意 - } - } - return left; -} -``` - -1、为什么 while(left < right) 而不是 <= ? - -答:用相同的方法分析,因为 right = nums.length 而不是 nums.length - 1 。因此每次循环的「搜索区间」是 [left, right) 左闭右开。 - -while(left < right) 终止的条件是 left == right,此时搜索区间 [left, left) 为空,所以可以正确终止。 - -2、为什么没有返回 -1 的操作?如果 nums 中不存在 target 这个值,怎么办? - -答:因为要一步一步来,先理解一下这个「左侧边界」有什么特殊含义: - -![](../pictures/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/binarySearch1.png) - -对于这个数组,算法会返回 1。这个 1 的含义可以这样解读:nums 中小于 2 的元素有 1 个。 - -比如对于有序数组 nums = [2,3,5,7], target = 1,算法会返回 0,含义是:nums 中小于 1 的元素有 0 个。 - -再比如说 nums 不变,target = 8,算法会返回 4,含义是:nums 中小于 8 的元素有 4 个。 - -综上可以看出,函数的返回值(即 left 变量的值)取值区间是闭区间 [0, nums.length],所以我们简单添加两行代码就能在正确的时候 return -1: - -```java -while (left < right) { - //... -} -// target 比所有数都大 -if (left == nums.length) return -1; -// 类似之前算法的处理方式 -return nums[left] == target ? left : -1; -``` - -1、为什么 left = mid + 1,right = mid ?和之前的算法不一样? - -答:这个很好解释,因为我们的「搜索区间」是 [left, right) 左闭右开,所以当 nums[mid] 被检测之后,下一步的搜索区间应该去掉 mid 分割成两个区间,即 [left, mid) 或 [mid + 1, right)。 - -4、为什么该算法能够搜索左侧边界? - -答:关键在于对于 nums[mid] == target 这种情况的处理: - -```java - if (nums[mid] == target) - right = mid; -``` - -可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。 - -5、为什么返回 left 而不是 right? - -答:都是一样的,因为 while 终止的条件是 left == right。 - - -### 三、寻找右侧边界的二分查找 - -寻找右侧边界和寻找左侧边界的代码差不多,只有两处不同,已标注: - -```java -int right_bound(int[] nums, int target) { - if (nums.length == 0) return -1; - int left = 0, right = nums.length; - - while (left < right) { - int mid = (left + right) / 2; - if (nums[mid] == target) { - left = mid + 1; // 注意 - } else if (nums[mid] < target) { - left = mid + 1; - } else if (nums[mid] > target) { - right = mid; - } - } - return left - 1; // 注意 -} -``` - -1、为什么这个算法能够找到右侧边界? - -答:类似地,关键点还是这里: - -```java - if (nums[mid] == target) { - left = mid + 1; -``` - -当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的下界 left,使得区间不断向右收缩,达到锁定右侧边界的目的。 - -2、为什么最后返回 left - 1 而不像左侧边界的函数,返回 left?而且我觉得这里既然是搜索右侧边界,应该返回 right 才对。 - -答:首先,while 循环的终止条件是 left == right,所以 left 和 right 是一样的,你非要体现右侧的特点,返回 right - 1 好了。 - -至于为什么要减一,这是搜索右侧边界的一个特殊点,关键在这个条件判断: - -```java - if (nums[mid] == target) { - left = mid + 1; - // 这样想: mid = left - 1 -``` - -![](../pictures/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/binarySearch2.png) - -因为我们对 left 的更新必须是 left = mid + 1,就是说 while 循环结束时,nums[left] 一定不等于 target 了,而 nums[left-1] 可能是 target。 - -至于为什么 left 的更新必须是 left = mid + 1,同左侧边界搜索,就不再赘述。 - -1、为什么没有返回 -1 的操作?如果 nums 中不存在 target 这个值,怎么办? - -答:类似之前的左侧边界搜索,因为 while 的终止条件是 left == right,就是说 left 的取值范围是 [0, nums.length],所以可以添加两行代码,正确地返回 -1: - -```java -while (left < right) { - // ... -} -if (left == 0) return -1; -return nums[left-1] == target ? (left-1) : -1; -``` - -### 四、最后总结 - -来梳理一下这些细节差异的因果逻辑: - -第一个,最基本的二分查找算法: - -```python -因为我们初始化 right = nums.length - 1 -所以决定了我们的「搜索区间」是 [left, right] -所以决定了 while (left <= right) -同时也决定了 left = mid+1 和 right = mid-1 - -因为我们只需找到一个 target 的索引即可 -所以当 nums[mid] == target 时可以立即返回 -``` - -第二个,寻找左侧边界的二分查找: - -```python -因为我们初始化 right = nums.length -所以决定了我们的「搜索区间」是 [left, right) -所以决定了 while (left < right) -同时也决定了 left = mid + 1 和 right = mid - -因为我们需找到 target 的最左侧索引 -所以当 nums[mid] == target 时不要立即返回 -而要收紧右侧边界以锁定左侧边界 -``` - -第三个,寻找右侧边界的二分查找: - -```python -因为我们初始化 right = nums.length -所以决定了我们的「搜索区间」是 [left, right) -所以决定了 while (left < right) -同时也决定了 left = mid + 1 和 right = mid - -因为我们需找到 target 的最右侧索引 -所以当 nums[mid] == target 时不要立即返回 -而要收紧左侧边界以锁定右侧边界 - -又因为收紧左侧边界时必须 left = mid + 1 -所以最后无论返回 left 还是 right,必须减一 -``` - -如果以上内容你都能理解,那么恭喜你,二分查找算法的细节不过如此。 - -通过本文,你学会了: - -1、分析二分查找代码时,不要出现 else,全部展开成 else if 方便理解。 - -2、注意「搜索区间」和 while 的终止条件,如果存在漏掉的元素,记得在最后检查。 - -3、如需要搜索左右边界,只要在 nums[mid] == target 时做修改即可。搜索右侧时需要减一。 - - -呵呵,此文对二分查找的问题无敌好吧!**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/think_like_computer/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" "b/think_like_computer/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" deleted file mode 100644 index 12e3acf409..0000000000 --- "a/think_like_computer/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" +++ /dev/null @@ -1,110 +0,0 @@ -# 信封嵌套问题 - -很多算法问题都需要排序技巧,其难点不在于排序本身,而是需要巧妙地排序进行预处理,将算法问题进行转换,为之后的操作打下基础。 - -信封嵌套问题就需要先按特定的规则排序,之后就转换为一个 [最长递增子序列问题](../动态规划系列/动态规划设计:最长递增子序列.md),可以用前文 [二分查找详解](二分查找详解.md) 的技巧来解决了。 - -### 一、题目概述 - -信封嵌套问题是个很有意思且经常出现在生活中的问题,先看下题目: - -![title](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/title.png) - -这道题目其实是最长递增子序列(Longes Increasing Subsequence,简写为 LIS)的一个变种,因为很显然,每次合法的嵌套是大的套小的,相当于找一个最长递增的子序列,其长度就是最多能嵌套的信封个数。 - -但是难点在于,标准的 LIS 算法只能在数组中寻找最长子序列,而我们的信封是由 `(w, h)` 这样的二维数对形式表示的,如何把 LIS 算法运用过来呢? - -![0](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/0.jpg) - -读者也许会想,通过 `w × h` 计算面积,然后对面积进行标准的 LIS 算法。但是稍加思考就会发现这样不行,比如 `1 × 10` 大于 `3 × 3`,但是显然这样的两个信封是无法互相嵌套的。 - -### 二、解法 - -这道题的解法是比较巧妙的: - -**先对宽度 `w` 进行升序排序,如果遇到 `w` 相同的情况,则按照高度 `h` 降序排序。之后把所有的 `h` 作为一个数组,在这个数组上计算 LIS 的长度就是答案。** - -画个图理解一下,先对这些数对进行排序: - -![1](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/1.jpg) - -然后在 `h` 上寻找最长递增子序列: - -![2](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/2.jpg) - -这个子序列就是最优的嵌套方案。 - -这个解法的关键在于,对于宽度 `w` 相同的数对,要对其高度 `h` 进行降序排序。因为两个宽度相同的信封不能相互包含的,逆序排序保证在 `w` 相同的数对中最多只选取一个。 - -下面看代码: - -```java -// envelopes = [[w, h], [w, h]...] -public int maxEnvelopes(int[][] envelopes) { - int n = envelopes.length; - // 按宽度升序排列,如果宽度一样,则按高度降序排列 - Arrays.sort(envelopes, new Comparator() - { - public int compare(int[] a, int[] b) { - return a[0] == b[0] ? - b[1] - a[1] : a[0] - b[0]; - } - }); - // 对高度数组寻找 LIS - int[] height = new int[n]; - for (int i = 0; i < n; i++) - height[i] = envelopes[i][1]; - - return lengthOfLIS(height); -} -``` - -关于最长递增子序列的寻找方法,在前文中详细介绍了动态规划解法,并用扑克牌游戏解释了二分查找解法,本文就不展开了,直接套用算法模板: - -```java -/* 返回 nums 中 LIS 的长度 */ -public int lengthOfLIS(int[] nums) { - int piles = 0, n = nums.length; - int[] top = new int[n]; - for (int i = 0; i < n; i++) { - // 要处理的扑克牌 - int poker = nums[i]; - int left = 0, right = piles; - // 二分查找插入位置 - while (left < right) { - int mid = (left + right) / 2; - if (top[mid] >= poker) - right = mid; - else - left = mid + 1; - } - if (left == piles) piles++; - // 把这张牌放到牌堆顶 - top[left] = poker; - } - // 牌堆数就是 LIS 长度 - return piles; -} -``` - -为了清晰,我将代码分为了两个函数, 你也可以合并,这样可以节省下 `height` 数组的空间。 - -此算法的时间复杂度为 $O(NlogN)$,因为排序和计算 LIS 各需要 $O(NlogN)$ 的时间。 - -空间复杂度为 $O(N)$,因为计算 LIS 的函数中需要一个 `top` 数组。 - -### 三、总结 - -这个问题是个 Hard 级别的题目,难就难在排序,正确地排序后此问题就被转化成了一个标准的 LIS 问题,容易解决一些。 - -其实这种问题还可以拓展到三维,比如说现在不是让你嵌套信封,而是嵌套箱子,每个箱子有长宽高三个维度,请你算算最多能嵌套几个箱子? - -我们可能会这样想,先把前两个维度(长和宽)按信封嵌套的思路求一个嵌套序列,最后在这个序列的第三个维度(高度)找一下 LIS,应该能算出答案。 - -实际上,这个思路是错误的。这类问题叫做「偏序问题」,上升到三维会使难度巨幅提升,需要借助一种高级数据结构「树状数组」,有兴趣的读者可以自行搜索。 - -有很多算法问题都需要排序后进行处理,阿东正在进行整理总结。希望本文对你有帮助。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/think_like_computer/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" "b/think_like_computer/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" deleted file mode 100644 index 3126dec09c..0000000000 --- "a/think_like_computer/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" +++ /dev/null @@ -1,134 +0,0 @@ -# 前缀和技巧 - -今天来聊一道简单却十分巧妙的算法问题:算出一共有几个和为 k 的子数组。 - -![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/title.png) - -那我把所有子数组都穷举出来,算它们的和,看看谁的和等于 k 不就行了。 - -关键是,**如何快速得到某个子数组的和呢**,比如说给你一个数组 `nums`,让你实现一个接口 `sum(i, j)`,这个接口要返回 `nums[i..j]` 的和,而且会被多次调用,你怎么实现这个接口呢? - -因为接口要被多次调用,显然不能每次都去遍历 `nums[i..j]`,有没有一种快速的方法在 O(1) 时间内算出 `nums[i..j]` 呢?这就需要**前缀和**技巧了。 - -### 一、什么是前缀和 - -前缀和的思路是这样的,对于一个给定的数组 `nums`,我们额外开辟一个前缀和数组进行预处理: - -```java -int n = nums.length; -// 前缀和数组 -int[] preSum = new int[n + 1]; -preSum[0] = 0; -for (int i = 0; i < n; i++) - preSum[i + 1] = preSum[i] + nums[i]; -``` - -![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/1.jpg) - -这个前缀和数组 `preSum` 的含义也很好理解,`preSum[i]` 就是 `nums[0..i-1]` 的和。那么如果我们想求 `nums[i..j]` 的和,只需要一步操作 `preSum[j+1]-preSum[i]` 即可,而不需要重新去遍历数组了。 - -回到这个子数组问题,我们想求有多少个子数组的和为 k,借助前缀和技巧很容易写出一个解法: - -```java -int subarraySum(int[] nums, int k) { - int n = nums.length; - // 构造前缀和 - int[] sum = new int[n + 1]; - sum[0] = 0; - for (int i = 0; i < n; i++) - sum[i + 1] = sum[i] + nums[i]; - - int ans = 0; - // 穷举所有子数组 - for (int i = 1; i <= n; i++) - for (int j = 0; j < i; j++) - // sum of nums[j..i-1] - if (sum[i] - sum[j] == k) - ans++; - - return ans; -} -``` - -这个解法的时间复杂度 $O(N^2)$ 空间复杂度 $O(N)$,并不是最优的解法。不过通过这个解法理解了前缀和数组的工作原理之后,可以使用一些巧妙的办法把时间复杂度进一步降低。 - -### 二、优化解法 - -前面的解法有嵌套的 for 循环: - -```java -for (int i = 1; i <= n; i++) - for (int j = 0; j < i; j++) - if (sum[i] - sum[j] == k) - ans++; -``` - -第二层 for 循环在干嘛呢?翻译一下就是,**在计算,有几个 `j` 能够使得 `sum[i]` 和 `sum[j]` 的差为 k。**毎找到一个这样的 `j`,就把结果加一。 - -我们可以把 if 语句里的条件判断移项,这样写: - -```java -if (sum[j] == sum[i] - k) - ans++; -``` - -优化的思路是:**我直接记录下有几个 `sum[j]` 和 `sum[i] - k` 相等,直接更新结果,就避免了内层的 for 循环**。我们可以用哈希表,在记录前缀和的同时记录该前缀和出现的次数。 - -```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; -} -``` - -比如说下面这个情况,需要前缀和 8 就能找到和为 k 的子数组了,之前的暴力解法需要遍历数组去数有几个 8,而优化解法借助哈希表可以直接得知有几个前缀和为 8。 - -![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/2.jpg) - -这样,就把时间复杂度降到了 $O(N)$,是最优解法了。 - -### 三、总结 - -前缀和不难,却很有用,主要用于处理数组区间的问题。 - -比如说,让你统计班上同学考试成绩在不同分数段的百分比,也可以利用前缀和技巧: - -```java -int[] scores; // 存储着所有同学的分数 -// 试卷满分 150 分 -int[] count = new int[150 + 1] -// 记录每个分数有几个同学 -for (int score : scores) - count[score]++ -// 构造前缀和 -for (int i = 1; i < count.length; i++) - count[i] = count[i] + count[i-1]; -``` - -这样,给你任何一个分数段,你都能通过前缀和相减快速计算出这个分数段的人数,百分比也就很容易计算了。 - -但是,稍微复杂一些的算法问题,不止考察简单的前缀和技巧。比如本文探讨的这道题目,就需要借助前缀和的思路做进一步的优化,借助哈希表去除不必要的嵌套循环。可见对题目的理解和细节的分析能力对于算法的优化是至关重要的。 - -希望本文对你有帮助。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/think_like_computer/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" "b/think_like_computer/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" deleted file mode 100644 index 498e95ec3a..0000000000 --- "a/think_like_computer/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" +++ /dev/null @@ -1,108 +0,0 @@ -# 区间交集问题 - -本文是区间系列问题的第三篇,前两篇分别讲了区间的最大不相交子集和重叠区间的合并,今天再写一个算法,可以快速找出两组区间的交集。 - -先看下题目,LeetCode 第 986 题就是这个问题: - -![title](../pictures/intersection/title.png) - -题目很好理解,就是让你找交集,注意区间都是闭区间。 - -### 思路 - -解决区间问题的思路一般是先排序,以便操作,不过题目说已经排好序了,那么可以用两个索引指针在 `A` 和 `B` 中游走,把交集找出来,代码大概是这样的: - -```python -# A, B 形如 [[0,2],[5,10]...] -def intervalIntersection(A, B): - i, j = 0, 0 - res = [] - while i < len(A) and j < len(B): - # ... - j += 1 - i += 1 - return res -``` - -不难,我们先老老实实分析一下各种情况。 - -首先,**对于两个区间**,我们用 `[a1,a2]` 和 `[b1,b2]` 表示在 `A` 和 `B` 中的两个区间,那么什么情况下这两个区间**没有交集**呢: - -![](../pictures/intersection/1.jpg) - -只有这两种情况,写成代码的条件判断就是这样: - -```python -if b2 < a1 or a2 < b1: - [a1,a2] 和 [b1,b2] 无交集 -``` - -那么,什么情况下,两个区间存在交集呢?根据命题的否定,上面逻辑的否命题就是存在交集的条件: - -```python -# 不等号取反,or 也要变成 and -if b2 >= a1 and a2 >= b1: - [a1,a2] 和 [b1,b2] 存在交集 -``` - -接下来,两个区间存在交集的情况有哪些呢?穷举出来: - -![](../pictures/intersection/2.jpg) - -这很简单吧,就这四种情况而已。那么接下来思考,这几种情况下,交集是否有什么共同点呢? - -![](../pictures/intersection/3.jpg) - -我们惊奇地发现,交集区间是有规律的!如果交集区间是 `[c1,c2]`,那么 `c1=max(a1,b1)`,`c2=min(a2,b2)`!这一点就是寻找交集的核心,我们把代码更进一步: - -```python -while i < len(A) and j < len(B): - a1, a2 = A[i][0], A[i][1] - b1, b2 = B[j][0], B[j][1] - if b2 >= a1 and a2 >= b1: - res.append([max(a1, b1), min(a2, b2)]) - # ... -``` - -最后一步,我们的指针 `i` 和 `j` 肯定要前进(递增)的,什么时候应该前进呢? - -![](../pictures/intersection/4.gif) - -结合动画示例就很好理解了,是否前进,只取决于 `a2` 和 `b2` 的大小关系: - -```python -while i < len(A) and j < len(B): - # ... - if b2 < a2: - j += 1 - else: - i += 1 -``` - -### 代码 - -```python -# A, B 形如 [[0,2],[5,10]...] -def intervalIntersection(A, B): - i, j = 0, 0 # 双指针 - res = [] - while i < len(A) and j < len(B): - a1, a2 = A[i][0], A[i][1] - b1, b2 = B[j][0], B[j][1] - # 两个区间存在交集 - if b2 >= a1 and a2 >= b1: - # 计算出交集,加入 res - res.append([max(a1, b1), min(a2, b2)]) - # 指针前进 - if b2 < a2: j += 1 - else: i += 1 - return res -``` - -总结一下,区间类问题看起来都比较复杂,情况很多难以处理,但实际上通过观察各种不同情况之间的共性可以发现规律,用简洁的代码就能处理。 - -另外,区间问题没啥特别厉害的奇技淫巧,其操作也朴实无华,但其应用却十分广泛,接之前的几篇文章: - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/think_like_computer/\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/think_like_computer/\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" deleted file mode 100644 index 2ab0477dbe..0000000000 --- "a/think_like_computer/\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" +++ /dev/null @@ -1,66 +0,0 @@ -# 区间调度问题之区间合并 - -上篇文章用贪心算法解决了区间调度问题:给你很多区间,让你求其中的最大不重叠子集。 - -其实对于区间相关的问题,还有很多其他类型,本文就来讲讲区间合并问题(Merge Interval)。 - -LeetCode 第 56 题就是一道相关问题,题目很好理解: - -![title](../pictures/mergeInterval/title.png) - -我们解决区间问题的一般思路是先排序,然后观察规律。 - -### 一、思路 - -一个区间可以表示为 `[start, end]`,前文聊的区间调度问题,需要按 `end` 排序,以便满足贪心选择性质。而对于区间合并问题,其实按 `end` 和 `start` 排序都可以,不过为了清晰起见,我们选择按 `start` 排序。 - -![1](../pictures/mergeInterval/1.jpg) - -**显然,对于几个相交区间合并后的结果区间 `x`,`x.start` 一定是这些相交区间中 `start` 最小的,`x.end` 一定是这些相交区间中 `end` 最大的。** - -![2](../pictures/mergeInterval/2.jpg) - -由于已经排了序,`x.start` 很好确定,求 `x.end` 也很容易,可以类比在数组中找最大值的过程: - -```java -int max_ele = arr[0]; -for (int i = 1; i < arr.length; i++) - max_ele = max(max_ele, arr[i]); -return max_ele; -``` - -### 二、代码 - -```python -# intervals 形如 [[1,3],[2,6]...] -def merge(intervals): - if not intervals: return [] - # 按区间的 start 升序排列 - intervals.sort(key=lambda intv: intv[0]) - res = [] - res.append(intervals[0]) - - for i in range(1, len(intervals)): - curr = intervals[i] - # res 中最后一个元素的引用 - last = res[-1] - if curr[0] <= last[1]: - # 找到最大的 end - last[1] = max(last[1], curr[1]) - else: - # 处理下一个待合并区间 - res.append(curr) - return res -``` - -看下动画就一目了然了: - -![3](../pictures/mergeInterval/3.gif) - -至此,区间合并问题就解决了。本文篇幅短小,因为区间合并只是区间问题的一个类型,后续还有一些区间问题。本想把所有问题类型都总结在一篇文章,但有读者反应,长文只会收藏不会看... 所以还是分成小短文吧,读者有什么看法可以在留言板留言交流。 - -本文终,希望对你有帮助。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/think_like_computer/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" "b/think_like_computer/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" deleted file mode 100644 index 1518adcc92..0000000000 --- "a/think_like_computer/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" +++ /dev/null @@ -1,199 +0,0 @@ -# 双指针技巧总结 - -我把双指针技巧再分为两类,一类是「快慢指针」,一类是「左右指针」。前者解决主要解决链表中的问题,比如典型的判定链表中是否包含环;后者主要解决数组(或者字符串)中的问题,比如二分查找。 - -### 一、快慢指针的常见算法 - -快慢指针一般都初始化指向链表的头结点 head,前进时快指针 fast 在前,慢指针 slow 在后,巧妙解决一些链表中的问题。 - -**1、判定链表中是否含有环** - -这应该属于链表最基本的操作了,如果读者已经知道这个技巧,可以跳过。 - -单链表的特点是每个节点只知道下一个节点,所以一个指针的话无法判断链表中是否含有环的。 - -如果链表中不含环,那么这个指针最终会遇到空指针 null 表示链表到头了,这还好说,可以判断该链表不含环。 -```java - -boolean hasCycle(ListNode head) { - while (head != null) - head = head.next; - return false; -} -``` - -但是如果链表中含有环,那么这个指针就会陷入死循环,因为环形数组中没有 null 指针作为尾部节点。 - -经典解法就是用两个指针,一个跑得快,一个跑得慢。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。 - -```java -boolean hasCycle(ListNode head) { - ListNode fast, slow; - fast = slow = head; - while (fast != null && fast.next != null) { - fast = fast.next.next; - slow = slow.next; - - if (fast == slow) return true; - } - return false; -} -``` - -**2、已知链表中含有环,返回这个环的起始位置** - -![1](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/1.png) - -这个问题一点都不困难,有点类似脑筋急转弯,先直接看代码: - -```java -ListNode detectCycle(ListNode head) { - ListNode fast, slow; - fast = slow = head; - while (fast != null && fast.next != null) { - fast = fast.next.next; - slow = slow.next; - if (fast == slow) break; - } - // 上面的代码类似 hasCycle 函数 - slow = head; - while (slow != fast) { - fast = fast.next; - slow = slow.next; - } - return slow; -} -``` - -可以看到,当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。这是为什么呢? - -第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。 - -![2](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/2.png) - -设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。 - -巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。 - -![3](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/3.png) - -所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后就会相遇,相遇之处就是环的起点了。 - -**3、寻找链表的中点** - -类似上面的思路,我们还可以让快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。 - -```java -while (fast != null && fast.next != null) { - fast = fast.next.next; - slow = slow.next; -} -// slow 就在中间位置 -return slow; -``` - -当链表的长度是奇数时,slow 恰巧停在中点位置;如果长度是偶数,slow 最终的位置是中间偏右: - -![center](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/center.png) - -寻找链表中点的一个重要作用是对链表进行归并排序。 - -回想数组的归并排序:求中点索引递归地把数组二分,最后合并两个有序数组。对于链表,合并两个有序链表是很简单的,难点就在于二分。 - -但是现在你学会了找到链表的中点,就能实现链表的二分了。关于归并排序的具体内容本文就不具体展开了。 - - -**4、寻找链表的倒数第 k 个元素** - -我们的思路还是使用快慢指针,让快指针先走 k 步,然后快慢指针开始同速前进。这样当快指针走到链表末尾 null 时,慢指针所在的位置就是倒数第 k 个链表节点(为了简化,假设 k 不会超过链表长度): - -```java -ListNode slow, fast; -slow = fast = head; -while (k-- > 0) - fast = fast.next; - -while (fast != null) { - slow = slow.next; - fast = fast.next; -} -return slow; -``` - - -### 二、左右指针的常用算法 - -左右指针在数组中实际是指两个索引值,一般初始化为 left = 0, right = nums.length - 1 。 - -**1、二分查找** - -前文「二分查找」有详细讲解,这里只写最简单的二分算法,旨在突出它的双指针特性: - -```java -int binarySearch(int[] nums, int target) { - int left = 0; - int right = nums.length - 1; - while(left <= right) { - int mid = (right + left) / 2; - if(nums[mid] == target) - return mid; - else if (nums[mid] < target) - left = mid + 1; - else if (nums[mid] > target) - right = mid - 1; - } - return -1; -} -``` - -**2、两数之和** - -直接看一道 LeetCode 题目吧: - -![title](../pictures/%E5%8F%8C%E6%8C%87%E9%92%88/title.png) - -只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 可以调整 sum 的大小: - -```java -int[] twoSum(int[] nums, int target) { - int left = 0, right = nums.length - 1; - while (left < right) { - int sum = nums[left] + nums[right]; - if (sum == target) { - // 题目要求的索引是从 1 开始的 - return new int[]{left + 1, right + 1}; - } else if (sum < target) { - left++; // 让 sum 大一点 - } else if (sum > target) { - right--; // 让 sum 小一点 - } - } - return new int[]{-1, -1}; -} -``` - -**3、反转数组** - -```java -void reverse(int[] nums) { - int left = 0; - int right = nums.length - 1; - while (left < right) { - // swap(nums[left], nums[right]) - int temp = nums[left]; - nums[left] = nums[right]; - nums[right] = temp; - left++; right--; - } -} -``` - -**4、滑动窗口算法** - -这也许是双指针技巧的最高境界了,如果掌握了此算法,可以解决一大类子字符串匹配的问题,不过「滑动窗口」稍微比上述的这些算法复杂些。 - -幸运的是,这类算法是有框架模板的,而且[这篇文章](滑动窗口技巧.md)就讲解了「滑动窗口」算法模板,帮大家秒杀几道 LeetCode 子串匹配的问题。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/think_like_computer/\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/think_like_computer/\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" deleted file mode 100644 index 37f52cea19..0000000000 --- "a/think_like_computer/\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" +++ /dev/null @@ -1,279 +0,0 @@ -# 回溯算法详解 - -这篇文章是很久之前的一篇《回溯算法详解》的进阶版,之前那篇不够清楚,就不必看了,看这篇就行。把框架给你讲清楚,你会发现回溯算法问题都是一个套路。 - -废话不多说,直接上回溯算法框架。**解决一个回溯问题,实际上就是一个决策树的遍历过程**。你只需要思考 3 个问题: - -1、路径:也就是已经做出的选择。 - -2、选择列表:也就是你当前可以做的选择。 - -3、结束条件:也就是到达决策树底层,无法再做选择的条件。 - -如果你不理解这三个词语的解释,没关系,我们后面会用「全排列」和「N 皇后问题」这两个经典的回溯算法问题来帮你理解这些词语是什么意思,现在你先留着印象。 - -代码方面,回溯算法的框架: - -```python -result = [] -def backtrack(路径, 选择列表): - if 满足结束条件: - result.add(路径) - return - - for 选择 in 选择列表: - 做选择 - backtrack(路径, 选择列表) - 撤销选择 -``` - -**其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」**,特别简单。 - -什么叫做选择和撤销选择呢,这个框架的底层原理是什么呢?下面我们就通过「全排列」这个问题来解开之前的疑惑,详细探究一下其中的奥妙! - -### 一、全排列问题 - -我们在高中的时候就做过排列组合的数学题,我们也知道 `n` 个不重复的数,全排列共有 n! 个。 - -PS:**为了简单清晰起见,我们这次讨论的全排列问题不包含重复的数字**。 - -那么我们当时是怎么穷举全排列的呢?比方说给三个数 `[1,2,3]`,你肯定不会无规律地乱穷举,一般是这样: - -先固定第一位为 1,然后第二位可以是 2,那么第三位只能是 3;然后可以把第二位变成 3,第三位就只能是 2 了;然后就只能变化第一位,变成 2,然后再穷举后两位…… - -其实这就是回溯算法,我们高中无师自通就会用,或者有的同学直接画出如下这棵回溯树: - -![](../pictures/backtracking/1.jpg) - -只要从根遍历这棵树,记录路径上的数字,其实就是所有的全排列。**我们不妨把这棵树称为回溯算法的「决策树」**。 - -**为啥说这是决策树呢,因为你在每个节点上其实都在做决策**。比如说你站在下图的红色节点上: - -![](../pictures/backtracking/2.jpg) - -你现在就在做决策,可以选择 1 那条树枝,也可以选择 3 那条树枝。为啥只能在 1 和 3 之中选择呢?因为 2 这个树枝在你身后,这个选择你之前做过了,而全排列是不允许重复使用数字的。 - -**现在可以解答开头的几个名词:`[2]` 就是「路径」,记录你已经做过的选择;`[1,3]` 就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层,在这里就是选择列表为空的时候**。 - -如果明白了这几个名词,**可以把「路径」和「选择」列表作为决策树上每个节点的属性**,比如下图列出了几个节点的属性: - -![](../pictures/backtracking/3.jpg) - -**我们定义的 `backtrack` 函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层,其「路径」就是一个全排列**。 - -再进一步,如何遍历一棵树?这个应该不难吧。回忆一下之前「学习数据结构的框架思维」写过,各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样: - -```java -void traverse(TreeNode root) { - for (TreeNode child : root.childern) - // 前序遍历需要的操作 - traverse(child); - // 后序遍历需要的操作 -} -``` - -而所谓的前序遍历和后序遍历,他们只是两个很有用的时间点,我给你画张图你就明白了: - -![](../pictures/backtracking/4.jpg) - -**前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行**。 - -回想我们刚才说的,「路径」和「选择」是每个节点的属性,函数在树上游走要正确维护节点的属性,那么就要在这两个特殊时间点搞点动作: - -![](../pictures/backtracking/5.jpg) - -现在,你是否理解了回溯算法的这段核心框架? - -```python -for 选择 in 选择列表: - # 做选择 - 将该选择从选择列表移除 - 路径.add(选择) - backtrack(路径, 选择列表) - # 撤销选择 - 路径.remove(选择) - 将该选择再加入选择列表 -``` - -**我们只要在递归之前做出选择,在递归之后撤销刚才的选择**,就能正确得到每个节点的选择列表和路径。 - -下面,直接看全排列代码: - -```java -List> res = new LinkedList<>(); - -/* 主函数,输入一组不重复的数字,返回它们的全排列 */ -List> permute(int[] nums) { - // 记录「路径」 - LinkedList track = new LinkedList<>(); - backtrack(nums, track); - return res; -} - -// 路径:记录在 track 中 -// 选择列表:nums 中不存在于 track 的那些元素 -// 结束条件:nums 中的元素全都在 track 中出现 -void backtrack(int[] nums, LinkedList track) { - // 触发结束条件 - if (track.size() == nums.length) { - res.add(new LinkedList(track)); - return; - } - - for (int i = 0; i < nums.length; i++) { - // 排除不合法的选择 - if (track.contains(nums[i])) - continue; - // 做选择 - track.add(nums[i]); - // 进入下一层决策树 - backtrack(nums, track); - // 取消选择 - track.removeLast(); - } -} -``` - -我们这里稍微做了些变通,没有显式记录「选择列表」,而是通过 `nums` 和 `track` 推导出当前的选择列表: - -![](../pictures/backtracking/6.jpg) - -至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是很高效,应为对链表使用 `contains` 方法需要 O(N) 的时间复杂度。有更好的方法通过交换元素达到目的,但是难理解一些,这里就不写了,有兴趣可以自行搜索一下。 - -但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。**这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高**。 - -明白了全排列问题,就可以直接套回溯算法框架了,下面简单看看 N 皇后问题。 - -### 二、N 皇后问题 - -这个问题很经典了,简单解释一下:给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。 - -PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。 - -这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。 - -直接套用框架: - -```cpp -vector> res; - -/* 输入棋盘边长 n,返回所有合法的放置 */ -vector> solveNQueens(int n) { - // '.' 表示空,'Q' 表示皇后,初始化空棋盘。 - vector board(n, string(n, '.')); - backtrack(board, 0); - return res; -} - -// 路径:board 中小于 row 的那些行都已经成功放置了皇后 -// 选择列表:第 row 行的所有列都是放置皇后的选择 -// 结束条件:row 超过 board 的最后一行 -void backtrack(vector& board, int row) { - // 触发结束条件 - if (row == board.size()) { - res.push_back(board); - return; - } - - int n = board[row].size(); - for (int col = 0; col < n; col++) { - // 排除不合法选择 - if (!isValid(board, row, col)) - continue; - // 做选择 - board[row][col] = 'Q'; - // 进入下一行决策 - backtrack(board, row + 1); - // 撤销选择 - board[row][col] = '.'; - } -} -``` - -这部分主要代码,其实跟全排列问题差不多,`isValid` 函数的实现也很简单: - -```cpp -/* 是否可以在 board[row][col] 放置皇后? */ -bool isValid(vector& board, int row, int col) { - int n = board.size(); - // 检查列是否有皇后互相冲突 - for (int i = 0; i < n; i++) { - if (board[i][col] == 'Q') - return false; - } - // 检查右上方是否有皇后互相冲突 - for (int i = row - 1, j = col + 1; - i >= 0 && j < n; i--, j++) { - if (board[i][j] == 'Q') - return false; - } - // 检查左上方是否有皇后互相冲突 - for (int i = row - 1, j = col - 1; - i >= 0 && j >= 0; i--, j--) { - if (board[i][j] == 'Q') - return false; - } - return true; -} -``` - -函数 `backtrack` 依然像个在决策树上游走的指针,通过 `row` 和 `col` 就可以表示函数遍历到的位置,通过 `isValid` 函数可以将不符合条件的情况剪枝: - -![](../pictures/backtracking/7.jpg) - -如果直接给你这么一大段解法代码,可能是懵逼的。但是现在明白了回溯算法的框架套路,还有啥难理解的呢?无非是改改做选择的方式,排除不合法选择的方式而已,只要框架存于心,你面对的只剩下小问题了。 - -当 `N = 8` 时,就是八皇后问题,数学大佬高斯穷尽一生都没有数清楚八皇后问题到底有几种可能的放置方法,但是我们的算法只需要一秒就可以算出来所有可能的结果。 - -不过真的不怪高斯。这个问题的复杂度确实非常高,看看我们的决策树,虽然有 `isValid` 函数剪枝,但是最坏时间复杂度仍然是 O(N^(N+1)),而且无法优化。如果 `N = 10` 的时候,计算就已经很耗时了。 - -**有的时候,我们并不想得到所有合法的答案,只想要一个答案,怎么办呢**?比如解数独的算法,找所有解法复杂度太高,只要找到一种解法就可以。 - -其实特别简单,只要稍微修改一下回溯算法的代码即可: - -```cpp -// 函数找到一个答案后就返回 true -bool backtrack(vector& board, int row) { - // 触发结束条件 - if (row == board.size()) { - res.push_back(board); - return true; - } - ... - for (int col = 0; col < n; col++) { - ... - board[row][col] = 'Q'; - - if (backtrack(board, row + 1)) - return true; - - board[row][col] = '.'; - } - - return false; -} -``` - -这样修改后,只要找到一个答案,for 循环的后续递归穷举都会被阻断。也许你可以在 N 皇后问题的代码框架上,稍加修改,写一个解数独的算法? - -### 三、最后总结 - -回溯算法就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作,算法框架如下: - -```python -def backtrack(...): - for 选择 in 选择列表: - 做选择 - backtrack(...) - 撤销选择 -``` - -**写 `backtrack` 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集**。 - -其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」? - -某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/think_like_computer/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" "b/think_like_computer/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" deleted file mode 100644 index 83d005dad0..0000000000 --- "a/think_like_computer/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" +++ /dev/null @@ -1,76 +0,0 @@ -# 字符串乘法 - -对于比较小的数字,做运算可以直接使用编程语言提供的运算符,但是如果相乘的两个因数非常大,语言提供的数据类型可能就会溢出。一种替代方案就是,运算数以字符串的形式输入,然后模仿我们小学学习的乘法算术过程计算出结果,并且也用字符串表示。 - -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/title.png) - -需要注意的是,`num1` 和 `num2` 可以非常长,所以不可以把他们直接转成整型然后运算,唯一的思路就是模仿我们手算乘法。 - -比如说我们手算 `123 × 45`,应该会这样计算: - -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/1.jpg) - -计算 `123 × 5`,再计算 `123 × 4`,最后错一位相加。这个流程恐怕小学生都可以熟练完成,但是你是否能**把这个运算过程进一步机械化**,写成一套算法指令让没有任何智商的计算机来执行呢? - -你看这个简单过程,其中涉及乘法进位,涉及错位相加,还涉及加法进位;而且还有一些不易察觉的问题,比如说两位数乘以两位数,结果可能是四位数,也可能是三位数,你怎么想出一个标准化的处理方式?这就是算法的魅力,如果没有计算机思维,简单的问题可能都没办法自动化处理。 - -首先,我们这种手算方式还是太「高级」了,我们要再「低级」一点,`123 × 5` 和 `123 × 4` 的过程还可以进一步分解,最后再相加: - -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/2.jpg) - -现在 `123` 并不大,如果是个很大的数字的话,是无法直接计算乘积的。我们可以用一个数组在底下接收相加结果: - -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/3.jpg) - -整个计算过程大概是这样,**有两个指针 `i,j` 在 `num1` 和 `num2` 上游走,计算乘积,同时将乘积叠加到 `res` 的正确位置**: - -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/4.gif) - -现在还有一个关键问题,如何将乘积叠加到 `res` 的正确位置,或者说,如何通过 `i,j` 计算 `res` 的对应索引呢? - -其实,细心观察之后就发现,**`num1[i]` 和 `num2[j]` 的乘积对应的就是 `res[i+j]` 和 `res[i+j+1]` 这两个位置**。 - -![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/6.jpg) - -明白了这一点,就可以用代码模仿出这个计算过程了: - -```java -string multiply(string num1, string num2) { - int m = num1.size(), n = num2.size(); - // 结果最多为 m + n 位数 - vector res(m + n, 0); - // 从个位数开始逐位相乘 - for (int i = m - 1; i >= 0; i--) - for (int j = n - 1; j >= 0; j--) { - int mul = (num1[i]-'0') * (num2[j]-'0'); - // 乘积在 res 对应的索引位置 - int p1 = i + j, p2 = i + j + 1; - // 叠加到 res 上 - int sum = mul + res[p2]; - res[p2] = sum % 10; - res[p1] += sum / 10; - } - // 结果前缀可能存的 0(未使用的位) - int i = 0; - while (i < res.size() && res[i] == 0) - i++; - // 将计算结果转化成字符串 - string str; - for (; i < res.size(); i++) - str.push_back('0' + res[i]); - - return str.size() == 0 ? "0" : str; -} -``` - -至此,字符串乘法算法就完成了。 - -**总结一下**,我们习以为常的一些思维方式,在计算机看来是非常难以做到的。比如说我们习惯的算术流程并不复杂,但是如果让你再进一步,翻译成代码逻辑,并不简单。算法需要将计算流程再简化,通过边算边叠加的方式来得到结果。 - -俗话教育我们,不要陷入思维定式,不要程序化,要发散思维,要创新。但我觉得程序化并不是坏事,可以大幅提高效率,减小失误率。算法不就是一套程序化的思维吗,只有程序化才能让计算机帮助我们解决复杂问题呀! - -也许算法就是一种**寻找思维定式的思维**吧,希望本文对你有帮助。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/think_like_computer/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" "b/think_like_computer/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" deleted file mode 100644 index f45dae5c32..0000000000 --- "a/think_like_computer/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" +++ /dev/null @@ -1,120 +0,0 @@ -# 常用的位操作 - -本文分两部分,第一部分列举几个有趣的位操作,第二部分讲解算法中常用的 n & (n - 1) 操作,顺便把用到这个技巧的算法题列出来讲解一下。因为位操作很简单,所以假设读者已经了解与、或、异或这三种基本操作。 - -位操作(Bit Manipulation)可以玩出很多奇技淫巧,但是这些技巧大部分都过于晦涩,没必要深究,读者只要记住一些有用的操作即可。 - -### 一、几个有趣的位操作 - -1. 利用或操作 `|` 和空格将英文字符转换为小写 - -```c -('a' | ' ') = 'a' -('A' | ' ') = 'a' -``` - -2. 利用与操作 `&` 和下划线将英文字符转换为大写 - -```c -('b' & '_') = 'B' -('B' & '_') = 'B' -``` - -3. 利用异或操作 `^` 和空格进行英文字符大小写互换 - -```c -('d' ^ ' ') = 'D' -('D' ^ ' ') = 'd' -``` - -PS:以上操作能够产生奇特效果的原因在于 ASCII 编码。字符其实就是数字,恰巧这些字符对应的数字通过位运算就能得到正确的结果,有兴趣的读者可以查 ASCII 码表自己算算,本文就不展开讲了。 - -4. 判断两个数是否异号 - -```c -int x = -1, y = 2; -bool f = ((x ^ y) < 0); // true - -int x = 3, y = 2; -bool f = ((x ^ y) < 0); // false -``` - -PS:这个技巧还是很实用的,利用的是补码编码的符号位。如果不用位运算来判断是否异号,需要使用 if else 分支,还挺麻烦的。读者可能想利用乘积或者商来判断两个数是否异号,但是这种处理方式可能造成溢出,从而出现错误。(关于补码编码和溢出,参见前文) - -5. 交换两个数 - -```c -int a = 1, b = 2; -a ^= b; -b ^= a; -a ^= b; -// 现在 a = 2, b = 1 -``` - -6. 加一 - -```c -int n = 1; -n = -~n; -// 现在 n = 2 -``` - -7. 减一 - -```c -int n = 2; -n = ~-n; -// 现在 n = 1 -``` - -PS:上面这三个操作就纯属装逼用的,没啥实际用处,大家了解了解乐呵一下就行。 - -### 二、算法常用操作 n&(n-1) - -这个操作是算法中常见的,作用是消除数字 n 的二进制表示中的最后一个 1。 - -看个图就很容易理解了: - -![n](../pictures/%E4%BD%8D%E6%93%8D%E4%BD%9C/1.png) - -1. 计算汉明权重(Hamming Weight) - -![title](../pictures/%E4%BD%8D%E6%93%8D%E4%BD%9C/title.png) - -就是让你返回 n 的二进制表示中有几个 1。因为 n & (n - 1) 可以消除最后一个 1,所以可以用一个循环不停地消除 1 同时计数,直到 n 变成 0 为止。 - -```cpp -int hammingWeight(uint32_t n) { - int res = 0; - while (n != 0) { - n = n & (n - 1); - res++; - } - return res; -} -``` - -1. 判断一个数是不是 2 的指数 - -一个数如果是 2 的指数,那么它的二进制表示一定只含有一个 1: - -```cpp -2^0 = 1 = 0b0001 -2^1 = 2 = 0b0010 -2^2 = 4 = 0b0100 -``` - -如果使用位运算技巧就很简单了(注意运算符优先级,括号不可以省略): - -```cpp -bool isPowerOfTwo(int n) { - if (n <= 0) return false; - return (n & (n - 1)) == 0; -} -``` - -以上便是一些有趣/常用的位操作。其实位操作的技巧很多,有一个叫做 Bit Twiddling Hacks 的外国网站收集了几乎所有位操作的黑科技玩法,感兴趣的读者可以点击「阅读原文」按钮查看。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/think_like_computer/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" "b/think_like_computer/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" deleted file mode 100644 index 7ce6829321..0000000000 --- "a/think_like_computer/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" +++ /dev/null @@ -1,296 +0,0 @@ -# 滑动窗口技巧 - -本文详解「滑动窗口」这种高级双指针技巧的算法框架,带你秒杀几道高难度的子字符串匹配问题。 - -LeetCode 上至少有 9 道题目可以用此方法高效解决。但是有几道是 VIP 题目,有几道题目虽不难但太复杂,所以本文只选择点赞最高,较为经典的,最能够讲明白的三道题来讲解。第一题为了让读者掌握算法模板,篇幅相对长,后两题就基本秒杀了。 - -本文代码为 C++ 实现,不会用到什么编程方面的奇技淫巧,但是还是简单介绍一下一些用到的数据结构,以免有的读者因为语言的细节问题阻碍对算法思想的理解: - -`unordered_map` 就是哈希表(字典),它的一个方法 count(key) 相当于 containsKey(key) 可以判断键 key 是否存在。 - -可以使用方括号访问键对应的值 map[key]。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key] 赋值为 0。 - -所以代码中多次出现的 `map[key]++` 相当于 Java 的 `map.put(key, map.getOrDefault(key, 0) + 1)`。 - -本文大部分代码都是图片形式,可以点开放大,更重要的是可以左右滑动方便对比代码。下面进入正题。 - -### 一、最小覆盖子串 - -![题目链接](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/title1.png) - -题目不难理解,就是说要在 S(source) 中找到包含 T(target) 中全部字母的一个子串,顺序无所谓,但这个子串一定是所有可能子串中最短的。 - -如果我们使用暴力解法,代码大概是这样的: - -```java -for (int i = 0; i < s.size(); i++) - for (int j = i + 1; j < s.size(); j++) - if s[i:j] 包含 t 的所有字母: - 更新答案 -``` - -思路很直接吧,但是显然,这个算法的复杂度肯定大于 O(N^2) 了,不好。 - -滑动窗口算法的思路是这样: - -1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。 - -2、我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。 - -3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。 - -4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。 - -这个思路其实也不难,**第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解。**左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。 - -下面画图理解一下,needs 和 window 相当于计数器,分别记录 T 中字符出现次数和窗口中的相应字符的出现次数。 - -初始状态: - -![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/0.png) - -增加 right,直到窗口 [left, right] 包含了 T 中所有字符: - -![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/1.png) - - -现在开始增加 left,缩小窗口 [left, right]。 - -![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/2.png) - -直到窗口中的字符串不再符合要求,left 不再继续移动。 - -![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/3.png) - - -之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。 - -如果你能够理解上述过程,恭喜,你已经完全掌握了滑动窗口算法思想。至于如何具体到问题,如何得出此题的答案,都是编程问题,等会提供一套模板,理解一下就会了。 - -上述过程可以简单地写出如下伪码框架: - -```cpp -string s, t; -// 在 s 中寻找 t 的「最小覆盖子串」 -int left = 0, right = 0; -string res = s; - -while(right < s.size()) { - window.add(s[right]); - right++; - // 如果符合要求,移动 left 缩小窗口 - while (window 符合要求) { - // 如果这个窗口的子串更短,则更新 res - res = minLen(res, window); - window.remove(s[left]); - left++; - } -} -return res; -``` - -如果上述代码你也能够理解,那么你离解题更近了一步。现在就剩下一个比较棘手的问题:如何判断 window 即子串 s[left...right] 是否符合要求,是否包含 t 的所有字符呢? - -可以用两个哈希表当作计数器解决。用一个哈希表 needs 记录字符串 t 中包含的字符及出现次数,用另一个哈希表 window 记录当前「窗口」中包含的字符及出现的次数,如果 window 包含所有 needs 中的键,且这些键对应的值都大于等于 needs 中的值,那么就可以知道当前「窗口」符合要求了,可以开始移动 left 指针了。 - -现在将上面的框架继续细化: - -```cpp -string s, t; -// 在 s 中寻找 t 的「最小覆盖子串」 -int left = 0, right = 0; -string res = s; - -// 相当于两个计数器 -unordered_map window; -unordered_map needs; -for (char c : t) needs[c]++; - -// 记录 window 中已经有多少字符符合要求了 -int match = 0; - -while (right < s.size()) { - char c1 = s[right]; - if (needs.count(c1)) { - window[c1]++; // 加入 window - if (window[c1] == needs[c1]) - // 字符 c1 的出现次数符合要求了 - match++; - } - right++; - - // window 中的字符串已符合 needs 的要求了 - while (match == needs.size()) { - // 更新结果 res - res = minLen(res, window); - char c2 = s[left]; - if (needs.count(c2)) { - window[c2]--; // 移出 window - if (window[c2] < needs[c2]) - // 字符 c2 出现次数不再符合要求 - match--; - } - left++; - } -} -return res; -``` - -上述代码已经具备完整的逻辑了,只有一处伪码,即更新 res 的地方,不过这个问题太好解决了,直接看解法吧! - -```cpp -string minWindow(string s, string t) { - // 记录最短子串的开始位置和长度 - int start = 0, minLen = INT_MAX; - int left = 0, right = 0; - - unordered_map window; - unordered_map needs; - for (char c : t) needs[c]++; - - int match = 0; - - while (right < s.size()) { - char c1 = s[right]; - if (needs.count(c1)) { - window[c1]++; - if (window[c1] == needs[c1]) - match++; - } - right++; - - while (match == needs.size()) { - if (right - left < minLen) { - // 更新最小子串的位置和长度 - start = left; - minLen = right - left; - } - char c2 = s[left]; - if (needs.count(c2)) { - window[c2]--; - if (window[c2] < needs[c2]) - match--; - } - left++; - } - } - return minLen == INT_MAX ? - "" : s.substr(start, minLen); -} -``` - -如果直接甩给你这么一大段代码,我想你的心态是爆炸的,但是通过之前的步步跟进,你是否能够理解这个算法的内在逻辑呢?你是否能清晰看出该算法的结构呢? - -这个算法的时间复杂度是 O(M + N),M 和 N 分别是字符串 S 和 T 的长度。因为我们先用 for 循环遍历了字符串 T 来初始化 needs,时间 O(N),之后的两个 while 循环最多执行 2M 次,时间 O(M)。 - -读者也许认为嵌套的 while 循环复杂度应该是平方级,但是你这样想,while 执行的次数就是双指针 left 和 right 走的总路程,最多是 2M 嘛。 - - -### 二、找到字符串中所有字母异位词 - -![题目链接](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/title2.png) - -这道题的难度是 Easy,但是评论区点赞最多的一条是这样: - -`How can this problem be marked as easy???` - -实际上,这个 Easy 是属于了解双指针技巧的人的,只要把上一道题的代码改中更新 res 部分的代码稍加修改就成了这道题的解: - -```cpp -vector findAnagrams(string s, string t) { - // 用数组记录答案 - vector res; - int left = 0, right = 0; - unordered_map needs; - unordered_map window; - for (char c : t) needs[c]++; - int match = 0; - - while (right < s.size()) { - char c1 = s[right]; - if (needs.count(c1)) { - window[c1]++; - if (window[c1] == needs[c1]) - match++; - } - right++; - - while (match == needs.size()) { - // 如果 window 的大小合适 - // 就把起始索引 left 加入结果 - if (right - left == t.size()) { - res.push_back(left); - } - char c2 = s[left]; - if (needs.count(c2)) { - window[c2]--; - if (window[c2] < needs[c2]) - match--; - } - left++; - } - } - return res; -} -``` - -因为这道题和上一道的场景类似,也需要 window 中包含串 t 的所有字符,但上一道题要找长度最短的子串,这道题要找长度相同的子串,也就是「字母异位词」嘛。 - -### 三、无重复字符的最长子串 - -![题目链接](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/title3.png) - -遇到子串问题,首先想到的就是滑动窗口技巧。 - -类似之前的思路,使用 window 作为计数器记录窗口中的字符出现次数,然后先向右移动 right,当 window 中出现重复字符时,开始移动 left 缩小窗口,如此往复: - -```cpp -int lengthOfLongestSubstring(string s) { - int left = 0, right = 0; - unordered_map window; - int res = 0; // 记录最长长度 - - while (right < s.size()) { - char c1 = s[right]; - window[c1]++; - right++; - // 如果 window 中出现重复字符 - // 开始移动 left 缩小窗口 - while (window[c1] > 1) { - char c2 = s[left]; - window[c2]--; - left++; - } - res = max(res, right - left); - } - return res; -} -``` - -需要注意的是,因为我们要求的是最长子串,所以需要在每次移动 right 增大窗口时更新 res,而不是像之前的题目在移动 left 缩小窗口时更新 res。 - -### 最后总结 - -通过上面三道题,我们可以总结出滑动窗口算法的抽象思想: - -```java -int left = 0, right = 0; - -while (right < s.size()) { - window.add(s[right]); - right++; - - while (valid) { - window.remove(s[left]); - left++; - } -} -``` - -其中 window 的数据类型可以视具体情况而定,比如上述题目都使用哈希表充当计数器,当然你也可以用一个数组实现同样效果,因为我们只处理英文字母。 - -稍微麻烦的地方就是这个 valid 条件,为了实现这个条件的实时更新,我们可能会写很多代码。比如前两道题,看起来解法篇幅那么长,实际上思想还是很简单,只是大多数代码都在处理这个问题而已。 - -如果本文对你有帮助,欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚~ - -![公众号 labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git "a/think_like_computer/\347\203\247\351\245\274\346\216\222\345\272\217.md" "b/think_like_computer/\347\203\247\351\245\274\346\216\222\345\272\217.md" deleted file mode 100644 index 668f3c6756..0000000000 --- "a/think_like_computer/\347\203\247\351\245\274\346\216\222\345\272\217.md" +++ /dev/null @@ -1,124 +0,0 @@ -# 烧饼排序 - -烧饼排序是个很有意思的实际问题:假设盘子上有 `n` 块**面积大小不一**的烧饼,你如何用一把锅铲进行若干次翻转,让这些烧饼的大小有序(小的在上,大的在下)? - -![](../pictures/pancakeSort/1.jpg) - -设想一下用锅铲翻转一堆烧饼的情景,其实是有一点限制的,我们每次只能将最上面的若干块饼子翻转: - -![](../pictures/pancakeSort/2.png) - -我们的问题是,**如何使用算法得到一个翻转序列,使得烧饼堆变得有序**? - -首先,需要把这个问题抽象,用数组来表示烧饼堆: - -![](../pictures/pancakeSort/title.png) - -如何解决这个问题呢?其实类似上篇文章 [递归反转链表的一部分](../数据结构系列/递归反转链表的一部分.md),这也是需要**递归思想**的。 - -### 一、思路分析 - -为什么说这个问题有递归性质呢?比如说我们需要实现这样一个函数: - -```java -// cakes 是一堆烧饼,函数会将前 n 个烧饼排序 -void sort(int[] cakes, int n); -``` - -如果我们找到了前 `n` 个烧饼中最大的那个,然后设法将这个饼子翻转到最底下: - -![](../pictures/pancakeSort/3.jpg) - -那么,原问题的规模就可以减小,递归调用 `pancakeSort(A, n-1)` 即可: - -![](../pictures/pancakeSort/4.jpg) - -接下来,对于上面的这 `n - 1` 块饼,如何排序呢?还是先从中找到最大的一块饼,然后把这块饼放到底下,再递归调用 `pancakeSort(A, n-1-1)`…… - -你看,这就是递归性质,总结一下思路就是: - -1、找到 `n` 个饼中最大的那个。 - -2、把这个最大的饼移到最底下。 - -3、递归调用 `pancakeSort(A, n - 1)`。 - -base case:`n == 1` 时,排序 1 个饼时不需要翻转。 - -那么,最后剩下个问题,**如何设法将某块烧饼翻到最后呢**? - -其实很简单,比如第 3 块饼是最大的,我们想把它换到最后,也就是换到第 `n` 块。可以这样操作: - -1、用锅铲将前 3 块饼翻转一下,这样最大的饼就翻到了最上面。 - -2、用锅铲将前 `n` 块饼全部翻转,这样最大的饼就翻到了第 `n` 块,也就是最后一块。 - -以上两个流程理解之后,基本就可以写出解法了,不过题目要求我们写出具体的反转操作序列,这也很简单,只要在每次翻转烧饼时记录下来就行了。 - -### 二、代码实现 - -只要把上述的思路用代码实现即可,唯一需要注意的是,数组索引从 0 开始,而我们要返回的结果是从 1 开始算的。 - -```java -// 记录反转操作序列 -LinkedList res = new LinkedList<>(); - -List pancakeSort(int[] cakes) { - sort(cakes, cakes.length); - return res; -} - -void sort(int[] cakes, int n) { - // base case - if (n == 1) return; - - // 寻找最大饼的索引 - int maxCake = 0; - int maxCakeIndex = 0; - for (int i = 0; i < n; i++) - if (cakes[i] > maxCake) { - maxCakeIndex = i; - maxCake = cakes[i]; - } - - // 第一次翻转,将最大饼翻到最上面 - reverse(cakes, 0, maxCakeIndex); - res.add(maxCakeIndex + 1); - // 第二次翻转,将最大饼翻到最下面 - reverse(cakes, 0, n - 1); - res.add(n); - - // 递归调用 - sort(cakes, n - 1); -} - -void reverse(int[] arr, int i, int j) { - while (i < j) { - int temp = arr[i]; - arr[i] = arr[j]; - arr[j] = temp; - i++; j--; - } -} -``` - -通过刚才的详细解释,这段代码应该是很清晰了。 - -算法的时间复杂度很容易计算,因为递归调用的次数是 `n`,每次递归调用都需要一次 for 循环,时间复杂度是 O(n),所以总的复杂度是 O(n^2)。 - -**最后,我们可以思考一个问题​**:按照我们这个思路,得出的操作序列长度应该为​ `2(n - 1)`,因为每次递归都要进行 2 次翻转并记录操作,总共有 `n` 层递归,但由于 base case 直接返回结果,不进行翻转,所以最终的操作序列长度应该是固定的 `2(n - 1)`。 - -显然,这个结果不是最优的(最短的),比如说一堆煎饼 `[3,2,4,1]`,我们的算法得到的翻转序列是 `[3,4,2,3,1,2]`,但是最快捷的翻转方法应该是 `[2,3,4]`: - -初始状态 :[3,2,4,1] -翻前 2 个:[2,3,4,1] -翻前 3 个:[4,3,2,1] -翻前 4 个:[1,2,3,4] - -如果要求你的算法计算排序烧饼的**最短**操作序列,你该如何计算呢?或者说,解决这种求最优解法的问题,核心思路什么,一定需要使用什么算法技巧呢? - -不妨分享一下你的思考。 - -坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章: - -![labuladong](../pictures/labuladong.jpg) diff --git "a/think_like_computer/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" "b/think_like_computer/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" deleted file mode 100644 index 31f44b015b..0000000000 --- "a/think_like_computer/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" +++ /dev/null @@ -1,81 +0,0 @@ -# 算法学习之路 - -之前发的那篇关于框架性思维的文章,我也发到了不少其他圈子,受到了大家的普遍好评,这一点我真的没想到,首先感谢大家的认可,我会更加努力,写出通俗易懂的算法文章。 - -有很多朋友问我数据结构和算法到底该怎么学,尤其是很多朋友说自己是「小白」,感觉这些东西好难啊,就算看了之前的「框架思维」,也感觉自己刷题乏力,希望我能聊聊我从一个非科班小白一路是怎么学过来的。 - -首先要给怀有这样疑问的朋友鼓掌,因为你现在已经「知道自己不知道」,而且开始尝试学习、刷题、寻求帮助,能做到这一点本身就是及其困难的。 - -关于「框架性思维」,对于一个小白来说,可能暂时无法完全理解(如果你能理解,说明你水平已经不错啦,不是小白啦)。就像软件工程,对于我这种没带过项目的人来说,感觉其内容枯燥乏味,全是废话,但是对于一个带过团队的人,他就会觉得软件工程里的每一句话都是精华。暂时不太理解没关系,留个印象,功夫到了很快就明白了。 - -下面写一写我一路过来的一些经验。如果你已经看过很多「如何高效刷题」「如何学习算法」的文章,却还是没有开始行动并坚持下去,本文的第五点就是写给你的。 - -我觉得之所以有时候认为自己是「小白」,是由于知识某些方面的空白造成的。具体到数据结构的学习,无非就是两个问题搞得不太清楚:**这是啥?有啥用?** - -举个例子,比如说你看到了「栈」这个名词,老师可能会讲这些关键词:先进后出、函数堆栈等等。但是,对于初学者,这些描述属于文学词汇,没有实际价值,没有解决最基本的两个问题。如何回答这两个基本问题呢?回答「这是啥」需要看教科书,回答「有啥用」需要刷算法题。 - -**一、这是啥?** - -这个问题最容易解决,就像一层窗户纸,你只要随便找本书看两天,自己动手实现一个「队列」「栈」之类的数据结构,就能捅破这层窗户纸。 - -这时候你就能理解「框架思维」文章中的前半部分了:数据结构无非就是数组、链表为骨架的一些特定操作而已;每个数据结构实现的功能无非增删查改罢了。 - -比如说「列队」这个数据结构,无非就是基于数组或者链表,实现 enqueue 和 dequeue 两个方法。这两个方法就是增和删呀,连查和改的方法都不需要。 - -**二、有啥用?** - -解决这个问题,就涉及算法的设计了,是个持久战,需要经常进行抽象思考,刷算法题,培养「计算机思维」。 - -之前的文章讲了,算法就是对数据结构准确而巧妙的运用。常用算法问题也就那几大类,算法题无非就是不断变换场景,给那几个算法框架套上不同的皮。刷题,就是在锻炼你的眼力,看你能不能看穿问题表象揪出相应的解法框架。 - -比如说,让你求解一个迷宫,你要把这个问题层层抽象:迷宫 -> 图的遍历 -> N 叉树的遍历 -> 二叉树的遍历。然后让框架指导你写具体的解法。 - -抽象问题,直击本质,是刷题中你需要刻意培养的能力。 - -**三、如何看书** - -直接推荐一本公认的好书,《算法第 4 版》,我一般简写成《算法4》。不要蜻蜓点水,这本书你能选择性的看上 50%,基本上就达到平均水平了。别怕这本书厚,因为起码有三分之一不用看,下面讲讲怎么看这本书。 - -看书仍然遵循递归的思想:自顶向下,逐步求精。 - -这本书知识结构合理,讲解也清楚,所以可以按顺序学习。**书中正文的算法代码一定要亲自敲一遍**,因为这些真的是扎实的基础,要认真理解。不要以为自己看一遍就看懂了,不动手的话理解不了的。但是,开头部分的基础可以酌情跳过;书中的数学证明,如不影响对算法本身的理解,完全可以跳过;章节最后的练习题,也可以全部跳过。这样一来,这本书就薄了很多。 - -相信读者现在已经认可了「框架性思维」的重要性,这种看书方式也是一种框架性策略,抓大放小,着重理解整体的知识架构,而忽略证明、练习题这种细节问题,即**保持自己对新知识的好奇心,避免陷入无限的细节被劝退。** - -当然,《算法4》到后面的内容也比较难了,比如那几个著名的串算法,以及正则表达式算法。这些属于「经典算法」,看个人接受能力吧,单说刷 LeetCode 的话,基本用不上,量力而行即可。 - -**四、如何刷题** - -首先声明一下,**算法和数学水平没关系,和编程语言也没关系**,你爱用什么语言用什么。算法,主要是培养一种新的思维方式。所谓「计算机思维」,就跟你考驾照一样,你以前骑自行车,有一套自行车的规则和技巧,现在你开汽车,就需要适应并练习开汽车的规则和技巧。 - -LeetCode 上的算法题和前面说的「经典算法」不一样,我们权且称为「解闷算法」吧,因为很多题目都比较有趣,有种在做奥数题或者脑筋急转弯的感觉。比如说,让你用队列实现一个栈,或者用栈实现一个队列,以及不用加号做加法,开脑洞吧? - -当然,这些问题虽然看起来无厘头,实际生活中也用不到,但是想解决这些问题依然要靠数据结构以及对基础知识的理解,也许这就是很多公司面试都喜欢出这种「智力题」的原因。下面说几点技巧吧。 - -**尽量刷英文版的 LeetCode**,中文版的“力扣”是阉割版,不仅很多题目没有答案,而且连个讨论区都没有。英文版的是真的很良心了,很多问题都有官方解答,详细易懂。而且讨论区(Discuss)也沉淀了大量优质内容,甚至好过官方解答。真正能打开你思路的,很可能是讨论区各路大神的思路荟萃。 - -PS:**如果有的英文题目实在看不懂,有个小技巧**,你在题目页面的 url 里加一个 -cn,即 https://leetcode.com/xxx 改成 https://leetcode-cn.com/xxx,这样就能切换到相应的中文版页面查看。 - -对于初学者,**强烈建议从 Explore 菜单里最下面的 Learn 开始刷**,这个专题就是专门教你学习数据结构和基本算法的,教学篇和相应的练习题结合,不要太良心。 - -最近 Learn 专题里新增了一些内容,我们挑数据结构相关的内容刷就行了,像 Ruby,Machine Learning 就没必要刷了。刷完 Learn 专题的基础内容,基本就有能力去 Explore 菜单的 Interview 专题刷面试题,或者去 Problem 菜单,在真正的题海里遨游了。 - -无论刷 Explore 还是 Problems 菜单,**最好一个分类一个分类的刷,不要蜻蜓点水**。比如说这几天就刷链表,刷完链表再去连刷几天二叉树。这样做是为了帮助你提取「框架」。一旦总结出针对一类问题的框架,解决同类问题可谓是手到擒来。 - -**五、道理我都懂,还是不能坚持下去** - -这其实无关算法了,还是老生常谈的执行力的问题。不说什么破鸡汤了,我觉得**解决办法就是「激起欲望」**,注意我说的是欲望,而不是常说的兴趣,拿我自己说说吧。 - -半年前我开始刷题,目的和大部分人都一样的,就是为毕业找工作做准备。只不过,大部分人是等到临近毕业了才开始刷,而我离毕业还有一阵子。这不是炫耀我多有觉悟,而是我承认自己的极度平凡。 - -首先,我真的想找到一份不错的工作(谁都想吧?),我想要高薪呀!否则我在朋友面前,女神面前放下的骚话,最终都会反过来啪啪地打我的脸。我也是要恰饭,要面子,要虚荣心的嘛。赚钱,虚荣心,足以激起我的欲望了。 - -但是,我不擅长 deadline 突击,我理解东西真的慢,所以干脆笨鸟先飞了。智商不够,拿时间来补,我没能力两个月突击,干脆拉长战线,打他个两年游击战,我还不信耗不死算法这个强敌。事实证明,你如果认真学习一个月,就能够取得肉眼可见的进步了。 - -现在,我依然在坚持刷题,而且为了另外一个原因,这个公众号。我没想到自己的文字竟然能够帮助到他人,甚至能得到认可。这也是虚荣心啊,我不能让读者失望啊,我想让更多的人认可(夸)我呀! - -以上,不光是坚持刷算法题吧,很多场景都适用。执行力是要靠「欲望」支撑的,我也是一凡人,只有那些看得见摸得着的东西才能使我快乐呀。读者不妨也尝试把刷题学习和自己的切身利益联系起来,这恐怕是坚持下去最简单直白的理由了。 - -**致力于把算法讲清楚!欢迎关注我的微信公众号 labuladong,查看更多通俗易懂的文章**: - -![labuladong](../pictures/labuladong.png) \ No newline at end of file diff --git a/translation_requirements.md b/translation_requirements.md new file mode 100644 index 0000000000..7e1d329813 --- /dev/null +++ b/translation_requirements.md @@ -0,0 +1,30 @@ +Thank you all for your participation in the translation! + +please clone newest `english` branch, please see [closed pull request](https://github.com/labuladong/fucking-algorithm/pulls?q=is%3Apr+is%3Aclosed) Make sure that the article you are translating is currently not available in English. + +After the translation is completed, please delete the public account QR code at the end of the text. For the first translated version submitted, you can add authors and translators below the first level heading at the beginning of the article: + +**Translator: [YourName](https://github.com/YourName)** + +**Author: [labuladong](https://github.com/labuladong)** + +Your link can point you wherever you want. +### Translation convention + +1、The translation must be smooth and consistent with the English grammar. Basic technical terms such as Queue, stack, binary tree, etc. should be used correctly. Misuse of such words can be confusing. For Chinese that is not easy to translate, you can modify it according to your understanding. **After translation, be sure to check the basic grammar with a tool, such as pasting your English text into Word to see if there are any basic grammatical errors **. + +2、**All content should be subject to the `master` branch**, because the` english` branch is only for translation and will not update the article. So if you find that an article in the `master` does not exist or conflicts in the` english` branch, translate it based on the md file in the `master` branch. Do n’t forget to add the relevant picture folder to` english `Branch. + +3、**Boldness and other information need to be retained, while encouraging the expansion of your own knowledge**, adding references, adding important knowledge points in bold or using English (or other languages) unique forms of expression to express certain ideas. + +4. For pictures, Chinese characters are rarely included. If it does not affect understanding, such as the public number watermark in the lower right corner of the picture, you do not need to modify it.**If the Chinese character involves algorithmic understanding, you need to modify the picture together**, erase the Chinese character and replace it with English, or if there are few Chinese characters, add the corresponding English beside the Chinese characters. **For some pictures that describe the topic**, they are all the pictures I took on the Chinese version of LeetCode. You can go to the English version of LeetCode to find the corresponding topic screenshot replacement. If you do n’t know which question, you can leave a message in the issue and I will give You look for. The original Chinese md file needs to be deleted. + +5. **Keep the original directory structure, but the names of files and folders should be changed to English**. The name of the md file should be modified to the appropriate English according to the content of the specific article (files should not include spaces), the pictures cited in the article The path sometimes also contains Chinese, you need to change the folder containing the picture to appropriate English. **After the translation is complete, you need to delete the md file in the original Chinese**. If you added an English version of the picture, you should also delete the original Chinese picture. + +6. **Only process the articles (and related pictures) agreed in the issue, and do not move any other content**, otherwise conflicts will easily occur when you submit results to the main repository in the future. If there is a conflict, you need to find a way to use Git tools to resolve the version conflict between the local repository and the main repository before you can submit a pull request. It is very important to practice using Git. + +In fact, none of the algorithmic questions we brush up have any special English words, and many of the crooked nuts are not necessarily English. Google Translator's translation of articles with a bit of terms (stack, queue, etc.) is very poor, and even the code translates for you, so don't be afraid to turn it bravely, we will gradually improve in iterations ~ + +PS: Again, don't modify the articles other than those agreed in the issue, so that your pr will not conflict. Committing your branch also requires committing to the `english` branch. Do not submit any changes to the` master` branch. + +**Become a contributor!** diff --git a/workflow.png b/workflow.png deleted file mode 100644 index 69a7499c9b..0000000000 Binary files a/workflow.png and /dev/null differ diff --git "a/\347\277\273\350\257\221\347\272\246\345\256\232\357\274\210\345\277\205\350\257\273\357\274\211.md" "b/\347\277\273\350\257\221\347\272\246\345\256\232\357\274\210\345\277\205\350\257\273\357\274\211.md" index 438286d5f6..975c728589 100644 --- "a/\347\277\273\350\257\221\347\272\246\345\256\232\357\274\210\345\277\205\350\257\273\357\274\211.md" +++ "b/\347\277\273\350\257\221\347\272\246\345\256\232\357\274\210\345\277\205\350\257\273\357\274\211.md" @@ -1 +1,31 @@ -参见 [README.md](README.md) \ No newline at end of file +感谢各位老铁前来参与翻译! + +请 clone 最新的 `english` 分支,请查看 [已关闭的 pull request](https://github.com/labuladong/fucking-algorithm/pulls?q=is%3Apr+is%3Aclosed) 确保你准备翻译的文章暂时没有英文版本。 + +翻译完成后,请删除文末的公众号二维码。对于第一个提交的翻译版本,你可以在文章开头的**一级标题下方**添加作者和翻译者: + +**Translator: [YourName](https://github.com/YourName)** + +**Author: [labuladong](https://github.com/labuladong)** + +你的链接可以指向任何你希望的地方。 + +### 翻译约定 + +1、翻译首先要通顺,符合英文的语法,对基本的专业术语应该做到正确地使用,诸如 Queue, stack, binary tree 等,这种词语用错会让人很迷惑。对于不容易翻译出来的中文,可以按你的理解修改。翻译完成后一定要用工具检查一下基本语法,**比如将你的英文文本粘贴到 Word 中,查看是否有基本的语法错误**。 + +2、**所有内容应以 `master` 分支为准**,因为 `english` 分支仅作为翻译,不会更新文章。所以如果你发现 `master` 中的某一篇文章在 `english` 分支中不存在或有冲突,以 `master` 分支中的 md 文件为准进行翻译,别忘了把相关图片文件夹加入 `english` 分支。 + +3、**加粗等信息需要保留,同时鼓励扩展自己的知识**,增加参考文献,将重要知识点添加粗体或使用英语(或其他语言)特有的表达形式来表达某些思想。 + +4、对于图片,很少包含汉字,如果不影响理解,比如图片右下角的公众号水印,就不必修改了。**如果汉字涉及算法理解,需要把图片一同修改了**,把汉字抹掉换成英文,或者汉字比较少的话,在汉字旁添加对应英文。**对于一些描述题目的图片**,都是我在中文版 LeetCode 上截的图,你可以去英文版 LeetCode 上寻找对应题目截图替换,如果不知道是哪一题,可以在 issue 留言我给你找。 + +5、**保持原有的目录结构,但文件和文件夹的名称应改为英文**,md 文件的名称根据具体文章内容修改成恰当的英文(文件名不要带空格),文章引用的图片路径有时也会包含中文,需要你将装有该图片的文件夹改成适当的英文。**翻译完成后需要删除原中文的 md 文件**,如增加了英文版图片,也应该把中文原版的图片删除。 + +6、**只处理在 issue 中约定的文章(和相关的图片),不要动其他任何的内容**,否则后续你对主仓库提交成果的时候,容易出现冲突。如果出现冲突,你需要先想办法使用 Git 工具解决本地仓库和主仓库的版本冲突才能提交 pull request,练习 Git 的使用是非常重要的。 + +其实咱们刷的算法题都没有什么特别生僻的英文单词,而且很多歪果仁母语也不一定是英文。Google Translator 翻译带点术语(栈、队列这种)的文章效果很差,甚至代码都给你翻译,所以不要害怕,勇敢地翻就行了,我们会在一次次迭代中慢慢变好的~ + +PS:另外再强调一下,不要修改 issue 中约定的之外的文章,这样你的 pr 就不会产生冲突。提交你的分支也需要提交到 `english` 分支,不要向 `master` 分支提交任何修改。 + +**Become a contributor, 奥利给**! \ No newline at end of file