文章目錄
一、鏈表的基本方法
鏈表是一種通過節點存儲元素,並且節點與節點之間是連接起來的數據結構。
0->1->2->3->4->5->NULL
- 對於鏈表,如果要想訪問存在這個鏈表中的所有節點,相應的必須把鏈表的頭給存儲起來。通常鏈表的頭叫做head,即在鏈表LinkedList類中,應該又一個Node型的變量head,它指向鏈表中的第一個節點。
private Node head;
- 需要設置一個私有的size來記錄鏈表中元素的個數,這個size只能在類內操作,不允許外部進行修改。
- 添加查看鏈表中元素個數和判斷鏈表是否爲空的方法
// 支持泛型
public class LinkedList<E> {
// 節點設計成鏈表類中的內部類
// 設計私有類,只有在鏈表數據結構內纔可以訪問到Node
private class Node {
// 設計成public 在LinkedList中可以隨意訪問操作 不需要設置get,set方法
public E e; // 存放元素
public Node next; // 指向Node的引用
public Node(E e, Node next) {
// 將用戶傳來的數據交給節點
this.e = e;
this.next = next;
}
// 用戶只傳來e
public Node(E e) {
this(e, null);
}
// 用戶什麼都不傳
public Node() {
this(null, null);
}
// 對每一個節點設置toString方法
@Override
public String toString() {
// 每一個節點,直接打印e所對應的toString
return e.toString();
}
}
// 第一個節點
private Node head;
// 用戶不能在外部直接修改size
private int size; // 來記錄鏈表中有多少個元素
// 鏈表構造函數
public LinkedList() {
// 對於一個空的鏈表來說,它是存在一個節點的,這個節點就是唯一的虛擬頭節點
head = null;
size = 0;
}
// 獲取鏈表中元素的個數
public int getSize() {
return size;
}
// 返回鏈表是否爲空
public boolean isEmpty() {
// size是否爲0
return size == 0;
}
// 獲取鏈表中元素的個數
public int getSize() {
return size;
}
// 返回鏈表是否爲空
public boolean isEmpty() {
// size是否爲0
return size == 0;
}
}
二、在鏈表頭添加元素
1. 與數組的對比
對於數組,向數組尾添加一個元素非常的方便。相反,對於鏈表來說,在鏈表的頭部添加元素是非常方便的。
這是因爲在數組中,size直接指向了數組中最後一個元素的下一個位置,它跟蹤數組的尾巴。而對於鏈表,設立了鏈表的頭head,有一個變量跟蹤鏈表的頭,沒有相應的變量去跟蹤鏈表的尾部,所以在鏈表的頭部添加元素非常方便。
2. 添加的具體過程
假設將新的元素10
添加進0->1->2->3->4->5->NULL這個鏈表中,首選需要把元素10
放進一個節點裏,這個節點裏存儲了元素10以及相應的next
。添加的關鍵在於如何將這個節點掛接到鏈表中,同時不破壞現有鏈表的結構。
- 將元素10這個節點(Node)的next指向現在鏈表的頭部(head)也就是
node.next=head
。 - 此時的鏈表已經變成了
10->0->1->2->3->4->5->NULL
,存儲10的節點成了新的頭部。 - 接下來需要維護一下head,讓
head=node
,即讓head指向新的10的節點。這樣便完成了將10插入到鏈表的頭部。 - 整個過程是在一個函數中執行的,對於node這個變量來說,指向的是“10”這個節點,在
函數結束之後,node的塊作用域也就結束
了,node變量相應的也就沒用了。
LinkedList中添加一個新的方法,作用在鏈表頭添加新的元素。
public void addFirst(E e) {
// 首先創建一個新的節點 此時next爲空
Node node = new Node(e);
// 讓node的next指向head
node.next = head;
// 更新一下head爲新的node元素
head = node;
// 之後維護一下size
size++;
}
其中也可以將邏輯的直接一行表示,讓head直接指向新的節點
。
public void addFirst(E e) {
// 首先new一個新的Node,傳入e,Node直接指向當前鏈表的head,然後將Node賦值給head進行更新
head = new Node(e, head);
// 之後維護一下size
size++;
}
三、在鏈表中插入元素
1. 插入元素的具體邏輯
已有鏈表0->1->2->3->4->5->NULL
在鏈表“索引”爲2
的地方添加一個新的元素9,對於鏈表來說沒有索引的概念,具體來說是在當前節點1的位置之後
添加一個新的元素9,之後是節點2。
- 首先創建“9”的節點
- 要想把
“9”
插入到正確的位置,就必須要找到插入“9”節點之後,這個“9”節點之前的節點是誰。把它稱做prev
。prev的初始化適合head在同一個地方。 - 在這個例子中,需要
先找到“1”節點讓它的next指向“9”
,再將“9”的next指向
原本“1”節點之後節點“2”
- 當前需要搜索插入“9”之前的節點是誰,因爲插入了“9”之後索引爲2,那麼插入“9”之前的節點索引爲1。這是就需要從0開始遍歷,遍歷到索引爲1的位置就可以了,相應的維護prev找到1的位置。
一旦找到對應位置1後
- 首先將node的next指向prev的下一個元素,
node.next = prev.next;
在這裏prev.next是插入位置的後一個節點
- 之後讓prev的next等於node
prev.next = node;
在這裏,prev.next指向新的node節點
。
(這兩點的順序是不允許改變的,如果調換順序執行,prev.next已經賦值node了,再執行node.next = prev.next,實際是讓它又指向了自己。順序是非常重要的)
Node node = new Node(e);
node.next = prev.next;
prev.next = node;
也可以進行相應的縮寫
prev.next = new Node(e, prev.next);
這個過程的關鍵,在於找到待添加節點的前一個節點,也是prev變量進行的事情。但值得注意的是:如果想把新的元素,添加在“索引”爲0的地方,即添加在鏈表的頭部,鏈表頭部的節點是不存在前一個節點的
。對於這種情況需要特殊處理,就是前面一節提到的往鏈表頭部添加新的元素。
2. 實現添加元素的方法
// 鏈表的index添加新的元素e
// 真正使用鏈表很少有這樣的操作,當選擇使用鏈表的時候,通常選擇不使用"索引"
public void add(int index, E e) {
// 需要先判斷index的合法性
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
if (index == 0) {
addFirst(e);
} else {
// 設計一個Node從head開始
Node prev = head;
for (int i = 0; i < index - 1; i++) {
// 當前prev存的節點的下一個節點放進prev中
// 遍歷一直挪動prev位置存放下一個節點
prev = prev.next;
}
// 插入過程
prev.next = new Node(e, prev.next);
// 插入之後維護size
size++;
}
}
3. 相應實現末尾添加
// 在鏈表的末尾添加一個新的元素e
public void addLast(E e) {
// 複用add(),只需要在size添加即可
add(size, e);
}
四、爲鏈表設立虛擬頭結點
1. 鏈表頭添加元素時的特殊性
在鏈表添加元素的過程中,在向鏈表任意一個地方添加元素的時候,在鏈表頭添加元素和在鏈表其他位置添加元素的邏輯是不一樣的,因爲鏈表頭沒有前一個元素的節點,所以需要進行特殊處理。
2. 解決辦法
可以造一個鏈表頭之前的節點,這個節點不存儲任意的元素
null->0->1->2->3->4->5->NULL
將這個空節點稱之爲整個鏈表真正的head,通常叫做dummyHead
,即虛擬頭節點。這樣,鏈表的第一個元素是dummyHead.next
所對應的元素。dummyHead位置的元素是根本不存在的,對用戶來講也根本沒有意義,它是0節點之前的虛擬元素。
3. 具體的邏輯實現
在LinkedList()中,head存放一個具體的元素,初始化時是null。引入虛擬頭節點,則private Node dummyHead
構造函數初始化時,則需要引人一個節點
public LinkedList() {
// 對於一個空的鏈表來說,它是存在一個節點的,這個節點就是唯一的虛擬頭節點
dummyHead = new Node(null, null);
size = 0;
}
對於add()添加操作也需要進行相應的改動,不再需要判斷index==0的情況
public void add(int index, E e) {
// 需要先判斷index的合法性
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
// 此時dummyHead指向的是0元素之前一個的位置的節點
Node prev = dummyHead;
// 只需要遍歷到index就可以了,因爲是從dummyHead開始遍歷的
for (int i = 0; i < index; i++) {
// 當前prev存的節點的下一個節點放進prev中
// 遍歷一直挪動prev位置存放下一個節點
prev = prev.next;
}
prev.next = new Node(e, prev.next);
// 插入之後維護size
size++;
}
如此一來,在鏈表的頭部添加一個元素可以複用add()方法
// 在鏈表頭添加新的元素
public void addFirst(E e) {
add(0, e);
}
五、遍歷、查詢、修改
1. 按照“索引”查找元素
這裏需要注意需要查找index元素,應該從整個鏈表的第一個元素開始遍歷,即dummyHead.next開始遍歷。
// 查詢操作
public E get(int index) {
// get之前先判斷合法性
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
// 遍歷鏈表,是從索引爲0開始的,從當前的開始遍歷
Node cur = dummyHead.next;
for (int i = 0; i < index; i++)
cur = cur.next;
// 最終cur裏存儲的e就是需要查找的元素
return cur.e;
}
// 獲取鏈表第一個元素
public E getFirst() {
return get(0);
}
// 獲取鏈表最後第一個元素
public E getLast() {
return get(size - 1);
}
2. 更新鏈表元素
// 鏈表的更新,修改
public void set(int index, E e) {
// 判斷合法
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
// 進行一次遍歷,找到第index元素進行替換
Node cur = dummyHead.next;
for (int i = 0; i < index; i++)
cur = cur.next;
// 最終cur裏存儲的e等於新的e
cur.e = e;
}
3. 查找鏈表中是否有元素e
這裏的遍歷和以前的查詢操作遍歷有所不同,之前的遍歷都有索引,知道自己要查詢或者修改的是鏈表中的第幾個節點,而這裏需要從頭對鏈表都進行一次遍歷。
// 查找鏈表中是否存在元素e
public boolean contains(E e) {
// 設置cur從第一個節點開始
Node cur = dummyHead.next;
// 不知道循環多少次使用while
// cur 節點不等他null的話,意味着當前cur節點是一個有效節點
while (cur != null) {
// cur.e是用戶傳來的e,返回true
if (cur.e.equals(e))
return true;
// 否則就看下一個節點
cur = cur.next;
// 直到cur爲空,說明把整個鏈表遍歷了一遍
}
return false;
}
六、從鏈表中刪除元素
1. 刪除邏輯
依然使用有虛擬頭節點的鏈表,刪除的節點爲取名爲delNode
。
null->0->1->2->3->4->5->NULL
假設刪除“索引”爲2的位置的元素,其實就是刪除節點2元素。
- 首先找到待刪除元素的前一個元素
節點1
用prev進行標註 - 此時要刪除的節點2就是prev.next對應的節點
- 將prev節點的next的指針指向要刪除節點2的
下一個
節點3,即是將“1的節點”指向“3的節點”
。具體的操作爲prev.next = delNode.next;
- 此時null->0->1->2->3->4->5->NULL相當於1直接連接到3節點看似刪除了,2節點其實並沒有刪除
- 爲了方便Java能夠回收2節點的空間,還需要讓2節點的next和整個鏈表脫離,即
delNode.next = null
。運行之後纔算真正的刪除。
需要刪除元素,必須找到next產生變化。
2. 實現刪除方法remove()
// 刪除鏈表中index位置元素 返回刪除的元素
public E remove(int index) {
// 判斷合法
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
// prev存了待刪除節點之前的節點
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
// 將刪除的節點保存進retNode;
Node retNode = prev.next;
// 將當前的前一個元素的節點指向當前後一個節點
prev.next = retNode.next;
// 讓刪除元素的節點的指向爲空
retNode.next = null;
// 維護size
size--;
// 將刪除的元素返回
return retNode.e;
}
// 刪除第一個元素,返回刪除的元素
public E removeFirst() {
return remove(0);
}
// 刪除第一個元素,返回刪除的元素
public E removeLast() {
return remove(size - 1);
}
七、完整的鏈表類LinkedList
LinkedList.java
// 支持泛型
public class LinkedList<E> {
// 節點設計成鏈表類中的內部類
// 設計私有類,只有在鏈表數據結構內纔可以訪問到Node
private class Node {
// 設計成public 在LinkedList中可以隨意訪問操作 不需要設置get,set方法
public E e; // 存放元素
public Node next; // 指向Node的引用
public Node(E e, Node next) {
// 將用戶傳來的數據交給節點
this.e = e;
this.next = next;
}
// 用戶只傳來e
public Node(E e) {
this(e, null);
}
// 用戶什麼都不傳
public Node() {
this(null, null);
}
// 對每一個節點設置toString方法
@Override
public String toString() {
// 每一個節點,直接打印e所對應的toString
return e.toString();
}
}
// 修改爲dummyHead設置虛擬頭節點
private Node dummyHead;
// 用戶不能在外部直接修改size
private int size; // 來記錄鏈表中有多少個元素
// 鏈表構造函數
public LinkedList() {
// 對於一個空的鏈表來說,它是存在一個節點的,這個節點就是唯一的虛擬頭節點
dummyHead = new Node(null, null);
size = 0;
}
// 獲取鏈表中元素的個數
public int getSize() {
return size;
}
// 返回鏈表是否爲空
public boolean isEmpty() {
// size是否爲0
return size == 0;
}
// 使用虛擬頭節點添加元素
public void add(int index, E e) {
// 需要先判斷index的合法性
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
// 此時dummyHead指向的是0元素之前一個的位置的節點
Node prev = dummyHead;
// 只需要遍歷到index就可以了,因爲是從dummyHead開始遍歷的
for (int i = 0; i < index; i++) {
// 當前prev存的節點的下一個節點放進prev中
// 遍歷一直挪動prev位置存放下一個節點
prev = prev.next;
}
prev.next = new Node(e, prev.next);
// 插入之後維護size
size++;
}
// 在鏈表頭添加新的元素
public void addFirst(E e) {
add(0, e);
}
// 在鏈表的末尾添加一個新的元素e
public void addLast(E e) {
// 複用add(),只需要在size添加即可
add(size, e);
}
// 查詢操作
public E get(int index) {
// get之前先判斷合法性
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
// 遍歷鏈表,是從索引爲0開始的,從當前的開始遍歷
Node cur = dummyHead.next;
for (int i = 0; i < index; i++)
cur = cur.next;
// 最終cur裏存儲的e就是需要查找的元素
return cur.e;
}
// 獲取鏈表第一個元素
public E getFirst() {
return get(0);
}
// 獲取鏈表最後第一個元素
public E getLast() {
return get(size - 1);
}
// 鏈表的更新,修改
public void set(int index, E e) {
// 判斷合法
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
// 進行一次遍歷,找到第index元素進行替換
Node cur = dummyHead.next;
for (int i = 0; i < index; i++)
cur = cur.next;
// 最終cur裏存儲的e等於新的e
cur.e = e;
}
// 查找鏈表中是否存在元素e
public boolean contains(E e) {
// 設置cur從第一個節點開始
Node cur = dummyHead.next;
// 不知道循環多少次使用while
// cur 節點不等他null的話,意味着當前cur節點是一個有效節點
while (cur != null) {
// cur.e是用戶傳來的e,返回true
if (cur.e.equals(e))
return true;
// 否則就看下一個節點
cur = cur.next;
// 直到cur爲空,說明把整個鏈表遍歷了一遍
}
return false;
}
// 刪除鏈表中index位置元素 返回刪除的元素
public E remove(int index) {
// 判斷合法
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Add failed.Illegal index.");
}
// prev存了待刪除節點之前的節點
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
// 將刪除的節點保存進retNode;
Node retNode = prev.next;
// 將當前的前一個元素的節點指向當前後一個節點
prev.next = retNode.next;
// 讓刪除元素的節點的指向爲空
retNode.next = null;
// 維護size
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();
// Node cur = dummyHead.next;
// while (cur != null) {
// res.append(cur + "->");
// cur = cur.next;
// }
for (Node cur = dummyHead.next; cur != null; cur = cur.next) {
res.append(cur.e + "->");
}
// 最後跟一個空表示達到了鏈表的結尾
res.append("NULL");
return res.toString();
}
}
八、測試類及結果
編寫測試函數對鏈表類進行測試
Main.java
public class Main {
public static void main(String[] args) {
LinkedList<Integer> linkdelist = new LinkedList<>();
// 爲鏈表添加0-4這5個元素
for (int i = 0; i < 5; i++) {
linkdelist.addFirst(i);
System.out.println(linkdelist);
}
// 測試2的位置添加100
linkdelist.add(2,100);
System.out.println(linkdelist);
// 測試是否存在元素3
boolean res = linkdelist.contains(3);
System.out.println(res);
// 測試刪除元素
linkdelist.remove(1);
System.out.println(linkdelist);
// 刪除最後一個元素
linkdelist.removeLast();
System.out.println(linkdelist);
}
}
測試結果:
需要注意的是:當遍歷存入5個元素的時候,0在最右側,4在最左側。
0->NULL
1->0->NULL
2->1->0->NULL
3->2->1->0->NULL
4->3->2->1->0->NULL
4->3->100->2->1->0->NULL
true
4->100->2->1->0->NULL
4->100->2->1->NULL
九、鏈表的時間複雜度分析
添加操作 O(n)
- addLast(e) 向鏈表尾添加一個元素,
O(n)
必須從鏈表頭遍歷到尾部 - addFirst(e) 向鏈表頭添加一個元素,
O(1)
- add(index,e) 鏈表任意位置添加元素,
O(n/2) = O(n)
刪除操作 O(n)
- removeLast(e) 從鏈表尾部刪除一個元素
O(n)
- removeFirst(e) 從鏈表頭部刪除一個元素
O(1)
- remove(index) 鏈表任意位置刪除一個元素
O(n/2) = O(n)
修改操作 O(n)
鏈表不支持隨機訪問,需要修改元素時必須從頭像後遍歷查找,所以set(index,e)是O(n)
。
查找操作 O(n)
不論是get(index),還是contains(e),都需要從頭遍歷整個鏈表,所以是O(n)
。
綜上所述,對於鏈表來說,增、刪、改、查的時間複雜度都是O(n)級別的,其中對於鏈表頭的操作都是O(1)級別的。
寫在最後
如果代碼有還沒有看懂的或者我寫錯的地方,歡迎評論,我們一起學習討論,共同進步。
推薦學習地址:
liuyubobobo老師的《玩轉數據結構》:https://coding.imooc.com/class/207.html
最後,祝自己早日鹹魚翻身,拿到心儀的Offer,衝呀!