1. 程式人生 > >【小白學演算法】5.連結串列(linked list),連結串列的插入、讀取

【小白學演算法】5.連結串列(linked list),連結串列的插入、讀取

連結串列其實也就是 線性表的鏈式儲存結構,與之前講到的順序儲存結構不同。 我們知道順序儲存結構中的元素地址都是連續的,那麼這就有一個最大的缺點:當做插入跟刪除操作的時候,大量的元素需要移動。 如圖所示,元素在記憶體中的位置是挨著的,當中有元素被刪除,就產生空隙,於是乎後面的元素需要向前挪動去彌補。 ![](https://img2020.cnblogs.com/blog/1268169/202103/1268169-20210314224721609-679832654.png) 正是因為順序儲存有這這個缺點,所以鏈式儲存結構就變得非常的有意義。 ### 一、連結串列的儲存形式 首先,連結串列是有序的列表,但是在記憶體中它是這樣儲存的: ![](https://img2020.cnblogs.com/blog/1268169/202103/1268169-20210314225905381-1468488339.png) * head:這是頭指標,是連結串列指向第一個結點的指標。無論連結串列是否為空,頭指標均不為空。 * 結點:由data域和next域共同組成,前者儲存資料元素本身,後者儲存後繼位置。 上圖所示中,各個結點不一定是連續存放的,最終會有N個節點連結成一個連結串列,所以就成了鏈式儲存結構。 另外,因為此連結串列的每個結點中只包含一個next域,所以叫單鏈表。 ### 二、頭指標和頭結點 #### 1.頭指標 上面提到了頭指標,它是連結串列的必要元素。 因為連結串列既然也是線性表,所以還是要有頭有尾,頭指標就是連結串列中第一個結點的儲存位置。 而最後一個結點,指標指向空,通常用NULL表示或者'^'來表示。 ![](https://img2020.cnblogs.com/blog/1268169/202103/1268169-20210314233454248-1539564243.png) #### 2.頭結點 與頭指標不同,頭結點是不一定要有的,得更具實際需求來定。 有時候為了更加方便的操作連結串列,會在單鏈表的**第一個結點前**設一個結點,稱為頭結點。 加了頭結點後,對於第一結點來說,在其之前插入結點或者刪除第一結點,操作方式就與其它的結點相同了,不需要進行額外的判斷處理。 頭結點跟其他結點不同,它的資料域可以不儲存任何資訊,有必要的話,可以儲存一些其他的附加資訊,比如線性表的長度等。 ![](https://img2020.cnblogs.com/blog/1268169/202103/1268169-20210315000401988-352104062.png) 現在我們已經知道了單向連結串列的儲存形式以及其構成有哪些,那麼現在可以用更直觀的圖來展示單向連結串列中資料元素之間的關係了。 ![](https://img2020.cnblogs.com/blog/1268169/202103/1268169-20210315230534286-1053981054.png) ### 三、程式碼實現一個單鏈表 #### 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.考慮順序的情況下新增連結串列 上面每個英雄有自己的排名,那麼如果我想不關心新增的順序,在連結串列中最終都可以按照英雄的排名進行儲存,如何實現呢? 這裡的話就沒有上面直接在末尾新增那麼直接了,但是也不算難理解,看個示意圖。 ![](https://img2020.cnblogs.com/blog/1268169/202103/1268169-20210317233901843-1270229788.png) 如圖所示,現在有一個結點2要新增進來,那麼來梳理一下實現的思路: 1. 先要找到結點2應該新增到的位置,沒錯就是結點1與結點4之間 2. 將結點1的next指向結點2,再將結點2的next指向結點4即可 是不是很簡單,不過為了實現第2點,我們還是需要藉助一個輔助變數temp,可以把它看作一個指標。 ![](https://img2020.cnblogs.com/blog/1268169/202103/1268169-20210317234913723-2039159686.png) 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個結點的指標做下調整即可。 下面會繼續單鏈表的修改和刪