在前面實現的三種線性數據結構:動態數組、棧和隊列 雖然對用戶而言實現了動態的功能,但在底層上仍是依託着靜態數組,使用 resize 方法解決固定容量的問題,從根本上來講還不是真正的動態。html
而對於鏈表而言,則是真正的動態數據結構。java
由於鏈表的實現是將一個個節點靠地址的指向將這些節點掛接起來而組成的。數組
簡單來講,每一次在鏈表上添加新數據就是在一個已有節點的指針域上指定它的下一個節點的地址爲存放新數據的節點的地址。這樣子,不管是從底層上仍是用戶的角度上,都不用擔憂容量的問題,因此鏈表是真正的動態數據結構。數據結構
一樣,鏈表也是一個很重要的數據結構。對於鏈表而言,它是最簡單的動態數據結構,能夠幫助咱們更深刻地理解引用(指針)、更深刻地理解遞歸以及能夠用來輔助組成其餘的數據結構。app
對鏈表而言,數據是存儲在「節點」(Node)中的,可使用一個數據域來存儲數據,這裏我稱爲 element;而後節點中還有一個用來指向下一個節點位置的節點域,通常稱爲 next。而對於鏈表的結尾,通常是以 NULL 做爲結尾,因此鏈表中的最後一個節點的節點域 next 指向的是 NULL。dom
圖示以下:ide
因此能夠先暫時設計鏈表的基本結構代碼以下:函數
/** * 鏈表類 * 支持泛型 * * @author 踏雪彡尋梅 * @date 2020-02-03 - 21:08 */ public class LinkedList<E> { /** * 鏈表的節點 * 對於用戶而言,不須要知道鏈表的底層結構是怎樣的,只須要知道鏈表是一種線性數據結構,能夠增刪改查數據 */ private class Node { /** * 節點存儲的數據 */ public E element; /** * 用於指向下一個節點,使節點與節點之間掛接起來組成鏈表 */ public Node next; /** * 構造函數 * 構造一個存有數據並指向了下一個節點的節點 * * @param element 存往該節點中的數據 * @param next 該節點的下一個節點 */ public Node(E element, Node next) { this.element = element; this.next = next; } /** * 構造函數 * 構造一個存有數據但沒有指向下一個節點的節點 * * @param element 存往該節點中的數據 */ public Node(E element) { this(element, null); } /** * 構造函數 * 構造一個空節點 */ public Node() { this(null, null); } /** * 重寫 toString 方法以顯示節點中存儲的數據信息 * * @return 返回節點中存儲的數據信息 */ @Override public String toString() { return element.toString(); } } }
從以上設計也可簡單的分析鏈表的優缺點以下:oop
優勢:真正的動態結構,不須要處理固定容量的問題。測試
缺點:喪失了隨機訪問的能力。即不像數組同樣能夠經過索引快速地獲取到數據。
綜上,可簡單對比數組和鏈表的使用場景以下:
數組最好用於索引有語意的狀況,不適合用於索引沒有語意的狀況。
有語意的狀況:如一個班級中第二名的分數可這樣表示:score[2]。
數組也能夠沒有語意,並非任什麼時候候索引都是有語意的,不是全部有語意的這樣的一個標誌就適合作索引,如身份證號:身份證號的保存會存在空間的浪費(有些索引不是身份證號碼)。
最大的優勢:支持快速查詢。
相比數組,將一個靜態數組改變爲一個動態數組,就是在對於不方便使用索引的時候處理有關數據存儲的問題,對於這樣的存儲數據的需求使用鏈表是更合適的。因此鏈表不適合用於索引有語意的狀況,更適合處理索引沒有語意的狀況。
另外,對於查看鏈表中的各個元素,也是須要一一遍歷過去的,那麼此時就須要一個變量 head 來指向鏈表頭部的位置,以便查看鏈表信息所用。同時由於有了這個變量 head 來指向鏈表頭的位置,那麼往鏈表頭部添加新元素是十分方便的,這和以前實現的數組數據結構在數組尾部添加元素十分方即是同一個道理,數組中有 size 變量指向下一個新元素位置跟蹤尾部。
此時鏈表的結構以下圖所示:
此時設計鏈表基本結構代碼以下,其中使用了一個變量 size 來實時記錄鏈表元素的個數以及增長了兩個基本方法用於獲取鏈表當前元素個數和判斷鏈表是否爲空:
/** * 鏈表類 * 支持泛型 * * @author 踏雪彡尋梅 * @date 2020-02-03 - 21:08 */ public class LinkedList<E> { /** * 鏈表的節點 * 對於用戶而言,不須要知道鏈表的底層結構是怎樣的,只須要知道鏈表是一種線性數據結構,能夠增刪改查數據 */ private class Node { /** * 節點存儲的數據 */ public E element; /** * 用於指向下一個節點,使節點與節點之間掛接起來組成鏈表 */ public Node next; /** * 構造函數 * 構造一個存有數據並指向了下一個節點的節點 * * @param element 存往該節點中的數據 * @param next 該節點的下一個節點 */ public Node(E element, Node next) { this.element = element; this.next = next; } /** * 構造函數 * 構造一個存有數據但沒有指向下一個節點的節點 * * @param element 存往該節點中的數據 */ public Node(E element) { this(element, null); } /** * 構造函數 * 構造一個空節點 */ public Node() { this(null, null); } /** * 重寫 toString 方法以顯示節點中存儲的數據信息 * * @return 返回節點中存儲的數據信息 */ @Override public String toString() { return element.toString(); } } /** * 鏈表的頭節點 * 存儲第一個元素的節點 */ private Node head; /** * 鏈表當前元素個數 */ private int size; /** * 構造函數 * 構造一個空鏈表 */ public LinkedList() { head = null; size = 0; } /** * 獲取鏈表中的當前元素個數 * * @return 返回鏈表當前元素個數 */ public int getSize() { return size; } /** * 判斷鏈表是否爲空 * * @return 鏈表爲空返回 true;不然返回 fasle */ public boolean isEmpty() { return size == 0; } }
在上文介紹過,在鏈表頭部添加元素是十分方便的,因此先實現這個操做。
對於這個操做,實現的具體步驟以下:
建立一個新節點 newNode 存儲新元素 newElement,新節點的節點域 next 指向 NULL。
將 newNode 的 節點域 next 指向當前鏈表頭 head,使新節點掛接在鏈表頭部。即 newNode.next = head。
最後將 head 指向 newNode,使鏈表頭爲新增的節點。即 head = newNode。
綜上,在鏈表頭添加過程以下圖所示:
設計在鏈表頭部添加元素代碼以下所示:
/** * 在鏈表頭添加新的元素 newElement * * @param newElement 新元素 */ public void addFirst(E newElement) { // 建立一個新節點存儲新元素,該節點的 next 指向 NULL Node newNode = new Node(newElement); // 使 newNode 的 next 指向鏈表頭 newNode.next = head; // 將鏈表頭設爲鏈表新添加的新節點 head = newNode; // 以上三行代碼可以使用 Node 的另外一個構造函數簡寫爲: // head = new Node(newElement, head); // 維護 size,鏈表當前元素個數 + 1 size++; }
除了在鏈表頭部添加元素,還能夠指定一個位置來進行添加元素。這個操做在鏈表的操做中不常使用,通常常出如今試題中,這裏實現出來用來幫助深刻理解鏈表的思惟。
對於這個操做,指定的添加元素位置這裏設計爲用 index 表示(從 0 開始計數),實現的具體步驟以下:
判斷指定的添加位置 index 是否爲合法值。
使用一個節點變量 prev 來找到指定插入位置 index 的前一個節點位置,初始時 prev 指向鏈表頭 head。
建立一個新節點 newNode 存儲新元素 newElement,新節點的節點域 next 指向 NULL。
使用 prev 找到指定位置 index 的前一個位置(index - 1 處,即插入位置的前一個節點)後,將 newNode 的 next 指向 prev 的 next 指向的節點,即將新節點掛接在插入位置的原節點前面。(newNode.next = prev.next)
將 prev 的 next 指向新節點 newNode,即將鏈表先後都掛接了起來。此時新節點處於 index 處,而原來處於 index 的節點和以後的節點都日後挪了一個位置。(prev.next = newNode)
對於以上步驟,關鍵在於找到要添加的節點的前一個節點。
而找到前一個節點這個操做有一個特殊狀況,即指定添加位置 index 爲 0 的時候,也就是將元素添加到鏈表頭,而鏈表頭是沒有前一個節點的(對於鏈表頭沒有前一個節點後續會實現一個虛擬頭節點放置到鏈表頭的前一個節點,方便鏈表的操做)。
因此這個操做須要進行特殊處理:
使用一個判斷判斷 index 是否爲 0,若是爲 0 使用前面實現的 addFirst(E newElement) 方法將新節點添加到鏈表頭。
綜上,在鏈表指定位置處添加元素過程以下圖所示:
須要注意的是 newNode.next = prev.next 和 prev.next = newNode 的順序不能相反,不然將會出現錯誤,具體結果圖示以下:
設計在鏈表指定位置處添加元素代碼以下所示:
/** * 在鏈表的指定位置 index(從 0 開始計數)處添加新元素 newElement * * @param index 指定的添加位置,從 0 開始計數 * @param newElement 新元素 */ public void add(int index, E newElement) { // 判斷 index 是否合法 if (index < 0 || index > size) { throw new IllegalArgumentException("Add failed.Illegal index."); } if (index == 0) { // 若是 index 爲 0,使用 addFirst(E newElement) 方法將新元素添加到鏈表頭。 addFirst(newElement); } else { // 不然將新元素添加到 index 處 // 找到 index 的前一個節點 Node prev = head; for (int i = 0; i < index - 1; i++) { prev = prev.next; } // 建立一個新節點存儲新元素,該節點的 next 指向 NULL Node newNode = new Node(newElement); // 將新節點添加到 index 處 newNode.next = prev.next; prev.next = newNode; // 以上三行代碼可以使用 Node 的另外一個構造函數簡寫爲: // prev.next = new Node(newElement, prev.next); // 維護 size,鏈表當前元素個數 + 1 size++; } }
由以上實現可複用其實現一個在鏈表末尾添加新元素的方法 addLast:
/** * 在鏈表末尾添加新的元素 newElement * @param newElement 新元素 */ public void addLast(E newElement) { add(size, newElement); }
在上面實現的在鏈表指定位置處添加元素中,能夠發現有一個特殊狀況爲指定在頭部添加元素時,頭部元素沒有前一個節點,因此須要作一個特殊處理。爲了讓這個操做統一爲每一個節點均可以找到前置節點,須要在鏈表中設置一個虛擬頭節點 dummyHead。這個節點這裏設計爲不存儲數據,只用於指向鏈表中的第一個元素。
添加虛擬頭節點後的鏈表基本結構圖示以下:
此時更改鏈表的實現代碼以下:
/** * 鏈表類 * 支持泛型 * * @author 踏雪彡尋梅 * @date 2020-02-03 - 21:08 */ public class LinkedList<E> { /** * 鏈表的節點 * 對於用戶而言,不須要知道鏈表的底層結構是怎樣的,只須要知道鏈表是一種線性數據結構,能夠增刪改查數據 */ private class Node { /** * 節點存儲的數據 */ public E element; /** * 用於指向下一個節點,使節點與節點之間掛接起來組成鏈表 */ public Node next; /** * 構造函數 * 構造一個存有數據並指向了下一個節點的節點 * * @param element 存往該節點中的數據 * @param next 該節點的下一個節點 */ public Node(E element, Node next) { this.element = element; this.next = next; } /** * 構造函數 * 構造一個存有數據但沒有指向下一個節點的節點 * * @param element 存往該節點中的數據 */ public Node(E element) { this(element, null); } /** * 構造函數 * 構造一個空節點 */ public Node() { this(null, null); } /** * 重寫 toString 方法以顯示節點中存儲的數據信息 * * @return 返回節點中存儲的數據信息 */ @Override public String toString() { return element.toString(); } } /** * 鏈表的虛擬頭節點 * 不存儲數據 * next 指向鏈表中的第一個元素 */ private Node dummyHead; /** * 鏈表當前元素個數 */ private int size; /** * 構造函數 * 構造一個空鏈表 */ public LinkedList() { // 建立虛擬頭節點,存儲 null,初始時 next 指向 null dummyHead = new Node(null, null); size = 0; } /** * 獲取鏈表中的當前元素個數 * * @return 返回鏈表當前元素個數 */ public int getSize() { return size; } /** * 判斷鏈表是否爲空 * * @return 鏈表爲空返回 true;不然返回 fasle */ public boolean isEmpty() { return size == 0; } /** * 在鏈表的指定位置 index(從 0 開始計數)處添加新元素 newElement * * @param index 指定的添加位置,從 0 開始計數 * @param newElement 新元素 */ public void add(int index, E newElement) { // 判斷 index 是否合法 if (index < 0 || index > size) { throw new IllegalArgumentException("Add failed.Illegal index."); } // 將新元素添加到 index 處 // 找到 index 的前一個節點 Node prev = dummyHead; for (int i = 0; i < index; i++) { prev = prev.next; } // 建立一個新節點存儲新元素,該節點的 next 指向 NULL Node newNode = new Node(newElement); // 將新節點添加到 index 處 newNode.next = prev.next; prev.next = newNode; // 以上三行代碼可以使用 Node 的另外一個構造函數簡寫爲: // prev.next = new Node(newElement, prev.next); // 維護 size,鏈表當前元素個數 + 1 size++; } /** * 在鏈表頭添加新的元素 newElement * * @param newElement 新元素 */ public void addFirst(E newElement) { add(0, newElement); } /** * 在鏈表末尾添加新的元素 newElement * * @param newElement 新元素 */ public void addLast(E newElement) { add(size, newElement); } }
此時,在 add 方法的實現中添加新元素的操做就都統一爲同一個步驟了,每個節點都能找到其前置節點。
須要注意的是,和以前 prev 從鏈表的第一個元素開始遍歷尋找更改成了從虛擬頭節點開始遍歷尋找,因此遍歷的終止條件從 i < index - 1 變爲了 i < index。爲了方便理解這個過程,能夠參考如下圖示:
原實現:
現實現:
在更改了 add 方法以後,addFirst 方法也可精簡爲複用 add 方法就可實如今鏈表頭部添加元素的功能了。這也是使用了虛擬頭節點以後帶來的便利。
對於此操做,這裏實現兩個類型的方法用於查詢鏈表中的元素:
get 方法:得到鏈表中某個位置的元素(位置從 0 開始計數)。該操做在鏈表中不常使用,能夠用來增強鏈表的理解。具體實現以下:
/** * 得到鏈表的第 index 個位置的元素 * * @param index 須要獲取的元素的位置,從 0 開始計數 * @return 返回鏈表中的 index 處的元素 */ public E get(int index) { // 判斷 index 的合法性 if (index < 0 || index >= size) { throw new IllegalArgumentException("Get failed.Illegal index."); } // 從鏈表中第一個元素開始遍歷,找處處於 index 的節點 Node currentElement = dummyHead.next; for (int i = 0; i < index; i++) { currentElement = currentElement.next; } // 返回處於 index 的元素 return currentElement.element; }
由以上實現可衍生出兩個方法分別用來獲取鏈表中第一個元素和鏈表中最後一個元素:
/** * 得到鏈表的第一個元素 * * @return 返回鏈表的第一個元素 */ public E getFirst() { return get(0); } /** * 得到鏈表的最後一個元素 * * @return 返回鏈表的最後一個元素 */ public E getLast() { // index 從 0 開始計數,size 爲當前元素個數,因此最後一個元素的位置對應爲 size - 1 return get(size - 1); }
contains 方法:判斷用戶給定的一個元素是否存在於鏈表中,存在返回 true,不存在返回 false。具體實現以下:
/** * 查找鏈表中是否含有元素 element * * @param element 須要查找的元素 * @return 若是包含 element 返回 true;不然返回 false */ public boolean contains(E element) { // 從鏈表中第一個元素開始遍歷,依次判斷是否包含有元素 element Node currentElement = dummyHead.next; while (currentElement != null) { if (currentElement.element.equals(element)) { // 相等說明鏈表中包含元素 element,返回 true return true; } currentElement = currentElement.next; } // 整個鏈表遍歷完尚未找到則返回 false return false; }
對於此操做,實現的目的是修改鏈表中某個位置(位置從 0 開始計數)的元素爲指定的新元素。該操做在鏈表中也不常使用,能夠用來增強鏈表的理解。具體實現以下:
/** * 修改鏈表的第 index 個位置的元素爲 newElement * * @param index 須要修改的元素的位置,從 0 開始計數 * @param newElement 替換老元素的新元素 */ public void set(int index, E newElement) { // 判斷 index 的合法性 if (index < 0 || index >= size) { throw new IllegalArgumentException("Set failed.Illegal index."); } // 從鏈表中第一個元素開始遍歷,找處處於 index 的節點 Node currentElement = dummyHead.next; for (int i = 0; i < index; i++) { currentElement = currentElement.next; } // 修改 index 的元素爲 newElement currentElement.element = newElement; }
對於刪除操做,目的爲刪除鏈表中某個位置的元素並返回刪除的元素。該操做在鏈表中也不常使用,能夠用來增強鏈表的理解。實現的步驟以下:
找到待刪除節點 delNode 的前置節點 prev。
將 prev 的 next 指向待刪除節點 dalNode 的 next。即越過了 delNode,和它後面的節點掛接了起來。(prev.next = delNode.next)
將 delNode 的 next 指向 null,至此,delNode 和鏈表脫離關係,從鏈表中被刪除。(delNode.next = null)
返回 delNode 中存儲的元素 element,即返回刪除的元素。
刪除過程圖示以下:
代碼具體實現以下:
/** * 從鏈表中刪除 index 位置的節點並返回刪除的元素 * * @param index 要刪除節點在鏈表中的位置,從 0 開始計數 * @return 返回刪除的元素 */ public E remove(int index) { // 判斷 index 的合法性 if (index < 0 || index >= size) { throw new IllegalArgumentException("Remove failed.Illegal index."); } // 從虛擬頭節點開始遍歷找到待刪除節點的前置節點 Node prev = dummyHead; for (int i = 0; i < index; i++) { prev = prev.next; } // 記錄待刪除節點 Node delNode = prev.next; // 進行刪除操做 prev.next = delNode.next; delNode.next = null; // 維護 size,鏈表當前元素個數 - 1 size--; // 返回刪除的元素 return delNode.element; }
由以上實現可衍生出兩個方法分別用於刪除鏈表中的第一個元素和最後一個元素:
/** * 從鏈表中刪除第一個元素所在的節點並返回刪除的元素 * * @return 返回刪除的元素 */ public E removeFirst() { return remove(0); } /** * 從鏈表中刪除最後一個元素所在的節點並返回刪除的元素 * * @return 返回刪除的元素 */ public E removeLast() { return remove(size - 1); }
實現到此,已經能夠重寫 toString 方法顯示鏈表中元素信息來測試以上實現的基本操做了,以此驗證設計的邏輯沒有出錯。
對於此方法,這裏設計爲下:
/** * 重寫 toString 方法,以便觀察鏈表中的元素 * * @return 返回當前鏈表信息 */ @Override public String toString() { StringBuilder result = new StringBuilder(); result.append(String.format("LinkedList: size = %d, Elements: dummyHead -> ", size)); // 從鏈表中第一個元素開始遍歷,依次將鏈表中元素信息添加到結果信息中 Node currentElement = dummyHead.next; while (currentElement != null) { result.append(currentElement + " -> "); currentElement = currentElement.next; } // 以上遍歷的等價寫法: // for (Node currentElement = dummyHead.next; currentElement != null; currentElement = currentElement.next) { // result.append(currentElement + " -> "); // } result.append("NULL"); return result.toString(); }
接着測試以上實現的基本操做,測試代碼以下:
/** * 測試 LinkedList */ public static void main(String[] args) { LinkedList<Integer> linkedList = new LinkedList<>(); // 測試 isEmpty 方法 System.out.println("==== 測試 isEmpty 方法 ===="); System.out.println("當前鏈表是否爲空: " + linkedList.isEmpty()); // 測試鏈表的添加操做 System.out.println("\n==== 測試 addFirst 方法 ===="); for (int i = 0; i < 5; i++) { linkedList.addFirst(i); System.out.println(linkedList); } System.out.println("\n==== 測試 add 方法 ===="); System.out.println("添加 888 到鏈表中的第 2 個位置(從 0 開始計數): "); linkedList.add(2, 888); System.out.println(linkedList); System.out.println("\n==== 測試 addLast 方法 ===="); linkedList.addLast(999); System.out.println(linkedList); // 測試 contains 方法 System.out.println("\n==== 測試 contains 方法 ===="); System.out.println(linkedList); boolean flag = linkedList.contains(888); System.out.println("鏈表中是否存在 888: " + flag); flag = linkedList.contains(777); System.out.println("鏈表中是否存在 777: " + flag); // 測試 get 方法 System.out.println("\n==== 測試 get 方法 ===="); System.out.println(linkedList); Integer element = linkedList.getFirst(); System.out.println("鏈表中的第一個元素爲: " + element); element = linkedList.getLast(); System.out.println("鏈表中的最後一個元素爲: " + element); element = linkedList.get(3); System.out.println("鏈表中的第 3 個位置(從 0 開始計數)的元素爲: " + element); // 測試 isEmpty 方法 System.out.println("\n==== 測試 isEmpty 方法 ===="); System.out.println("當前鏈表是否爲空: " + linkedList.isEmpty()); // 測試 set 方法 System.out.println("\n==== 測試 set 方法 ===="); System.out.println(linkedList); linkedList.set(3, 12); System.out.println("更改鏈表中的第 3 個位置(從 0 開始計數)的元素爲 12 後: "); System.out.println(linkedList); // 測試鏈表的刪除操做 System.out.println("\n==== 測試 remove 方法 ===="); Integer delElement = linkedList.remove(3); System.out.println("刪除鏈表中的第 3 個位置(從 0 開始計數)的元素後: "); System.out.println(linkedList); System.out.println("刪除的元素爲: " + delElement); System.out.println("\n==== 測試 removeFirst 方法 ===="); delElement = linkedList.removeFirst(); System.out.println("刪除鏈表中的第一個元素後: "); System.out.println(linkedList); System.out.println("刪除的元素爲: " + delElement); System.out.println("\n==== 測試 removeLast 方法 ===="); delElement = linkedList.removeLast(); System.out.println("刪除鏈表中的最後一個元素後: "); System.out.println(linkedList); System.out.println("刪除的元素爲: " + delElement); }
測試結果:
==== 測試 isEmpty 方法 ==== 當前鏈表是否爲空: true ==== 測試 addFirst 方法 ==== LinkedList: size = 1, Elements: dummyHead -> 0 -> NULL LinkedList: size = 2, Elements: dummyHead -> 1 -> 0 -> NULL LinkedList: size = 3, Elements: dummyHead -> 2 -> 1 -> 0 -> NULL LinkedList: size = 4, Elements: dummyHead -> 3 -> 2 -> 1 -> 0 -> NULL LinkedList: size = 5, Elements: dummyHead -> 4 -> 3 -> 2 -> 1 -> 0 -> NULL ==== 測試 add 方法 ==== 添加 888 到鏈表中的第 2 個位置(從 0 開始計數): LinkedList: size = 6, Elements: dummyHead -> 4 -> 3 -> 888 -> 2 -> 1 -> 0 -> NULL ==== 測試 addLast 方法 ==== LinkedList: size = 7, Elements: dummyHead -> 4 -> 3 -> 888 -> 2 -> 1 -> 0 -> 999 -> NULL ==== 測試 contains 方法 ==== LinkedList: size = 7, Elements: dummyHead -> 4 -> 3 -> 888 -> 2 -> 1 -> 0 -> 999 -> NULL 鏈表中是否存在 888: true 鏈表中是否存在 777: false ==== 測試 get 方法 ==== LinkedList: size = 7, Elements: dummyHead -> 4 -> 3 -> 888 -> 2 -> 1 -> 0 -> 999 -> NULL 鏈表中的第一個元素爲: 4 鏈表中的最後一個元素爲: 999 鏈表中的第 3 個位置(從 0 開始計數)的元素爲: 2 ==== 測試 isEmpty 方法 ==== 當前鏈表是否爲空: false ==== 測試 set 方法 ==== LinkedList: size = 7, Elements: dummyHead -> 4 -> 3 -> 888 -> 2 -> 1 -> 0 -> 999 -> NULL 更改鏈表中的第 3 個位置(從 0 開始計數)的元素爲 12 後: LinkedList: size = 7, Elements: dummyHead -> 4 -> 3 -> 888 -> 12 -> 1 -> 0 -> 999 -> NULL ==== 測試 remove 方法 ==== 刪除鏈表中的第 3 個位置(從 0 開始計數)的元素後: LinkedList: size = 6, Elements: dummyHead -> 4 -> 3 -> 888 -> 1 -> 0 -> 999 -> NULL 刪除的元素爲: 12 ==== 測試 removeFirst 方法 ==== 刪除鏈表中的第一個元素後: LinkedList: size = 5, Elements: dummyHead -> 3 -> 888 -> 1 -> 0 -> 999 -> NULL 刪除的元素爲: 4 ==== 測試 removeLast 方法 ==== 刪除鏈表中的最後一個元素後: LinkedList: size = 4, Elements: dummyHead -> 3 -> 888 -> 1 -> 0 -> NULL 刪除的元素爲: 999 進程已結束,退出代碼 0
從結果能夠看出以上實現的基本操做沒有出現錯誤,說明了實現的邏輯是正確的,接下來對以上實現的基本操做作一些簡單的時間複雜度分析。
添加操做
addLast 方法:對於該方法,每次都要遍歷整個鏈表進行添加,因此該方法的時間複雜度是 O(n) 級別的。
addFirst 方法:對於該方法,每次都是在鏈表頭部作操做,因此該方法的時間複雜度是 O(1) 級別的。
add 方法:對於該方法,平均來講,每次添加元素須要遍歷 n/2 個元素,因此該方法的時間複雜度是 O(n/2) = O(n) 級別的。
綜上,添加操做的時間複雜度爲 O(n)。
刪除操做
removeLast 方法:對於該方法,每次都要遍歷整個鏈表進行刪除,因此該方法的時間複雜度是 O(n) 級別的。
removeFirst 方法:對於該方法,每次都是在鏈表頭部作操做,因此該方法的時間複雜度是 O(1) 級別的。
remove 方法:對於該方法,平均來講,每次刪除元素須要遍歷 n/2 個元素,因此該方法的時間複雜度是 O(n/2) = O(n) 級別的。
綜上,刪除操做的時間複雜度爲 O(n)。
修改操做
set 方法:對於該方法,平均來講,每次修改元素須要遍歷 n/2 個元素,因此該方法的時間複雜度是 O(n/2) = O(n) 級別的。
因此修改操做的時間複雜度也爲 O(n)。
查找操做
getLast 方法:對於該方法,每次都要遍歷整個鏈表進行查找,因此該方法的時間複雜度是 O(n) 級別的。
getFirst 方法:對於該方法,每次都是在鏈表頭部作操做,因此該方法的時間複雜度是 O(1) 級別的。
get 方法:對於該方法,平均來講,每次查找元素須要遍歷 n/2 個元素,因此該方法的時間複雜度是 O(n/2) = O(n) 級別的。
contains 方法:對於該方法,平均來講,每次查找判斷元素是否存在也是須要遍歷 n/2 個元素,因此該方法的時間複雜度也是 O(n/2) = O(n) 級別的。
綜上,查找操做的時間複雜度爲 O(n)。
因此對於鏈表而言,增刪改查的時間複雜度都是 O(n) 級別的。
可是若是不對鏈表中元素進行修改操做,添加和刪除操做也只針對鏈表頭進行操做和查找操做也只查鏈表頭的元素的話,此時總體的時間複雜度就是 O(1) 級別的了,又因爲鏈表總體是動態的,不會浪費大量的內存空間,此時具備必定的優點,顯而易見知足這些條件的數據結構爲棧,此時就可使用鏈表來實現棧發揮鏈表的優點了。固然,對於鏈表而言,還有一些改進方式使其在一些應用場景具備優點,好比給鏈表添加尾指針後使用鏈表來實現隊列。
對於棧這個數據結構,它只針對一端進行操做,即針對棧頂進行操做,是一個後入先出的數據結構。
上文說到若是鏈表不使用修改操做,只使用添加、刪除、查找鏈表頭的操做是知足棧這個數據結構的特色的。因此可使用鏈表頭做爲棧頂,用鏈表做爲棧的底層實現來實現棧這個數據結構,發揮鏈表的動態優點。最後,再和以前基於動態數組實現的棧進行一些效率上的對比,查看二者的差距。接下來,開始實現使用鏈表實現棧。
對於使用鏈表實現棧,將使用一個 LinkedListStack 類實現以前實現數組棧時定義的棧的接口 Stack 來實現棧的一系列的操做。
回顧棧的接口 Stack 的實現以下:
/** * 定義棧支持的操做的接口 * 支持泛型 * * @author 踏雪尋梅 * @date 2020/1/8 - 19:20 */ public interface Stack<E> { /** * 獲取棧中元素個數 * * @return 棧中若是有元素,返回棧中當前元素個數;棧中若是沒有元素返回 0 */ int getSize(); /** * 判斷棧是否爲空 * * @return 棧爲空,返回 true;棧不爲空,返回 false */ boolean isEmpty(); /** * 入棧 * 將元素 element 壓入棧頂 * * @param element 入棧的元素 */ void push(E element); /** * 出棧 * 將當前棧頂元素出棧並返回 * * @return 返回當前出棧的棧頂元素 */ E pop(); /** * 查看當前棧頂元素 * * @return 返回當前的棧頂元素 */ E peek(); }
對於 LinkedListStack 類的實現,只須要複用鏈表類中的方法就可實現棧的這些基本操做了,具體實現以下:
/** * 基於 LinkedList 實現的鏈表棧 * 支持泛型 * * @author 踏雪彡尋梅 * @date 2020/2/5 - 12:22 */ public class LinkedListStack<E> implements Stack<E> { /** * 基於該鏈表實現棧 */ private LinkedList<E> linkedList; /** * 構造函數 * 構造一個空的鏈表棧 */ public LinkedListStack() { linkedList = new LinkedList<>(); } @Override public int getSize() { return linkedList.getSize(); } @Override public boolean isEmpty() { return linkedList.isEmpty(); } @Override public void push(E element) { linkedList.addFirst(element); } @Override public E pop() { return linkedList.removeFirst(); } @Override public E peek() { return linkedList.getFirst(); } /** * 重寫 toString 方法顯示鏈表棧中的各信息 * * @return 返回鏈表棧的信息 */ @Override public String toString() { StringBuilder result = new StringBuilder(); result.append(String.format("LinkedListStack: size = %d, top [ ", getSize())); for (int i = 0; i < getSize(); i++) { E e = linkedList.get(i); result.append(e); // 若是不是最後一個元素 if (i != getSize() - 1) { result.append(", "); } } result.append(" ] bottom"); return result.toString(); } }
接下來,對以上實現作一些測試,檢測是否和預期結果不符,測試代碼以下:
/** * 測試 LinkedListStack */ public static void main(String[] args) { LinkedListStack<Integer> stack = new LinkedListStack<>(); // 判斷棧是否爲空 System.out.println("==== 測試 isEmpty ===="); System.out.println("當前棧是否爲空: " + stack.isEmpty()); System.out.println("\n==== 測試鏈表棧的入棧,入棧 10 次 ===="); for (int i = 0; i < 10; i++) { // 入棧 stack.push(i); // 打印入棧過程 System.out.println(stack); } System.out.println("\n==== 測試鏈表棧的出棧,出棧 1 次 ===="); // 進行一次出棧 stack.pop(); // 查看出棧後的狀態 System.out.println(stack); // 查看當前棧頂元素 System.out.println("\n==== 測試鏈表棧的查看棧頂元素 ===="); Integer topElement = stack.peek(); System.out.println("當前棧頂元素: " + topElement); // 判斷棧是否爲空 System.out.println("\n==== 測試 isEmpty ===="); System.out.println("當前棧是否爲空: " + stack.isEmpty()); }
測試結果:
==== 測試 isEmpty ==== 當前棧是否爲空: true ==== 測試鏈表棧的入棧,入棧 10 次 ==== LinkedListStack: size = 1, top [ 0 ] bottom LinkedListStack: size = 2, top [ 1, 0 ] bottom LinkedListStack: size = 3, top [ 2, 1, 0 ] bottom LinkedListStack: size = 4, top [ 3, 2, 1, 0 ] bottom LinkedListStack: size = 5, top [ 4, 3, 2, 1, 0 ] bottom LinkedListStack: size = 6, top [ 5, 4, 3, 2, 1, 0 ] bottom LinkedListStack: size = 7, top [ 6, 5, 4, 3, 2, 1, 0 ] bottom LinkedListStack: size = 8, top [ 7, 6, 5, 4, 3, 2, 1, 0 ] bottom LinkedListStack: size = 9, top [ 8, 7, 6, 5, 4, 3, 2, 1, 0 ] bottom LinkedListStack: size = 10, top [ 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 ] bottom ==== 測試鏈表棧的出棧,出棧 1 次 ==== LinkedListStack: size = 9, top [ 8, 7, 6, 5, 4, 3, 2, 1, 0 ] bottom ==== 測試鏈表棧的查看棧頂元素 ==== 當前棧頂元素: 8 ==== 測試 isEmpty ==== 當前棧是否爲空: false 進程已結束,退出代碼 0
從結果能夠看出,實現的結果和預期是相符的,實現了棧的各個基本操做。總體的時間複雜度前面也分析過了,都是對鏈表頭部進行操做,時間複雜度是 O(1) 級別的,而且擁有了鏈表的總體動態性。接下來和以前實現的數組棧進行一些效率上的對比:
測試代碼:
import java.util.Random; /** * 對比數組棧和鏈表棧的效率差距 * * @author 踏雪彡尋梅 * @date 2020/2/5 - 12:52 */ public class Main { /** * 測試使用 stack 運行 opCount 個 push 和 pop 操做所須要的時間,單位: 秒 * * @param stack 測試使用的棧 * @param opCount 測試的數量級 * @return 返回測試的運行時間,單位: 秒 */ private static double testStack(Stack<Integer> stack, int opCount) { long startTime = System.nanoTime(); Random random = new Random(); for (int i = 0; i < opCount; i++) { stack.push(random.nextInt(Integer.MAX_VALUE)); } for (int i = 0; i < opCount; i++) { stack.pop(); } long endTime = System.nanoTime(); return (endTime - startTime) / 1000000000.0; } public static void main(String[] args) { int opCount = 10000; ArrayStack<Integer> arrayStack = new ArrayStack<>(); double time1 = testStack(arrayStack, opCount); System.out.println("ArrayStack, time: " + time1 + " s"); LinkedListStack<Integer> linkedListStack = new LinkedListStack<>(); double time2 = testStack(linkedListStack, opCount); System.out.println("LinkedListStack, time: " + time2 + " s"); } }
測試結果-1(opCount 爲 1 萬時)
測試結果-2(opCount 爲 10 萬時)
測試結果-3(opCount 爲 100 萬時)
測試結果-4(opCount 爲 1000 萬時)
從這幾個測試結果能夠看出在我這臺機器上當數據量較小時,鏈表棧耗時比數組棧短,隨着數據量增大,數組棧耗時比鏈表棧短。
但歸根結底,這兩個棧的入棧和出棧操做的時間複雜度是同一級別 O(1) 的。
這種有時快有時慢的狀況主要跟內部實現相關:
對於數組棧來講可能時不時須要從新分配數組空間進行擴容或者減容,這一操做會消耗一些時間。
對於鏈表棧來講則是有不少 new 新節點 Node 的操做,這些 new Node 的操做也會消耗一些時間。
因此這兩種棧之間的時間對比會出現這種常數倍的差別,屬於正常的狀況,它們之間沒有複雜度上的巨大的差別,整體上的時間複雜度仍是同一級別的,具體的時間差別最多也就是幾倍的差別,不會產生巨大的差別。
固然,相比數組棧須要時不時從新分配數組空間達到動態伸縮容量的目的,鏈表棧的動態性將會顯得更有優點一些,不須要咱們像數組棧同樣手動地進行伸縮容量處理。
對於隊列這個數據結構,它是針對兩端進行操做,即針對隊首和隊尾進行操做,是一個先入先出的數據結構。
而在以前的鏈表實現中,只有針對鏈表頭的操做是 O(1) 級別的,那麼若是用來實現隊列這種數據結構的話,就會有一端的操做是 O(n) 級別的,爲了解決這個問題,能夠給鏈表添加一個尾指針 tail 用於追蹤鏈表尾部,達到對鏈表首尾兩端的操做都是 O(1) 級別進而實現隊列的目的。
當給鏈表添加尾指針 tail 後,能夠發如今尾部刪除元素是須要從頭遍歷找到前置節點的進行刪除操做的,這個過程是 O(n) 的,不知足咱們的需求;而若是在尾部添加元素的話,就和在鏈表頭添加元素一個道理,是十分方便的,只須要 O(1) 的複雜度。再看回鏈表頭,由以前的實現能夠發如今頭部刪除元素很是方便,只須要 O(1) 的複雜度。
因此能夠設計爲在鏈表頭進行刪除元素的操做,在鏈表尾進行添加元素的操做。即將鏈表頭做爲隊首,鏈表尾做爲隊尾。
因爲在隊列中只須要對兩端進行操做,因此這裏實現隊列時就不復用前面實現的鏈表類了。在以前實現的鏈表類中,設計了虛擬頭節點便於統一操做鏈表中的全部數據。而在如今的隊列實現中只須要在頭部刪除元素在尾部添加元素,因此不須要使用虛擬頭節點。只須要兩個變量 head、tail 分別指向鏈表中的第一個元素和鏈表中的最後一個非 NULL 元素便可。
須要注意的是這樣設計後當隊列爲空時,head 和 tail 都指向 NULL。
改進後的鏈表隊列基本結構以下圖所示:
和以前實現棧同樣,這裏也是實現一個 LinkedListQueue 類實現以前實現數組隊列時定義的隊列的接口 Queue 來實現隊列的一系列的操做。
回顧隊列的接口 Queue 的實現以下:
/** * 定義隊列支持的操做的接口 * 支持泛型 * * @author 踏雪尋梅 * @date 2020/1/9 - 16:52 */ public interface Queue<E> { /** * 獲取隊列中元素個數 * * @return 隊列中若是有元素,返回隊列中當前元素個數;隊列中若是沒有元素返回 0 */ int getSize(); /** * 判斷隊列是否爲空 * * @return 隊列爲空,返回 true;隊列不爲空,返回 false */ boolean isEmpty(); /** * 入隊 * 將元素 element 添加到隊尾 * * @param element 入隊的元素 */ void enqueue(E element); /** * 出隊 * 將隊首的元素出隊並返回 * * @return 返回當前出隊的隊首的元素 */ E dequeue(); /** * 查看當前隊首元素 * * @return 返回當前的隊首元素 */ E getFront(); }
對於 LinkedListQueue 類的實現,具體實現以下:
/** * 鏈表隊列 * 支持泛型 * * @author 踏雪彡尋梅 * @date 2020/2/5 - 14:45 */ public class LinkedListQueue<E> implements Queue<E> { /** * 鏈表的節點 * 對於用戶而言,不須要知道鏈表的底層結構是怎樣的,只須要知道鏈表是一種線性數據結構,能夠增刪改查數據 */ private class Node { /** * 節點存儲的數據 */ public E element; /** * 用於指向下一個節點,使節點與節點之間掛接起來組成鏈表 */ public Node next; /** * 構造函數 * 構造一個存有數據並指向了下一個節點的節點 * * @param element 存往該節點中的數據 * @param next 該節點的下一個節點 */ public Node(E element, Node next) { this.element = element; this.next = next; } /** * 構造函數 * 構造一個存有數據但沒有指向下一個節點的節點 * * @param element 存往該節點中的數據 */ public Node(E element) { this(element, null); } /** * 構造函數 * 構造一個空節點 */ public Node() { this(null, null); } /** * 重寫 toString 方法以顯示節點中存儲的數據信息 * * @return 返回節點中存儲的數據信息 */ @Override public String toString() { return element.toString(); } } /** * 用於指向鏈表隊列的第一個節點 */ private Node head; /** * 用於指向鏈表隊列的最後一個非 NULL 節點 */ private Node tail; /** * 鏈表隊列當前元素個數 */ private int size; /** * 構造函數 * 構造一個空的鏈表隊列 */ public LinkedListQueue() { // 鏈表隊列爲空時, head 和 tail 都指向 null head = null; tail = null; size = 0; } @Override public int getSize() { return size; } @Override public boolean isEmpty() { return size == 0; } @Override public void enqueue(E element) { if (tail == null) { // 空隊時入隊 tail = new Node(element); head = tail; } else { // 非空隊時入隊 tail.next = new Node(element); tail = tail.next; } // 維護 size,隊列當前元素個數 + 1 size++; } @Override public E dequeue() { // 出隊時判斷隊列是否爲空 if (isEmpty()) { throw new IllegalArgumentException("Dequeue failed. Cannot dequeue from an empty queue."); } // 記錄要出隊的節點 Node dequeueNode = head; // 將隊頭節點出隊 head = head.next; dequeueNode.next = null; // 若是出隊後隊列爲空,維護 tail 指向 null,空隊時 head 和 tail 都指向 null if (head == null) { tail = null; } // 維護 size,隊列當前元素個數 - 1 size--; // 返回出隊元素 return dequeueNode.element; } @Override public E getFront() { // 獲取隊頭元素時判斷隊列是否爲空 if (isEmpty()) { throw new IllegalArgumentException("GetFront failed. Queue is empty."); } // 返回隊頭元素 return head.element; } /** * 重寫 toString 方法顯示鏈表隊列的詳細信息 * * @return 返回鏈表隊列的詳細詳細 */ @Override public String toString() { StringBuilder result = new StringBuilder(); result.append(String.format("LinkedListQueue: size: %d, front [ ", getSize())); // 從鏈表中第一個元素開始遍歷,依次將鏈表中元素信息添加到結果信息中 Node currentElement = head; while (currentElement != null) { result.append(currentElement + "->"); currentElement = currentElement.next; } result.append("NULL ] tail"); return result.toString(); } }
在實現中須要注意的是在入隊時若是是空隊列須要維護 head 指向 tail,不然 head 會指向 null。以及在出隊時須要判斷出隊後隊列是否爲空,若是爲空須要維護 tail 指向 null。以及須要注意隊列爲空時 head 和 tail 都指向 null。
接下來,對以上實現作一些測試,檢測是否和預期結果不符,測試代碼以下:
/** * 測試 LinkedListQueue */ public static void main(String[] args) { LinkedListQueue<Integer> queue = new LinkedListQueue<>(); // 判斷隊列是否爲空 System.out.println("==== 測試 isEmpty ===="); System.out.println("當前隊列是否爲空: " + queue.isEmpty()); System.out.println("\n==== 測試入隊和出隊, 10 次 入隊, 每 3 次入隊就出隊 1 次===="); for (int i = 0; i < 10; i++) { // 入隊 queue.enqueue(i); // 顯示入隊過程 System.out.println(queue); // 每入隊 3 個元素就出隊一次 if (i % 3 == 2) { // 出隊 queue.dequeue(); // 顯示出隊過程 System.out.println("\n" + queue + "\n"); } } // 判斷隊列是否爲空 System.out.println("\n==== 測試 isEmpty ===="); System.out.println("當前隊列是否爲空: " + queue.isEmpty()); // 獲取隊首元素 System.out.println("\n==== 測試 getFront ===="); System.out.println(queue); Integer front = queue.getFront(); System.out.println("當前隊列隊首元素爲: " + front); }
測試結果:
==== 測試 isEmpty ==== 當前隊列是否爲空: true ==== 測試入隊和出隊, 10 次 入隊, 每 3 次入隊就出隊 1 次==== LinkedListQueue: size: 1, front [ 0->NULL ] tail LinkedListQueue: size: 2, front [ 0->1->NULL ] tail LinkedListQueue: size: 3, front [ 0->1->2->NULL ] tail LinkedListQueue: size: 2, front [ 1->2->NULL ] tail LinkedListQueue: size: 3, front [ 1->2->3->NULL ] tail LinkedListQueue: size: 4, front [ 1->2->3->4->NULL ] tail LinkedListQueue: size: 5, front [ 1->2->3->4->5->NULL ] tail LinkedListQueue: size: 4, front [ 2->3->4->5->NULL ] tail LinkedListQueue: size: 5, front [ 2->3->4->5->6->NULL ] tail LinkedListQueue: size: 6, front [ 2->3->4->5->6->7->NULL ] tail LinkedListQueue: size: 7, front [ 2->3->4->5->6->7->8->NULL ] tail LinkedListQueue: size: 6, front [ 3->4->5->6->7->8->NULL ] tail LinkedListQueue: size: 7, front [ 3->4->5->6->7->8->9->NULL ] tail ==== 測試 isEmpty ==== 當前隊列是否爲空: false ==== 測試 getFront ==== LinkedListQueue: size: 7, front [ 3->4->5->6->7->8->9->NULL ] tail 當前隊列隊首元素爲: 3 進程已結束,退出代碼 0
從結果能夠看出,實現的結果和預期是相符的,實現了隊列的各個基本操做。總體的時間複雜度前面也簡單分析過了,針對鏈表頭部和尾部進行操做,時間複雜度都是 O(1) 級別的,而且擁有了鏈表的總體動態性。接下來和以前實現的數組隊列和循環隊列進行一些效率上的對比:
測試代碼:
import java.util.Random; /** * 測試 ArrayQueue、LoopQueue 和 LinkedListQueue 的效率差距 * * @author 踏雪尋梅 * @date 2020/1/8 - 16:49 */ public class Main2 { public static void main(String[] args) { // 測試數據量 int opCount = 10000; // 測試數組隊列所須要的時間 ArrayQueue<Integer> arrayQueue = new ArrayQueue<>(); double arrayQueueTime = testQueue(arrayQueue, opCount); System.out.println("arrayQueueTime: " + arrayQueueTime + " s."); // 測試循環隊列所須要的時間 LoopQueue<Integer> loopQueue = new LoopQueue<>(); double loopQueueTime = testQueue(loopQueue, opCount); System.out.println("loopQueueTime: " + loopQueueTime + " s."); // 測試鏈表隊列所須要的時間 LinkedListQueue<Integer> linkedListQueue = new LinkedListQueue<>(); double linkedListQueueTime = testQueue(linkedListQueue, opCount); System.out.println("linkedListQueueTime: " + linkedListQueueTime + " s."); } /** * 測試使用隊列 queue 運行 opCount 個 enqueue 和 dequeue 操做所須要的時間,單位: 秒 * @param queue 測試的隊列 * @param opCount 測試的數據量 * @return 返回整個測試過程所須要的時間,單位: 秒 */ private static double testQueue(Queue<Integer> queue, int opCount) { long startTime = System.nanoTime(); // 用於生成隨機數入隊 Random random = new Random(); // opCount 次 enqueue for (int i = 0; i < opCount; i++) { // 入隊 queue.enqueue(random.nextInt(Integer.MAX_VALUE)); } // opCount 次 dequeue for (int i = 0; i < opCount; i++) { // 出隊 queue.dequeue(); } long endTime = System.nanoTime(); // 將納秒單位的時間轉換爲秒單位 return (endTime - startTime) / 1000000000.0; } }
測試結果-1(opCount 爲 1 萬時)
測試結果-2(opCount 爲 10 萬時)
測試結果-3(opCount 爲 100 萬時)
從以上幾種結果能夠看出,在我這臺機器上數組隊列耗時比基於數組的循環隊列和鏈表隊列要大的多,基於數組的循環隊列耗時和鏈表隊列相差不大,是常數倍的差別。
對於數組隊列而言它的入隊操做是 O(1) 級別的、出隊操做是 O(n) 級別的,因此在以上測試中總體時間複雜度是 O(n2) 級別的(進行了 n 次入隊和出隊)。
而基於數組的循環隊列和鏈表隊列的入隊操做和出隊操做都是 O(1) 級別的,因此在以上測試中總體而言時間複雜度是 O(n) 級別的(都進行了 n 次入隊和出隊)。但這二者有時候也會出現一個快一點一個慢一點的狀況,也是和前面的鏈表棧和數組棧的狀況是同樣的,這裏再也不闡述。
總而言之基於數組實現的循環隊列和鏈表隊列這二者是同一級別的複雜度的,相比數組隊列的時間複雜度快了不少,時間上的差別是巨大的。
最後,鏈表隊列在動態性上相比基於數組實現的循環隊列會更好一些,不須要手動進行伸縮容量的實現。
實現到此處,鏈表的常見基本操做也都實現完成了,對於鏈表而言,也還存在着一些改進方案,好比給節點增長一個前置指針域用於指向當前節點的前置節點,使鏈表變成雙鏈表等等。這裏就再也不實現了,具體過程仍是大同小異的。
鏈表是一種真正的動態的數據結構,不須要像數組同樣手動地處理動態伸縮容量。
鏈表在針對頭部和尾部作特殊處理後,能夠實現棧和隊列這兩種數據結構,極大地發揮了鏈表的動態特性。
若有寫的不足的,請見諒,請你們多多指教。