引言
? ? ? ? 鏈表是一種線性表結構,其特點是非順序存儲,可充分利用碎片化的空閑內存。鏈表中的元素稱為結點,結點通過邏輯指針維護相鄰結點間的位置關系。 鏈表作為基礎數據結構之一有多種形式:單鏈表、循環鏈表、雙向鏈表及雙向循環鏈表、跳表等。
從上圖可見,鏈表中每個結點包含數據域和指向下一個結點的指針域。比較特別的是開頭和結尾兩個結點。我們習慣上把開頭結點叫頭結點,結尾結點叫尾結點。其中頭結點中有個頭指針,頭指針中存有鏈表結構遍歷的基地址,是鏈表能夠遍歷的前提。而尾結點指向下一個結點的指針域是空地址NULL。
單鏈表常規操作:
鏈表反轉:就是將原結點的next指針都統一指向其前驅結點。
? ? ? ? a、雙指針迭代法:指針法就是利用兩個指針不斷的將結點的next指針設置為其前驅結點和不斷重置前驅結點的過程。
? ? ? ? 第一次迭代,currentNode結點指向第一個結點,其prev指針此時指向null。
? ? ? ? 第二次迭代,currentNode指針后移一位,指向結點2;prev指針也后移一位指向結點3。
? ? ? ? 第三次迭代,currentNode指針再次后移一位,指向結點1;prev指針后移一位指向結點2,終止迭代。
? ? ? ? b、頭插法:新建頭結點(next指向null的結點),迭代鏈表結點,將每個結點設置為頭結點的next,將頭結點之前的next結點,設置為當前結點的next。簡單來說,頭插法就是將新增的結點插入到頭結點和其后續結點之間,從而組成一個新的鏈表。
? ? ? ? 步驟1:新建頭結點(next指針指向null的結點),首次迭代原始鏈表,并將原始鏈表第一個結點3,插入到頭結點。
? ? ? ? 步驟2:迭代原始鏈表,將第二個結點2,插入到頭結點和結點3之間。
? ? ? ? 步驟3:迭代原始鏈表,將第三個結點1,插入到頭結點和結點2之間,迭代結束。
? ? ? ? 步驟4:反轉后的新鏈表
????????c、遞歸法:遞歸實現可以看做是逆向的迭代調用,或者看做是嵌套函數調用。
鏈表中環的檢測:只要后續結點與某個前驅結點一致,說明鏈表中有環存在。
????????快慢雙指針法:如果有環存在,則兩個指針勢必會相遇;否則快指針遍歷結束。如下圖,鏈表中有環存在,快慢指針在第三次鏈表迭代后相遇。
刪除倒數第N個節點:將鏈表反轉,刪除正數第n個元素后再次反轉。
代碼實現,GitHub地址:https://github.com/SolodanceMagicq/algorithm_practice/blob/master/src/algo/java/linkedlist/LinkedListAlgo.java
鏈表與數組性能大比拼
核心應用
如何基于鏈表實現緩存LRU淘汰策略?
? ? ? ?維護一個有序單鏈表,越早訪問的元素越靠近鏈表的尾部存儲,當有元素訪問時,順序遍歷鏈表。????
? ? ? ?1、如果當前元素已經在緩存鏈表中了,則先遍歷得到當前元素對應的結點,再將其從緩存中刪除,然后將其插入到鏈表頭部。
? ? ? ?2、如果當前元素不在緩存鏈表中,則分兩種情況:
? ? ? ? ? ? ?a、 當前緩存未滿,直接將當前元素插入到鏈表頭部。
? ? ? ? ? ? ?b 、當前緩存已滿,則鏈表尾結點刪除,將當前元素插入到鏈表頭部。
代碼實現,GitHub地址:https://github.com/SolodanceMagicq/algorithm_practice/blob/master/src/algo/java/linkedlist/LRUSingleLinkedList.java
JDK的集合類LinkedList
? ? ? ?LinkeList是一個基于雙向鏈表實現的集合,同時實現了雙端隊列,使其具備了隊列和棧的特點。采用空間換時間思想,減少元素搜索的時間復雜度。實現細節詳見《ArrayList&LinkedList源碼分析》http://www.lxweimin.com/p/8f94e7694886
Redis中的有序集合
? ? ? ?Redis的有序集合即是跳表實現。跳表是一個由鏈表+多級索引組成的動態數據結構。其查詢、插入、刪除操作的時間復雜度均為O(logn),可實現精確查找、按區間查找等特性。比較值得一提的是傳統意義上二分查找只能應用在順序存儲結構(數組)的有序數列上,像鏈表這樣的非順序的存儲結構,通過多級索引巧妙的利用空間換性能(空間復雜度為O(n))實現了在查詢操作上,類似數組的二分查找的時間復雜度O(logn)。
? ? ? ?跳表索引動態更新:作為一種動態數據結構跳表是通過“隨機函數”來維護索引與原始鏈表大小的平衡,也就是說如果鏈表中結點多了,索引結點也相應的增多,避免復雜度退化,以及查找、插入、刪除的性能下降。相對于紅黑樹、AVL樹這樣的平衡二叉樹,是通過左右旋的方式保持左右子樹的大小平衡。
流水線設計模式
? ? ? ? 設計思路:將一整件事情,分成多個流水線Line x ,每條流水線是由A、B、C、D、E等多個結點構成的一條單鏈表。其中鏈表的結點也稱為流水線的處理環節,且前一個結點的輸出是后一個結點的輸入。流水線這個復合結點,負責設置并啟動線上的各環節結點,處理任務。這樣整個事情,橫向上切分為三條線并發處理,縱向又被切分為N個階段。每個階段由N(流水線條數)個相同的處理任務,將各階段的任務統一交由一個固定線程數的線程池處理。雖然橫向上每個流水線的執行效率固定(各環節均要等待其前面的環節數據到來才開始處理自己的事情);但如果流水線數量多,各環節同類任務的處理效率就有了提升。因此流水線模式適合將大的問題拆分成大量流水線處理數據的場景,如果流水線數量少,各環節同類任務少,則整體性能體現不明顯。
總結
? ? ? ?鏈表和數組都是非常基礎的線性表結構。
? ? ? ?數組支持隨機訪問特性,可以充分利用CPU緩存行,通過尋址公式來高效訪問內存中的元素時間復雜度是O(1),但由于數組要維護物理存儲空間連續性,在大量插入、刪除元素后遷移數據,時間復雜度是O(n)。
? ? ? ?鏈表不需要維護連續的內存空間,可以充分利用碎片化的空閑內存,但失去了對CPU緩存的支持。通過邏輯指保持相鄰結點間的位置關系,因此只能從頭遍歷查找定位元素,時間復雜度是O(n)。但刪除和插入操作不需移動元素,只是改變相鄰元素的指針,時間復雜度是O(1)。
? ? ? ?隨著對鏈表的擴展及基于鏈表衍生的新的數據結構,像雙向鏈表、跳表等,都是采用空間換性能的方式,來降低鏈表的查詢復雜度。