一、前言
在前面兩章我們講解了動態數組、棧和隊列的講解,這些底層都是依託靜態數組,靠 resize 解決固定容量問題的,之前雖然用戶看到的是動態數組,但是依然使用的是靜態數組,他是依靠 resize 這個方法解決 固定容量問題 ,但是我們今天要講解的 鏈表 不一樣,鏈表 是我們數據結構學習的一個重點,也有可能是一個難點,爲什麼鏈表這麼重要呢?因爲他是最簡單的也是 真正的動態數據結構。
二、爲什麼鏈表很重要
- 鏈表是一個真正的動態數據結構
- 最簡單的動態數據結構
- 更深入的理解引用(或者指針)
- 更深入的理解遞歸
- 輔助組成其他數據結構
更深入的理解引用(或者指針):和內存相關,雖然在 java 中大家不用手動的管理內存,但是對 鏈表 這種數據結構,更加深入的理解,可以幫助大家對引用、指針、甚至計算機系統中和內存管理相關的很多話題,有更加深入的認識。
更深入的理解遞歸:鏈表 本來也是有他非常清晰的遞歸結構的,、由於 鏈表 這種數據結構是 數據結構,我們可以更加 深入理解遞歸,對於遞歸這種深入理解是不可獲取的。
鏈表 本身也是具有功能性:輔助組成其他數據結構(hashMap 、棧和隊列)
三、什麼是鏈表
鏈表 是一種數據結構,在內存中通過 節點記錄內存地址 而相互鏈接形成一條鏈的儲存方式。相比數組而言,鏈表在內存中不需要連續的區域,只需要每一個節點都能夠 記錄下一個節點 的 內存地址 ,通過 引用 進行查找,這樣的特點也就造就了 鏈表 增刪操作時間消耗很小,而查找遍歷時間消耗很大的特點。
我們日常在 Java 中使用的 LinkedList 即爲 雙向鏈表。而在鏈表是由其基本組成單元節點 (Node) 來實現的。我們在日常中見到的鏈表大部分都是 單鏈表和雙鏈表
單元節點 (Node):
class Node{
E e;
Node next;
}
- e 就是鏈表元素
- next 指的是當前節點的下一個節點
- 對於 鏈表 來說它就像我們的火車一樣,每一個節點其實就是一節車廂,我們在車廂中存儲真正的數據,而車廂和車廂之間還要進行連接,讓我們數據是整合在一起的,用戶可以方便的在所有的數據上進行查詢或其他操作,那麼 數據和數據連接 就是由這個 next 來完成的
- 當然 鏈表 不能無窮無盡,如果一個節點的 next 是 Null 了,就說明這個節點是最後一個節點了,這就是 鏈表
如下圖所示(單鏈表):
鏈表的優點:真正的動態,不需要處理固定容量的問題
鏈表的缺點:喪失了隨機訪問的能力
在數組中:每一個索引,直接從數組中拿出索引對應的元素,這是因爲從底層機制上,數組所開闢的空間,在內存裏是連續分佈的,所以我們可以直接可以去找這個數組的偏移,直接計算出這個數據所存儲的內存地址,可以直接使用。
鏈表:而鏈表是靠 Next 一層一層連接的,需要藉助這個 Next 一點一點的去找我們需要取出來的元素。
四、創建我們自己的鏈表
4.1 鏈表基本結構
/**
* 底層鏈表的內部類
* @param <E>
*/
public class LinkedList<E> {
//設計私有的內部類,對於用戶來說不需要知道鏈表底層實現,
// 不需要知道node這個節點,對用戶屏蔽編碼實現的底層實現
private class Node{
public E e;
public Node next;//public 可以在LinkedList隨意操作
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();
}
}
}
內部類Node:設計私有的內部類,對於用戶來說不需要知道鏈表底層實現,不需要知道node這個節點,對用戶屏蔽編碼實現的底層實現
e:元素
next:指向Node的一個引用
4.2 添加元素
之前我們講的是如何在數組中添加元素,我們在數組尾添加元素是非常方便的,因爲對於數組來說是順序排放的,有意思的是對於鏈表來說,恰恰相反,在鏈表頭添加元素是非常方便的,其實這樣非常好理解,對於數組來說我們有 size 這個變量,它直接指向了數組中最後一個元素下一個位置,也就是下一個待添加元素的位置,所以直接添加就非常容易,因爲有 size 這個變量,在跟蹤數組的尾巴,而對於鏈表來說我們設立了鏈表的一個頭 head ,而沒有變量來跟蹤鏈表的尾巴,所以我們在鏈表頭添加元素是非常方便的,最關鍵的就是 node.next = head 和 head = node,如下圖所示:
4.2.1 鏈表頭添加元素
代碼實現:
//在鏈表頭中添加元素e
public void addFirst(E e){
//方式一
// Node node = new Node(e);
// node.next = head;
// head = node;
//方式二
head = new Node(e,head);
size ++;
}
4.2.2 鏈表中間添加元素:
我們需要在索引爲2的地方添加元素666
,我們只需要找到 元素666
要 插入之前的節點(1) ,我們管它叫 prev,然後把 之前節點的(1) next 指向 666
,然後在將 666
的這個 節點指向之前節點(1) 的 之後的節點(2) ,就完成了整個插入了,其中關鍵代碼就是 node.next = prev.next 和 prev.next = node;
,其中關鍵:我們要找到添加節點的前一個節點 。
代碼實現:
//在鏈表的index(0-based)位置添加新的元素e
public void add(int index,E e){
if(index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Illegal index.");
if(index == 0)
addFirst(e);
else{
Node prev = head;
for (int i = 0; i < index - 1; i++) {//將prev 放入下一個節點,直到移動到index - 1
prev = prev.next;
//方式一
// Node node = new Node(e);
// node.next = prev.next;
// prev.next = node;
//方式二
prev.next = new Node(e,prev.next);
size++;
}
}
}
//在鏈表末尾添加新的元素e
public void addLast(E e){
add(size,e);
}
4.2.3 添加操作時間複雜度
功能 | 時間複雜度 |
---|---|
addList(e) | O(n) |
addFirst(e) | O(1) |
add(index,e) | O(n/2) = O(n) |
4.3 爲鏈表設計虛擬頭結點
上面我們介紹了鏈表的添加操作,那麼我們在添加的時候遇到了一個問題,就是在鏈表任意一個地方的時候,添加一個元素,在鏈表頭添加一個元素,和在鏈表其他地方添加元素,邏輯上會有差別,爲什麼在鏈表頭添加元素會比較特殊呢,因爲我們在鏈表添加元素的過程,要找到待添加的 之前的一個節點,但是由於對於鏈表頭沒有之前的一個節點,不過我們可以自己創建一個頭結點,這個頭節點就是 虛擬頭結點,這個節點對於用戶來說是不存在, 用戶也不會感知到這個節點的存在,我們是屏蔽了這個節點的存在,如下圖所示:
代碼實現:
private Node dummyHead;
int size;
public LinkedList(){
dummyHead = new Node(null,null);
size = 0;
}
//獲取鏈表中的元素個數
public int getSize(){
return size;
}
//返回鏈表是否爲空
public boolean isEmpty(){
return size == 0;
}
//在鏈表的index(0-based)位置添加新的元素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
public void addFirst(E e){
add(0,e);
}
//在鏈表末尾添加新的元素e
public void addLast(E e){
add(size,e);
}
4.4 鏈表元素 get、set、是否存在操作
//在鏈表的index(0-based)位置添加新的元素e
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;
}
//獲得鏈表的第一個元素
public E getFirst(){
return get(0);
}
//獲取鏈表的最後一個元素
public E getLast(){
return get(size - 1);
}
//在鏈表的index(0-based)位置添加新的元素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
public boolean contains(E e){
Node cur = dummyHead.next;
while (cur != null){
if(cur.e.equals(e))
return true;
cur = cur.next;
}
return false;
}
4.5.1 修改和查找操作時間複雜度
功能 | 時間複雜度 |
---|---|
set(index,e) | O(n) |
get(index) | O(n) |
contains(e) | O(n) |
4.5 刪除鏈表元素
加入我們想要刪除索引爲 (2) 位置的元素,我們需要找到 待刪除節點之前的一個位置,也就是(1) ,我們用 prev 表示,找到這個節點之後,那麼 (2) 就是我們需要刪除的索引了 我們叫 delNode,如下圖所示:
代碼實現:
//從鏈表中刪除Index(0-based)位置的元素,返回刪除的元素
public E remove(int index){
if(index < 0 || index > size)
throw new IllegalArgumentException("Remove failed. Illegal index.");
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;
}
//從鏈表中刪除第一個位置的元素
public E removeFirst(){
return remove(0);
}
//從鏈表中刪除最後一個位置的元素
public E removeLast(){
return remove(size - 1);
}
4.5.1 刪除操作時間複雜度
功能 | 時間複雜度 |
---|---|
removeList(e) | O(n) |
removeFirst(e) | O(1) |
remove(index,e) | O(n/2) = O(n) |
4.6 完整代碼
/**
* 底層鏈表的內部類
* @param <E>
*/
public class LinkedList<E> {
private class Node{
public E e;
public Node next;//public 可以在LinkedList隨意操作
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();
}
}
private Node dummyHead;
int size;
public LinkedList(){
dummyHead = new Node(null,null);
size = 0;
}
//獲取鏈表中的元素個數
public int getSize(){
return size;
}
//返回鏈表是否爲空
public boolean isEmpty(){
return size == 0;
}
//在鏈表頭中添加元素e
public void addFirst(E e){
//方式一
// Node node = new Node(e);
// node.next = head;
// head = node;
//方式二
add(0,e);
}
//在鏈表的index(0-based)位置添加新的元素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
public void addLast(E e){
add(size,e);
}
//在鏈表的index(0-based)位置添加新的元素e
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;
}
//獲得鏈表的第一個元素
public E getFirst(){
return get(0);
}
//獲取鏈表的最後一個元素
public E getLast(){
return get(size - 1);
}
//在鏈表的index(0-based)位置添加新的元素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
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)位置的元素,返回刪除的元素
public E remove(int index){
if(index < 0 || index > size)
throw new IllegalArgumentException("Remove failed. Illegal index.");
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;
}
//從鏈表中刪除第一個位置的元素
public E removeFirst(){
return remove(0);
}
//從鏈表中刪除最後一個位置的元素
public E removeLast(){
return remove(size - 1);
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
for (Node cur = dummyHead.next;cur != null; cur= cur.next)
res.append(cur + "->");
res.append("Null");
return res.toString();
}
}
4.2.7 結果測試:
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
//添加元素 0-4
for (int i = 0; i < 5 ; i++) {
linkedList.addFirst(i);
System.out.println(linkedList);
}
//添加第二個元素添加 666
linkedList.add(2,666);
System.out.println(linkedList);
//刪除第二個元素 666
linkedList.remove(2);
System.out.println(linkedList);
//刪除第一個元素
linkedList.removeFirst();
System.out.println(linkedList);
//刪除最後一個元素
linkedList.removeLast();
System.out.println(linkedList);
}
打印結果:
0->Null
1->0->Null
2->1->0->Null
3->2->1->0->Null
4->3->2->1->0->Null
4->3->666->2->1->0->Null
4->3->2->1->0->Null
3->2->1->0->Null
3->2->1->Null
四、鏈表時間複雜度分析
功能 | 時間複雜度 |
---|---|
增加 | O(n) |
刪除 | O(n) |
修改 | O(n) |
查詢 | O(n) |
對於增加和刪除來說,如果是對鏈表頭進行操作,那麼就是 O(1) 級別的複雜度,對於查詢來說,也是一樣
五、鏈表應用
5.1 使用棧實現鏈表
5.1.1 接口類:
/**
* @program: Data-Structures
* @ClassName Stack
* @description:
* @author: lyy
* @create: 2019-11-20 21:51
* @Version 1.0
**/
public interface Stack<E> {
int getSize();
boolean isEmpty();
void push(E e);
E pop();
E peek();
}
5.1.2 實現類:
import com.lyy.datasty.Mystack.Stack;
//鏈表棧實現
public class LinkedListStack<E> implements Stack<E> {
private LinkedList1<E> list;
public LinkedListStack(){
list = new LinkedList1<>();
}
@Override
public int getSize() {
return list.getSize();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public void push(E e) {
list.addFirst(e);
}
@Override
public E pop() {
return list.removeFirst();
}
@Override
public E peek() {
return list.getFirst();
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("Stack:top ");
res.append(list);
return res.toString();
}
}
5.1.3 運行結果:
public static void main(String[] args) {
LinkedListStack<Integer> stack = new LinkedListStack<>();
for (int i = 0; i < 5; i++) {
stack.push(i);
System.out.println(stack);
}
stack.pop();
System.out.println(stack);
}
5.1.4 結果打印:
Stack:top 0->Null
Stack:top 1->0->Null
Stack:top 2->1->0->Null
Stack:top 3->2->1->0->Null
Stack:top 4->3->2->1->0->Null
Stack:top 3->2->1->0->Null
5.2 使用鏈表實現隊列
5.2.1 接口類
/**
* @program: Data-Structures
* @ClassName Queue
* @description:
* @author: lyy
* @create: 2019-11-21 21:54
* @Version 1.0
**/
public interface Queue<E> {
int getSize();
boolean isEmpty();
void enqueue(E e);
E dequeue();
E getFront();
}
5.2.2 實現類
public class LinkedListQueue<E> implements Queue<E>{
//設計私有的內部類,對於用戶來說不需要知道鏈表底層實現,
// 不需要知道node這個節點,對用戶屏蔽編碼實現的底層實現
private class Node{
public E e;
public Node next;//public 可以在LinkedList隨意操作
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();
}
}
private Node head,tail;
private int size;
public LinkedListQueue(){
head = null;
tail = null;
size = 0;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void enqueue(E e) {
if(tail == null){
tail = new Node(e);
head = tail;
}else{
tail.next = new Node(e);
tail = tail.next;
}
size ++;
}
@Override
public E dequeue() {
if(isEmpty())
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
Node retNode = head;
head = head.next;
retNode.next = null;
if(head == null)
tail = null;
size --;
return retNode.e;
}
@Override
public E getFront() {
if(isEmpty())
throw new IllegalArgumentException("queue is empty.");
return head.e;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("Queue:front ");
Node cur = head;
while (cur != null) {
res.append(cur + "->");
cur = cur.next;
}
res.append("Null tail");
return res.toString();
}
}
5.2.2 測試類
public static void main(String[] args) {
LinkedListQueue<Integer> queue = new LinkedListQueue<>();
for (int i = 0; i < 10; i++) {
queue.enqueue(i);
System.out.println(queue);
if(i % 3 ==2){
queue.dequeue();
System.out.println(queue);
}
}
}
打印結果:
Queue:front 0->Null tail
Queue:front 0->1->Null tail
Queue:front 0->1->2->Null tail
Queue:front 1->2->Null tail
Queue:front 1->2->3->Null tail
Queue:front 1->2->3->4->Null tail
Queue:front 1->2->3->4->5->Null tail
Queue:front 2->3->4->5->Null tail
Queue:front 2->3->4->5->6->Null tail
Queue:front 2->3->4->5->6->7->Null tail
Queue:front 2->3->4->5->6->7->8->Null tail
Queue:front 3->4->5->6->7->8->Null tail
Queue:front 3->4->5->6->7->8->9->Null tail
六、更多鏈表結構
6.1 雙鏈表
代碼:
class Node{
E e;
Node next,prev;
}
6.1 循環列表
代碼:
class Node{
E e;
Node next,prev;
}
在java中,LinkedList 底層使用的就是 循環鏈表,也就是循環雙向鏈表,到這裏我們鏈表就講解完成了,在閱讀中大家如果覺得有改進或者疑問的地方,歡迎大家在下面進行留言,博主看到了會第一時間回覆大家,我是牧小農,我喂自己帶鹽,學習路上你我同行,大家加油!