線性表的鏈式儲存與實現
實現線性表的另一種方法是鏈式儲存,即用指標將儲存線性表中資料元素的那些單元依次串聯在一起。這種方法避免了在陣列中用連續的單元儲存元素的缺點,因而在執行插入或刪除運算時,不再需要移動元素來騰出空間或填補空缺。然而我們為此付出的代價是,需要在每個單元中設定指標來表示元素之間的邏輯關係,增加了額外的儲存空間的開銷。
所謂鏈式儲存其實就是通過連結串列來實現線性表,而連結串列有不同的形式,比如單鏈表、迴圈連結串列、雙向連結串列等。那麼我們首先來介紹一下連結串列。
1.連結串列
1.1 單鏈表
連結串列是一系列的儲存資料元素的單元通過指標串聯起來形成的,因此每個單元至少有兩個域,一個域用於資料元素的儲存,另一個域是指向其他單元的指標。這裡,具有一個數據域和多個指標域的儲存單元通常稱為結點(Node)
最簡單的連結串列就是單鏈表,它有一個數據域與一個指標域,見下圖,資料域用來儲存資料元素,指標域用來指向下一個具有相同結構的結點。
Java中並沒有顯示的指標,而實際上物件的訪問就是使用指標來實現的,即在Java中使用物件的引用來代替指標。因此使用Java來實現連結串列的結點結構時,一個結點本身就是一個物件。結點的資料域可以使用Object型別的物件,而指標域就是指向一個結點的引用。下面就是一個單鏈表結點的定義:
public class Node {
private Object data;
private Node next;
public Node() {
this (null,null);
}
public Node(Object data, Node next) {
super();
this.data = data;
this.next = next;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
單鏈表就是通過上述定義的結點使用next
域依次串聯而成,結構如下:
連結串列的第一個結點和最後一個結點,分別稱為首結點和尾結點。尾結點的特徵是其next
域為null
。連結串列中的每一個結點的next
引用都相當於一個指標,指向另一個結點,藉助這些next
引用,我們可以從連結串列的首結點移動到尾結點。
在單鏈表中通常使用head引用來指向連結串列的首結點,由head引用可以完成對整個連結串列中所有結點的訪問。有時也可以根據需要使用指向尾結點的tail引用來方便某些操作的實現。
與陣列類似,單鏈表的結點也具有一個線性次序,即如果結點P的next引用指向結點S,則P就是S的直接前驅,而S是P的直接後繼。單鏈表的一個重要特性就是隻能通過前驅結點找到後繼結點,而無法從後繼結點找到前驅結點。
單鏈表的查詢操作
在單鏈表中進行查詢操作,只能從連結串列的首結點開始,通過每個結點的next引用來依次訪問連結串列中的每個結點以完成相應的查詢操作。如下圖:
在單鏈表中查詢操作的時間複雜度與在陣列中一樣,也是O(n)。
單鏈表的插入操作
在單鏈表中資料元素的插入,是通過在連結串列中插入資料元素所屬的結點來完成的。對於連結串列的不同位置,比如表頭、表的中間位置和表尾,插入的過程會有區別,如下圖,a,b,c分別為在這三個位置的插入過程:
在已知單鏈表中某個結點引用的基礎上,完成結點的插入操作的需要O(1)時間,這要比陣列的插入操作快很多。但是,通常我們的需求是在第i個結點之前插入一個新結點,這樣的話,我們就必須先找到第i-1個結點,所以在插入之前也需要一個查詢操作,故它的時間複雜度是O(n)。
單鏈表的刪除操作
單鏈表的刪除操作也是通過刪除結點來完成的。同樣的,在連結串列的表頭、表的中間位置和表尾刪除結點,過程也是不一樣的,如下:
在單鏈表中刪除一個結點時,除首結點外都必須知道該結點的直接前驅結點的引用。也就是說,如果在已知單鏈表中某個結點引用的基礎上,完成其後續結點的刪除操作需要的時間是O(1),這比在陣列中的刪除操作要快的多。但是在不知道結點引用的情況下,我們刪除第i個結點,需要先找到第i-1個結點,故它的時間複雜度也為O(n)。
1.2 迴圈連結串列
迴圈連結串列(circular linked list)是另一種形式的鏈式儲存結構。它的特點是表中的最後一個結點的指標域指向首結點,整個連結串列形成一個環。由此,從表中任一結點出發均可找到表中其他結點。
迴圈連結串列的操作和單鏈表基本一致,差別僅在於演算法中的迴圈條件不是next域為空,而是next域是否等於首結點。
1.3 雙向連結串列
上述討論的鏈式儲存結構的結點中只有一個指示直接後繼的指標域,由此,從某個結點出發只能順時針往後尋找其他結點。若要尋找結點的直接前驅,,則需從表頭出發,這需要O(n)的時間。
為此,我們可以擴充套件單鏈表的結點結構,使得通過一個結點的引用,不但能夠訪問其後繼結點,也可以方便地訪問其前驅結點。擴充套件單鏈表結點結構的方法是,在單鏈表結點結構中新增加一個域,該域用於指向結點的直接前驅結點,如下圖所示結點結構:
同樣的,我們也可以在Java中定義雙向連結串列的結點:
public class DuLNode {
private Object data;
private DuLNode pre;
private DuLNode next;
public DuLNode() {
this(null, null, null);
}
public DuLNode(Object data, DuLNode pre, DuLNode next) {
super();
this.data = data;
this.pre = pre;
this.next = next;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public DuLNode getPre() {
return pre;
}
public void setPre(DuLNode pre) {
this.pre = pre;
}
public DuLNode getNext() {
return next;
}
public void setNext(DuLNode next) {
this.next = next;
}
}
雙向連結串列是通過上述定義的結點使用pre以及next域依次串聯在一起而形成的。一個雙向連結串列的結構如下:
在雙向連結串列中同樣需要完成資料元素的查詢、插入、刪除等操作。在雙向連結串列中進行查詢與在單鏈表中類似,只不過在雙向連結串列中查詢操作可以從連結串列的首結點開始,也可以從連結串列的尾結點開始,但是需要的時間和在單鏈表中一樣,在平均情況下,需要比較大約一半的資料元素,即T(n)= n/2,時間複雜度也為O(n)。
單鏈表的插入操作,除了首結點以外必須在某個已知結點後面進行,而在雙向連結串列中插入操作在一個已知結點之前或之後都可以進行。例如在某個結點p之前插入一個新結點的過程如下:
在結點p之後插入一個新結點的操作與上述類似。
單鏈表的刪除操作,除了首結點之外必須在知道待刪結點的前驅結點的基礎上才能進行,而在雙向連結串列中在已知某個結點引用的前提下,可以完成該結點自身的刪除,如下所示:
2.線性表的單鏈表實現
在使用連結串列實現線性表時,既可以使用單鏈表,也可以使用雙向連結串列。在這裡我們首先使用單鏈表來實現線性表。
在使用單鏈表實現線性表時,線性表中的每個資料元素對應單鏈表中的一個結點,而線性表元素之間的邏輯關係是通過單鏈表中元素在結點之間的指向來表示的。
通常,在使用單鏈表實現線性表的時候,為了使程式更加簡潔,我們通常在單鏈表的最前面新增一個啞元結點,也稱為頭結點。在頭結點中不儲存任何實質的資料物件,其next域指向線性表中0號元素所在的結點。頭結點的引入可以使線性表運算中的一些邊界條件更容易處理。一個帶頭結點的單鏈表實現線性表的結構圖如下:
通過上圖我們可以發現,在帶頭結點的單鏈表中,任何基於序號的插入和刪除,都可以轉化為在某個特定結點之後完成結點的插入、刪除,而不需要考慮插入和刪除的位置是在連結串列的首部、中間還是尾部。這給我們帶來了很大的方便。
下述程式碼通過單鏈表實現了一個線性表:
public class MySingleLinkedList<T> {
/**
* 內部的結點類
*
* @author Gavin
*
*/
private class Node {
private T data;
private Node next;
public Node() {
this(null, null);
}
public Node(T data, Node next) {
super();
this.data = data;
this.next = next;
}
public Object getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
private Node head; // 單鏈表頭結點的引用,頭結點資料域為空
private int size; // 線性表中資料元素的個數
public MySingleLinkedList() {
head = new Node();
size = 0;
}
/**
* 返回線性表的大小,即線性表中資料元素的個數
*
* @return
*/
public int size() {
return size;
}
/**
* 如果線性表為空,則返回true,否則返回false
*
* @return
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* 判斷元素是否存在於線性表中,存在則返回true,否則返回false
*
* @param e
* @return
*/
public boolean contains(T e) {
return indexOf(e) >= 0;
}
/**
* 返回元素e線上性表中的序號,即下標。 要注意e為null的情況。如果不存在元素e,則返回-1。
*
* @param e
* @return
*/
public int indexOf(T e) {
Node p = head.getNext();
int index = 0;
if (e == null) {
while (p != null) {
if (p.getData() == null) {
return index;
} else {
p = p.getNext();
index += 1;
}
}
} else {
while (p != null) {
if (e.equals(p.getData())) {
return index;
} else {
p = p.getNext();
index += 1;
}
}
}
return -1;
}
/**
* 輔助方法,獲取元素e所在結點的前驅結點。考慮元素e為null的情況
*
* @param e
* @return
*/
private Node getPreNode(T e) {
Node p = head;
if(e == null){
while(p.getNext() != null){
if(p.getNext().getData() == null){
return p;
}
}
}else{
while(p.getNext() != null){
if(e.equals(p.getNext().getData())){
return p;
}
}
}
return null;
}
/**
* 輔助方法,獲取序號為i(0<=i<=size)的元素所在結點的前驅結點。
* i=0時,獲取到的前驅結點即為頭結點。i=size時,獲取到的前驅結點即為最後一個結點
*
* @param i
* @return
*/
private Node getPreNode(int i) {
Node p = head;
for (; i > 0; i--) {
p = p.getNext();
}
return p;
}
/**
* 輔助方法,獲取序號為i(0<=i<size)的元素所在的結點
*
* @param i
* @return
*/
private Node getNode(int i) {
Node p = head;
for (; i >= 0; i--) {
p = p.getNext();
}
return p;
}
/**
* 將資料元素e插入到序號為i的位置
*
* @param i
* @param e
*/
public void add(int i, T e) {
// 下標越界的情況
if (i < 0 || i > size) {
throw new IndexOutOfBoundsException();
}
// 前驅結點
Node preNode = getPreNode(i);
// 當前結點
Node newNode = new Node();
newNode.setData(e);
// 插入結點
newNode.setNext(preNode.getNext());
preNode.setNext(newNode);
// 資料大小增加1
size += 1;
}
/**
* 預設將資料元素插入到最後,即序號為size的位置
*
* @param e
*/
public void add(T e) {
add(size(), e);
}
/**
* 刪除序號為i的資料元素
*
* @param i
* @return
*/
public T remove(int i) {
// 下標越界的情況
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException();
}
// 前驅結點
Node preNode = getPreNode(i);
T oldData = (T) preNode.getNext().getData();
preNode.setNext(preNode.getNext().getNext());
size -= 1;
return oldData;
}
/**
* 刪除線性表中第一個與e相同的元素
*
* @param e
* @return
*/
public boolean remove(T e) {
// 前驅結點
Node preNode = getPreNode(e);
if (preNode == null) {
return false;
}
preNode.setNext(preNode.getNext().getNext());
size -= 1;
return true;
}
/**
* 獲取序號為i的資料元素
*
* @param i
* @return
*/
public T get(int i) {
// 下標越界的情況
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException();
}
Node node = getNode(i);
return (T) node.getData();
}
/**
* 替換線性表中序號為i的資料元素為e,返回原資料元素
*
* @param i
* @param e
* @return
*/
public T set(int i, T e) {
// 下標越界的情況
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException();
}
Node node = getNode(i);
T oldData = (T) node.getData();
node.setData(e);
return oldData;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("[");
Node p = head.next;
int index = 0;
while (p != null) {
stringBuilder.append(p.getData());
if (index == size - 1) {
stringBuilder.append("]");
} else {
stringBuilder.append(", ");
}
p = p.getNext();
index += 1;
}
return stringBuilder.toString();
}
}
說明:在MySingleLinkedList
類中有2個成員變數,其中head
是帶頭結點的單鏈表的頭結點的引用;而size是線性表的大小,也就是資料元素的個數。
方法size()
,isEmpty()
的時間複雜度均為O(1),通過成員變數size即可以直接判斷出線性表的大小以及線性表是否為空。
由於連結串列中每個結點在記憶體中的地址不是連續的,所以連結串列不具有隨機存取的特性。因此,線性表中的一些基於資料元素或序號的插入、刪除操作均依賴於對應元素在單鏈表中的前驅結點的引用。故它們的時間複雜度都是O(n)。
3.線性表的雙向連結串列實現
另外,我們可以選擇使用雙向連結串列來實現線性表。
在使用雙向連結串列實現線性表時,為了更加簡潔,我們可以使用帶兩個啞元結點的雙向連結串列,即head頭結點,和tail尾結點,它們的資料域data為null,頭結點的pre為null,而尾結點的next為空。如下結構所示:
下述程式碼實現了利用雙向連結串列實現線性表:
public class MyDoubleLinkedList<T> {
/**
* 內部結點類
*
* @author Gavin
*
*/
private class Node {
private T data;
private Node pre;
private Node next;
public Node() {
this(null, null, null);
}
public Node(T data, Node pre, Node next) {
super();
this.data = data;
this.pre = pre;
this.next = next;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node getPre() {
return pre;
}
public void setPre(Node pre) {
this.pre = pre;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
private int size;// 線性表的長度,即資料元素的個數
private Node head;// 頭結點
private Node tail;// 尾結點
public MyDoubleLinkedList() {
// 初始化
size = 0;
head = new Node();
tail = new Node();
head.setNext(tail);
tail.setPre(head);
}
/**
* 獲取線性表的大小,即資料元素的個數
*
* @return
*/
public int size() {
return size;
}
/**
* 判斷線性表是否為空,為空則返回true,否則返回false
*
* @return
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* 返回元素e線上性表中的第一個序號。沒有元素e則返回-1
*
* @param e
* @return
*/
public int indexOf(T e) {
// 從頭結點開始往後找
Node p = head.getNext();
int index = 0;
if (e == null) {
while (p != null) {
if (p.getData() == null) {
return index;
} else {
p = p.getNext();
index += 1;
}
}
} else {
while (p != null) {
if (e.equals(p.getData())) {
return index;
} else {
p = p.getNext();
index += 1;
}
}
}
return -1;
}
/**
* 返回元素e線上性表中的最後一個序號。沒有元素e則返回-1
*
* @param e
* @return
*/
public int lastIndexOf(T e) {
// 從尾結點開始往前找
Node p = tail.getPre();
int index = size-1;
if (e == null) {
while (p != null) {
if (p.getData() == null) {
return index;
} else {
p = p.getPre();
index -= 1;
}
}
} else {
while (p != null) {
if (e.equals(p.getData())) {
return index;
} else {
p = p.getPre();
index -= 1;
}
}
}
return -1;
}
/**
* 判斷某個元素是否線上性表中,是則返回true,否則返回false
*
* @param e
* @return
*/
public boolean contains(T e) {
return indexOf(e) >= 0;
}
/**
* 輔助方法,獲取序號為i(0<=i<=size)的元素所在的結點的前驅結點
* @param i
* @return
*/
private Node getPreNode(int i) {
Node p = null;
if (i < size() / 2) {
// 如果在前半段,就從頭結點開始查詢
p = head;
for (; i > 0; i--) {
p = p.getNext();
}
} else {
// 如果在後半段,就從尾結點開始查詢
p = tail;
for (; i <= size; i++) {
p = p.getPre();
}
}
return p;
}
/**
* 輔助方法,獲取元素e所在結點的前驅結點。考慮元素e為null的情況
*
* @param e
* @return
*/
private Node getPreNode(T e) {
Node p = head;
if(e == null){
while(p.getNext() != null){
if(p.getNext().getData() == null){
return p;
}
}
}else{
while(p.getNext() != null){
if(e.equals(p.getNext().getData())){
return p;
}
}
}
return null;
}
/**
* 在序號為i的位置上插入結點
* @param i
* @param e
*/
public void add(int i, T e) {
// 下標越界
if(i < 0 || i > size){
throw new IndexOutOfBoundsException();
}
Node preNode = getPreNode(i);
Node newNode = new Node(e,preNode,preNode.getNext());
// 改變前後兩個結點的指標
preNode.getNext().setPre(newNode);
preNode.setNext(newNode);
// 大小加1
size += 1;
}
/**
* 預設線上性表的末尾新增元素
* @param e
*/
public void add(T e){
add(size(), e);
}
/**
* 刪除序號為i的元素
* @param i
* @return
*/
public T remove(int i){
// 下標越界
if(i < 0 || i >= size){
throw new IndexOutOfBoundsException();
}
Node preNode = getPreNode(i);
T oldData = preNode.getNext().getData();
preNode.setNext(preNode.getNext().getNext());
preNode.getNext().setPre(preNode);
size -= 1;
return oldData;
}
/**
* 刪除線性表中第一個與e相同的元素
*
* @param e
* @return
*/
public boolean remove(T e) {
// 前驅結點
Node preNode = getPreNode(e);
if (preNode == null) {
return false;
}
preNode.setNext(preNode.getNext().getNext());
preNode.getNext().setPre(preNode);
size -= 1;
return true;
}
/**
* 獲取序號為i的資料元素
*
* @param i
* @return
*/
public T get(int i){
// 下標越界
if(i < 0 || i >= size){
throw new IndexOutOfBoundsException();
}
return getPreNode(i).getNext().getData();
}
/**
* 替換線性表中序號為i的資料元素為e,返回原資料元素
*
* @param i
* @param e
* @return
*/
public T set(int i, T e) {
// 下標越界的情況
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException();
}
Node node = getPreNode(i).getNext();
T oldData = (T) node.getData();
node.setData(e);
return oldData;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("[");
Node p = head.next;
int index = 0;
while (p.getNext()!= null) {
stringBuilder.append(p.getData());
if (index == size - 1) {
stringBuilder.append("]");
} else {
stringBuilder.append(", ");
}
p = p.getNext();
index += 1;
}
return stringBuilder.toString();
}
}
說明:MyDoubleLinkedList
中有3個成員變數,其中size是線性表的大小,head是頭結點,tail是尾結點。
利用雙向連結串列實現線性表與單向連結串列實現的線性表基本上是類似的,只是在增加和刪除的時候修改的指標不一樣。
另外,利用雙向連結串列,在對元素進行隨機訪問的時候,可以根據序號選擇從頭結點或者尾結點進行遍歷,相對於單鏈表只能從頭結點進行遍歷來說,速度上也會快很多。
在時間複雜度上,與單鏈表實現的線性表是一樣的。size()
和isEmpty()
方法需要O(1)時間,而其他的查詢,增加和刪除等,都需要O(n)時間。