數據結構003_鏈表
- 2020 年 9 月 17 日
- 筆記
鏈表內容我打算就寫在這一篇里。包括單鏈表,雙向鏈表,單向及雙向環形鏈表,josephu問題。
- 鏈表是以節點的方式來存儲,是鏈式存儲
- 每個節點包含data域,next域(指向下一個節點)
- 鏈表在記憶體中未必是連續存儲
- 鏈表根據實際需求確定帶有頭結點和不帶頭結點
一 單鏈表的增刪改查
單鏈表的兩種添加方式,一種是直接載入鏈表尾部,另一種是按節點順序添加到指定位置。
兩種都比較簡單,直接上程式碼。
package com.njbj.linkedlist; import java.util.Stack; 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.add(hero1); singleLinkedList.add(hero4); singleLinkedList.add(hero2); singleLinkedList.add(hero3); // 測試一下單鏈表的反轉功能 System.out.println("原來鏈表的情況~~"); singleLinkedList.list(); // System.out.println("反轉單鏈表~~"); // reversetList(singleLinkedList.getHead()); // singleLinkedList.list(); System.out.println("測試逆序列印單鏈表, 沒有改變鏈表的結構~~"); reversePrint(singleLinkedList.getHead()); /* * //加入按照編號的順序 singleLinkedList.addByOrder(hero1); * singleLinkedList.addByOrder(hero4); * singleLinkedList.addByOrder(hero2); * singleLinkedList.addByOrder(hero3); * * //顯示一把 singleLinkedList.list(); * * //測試修改節點的程式碼 HeroNode newHeroNode = new HeroNode(2, "小盧", "玉麒麟~~"); * singleLinkedList.update(newHeroNode); * * System.out.println("修改後的鏈表情況~~"); singleLinkedList.list(); * * //刪除一個節點 singleLinkedList.del(1); singleLinkedList.del(4); * System.out.println("刪除後的鏈表情況~~"); singleLinkedList.list(); * * //測試一下 求單鏈表中有效節點的個數 System.out.println("有效的節點個數=" + * getLength(singleLinkedList.getHead()));//2 * * //測試一下看看是否得到了倒數第K個節點 HeroNode res = * findLastIndexNode(singleLinkedList.getHead(), 3); * System.out.println("res=" + res); */ } // 方式2: // 可以利用棧這個數據結構,將各個節點壓入到棧中,然後利用棧的先進後出的特點,就實現了逆序列印的效果 public static void reversePrint(HeroNode head) { if (head.next == null) { return;// 空鏈表,不能列印 } // 創建要給一個棧,將各個節點壓入棧 Stack<HeroNode> stack = new Stack<HeroNode>(); HeroNode cur = head.next; // 將鏈表的所有節點壓入棧 while (cur != null) { stack.push(cur); cur = cur.next; // cur後移,這樣就可以壓入下一個節點 } // 將棧中的節點進行列印,pop 出棧 while (stack.size() > 0) { System.out.println(stack.pop()); // stack的特點是先進後出 } } // 將單鏈表反轉 public static void reversetList(HeroNode head) { // 如果當前鏈表為空,或者只有一個節點,無需反轉,直接返回 if (head.next == null || head.next.next == null) { return; } // 定義一個輔助的指針(變數),幫助我們遍歷原來的鏈表 HeroNode cur = head.next; HeroNode next = null;// 指向當前節點[cur]的下一個節點 HeroNode reverseHead = new HeroNode(0, "", ""); // 遍歷原來的鏈表,每遍歷一個節點,就將其取出,並放在新的鏈表reverseHead 的最前端 // 動腦筋 while (cur != null) { next = cur.next;// 先暫時保存當前節點的下一個節點,因為後面需要使用 cur.next = reverseHead.next;// 將cur的下一個節點指向新的鏈表的最前端 reverseHead.next = cur; // 將cur 連接到新的鏈表上 cur = next;// 讓cur後移 } // 將head.next 指向 reverseHead.next , 實現單鏈表的反轉 head.next = reverseHead.next; } // 查找單鏈表中的倒數第k個結點 【新浪面試題】 // 思路 // 1. 編寫一個方法,接收head節點,同時接收一個index // 2. index 表示是倒數第index個節點 // 3. 先把鏈表從頭到尾遍歷,得到鏈表的總的長度 getLength // 4. 得到size 後,我們從鏈表的第一個開始遍歷 (size-index)個,就可以得到 // 5. 如果找到了,則返回該節點,否則返回nulll public static HeroNode findLastIndexNode(HeroNode head, int index) { // 判斷如果鏈表為空,返回null if (head.next == null) { return null;// 沒有找到 } // 第一個遍歷得到鏈表的長度(節點個數) int size = getLength(head); // 第二次遍歷 size-index 位置,就是我們倒數的第K個節點 // 先做一個index的校驗 if (index <= 0 || index > size) { return null; } // 定義給輔助變數, for 循環定位到倒數的index HeroNode cur = head.next; // 3 // 3 - 1 = 2 for (int i = 0; i < size - index; i++) { cur = cur.next; } return cur; } // 方法:獲取到單鏈表的節點的個數(如果是帶頭結點的鏈表,需求不統計頭節點) /** * * @param head * 鏈表的頭節點 * @return 返回的就是有效節點的個數 */ public static int getLength(HeroNode head) { if (head.next == null) { // 空鏈表 return 0; } int length = 0; // 定義一個輔助的變數, 這裡我們沒有統計頭節點 HeroNode cur = head.next; while (cur != null) { length++; cur = cur.next; // 遍歷 } return length; } } // 定義SingleLinkedList 管理我們的英雄 class SingleLinkedList { // 先初始化一個頭節點, 頭節點不要動, 不存放具體的數據 private HeroNode head = new HeroNode(0, "", ""); // 返回頭節點 public HeroNode getHead() { return head; } // 添加節點到單向鏈表 // 思路,當不考慮編號順序時 // 1. 找到當前鏈表的最後節點 // 2. 將最後這個節點的next 指向 新的節點 public void add(HeroNode heroNode) { // 因為head節點不能動,因此我們需要一個輔助遍歷 temp HeroNode temp = head; // 遍歷鏈表,找到最後 while (true) { // 找到鏈表的最後 if (temp.next == null) {// break; } // 如果沒有找到最後, 將將temp後移 temp = temp.next; } // 當退出while循環時,temp就指向了鏈表的最後 // 將最後這個節點的next 指向 新的節點 temp.next = heroNode; } // 第二種方式在添加英雄時,根據排名將英雄插入到指定位置 // (如果有這個排名,則添加失敗,並給出提示) public void addByOrder(HeroNode heroNode) { // 因為頭節點不能動,因此我們仍然通過一個輔助指針(變數)來幫助找到添加的位置 // 因為單鏈表,因為我們找的temp 是位於 添加位置的前一個節點,否則插入不了 HeroNode temp = head; boolean flag = false; // flag標誌添加的編號是否存在,默認為false while (true) { if (temp.next == null) {// 說明temp已經在鏈表的最後 break; // } if (temp.next.no > heroNode.no) { // 位置找到,就在temp的後面插入 break; } else if (temp.next.no == heroNode.no) {// 說明希望添加的heroNode的編號已然存在 flag = true; // 說明編號存在 break; } temp = temp.next; // 後移,遍歷當前鏈表 } // 判斷flag 的值 if (flag) { // 不能添加,說明編號存在 System.out.printf("準備插入的英雄的編號 %d 已經存在了, 不能加入\n", heroNode.no); } else { // 插入到鏈表中, temp的後面 heroNode.next = temp.next; temp.next = heroNode; } } // 修改節點的資訊, 根據no編號來修改,即no編號不能改. // 說明 // 1. 根據 newHeroNode 的 no 來修改即可 public void update(HeroNode newHeroNode) { // 判斷是否空 if (head.next == null) { System.out.println("鏈表為空~"); return; } // 找到需要修改的節點, 根據no編號 // 定義一個輔助變數 HeroNode temp = head.next; boolean flag = false; // 表示是否找到該節點 while (true) { if (temp == null) { break; // 已經遍歷完鏈表 } if (temp.no == newHeroNode.no) { // 找到 flag = true; break; } temp = temp.next; } // 根據flag 判斷是否找到要修改的節點 if (flag) { temp.name = newHeroNode.name; temp.nickname = newHeroNode.nickname; } else { // 沒有找到 System.out.printf("沒有找到 編號 %d 的節點,不能修改\n", newHeroNode.no); } } // 刪除節點 // 思路 // 1. head 不能動,因此我們需要一個temp輔助節點找到待刪除節點的前一個節點 // 2. 說明我們在比較時,是temp.next.no 和 需要刪除的節點的no比較 public void del(int no) { HeroNode temp = head; boolean flag = false; // 標誌是否找到待刪除節點的 while (true) { if (temp.next == null) { // 已經到鏈表的最後 break; } if (temp.next.no == no) { // 找到的待刪除節點的前一個節點temp flag = true; break; } temp = temp.next; // temp後移,遍歷 } // 判斷flag if (flag) { // 找到 // 可以刪除 temp.next = temp.next.next; } else { System.out.printf("要刪除的 %d 節點不存在\n", no); } } // 顯示鏈表[遍歷] public void list() { // 判斷鏈表是否為空 if (head.next == null) { System.out.println("鏈表為空"); return; } // 因為頭節點,不能動,因此我們需要一個輔助變數來遍歷 HeroNode temp = head.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 no, String name, String nickname) { this.no = no; this.name = name; this.nickname = nickname; } // 為了顯示方法,我們重新toString @Override public String toString() { return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]"; } }
二、雙向鏈表的增刪改查
單向鏈表查找只能是一個方向,而雙向鏈表可以是向前或者向後查找
單向鏈表不能自我刪除,刪除主要靠輔助節點。雙向鏈表可自我刪除。
雙向鏈表的操作都在程式碼里了,思路就在注釋里,直接貼程式碼:
package com.njbj.linkedlist; public class DoubleLinkedListDemo { public static void main(String[] args) { // 測試 System.out.println("雙向鏈表的測試"); // 先創建節點 HeroNode2 hero1 = new HeroNode2(1, "宋江", "及時雨"); HeroNode2 hero2 = new HeroNode2(2, "盧俊義", "玉麒麟"); HeroNode2 hero3 = new HeroNode2(3, "吳用", "智多星"); HeroNode2 hero4 = new HeroNode2(4, "林沖", "豹子頭"); // 創建一個雙向鏈表 DoubleLinkedList doubleLinkedList = new DoubleLinkedList(); doubleLinkedList.add(hero1); doubleLinkedList.add(hero2); doubleLinkedList.add(hero3); doubleLinkedList.add(hero4); doubleLinkedList.list(); // 修改 HeroNode2 newHeroNode = new HeroNode2(4, "公孫勝", "入雲龍"); doubleLinkedList.update(newHeroNode); System.out.println("修改後的鏈表情況"); doubleLinkedList.list(); // 刪除 doubleLinkedList.del(3); System.out.println("刪除後的鏈表情況~~"); doubleLinkedList.list(); } } // 創建一個雙向鏈表的類 class DoubleLinkedList { // 先初始化一個頭節點, 頭節點不要動, 不存放具體的數據 private HeroNode2 head = new HeroNode2(0, "", ""); // 返回頭節點 public HeroNode2 getHead() { return head; } // 遍歷雙向鏈表的方法 // 顯示鏈表[遍歷] public void list() { // 判斷鏈表是否為空 if (head.next == null) { System.out.println("鏈表為空"); return; } // 因為頭節點,不能動,因此我們需要一個輔助變數來遍歷 HeroNode2 temp = head.next; while (true) { // 判斷是否到鏈表最後 if (temp == null) { break; } // 輸出節點的資訊 System.out.println(temp); // 將temp後移, 一定小心 temp = temp.next; } } // 添加一個節點到雙向鏈表的最後. public void add(HeroNode2 heroNode) { // 因為head節點不能動,因此我們需要一個輔助遍歷 temp HeroNode2 temp = head; // 遍歷鏈表,找到最後 while (true) { // 找到鏈表的最後 if (temp.next == null) {// break; } // 如果沒有找到最後, 將將temp後移 temp = temp.next; } // 當退出while循環時,temp就指向了鏈表的最後 // 形成一個雙向鏈表 temp.next = heroNode; heroNode.pre = temp; } // 修改一個節點的內容, 可以看到雙向鏈表的節點內容修改和單向鏈表一樣 // 只是 節點類型改成 HeroNode2 public void update(HeroNode2 newHeroNode) { // 判斷是否空 if (head.next == null) { System.out.println("鏈表為空~"); return; } // 找到需要修改的節點, 根據no編號 // 定義一個輔助變數 HeroNode2 temp = head.next; boolean flag = false; // 表示是否找到該節點 while (true) { if (temp == null) { break; // 已經遍歷完鏈表 } if (temp.no == newHeroNode.no) { // 找到 flag = true; break; } temp = temp.next; } // 根據flag 判斷是否找到要修改的節點 if (flag) { temp.name = newHeroNode.name; temp.nickname = newHeroNode.nickname; } else { // 沒有找到 System.out.printf("沒有找到 編號 %d 的節點,不能修改\n", newHeroNode.no); } } // 從雙向鏈表中刪除一個節點, // 說明 // 1 對於雙向鏈表,我們可以直接找到要刪除的這個節點 // 2 找到後,自我刪除即可 public void del(int no) { // 判斷當前鏈表是否為空 if (head.next == null) {// 空鏈表 System.out.println("鏈表為空,無法刪除"); return; } HeroNode2 temp = head.next; // 輔助變數(指針) boolean flag = false; // 標誌是否找到待刪除節點的 while (true) { if (temp == null) { // 已經到鏈表的最後 break; } if (temp.no == no) { // 找到的待刪除節點的前一個節點temp flag = true; break; } temp = temp.next; // temp後移,遍歷 } // 判斷flag if (flag) { // 找到 // 可以刪除 // temp.next = temp.next.next;[單向鏈表] temp.pre.next = temp.next; // 這裡我們的程式碼有問題? // 如果是最後一個節點,就不需要執行下面這句話,否則出現空指針 if (temp.next != null) { temp.next.pre = temp.pre; } } else { System.out.printf("要刪除的 %d 節點不存在\n", no); } } } // 定義HeroNode2 , 每個HeroNode 對象就是一個節點 class HeroNode2 { public int no; public String name; public String nickname; public HeroNode2 next; // 指向下一個節點, 默認為null public HeroNode2 pre; // 指向前一個節點, 默認為null // 構造器 public HeroNode2(int no, String name, String nickname) { this.no = no; this.name = name; this.nickname = nickname; } // 為了顯示方法,我們重新toString @Override public String toString() { return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]"; } }
三、單向環形鏈表
1.約瑟夫問題描述:
先構成一個有n個節點的單鏈表,從k節點起從1開始計數,計到m時,對應節點從鏈表中刪除。然後再從刪除節點的下一個節點重新從1開始計數,知道最後一個節點從鏈表中刪除演算法結束。(最後產生一個出隊編號的序列)
2.
- 構建一個單向的環形鏈表思路:
- 1). 先創建第一個節點, 讓 first 指向該節點,並形成環形
- 2). 後面當我們每創建一個新的節點,就把該節點,加入到已有的環形鏈表中即可.
- 遍歷環形鏈表思路:
- 1). 先讓一個輔助指針(變數) curBoy,指向first節點
- 2). 然後通過一個while循環遍歷 該環形鏈表即可 curBoy.next == first 結束
示例和程式碼:
根據用戶的輸入,生成一個小孩出圈的順序
n = 5 , 即有5個人
k = 1, 從第一個人開始報數
m = 2, 數2下
分析:
1. 需求創建一個輔助指針(變數) helper , 事先應該指向環形鏈表的最後這個節點.
補充: 小孩報數前,先讓 first 和 helper 移動 k - 1次
2. 當小孩報數時,讓first 和 helper 指針同時 的移動 m - 1 次
3. 這時就可以將first 指向的小孩節點 出圈
first = first .next
helper.next = first
原來first 指向的節點就沒有任何引用,就會被回收
出圈的順序
2->4->1->5->3

