Java數據結構和算法 - 鏈表

Q: 爲何要引入鏈表的概念?它是解決什麼問題的?

A: 數組做爲數據存儲結構有必定的缺陷,在無序數組中,搜索是低效的;而在有序數組中,插入效率又很低;無論在哪個數組中刪除效率都很低;何況一個數組建立後,它的大小是不可改變的。java

A: 在本篇中,咱們將學習一種新的數據結構 —— 鏈表,它能夠解決上面的一些問題,鏈表多是繼數組以後第二種使用最普遍的通用存儲結構了。git

Q: 結點?

A: 在鏈表中,每一個數據項都被包含在「結點」中,可使用Node, 或者Entry等名詞來表示結點,本篇使用Entry來表示。每一個Entry對象中包含一個對下一個結點引用的字段(一般叫作next),單鏈表中每一個結點的結構圖以下: 
 
定義單鏈表結點的類定義以下:程序員

class Entry<E> { E mElement; Entry<E> mNext; public Entry(E element, Entry<E> next) { mElement = element; mNext = next; } } 

Q: 單鏈表?

A: 構成鏈表的結點只有一個指向後繼結點的指針域。算法

Q: 單鏈表的Java實現?

A: 示例:SingleLinkedList.java數組

A: LinkedList類只包含一個數據項mHeader,叫作表頭:即對鏈表中第一個節點的引用。它是惟一的鏈表須要維護的永久信息,用以定位全部其餘的連接點。從mHeader出發,沿着鏈表經過每一個結點的mNext字段,就能夠找到其餘的結點。數據結構

A: addFirst()方法 —— 做用是在表頭插入一個新結點。 學習

A: removeFirst()方法 —— 是addFirst()方法的逆操做,它經過把mHeader從新指向第二個結點,斷開了和第一個結點的鏈接。 
 
在C++中,從鏈表取下一個結點後,須要考慮如何釋放這個結點。它仍然在內存中的某個地方,可是如今沒有任何指針指向它,將如何處理它呢?在Java中,垃圾回收(GC)將在將來的某個時刻銷燬它,如今這不是程序員操心的工做。 
注意,removeFirst()方法假定鏈表不是空的,所以調用它以前,應該首先調用empty()方法覈實這一點。測試

Q: 如何查找和刪除指定的結點?

A: indexOf(Object)方法 —— 返回此列表中首次出現的指定元素的索引,若是此列表中不包含該元素,則返回 -1。spa

get(int)方法 —— 返回此列表中指定位置處的元素。設計

A: remove(Object) —— 今後列表中移除首次出現的指定元素(若是存在)。 
先搜索要刪除的結點,若是找到了,必須把前一個結點和後一個結點連起來,知道前一個結點的惟一方法就是擁有一個對它的引用previous(每當current變量賦值爲current.next以前,先把previous變量賦值爲current)。 

A: 示例: SingleLinkedList.java

Q: 雙端鏈表?

A: 雙端鏈表(double-ended list )是在上邊的單鏈表基礎上加了一個表尾,即對最後一個結點的引用。以下圖: 

A: 對最後一個結點的引用容許像表頭同樣,在表尾直接插入一個結點。固然,仍然能夠在普通的單鏈表的表尾插入一個結點,方法是遍歷整個鏈表直到到達表尾,可是這種方法效率很低。

Q: 雙端鏈表的Java實現?

A: 示例: DoubleEndedList.java

A: DoubleEndedList有兩個項,header和tailer,一個指向鏈表中的第一個結點,另外一個指向最後一個結點。

A: 若是鏈表中只有一個結點,header和last都指向它。若是沒有結點,兩個都爲null值。

A: 若是鏈表只有一個結點,刪除時tailer必須被賦值爲null。

A: addLast()方法 —— 在表尾插入一個新結點。

Q: 鏈表的效率?

A: 在表頭插入和刪除速度很快,僅須要改變一兩個引用值,因此花費O(1)的時間。

A: 查找、刪除和在指定結點的前面插入都須要搜索鏈表中一半的結點,須要O(N)次比較,在數組中執行這些操做也須要O(N)次比較。可是鏈表仍然要快一些,由於插入和刪除結點時,鏈表不須要移動任何東西。

A: 鏈表比數組還有一個優勢是,鏈表須要多少內存就能夠用多少內存,不像數組在建立時大小就固定了。

A: 向量是一種可擴展的數組,它能夠經過可變長度解決這個問題,可是它常常只容許以固定的增量擴展(好比快要溢出的時候,就增長1倍的數組容量)。這個解決方案在內存使用效率上來講仍是要比鏈表低。

Q: 用鏈表實現的棧?

A: 示例:Stack.java

