鏈表其實也就是 線性表的鏈式存儲結構,與之前講到的順序存儲結構不同。
我們知道順序存儲結構中的元素地址都是連續的,那么這就有一個最大的缺點:當做插入跟刪除操作的時候,大量的元素需要移動。
如圖所示,元素在內存中的位置是挨着的,當中有元素被刪除,就產生空隙,於是乎后面的元素需要向前挪動去彌補。
正是因為順序存儲有這這個缺點,所以鏈式存儲結構就變得非常的有意義。
一、鏈表的存儲形式
首先,鏈表是有序的列表,但是在內存中它是這樣存儲的:
- head:這是頭指針,是鏈表指向第一個結點的指針。無論鏈表是否為空,頭指針均不為空。
- 結點:由data域和next域共同組成,前者存儲數據元素本身,后者存儲后繼位置。
上圖所示中,各個結點不一定是連續存放的,最終會有N個節點鏈接成一個鏈表,所以就成了鏈式存儲結構。
另外,因為此鏈表的每個結點中只包含一個next域,所以叫單鏈表。
二、頭指針和頭結點
1.頭指針
上面提到了頭指針,它是鏈表的必要元素。
因為鏈表既然也是線性表,所以還是要有頭有尾,頭指針就是鏈表中第一個結點的存儲位置。
而最后一個結點,指針指向空,通常用NULL表示或者'^'來表示。
2.頭結點
與頭指針不同,頭結點是不一定要有的,得更具實際需求來定。
有時候為了更加方便的操作鏈表,會在單鏈表的第一個結點前設一個結點,稱為頭結點。
加了頭結點后,對於第一結點來說,在其之前插入結點或者刪除第一結點,操作方式就與其它的結點相同了,不需要進行額外的判斷處理。
頭結點跟其他結點不同,它的數據域可以不存儲任何信息,有必要的話,可以存儲一些其他的附加信息,比如線性表的長度等。
現在我們已經知道了單向鏈表的儲存形式以及其構成有哪些,那么現在可以用更直觀的圖來展示單向鏈表中數據元素之間的關系了。
三、代碼實現一個單鏈表
1.直接在鏈表尾部依次添加
比如,現在要用單鏈表來存儲LOL里英雄的信息。如果不帶英雄排名順序的話,那么可以直接依次在鏈表的末尾增加新的結點即可。
package linkedlist;
public class SingleLinkedListDemo {
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.addHero(hero1);
singleLinkedList.addHero(hero2);
singleLinkedList.addHero(hero3);
singleLinkedList.addHero(hero4);
// 顯示鏈表內容
singleLinkedList.linkList();
}
}
// 定義SingleLinkedList 管理英雄
class SingleLinkedList {
// 初始化一個頭結點,不要動這個結點。
private HeroNode headNode = new HeroNode(0, "","");
// 添加結點 到 單向鏈表
// 當不考慮英雄順序時,找到當前鏈表的最后一個結點,再講此結點的next指向新的結點即可
public void addHero(HeroNode heroNode) {
// 因為head結點不能動,所以新建一個臨時變量,幫助遍歷
HeroNode temp = headNode;
// 開始遍歷鏈表,到最后,找最后的結點
while (true) {
// 等於null時就是最后了
if (temp.next == null) {
break;
}
// 否則就不是最后,將temp繼續向后移動
temp = temp.next;
}
// 直到退出循環,此時temp就指向了鏈表的最后
// 將最后的結點指向這個新的結點
temp.next = heroNode;
}
// 顯示鏈表內容的方法
public void linkList() {
// 判斷鏈表是否為空,空的話就不用繼續了
if (headNode.next == null) {
System.out.println("鏈表為空");
return;
}
HeroNode temp = headNode.next;
while (true) {
// 判斷是否已經到了鏈表最后
if (temp == null) {
break;
}
// 輸出結點信息
System.out.println(temp);
// 然后后移temp繼續輸出下一個結點
temp = temp.next;
}
}
}
// 定義HeroNode,每個HeroNode對象就是一個結點
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; // 指向下一個結點
// 構造器
public HeroNode(int heroNo, String heroName, String heroNickname) {
this.no = heroNo;
this.name = heroName;
this.nickname = heroNickname;
}
// 為了方便顯示,重寫toString方法
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
運行一下
HeroNode{no=1, name='易大師', nickname='無極劍聖'}
HeroNode{no=2, name='李青', nickname='盲僧'}
HeroNode{no=3, name='艾希', nickname='寒冰射手'}
HeroNode{no=4, name='菲奧娜', nickname='無雙劍姬'}
Process finished with exit code 0
可以看到,鏈表中的結點是按照添加的順序依次儲存的。
2.考慮順序的情況下添加鏈表
上面每個英雄有自己的排名,那么如果我想不關心添加的順序,在鏈表中最終都可以按照英雄的排名進行存儲,如何實現呢?
這里的話就沒有上面直接在末尾添加那么直接了,但是也不算難理解,看個示意圖。
如圖所示,現在有一個結點2要添加進來,那么來梳理一下實現的思路:
- 先要找到結點2應該添加到的位置,沒錯就是結點1與結點4之間
- 將結點1的next指向結點2,再將結點2的next指向結點4即可
是不是很簡單,不過為了實現第2點,我們還是需要借助一個輔助變量temp,可以把它看作一個指針。
temp會從頭開始遍歷鏈表,來找到結點2應該添加到的位置,此時會停在結點1,那么:
- 結點2.next = temp.next,這樣可以將結點2指向結點4
- temp.next = 結點2,這樣可以將結點1指向結點2
這樣我們的目的就達成了,代碼也就知道怎么去改了。
決定在SingleLinkedList類中,增加一個新方法,可以跟據英雄的排名進行添加。
package linkedlist;
public class SingleLinkedListDemo {
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.addByNo(hero1);
singleLinkedList.addByNo(hero4);
singleLinkedList.addByNo(hero2);
singleLinkedList.addByNo(hero3);
// 顯示鏈表內容
singleLinkedList.linkList();
}
}
// 定義SingleLinkedList 管理英雄
class SingleLinkedList {
// 初始化一個頭結點,不要動這個結點。
private HeroNode headNode = new HeroNode(0, "","");
// 添加結點 到 單向鏈表
// 當不考慮英雄順序時,找到當前鏈表的最后一個結點,再講此結點的next指向新的結點即可
public void addHero(HeroNode heroNode) {
// 因為head結點不能動,所以新建一個臨時變量,幫助遍歷
HeroNode temp = headNode;
// 開始遍歷鏈表,到最后,找最后的結點
while (true) {
// 等於null時就是最后了
if (temp.next == null) {
break;
}
// 否則就不是最后,將temp繼續向后移動
temp = temp.next;
}
// 直到退出循環,此時temp就指向了鏈表的最后
// 將最后的結點指向這個新的結點
temp.next = heroNode;
}
// 添加方法2:根據排名將英雄按照排名順序依次放到對應位置
public void addByNo(HeroNode heroNode) {
// 借助temp遍歷鏈表,找到添加位置的前一個結點
HeroNode temp = headNode;
// 考慮一種情況:當添加的位置已經存在對應排名的英雄,則不能添加
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.no > heroNode.no) { // 位置找到,在temp的后面添加
break;
} else if (temp.next.no == heroNode.no) { // 目標添加位置,已經存在對應編號,不能添加
flag = true;
break;
}
temp = temp.next; // 繼續后移
}
// 跳出循環,進行添加操作
if (flag) {
System.out.printf("准備插入的英雄編號%d已存在,不可加入\n", heroNode.no);
} else {
// 可以正常插入到鏈表
heroNode.next = temp.next;
temp.next = heroNode;
}
}
// 顯示鏈表內容的方法
public void linkList() {
// 判斷鏈表是否為空,空的話就不用繼續了
if (headNode.next == null) {
System.out.println("鏈表為空");
return;
}
HeroNode temp = headNode.next;
while (true) {
// 判斷是否已經到了鏈表最后
if (temp == null) {
break;
}
// 輸出結點信息
System.out.println(temp);
// 然后后移temp繼續輸出下一個結點
temp = temp.next;
}
}
}
// 定義HeroNode,每個HeroNode對象就是一個結點
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; // 指向下一個結點
// 構造器
public HeroNode(int heroNo, String heroName, String heroNickname) {
this.no = heroNo;
this.name = heroName;
this.nickname = heroNickname;
}
// 為了方便顯示,重寫toString方法
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
在main方法中,我們打亂結點添加的順序,運行一下,看看最終鏈表里是不是按照影響的排名順序存儲的
HeroNode{no=1, name='易大師', nickname='無極劍聖'}
HeroNode{no=2, name='李青', nickname='盲僧'}
HeroNode{no=3, name='艾希', nickname='寒冰射手'}
HeroNode{no=4, name='菲奧娜', nickname='無雙劍姬'}
Process finished with exit code 0
結果正確,符合預期,不管先添加誰,最終在鏈表里都是按照英雄的排名來存放。
繼續測試,我重復添加結點3,看下會如何。
// 加入對象結點
singleLinkedList.addByNo(hero1);
singleLinkedList.addByNo(hero4);
singleLinkedList.addByNo(hero2);
singleLinkedList.addByNo(hero3);
singleLinkedList.addByNo(hero3);
運行一下:
准備插入的英雄編號3已存在,不可加入
HeroNode{no=1, name='易大師', nickname='無極劍聖'}
HeroNode{no=2, name='李青', nickname='盲僧'}
HeroNode{no=3, name='艾希', nickname='寒冰射手'}
HeroNode{no=4, name='菲奧娜', nickname='無雙劍姬'}
Process finished with exit code 0
提示了已經存在了,不可加入。
四、總結
本文內容介紹了單鏈表的構成,另外代碼中也涉及到了單鏈表的讀取、插入。
讀取,說白了還是遍歷。
由於單鏈表的結構並沒有定義好表的長度,所以不方便用for循環來操作了,因為你不知道要循環多少次。
讀取的重點還是在於“指針后移”,從頭開始,一個個的找,直到找到你要取的元素。
所以說,單鏈表在讀取方便並沒有啥優勢。
插入的操作,就明顯好很多了,因為不需要驚動其他結點,只要將目標位置的前后2個結點的指針做下調整即可。
下面會繼續單鏈表的修改和刪除等。