3.1 鏈表(Linked List)
3.1.1 基本介紹
鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,但是並不會按線性的順序存儲數據,而是按照鏈式存儲的方式存儲數據,在每一個節點裏存到下一個節點的指針(Pointer)。由於不必須按順序存儲,鏈表在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是查找一個節點或者訪問特定編號的節點則需要O(n)的時間,而順序表相應的時間複雜度分別是O(logn)和O(1)。
使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,空間開銷比較大。
鏈表作爲一種基礎的數據結構可以用來生成其它類型的數據結構。鏈表通常由一連串節點組成,每個節點包含任意的實例數據(data fields)和一或兩個用來指向上一個/或下一個節點的位置的鏈接(“links”)。鏈表最明顯的好處就是,鏈表是一種自我指示數據類型,因爲它包含指向另一個相同類型的數據的指針(鏈接)。鏈表允許插入和移除表上任意位置上的節點,但是不允許隨機存取。鏈表有很多種不同的類型:單向鏈表,雙向鏈表以及循環鏈表。
3.1.2 單鏈表的應用實例
使用帶 head 頭的單向鏈表實現 增刪改查操作
添加
-
創建一個 head 頭節點,表示單鏈表的頭信息。
-
添加每一個節點時,直接添加到鏈表的最後節點。
有序添加 -
通過遍歷鏈表和輔助指針,找到新添加的節點的位置。
-
新添加節點的next指針 newNode.next = temp.next
-
temp節點的next指針 temp.next = newNode
修改 -
通過遍歷鏈表找到要修改的節點,然後修改可變的值
刪除
-
通過遍歷鏈表找到要刪除的這個節點的前一個節點temp。
-
修改temp節點的next temp.next = temp.next.next。
-
被刪除的節點沒有任何引用,會被垃圾回收機制回收。
列表 -
通過輔助指針,遍歷鏈表。
3.1.3 單鏈表的代碼實現
public class SingleLinkedListTest {
public static void main(String[] args) {
// 先創建節點,
HeroNode hero1 = new HeroNode(1, "宋江", "及時雨");
HeroNode hero2 = new HeroNode(2, "盧俊義", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吳用", "智多星");
HeroNode hero4 = new HeroNode(4, "林沖", "豹子頭");
// 創建鏈表
SingleLinkedList singleLinkedList = new SingleLinkedList();
// 加入元素
// singleLinkedList.add(hero1);
// singleLinkedList.add(hero4);
// singleLinkedList.add(hero2);
// singleLinkedList.add(hero3);
// 加入按照編號的順序
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero3);
// 顯示列表
singleLinkedList.list();
// 測試修改節點的代碼
HeroNode newHeroNode = new HeroNode(2, "小盧", "玉麒麟~~");
singleLinkedList.update(newHeroNode);
System.out.println("修改後的鏈表情況~~");
singleLinkedList.list();
// 刪除一個節點
singleLinkedList.delete(newHeroNode);
System.out.println("刪除後的鏈表情況~~");
singleLinkedList.list();
}
}
class SingleLinkedList {
private HeroNode first = new HeroNode(0, "", "");
public void add(HeroNode e) {
HeroNode temp = first;
// 遍歷鏈表,找到鏈表的最後節點
while (true) {
if (temp.next == null) {
break;
}
// 如果沒有找到最後,將將 temp 後移
temp = temp.next;
}
// 當退出 while 循環時,temp 就指向了鏈表的最後,將最後這個節點的 next 指向新的節點
temp.next = e;
}
public void addByOrder(HeroNode e) {
HeroNode temp = first;
boolean flag = false;
// 遍歷鏈表,找到鏈表的最後節點
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.order > e.order) {
// 位置找到,就在 temp 的後面插入
break;
} else if (temp.next.order == e.order) {
// 說明希望添加的 heroNode 的編號已然存在
flag = true;
break;
}
// 如果沒有找到最後,將將 temp 後移
temp = temp.next;
}
// 當退出 while 循環時,temp 就指向了鏈表的最後,判斷 flag 的值
if (flag) {
System.out.printf("準備插入的英雄的編號 %d 已經存在了, 不能加入\n", e.order);
} else {
// 插入到鏈表中,temp 的後面
e.next = temp.next;
temp.next = e;
}
}
public void update(HeroNode e) {
HeroNode temp = first;
boolean flag = false;
if (null == temp.next) {
System.out.println("鏈表爲空~");
return;
}
while (true) {
// 已經遍歷完列表
if (null == temp) {
break;
}
if (temp.order == e.order) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.name = e.name;
temp.nickname = e.nickname;
} else {
// 沒有找到
System.out.printf("沒有找到編號 %d 的節點,不能修改\n", e.order);
}
}
public void delete(HeroNode e) {
HeroNode temp = first;
boolean flag = false;
if (null == temp.next) {
System.out.println("鏈表爲空~");
return;
}
while (true) {
// 已經遍歷完列表
if (null == temp) {
break;
}
if (temp.next.order == e.order) {
// 找到的待刪除節點的前一個節點 temp
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.next = temp.next.next;
} else {
// 沒有找到
System.out.printf("沒有找到編號 %d 的節點,不能修改\n", e.order);
}
}
public void list() {
HeroNode temp = first.next;
if (null == temp) {
System.out.println("鏈表爲空~");
return;
}
while (true) {
if (null == temp) {
return;
}
System.out.println(temp);
temp = temp.next;
}
}
}
@Data
class HeroNode {
protected int order;
protected String name;
protected String nickname;
protected HeroNode next;
public HeroNode(int order, String name, String nickname) {
this.order = order;
this.name = name;
this.nickname = nickname;
}
}
3.1.4 單鏈表的常見問題
問題:獲取單鏈表中有效的節點的個數
方案:
public int size(HeroNode first) {
int size = 0;
// 定義輔助指針
HeroNode temp = first.next;
while (null != temp) {
size++;
temp = temp.next;
}
return size;
}
問題:獲取單鏈表中倒數第k個節點
方案:
- index 表示是倒數第 index 個節點。
- 首先判斷鏈表是否爲空,入參是否合法。
- 然後獲取鏈表的size。
- 在得到size後,從鏈表的第一個開始遍歷(size-index)個,存在就返回對應節點,否則就返回null。
public HeroNode findLastIndexNode(HeroNode first, int index) {
HeroNode temp = first.next;
if (null == temp) {
return null;
}
int size = size(first);
if (index <= 0 || index > size) {
return null;
}
for (int i = 0; i < size - index; i++) {
temp = temp.next;
}
return temp;
}
問題:實現單鏈表的反轉
方案:
- 定義臨時的反轉頭節點 HeroNode reversetNode = new HeroNode(0, “”, “”)。
- 遍歷鏈表將每個節點取出,然後插入到反轉頭節點的最前端。
- 修改頭節點的next:first.next = reversetNode.next。
public void reverseLinkedList(HeroNode first) {
HeroNode temp = first.next;
// 如果鏈表爲空或者鏈表只有一個節點,之際返回
if (null == temp || null == temp.next) {
return;
}
// 定義臨時的反轉頭節點
HeroNode reversetNode = new HeroNode(0, "", "");
// 定義臨時next節點
HeroNode next;
// 遍歷鏈表,然後將每個節點放到新鏈表的最前端
while (null != temp) {
next = temp.next;
temp.next = reversetNode.next;
reversetNode.next = temp;
temp = next;
}
first.next = reversetNode.next;
}
問題:反向遍歷單鏈表
方案:
- 可以先將單鏈表進行反轉,然後遍歷單鏈表。
- 使用棧數據結構,將節點壓入棧中,利用棧先進後出的特點,實現反向遍歷單鏈表。
public void reversePrint() {
HeroNode temp = first.next;
// 鏈表爲空
if (null == temp) {
return;
}
// 創建棧數據結構
Stack<HeroNode> stack = new Stack<>();
// 遍歷鏈表,將每個節點壓入到棧中
while (null != temp) {
stack.push(temp);
temp = temp.next;
}
// 將棧中的節點進行打印,pop 出棧
while (stack.size() > 0) {
// stack 的特點是先進後出
System.out.println(stack.pop());
}
}
問題:合併兩個有序的單鏈表,合併之後依然有序
方案:
3.2.1 雙向鏈表的應用實例
管理單向鏈表的缺點分析:
- 單向鏈表的查找方向只能是一個方向,而雙向鏈表可以向前或者向後查找。
- 單向鏈表不能自我刪除,需要靠輔助節點,而雙向鏈表則可以自我刪除。
添加
-
創建一個 head 頭節點,表示雙向鏈表的頭信息。
-
添加每一個節點時,直接添加到鏈表的最後節點。
-
temp.next = newNode 且 newNode.pre = temp。
修改 -
通過遍歷鏈表找到要修改的節點,然後修改可變的值。
刪除
-
通過遍歷鏈表找到要刪除的這個節點 temp。
-
修改temp節點 temp.pre.next = temp.next 且 temp.next.pre = temp.pre。
-
被刪除的節點沒有任何引用,會被垃圾回收機制回收。
列表 -
通過輔助指針,可以向前或者向後遍歷鏈表。
3.2.2 雙向鏈表的代碼實現
public class DoubleLinkedListTest {
public static void main(String[] args) {
// 先創建節點
DoubleHeroNode hero1 = new DoubleHeroNode(1, "宋江", "及時雨");
DoubleHeroNode hero2 = new DoubleHeroNode(2, "盧俊義", "玉麒麟");
DoubleHeroNode hero3 = new DoubleHeroNode(3, "吳用", "智多星");
DoubleHeroNode hero4 = new DoubleHeroNode(4, "林沖", "豹子頭");
// 創建一個雙向鏈表
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
// 添加
doubleLinkedList.addByOrder(hero1);
doubleLinkedList.addByOrder(hero4);
doubleLinkedList.addByOrder(hero3);
doubleLinkedList.addByOrder(hero2);
doubleLinkedList.list();
System.out.println(doubleLinkedList.size());
// 修改
DoubleHeroNode newHeroNode = new DoubleHeroNode(4, "公孫勝", "入雲龍");
doubleLinkedList.update(newHeroNode);
System.out.println("修改後的鏈表情況");
doubleLinkedList.list();
System.out.println(doubleLinkedList.size());
// 刪除
doubleLinkedList.delete(hero4);
System.out.println("刪除後的鏈表情況~~");
doubleLinkedList.list();
System.out.println(doubleLinkedList.size());
}
}
class DoubleLinkedList {
DoubleHeroNode first = new DoubleHeroNode(0, "", "");
public void add(DoubleHeroNode e) {
DoubleHeroNode temp = first;
// 遍歷鏈表,找到鏈表的最後節點
while (true) {
if (temp.next == null) {
break;
}
// 如果沒有找到最後,將將 temp 後移
temp = temp.next;
}
// 當退出 while 循環時,temp 就指向了鏈表的最後,將最後這個節點的 next 指向新的節點,新節點的 pre 指向temp
temp.next = e;
e.pre = temp;
}
public void addByOrder(DoubleHeroNode e) {
DoubleHeroNode temp = first;
boolean flag = false;
// 遍歷鏈表,找到鏈表對應的節點
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.order > e.order) {
// 位置找到,就在 temp 的後面插入
break;
} else if (temp.next.order == e.order) {
// 說明希望添加的 heroNode 的編號已然存在
flag = true;
break;
}
// 如果沒有找到最後,將將 temp 後移
temp = temp.next;
}
// 當退出 while 循環時,temp 就指向了鏈表的最後,判斷 flag 的值
if (flag) {
System.out.printf("準備插入的英雄的編號 %d 已經存在了, 不能加入\n", e.order);
} else {
// 插入到鏈表中,temp 的後面
e.next = temp.next;
e.pre = temp;
if (null != temp.next) {
temp.next.pre = e;
}
temp.next = e;
}
}
public void update(DoubleHeroNode e) {
DoubleHeroNode temp = first;
boolean flag = false;
if (null == temp.next) {
System.out.println("鏈表爲空~");
return;
}
while (true) {
// 已經遍歷完列表
if (null == temp) {
break;
}
if (temp.order == e.order) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.name = e.name;
temp.nickname = e.nickname;
} else {
// 沒有找到
System.out.printf("沒有找到編號 %d 的節點,不能修改\n", e.order);
}
}
public void delete(DoubleHeroNode e) {
DoubleHeroNode temp = first;
boolean flag = false;
if (null == temp.next) {
System.out.println("鏈表爲空~");
return;
}
while (true) {
// 已經遍歷完列表
if (null == temp) {
break;
}
if (temp.order == e.order) {
// 找到的待刪除節點 temp
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.pre.next = temp.next;
if (null != temp.next) {
temp.next.pre = temp.pre;
}
} else {
// 沒有找到
System.out.printf("沒有找到編號 %d 的節點,不能修改\n", e.order);
}
}
public void list() {
DoubleHeroNode temp = first.next;
if (null == temp) {
System.out.println("鏈表爲空~");
return;
}
while (true) {
if (null == temp) {
return;
}
System.out.println(temp);
temp = temp.next;
}
}
public int size() {
int size = 0;
// 定義輔助指針
DoubleHeroNode temp = first.next;
while (null != temp) {
size++;
temp = temp.next;
}
return size;
}
}
class DoubleHeroNode {
protected int order;
protected String name;
protected String nickname;
protected DoubleHeroNode pre;
protected DoubleHeroNode next;
public DoubleHeroNode(int order, String name, String nickname) {
this.order = order;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "HeroNode [order = " + order + ", name = " + name + ", nickname = " + nickname + "]";
}
}
3.3.1 單向環形鏈表
在一個循環鏈表中,首節點和末節點被連接在一起,這種方式在單向和雙向鏈表中皆可實現。要轉換一個循環鏈表,可以開始於任意一個節點然後沿着列表的一個方向直到返回開始的節點,循環鏈表可以被視爲“無頭無尾”,這種列表很利於節約數據存儲緩存,指向整個列表的指針可以被稱作訪問指針。
3.3.2 單向環形鏈表的應用實例
約瑟夫問題
設編號爲 1,2,… n 的 n 個人圍坐一圈,約定編號爲 k(1<=k<=n)的人從 1 開始報數,數到 m 的那個人出列,它的下一位又從 1 開始報數,數到 m 的那個人又出列,依次類推,直到所有人出列爲止,由此產生一個出隊編號的序列。
思路分析
使用循環鏈表來處理約瑟夫問題,先構成一個有 n 個節點的單向循環鏈表,然後由 k 節點開始從 1 開始計數,記到 m 時對應節點從鏈表刪除,然後再從被刪除節點的下一個節點開始從 1 計數,知道所有的節點從鏈表中刪除。
- 首先創建第一個節點 first。
- 然後創建一個節點,讓 fiirst 指向該節點,並形成環形單向鏈表。
- 後面每創建一個節點,就把該節點加入到環形鏈表。
- 定義輔助指針 current 指向 first 節點。
- 遍歷該環形單向鏈表,current.next = first 結束。
3.3.3 約瑟夫代碼的實現
public class CircleSingleLinkedListTest {
public static void main(String[] args) {
CircleSingleLinkedList linkedList = new CircleSingleLinkedList();
linkedList.add(new Node(1));
linkedList.add(new Node(2));
linkedList.add(new Node(3));
linkedList.add(new Node(4));
linkedList.add(new Node(5));
linkedList.list();
System.out.printf("鏈表的 size 爲 %d \n\n", linkedList.size());
System.out.println("約瑟夫問題的代碼實現:");
linkedList.countNode(linkedList.size(), 2, 1);
}
}
class CircleSingleLinkedList {
// 創建第一個節點,當前沒有編號
private Node first = null;
// 輔助指針
private Node current = null;
public void add(Node e) {
if (null == first) {
first = e;
// 形成環形鏈表
first.next = first;
// current 指向 first
current = first;
} else {
current.next = e;
e.next = first;
current = e;
}
}
/**
* 約瑟夫問題
*
* @param n 單向環形鏈表共有 n 個節點
* @param k 由 k 節點開始計數
* @param m 每次數 m
*/
public void countNode(int n, int k, int m) {
// 參數校驗
if (null == first || k < 1 || n < k) {
System.out.println("參數輸入有誤, 請重新輸入");
return;
}
// 定義輔助指針,並指向環形鏈表的最後一個位置
Node helpNode = first;
while (true) {
// 說明 helpNode 節點已經指向最後的節點
if (helpNode.next == first) {
break;
}
helpNode = helpNode.next;
}
// 報數前將 first 和 helpNode 移動 k - 1 次
for (int j = 0; j < k - 1; j++) {
first = first.next;
helpNode = helpNode.next;
}
// 循環刪除符合條件的節點
while (true) {
// 說明環形鏈表中只有一個節點
if (helpNode == first) {
break;
}
// 將 first 和 helpNode 指針同時移動 m - 1
for (int j = 0; j < m - 1; j++) {
first = first.next;
helpNode = helpNode.next;
}
// 這時 first 指向的節點,就是要刪除的節點
System.out.printf("編號 %d 刪除\n", first.number);
// 刪除對應的節點
first = first.next;
helpNode.next = first;
}
System.out.printf("編號 %d 刪除\n", first.number);
}
public int size() {
int size = 0;
if (null == first) {
return size;
}
Node helpNode = first;
while (true) {
if (helpNode.next == first) {
break;
}
size++;
helpNode = helpNode.next;
}
return size;
}
public void list() {
Node helpNode = first;
if (null == helpNode) {
System.out.println("鏈表爲空~~");
return;
}
while (true) {
System.out.printf("編號 %d \n", helpNode.number);
if (helpNode.next == first) {
break;
}
helpNode = helpNode.next;
}
}
}
@Getter
@Setter
class Node {
protected int number;
protected Node next;
public Node(int number) {
this.number = number;
}
}