A: 棧的使用者不須要知道棧用的是鏈表仍是數組實現。 所以Stack類的測試用例在這兩個上是沒有分別的。

Q: 用鏈表實現的隊列?

A: 示例:Queue.java

A: 展現了一個用雙端鏈表實現的隊列。

Q: 何時應該使用鏈表而不是數組來實現棧和隊列呢?

A: 這一點要取決因而否能精準地預測棧或隊列須要容納的數據量。若是這一點不是很清楚的話,鏈表就比數組表現出更好的適用性。二者都很快,因此速度可能不是考慮的重點。

Q: 什麼是抽象數據類型(ADT)?

A: 簡單來講,它是一種考慮數據結構的方式:着重於它作了什麼,而忽略它是怎麼作的。

A: 棧和隊列都是ADT的例子,前面已經看到棧和隊列既能夠用數組實現,也可使用鏈表實現,而對於使用它們的用戶徹底不知道具體的實現細節(用戶不只不知道方法是怎樣運行,也不知道數據是如何存儲的)。

A: ADT的概念在軟件設計過程當中很重要,若是須要存儲數據,那麼就要從它的實際操做上開始考慮,好比,是存取最後一個插入的數據項?仍是第一個?是特定值的項?仍是在特定位置上的項?回答這些問題會引出ADT的定義。

A: 只有在完整定義ADT後,才應該考慮細節問題。

A: 經過從ADT規範中剔除實現的細節,能夠簡化設計過程,在將來的某個時刻,易於修改實現。若是用戶只接觸ADT接口,應該能夠在不「干擾」用戶代碼的狀況下修改接口的實現。

A: 固然,一旦設計好ADT,必須仔細選擇內部的數據結構,以使規定的操做的效率儘量高。例如隨機存取元素a,那麼用鏈表表示就不夠好,由於對鏈表來講,隨機訪問不是一個高效的操做,選擇數據會獲得更好的效果。

Q: 有序鏈表?

A: 在有序鏈表中,數據是按照關鍵值有序排列的,有序鏈表的刪除經常是隻限於刪除在表頭的最小(或最大)的節點。

A: 通常,在大多數須要使用有序數組的場合也可使用有序鏈表。有序鏈表的優點在於插入的速度,由於元素不須要移動,並且鏈表能夠隨時擴展所需內存,數組只能侷限於一個固定大小的內存。

A: 示例:SortedLinkedList.java

A: 當算法找到要插入的位置,用一般的方式插入數據項:把新節點的next字段指向下一個節點,而後把前一個結點的next字段指向新節點。然而,須要考慮一些特殊狀況:節點有可能插在表頭,或者表尾。

Q: 有序鏈表的效率?

A: 在有序鏈表插入或刪除某一項最多須要O(N)次比較(平均N/2),由於必須沿着鏈表一步一步走才能找到正確的位置。然而,能夠在O(1)的時間內找到或刪除最小值,由於它總在表頭。

A: 若是一個應用頻繁地存取最小項,且不須要快速地插入,那麼有序鏈表是一個有效的方案選擇,例如,優先級隊列能夠用有序鏈表來實現。

Q: 鏈表插入排序(List Insertion Sort)?

A: 有序鏈表能夠用於一種高效的排序機制。假設有一個無序數組,若是從這個數組中取出數據,而後一個一個地插入有序鏈表,它們自動地按照順序排列。而後把它們從有序鏈表刪除,從新放入數組,那麼數組就排好序了。

A: 本質上與基於數組的插入排序是同樣的,都是O(N2)的比較次數,只是說對於數組會有一半已存在的數據會涉及移動,至關於N2/4次移動,相比之下,鏈表只需2 * N次移動:一次是從數組到鏈表,一次是從鏈表到數組。

A: 不過鏈表插入有一個缺點:就是它要開闢差很少兩倍的空間。

A: 示例: LinkedListSort.java

Q: 雙向鏈表?

A: 雙向鏈表提供了這樣的能力,即容許向前遍歷,也容許向後遍歷整個鏈表,其中祕密在於它的每一個數據結點中都有兩個指針,分別指向直接後繼和直接前驅。

A: 雙向鏈表沒必要是雙端鏈表(保持一個對鏈表最後一個元素的引用),但這種方式是頗有用的。因此下面的示例將包含雙端的性質。

Q: 基於雙向鏈表的雙端鏈表的Java實現?

A: 示例:DoublyLinkedList.java

A: addFirst(E)方法:將指定元素插入此列表的開頭。 

A: addLast(E)方法:將指定元素添加到此列表的結尾。 

A: add(index, E)方法: 在此列表中指定的位置插入指定的元素。 

A: remove(Object o)方法: 今後列表中移除首次出現的指定元素(若是存在)。 

Q: 基於雙向鏈表的雙端隊列?

