數據結構和算法-單鏈表
1. 鏈表介紹
??鏈表是有序的列表,但是它在內存中是存儲如下
小結:
- 鏈表是以節點的方式存儲
- 每個節點包含data域,next域,指向下一個節點。
- 如圖:發現鏈表的各個節點不一定是連續存儲。比如地址為150的節點下一個節點的地址不為160,而是指向地址為110的節點。
- 鏈表分帶頭節點的鏈表和沒有頭節點的鏈表,根據實際的需求來確定。
單鏈表(帶頭結點)邏輯結構示意圖如下
注意:圖2中邏輯結構的連接并不是按照地址的順序畫的,而是按照每個節點的next域的內存地址連接的。真實的內存存儲世圖1所示
2. 單鏈表的應用實例
使用帶head頭的單向鏈表實現-水滸英雄排行榜管理
- 完成對英雄人物的增刪改查操作,注:刪除和修改,查找。
- 第一種方法在添加英雄時,直接添加到鏈表尾部
- 第二種方法在添加英雄時,根據排名將英雄插入到指定位置(如果有這個排名,則添加失敗,并給出提示)
思路:使用一個類來代表一個節點 其中類中的屬性來表示節點中的內容
- 添加(創建)
- 先創建一個head節點,作用就是表示單鏈表的頭
- 后面每添加一個節點,就直接加入到鏈表的最后
- 遍歷
- 通過一個輔助變量遍歷,幫助遍歷整個鏈表
class HeroNode{int no;String name;String nickName;HeroNode next;
}
2.1 直接將節點添加到鏈表尾部
實現添加節點時,直接添加到鏈表尾部
public class SingleLinkedListDemo {public static void main(String[] args) {//進行測試//先創建節點HeroNode heroNode1 = new HeroNode(1, "松江", "及時雨");HeroNode heroNode2 = new HeroNode(2, "盧俊義", "玉麒麟");HeroNode heroNode3 = new HeroNode(3, "吳用", "智多星");HeroNode heroNode4 = new HeroNode(4, "林沖", "豹子頭");//創建一個列表SingleLinkedList singleLinkedList = new SingleLinkedList();//加入singleLinkedList.add(heroNode1);singleLinkedList.add(heroNode2);singleLinkedList.add(heroNode3);singleLinkedList.add(heroNode4);//顯示singleLinkedList.list();}
}//定義SingleLinkedList管理節點
class SingleLinkedList {//先初始化一個頭節點 頭節點不要動 不存放具體的數據private HeroNode head = new HeroNode(0, " ", " ");//添加節點到單向鏈表//思路,當不考慮編號順序時//1. 找到當前鏈表的最后節點//2. 將最后這個節點的next 指向 新的節點public void add(HeroNode heroNode) {//因為head節點不能動 因此需要一個輔助變量tempHeroNode temp = head;//遍歷鏈表 找到最后while (true) {//鏈表的最后一個節點的next為nullif (temp.next == null) {//說明此時temp就指向了鏈表的最后temp.next = heroNode;break;} else {temp = temp.next;//將此節點的下一個節點作為新循環的判斷(后移一個節點判斷)}}}//顯示鏈表[遍歷]public void list() {//判斷鏈表是否為空if (head.next == null) {System.out.println("鏈表為空");return;}//因為head節點不能動 因此需要一個輔助變量temp來遍歷HeroNode temp = head.next;while (true) {//輸出節點信息System.out.println(temp);//判斷是否到鏈表最后if (temp.next == null) {//說明這個節點是最后一個節點break;}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;}@Overridepublic String toString() {return "HeroNode{" +"no=" + no +", name='" + name + '\'' +", nickname='" + nickname + '\'' +", next=" + (next == null ? null : next.hashCode()) +'}';}
}
2.2 按順序添加節點
實現添加節點時,根據排名將英雄插入到指定位置(如果有這個排名,則添加失敗,并給出提示)
需要按照編號的順序添加節點 思路:
- 首先找到新添加的節點的位置,是通過輔助變量(指針),通過遍歷來搞定
- 新的節點.next = temp.next
- 將 temp.next = 新的節點
自己寫代碼如下,重寫了一下類
//定義SingleLinkedList2按順序管理節點
class SingleLinkedList2 {//先初始化一個頭節點 頭節點不要動 不存放具體的數據private HeroNode head = new HeroNode(0, " ", " ");//按順序添加節點到單向鏈表public void add(HeroNode heroNode) {if (head.next == null) {//說明此時空鏈表head.next = heroNode;//直接將此節點添加到頭節點的后面return;}HeroNode temp = head;do {if (heroNode.no < temp.next.no) {//將此節點插入到temp和temp.next中間
// HeroNode temp2 = temp.next;//保存temp的下一個節點
// temp.next = heroNode; //將temp下一行節點指向heroNode
// heroNode.next = temp2; //將heroNode的下一個節點指向到原temp.nextheroNode.next = temp.next; //將heroNode的下一個節點指向到temp.nexttemp.next = heroNode; //將temp下一行節點指向heroNodereturn; //直接結束戰斗} else if (heroNode.no == temp.next.no) {System.out.println("已經當有相同節點了 添加失敗 請自重");return;}temp = temp.next; //節點后移} while (temp.next != null);//如果正常退出 說明此對象的序號值最大 直接放在最后面temp.next = heroNode;}//顯示鏈表[遍歷]public void list() {if (head.next == null) {System.out.println("別鬧寶 空鏈表~~");return;}HeroNode temp = head;while (temp.next != null) {//如果下一個節點不為空(存了數據)System.out.println(temp.next); //輸出下一個節點temp = temp.next; //后移到下一個節點}}
}
韓老師教的如下 在SingleLinkedList類中新添加了一個按順序排列的函數
//第二種方式在添加英雄時 根據排名將英雄插入到指定位置
//(如果有這個排名 則添加失敗 并給出提示)
public void addByOrder(HeroNode heroNode) {//因為頭節點不能動 因此我們仍然通過一個輔助指針(變量)來幫助找到添加的位置//因為是單鏈表 找的temp是位于添加位置的前一個節點 否則插入不了HeroNode temp = head;boolean flag = false; //標志添加的編號是否存在 默認為falsewhile (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.next == null還是temp.next.no > heroNode.no 都需要把heroNode放在temp的后面//加入到鏈表中 temp的后面heroNode.next = temp.next;temp.next = heroNode;}
}
2.3 單鏈表節點的修改(根據no編號來修改)
自己寫的如下 在類中增加了一個修改函數
//修改節點的信息 根據no編號來修改 即no編號不能改
public void modify(int no, String name,String nickname) {if (head.next == null) {//說明此時空鏈表System.out.println("空鏈表 修改失敗 改不了");return;}HeroNode temp = head;while (temp.next != null){ //遍歷if(no == temp.next.no){ //如果編號相同temp.next.name = name;temp.next.nickname = nickname;return;//結束函數}temp = temp.next;}//如果正常退出循環 則說明沒找到對應的編號System.out.println("不好意思,沒找到對應編號的位置");
}
韓老師寫的如下
//修改節點的信息,根據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.println("修改失敗 沒有找到");}
}
2.4 單鏈表節點的刪除
自己寫的代碼如下
//節點的刪除 根據no編號來刪除
public void delete(int no){if (head.next == null) {//說明此時空鏈表System.out.println("空鏈表 刪除失敗 請自重寶~~");return;}HeroNode temp = head;while (temp.next != null){if(temp.next.no == no){temp.next = temp.next.next; //將temp指向后移兩位return;//結束函數}temp = temp.next;//后移一位}//正常退出while 則說明沒找到對應的編號System.out.println("沒找到對應的編號");
}
韓老師思路如下
- 從單鏈表中刪除一個節點的思路
- temp.next = temp.next.next
- 被刪除的節點 將不會有其它引用 會被垃圾回收機制回收
代碼如下
//刪除節點
//思路
//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){//找到待刪除節點的前一個節點tempflag = true;break;}temp = temp.next; //temp后移 遍歷}if(flag){//找到//可以刪除temp.next = temp.next.next;}else {System.out.println("要刪除的節點不存在");}
}
3. 單鏈表面試題
3.1 求單鏈表中節點的個數(不統計頭節點)
其中就是遍歷一遍即可
3.2 查找單鏈表中的倒數第k個節點
//查找單鏈表中倒數第k個節點
//思路
//1. 編寫一個方法 接收head節點 同時接收一個index
//2. index 表示是倒數第index個節點
//3. 先把鏈表從頭到尾遍歷,得到鏈表的總長度
//4. 得到size后,從鏈表的第一個開始遍歷(size - index)個 就可以得到
//5. 找到則返回該節點 否則返回null
public static HeroNode findLastIndexNode(HeroNode head, int index) {//判斷如果鏈表為空 返回nullif (head.next == null) {return null; //沒有找到}//第一次遍歷得到鏈表的長度int size = getLength(head);//第二次遍歷 size - index + 1位置 就是倒數的第k個節點//先做一個index的校驗if (index <= 0 || index > size) {return null;}//定義一個輔助變量HeroNode temp = head.next;for (int i = 0; i < size - index; i++) {temp = temp.next;}return temp;
}
3.3 單鏈表的反轉
按照自己寫的如下 思路就是每一次分別找到原鏈表中第size - i 個節點即新鏈表中第i個節點(包含頭節點),然后進行連接
public static void reverse(HeroNode head) {if (head.next == null) {//判斷如果鏈表為空return; //沒有找到}//第一次遍歷得到鏈表的長度int size = getLength(head);HeroNode temp1 = null;HeroNode temp2 = null;HeroNode temp3 = head.next; //保存好原鏈表的第一個節點for (int i = 0; i < size; i++) {temp1 = temp3;for (int j = 0; j < size - i - 1; j++) { //每個大循環依次從原鏈表中得到最后一個到第一個節點temp1 = temp1.next;}
// System.out.println(temp1);temp2 = head;for (int j = 0; j < i; j++) { //每個大循環依次從新鏈表得到頭節點到最后第二個節點temp2 = temp2.next;}
// System.out.println(i + " " + temp1 + " " + temp2);temp2.next = temp1; //重新將節點連接起來}temp3.next = null; //切記 別忘了把新鏈表的最后一個(原鏈表的第一個)的next置null}
韓老師的思路:
- 先定義一個節點 reverseHead = new HeroNode();
- 從頭到尾遍歷原來的鏈表,每遍歷一個節點,就將其取出,并放在新的鏈表reverseHead的最前端
- 原來的鏈表的head.next=reverseHead.next;
//將單鏈表反轉
public static void reverseList(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; //先讓此節點的next域指向新鏈表的首位reverseHead.next = cur; //再將新鏈表的頭部指向此節點 實現cur節點插入cur = next; //再將該cur引用重新指向原鏈表沒有進行操作的節點}//將head.next 指向reverseHead.next 實現單鏈表的反轉head.next = reverseHead.next;
}
不得不說啊,這韓老師就是牛啊,短短幾行代碼就搞定了!!!其實就類似于插入,將原鏈表的節點按順序插入到新鏈表的頭部與第一個節點之間,此節點就作為新鏈表的第一個節點。那么最后原鏈表中的第i個節點就依次排到新鏈表中的size-i個節點中了。
3.4 從尾到頭打印單鏈表
【方式一:反向遍歷 方式二:Stack棧】
3.4.1 逆序輸出 反向遍歷
韓老師沒演示這種 自己寫的如下
//遍歷逆序輸出
public static void reverseOutput(HeroNode head){if (head.next == null) {//判斷如果鏈表為空return; //沒有找到}//第一次遍歷得到鏈表的長度int size = getLength(head);HeroNode temp1 = null;for (int i = 0; i < size; i++) {temp1 = head.next;for (int j = 0; j < size - i - 1; j++) { //每個大循環依次從原鏈表中得到最后一個到第一個節點temp1 = temp1.next;}System.out.println(temp1);}
}
//遞歸逆序輸出
public static void reverseOutputDiGui(HeroNode head){if(head.next==null){//說明到頭了}else {reverseOutputDiGui(head.next);}if(head.no != 0){System.out.println(head);}
}
3.4.2 Stack棧
韓老師思路如下
-
可以利用棧這個數據結構,將各個節點壓入到棧中 然后利用棧的先進后出的特點 就實現了逆序打印的效果
/*** @author 小小低頭哥* @version 1.0* 演示Stack的使用*/ public class TestStack {public static void main(String[] args) {Stack<String> stack = new Stack<>();//入棧stack.add("jack");stack.add("tom");stack.add("smith");//出棧while (stack.size() > 0){System.out.println(stack.pop());//POP就是將棧頂的數據取出}} }
//逆序輸出 //利用棧這個數據結構,將各個節點壓入到棧中 然后利用棧的先進后出的特點 就實現了逆序打印的效果 public static void reversePrint(HeroNode head){if (head.next == null) {//判斷如果鏈表為空return; //空鏈表 不能打印}//創建一個棧 將各個節點壓入棧Stack<HeroNode> stack = new Stack<>();HeroNode cur = head.next;//將鏈表的所有節點壓入棧while (cur != null){stack.push(cur); //入棧cur = cur.next; //后移}//將戰中節點打印while (stack.size() > 0){System.out.println(stack.pop()); //出棧 先進后出} }
3.5 合并兩個有序的單鏈表,合并之后的鏈表依然有序
自己苦戰出來的,試過輸入沒毛病的情況下 ,結果還可以
思路:
以鏈表一為基準,遍歷鏈表一的每一個節點。在遍歷每個鏈表一的節點期間,將鏈表二中節點編號依次與此時遍歷的鏈表一中 的節點編號進行比較,滿足條件的則接在新鏈表的最后一個節點上。由于是鏈表一二都是順序排列,所以一旦鏈表二中節點編號有不滿足條件的,則將此時鏈表一中遍歷的節點接在新鏈表的最后一個節點上,并跳出遍歷鏈表二的循環,繼續遍歷鏈表一中的下一個節點,再接著上次鏈表二中第一個不滿足的節點進行比較(注意不是重新與鏈表二中的節點一個個比較)。以此循環。
/*** 合并兩個有序鏈表* 但是要確保輸入的兩個鏈表都是從小到大排序的 否則需要if(temp2.no <= temp1.no)中的 <= 換成 >=* @param head1* @param head2* @return 合并后的鏈表*/
public static HeroNode mergeList(HeroNode head1, HeroNode head2) {HeroNode newHero = new HeroNode(0," "," ");//新建一個節點HeroNode end = newHero; //保存newHero最后一個節點HeroNode temp1 = head1.next; //鏈表一指針HeroNode temp2 = head2.next; //鏈表二指針HeroNode next = null; //保存下一個節點while (true){//以鏈表一進行順序遍歷while (true){//當鏈表二中有節點的no 小于鏈表一中的當前節點的no時if(temp2.no <= temp1.no){ //從小到大排序next = temp2.next;end.next = temp2;//將temp2插入到newHero最后end = end.next; //將end后移一個temp2 = next; //將temp2指向鏈表二的下一個節點繼續與temp1比較}else {next = temp1.next;end.next = temp1;//將temp1插入到newHero最后end = end.next; //將end后移一個if(next == null){ //說明temp1比較完了 直接把temp2插入到newHero最后end.next = temp2;return newHero;}temp1 = next; //沒比較完 則temp1后移break; //結束鏈表二中的節點與鏈表一中temp1節點的比較(因為是有序的 所以鏈表二后面的肯定也不符合條件)}if(temp2 == null){//說明鏈表二中的節點都比較完畢 那么直接把鏈表一中剩下的節點temp1連接上新鏈表即可end.next = temp1;return newHero;}}}
}