diff --git a/.github/ISSUE_TEMPLATE/others.md b/.github/ISSUE_TEMPLATE/others.md new file mode 100644 index 0000000000..f03e25ec2c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/others.md @@ -0,0 +1,10 @@ +--- +name: 其他issue +about: 我还有一些其他的建议/问题 +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/translate.md b/.github/ISSUE_TEMPLATE/translate.md index f633c9fd9a..16da565b4a 100644 --- a/.github/ISSUE_TEMPLATE/translate.md +++ b/.github/ISSUE_TEMPLATE/translate.md @@ -2,14 +2,16 @@ name: 参与翻译 about: 我想参与仓库中文章的翻译工作 title: 'translate ' -labels: documentation +labels: translate assignees: '' --- @@ -20,4 +22,10 @@ assignees: '' 我准备将它翻译成:**英文** -**预计 X 天内翻译完成**,若由于种种原因没有完成,如果你愿意,你可以接替我的工作翻译这篇文章。 +**预计 X 天内翻译完成**,若由于种种原因,规定时间已过但此 issue 还未提交 pull request,则此 issue 自动失效。如果你愿意,你可以新开一个 issue 接替我的工作翻译这篇文章。 + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 4e8962e4ff..5ee9e6b31a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ 感谢各位老铁前来参与翻译! -请查看最新的 `english` 分支,保证你准备翻译的文章暂时没有英文版本。 +请 clone 最新的 `english` 分支,请查看 [已关闭的 pull request](https://github.com/labuladong/fucking-algorithm/pulls?q=is%3Apr+is%3Aclosed) 确保你准备翻译的文章暂时没有英文版本。 翻译完成后,请删除文末的公众号二维码。对于第一个提交的翻译版本,你可以在文章开头的**一级标题下方**添加作者和翻译者: @@ -12,22 +12,20 @@ ### 翻译约定 -1、翻译尽可能表达中文原意,你对基本的专业术语应该做到正确地使用,诸如 Queue, stack, binary tree 等词语。这种词语用错会让人很迷惑。基本的语法不能出错,建议搜索一些英语语法检查的在线网站,**或者最简单的,翻译后将你的文本粘贴到 Word 中,查看是否有基本的语法错误**。 +1、翻译首先要通顺,符合英文的语法,对基本的专业术语应该做到正确地使用,诸如 Queue, stack, binary tree 等,这种词语用错会让人很迷惑。对于不容易翻译出来的中文,可以按你的理解修改。翻译完成后一定要用工具检查一下基本语法,**比如将你的英文文本粘贴到 Word 中,查看是否有基本的语法错误**。 2、**所有内容应以 `master` 分支为准**,因为 `english` 分支仅作为翻译,不会更新文章。所以如果你发现 `master` 中的某一篇文章在 `english` 分支中不存在或有冲突,以 `master` 分支中的 md 文件为准进行翻译,别忘了把相关图片文件夹加入 `english` 分支。 3、**加粗等信息需要保留,同时鼓励扩展自己的知识**,增加参考文献,将重要知识点添加粗体或使用英语(或其他语言)特有的表达形式来表达某些思想。 -4、对于图片,很少包含汉字,如果不影响理解,比如图片右下角的公众号水印,就不必修改了。**如果汉字涉及算法理解,需要把图片一同修改了**,把汉字抹掉换成英文,或者汉字比较少的话,在汉字旁添加对应英文。**对于一些描述题目的图片**,都是我在中文版 LeetCode 上截的图,你可以去英文版 LeetCode 上寻找对应题目截图替换,如果不知道是哪一题,可以要求我给你找。 +4、对于图片,很少包含汉字,如果不影响理解,比如图片右下角的公众号水印,就不必修改了。**如果汉字涉及算法理解,需要把图片一同修改了**,把汉字抹掉换成英文,或者汉字比较少的话,在汉字旁添加对应英文。**对于一些描述题目的图片**,都是我在中文版 LeetCode 上截的图,你可以去英文版 LeetCode 上寻找对应题目截图替换,如果不知道是哪一题,可以在 issue 留言我给你找。原中文 md 文件需要删除。 -5、**保持原有的目录结构,但文件和文件夹的名称应改为英文**,md 文件的名称根据具体文章内容修改成恰当的英文,文章引用的图片路径有时也会包含中文,需要你将装有该图片的文件夹改成适当的英文。 +5、**保持原有的目录结构,但文件和文件夹的名称应改为英文**,md 文件的名称根据具体文章内容修改成恰当的英文(文件名不要带空格),文章引用的图片路径有时也会包含中文,需要你将装有该图片的文件夹改成适当的英文。**翻译完成后需要删除原中文的 md 文件**,如增加了英文版图片,也应该把中文原版的图片删除。 6、**只处理在 issue 中约定的文章(和相关的图片),不要动其他任何的内容**,否则后续你对主仓库提交成果的时候,容易出现冲突。如果出现冲突,你需要先想办法使用 Git 工具解决本地仓库和主仓库的版本冲突才能提交 pull request,练习 Git 的使用是非常重要的。 其实咱们刷的算法题都没有什么特别生僻的英文单词,而且很多歪果仁母语也不一定是英文。Google Translator 翻译带点术语(栈、队列这种)的文章效果很差,甚至代码都给你翻译,所以不要害怕,勇敢地翻就行了,我们会在一次次迭代中慢慢变好的~ -Github 具体的协作方式我在仓库置顶的 [issue](https://github.com/labuladong/fucking-algorithm/issues/9) 中有写,很简单,如果你之前没有协作过,这次翻译工作更是你对新事物的尝试和学习机会。不要害怕,Git 仓库的一切都是可以恢复的,不会出现操作不熟练而搞砸,**放开手干就完事儿了**。 - -PS:另外再强调一下,不要修改 issue 中约定的之外的文章,以便你的仓库后续合并进主仓库,提交你的分支也需要提交到 `english` 分支,翻译工作不要向 `master` 分支提交任何修改。 +PS:另外再强调一下,不要修改 issue 中约定的之外的文章,这样你的 pr 就不会产生冲突。提交你的分支也需要提交到 `english` 分支,不要向 `master` 分支提交任何修改。 **Become a contributor, 奥利给**! diff --git a/common_knowledge/SessionAndCookie.md b/common_knowledge/SessionAndCookie.md new file mode 100644 index 0000000000..b1b0650297 --- /dev/null +++ b/common_knowledge/SessionAndCookie.md @@ -0,0 +1,135 @@ +# Session and Cookie + +**Translator: [YourName](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. + +### Introduction to session and cookie + +Cookies come 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. + +### 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 算法详解」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/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..9102703f5c --- /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](../think_like_computer/学习数据结构和算法的高效方法.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/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/\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/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/SuperEggDrop(Advanced).md b/dynamic_programming/SuperEggDrop(Advanced).md new file mode 100644 index 0000000000..0c004ab41a --- /dev/null +++ b/dynamic_programming/SuperEggDrop(Advanced).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/Throwing Eggs in High Buildings.md b/dynamic_programming/Throwing Eggs in High Buildings.md new file mode 100644 index 0000000000..545e5834eb --- /dev/null +++ b/dynamic_programming/Throwing Eggs in High Buildings.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/\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/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/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/The Longest Palindromic Substring.md b/interview/TheLongestPalindromicSubstring.md similarity index 100% rename from interview/The Longest Palindromic Substring.md rename to interview/TheLongestPalindromicSubstring.md 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/\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\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/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/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/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/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/11.png b/pictures/double_pointer/11.png similarity index 100% rename from pictures/11.png rename to pictures/double_pointer/11.png diff --git a/pictures/22.png b/pictures/double_pointer/22.png similarity index 100% rename from pictures/22.png rename to pictures/double_pointer/22.png diff --git a/pictures/33.png b/pictures/double_pointer/33.png similarity index 100% rename from pictures/33.png rename to pictures/double_pointer/33.png 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/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/\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/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/\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/\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/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/Detailed Binary Search.md b/think_like_computer/Detailed Binary Search.md new file mode 100644 index 0000000000..6309372973 --- /dev/null +++ b/think_like_computer/Detailed Binary Search.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/Details about Backtracking.md b/think_like_computer/DetailsaboutBacktracking.md similarity index 100% rename from think_like_computer/Details about Backtracking.md rename to think_like_computer/DetailsaboutBacktracking.md 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/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/double_pointer.md b/think_like_computer/double_pointer.md new file mode 100644 index 0000000000..c078209b3e --- /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/22.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/33.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/44.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/\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\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/\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/workflow.png b/workflow.png deleted file mode 100644 index 69a7499c9b..0000000000 Binary files a/workflow.png and /dev/null differ