鏈表的簡單理解

  • 2020 年 4 月 10 日
  • 筆記

鏈表

​ 通過與數組相對比來理解鏈表,數組是一組連續的地址可以通過順移來遍歷,相對的鏈表是一組不連續的地址塊,每個地址塊都存儲了下一個地址塊的地址,可以通過這個存儲的地址來進行迭代,就像很多個連起來的數組,這樣解決了數組的擴容問題,用鏈表擴容的時候再也不需要,重新找一大塊位置了,只需要找到一個地址塊(Node)的大小就夠了,這也就帶來了一個缺點,因為這些Node不是連續的,想要直接讀取其中一個Node就要從頭節點(Head)開始一個一個迭代,這就不如數組可以直接通過對地址進行增加操作直接訪問,而且增加了存儲下一個節點地址數據的位置,使得存儲同樣的數據,鏈表佔用內存更大。

我們從鏈表頭指針一個一個向下,就可以遍歷整個鏈表,但是有一個缺點,只能前進,不能後退,這缺點有點太大了吧, 用指向下一個Node的指針來訪問下一個Node,那麼我們要訪問前一個Node,怎麼辦,自然是需要一個指向前一個Node的指針了,這就是雙(向)鏈表

既然都可以雙向訪問了,那我們讓它首尾相接也不算過分吧,這就是循環鏈表

這裡的鏈表都是沒有表頭的,有些時候我們可以把第一個節點設置為表頭,數據部分可以用來存儲鏈表的長度等有意義的數據。

Java實現

下面我們來看看LinkedList是怎麼來實現的,首先是節點的定義

private static class Node<E> {      E item;      Node<E> next;      Node<E> prev;        Node(Node<E> prev, E element, Node<E> next) {          this.item = element;          this.next = next;          this.prev = prev;      }  }  

item用來存儲數據,next用來存儲下一個節點的Node對象地址,prev用來存儲上一個節點的Node對象地址,操作我也們只看兩個一個是add一個是remove,因為這是基本操作,其餘的複雜操作都是基於這兩個操作的,查與改也就是簡單的遍歷,很容易理解。首先來看add

transient Node<E> first;  transient Node<E> last;    public boolean add(E e) {      linkLast(e);      return true;  }  
void linkLast(E e) {      final Node<E> l = last;      final Node<E> newNode = new Node<>(l, e, null);      last = newNode;      if (l == null)          first = newNode;      else          l.next = newNode;      size++;      modCount++;  }  

類內部定義了兩個變量,first和last,分別用來記錄頭節點和尾節點的

一開始使用一個臨時節點 l 來記錄尾節點的地址,然後把尾節點設置為新加入的節點,之後出現了兩種選擇,第一種是這個鏈表是空的,之前沒有節點,那麼這個鏈表的last就是null,也就是l是null,這樣的話頭尾節點都是這個新加入的節點,第二種是如果這個鏈表不是空的,那麼就是l不是null,這個新的節點要加到之前的節點的後面,也就是把這個節點的地址記錄到尾節點的next上,也就是l.next。add還有一個同名的重載函數,在指定的位置插入元素,根據之前的數組隨筆不難猜測,肯定是先要判斷index範圍,然後再進行插入,這個插入操作是和刪除相反的,所以我們就不看插入了,直接來看刪除

public boolean remove(Object o) {      if (o == null) {          for (Node<E> x = first; x != null; x = x.next) {              if (x.item == null) {                  unlink(x);                  return true;              }          }      } else {          for (Node<E> x = first; x != null; x = x.next) {              if (o.equals(x.item)) {                  unlink(x);                  return true;              }          }      }      return false;  }  

根據傳入的對象是不是null分別用循環來查找數據值與o相等的節點然後調用unlink

E unlink(Node<E> x) {      // assert x != null;      final E element = x.item;      final Node<E> next = x.next;      final Node<E> prev = x.prev;        if (prev == null) {      // 該節點的前一個節點是null 那他就是頭節點          first = next;		// 直接把first的值設置為下一個節點的地址就刪除了頭節點      } else {			    // 不是頭節點的話          prev.next = next; 	 // 該節點的前一個節點 的下一個節點 就應該是 這個節點的下一個節點          x.prev = null;		// 把存儲該節點前一個節點的變量賦值為null 方便gc      }        if (next == null) {		 // 該節點的下一個節點為null 那麼他就是尾節點          last = prev;		 // last所指向的節點就是該節點的前一個節點      } else {				// 不是尾節點的話          next.prev = prev;	 // 該節點的下一個節點 的前一個節點 就應該是 這個節點的前一個節點          x.next = null;		 // 把存儲該節點下一個節點的變量賦值為null 方便gc      }        x.item = null;			// 方便gc      size--;      modCount++;      return element;  }  

刪除尾節點和刪除頭節點異曲同工我就不畫圖了,還有單個節點的刪除就是fisrt和last都置為null

總結

鏈表通過在節點裏保存前後節點的地址,把一系列的節點連接起來,方便擴容,較為靈活,但是所佔空間也變大,具體使用鏈表還是數組需要根據實際需求來判斷。