A: 雙向鏈表能夠用來做爲雙端隊列的基礎。在雙端隊列中,能夠從任何一頭插入和刪除,雙向鏈表提供了這個能力。

Q: 爲何要引入迭代器的概念?

A: ArrayList底層維護的是一個數組;LinkedList是鏈表結構的;HashSet依賴的是哈希表,每種容器都有本身特有的數據結構。由於容器的內部結構不一樣,不少時候可能不知道該怎樣去遍歷一個容器中的元素。因此爲了使對容器內元素的操做更爲簡單,Java引入了迭代器。

A: 把訪問邏輯從不一樣類型的集合類中抽取出來,從而避免向外部暴露集合的內部結構。
對於數組咱們使用的是下標來進行處理的:

for (int i = 0; i < array.length; i++) { System.out.println(array[i]); } 

對於鏈表,咱們從表頭開始遍歷:

    public void displayForward() { System.out.print("List (first-->last): ["); Entry<E> current = mHeader; while(current != null) { E e = current.mElement; System.out.print(e); if (current.mNext != null) { System.out.print(" "); } current = current.mNext; } System.out.print("]\n"); } 

A: 不一樣的集合會對應不一樣的遍歷方法,客戶端代碼沒法複用。在實際應用中如何將上面兩個集合整合是至關麻煩的。因此纔有Iterator,它老是用同一種邏輯來遍歷集合。使得客戶端自身不須要來維護集合的內部結構,全部的內部狀態都由Iterator來維護。客戶端不用直接和集合進行打交道,而是控制Iterator向它發送向前向後的指令,就能夠遍歷集合。

A: 迭代器模式就是提供一種方法對一個容器對象中的各個元素進行訪問,而又不暴露該對象容器的內部細節。

Q: 迭代器定義的接口?

A: 迭代器包含對數據結構中數據項的引用,而且用來遍歷這些結構的對象。下面是迭代器的接口定義:

public interface Iterator<E> { boolean hasNext(); E next(); void remove(); } 
public interface ListIterator<E> extends Iterator<E> { boolean hasPrevious(); E previous(); int nextIndex(); int previousIndex(); void set(E e); } 

A: 每一個容器的iterator()方法返回一個標準的Iterator實現。通常而言,Java中迭代器和鏈表以前的鏈接是經過把迭代器設爲鏈表的內部類來實現,而C++是"友元"來實現。

A: 以下圖顯示了指向鏈表的某個結點的兩個迭代器:

Q: JDK1.6的LinkedList的迭代器?

A: 迭代器類ListItr實現ListIterator接口,定義以下:

private class ListItr implements ListIterator<E> { } 

A: 示例:ListIteratorTestCase.java

Q: 迭代器指向哪裏?

A: 迭代器類的一個設計問題是決定在不一樣的操做後,迭代器應該指向哪裏。而JDK1.6中LinkedList.ListItr中的add()實現,next指針一直指向表頭,這裏假設調用的是iterator(),不指定下標。

Q: 本篇小結

  • 鏈表包含一個LinkedList對象和許多Entry對象。
  • next字段爲null意味着鏈表的結尾。
  • 在表頭插入結點須要把新結點的next字段指向原來的第一個結點,而後把header指向新結點。
  • 在表頭刪除結點要把header指向header.next。
  • 爲了遍歷鏈表,從header開始,而後從一個結點到下一個結點,方法是用每一個結點的next字段找到下一個結點。
  • 經過遍歷鏈表能夠找到擁有特定值的結點,一旦找到,能夠顯示、刪除或用其餘方式操縱該結點。
  • 新結點能夠插在某個特定值的結點的前面或後面,首先要遍歷找到這個結點。
  • 雙端鏈表在鏈表中維護一個指向最後一個結點的引用,它一般和header同樣,叫作tailer。
  • 雙端鏈表容許在表尾插入數據項。
  • 抽象數據類型是一種數據存儲類,不涉及它的實現。
  • 棧和隊列是ADT,它們既能夠用數組實現,也能夠用鏈表實現。
  • 有序鏈表中,結點按照關鍵字升序或降序排列。
  • 在有序鏈表中插入須要O(N) 的時間,由於必需要找到正確的插入點,最小值結點的刪除須要O(1)時間。
  • 雙向鏈表中,每一個結點包含對前一個結點的引用,同時有對後一個結點的引用。
  • 雙向鏈表容許反向遍歷,並能夠從表尾刪除。
  • 迭代器是一個引用,它被封裝在類對象中,這個引用指向相關聯的鏈表中的結點。
  • 迭代器方法容許使用者沿鏈表移動迭代器,並訪問當前所指的結點。
  • 能用迭代器遍歷鏈表,在選定的結點上執行某些操做。
相關文章
相關標籤/搜索