鏈表在物理上/邏輯上
鏈表是一種物理存儲單元上非連續、非順序的存儲結構。
數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。
鏈表每個結點的組成
鏈表由一系列結點(鏈表中每一個元素稱爲結點)組成,結點可以在運行時動態生成。
每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。
鏈表使用過程中的時間複雜度
鏈表在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是查找一個節點或者訪問特定編號的節點則需要O(n)的時間,而線性表和順序表相應的時間複雜度分別是O(logn)和O(1)。
簡單說,查詢慢,增刪快。
因爲需要加入一個指針域,所以空間開銷比較大。
單向鏈表
它存儲的數據分散在內存中,每個結點只能也只有它能知道下一個結點的存儲位置。由N各節點(Node)組成單向鏈表,每一個Node記錄本Node的數據及下一個Node。向外暴露的只有一個頭節點(Head),我們對鏈表的所有操作,都是直接或者間接地通過其頭節點來進行的。
嘗試自己寫一個簡單的
首先準備好結點(class Node)實現一個單向的,所以只需要準備當前結點的數據,和next地址就可以了。
具體的增刪改查方法就是new Node並且處理next 和data就可以了。
public class MyLink<E> {
/**
* 頭結點,聲明鏈表的時候,實例化一個空的頭結點
*/
Node head = null;
int size = 0;
/**
* 鏈表插入數據 插入的數據指向的對象,是否存在下一個結點,
* @param d
*/
private synchronized void addLastNode(E d){
final Node<E> newNode = new Node(d);
if(head ==null){
head = newNode;
size++;
return;
}
//定義一個臨時的結點 判斷頭結點下面的值,且不改變實際頭結點的值
Node<E> tmp = head;
//當前結點的下一個結點是否有值 如果有,當前結點變爲next結點
while(tmp.next!=null){
tmp = tmp.next;
}
//直到last Node 給當前結點的下一個結點賦爲需要添加的值
tmp.next = newNode;
size++;
}
/**
* 給頭結點添加元素
* @param d
*/
private synchronized void addHeadNode(E d){
//聲明一個頭結點 next爲null,需要將原來的頭結點 變成newNode的next
final Node<E> newNode = new Node(d);
newNode.next = head;
head = newNode;
size++;
}
/**
* 給中間結點添加新結點
* @param d
*/
private synchronized void addSizeNode(int index,E d){
Node<E> newNode = null;
//插入中間元素
for (int i = 0; i < index-1; i++){
//next index次
newNode = head.next;
}
//得到需要插入的結點的位置,往該節點下插入新的
Node<E> addNode = new Node<>(d);
addNode.next = newNode.next;
newNode.next = addNode;
size++;
}
/**
* 實例一個刪除頭部元素
* @param index
*/
public synchronized void deleteHeadNode(){
//現有頭部元素作爲新的頭部
Node newHead = head.next;
//原有頭部data設置爲null;
head.data = null;
head.next = null;
head = newHead;
size--;
}
public static void main(String[] args) {
MyLink<SystemA> myLink = new MyLink<>();
myLink.addHeadNode(new SystemA("001","張三","true"));
myLink.addLastNode(new SystemA("002","李四","true"));
myLink.addLastNode(new SystemA("004","王五","true"));
myLink.addSizeNode(2,new SystemA("003","李四","true"));
myLink.deleteHeadNode();
System.out.println("長度是:"+myLink.size);
System.out.println("頭元素是:"+myLink.head.data.toString());
System.out.println("頭.next元素是:"+myLink.head.next.data.toString());
}
}
class Node<E>{
Node next = null;//結點的引用,指向下一個結點
E data;//結點的對象,內容
public Node(E data){
this.data = data;
}
}
自己實現的過程中,發現刪除元素,新增元素很容易實現,但是需要先找到元素之後再做這些操作才簡單。數據的二分法就比較的好。
邊界值問題需要考慮很多,比如當前鏈表的長度,新增的時候是否越界、刪除的時候是否越界、長度爲0的時候等等等。
那些問題大佬們早就考慮到了
一般實現好的鏈表會有限定好的值,而且有跳躍表來簡化鏈表的查找數據。
單向鏈表反轉
單向鏈表反轉的方式有比較多的,很粗暴的拆了重組。
從頭結點一直next,直到node.next==null,取當前結點爲新的頭結點【head】並且切斷該結點。
繼續切已經被切過尾巴的鏈表,得到的數據添加到新的鏈表中…直到老的鏈表爲null,返回新的鏈表表頭就可以了。時間複雜度O(n)。
雙向鏈表
雙向鏈表也是同樣的準備Node
public class MyLinkDou<E> {
int size = 0;
Node head = null;
Node last = null;
class Node {
//實際存儲的數據
public E e;
//下一個結點
public Node next;
//上一個結點
public Node pre;
/**
*
* @param e
*/
public Node(E e){
this.e = e;
next = null;
pre = null;
}
}
}
其他的增刪改查與單向的挺像的。
/**
* 根據下標找到當前結點的上一個結點
* @param index
* @return
*/
public Node findpreNode(int index){
Node presend = head;
int nowindex = -1;
while(presend.next != null){
if( nowindex== index - 1){
return presend;
}
presend = presend.next;
nowindex++;
}
return null;
}
/**
* 根據下標插入一個數據
* @param index
* @param e
*/
public void addIndex(int index,E e){
if(index <0||index>=size)
return;
Node node = new Node(e);
Node preNode = this.findpreNode(index);
node.next = preNode.next;
preNode.next.pre = node;
preNode.next = node;
node.pre = preNode;
size++;
}
閱讀LinkedList源碼文檔註釋 哇哦,看着好複雜,但是能看到官方的鏈表寫的有多麼的強大。
首先多線程下不安全
*<p><strong>Note that this implementation is not synchronized.</strong>
* If multiple threads access a linked list concurrently, and at least
* one of the threads modifies the list structurally, it <i>must</i> be
* synchronized externally. (A structural modification is any operation
* that adds or deletes one or more elements; merely setting the value of
* an element is not a structural modification.) This is typically
* accomplished by synchronizing on some object that naturally
* encapsulates the list.
其次是雙線鏈表
* Doubly-linked list implementation of the {@code List} and {@code Deque}
* interfaces. Implements all optional list operations, and permits all
* elements (including {@code null}).
源碼裏寫到的結點類,只有一個全參構造器
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;
}
}
add方法有六個、remove方法有9個、返回迭代器、clone、等方法。原來學習Java數據結構的時候對鏈表不是很瞭解,自己手寫後,對這種數據結構也有了自己的認識。
跳躍表
上面說到,鏈表尋找數據很麻煩,比如ABCDEFG,這種順序如果要得到F,需要遍歷ABCDE結點,纔可以到達F結點,如果可以ACEF的跳躍式的訪問結點,那麼鏈表查找某個值就快很多了;如果可以AEF的,甚至直接AF,那麼取F的值,也是比較簡單的。
跳躍表:
https://www.cnblogs.com/acfox/p/3688607.html