非科班進大廠必備算法

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基礎數據結構的融合是成爲龐大系統的基石。比如Redis中的跳躍表,數據庫索引B+樹等,只有對基礎的數據結構足夠的熟悉才能更容易去理解稍微複雜的結構,就彷彿我們闖關打怪一樣,一步一步解鎖直到結局。今天想和大家一起分享的是常見數據結構以及面試中的高頻手撕算法題,一定要去手動寫這些代碼,可說百分之七八十都是這些題,一定要好好掌握。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/21/213e02bd3366f83a8171b29ff15f041d.png","alt":"高頻手撕算法合集","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1 數據結構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鏈表屬於數據結構中的"},{"type":"text","marks":[{"type":"strong"}],"text":"線性結構"},{"type":"text","text":"的一種,我們先看看什麼是數據結構"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數據結構是:結構的定義+結構的操作"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"想必大夥兒應該玩兒過拼圖,拼圖之前我們先看看說明書,看看包含幾個部分,然後對這些部分進行拼裝,隨後拼好候進行組合直到完成。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼數據結構中的結構定義是這個數據結構長什麼樣子,有些什麼性質?結構的操作意思是這個結構可以支持什麼操作,但是不管你怎麼的操作,不能"},{"type":"text","marks":[{"type":"strong"}],"text":"破壞"},{"type":"text","text":"了它的結構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2 鏈表定義"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個鏈表是由1個或者多個節點組成,每個節點包含兩個信息,一個是"},{"type":"text","marks":[{"type":"strong"}],"text":"數據信息"},{"type":"text","text":",用來"},{"type":"text","marks":[{"type":"strong"}],"text":"存儲數據"},{"type":"text","text":",一個是"},{"type":"text","marks":[{"type":"strong"}],"text":"地址信息"},{"type":"text","text":",用來存儲下個節點的"},{"type":"text","marks":[{"type":"strong"}],"text":"地址"},{"type":"text","text":"。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9c/9c36d7135b0828bd772e7c234c285006.png","alt":"鏈表節點","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鏈表結構由一個個節點組成,我們不需要對結構做任何改變,只需要按照需求修改鏈表結構中的"},{"type":"text","marks":[{"type":"strong"}],"text":"數據域"},{"type":"text","text":"即可。從上圖我們知道此事數據域類型爲整型763,指針域爲0x56432,這個地址正好是第二個節點的地址,所以這兩個節點在邏輯上是有個"},{"type":"text","marks":[{"type":"strong"}],"text":"指向關係"},{"type":"text","text":",也是通過這種方式將兩個節點進行了關聯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二個節點中的指針域爲"},{"type":"text","marks":[{"type":"strong"}],"text":"0x0"},{"type":"text","text":",這是一個特殊的地址,叫做"},{"type":"text","marks":[{"type":"strong"}],"text":"空地址"},{"type":"text","text":",指向空地址意味着它是這個鏈表結構的最後一個節點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那在代碼中是什麼樣子呢"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"struct Node {\n\tint data;\n\tstruct Node *next;\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個結構很清晰,數據域根據我們的需求而定,想存整型就改成整型,想存字符串就寫字符串。而指針域用來維護整個鏈表結構,一般來說直接用即可,如果需要內存中的鏈表結構,一定要修改節點內部next指針域中存儲的地址值"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3 鏈表操作"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說到鏈表結構,我們習慣性的和數組聯繫在一起。只是數組結構在"},{"type":"text","marks":[{"type":"strong"}],"text":"內存中是連續"},{"type":"text","text":"的,而鏈表結構因爲指針域的存在,每個節點在內存中存儲的位置未必連續。下面我們按照數組的方式給鏈表也編個號。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d2/d2c54743b256e7265380a9d3b16b4e56.png","alt":"單鏈表","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們定義一個向鏈表插入節點的函數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"```c++"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"struct Node "},{"type":"text","marks":[{"type":"italic"}],"text":"insert(struct Node "},{"type":"text","text":"head, int ind, struct Node *a);"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"\n- 第一個參數爲待操作的鏈表的頭結點地址,也就是第一個節點的地址\n- 第二個參數爲插入位置\n- 第三個參數爲指針變量,指向要插入的新節點\n\n簡單的說就是向 head 指向的鏈表的 ind 位置插入一個由 a 指向的節點,返回值爲插入新節點後的**表頭地址**。爲什麼要返回它呢?因爲我們插入的節點很可能在頭部,此時就會改變鏈表的結構且改變頭結點地址,所以需要返回。\n\n那麼我們插入一個元素,顯然會改變鏈表的節點,操作方法爲修改鏈表節點的 next 指針域即可,那麼爲了插入成功,我們需要修改哪些節點呢?\n\n首先是讓 ind - 1 位置的節點指向 a 節點,然後是 a 節點指向原 ind 位置的節點,也就是說,涉及到兩個節點的 next 指針域的值的修改,一個是 ind - 1 位置的節點,一個是 a 節點自身。我們就可以先找到 ind - 1 位置的節點,然後再進行相關操作即可。\n\n```c++\nstruct Node *insert(struct Node *head, int ind, struct Node *a) {\n\tstruct Node ret, *p = &ret;\n\tret.next = head;\n\t// 從虛擬頭節點開始向後走 ind 步\n\twhile (ind--) p = p->next;\n\t// 完成節點的插入操作\n\ta->next = p->next;\n\tp->next = a;\n\t// 返回真正的鏈表頭節點地址\n\treturn ret.next;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏非常關心且非常重要的是"},{"type":"text","marks":[{"type":"strong"}],"text":"虛擬節點"},{"type":"text","text":"。我們爲什麼引入虛擬節點?是爲了讓我們的插入操作"},{"type":"text","marks":[{"type":"strong"}],"text":"統一化"},{"type":"text","text":"?什麼是統一化?舉個例子,假設我們現在是在第5個位置插入元素,我們自然需要從頭遍歷到第四個節點,確定了第四個節點後,修改相關的next指針域,也就是如果我們想插入到 nid 位,就需要從頭節點向後移動 ind-1 步,那麼如果插入的位置爲0呢?我們總不能走-1步吧,所以這個時候我們只好對ind=0的情況進行單獨的判斷了,這樣明顯是不完美了,所以我們爲了統一ind在等於0和不等於0時的情況,引入虛擬節點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ok,我們看看是不是方便了。增加了虛擬節點,如果插入第5個位置,我們只需要向後移動5位,如果插入到0號位置,向後移動0步即可,即p指針指向虛擬節點不懂,直接將新的節點插入到虛擬頭結點後面完事兒。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/08/08b3f52ad8ca1369834afd026f57f0ff.png","alt":"虛擬節點","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好勒,這裏出現了第一個重要的技巧。在我們插入鏈表節點的時候,加上虛擬節點是個實用技巧。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼我們看看插入和刪除的操作動態以及實現方式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4 案例"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"案例1"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們看個題吧,定義一個快樂數,什麼是快樂數,所謂快樂數即通過有限次變換後等於1 的數字。怎麼變換呢,給出一個非1的數字,然後出去位數,求各個位數的平方和,得到數字A,假設A不死1,那就繼續對元素A的每一位進行平方和,得到數字B。。。。知道最後能夠=1"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,一開始的數字是 19,經過變換規則 ,得到數字 82;因爲不是 1 ,所以接着做變換,就是 ,再做一次變換 ,最後一次做變換,得到了 1 以後,停止"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個題的難點不是判斷數是不是快樂數,而是如何判斷一個數"},{"type":"text","marks":[{"type":"strong"}],"text":"不是"},{"type":"text","text":"快樂數,如果不是快樂數,說明沒有辦法通過有限的次數到達數字1,那麼到底是 經過多少次呢?1k次,10w次?很難確定上限。在說這個問題之前我們先看幾個"},{"type":"text","marks":[{"type":"strong"}],"text":"高頻鏈表"},{"type":"text","text":"練習題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"例題1 用數組判斷鏈表中是否有環"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面我們介紹了最後一個節點指向空,可是你有沒有想過如果鏈表的最後一個節點不是空地址而是指向鏈表中的一個節點,這不就是環了?"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/04/0457b70f0bc57e1990cbf4bc199b53cd.png","alt":"鏈表環","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上圖所示,節點8指向了3,這樣形成了3,4,5,6,7,8的環狀結構,此時使用指針遍歷鏈表將永無止境。那通過什麼辦法判斷是否有環呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用數組標記的方法。記錄"},{"type":"text","marks":[{"type":"strong"}],"text":"出現過"},{"type":"text","text":"的節點信息,每次遍歷新節點就去數組查看記錄,這樣的時間複雜度不給力。經過第一個節點,需要在數組查找0次,第2個節點,數組查找1次,第i個節點,在數組查找i-1次,直到遍歷第n+1個節點,查找的總次數爲(n + 1) * n / 2,這樣時間複雜度爲O(n^2)。太慢了,給我優化"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"快慢指針法"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"AB兩位同學跑步,A同學速度快,B同學速度慢,他們並不知道跑道是環形的,如果是環形,跑得快的,在足夠的時間終究會從速度慢的B同學經過,形成相遇的情況。如果不是環形,速度快的先到重點,不會相遇---快慢指針法。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/76/760a4c685095c2d4d6b162e63415b468.png","alt":"快慢指針","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這裏,我們將鏈表當做跑道,跑道上兩個指針,指針A每次走兩步,指針B每次走兩步,如果快的指針先跑到終點註定沒有環,如果兩指針相遇則有環。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"```c++"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"int hasCycle(struct Node *head) {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"\tif (head == NULL) return 0;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" // p 是慢指針,q 是快指針"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" struct Node "},{"type":"text","marks":[{"type":"italic"}],"text":"p = head, "},{"type":"text","text":"q = head;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"\t// 每次循環,p 走1步,q 走2步"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"\tdo {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" p = p->next;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" q = q->next;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" if (q == NULL) return 0;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"\t\t\tq = q->next;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"\t\t} while (p != q && q);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"\treturn p == q;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"}"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"\n## 3 二分查找初探\n\n> 說到二分查找,這裏就有個笑話了。\n\n小孫同學去圖書館借書,一次性了借了40本書,出圖書館的時候報警了,不知道哪一本書沒有**消磁**,然後把書放在地上,準備一本本嘗試。\n\n女生的操作被旁邊的阿姨看見了,阿姨說你這樣操作多慢啊,我來教你。於是將樹分爲兩摞,拿出第一luo過一下安檢,安檢機器想了,於是阿姨將這摞書分成兩部分,拿出一部分繼續嘗試,就這樣,阿姨每次減少一半,沒幾次就找到了沒有消磁的書。阿姨嘚瑟的來一句:小姑涼,這就是書中的二分查找算法,你這還得好好學習哇,第二天,圖書館發現丟了39本書。哈哈哈哈\n\n## 4 二分查找基礎\n\n> 最簡單的二分算法即在一個有序數組中,查找一個數字X是否存在。注意有序性。那麼如何在數組中查找一個數\n\n- 從頭到尾一個一個查找,找到即有數字x\n- 二分算法即通過確定一個區間,然後查找區間的一半和x比較,如果比x大則在x前半段查找。如果比x小則在後半段查找,只需要log2n的比較即可確定結果。\n\n\n![二分初探](https://img-blog.csdnimg.cn/20200906221123472.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0wxNTUxOTU0MzgzNw==,size_1,color_FFFFFF,t_70#pic_center)\n\n圖中呢,我們以查找 17 這個數字爲例,L 和 R 所圈定的,就是當前的**查找區間**,一開始 L= 0,R = 6,mid 所指向的就是數組的中間位置,根據 L 和 R 計算得到 mid 的值是 3。查看數組第 3 位的值是 12,比待查找值 17 要小,說明如果 17 在這個有序數組中,那它一定在 mid 所指向位置的後面,而 mid 本身所指向的數字已經確定不是 17 了,所以下一次我們可以將查找區間,定位到 mid + 1 到 R,也就是將 L 調整到 mid + 1 (即數組第 4\n位)的位置。\n\n**1 第一種小白寫法**\n\n```c++\nint BinarySerach(vector& nums,int n, int target) {\n int left = 0, right = n-1;\n while (left <= right) {\n int mid = (left+right)/2;\n if (nums[mid] == target) return mid;\n else if (nums[mid] < target) left = mid + 1;\n else right = mid-1;\n }\n return -1;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"面試官發話了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4e/4ebf81aea872ad1877d05991b0560e67.jpeg","alt":"img","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"strong"}],"text":"方法二優化版"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果right和left比較的時候,兩者之和可能溢出。那麼改進的方法是mid=left+(right-left)/2.還可以"},{"type":"text","marks":[{"type":"strong"}],"text":"繼續優化"},{"type":"text","text":",我們將除以2這種操作轉換爲位運算mid=left+((right-left)>>1)."}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/47/4715f8f7359446d35cd9e5a41352acd3.gif","alt":"在這裏插入圖片描述","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"哪有這麼簡單的事兒,大多數的筆試面試中可能會出現下面的幾種情況。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"二分的各種變種"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏主要是看看原始數組有重複數的情況。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/22/2276bef8cecf33f3bd1ef92ad803e6b0.png","alt":"二分","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"1 查找第一個值等於給定值的情況(查找元素7)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"思路"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先7與中間值a[4]比較,發現小於7,於是在5到9中繼續查找,中間a[7]=7,但是這個數7不是第一次出現的。那麼我們檢查這個值的前面是不是等於7,如果等於7,說明目前這個值不是第一次出現的7,此時更新rihgt=mid-1。ok我們看看代碼"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"```c++"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"int BinarySerach(vector& nums, int n,int target) {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" int left = 0, right = n-1;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" while (left <= right) {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" int mid = left+((right-left)>>1);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" if (nums[mid]>value)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" right=mid-1;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" } else if(nums[mid] 假設nums[mid]這個值已經是最後一個元素了,那麼它肯定是要找到最後一個值。如果nums[mid]的下一個不等於value,那說明nums[mid]就是我們需要找到最後一個等於給定值的值。\n\n```c++\nint BinarySerach(vector& nums, int n,int target) {\n int left = 0, right = n-1;\n while (left <= right) {\n int mid = left+((right-left)>>1);\n if (nums[mid]>value)\n {\n right=mid-1;\n } else if(nums[mid]& nums, int n,int target) {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" int left = 0, right = n-1;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" while (left <= right) {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" int mid = left+((right-left)>>1);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" if (nums[mid]>value)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" right=mid-1;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" } else if(nums[mid]& nums, int n,int target) {\n int left = 0, right = n-1;\n while (left <= right) {\n int mid = left+((right-left)>>1);\n if (nums[mid]>=value)\n {\n if(mid==0||nums[mid-1]& nums, int n,int target) {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" int left = 0, right = n-1;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" while (left <= right) {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" int mid = left+((right-left)>>1);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" if (nums[mid]>value)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" right=mid-1;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" }else"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" if(mid==n-1||(nums[mid+1]>value))"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" return mid;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" }else"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" {"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" left=mid+1;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" return -1;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"\n## 4 隊列\n\n> 例子:滑動窗口最大值\n\n**隊列回憶:**\n\n> 火車站買票應該都經歷過,窗口小姐姐每次服務排在最前面的那個人,買完票則從頭部離開,後面人往前一步接替離開的人繼續購票,這就是典型的隊列結構。\n\n計算機中的隊列和其類似,先到先得,先入先出,每個元素從尾部入隊,從頭部處理完出隊\n\n![隊列定義](https://img-blog.csdnimg.cn/20200906220007386.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0wxNTUxOTU0MzgzNw==,size_1,color_FFFFFF,t_70#pic_center)\n\n**單調隊列**\n\n> 假設將學生從高年級到低年級排列,隨着時間的推移,高年級同學從隊列頭部畢業,低年級從尾部進入。大部分學校都有校隊,假設小林高三,我高二,小七高一,小林畢業接班的是我,我畢業,很可能就是小七接班,而當我進隊的那一刻,小七即使進的早但是戰鬥力沒我高,所以小七是永遠沒計劃被選中啦。所以,縱觀全隊,不僅有着隊列的性質,也有着單調的性質,所以就是單調隊列。\n\n**爲什麼需要單調隊列**\n\n> 比較明顯的作用是,用來維護隊列處理順序中的區間最大值。\n\n**高頻面試題----滑動窗口最大值**\n\n> 滑動窗口沒向後滑動一位,就有一個元素從隊首出隊,同時也會有個元素從隊尾入隊。這個題需要求區間的最大值:意味着需要維護在隊列處理順序中的區間最大值,直接上代碼附上註釋\n\n```c\n#define MAX_N 1000\nint q[MAX_N + 5], head, tail;\nvoid interval_max_number(int *a, int n, int m) {\n\thead = tail = 0;\n\tfor (int i = 0; i < n; i++) {\n // a[i] 入隊,將違反單調性的從隊列 q 中踢出\n while (head < tail && a[q[tail - 1]] < a[i]) tail--;\n q[tail++] = i; // i 入隊\n // 判斷隊列頭部元素是否出了窗口範圍\n if (i - m == q[head]) head++;\n // 輸出區間內最大值\n if (i + 1 >= m) {\n printf(\"interval(%d, %d)\", i - m + 1, i);\n printf(\" = %d\\n\", a[q[head]]);\n }\n\t}\t\n\treturn ;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"5 棧與單調棧"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"棧結構對應於隊列,可以將棧想象爲一個只有單出口的羽毛球筒,羽毛球只能從單一的入口放入和取出。假設我們將1,2,3三個球放進球桶,如果取出來此時就是3,2,1。性質就很明顯了,先進後出的結構"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"棧結構本身維護的是一種完全包含的關係。這種包含關係在函數之間的運行體現的玲離盡致,也就是一種包含關係,如果主函數調用函數B,那麼函數B一定會在主函數結束之前結束。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"單調棧"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時應該瞭解了棧和隊列,那麼我問你,你覺得棧和隊列最大的區別是啥?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能毫不猶豫的可以回答棧是"},{"type":"text","marks":[{"type":"strong"}],"text":"先進後出"},{"type":"text","text":",隊列是"},{"type":"text","marks":[{"type":"strong"}],"text":"先進先出"},{"type":"text","text":"。ok,那我再問你,堵住了出口的單調隊列和棧有什麼區別?這是不是就沒什麼區別了,單調隊列爲了維護其"},{"type":"text","marks":[{"type":"strong"}],"text":"單調性"},{"type":"text","text":",在入隊的時候會將違反單調性的元素彈出去,這就相當於棧的同一段進出,是的,堵住出口的單調隊列就是我們現在要說的單調棧,目前以單調遞減棧爲例"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/13/1369cb98896b2cb091ea67c5ef3cd878.png","alt":"單調棧","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當序列中的12號元素入棧以後,此時單調棧有4個元素,從棧底到棧頂分別爲23,18,15,9,按照原始序列爲2 5 9 12。此時我們關注12號元素和9號元素的關係。如果12號元素入棧,爲了保證棧的單調遞減性,最終放在9號上面,此時我們雖然不是第十個元素和十一號元素值多少,但是這兩個元素的值一定是比9號元素小,這就是單調棧的性質。所以,單調隊列是用來維護區最值的高效結構,單調棧呢是維護最近大於或小於的高效結構。下面看個例子"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"題目:判斷括號序列是否合法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"示例 合法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"({})"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[]([]){()}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"示例 非合法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"([)]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(((){}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public boolean isValid(String s) {\n Stack stack = new Stack<>();\n Map map = new HashMap<>();\n char[] chars = s.toCharArray();\n map.put(')','(');\n map.put('}','{');\n map.put(']','[');\n for(int i=0;i < s.length();i++){\n if(!map.containsKey(chars[i])) {\n \t//爲左括號時直接入棧\n stack.push(chars[i]);\n }else{\n \t//爲右括號時,如果棧爲空或者棧頂與該括號類型不匹配返回false\n if(stack.empty() || map.get(chars[i]) != stack.pop()){\n return false;\n }\n }\n }\n //字符串遍歷完畢後,如果棧爲空返回true,反之返回false\n return stack.empty();\n }\n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"6 遞推套路"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在分享遞推之前,先和大家分享與之緊密的數學原理:容斥原理"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在計數問題中,爲了保證計數的準確程度,通常會保證兩個問題,"},{"type":"text","marks":[{"type":"strong"}],"text":"第一個問題"},{"type":"text","text":"是沒有重複,"},{"type":"text","marks":[{"type":"strong"}],"text":"第二個問題"},{"type":"text","text":"是沒有遺漏。這兩個問題相對來說,第二點比較容易做到。比如對某地區進行爆炸式轟炸,爲了保證炸的覆蓋面,足夠多的炸彈即可,但是如果保障一塊土地只能炸一次就比較難搞了。那麼容斥原理就是解決這個問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"容斥原理是什麼?"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先不考慮重疊的情況,先將所有對象數目計算出來,然後將重複計算的排斥出去,是的,計算的結果不僅不遺漏也不重複。簡單的說就是在計算的過程中,如果加多了就減去多的部分,如果減多了就加回來一部分,直到不多不少。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們看一個兔子繁殖問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設有一片草原上,莫名其妙來了一隻外星兔子,這種外星兔子呢,第一個月的時候是幼體,第二個月成長爲成體,從第三個月開始,成體兔子每個月都會產生出一隻克隆體的幼體兔子,而且這種兔子不會衰老,一旦成體以後,就會一直生下去。按照這種情況,請你計算出第 n 個月,草原上有多"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"少只兔子?"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時給出前面6個月的情況"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/44/447d1d6b1c87f58d455cc0c9456a281d.png","alt":"六個月兔子情況","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上圖我們可以發現,從第一個月到第六個月,草原上的兔子數量分別爲1,1,2,3,5,8"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第六個月共有8只兔子,其中包含5只成兔,3只幼兔,爲什麼是5只成兔,因爲第六個月的兔子數量等於第五個月的兔子總數,六個月的3只幼兔是等於第四個月的兔子數量"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d5/d5a2595a3f921f61194e6ca68915a665.png","alt":"後三個月情況","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結論就比較清晰了:從第三個月開始,第n個月的兔子數量等於該月的成兔數量與幼兔數量之和,也就是等於第n-1個月的兔子數量與第n-2兔子數量之和。這種根據前面的數量來推後面的數量的情況叫做遞推,那麼遞推算法套路通常是怎麼樣呢"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"確定遞推的狀態,多畫圖前面幾步"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推導遞推公式"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"程序的編寫"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們根據三步走的方式來闡釋解決兔子的這個問題"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"f(n)表示n個月兔子的數量"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞推公式(第一個月合第二個月兔子的數量爲1,到了第三個月即等於前面兩個月之和)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/14/14779e0569568df786688ed985ac5eeb.png","alt":"遞推公式","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"案例2 湊錢幣問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用 1 元、2 元、5 元、10 元、20 元、50 元和 100"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"元湊成 1000 元錢,總共有多少種方案"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"確定遞推狀態,需要分析自變量與因變量,自變量兩個分別爲幣種種類和拼湊的錢幣數量,因變量1個爲方案總數,因此我們的狀態定義爲f(i,j),i種錢幣,拼湊j元錢的方案總數。比如f \\[3][10]即使用三種錢幣,湊出10元的方案總數"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我們不使用第三種錢幣,那麼此時等價於使用前兩種錢幣拼湊10元錢的方案總數,即f\\[2][10]。如果使用至少1張5塊錢,那麼我們在這些方案中去掉一張5元錢,剩下的方案數爲f\\[3][5],所以此時的遞推公式爲f\\[3][10] = f\\[2][10] + f\\[3][5]。這只是一般情況,假設我們沒有使用第i種錢幣,拼湊j元的方案爲f(i-1,j),代表使用前i-1種錢幣的方案總數。剩下的使用了第i中錢幣,由於都存在第i錢幣1張,假設第i種錢幣的面額爲val[i],那麼此時我們的前i種錢幣,湊j-val[i]的錢數,此時方案總數爲f(i,j-val[i]);所以公式爲f(i,j)=f(i-1,j)+f(i,j-val[i])"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2b/2b10cbe5e5a7cc3e9b02249e91de4293.png","alt":"推理","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"7 動態規劃"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"動態規劃通常簡稱DP(dynamic programming),如果按照問題類型來劃分,將分爲線性DP、區間DP,數位DP等等,每當說到動態規劃就會想最優子結構,重疊子問題等等,這些詞彙苦澀難懂,不要慌,再難的問題也是建立在基礎問題上,逐步拆分,這也是動態規劃的思想,相信通過下面動態規劃四步走的方式,加上習題的練習,一定會讓你對動態規劃有個新的理解。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"四個步驟分爲:狀態定義,狀態轉移方程,正確性的證明和實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"狀態定義"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實上面說遞推的時候就已經有所涉及狀態定義,通常在推導的過程中,如果感覺推不下去了,很有可能就是我們的狀態定義出現了問題。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一個狀態:dp\\[i][j]代表從起始點到(I,j)路徑的最大值"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二個狀態:dp\\[i][j]代表從底邊的某個點出發,到達(i,j)路徑的最大值"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"狀態轉移方程"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的兩種狀態定義對應這裏兩個轉移方向。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/21/21a9374756c810a5ff607b9fcf379872.png","alt":"狀態轉移過程","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上圖所示,我們想要求得dp\\[i][j],需要知道dp|[i-1]|[j-1]和dp\\[i-1][j]的值。因爲只有(i - 1, j - 1) 和 (i - 1, j) 這兩個點,才能能走到 (i, j)此時的狀態轉移方程爲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一種狀態轉移方程dp\\[i][j] = max(dp\\[i - 1][j - 1], dp\\[i - 1][j]) + val\\[i][j]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二冊中狀態轉移方程dp\\[i][j] = max(dp\\[i + 1][j], dp\\[i + 1][j + 1]) + val\\[i][j]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從這裏可以知道我們的狀態定義不一樣,我們的轉移方程就很不一樣吧"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"正確性證明"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數學歸納法通常採用三步走的方式,常用的正確性證明方法爲數學歸納法。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一步,第一個階段所有dp值可以輕鬆獲得,也就是初始化dp\\[1][1],等於val\\[1][1]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二步,假設如果第i-1階段的所有狀態值都正確得到,那麼根據狀態方程dp\\[i][j]=max(dp\\[i - 1][j], dp\\[i - 1][j + 1]) + val[i][j] 來說,此時就可以計算得到第i階段中的素有狀態值"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第三步:得出結論,所有的狀態值計算正確"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們繼續分析動態規劃問題中的0/1揹包問題,通常分爲三類,0/1揹包問題,完全揹包問題和多重揹包問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"0/1揹包問題是另外兩種揹包問題的基礎,簡單描述一下,假設有個揹包,載重上限爲W,此時有n個物品,第i個物品的重量是wi,價值爲vi,那麼在不超過揹包重量上限的前提下,能獲得的最大物品價值總和?同樣我們採用四步走的方式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"狀態定義"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先分析揹包問題中的自變量和因變量,其中因變量比較好確定,就是所求最大價值總和,自變量呢,在此自變量爲物品種類和揹包承重上限,因爲這兩者會影響價值總和的最大值,所以我們設置一個二維狀態。dp\\[i][j]代表使用前i個物品,揹包最大載重爲j的情況下最大價值總和。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"狀態方程"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說白了就是找映射函數,dp\\[i][j]的表達式。我們將dp\\[i][j]分爲兩大類,第一類是不選擇第i個物品最大價值和,第二類爲選擇了第i個物品的最大價值和。然後在兩者中選擇最大值就是價值更大的方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果選擇第i個物品,此時的最大價值爲dp\\[i-1][j-wi]+vi,既然選擇了第i個商品,那麼就需要留出一個位置,那麼此時對於剩餘的i-1個商品的載重空間就只剩下j-wi了,此時i-1個物品選擇的最大價值和爲dp\\[i-1][j-wi],然後加上vi就是當前獲得最大價值和。所以轉移方程爲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"dp[i][j] = max(dp\\[i - 1][j], dp\\[i - 1][j - w[i]] + v[i])"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"正確性證明"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 首先dp\\[0][j]=0,意味着沒有物品的時候,無論揹包限重多少,能夠得到的最大價值和都是0,所以k0爭取"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其次,假設我們已經獲取i-1個物品的價值最大值,所有dp\\[i-1]的值,那麼根據狀態方程,我們能知道所有dp\\[i]的值"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後兩步聯合,整個求解對於任意dp\\[i][j]成立"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現"}]}]}]},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"#define MAX_V 10000\n\n#define MAX_N 100\n\nint v[MAXN + 5], w[MAXN + 5];\n\nint dp[MAXN + 5][MAXV + 5];\n\nint get_dp(int n, int W) {\n\n // 初始化 dp[0] 階段\n\n for (int i = 0; i <= W; i++) dp[0][i] = 0;\n\n // 假設 dp[i - 1] 成立,計算得到 dp[i]\n\n // 狀態轉移過程,i 代表物品,j 代表揹包限重\n\n for (int i = 1; i <= n; i++) {\n\n \t \tfor (int j = 0; j <= W; j++) {\n\n // 不選擇第 i 種物品時的最大值\n\n dp[i][j] = dp[i - 1][j];\n\n // 與選擇第 i 種物品的最大值作比較,並更新\n\n if (j >= w[i] && dp[i][j] < dp[i - 1][j - w[i]] + v[i]) {\n\n \tdp[i][j] = dp[i - 1][j - w[i]] + v[i];\n\n \t\t }\n\n \t }\n\n\t}\n\n\treturn dp[n][W];\n\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"8 貪心"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實我們大學學習的好幾種應用都是採用了貪心的算法啊,比如Huffman Coding,Prim最小生成樹等"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先來看一道之前美團的一道筆試題--跳一跳"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有n個盒子排成一行,每個盒子上面有一個數字a[i],表示最多能向右跳a[i]個盒子;小林站在左邊第一個盒子,請問能否到達最右邊的盒子?比如說:[1, 2, 3, 0, 4] 可以到達第5個盒子;[3, 2, 1, 0, 4] 無法到達第5個盒子;"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"思路:自然而然的想法,儘可能的往右邊跳,看最後能夠到達,從第一個盒子開始從右遍歷,對於每個經過的盒子,不斷地更新maxRight值。那麼貪心算法的思考過程通常是怎麼樣的?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類似於動態規劃,"},{"type":"text","marks":[{"type":"strong"}],"text":"大事化小,小事化了"},{"type":"text","text":"。所謂大事化小,將大的問題,找到和子問題的重複部分,將複雜問題拆分爲小的問題。小事化了,通過對小事的打磨找到較爲核心的策略。上例子"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分糖果問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們有 m 個糖果和 n 個孩子。我們現在要把糖果分給這些孩子喫,但是糖果少,孩子多(m\n#include \n#include \nusing namespace std;\n\n\n/*\n解題思路:\n遍歷兩邊,首先每個人得一塊糖,第一遍從左到右,若當前點比前一個點高就比前者多一塊。\n這樣保證了在一個方向上滿足了要求。第二遍從右往左,若左右兩點,左側高於右側,但\n左側的糖果數不多於右側,則左側糖果數等於右側糖果數+1,這就保證了另一個方向上滿足要求。\n\n最後將各個位置的糖果數累加起來就可以了。\n*/\n\n\nint candyCount(vector&rating) {\n\n int res = 0;\n //孩子總數\n int n = rating.size();\n\n //糖果集合\n vector candy(n, 1);\n //從左往右遍歷\n for (int i = 0;i < n - 1;i++) {\n if (rating[i + 1] > rating[i])candy[i + 1] = candy[i] + 1;\n }\n //從右往左\n for (int i = n - 1;i > 0;i--) {\n if (rating[i - 1] > rating[i] && candy[i - 1] <= candy[i])\n \tcandy[i - 1] = candy[i] + 1;\n }\n\n //累加結果\n for (auto a : candy) {\n res += a;\n }\n\n return res;\n}\n//測試函數\nint main() {\n\n vector rating{1,3,2,1,4,5,2};\n cout << candyCount(rating) << endl;\n return 0;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"嘮嗑"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也許當我們工作了以後,才知道如果有一小段時間可以安心的思考一道算法題目是多麼美好的事兒。算法的技巧文章太多太多,可謂眼花繚亂,以致於自己都不知道看什麼資料。看太多的書真不如\"刀槍實戰\",直接去Leetcode或者其他算法平臺練習,不過練習真的需要章法,畢竟咋們時間也有限,在我看來分類刷比較好,最好可以從樹來刷題,咋們不能靠數量取勝,需要讀題思考,寫代碼,看看是否有其他方法來優化,以致於讓自己更深入的瞭解這代碼邏輯以及優化原理。好了,今天就到這了,如果需要算法學習資料,可以私信我啦。求個"},{"type":"text","marks":[{"type":"strong"}],"text":"點贊"},{"type":"text","text":",明天更美好。"}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7a/7a5447dfabaccd031aed1981596ce396.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章