三、鏈表(LinkedList)
下面將有一種新的數據存儲結構,它可以解決上面的一些問題。這種數據存儲結構就是鏈表。鏈表可能是繼數組之後第二種使用最廣泛的通用存儲結構。
- 單鏈表
- 雙端鏈表
- 有序鏈表
- 雙向列表
- 有迭代器的列表
鏈表與數組一樣,都作爲數據的基本存儲結構,但是在存儲原理上二者是不同的。在數組中,數據是存儲在一段連續的內存空間中,我們可以通過下標來訪問數組中的元素;而在鏈表中,元素是存儲在不同的內存空間中,前一個元素的位置維護了後一個元素在內存中的地址,在Java中,就是前一個元素維護了後一個元素的引用。在本教程我們,我們將鏈表中的每個元素稱之爲一個節點(Node)。對比數組, 鏈表的數據結構可以用下圖表示:
這張圖顯示了一個鏈表的數據結構,鏈表中的每個Node都維護2個信息:一個是這個Node自身存儲的數據Data,另一個是下一個Node的引用,圖中用Next表示。對於最後一個Node,因爲沒有下一個元素了,所以其並沒有引用其他元素,在圖中用紫色框來表示。
這張圖主要顯示的是鏈表中Node的內部結構和Node之間的關係。一般情況下,我們在鏈表中還要維護第一個Node的引用,原因是在鏈表中訪問數據必須通過前一個元素才能訪問下一個元素,如果不知道第一個Node的話,後面的Node都不可以訪問。事實上,對鏈表中元素的訪問,都是從第一個Node中開始的,第一個Node是整個鏈表的入口;而在數組中,我們可以通過下標進行訪問元素。
public class Node {
//Node中維護的數據
private Object data;
//下一個元素的引用
private Node next;
// setters and getters
}
1、單鏈表Java實現
1)、SingleLinkList中要維護的信息:維護第一個節點(firstNode)的引用,作爲整個鏈表的入口;
2)、插入操作分析:基於鏈表的特性,插入到鏈表的第一個位置是非常快的,因爲只要改變fisrtNode的引用即可。因此對於單鏈表,我們會提供addFirst方法。
3)、查找操作分析:從鏈表的fisrtNode開始進行查找,如果確定Node中維護的data就是我們要查找的數據,即返回,如果不是,根據next獲取下一個節點,重複這些步驟,直到找到最後一個元素,如果最後一個都沒找到,返回null。
4)、刪除操作分析 : 首先查找到要刪除的元素節點,同時將這個節點的上一個節點和下一個節點也要記錄下來,只要將上一個節點的next引用直接指向下一個節點即可,這就相當於 刪除了這個節點。如果要刪除的是第一個節點,直接將LinkList的firstNode指向第二個節點即可。如果刪除的是最後一個節點,只要將上一個節 點的next引用置爲null即可。上述分析,可以刪除任意節點,具有通用性但是效率較低。通常情況下,我們還會提供一個removeFirst方法,因爲這個方法效率較高,同樣只要改變fisrtNode的引用即可。
此外,根據情況而定,可以選擇是否要維護鏈表中元素的數量size,不過這不是實現一個鏈表必須的核心特性。
下面是代碼實現:
public class SingleLinkList<V> {
protected Node firstNode = null;// 鏈表的第一個節點
protected int size;// 鏈表中維護的節點總數
/**
* 添加到鏈表最前面
* @return
*/
public Node addFirst(V v) {
Node node = new Node();
node.setData(v);
Node currentFirst = firstNode;
node.setNext(currentFirst);
firstNode = node;
size++;
return node;
}
/**
* 如果鏈表中包含要刪除的元素,刪除第一個匹配上的要刪除的元素
*/
public void remove(V v) {
if (size == 0) {
return;
}
if (size == 1) {
firstNode = null;
size--;
return;
}
if (Objects.equals(firstNode.getData(), v)) {
firstNode = firstNode.getNext();
size--;
}
Node pre = firstNode;
Node next = pre.getNext();
while (next != null) {
if (Objects.equals(next.getData(), v)) {
pre.setNext(next.getNext());
size--;
next = pre.getNext();
}else {
pre = pre.getNext();
next = pre.getNext();
}
}
}
/**
* 是否包含,包含返回true,不包含返回false
*/
public boolean contains(V v){
if (size == 0) {
return false;
}
Node current = firstNode;
while (current != null) {
if (Objects.equals(v, current.getData())) {
return true;
}
current = current.getNext();
}
return false;
}
/**
* 獲取第一個元素
*/
public V getFirst(){
if (size == 0) {
return null;
}
return (V)firstNode.getData();
}
/**
* 刪除第一個元素
*/
public V removeFirst(){
if (size == 0) {
return null;
}
Node temp = firstNode.getNext();
firstNode = temp;
if (temp == null) {
return null;
}
return (V)temp.getData();
}
/**
* 打印鏈表的所有元素
*/
public void showAll(){
if (size != 0) {
Node current = firstNode;
while (current != null){
System.out.print(current.getData() + "/");
current = current.getNext();
}
}
}
/**
* 獲取元素個數
*/
public int getSize(){
return size;
}
}
2、雙端鏈表Java實現
雙端鏈表與傳統的鏈表非常類似,但是它有一個新增的特性:即對鏈表中最後一個節點的引用lastNode。我們可以像在單鏈表中在表頭插入一個元素一樣,在鏈表的尾端插入元素。如果不維護對最後一個節點的引用,我們必須要迭代整個鏈表才能得到最後一個節點,然後再插入,效率很低。因此我們在雙鏈表中添加一個addLast方法,用於添加節點到末尾。
addLast方法分析:直接將鏈表中維護的lastNode的next引用指向新的節點,再將lastNode的引用指向新的節點即可。
因爲單鏈表中,大部分的代碼在雙端鏈表中都可以重用,所以此處我們編寫的DoubleLinkList只要繼承SingleLinkList,添加必要的屬性和方法支持從尾部操作即可。
下面是代碼實現:
public class DoubleLinkList<V> extends SingleLinkList<V> {
protected Node lastNode = null;
/**
* 添加到鏈表最後
*/
public void addLast(V v) {
Node node = new Node();
node.setData(v);
if (size == 0) {// 說明沒有任何元素,說明第一個元素
firstNode = node;
} else {// 如果有元素,將最後一個節點的next指向新的節點即可
/*
* 這裏有一個要注意的地方: 當size=1的時候,firstNode和lastNode指向同一個引用
* 因此lastNode.setNext時,fisrtNode的next引用也會改變;
* 當size!=1的時候,lastNode的next的改變與firstNode無關
*/
lastNode.setNext(node);
}
// 將lastNode引用指向新node
lastNode = node;
size++;
}
/**
* 當鏈表中沒有元素時,清空lastNode引用
*/
@Override
public void remove(V v) {
super.remove(v);
if (size == 0) {
lastNode = null;
}
}
/**
* 因爲在SingleLinkList中並沒有維護lastNode的信息,我們要自己維護
*/
@Override
public Node addFirst(V v) {
Node node = super.addFirst(v);
if (size == 1) {// 如果鏈表爲size爲1,將lastNode指向當前節點
lastNode = node;
}
return node;
}
}
3、有序鏈表
所謂有序鏈表,就是鏈表中Node節點之間的引用關係是根據Node中維護的數據data的某個字段爲key值進行排序的。爲了在一個有序鏈表中插入,算法必須首先搜索鏈表,直到找到合適的位置:它恰好在第一個比它大的數據項前面。
當算法找到了要插入的數據項的位置,用通常的方式插入數據項:把新的節點Node指向下一個節點,然後把前一個節點Node的next字段改爲指向新的節點。然而,需要考慮一些特殊情況,連接點有可能插入在表頭或者表尾。