4.1 链表(Linked List)介绍
链表是有序的列表,但是它在内存中的存储结构如下:
小结上图:
(1)链表是以节点的方式来存储,是链式存储
(2)每个节点包含 data 域, next 域:指向下一个节点.
(3)如图:发现链表的各个节点不一定是连续存储.
(4)链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
单链表(带头结点) 逻辑结构示意图如下:
4.2 单链表的应用实例
使用带head头的单向链表实现 –水浒英雄排行榜管理
完成对英雄人物的增删改查操作。
4.2.1 添加(方法1)
第一种方法在添加英雄时,直接添加到链表的尾部
思路分析:
代码实现:
package com.xjs.linkedlist;
/**
* @Author: 谢家升
* @Date: 2022/2/8-02-08-14:59
* @Version: 1.0
*/
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();
//加入
System.out.println("========添加方法1============");
singleLinkedList.add(hero1);
singleLinkedList.add(hero4);
singleLinkedList.add(hero2);
singleLinkedList.add(hero3);
//输出
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 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;
}
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickName + "]";
}
}
4.2.2 添加(方法2)
第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
package com.xjs.linkedlist;
/**
* @Author: 谢家升
* @Date: 2022/2/8-02-08-14:59
* @Version: 1.0
*/
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();
//加入
//System.out.println("========添加方法1============");
//singleLinkedList.add(hero1);
//singleLinkedList.add(hero4);
//singleLinkedList.add(hero2);
//singleLinkedList.add(hero3);
//加入按照编号顺序
System.out.println("========添加方法2============");
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero3);//测试重复添加
//输出
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;//初始化为false
//借助辅助变量temp,来进行循环遍历链表,找到合适插入的位置
while (true) {
if (temp.next == null) {//表示temp已经到达链表的最后了
break;
}
if (temp.next.no == heroNode.no) {
//表示待插入的英雄的编号在此链表中已然存在,不能再加入
flag = true;//说明编号存在
} else if (temp.next.no > heroNode.no){//位置找到,就在temp的后面插入
break;
}
//temp后移 ,遍历当前链表
temp = temp.next;
}
//对flag进行判断
if (flag) {
//说明待插入的英雄的编号在此链表中已然存在,不能再加入
System.out.printf("待加入的英雄编号%d在链表中已经存在,不能再加入了\n",heroNode.no);
}else {
//已经找到应该插入的位置,并且待插入的英雄编号不在链表中,执行添加操作
//插入到链表中, temp的后面
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//显示链表[遍历]
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;
}
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickName + "]";
}
}
4.2.3 修改节点
思路分析:
(1)先找到该节点,通过遍历
(2)temp.name = heroNode.name;
(3)temp.nickName = heroNode.nickName;
package com.xjs.linkedlist;
/**
* @Author: 谢家升
* @Date: 2022/2/8-02-08-14:59
* @Version: 1.0
*/
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();
//加入
//System.out.println("========添加方法1============");
//singleLinkedList.add(hero1);
//singleLinkedList.add(hero4);
//singleLinkedList.add(hero2);
//singleLinkedList.add(hero3);
//加入按照编号顺序
System.out.println("========添加方法2============");
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 管理我们的英雄
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;//初始化为false
//借助辅助变量temp,来进行循环遍历链表,找到合适插入的位置
while (true) {
if (temp.next == null) {//表示temp已经到达链表的最后了
break;
}
if (temp.next.no == heroNode.no) {
//表示待插入的英雄的编号在此链表中已然存在,不能再加入
flag = true;//说明编号存在
} else if (temp.next.no > heroNode.no){//位置找到,就在temp的后面插入
break;
}
//temp后移 ,遍历当前链表
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 heroNode) {
//判断是否空
if(head.next == null) {
System.out.println("链表为空~");
return;
}
//找到需要修改的节点, 根据no编号
//先定义一个变量,标识链表中是否存在要修改的英雄的编号
boolean flag = false;//表示是否找到该节点
//还是借助temp 变量来遍历链表,找到要修改的英雄
HeroNode temp = head.next;
while (true) {
if (temp.no == heroNode.no) {//找到了待修改的英雄
flag = true;//表示找到了
break;
}
if (temp.next == null) {//链表遍历完了
break;
}
//temp后移
temp = temp.next;
}
//对flag进行判断
if (flag) {
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
} else {//没有找到
System.out.printf("没有找到英雄编号%d,无法修改\n",heroNode.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;
}
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickName + "]";
}
}
4.2.4 删除节点
思路分析:
代码实现:
package com.xjs.linkedlist;
/**
* @Author: 谢家升
* @Date: 2022/2/8-02-08-14:59
* @Version: 1.0
*/
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();
//加入
//System.out.println("========添加方法1============");
//singleLinkedList.add(hero1);
//singleLinkedList.add(hero4);
//singleLinkedList.add(hero2);
//singleLinkedList.add(hero3);
//加入按照编号顺序
System.out.println("========添加方法2============");
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.delete(1);
singleLinkedList.delete(4);
singleLinkedList.delete(2);
singleLinkedList.delete(3);
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;//初始化为false
//借助辅助变量temp,来进行循环遍历链表,找到合适插入的位置
while (true) {
if (temp.next == null) {//表示temp已经到达链表的最后了
break;
}
if (temp.next.no == heroNode.no) {
//表示待插入的英雄的编号在此链表中已然存在,不能再加入
flag = true;//说明编号存在
} else if (temp.next.no > heroNode.no){//位置找到,就在temp的后面插入
break;
}
//temp后移 ,遍历当前链表
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 heroNode) {
//判断是否空
if(head.next == null) {
System.out.println("链表为空~");
return;
}
//找到需要修改的节点, 根据no编号
//先定义一个变量,标识链表中是否存在要修改的英雄的编号
boolean flag = false;//表示是否找到该节点
//还是借助temp 变量来遍历链表,找到要修改的英雄
HeroNode temp = head.next;
while (true) {
if (temp.no == heroNode.no) {//找到了待修改的英雄
flag = true;//表示找到了
break;
}
if (temp.next == null) {//链表遍历完了
break;
}
//temp后移
temp = temp.next;
}
//对flag进行判断
if (flag) {
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
} else {//没有找到
System.out.printf("没有找到英雄编号%d,无法修改\n",heroNode.no);
}
}
//删除节点
//思路
//1. head 不能动,因此我们需要一个temp辅助节点找到待删除节点的前一个节点
//2. 说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较
public void delete(int no) {
//判断链表是否为null
if (head.next == null) {
System.out.println("链表为空,无法删除~");
return;
}
//定义一个变量,判断是否找到待删除的节点
boolean flag = false;
//借助临时变量来遍历链表,找到待删除节点的前一个节点temp
HeroNode temp = head;
while (true) {
if (temp.next == null) {//链表遍历结束
break;
}
if (temp.next.no == no) {//找到了
flag = true;
break;
}
temp = temp.next;
}
//对flag进行判断
if (flag) {//找到了,直接删除
temp.next = temp.next.next;
}else {
System.out.printf("没有找到英雄编号%d, 无法删除...",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;
}
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickName + "]";
}
}
4.3 单链表面试题(新浪、百度、腾讯)
单链表的常见面试题有如下:
(1)求单链表中有效节点的个数
/**
* 该方法:获取到单链表的节点的个数(如果是带头结点的链表,需求不统计头节点)
* @param head 链表的头节点
* @return 返回的就是有效节点的个数
*/
public static int getLength(HeroNode head) {
//判断链表是否为null
if (head.next == null) {//链表为空
return 0;
}
//借助赋值遍历进行遍历链表,这里我们没有统计头节点
HeroNode cur = head.next;
//定义一个变量,记录有效节点个数
int length = 0;
while (cur != null) {
length++;
cur = cur.next;//遍历
}
return length;
}
(2)查找单链表中的倒数第 K 个节点【新浪面试题】
//查找单链表中的倒数第k个结点 【新浪面试题】
//思路
//1. 编写一个方法,接收head节点,同时接收一个index
//2. index 表示是倒数第index个节点
//3. 先把链表从头到尾遍历,得到链表的总的长度 getLength
//4. 得到size 后,我们从链表的第一个开始遍历 (size-index)个,就可以得到
//5. 如果找到了,则返回该节点,否则返回null
public static HeroNode getLastIndexNode(HeroNode head, int index) {
//判断链表是否为null
if (head.next == null) {
return null;
}
//第一次遍历,得到链表的有效节点个数size
int size = getLength(head);
//第二次遍历 size-index 位置,就是我们倒数的第K个节点
//要先对index进行校验
if (index <= 0 || index > size) {
return null;
}
//再次遍历,通过for循环来控制
//遍历需要一个赋值变量
HeroNode cur = head.next;
for (int i = 0; i < size - index; i++) {
cur = cur.next;
}
return cur;
}
(3)单链表的反转【腾讯面试题】
韩老师视频讲解: 单链表的反转.
//将单链表反转
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, "", "");
//进行循环遍历,同时将遍历的每一个节点都依次接到新的头节点reverseListHead的后面
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)从尾到头打印单链表【百度面试题】
思路分析:
简单演示栈的使用:
package com.xjs.linkedlist;
import java.util.Stack;
/**
* @Author: 谢家升
* @Date: 2022/2/9-02-09-17:19
* @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");
//出栈
//smith, tom, jack
while (stack.size() > 0) {
System.out.println(stack.pop());//pop就是将栈顶的数据取出
}
}
}
代码实现:
//从尾到头打印单链表
//可以利用栈这个数据结构,将各个节点压入到栈中,
//然后利用栈的先进后出的特点,就实现了逆序打印的效果
public static void reversePrint(HeroNode head) {
if (head.next == null) {
return;//空链表,不能打印
}
HeroNode cur = head.next;
//创建要给一个栈,将各个节点压入栈
Stack<HeroNode> stack = new Stack<>();
//将链表的所有节点压入栈
while (cur != null) {
stack.push(cur);
cur = cur.next;//cur后移,这样就可以压入下一个节点
}
//将栈中的节点进行打印,pop 出栈
while (stack.size() > 0) {
System.out.println(stack.pop());//stack的特点是先进后出
}
}
(5)合并两个有序的单链表,合并之后的链表依然有序
4.4 双向链表应用实例
4.4.1 双向链表的操作分析和实现
使用带 head 头的双向链表实现 — 水浒英雄排行榜
-
管理单项链表的缺点分析:
(1)单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
(2)单项链表不能自我删除,需要靠辅助节点,而双向链表则可以自我删除,所以前面我们单链表删除节点时,总是需要借助辅助变量 temp ,temp 是指待删除节点的前一个节点(认真体会)
(3)分析双向链表如何完成遍历,添加,修改和删除的思路。
-
代码实现:
package com.xjs.linkedlist;
import javax.lang.model.element.VariableElement;
/**
* @Author: 谢家升
* @Date: 2022/2/9-02-09-20:53
* @Version: 1.0
*/
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.delete(3);
System.out.println("删除后的链表情况~~");
doubleLinkedList.list();
}
}
// 创建一个双向链表的类,一个节点就是一个HeroNode2
class DoubleLinkedList {
// 先初始化一个头节点, 头节点不要动, 不存放具体的数据
private HeroNode2 head2 = new HeroNode2(0, "", "");
//获取头节点
public HeroNode2 getHead2() {
return head2;
}
//添加节点(默认添加到链表的最后)
public void add(HeroNode2 heroNode2) {
// 因为head节点不能动,因此我们需要一个辅助遍历 temp
HeroNode2 temp = head2;
//通过辅助变量遍历链表,找到链表的最后
while (true) {
if (temp.next == null) {//遍历完了双向链表
break;
}
temp = temp.next;
}
// 当退出while循环时,temp就指向了链表的最后
// 形成一个双向链表
temp.next = heroNode2;
heroNode2.pre = temp;
}
// 从双向链表中删除一个节点
// 说明
// 1 对于双向链表,我们可以直接找到要删除的这个节点
// 2 找到后,自我删除即可
public void delete(int no) {
if (head2.next == null) {
System.out.println("双向链表为空,无法删除...");
return;
}
HeroNode2 temp = head2.next;
boolean flag = false;//判断是否找到待删除的节点
while (true) {
if (temp.no == no) {//找到了
flag = true;
break;
}
if (temp.next == null) {//遍历完链表,没找到
break;
}
temp = temp.next;
}
if (flag) {
temp.pre.next = temp.next;
//注意,这里如果删除的是链表的最后一个元素就会出现空指针异常 temp.next == null
if (temp.next != null) {
temp.next.pre = temp.pre;
}
} else {
System.out.printf("双向链表中没有找到英雄编号%d, 无法删除~\n", no);
}
}
// 修改一个节点的内容, 可以看到双向链表的节点内容修改和单向链表一样
// 只是 节点类型改成 HeroNode2
public void update(HeroNode2 heroNode2) {
// 判断是否空
if (head2.next == null) {
System.out.println("双向链表为空");
return;
}
HeroNode2 temp = head2.next;
boolean flag = false;//用于判断是否找到了待修改的节点
while (temp != null) {
if (temp.no == heroNode2.no) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.name = heroNode2.name;
temp.nickName = heroNode2.nickName;
} else {
System.out.printf("没有找到英雄编号%d,无法修改\n", heroNode2.no);
}
}
//展示双向链表
public void list() {
// 判断链表是否为空
if (head2.next == null) {
System.out.println("双向链表为空");
return;
}
// 因为头节点,不能动,因此我们需要一个辅助变量来遍历
HeroNode2 temp = head2.next;
while (temp != null) {
System.out.println(temp);
temp = temp.next;//一定要后移,不然就是死循环
}
}
}
//定义一个类 HeroNode2 来管理我们的英雄
class HeroNode2 {
public int no;
public String name;
public String nickName;
public HeroNode2 next;
public HeroNode2 pre;
//构造器
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.4.2 作业和思路提示
双向链表的第二种添加方式,按照编号顺序【示意图】按照单链表的顺序添加,稍作修改即可。
//根据编号顺序添加节点
public void addByOrder(HeroNode2 heroNode2) {
//因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
//我们找的temp 是位于 添加位置的前一个节点
HeroNode2 temp = head2;
//再定义一个变量来标识待插入的英雄的编号是否在链表中已经存在
boolean flag = false;//初始化为false
//借助辅助变量temp,来进行循环遍历链表,找到合适插入的位置
while (true) {
if (temp.next == null) {//表示temp已经到达链表的最后了
break;
}
if (temp.next.no == heroNode2.no) {
//表示待插入的英雄的编号在此链表中已然存在,不能再加入
flag = true;//说明编号存在
} else if (temp.next.no > heroNode2.no) {//位置找到,就在temp的后面插入
break;
}
//temp后移 ,遍历当前链表
temp = temp.next;
}
//对flag进行判断
if (flag) {
//说明待插入的英雄的编号在此链表中已然存在,不能再加入
System.out.printf("待加入的英雄编号%d在链表中已经存在,不能再加入了\n", heroNode2.no);
} else {
//已经找到应该插入的位置,并且待插入的英雄编号不在链表中,执行添加操作
//插入到链表中, temp的后面
heroNode2.next = temp.next;
temp.next = heroNode2;
heroNode2.pre = temp;
if (temp.next != null) {
temp.next.pre = heroNode2;
}
}
}
4.5 单向环形链表应用场景
链接: 韩老师视频链接.
Josephu(约瑟夫、约瑟夫环) 问题
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示:用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单向循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
4.6 单向环形链表介绍
4.7 Josephu 问题
约瑟夫问题示意图:
4.7.1 约瑟夫问题
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
4.7.2 提示
用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单向循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
4.7.3 创建环形链表的思路图解
4.7.4 约瑟夫问题-小孩出圈的思路分析图
4.8 Josephu 问题的代码实现
package com.xjs.linkedlist;
/**
* @Author: 谢家升
* @Date: 2022/2/10-02-10-11:14
* @Version: 1.0
*/
@SuppressWarnings({"all"})
public class Josepfu {
public static void main(String[] args) {
//创建链表
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
//添加数据
circleSingleLinkedList.add(5);
//显示链表节点数据
circleSingleLinkedList.showBoy();
System.out.println("=======Josepfu问题=======");
circleSingleLinkedList.countBoy(1, 2, 5);
}
}
//创建单向环形链表
class CircleSingleLinkedList {
//定义头节点
Boy first = null;
//根据用户输入,计算出小孩出圈的顺序
/**
* @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();
}
//小孩报数前,让 helper 和 first 同时移动 startNo - 1次
for (int i = 0; i < startNo - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//当小孩报数时,让first 和 helper 指针同时 的移动 countNum - 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.getNext();
helper.setNext(first);
}
System.out.printf("圈中最后一个小孩的编号是%d", first.getNo());
}
//添加小孩
public void add(int num) {
//先对传入的 num 做一下校验
if (num < 1) {
System.out.printf("你输入的数字%d没有意义", num);
return;
}
//辅助指针
Boy cur = null;
//进行循环添加小孩
for (int i = 1; i <= num; i++) {
//创建节点
Boy boy = new Boy(i);
//对第一个小孩进行处理
if (i == 1) {
first = boy;
first.setNext(first);
cur = first;
} else {
cur.setNext(boy);
boy.setNext(first);
cur = boy;
}
}
}
//显示链表
public void showBoy() {
//先判断链表是否为空
if (first == null) {
System.out.println("当前链表中没有任何小孩~~~");
return;
}
Boy cur = first;
//这样写是错误的,先判断再输出,最后一个节点没有机会输出
// while (cur.getNext() != first) {
// System.out.printf("小孩的编号 %d \n", cur.getNo());
// cur = cur.getNext();
// }
while (true) {
System.out.printf("小孩的编号 %d \n", cur.getNo());
if (cur.getNext() == first) {
break;
}
cur = cur.getNext();
}
}
}
// 创建一个Boy类,表示一个节点
class Boy {
private int no;
private Boy next;
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;
}
@Override
public String toString() {
return "Boy{" +
"no=" + no +
'}';
}
}