什么是鏈表?
鏈表是一種在物理上非連續,非順序的數據結構,由若干節點(node)所組成。
單向鏈表的每一個節點包含兩個部分,一部分存放數據的變量,另一部分是指向下一個節點的指針。鏈表的第一個節點稱為頭節點,最后一個節點稱為尾結點,尾結點的指針指向空。與數組按照索引來隨機查找數據不同,對于鏈表的其中一個節點A,我們只能根據節點A的next指針來找到該節點的下一個節點B,在根據節點B的next指針找到一下個節點C,以此類推。
那么如何找到該節點的前一個節點呢?可以使用雙向鏈表。
單向鏈表存儲結構如圖:
節點代碼如下:
/**
* 定義存儲數據的節點
*/
private class Node {
public E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return e.toString();
}
}
什么是雙向鏈表?
雙向鏈表比單向鏈表稍微復雜一些,它的每一個節點除了擁有data和next指針還包含一個指向前置節點的prev指針。
雙向鏈表存儲結構如圖:
鏈表的實現
1、查找節點
查找元素時,鏈表不像數組那樣可以通過索引來進行快速定位,只能從頭節點向后一個一個的查找。
2、更新節點
如果不考慮查找節點的過程,鏈表的更新過程非常節點,直接把舊數據替換成新數據即可。
3、插入節點
與數組類似,鏈表插入節點同樣可以分為三種情況:
-
頭部插入
頭部插入,分為兩個步驟:
1、把新節點的next指針指向當前頭節點。
2、把新節點變為鏈表的頭節點。
-
尾部插入
尾部插入是最簡單的情況,直接把尾結點的next執行指向新節點即可。
-
中間插入
中間插入,分為兩個步驟:
1、新節點的next指針指向插入位置的節點。
2、插入位置的前置節點的next指針指向新節點。
4、刪除節點
同樣分為三種情況:
-
頭部刪除
把當前鏈表的頭結點更新為原頭節點的next指針即可。
-
尾部刪除
把尾結點的前置節點的next指針指向為空即可。
-
中間刪除
把要刪除的前置節點的next指針指向要刪除節點的next指針即可。
整體代碼如下:
/**
* 描述:鏈表。
* <p>
* Create By ZhangBiao
* 2020/5/11
*/
public class LinkedList<E> {
/**
* 虛擬頭結點
*/
private Node dummyHead;
private int size;
public LinkedList() {
this.dummyHead = new Node(null, null);
this.size = 0;
}
/**
* 獲取鏈表中的元素個數。
*
* @return
*/
public int getSize() {
return size;
}
/**
* 返回鏈表是否為空。
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 在鏈表的index(0-based)位置添加新的元素e,在鏈表中不是一個常用的操作,當做練習。
*
* @param index
* @param e
*/
public void add(int index, E e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Illegal index.");
}
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
prev.next = new Node(e, prev.next);
size++;
}
/**
* 在鏈表頭添加新的元素e。
*
* @param e
*/
public void addFirst(E e) {
add(0, e);
}
/**
* 在鏈表末尾添加新的元素e。
*
* @param e
*/
public void addLast(E e) {
add(size, e);
}
/**
* 獲得鏈表的第index(0-based)個位置的元素。
* 在鏈表中不是一個常用的操作,當做練習用。
*
* @param index
* @return
*/
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Get failed. Illegal index.");
}
Node cur = dummyHead.next;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
return cur.e;
}
/**
* 獲得鏈表的第一個元素。
*
* @return
*/
public E getFirst() {
return get(0);
}
/**
* 獲得鏈表的最后一個元素。
*
* @return
*/
public E getLast() {
return get(size - 1);
}
/**
* 修改鏈表的第index(0-based)個位置的元素為e。
* 在鏈表中不是一個常用的操作,當做練習用。
*
* @param index
* @param e
*/
public void set(int index, E e) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Set failed. Illegal index.");
}
Node cur = dummyHead.next;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
cur.e = e;
}
/**
* 查找鏈表中是否有元素e。
*
* @param e
* @return
*/
public boolean contains(E e) {
Node cur = dummyHead.next;
while (cur != null) {
if (cur.e.equals(e)) {
return true;
}
cur = cur.next;
}
return false;
}
/**
* 從鏈表中刪除index(0-based)位置的元素并返回刪除的元素。
*
* @param index
* @return
*/
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed. Index is illegal.");
}
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
Node retNode = prev.next;
prev.next = retNode.next;
retNode.next = null;
size--;
return retNode.e;
}
/**
* 從鏈表中刪除第一個元素并返回刪除的元素。
*
* @return
*/
public E removeFirst() {
return remove(0);
}
/**
* 從鏈表中刪除最后一個元素并返回刪除的元素。
*
* @return
*/
public E removeLast() {
return remove(size - 1);
}
/**
* 從鏈表中刪除元素e
*
* @param e
*/
public void removeElement(E e) {
Node prev = dummyHead;
while (prev.next != null) {
if (prev.next.e.equals(e)) {
break;
}
prev = prev.next;
}
if (prev.next != null) {
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size--;
}
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
/*Node cur = dummyHead.next;
while (cur != null) {
result.append(cur + " -> ");
cur = cur.next;
}*/
for (Node cur = dummyHead.next; cur != null; cur = cur.next) {
result.append(cur + " -> ");
}
result.append("NULL");
return result.toString();
}
/**
* 定義存儲數據的節點
*/
private class Node {
public E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return e.toString();
}
}
}
在這里我使用了虛擬頭結點,為什么使用虛擬頭結點?
在添加元素的過程中遇到一個問題:現在要在任意位置上添加一個元素,在鏈表頭添加元素與在其他位置邏輯會有差別,那么為什么在鏈表頭添加元素比較特殊呢?這是因為我們為鏈表添加元素的過程要找到待添加元素位置的前置節點,但是由于對于鏈表頭來說,它沒有前置節點,所以在邏輯上就比較特殊一些,解決方式也比較簡單,我們的核心問題不就是鏈表頭它沒有前置節點嘛,那么我們就可以造一個鏈表頭的前置節點,對于這個前置節點,他不存儲任何的元素,這樣一來,對于我們的鏈表來說,它第一個元素就是虛擬頭結點的next所對應的那個元素,而不是虛擬頭結點。注意:==虛擬頭節點的那個元素是根本不存在的,對于用戶來講也是沒有意義的,這只是為了我們編寫邏輯方便而出現的虛擬頭節點==。
添加虛擬頭節點后的存儲結構如圖:
鏈表的優劣勢
1、優點
真正動態,不需要處理固定容量的問題。
2、缺點
喪失隨機訪問能力。