4.1 鏈表(Linked List)
文章目錄
鏈表是有序的列表,但是它的內容中的存儲如下:
小結:
1)鏈表是以節點的方式來存儲,是鏈式存儲。
2)每個節點包含 data 域,next 域:指向下一個節點。
3)鏈表的各個節點不一定是連續存儲的。
4)鏈表分帶頭節點的鏈表和沒有頭節點的鏈表,根據實際的需求來確定
單鏈表(帶頭節點)邏輯結構示意圖:
4.2 單鏈表的應用實例
使用帶頭節點的單向鏈表實現—水滸英雄排名管理。
- 完成對英雄人物的增刪改查操作。
2)第一種方法在添加英雄時,直接添加到鏈表的尾部。
class HeroNode{
int no;
String name;
String nickName;
HeroNode next;
}
添加(創建)
1.先創建一個head頭節點,作用就是表示單鏈表的頭
2.後面我們每添加一個節點,就直接加入到鏈表的最後。
3.通過一個輔助變量變量,幫助遍歷整個鏈表。
3)第二種方式在添加英雄時,根據排名將英雄插入到指定位置。
需要按照編號的順序添加:
1.首先找到新添加的節點要添的位置,是通過輔助變量(相當於指針)通過遍歷搞定。
2.新的節點.next = temp.next
3.將temp.next = 新的節點
3)修改節點功能
思路(1)先找到該節點,通過遍歷。
(2)temp.name = newHero.name;
temp.nickname = newHero.nickname
4) 刪除節點:
從單鏈表中刪除一個節點的思路:
1.我們先找到需要刪除的這個節點的前一個節點temp
2.temp.next = temp.next.next
3.被刪除的節點,將不會有其它引用指向,會被垃圾回收機制回收。
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(hero3);
singleLinkedList.add(hero2);
//顯示一把
singleLinkedList.list();
//測試修改節點的代碼
HeroNode newHeroNode = new HeroNode(2,"小盧","玉麒麟");
singleLinkedList.update(newHeroNode);
System.out.println("修改後的鏈表情況~~");
singleLinkedList.list();
//刪除一個節點
singleLinkedList.del(1);
System.out.println("刪除後的鏈表情況~~");
singleLinkedList.list();
}
}
//定義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){
break;
}
if(temp.next.no > heroNode.no){
//位置找到,就在temp的後面插入
break;
}else if(temp.next.no == heroNode.no){
//說明希望添加的heroNode的編號已然存在
flag = true;
break;
}
temp = temp.next;
}
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編號來修改,即no編號不能改.
//說明
//1. 根據 newHeroNode 的 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;
}
//判斷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.next;
}
}
}
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 + '\'' +
'}';
}
}
4.2.1.求單鏈表中有效節點的個數
// 1.求單鏈表中有效節點的個數
/**
*
* @param head
* @return 返回的就是有效節點的個數
*/
public static int getLength(HeroNode head){
if (head.next == null){
return 0;
}
int lenght = 0;
//定義一個輔助的變量, 這裏我們沒有統計頭節點
HeroNode cur = head.next;
while (cur != null){
lenght++;
cur = cur.next;
}
return lenght;
}
4.2.2. 查找單鏈表中的倒數第k個結點
//2.查找單鏈表中的倒數第k個結點 【新浪面試題】
/*
思路:
1.編寫一個方法,接收head節點,同時接收一個indx
2.index 表示是倒數第index第x個節點
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;
for(int i = 0;i < size-index;i++){
cur = cur.next;
}
return cur;
}
4.2.3.單鏈表的反轉
// 3.單鏈表的反轉【騰訊面試題,有點難度】
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;
}
4.2.4從尾到頭打印單鏈表
//方式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.add(cur);
cur = cur.next;
}
//打印
while (stack.size() >0){
System.out.println(stack.pop());
}
}
4.3 雙向鏈表應用實例
1.單向鏈表,查找的方向只能是一個方向,而雙向鏈表可以向前或者向後查找。
2.單向鏈表不能自我刪除,需要靠輔助節點 ,而雙向鏈表,則可以自我刪除,所以前面我們單鏈表刪除時節點,總是找到temp,temp是待刪除節點的前一個節點(認真體會).
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("k");
return;
}
HeroNode2 temp = head.next;
while (true){
if(temp == null){
break;
}
System.out.println(temp);
temp = temp.next;
}
}
// 添加一個節點到雙向鏈表的最後.
public void add(HeroNode2 heroNode2){
HeroNode2 temp = head;
while (true){
if(temp.next == null){
break;
}
temp = temp.next;
}
temp.next = heroNode2;
heroNode2.pre = temp;
}
// 修改一個節點的內容, 可以看到雙向鏈表的節點內容修改和單向鏈表一樣
// 只是 節點類型改成 HeroNode2
public void update(HeroNode2 newheroNode2){
if(head.next == null){
System.out.println("空");
return;
}
HeroNode2 temp = head.next;
boolean flag = false;
while (true){
if(temp == null){
break;
}
if(temp.no == newheroNode2.no){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
temp.name = newheroNode2.name;
temp.nickname = newheroNode2.nickname;
}else{
System.out.printf("沒有找到 編號 %d 的節點,不能修改\n", newheroNode2.no);
}
}
// 從雙向鏈表中刪除一個節點,
// 說明
// 1 對於雙向鏈表,我們可以直接找到要刪除的這個節點
// 2 找到後,自我刪除即可
public void del(int no){
if(head.next == null){
System.out.println("K");
return;
}
HeroNode2 temp = head.next;
boolean flag = false;
while (true){
if(temp == null){
break;
}
if(temp.no == no){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
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;
}
@Override
public String toString() {
return "HeroNode2{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
4.3 單向環形鏈表應用場景
Josephu(約瑟夫、約瑟夫環) 問題
Josephu 問題爲:設編號爲1,2,… n的n個人圍坐一圈,約定編號爲k(1<=k<=n)的人從1開始報數,數到m 的那個人出列,它的下一位又從1開始報數,數到m的那個人又出列,依次類推,直到所有人出列爲止,由此產生一個出隊編號的序列。
提示:用一個不帶頭結點的循環鏈表來處理Josephu 問題:先構成一個有n個結點的單循環鏈表,然後由k結點起從1開始計數,計到m時,對應結點從鏈表中刪除,然後再從被刪除結點的下一個結點又從1開始計數,直到最後一個結點從鏈表中刪除算法結束。
構建一個單向的環形鏈表思路:
1.先創建第一個節點,讓first指向該節點,並形成環
2.後面當我們每創建一個新的節點,把該節點,加入到已有的環形鏈表中即可。
遍歷環形鏈表
1.先讓一個輔助指針(變量)curBoy,指向first節點
2.然後通過一個while循環遍歷該環形鏈表即可,curBoy.next == first。結束。
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
}
}
// 創建一個環形的單向鏈表
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;
}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){
break;
}
helper = helper.getNext();
//小孩報數前,先讓 first 和 helper 移動 startNo - 1次
for (int i = 0; i <startNo - 1 ; i++) {
first = first.getNext();
helper = helper.getNext();
}
//當小孩報數時,讓first 和 helper 指針同時 的移動 m - 1 次, 然後出圈
//這裏是一個循環操作,知道圈中只有一個節點
while (true){
if(helper == first){
//說明圈中只有一個節點
break;
}
//讓 first 和 helper 指針同時 的移動 countNum - 1
for (int i = 0; i <countNum - 1 ; i++) {
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 Boy getNext() {
return next;
}
public void setNo(int no) {
this.no = no;
}
public void setNext(Boy next) {
this.next = next;
}
}