package com.atguigu.linkedlist; public class Josepfu { public static void main(String[] args) { // 測試一把看看構建環形鏈表,和遍歷是否ok CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList(); circleSingleLinkedList.addBoy(125);// 加入5個小孩節點 circleSingleLinkedList.showBoy(); // 測試一把小孩出圈是否正確 circleSingleLinkedList.countBoy(10, 20, 125); // 2->4->1->5->3 // String str = "7*2*2-5+1-5+3-3"; } } // 創建一個環形的單向鏈表 class CircleSingleLinkedList { // 創建一個first節點,當前沒有編號 private Boy first = null; // 添加小孩節點,構建成一個環形的鏈表 public void addBoy(int nums) { // nums 做一個數據校驗 if (nums < 1) { System.out.println("nums的值不正確"); return; } Boy curBoy = null; // 輔助指針,幫助構建環形鏈表 // 使用for來創建我們的環形鏈表 for (int i = 1; i <= nums; i++) { // 根據編號,創建小孩節點 Boy boy = new Boy(i); // 如果是第一個小孩 if (i == 1) { first = boy; first.setNext(first); // 構成環 curBoy = first; // 讓curBoy指向第一個小孩 } else { curBoy.setNext(boy);// boy.setNext(first);// curBoy = boy; } } } // 遍歷當前的環形鏈表 public void showBoy() { // 判斷鏈表是否為空 if (first == null) { System.out.println("沒有任何小孩~~"); return; } // 因為first不能動,因此我們仍然使用一個輔助指針完成遍歷 Boy curBoy = first; while (true) { System.out.printf("小孩的編號 %d \n", curBoy.getNo()); if (curBoy.getNext() == first) {// 說明已經遍歷完畢 break; } curBoy = curBoy.getNext(); // curBoy後移 } } // 根據用戶的輸入,計算出小孩出圈的順序 /** * * @param startNo * 表示從第幾個小孩開始數數 * @param countNum * 表示數幾下 * @param nums * 表示最初有多少小孩在圈中 */ public void countBoy(int startNo, int countNum, int nums) { // 先對數據進行校驗 if (first == null || startNo < 1 || startNo > nums) { System.out.println("參數輸入有誤, 請重新輸入"); return; } // 創建要給輔助指針,幫助完成小孩出圈 Boy helper = first; // 需求創建一個輔助指針(變數) helper , 事先應該指向環形鏈表的最後這個節點 while (true) { if (helper.getNext() == first) { // 說明helper指向最後小孩節點 break; } helper = helper.getNext(); } // 小孩報數前,先讓 first 和 helper 移動 k - 1次 for (int j = 0; j < startNo - 1; j++) { first = first.getNext(); helper = helper.getNext(); } // 當小孩報數時,讓first 和 helper 指針同時 的移動 m - 1 次, 然後出圈 // 這裡是一個循環操作,知道圈中只有一個節點 while (true) { if (helper == first) { // 說明圈中只有一個節點 break; } // 讓 first 和 helper 指針同時 的移動 countNum - 1 for (int j = 0; j < countNum - 1; j++) { first = first.getNext(); helper = helper.getNext(); } // 這時first指向的節點,就是要出圈的小孩節點 System.out.printf("小孩%d出圈\n", first.getNo()); // 這時將first指向的小孩節點出圈 first = first.getNext(); helper.setNext(first); // } System.out.printf("最後留在圈中的小孩編號%d \n", first.getNo()); } } // 創建一個Boy類,表示一個節點 class Boy { private int no;// 編號 private Boy next; // 指向下一個節點,默認null public Boy(int no) { this.no = no; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public Boy getNext() { return next; } public void setNext(Boy next) { this.next = next; } }

