前篇、鏈表的概括
1、鏈表(Linked list)說明
是一種常見的基礎數據結構,是一種線性表,但是並不會按線性的順序存儲數據,而是在每一個節點裏存到下一個節點的指針(Pointer)。由於不必須按順序存儲,鏈表在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是查找一個節點或者訪問特定編號的節點則需要O(n)的時間,而順序表相應的時間複雜度分別是O(logn)和O(1)。
使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,空間開銷比較大。
在計算機科學中,鏈表作爲一種基礎的數據結構可以用來生成其它類型的數據結構。鏈表通常由一連串節點組成,每個節點包含任意的實例數據(data fields)和一或兩個用來指向上一個/或下一個節點的位置的鏈接(“links”)。鏈表最明顯的好處就是,常規數組排列關聯項目的方式可能不同於這些數據項目在記憶體或磁盤上順序,數據的訪問往往要在不同的排列順序中轉換。而鏈表是一種自我指示數據類型,因爲它包含指向另一個相同類型的數據的指針(鏈接)。鏈表允許插入和移除表上任意位置上的節點,但是不允許隨機存取。鏈表有很多種不同的類型:單向鏈表,雙向鏈表以及循環鏈表。
2、理解方式
基於鏈式存儲結構的鏈表。你可以這樣理解,比如說你要找一個人,名字叫張三,你首先跑到A,發現沒有,A告訴你B可能知道,你跑到了B。B說C可能知道,你跑到了C,張三果然在C那裏,如果沒有就這樣一直不停的去找。一張圖看一下
3、歷史
鏈表開發於1955-56,由當時所屬於蘭德公司(英語:RAND Corporation)的艾倫紐維爾(Allen Newell),克里夫肖(Cliff Shaw)和赫伯特西蒙(Herbert Simon)在他們編寫的信息處理語言(IPL)中做爲原始數據類型所編寫。IPL被作者們用來開發幾種早期的人工智能程序,包括邏輯推理機,通用問題解算器和一個計算機象棋程序。
4、java 底層鏈表的使用
java中有很多集合類底層都是通過鏈表來實現的。而且面試的時候,鏈表的實現是經常考的一個知識點, 鏈表典型的使用就是 java.util 包下的集合LinkedList (如圖)
一、單鏈表
1、描敘
鏈表中最簡單的一種是單向鏈表,它包含兩個域,一個信息域和一個指針域。這個鏈接指向列表中的下一個節點,而最後一個節點則指向一個空值。
鏈表最基本的結構是在每個節點保存數據和到下一個節點的地址,在最後一個節點保存一個特殊的結束標記,另外在一個固定的位置保存指向第一個節點的指針,有的時候也會同時儲存指向最後一個節點的指針。一般查找一個節點的時候需要從第一個節點開始每次訪問下一個節點,一直訪問到需要的位置。但是也可以提前把一個節點的位置另外保存起來,然後直接訪問。當然如果只是訪問數據就沒必要了,不如在鏈表上儲存指向實際數據的指針。這樣一般是爲了訪問鏈表中的下一個或者前一個(需要儲存反向的指針,見下面的雙向鏈表)節點。
2、圖文示例
一個單向鏈表的節點被分成兩個部分。第一個部分保存或者顯示關於節點的信息,第二個部分存儲下一個節點的地址。單向鏈表只可向一個方向遍歷。
3、代碼示例
創建了一個單鏈表
----實現了
尾部添加 (效率高)
索引查詢 (效率低)
索引刪除 (效率低)
size 獲取長度
----未實現
頭部添加數據
中間添加數據
索引更新數據
等等
package linkedList;
/**
* TODO 單鏈表
*
* @author ws
* @mail [email protected]
* @date 2020/5/27 0027 14:45
*/
public class LinkedList {
/**
* 頭節點
*/
private Node head;
/**
* 尾節點
*/
private Node tail;
/**
* 長度
*/
private int size;
/**
* 添加元素到結尾處
*
* @param data
*/
public void plus(Object data) {
if (head == null) {
// 第一個,也是最後一個
head = new Node(data);
tail = head;
} else {
tail.next = new Node(data);
tail = tail.next;
}
size++;
}
/**
* 根據索引獲取元素,查詢速度慢
*
* @param index
*/
public Object get(int index) {
this.checkElementIndex(index);
return this.getNode(index).data;
}
/**
* 刪除元素, 把刪除的上一個元素的下一個元素, 指向要刪除的下一個元素, 當索引爲0時,把下一個元素設置爲頭元素
*
* @param index
*/
public Object remove(int index) {
this.checkElementIndex(index);
Node delNode = null;
if (index == 0) {
// 要刪除的元素
delNode = getNode(index);
// 下一個元素
Node nextNode = delNode.next;
this.head = nextNode;
} else {
// 要刪除的上一個元素
Node preNode = getNode(index - 1);
// 要刪除的元素
delNode = preNode.next;
// 下一個元素
Node nextNode = delNode.next;
// 賦值
preNode.setNext(nextNode);
}
Object data = delNode.data;
// 賦空,讓jvm回收
delNode = null;
size--;
// 刪除
return data;
}
/**
* 獲取鏈表長度
*/
public int getSize() {
return size;
}
/**
* TODO 獲取頭元素
*/
private Node getHead() {
return head;
}
/**
* TODO 根據索引獲取節點,查詢速度慢
*
* @param index
*/
private Node getNode(int index) {
this.checkElementIndex(index);
//獲取第一個節點, 循環獲取下一個節點(依次 .next.next.next)
Node node = head;
if (head != null) {
for (int i = 0; i < index; i++) {
node = node.next;
}
}
return node;
}
/**
* TODO 判斷下標
*
* @param index
* @return void
* @date 2019/11/28 0028 9:37
*/
private void checkElementIndex(int index) {
if (index < 0 && index >= size) {
throw new IndexOutOfBoundsException("下標越界!");
}
}
/**
* TODO 鏈表元素
*/
class Node {
//節點值
private Object data;
//當前節點的下一個元素
private Node next;
public void setNext(Node next) {
this.next = next;
}
public Node(Object data) {
this.data = data;
}
}
測試代碼
public static void main(String[] args) {
// 添加
LinkedList linkedList = new LinkedList();
linkedList.plus("1");
linkedList.plus("2");
linkedList.plus("3");
// 獲取鏈表數據方式 .next.next.next
// System.out.println(linkedList.getHead().getData());
// System.out.println(linkedList.getHead().getNext().getData());
// System.out.println(linkedList.getHead().getNext().getNext().getData());
// 遍歷鏈表--> 每次 get都要 .next.next.next 一層一層的查下去,所以查詢速度非常忙
for (int i = 0; i < linkedList.getSize(); i++) {
System.out.println(linkedList.get(i));
}
// 刪除
System.out.println("刪除了數據:" + linkedList.remove(0));
// 遍歷
for (int i = 0; i < linkedList.getSize(); i++) {
System.out.println(linkedList.get(i));
}
}
}
二、雙鏈表 (效果同 LinkedList)
1、描敘
雙向鏈表也叫雙鏈表。雙向鏈表中不僅有指向後一個節點的指針,還有指向前一個節點的指針。這樣可以從任何一個節點訪問前一個節點,當然也可以訪問後一個節點,以至整個鏈表。一般是在需要大批量的另外儲存數據在鏈表中的位置的時候用。雙向鏈表也可以配合下面的其他鏈表的擴展使用。
由於另外儲存了指向鏈表內容的指針,並且可能會修改相鄰的節點,有的時候第一個節點可能會被刪除或者在之前添加一個新的節點。這時候就要修改指向首個節點的指針。有一種方便的可以消除這種特殊情況的方法是在最後一個節點之後、第一個節點之前儲存一個永遠不會被刪除或者移動的虛擬節點,形成一個下面說的循環鏈表。這個虛擬節點之後的節點就是真正的第一個節點。這種情況通常可以用這個虛擬節點直接表示這個鏈表,對於把鏈表單獨的存在數組裏的情況,也可以直接用這個數組表示鏈表並用第0個或者第-1個(如果編譯器支持)節點固定的表示這個虛擬節點。
2、圖文示例
“雙向鏈表”或“雙面鏈表”。每個節點有兩個連接:一個指向前一個節點,(當此“連接”爲第一個“連接”時,指向空值或者空列表);而另一個指向下一個節點,(當此“連接”爲最後一個“連接”時,指向空值或者空列表)
3、代碼示例
創建一個雙鏈表
實現了
1、add 最後添加節點數據 (效率高)
2、push 最前添加節點數據 (效率高)
3、get 索引查詢 (效率低)
4、remove 索引刪除 (效率低)
5、size 獲取長度
package linkedList;
/**
* 雙鏈表
*
* @author ws
* @mail [email protected]
* @date 2020/5/27 0027 14:45
*/
public class XiJiaLinkedList<E> {
/**
* 長度
*/
private int size = 0;
/**
* 第一個節點
*/
private Node first;
/**
* 最後一個節點
*/
private Node last;
/**
* TODO 獲取長度
*
* @return int
* @date 2019/11/28 0028 9:37
*/
public int getSize() {
return size;
}
/**
* TODO 添加數據(在最後)
*
* @param e
* @return void
* @date 2019/11/28 0028 9:26
*/
public void add(E e) {
Node node = new Node();
node.object = e;
//如果沒有上一個節點,首次添加爲第一個節點
if (first == null) {
first = node;
} else {
// 當前節點的上一個節點爲最後一個節點
node.prev = last;
// 上次最後一個節點的下一個節點爲當前新添加節點
last.next = node;
}
//當前新添加節點(最後)
last = node;
size++;
}
/**
* TODO 添加數據(在最前)
*
* @param e
* @return void
* @date 2019/11/28 0028 9:26
*/
public void push(E e) {
Node node = new Node();
node.object = e;
node.prev = null;
//判斷是否存在第一個節點,存在則爲當前節點的下一個節點, 並賦值上一個節點爲當前節點
if (first != null) {
node.next = first;
first.prev = node;
}
//首次添加最後節點=當前節點
if (last == null) {
last = node;
}
//第一個節點
first = node;
size++;
}
/**
* TODO 查詢
*
* @param index
* @return java.lang.Object
* @date 2019/11/28 0028 9:32
*/
public Object get(int index) {
checkElementIndex(index);
return getNode(index).object;
}
/**
* TODO 查詢實現,依次 .next可以看出查詢效率低
*
* @param index
* @return LinkedList.XiJiaLinkedList<E>.Node
* @date 2019/11/28 0028 9:34
*/
private Node getNode(int index) {
checkElementIndex(index);
Node node = first;
if (first != null) {
//獲取第一個節點
for (int i = 0; i < index; i++) {
//獲取下一個節點(依次 .next.next.next)
node = node.next;
}
}
return node;
}
/**
* TODO 刪除
*
* @param index
* @return void
* @date 2019/11/28 0028 9:37
*/
public E remove(int index) {
checkElementIndex(index);
//要刪除的節點
XiJiaLinkedList<E>.Node delNode = getNode(index);
//獲取刪除元素的上下節點
XiJiaLinkedList<E>.Node prevNode = delNode.prev;
XiJiaLinkedList<E>.Node nextNode = delNode.next;
if (prevNode == null) {
//要刪除的節點爲頭節點,下一個節點爲頭元素
first = nextNode;
first.prev = null;
} else if (nextNode == null) {
//要刪除的節點爲尾節點
last = prevNode;
last.next = null;
} else {
//中間的節點
prevNode.next = nextNode;
nextNode.prev = prevNode;
}
//GC回收
Object obj = delNode.object;
delNode = null;
size--;
return (E) obj;
}
/**
* TODO 判斷下標
*
* @param index
* @return void
* @date 2019/11/28 0028 9:37
*/
private void checkElementIndex(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("下標越界!");
}
}
/**
* TODO Node節點對象
*
* @date 2019/11/28 0028 9:48
* @return
*/
private class Node {
/**
* 節點內容
*/
private Object object;
/**
* 上一個節點
*/
private Node prev;
/**
* 下一個節點
*/
private Node next;
}
測試代碼
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
XiJiaLinkedList<String> list = new XiJiaLinkedList<String>();
list.add("張三");
list.add("李四");
list.add("王五");
list.push("趙六");
list.push("小七");
System.out.println("------------刪除之前-------------");
/*System.out.println(list.first.object);
System.out.println(list.first.next.object);
System.out.println(list.first.next.next.object);*/
for (int i = 0; i < list.getSize(); i++) {
System.out.println(list.get(i));
}
System.out.println("------------刪除了" + list.remove(2));
System.out.println("------------刪除之後-------------");
for (int i = 0; i < list.getSize(); i++) {
System.out.println(list.get(i));
}
}
}
三、循環鏈表
1、描敘
在一個 循環鏈表中, 首節點和末節點被連接在一起。這種方式在單向和雙向鏈表中皆可實現。要轉換一個循環鏈表,你開始於任意一個節點然後沿着列表的任一方向直到返回開始的節點。再來看另一種方法,循環鏈表可以被視爲“無頭無尾”。這種列表很利於節約數據存儲緩存, 假定你在一個列表中有一個對象並且希望所有其他對象迭代在一個非特殊的排列下。
指向整個列表的指針可以被稱作訪問指針。
循環鏈表中第一個節點之前就是最後一個節點,反之亦然。循環鏈表的無邊界使得在這樣的鏈表上設計算法會比普通鏈表更加容易。對於新加入的節點應該是在第一個節點之前還是最後一個節點之後可以根據實際要求靈活處理,區別不大(詳見下面實例代碼)。當然,如果只會在最後插入數據(或者只會在之前),處理也是很容易的。
另外有一種模擬的循環鏈表,就是在訪問到最後一個節點之後的時候,手工的跳轉到第一個節點。訪問到第一個節點之前的時候也一樣。這樣也可以實現循環鏈表的功能,在直接用循環鏈表比較麻煩或者可能會出現問題的時候可以用。
2、圖文示例
- 循環鏈表中第一個節點之前就是最後一個節點
- 在一個 循環鏈表中, 首節點和末節點被連接在一起。這種方式在單向和雙向鏈表中皆可實現
本文到此結束,如果覺得有用,勞煩各位點贊關注一下唄,將不定時持續更新更多的內容…,感謝大家的觀看!