近來有小夥伴問我:刷leetcode真的有用嗎,以爲收益很小,越刷越迷茫了...面試
誠然每一個人刷題的目的不同,233醬還不是爲了能水幾篇文章...算法
固然不止。我以爲刷題是一件有意思的事,就像小貓小狗咬本身尾巴,玩弄的不亦樂乎。比喻可能不太恰當,是有種沉迷小遊戲的感受。但是在艱難打野的過程當中,咱們不要忘了,最重要的是:瞭解每種技能包的特色,適合解決的問題和場景。在特定實戰場景下可以使用特定的技能包,自創技能包。這纔是武功的至高境界。sql
裝X結束,淺談開始。。數組
數據結構是指:一種數據組織、管理和存儲的格式,它能夠幫助咱們實現對數據高效的訪問和修改。瀏覽器
數據結構 = 數據元素 + 元素之間的結構。緩存
若是說數據結構是造大樓的骨架,算法就是具體的造樓流程。流程不一樣,效率資源不一樣。我會二者結合簡單探討下他們的特色和應用。安全
常見的數據結構可分爲:線性結構、樹形結構 和 圖狀結構。微信
常見的算法有:遞歸、排序、二分查找、搜索、哈希算法、貪心算法、分治算法、回溯算法、動態規劃、字符串匹配算法等。網絡
本文從 線性數據結構、遞歸 和 排序算法 談起。數據結構
線性結構:是指數據排成像一條線同樣的結構。每一個元素結點最多對應一個前驅結點和一個後繼結點。如數組, 鏈表,棧 ,隊列等。
數組是是由相同類型的元素(element)的集合所組成的數據結構,分配一塊連續的內存來存儲。利用元素的下標位置能夠計算出該元素對應的存儲地址。
優勢:
分配基於連續內存,是一種天生的索引結構,查詢修改元素的效率O(1)。同時能夠藉助 CPU 的緩存機制,預讀數組中的數據,因此訪問效率更高。
缺點:
數組的索引優勢也是它的缺點,由於它的索引是基於一塊連續內存元素存儲的位置下標決定的,增刪arr[i]時間複雜度O(n),須要總體移動數組arr[i-n-1]的位置。此外,分配大數組會佔用較大的內存。
可經過如下方式避免元素拷貝和佔用大的開銷:
1.懶刪除:刪除時只標記元素被刪除,並不真正的執行刪除。當數組總體內存不夠用時,再執行真正的刪除。
2.分塊思想:將一大塊內存分爲n個小塊,以 小塊 爲單位進行數組內存的拷貝。如Mysql的InnoDB引擎中每一個Buffer Pool實例由若干個chunk組成,實際內存申請操做以chunk爲單位。
3.縮容:曾經面試阿里時,就讓設計了一個縮容版的HashMap。浪費可恥,節約光榮。
4.鏈表。
鏈表的存在就是爲了解決數組的增刪複雜耗時,內存佔用較大的問題。它並不須要一塊連續的內存空間,它經過 指針 將一組零散的內存塊串聯起來。根據指針的不一樣,有單鏈表,雙向鏈表,循環鏈表之分。
優勢:
增刪arr[i]時間複雜度O(1),使用鏈表自己沒有大小的限制,自然地支持動態擴容。
缺點:
沒有「索引」,查詢時間複雜度O(n)。須要維護指針,更佔內存。同時內存不連續,容易形成內存碎片。
能夠看出:數組和鏈表是相互補充的一對數據結構。那怎麼彌補鏈表的不足呢?
內存這塊是很差解決,這是由 指針 決定的。關於索引,沒索引就幫它建索引好了:
1.結合hash表,記錄鏈表每一個結點的位置。
2.鏈表長度拉的過長時,考慮跳錶,紅黑樹這類數據結構。(別慌,後面會講~)
應用場景:
數組和鏈表的運用很普遍,他們是構成 數據結構的基礎。如棧,隊列,集合等等。
棧是一種受限制的線性數據結構。元素只能夠在棧頂被訪問。 符合先進後出的First-In-Last-Out的訪問方式。
用數組實現的叫順序棧,用鏈表實現的叫鏈式棧。可能有人會有疑問:我用數組鏈表在頭尾兩端可伸可縮,爲毛要用只能在頭部操做的棧結構呢?
這種FILO的結構固然是隻適用於FILO的場景。若是咱們將數組/鏈表這種結構封裝爲棧,就能夠只使用其pop/push的API,屏蔽掉實現細節。
應用場景:
1.編輯器的redo/undo操做。
2.瀏覽器的前進/後退操做。
3.編譯器的括號匹配校驗
4.數學計算中的表達式求值
5.函數調用
6.遞歸
7.字符串反轉...
隊列也是一種受限制的線性數據結構。 符合先進先出的First-In-First-Out 的訪問方式。一樣,用數組實現的隊列叫做順序隊列,用鏈表實現的隊列叫做鏈式隊列。
根據頭尾指針和操做的不一樣,隊列又可分爲雙端隊列,循環隊列,阻塞隊列,併發隊列。
存在併發的場景下,隊列存取元素的臨界區爲 隊列空時的取操做 和 隊列滿時的存操做。保證併發下的隊列存取安全爲阻塞隊列 和 併發隊列。二者的區別在於 同步資源的粒度不一樣。
enqueue
、dequeue
的安全,鎖粒度較大。如Java JUC包中的阻塞隊列。enqueue
、dequeue
的安全。enqueue
、dequeue
的安全。CAS的同步代價小較小,因此稱爲:無鎖併發隊列。如Disruptor框架中Ring Buffer就運用了這點。PS: 不少框架對線程池的需求都替換成了Disruptor來實現,如Log4j二、Canal等。應用場景:
隊列的做用其實就是現實中的排隊。當資源不足時,經過「隊列」 這種結構來實現排隊的效果。用於:
1.任務調度存在的地方:CPU/磁盤/線程池/任務調度框架...
2.兩個進程中數據的傳遞:如pipe/file IO/IO Buffer...
3.生產者消費者場景中..
4.LRU
遞歸 是一種算法求解的編碼實現。應用於如深度優先搜索、前中後序二叉樹遍歷(挖坑後面講~)等。由於接下來的排序算法如:歸併/快排 可經過遞歸來實現,因此咱們先看一下書寫遞歸的步驟。熟悉了遞歸的思想,它實際上是一種書寫簡單的編碼方式。
只要問題知足如下三點,都可使用遞歸來進行求解:
1.一個問題的解能夠分解爲幾個子問題的解
2.問題和子問題之間,除了數據規模不一樣,求解思路徹底同樣
3.存在遞歸終止條件
寫遞歸代碼的關鍵在於:找到如何將大問題分解爲小問題的規律,而且基於此寫出遞推公式,而後再敲定終止條件,最後將遞推公式和終止條件翻譯成代碼。
由於人並不擅長處理這種程序,因此在寫遞歸代碼的時候,咱們能夠自動屏蔽掉遞歸的執行過程。咱們只須要告訴程序:遞推公式 和 終止條件 是什麼,事情就會便Easy~
使用時的注意項:
1.stackoverflow: 實際函數調用層次太深,就會有系統棧或者虛擬機棧空間溢出的風險。
2.子問題的重複計算: 前面文章我有講 動態規劃經過避免子問題的重複計算可以下降時間複雜度。一種方式就是經過 遞歸 + 備忘錄(子問題的解保存起來)來解決。
233醬學習的第一個算法就是冒泡排序算法,我想很多碼農都經歷過被 「幾大排序算法」 支配的恐懼。
排序是咱們在項目工程中常常遇到的一個場景,如TopK,中位數問題等。有序 和 無序 的數據集合之間的差異在於 前者 「逆序對」 爲0.
小貼士: 若是i < j,且a[i] > a[j], 就稱爲一個逆序對,如 1,7,3,5 中的 <7,5>
反之則爲有序對,如<1,3>
不一樣的排序算法消滅逆序對的方式不同,體如今時空複雜度,排序方式,穩定性,適用場景等方面不一樣。
我先放一張網上排序算法的圖:
選擇排序算法時,咱們應該考慮算法的執行效率,內存消耗,穩定性等這些因素。
PS:如下內容主要引用極客時間王爭大佬的《數據結構和算法之美》課程,233能力有限,默默給大佬打廣告&點贊。
對於要排序的原始數據,數據的有序度不一樣,對排序的執行效率是有影響的。好比接近有序的待排序數據 插入排序的時間複雜度接近O(n)。咱們須要瞭解排序算法在不一樣數據下的性能表現。
2.時間複雜度的係數、常數 、低階
在對小規模的數據排序時,如10個,100個,1000個。須要把係數、常數、低階也考慮進來,才能選擇合適的排序算法。
3.比較次數和交換(或移動)次數
基於比較的排序算法的執行過程,會涉及兩種操做,一種是元素比較大小,另外一種是元素交換或移動。因此,若是咱們在分析排序算法的執行效率的時候,應該把比較次數和交換(或移動)次數也考慮進去。
上圖中有一列排序方式:原地排序(In-place) 和 外部排序(Out-place)。前者是指空間複雜度爲O(1)的排序算法,不須要在外部開闢內存空間。後者須要額外開闢空間來存儲中間狀態。前者的好處在於能夠藉助 CPU 的緩存機制,訪問效率更高。這是一個重要的考量因素。
小貼士:快排的空間複雜度爲是由於它的實現是遞歸調用的, 每次函數調用中只使用了常數的空間,所以空間複雜度等於遞歸深度O(logn)。
穩定性是指:待排序的序列中存在值相等的元素,通過排序以後,相等元素之間原有的前後順序是不變的。
爲啥要考慮排序算法的穩定性呢?
這是由於實際場景中的待排序的對象 排序維度多是多個。好比咱們對訂單先按照金額排序,再按照下單時間排序。實現簡單的思路爲:先給訂單按照 下單時間排序,再按照金額排序。 穩定性的排序算法可以保證 金額相同的兩個對象,在排序以後的下單順序不變。
下面主要從數據規模上討論這些排序算法的應用。
小規模數據排序
在小規模數據下,冒泡排序/選擇排序/插入排序實現較爲簡單,排除不穩定的選擇排序,插入排序(可類比打撲克抓牌時的排序思想)比冒泡排序(最大元素依次日後冒)好在交換次數少,小規模下排序效率更高。
此外當待排序序列的有序度比較高時,插入排序也好過歸併/快排這類O(nlogn)的效率。因此在小規模數據場景下,適合用插入排序。
大規模內存級數據排序
大規模數據排序適合考慮O(nlogn)級別的排序算法,這裏討論 歸併排序 和 快速排序。
歸併排序的思想是 分治 思想。將整個無序序列的排序 劃分爲 無序小序列的排序問題。子序列有序了,再合併起來有序的子序列,總體就排好序了。
歸併排序是外部排序。每次合併操做都須要申請額外的內存空間,在合併完成以後,臨時開闢的內存空間就被釋放掉了。在任意時刻,CPU 只會有一個函數在執行,也就只會有一個臨時的內存空間在使用。臨時內存空間最大也不會超過 n 個數據的大小,因此空間複雜度是 O(n)。
快速排序利用的也是 分治 思想。局部有序 最終 全局有序。它使用一個分區點數據(pivort)將元素分爲< pivort
,=pivort
,>pivort
三個部分。而後在< pivort
和 >pivort
這兩部分繼續遞歸處理,最終排序完成。
若是 快排合理的選擇pivort,多路指針參與分區能夠避免時間複雜度的惡化。並且快排是原地排序,相比歸併排序是外部排序,空間複雜度較高O(n)。快排的應用更爲普遍。
Java中Arrays.sort
是混合排序,實現策略分爲兩種:
Case1. 存儲的數據類型是基本數據類型
使用的是快排,在數據量很小的時候,使用的插入排序;
Case2. 存儲的數據類型是Object
使用的是歸併排序,在數據量很小的時候,使用的也是插入排序
大規模外部數據排序
當數據規模很大時,咱們並不能把全部數據都加載到內存。這時候能夠考慮時間複雜度是 O(n) 的外部排序算法:桶排序、計數排序、基數排序。外部排序是指數據存儲在外部磁盤中。
這裏時間複雜度之因此低是由於:這三個算法是非基於比較的排序算法,都不涉及元素之間的比較操做。
桶排序是按照某種屬性將元素分配到全局有序的子桶內,再在子桶內作局部排序。當子桶個數劃分的足夠大時,時間複雜度就接近O(n) 。
計數排序實際上是桶排序的一種特殊狀況。當要排序的 n 個數據,所處的範圍並不大的時候,好比最大值是 k,咱們就能夠把數據劃分紅 k 個桶。每一個桶內的數據值都是相同的,省掉了桶內排序的時間。
基數排序是根據每一位來排序,基數排序對要排序的數據是有要求的,須要能夠分割出獨立的「位」來比較,並且位之間有遞進的關係,若是 a 數據的高位比 b 數據大,那剩下的低位就不用比較了。除此以外,每一位的數據範圍不能太大,要能夠用線性排序算法來排序,不然,基數排序的時間複雜度就沒法作到 O(n) 了。
感謝您的閱讀,文中有錯誤或者太過淺顯的部分還請幫233醬指出&補充。以爲有收穫就四連「關注,點贊,在看,轉發」支持下233醬吧。
另外,關注公衆號【碼農知識點】加我微信好友,歡迎加入個人刷題技術討論羣。和233共同成長進步~
參考資料:
[1].維基百科