鏈表 LinkedList
寫在開頭
- 許久之前文章提到過的:
動態數組
、棧
、隊列
,其底層均依託於靜態數組
,通過resize()
進行動態擴容操作。 - 而
鏈表
,則爲真正的動態數據結構
,同樣也是最簡單的動態數據結構
。 鏈表
這種數據結構可以幫助我們瞭解計算機中指針(引用)
、遞歸
等概念。
節點 Node
-
數據存儲在
節點
中,需要多少節點就生產多少節點進行掛接,但失去了隨機訪問的能力,適合索引無語義的情況。class node { E e; Node next; }
鏈表數據結構創建 LinkedList,爲保證節點信息安全性,採用內部類方式進行構造
/**
* @author by Jiangyf
* @classname LinkedList
* @description 鏈表
* @date 2019/9/28 13:08
*/
public class LinkedList<E> {
/**
* 節點內部類
*/
private class Node {
public E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return "Node{" +
"e=" + e +
", next=" + next +
'}';
}
}
private Node head;
int size;
public LinkedList() {
head = null;
size = 0;
}
// 獲取鏈表容量
public int getSize() {
return size;
}
// 判斷鏈表是否爲空
public boolean isEmpty() {
return size == 0;
}
}
添加操作方法
-
從鏈表頭部添加元素
public void addFirst(E e) { head = new Node(e, head); size ++; }
-
從鏈表中間位置
index
處添加元素,注意:先連後斷
public void add(int index, E e) throws IllegalAccessException { // 索引校驗 if (index < 0 || index > size) { throw new IllegalAccessException("Add failed. Illegal index."); } // 判斷是操作是否爲頭部添加 if (index == 0) { addFirst(e); } else { // 創建前置節點 Node prev = head; // 定位到待插入節點前一個節點 for (int i = 0; i < index -1 ; i++) { prev = prev.next; } prev.next = new Node(e, prev.next); size ++; } }
-
在鏈表尾部位置添加元素
public void addLast(E e) throws IllegalAccessException { add(size, e); }
-
爲鏈表設立
虛擬頭結點(dummyHead)
,解決從頭部添加和其他位置添加的邏輯不一致情況虛擬頭結點
作爲鏈表內部機制進行設置,在原有head
節點改進爲dummyHead.next = head
,適配節點添加邏輯。- 修改代碼
-
添加
虛擬頭結點dummyHead
,不存放任何內容private Node dummyHead; public LinkedList() { dummyHead = new Node(null, null); size = 0; }
-
修改
add(index, e)
方法// 從鏈表中間添加元素 先連後斷 public void add(int index, E e) throws IllegalAccessException { // 索引校驗 if (index < 0 || index > size) { throw new IllegalAccessException("Add failed. Illegal index."); } // 創建前置節點 Node prev = dummyHead; // 定位到待插入節點前一個節點,遍歷index次原因爲 dummyHead爲head節點前一個節點 for (int i = 0; i < index ; i++) { prev = prev.next; } prev.next = new Node(e, prev.next); size ++; }
-
修改
addFirst(e)
方法public void addFirst(E e) throws IllegalAccessException { add(0, e); }
-
-
獲取指定位置
index
的節點元素public E get(int index) throws IllegalAccessException { // 索引校驗 if (index < 0 || index > size) { throw new IllegalAccessException("Get failed. Illegal index."); } // 定位到head節點 Node cur = dummyHead.next; for (int i = 0; i < index; i++) cur = cur.next; return cur.e; }
-
獲取頭結點、尾結點
public E getFirst() throws IllegalAccessException { return get(0); } public E getLast() throws IllegalAccessException { return get(size - 1); }
-
更新指定位置元素
public void set(int index, E e) throws IllegalAccessException { // 索引校驗 if (index < 0 || index > size) { throw new IllegalAccessException("Set failed. Illegal index."); } Node cur = dummyHead.next; for (int i = 0; i < index ; i++) cur = cur.next; cur.e = e; }
-
查找鏈表中是否存在元素
public boolean contains(E e) { Node cur = dummyHead.next; while(cur != null) { if (cur.e.equals(e)) { return true; } cur = cur.next; } return false; }
-
刪除鏈表元素節點
public E remove(int index) throws IllegalAccessException { // 索引校驗 if (index < 0 || index > size) { throw new IllegalAccessException("Remove failed. Illegal index."); } // 定位到待刪除節點的前一節點 Node prev = dummyHead; for (int i = 0; i < index - 1 ; i++) prev = prev.next; // 保存待刪除節點 Node retNode = prev.next; // 跨過待刪除節點進行連接 prev.next = retNode.next; // 待刪除節點next置空 retNode.next = null; size --; return retNode.e; } public E removeFirst() throws IllegalAccessException { return remove(0); } public E removeLast() throws IllegalAccessException { return remove(size - 1); }
-
通過上述方法,我們可以分析得出:鏈表的CURD操作的平均時間複雜度均爲O(n),鏈表的操作均要進行遍歷。
仔細想想,如果對鏈表的操作僅限於頭部
呢? 細思極恐,是不是複雜度就降爲O(1)啦?又由於鏈表是動態的,不會造成空間的浪費,所以當且僅當頭部
操作下,優勢是很明顯的!
-
基於
頭部操作
,用鏈表實現棧
,關於Stack接口
,可以查看 數據結構_2:棧public class LinkedListStack<E> implements Stack<E> { private LinkedList<E> list; public LinkedListStack(LinkedList<E> list) { this.list = new LinkedList<>(); } @Override public int getSize() { return list.getSize(); } @Override public boolean isEmpty() { return list.isEmpty(); } @Override public void push(E e) throws IllegalAccessException { list.addFirst(e); } @Override public E pop() throws IllegalAccessException { return list.removeFirst(); } @Override public E peek() throws IllegalAccessException { return list.getFirst(); } }
-
既然有了實現,那就拿
鏈表棧
和數組棧
比較下吧,創建測試函數private static double testStack(Stack<Integer> stack, int opCount) throws IllegalAccessException { 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; }
-
分別創建
鏈表棧
和數組棧
進行一百萬次的入棧和出站操作,比較兩者用時,貌似好像鏈表棧
好一點。
-
繼續加大數據量到一千萬次的入棧和出棧操作,此時鏈表棧的表現就不佳了。
原因大致是:數組棧
的pop和push操作基於數組尾部進行處理;而鏈表棧
的pop和push操作基於鏈表頭部操作,且投保操作含有創建新節點的操作(new Node),因此比較耗時。 -
棧
結構已經創建,那麼隊列
也是必不可少的,前文中的數組隊列
的構建是從頭部和尾部均進行了操作,由於出列操作的複雜度爲O(n),入列操作的複雜度爲O(1),進行了隊列結構的優化,於是產生了數組實現的循環隊列
,且性能要遠高於普通的數組隊列
。於是我們對鏈表
這一結構進行分析:-
由於存在
head
頭指針,頭部操作的複雜度爲O(1)【dummyHead的設定】 -
那麼基於這個原理,添加上
tail
尾指針,記錄鏈表尾部(索引),是否可以將尾部的操作複雜度降低呢?head
指針的定位是依賴於虛擬頭指針的結構設定,而tail
指針無此設定,若要進行尾部元素刪除操作,還需要定位到待刪除元素的前一元素,仍需要進行遍歷。 -
基於上述,
鏈表節點Node的next
設定,更有利於我們從鏈表首部進行出隊操作
,鏈表尾部進行入隊操作
。 -
採用
head
+tail
改造我們的LinkedListQueue
/** * @author by Jiangyf * @classname LinkedListQueue * @description 鏈表隊列 * @date 2019/9/28 16:35 */ public class LinkedListQueue<E> implements Queue<E> { /** * 節點內部類 */ private class Node { public E e; public Node next; public Node(E e, Node next) { this.e = e; this.next = next; } public Node(E e) { this(e, null); } public Node() { this(null, null); } @Override public String toString() { return "Node{" + "e=" + e + ", next=" + next + '}'; } } private Node head, tail; private int size; public LinkedListQueue() { this.head = null; this.tail = null; this.size = 0; } @Override public int getSize() { return size; } @Override public boolean isEmpty() { return size == 0; } @Override public void enqueue(E e) { // 入隊 從鏈表尾部進行 if (tail == null) { // 表示鏈表爲空 tail = new Node(e); head = tail; } else { // 不爲空,指向新創建的元素,尾指針後移 tail.next = new Node(e); tail = tail.next; } size ++; } @Override public E dequeue() { // 出隊 從鏈表頭部進行 if (isEmpty()) { throw new IllegalArgumentException("Queue is empty"); } // 獲取待出隊元素 Node retNode = head; // 頭指針後移 head = head.next; // 待刪除元素與鏈表斷開 retNode.next = null; if (head == null) { // 鏈表中僅有一個元素的情況,頭指針移動後變爲空鏈表 tail = null; } size --; return retNode.e; } @Override public E getFront() { if (isEmpty()) { throw new IllegalArgumentException("Queue is empty"); } return head.e; } }
-
同樣,與之前的
數組隊列
、循環隊列
、鏈表隊列
進行下性能測試(10萬數量級)
可見,循環隊列和鏈表隊列的性能遠高於數組隊列,其原因就是頭尾指針動態控制數據結構,而數組隊列出列時要反覆的進行數據複製,因此消耗時間較長。
-