《Java數據結構和演算法》- 鏈表

来源:https://www.cnblogs.com/fireway/archive/2018/06/04/9131546.html
-Advertisement-
Play Games

Q: 為什麼要引入鏈表的概念?它是解決什麼問題的? A: 數組作為數據存儲結構有一定的缺陷,在無序數組中,搜索是低效的;而在有序數組中,插入效率又很低;不管在哪一個數組中刪除效率都很低;況且一個數組創建後,它的大小是不可改變的。 A: 在本篇中,我們將學習一種新的數據結構 —— 鏈表,它可以解決上面 ...


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

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

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

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。

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)時間。
  • 雙向鏈表中,每個結點包含對前一個結點的引用,同時有對後一個結點的引用。
  • 雙向鏈表允許反向遍歷,並可以從表尾刪除。
  • 迭代器是一個引用,它被封裝在類對象中,這個引用指向相關聯的鏈表中的結點。
  • 迭代器方法允許使用者沿鏈表移動迭代器,並訪問當前所指的結點。
  • 能用迭代器遍歷鏈表,在選定的結點上執行某些操作。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 彈性佈局flex是一個幾年前的CSS屬性了,說它解放了一部分生產力不為過。至少解放了不少CSS佈局相關的面試題 :) 之前網上流行的各種XX佈局,什麼postion: absolute+margin,float+padding,各種都可以使用flex來取代之。 早兩年在使用的時候,還是會擔心有相容性 ...
  • lesson1 <!DOCTYPE html><html lang="en,zh"><!-- 告訴搜索引擎爬蟲,我們的網站是關於什麼內容的 --><head> <meta charset="utf-8"> <meta content="服裝" name="keywords"> <meta conte ...
  • 下麵是幾種方法的公用部分(右自適應也是一樣的,換一下方向) html: css: 1. 左脫離文檔流,右margin 1.1 左float 1.2 左absolute 2. flex佈局(左定寬,右flex設置為1,自動伸展,此處註意flex相容性,可以通postcss等後處理器自動加上首碼) 效果 ...
  • 單例設計模式 目的:讓類創建對象,在系統中只有唯一的實例,讓每一次創建的對象返回的記憶體地址都是相同的。 __new__方法 使用類名創建對象時,python解釋器首先會調用__new__方法為對象分配空間 __new__方法是有object基類提供的靜態方法,主要作用有兩個: 為記憶體中的對象分配空間 ...
  • 最近想系統學習並使用一下boost的asio非同步網路庫,所以需要編譯boost庫使用,下麵簡單介紹如何編譯。 ...
  • 今天介紹的專題是servlet和jsp,屬於web開發中的基礎,先來實際操作一下servlet,創建你的第一個web小項目。 1、新建一個maven項目。 滑鼠右鍵單擊eclipse左邊項目欄的空白區,選擇第一個new,在出現的下拉中選擇other,在新彈出的框中選擇maven,點開maven選擇m ...
  • 很多寫C/C++的人都知道“記憶體對齊”的概念以及規則,但不一定對他有很深入的瞭解。這篇文章試著從硬體到C++語言、更徹底地講一下C++的記憶體對齊。 ...
  • Java開源生鮮電商平臺-銷售管理設計與架構(源碼可下載) 說明:在Java開源生鮮電商平臺中,銷售人員我們稱為跟餐飲店老闆溝通與下載APP的一類地推人員。(所謂地推指的就是一個一個上門拜訪。) 由於銷售人員有以下幾類特性: 1. 時間隨意性,他們並不類似技術或者性質人員,需要天天呆在辦公室,他們是 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...