024 双向链表增删改查
使用带 head 头的双向链表实现-水浒英雄排行榜,
单向链表的缺点分析
- 单身链表,查找的方向只能是一个方向,而双向链表可以向前或是向后查的
- 单身链表不能自我删除,需要靠辅助节点,而 双向链表,则可以自我删除,所以前面单链表删除节点时,问题找到 temp ,tmep是待删除节点的前一个节点来删除
分析 双向链表的遍历,添加,修改,删除的操作思路
- 遍历方和单链表,只是可以向前,也可以向后查找
- 添加(默认添加到双向链表的最后)
- 先找到双向链表的最后这个节点
- temp.next = newHeroNode
- newHeroNode.pre = temp
- 修改思路和原来的单向链表一样
- 删除
- 因为是双向链表,因此,可以实现自我删除某个节点
- 直接找到要删除的这个节点,比如是 temp,
- temp.pre.next = temp.next
- temp.next.pre = temp.pre
package com.old.linkedList_016_023;
public class DoubleLinkedListDemo {
public static void main(String[] args) {
}
}
/**
* 创建一个双向链表的类
*/
class DoubleLinkedList {
/**
* 初始化头节点,头节点不要动
* 不放具体的数据
*/
private HeroNode2 head = new HeroNode2(0, "", "");
/**
* 返回头节点
*
* @return
*/
public HeroNode2 getHead() {
return head;
}
/**
* 显示链表
*/
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头结点不能动,因此需要一个辅助变量来遍历
HeroNode2 temp = head.next;
while (true) {
System.out.println(temp);
//判断是否到链表最后了
if (temp.next == null) {
break;
}
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 是没有数据,这里就将 temp 后面衔接了起来,
* 再将要添加的节点的前一个指向 temp ,形成了双向链表
* 因为要添加的节点的 pre 是空,他应该指向双向链表中的最后一个
* 而要添加的节点的 next 本来是空,他应该指向下一个要添加的节点
* 所以可以为空
*/
temp.next = heroNode;
temp.next.pre = temp;
}
/**
* 修改节点的信息,根据编号来修改,即 no 编号不能修改,
* no编号修改相添加
* <p>
* 根据 new HeroNode 的 no来修改
* <p>
* 视频代码
* <p>
* 修改一个节点的内容,可以看到双向节点内容修改和单身链表一样
* 只是节点类型进行了修改
*/
public void update(HeroNode2 newHeroNode) {
//判断是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根据 no 编号
//定义一个辅助变量
HeroNode2 temp = head;
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, 还在修改", 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) {
//已经找到链表的最后了, 已经找到了链表节点最后的 next
if (temp == null) {
break;
}
if (temp.no == no) {
//找到待删除节点的前一个节点
flag = true;
break;
}
temp = temp.next;//temp 后移,遍历
}
if (flag) {
//找到,temp 就是要删除的节点
//temp.next = temp.next.next; 这是单向链表的删除方法
temp.pre.next = temp.next;
/**
* 这里代码有问题,假如要删除的节点是最后的节点
* 最后一个节点的 next 是 null,而 null.pre 会出现 空指针异常
* 如果是最后一个节点就不需要执行下面这行代码
* 否则会出现空指针异常
*/
if (temp.next != null) {
temp.next.pre = temp.pre;
}
} else {
System.out.println("要删除的节点不存在:" + no);
}
}
}
/**
* 定义 HeroNode , 每个 HeroNode,对象就是一个节点
*/
class HeroNode2 {
public int no;
public String name;
public String nickname;
/**
* 指向下一个节点
* 默认为 null
*/
public HeroNode2 next;
/**
* 指向前一个节点
* 默认为 null
*/
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 "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
026 双向链表功能测试和小结
package com.old.linkedList_016_023;
/**
* 按照编号顺序添加
*/
public class DoubleLinkedListDemo {
public static void main(String[] args) {
System.out.println("双向链表测试");
HeroNode2 node1 = new HeroNode2(1, "宋江", "及时雨");
HeroNode2 node2 = new HeroNode2(2, "卢俊义", "玉麒麟");
HeroNode2 node3 = new HeroNode2(3, "吴用", "智多星");
HeroNode2 node4 = new HeroNode2(4, "林冲", "豹子头");
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.add(node1);
doubleLinkedList.add(node2);
doubleLinkedList.add(node3);
doubleLinkedList.add(node4);
doubleLinkedList.list();
node3.name = "测试名";
doubleLinkedList.update(node3);
System.out.println("修改后");
doubleLinkedList.list();
doubleLinkedList.del(3);
System.out.println("删除后:");
doubleLinkedList.list();
}
}
/**
* 创建一个双向链表的类
*/
class DoubleLinkedList {
/**
* 初始化头节点,头节点不要动
* 不放具体的数据
*/
private HeroNode2 head = new HeroNode2(0, "", "");
/**
* 返回头节点
*
* @return
*/
public HeroNode2 getHead() {
return head;
}
/**
* 显示链表
*/
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头结点不能动,因此需要一个辅助变量来遍历
HeroNode2 temp = head.next;
while (true) {
System.out.println(temp);
//判断是否到链表最后了
if (temp.next == null) {
break;
}
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 是没有数据,这里就将 temp 后面衔接了起来,
* 再将要添加的节点的前一个指向 temp ,形成了双向链表
* 因为要添加的节点的 pre 是空,他应该指向双向链表中的最后一个
* 而要添加的节点的 next 本来是空,他应该指向下一个要添加的节点
* 所以可以为空
*
* 上面看不懂了
*
* temp 是最后一个节点,这个节点的 next 就应该是要添加的子节点
* 而要添加的节点的 pre 就应该是这个最后的节点
*/
temp.next = heroNode;
// heroNode.pre = temp;两种写法,这种也可以
temp.next.pre = temp;
}
/**
* 修改节点的信息,根据编号来修改,即 no 编号不能修改,
* no编号修改相添加
* <p>
* 根据 new HeroNode 的 no来修改
* <p>
* 视频代码
* <p>
* 修改一个节点的内容,可以看到双向节点内容修改和单身链表一样
* 只是节点类型进行了修改
*/
public void update(HeroNode2 newHeroNode) {
//判断是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根据 no 编号
//定义一个辅助变量
HeroNode2 temp = head;
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, 还在修改", 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) {
//已经找到链表的最后了, 已经找到了链表节点最后的 next
if (temp == null) {
break;
}
if (temp.no == no) {
//找到待删除节点的前一个节点
flag = true;
break;
}
temp = temp.next;//temp 后移,遍历
}
if (flag) {
//temp.next = temp.next.next; 这是单向链表的删除方法
/**
* temp 就是要删除的节点
* 既然 temp 是要删除的节点, 那么它的 上一个节点(pre)
* 中(这里通过 pre 找到了上一个节点,即代表他当前在上一个节点)的 下一个(next,上一个节点的 next)就应该是
* temp 之后的节点
*/
temp.pre.next = temp.next;
/**
* 这里代码有问题,假如要删除的节点是最后的节点
* 最后一个节点的 next 是 null,而 null.pre 会出现 空指针异常
* 如果是最后一个节点就不需要执行下面这行代码
* 否则会出现空指针异常
*
* 这里的意思是:要删除的节点的 下一个节点 (next) 中的 上一个节点(pre)应该是 temp 的 上一个
*/
if (temp.next != null) {
temp.next.pre = temp.pre;
}
} else {
System.out.println("要删除的节点不存在:" + no);
}
}
}
/**
* 定义 HeroNode , 每个 HeroNode,对象就是一个节点
*/
class HeroNode2 {
public int no;
public String name;
public String nickname;
/**
* 指向下一个节点
* 默认为 null
*/
public HeroNode2 next;
/**
* 指向前一个节点
* 默认为 null
*/
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 "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
027 环形链表介绍和约瑟夫问题
josephu 问题为:设编号为1,2,…n的n个人围坐一圈,约定编号为 k (1 <= k <= n)的人从1开始报数,数到 m 的那个人出列,它的下一位又从1开始报数,数到 m 的那个人又出列,直到所有人出殡为止,由此产生出一个出队编号的序列
提示:用一个不带头结点的循环链表来处理 josephu 问题,先构成一个有 n 个结点的单循环链表,然后由 k 结点起从1开始计数,计到 m 时,对应结点从链表中删除, 然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除,算法结束
028-029 约瑟夫问题分析图解和实现1-2
构建一个单向的环形链表思路
- 先创建第一个节点,让 first 指向该节点,并形成环形
- 后面每创建一个新的节点,就把该节点,加入到已有的环形链表中即可
遍历环形链表
- 先让一个辅助指针(变量) curBoy ,指向first 节点
- 然后通过一个 while 循环遍历该环形链表即可 curBoy.next = first 结束
01 写的代码
package com.old.linkedList_016_023;
public class Joseph {
public static void main(String[] args) {
//测试
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5);
circleSingleLinkedList.showBoy();
}
}
/**
* 创建一个环形的单向链表
*/
class CircleSingleLinkedList{
/**
* 创建 first 节点,当前没有编号
*/
private Boy first = null;
/**
* 添加小孩节点,构建成一个环形链表
*
* nums表示要加多少个小孩
*/
public void addBoy(int nums){
if (nums < 1){
System.out.println("nums不能小于1");
return;
}
/**
* 辅助指针,帮助构建环形链表
*/
Boy curBoy = null;
//使用 for 创建环形链表
for (int i =1; i <= nums; i++) {
Boy boy = new Boy(i);
if (i == 1){
first = boy;
//构成环状, 让 curBoy指向第一个小孩
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 = curBoy.getNext();
}
}
}
/**
* 创建一个目标 boy 类,表示一个节点
*/
class Boy{
/**
* 编号
*/
private int no;
/**
* 指向下一个节点,默认为 null
*/
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) {
Next = next;
}
}
02
根据用户的输入,生成一个小孩出圈的顺序
n =5 即有5个人
k =1 即从第一个人开始报数
m =2 即数2下
- 需求创建一个辅助指针(变量)helper ,事先应该指向环形链表的最后这个节点
补充: 小孩报数时,先让 first 和 helper 移动 k -1 次 - 当小孩报数时,让 first 和 helper 指针同时的移动 m - 1 次
- 这里就可以将 first 指向的小孩节点出圈
first = first.next
helper.next = first
原来的 first指向的节点就没有任何引用,就会被回收
package com.old.linkedList_016_023;
public class Joseph {
public static void main(String[] args) {
//测试
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5);
circleSingleLinkedList.showBoy();
circleSingleLinkedList.countBody(1, 2, 5);
}
}
/**
* 创建一个环形的单向链表
*/
class CircleSingleLinkedList{
/**
* 创建 first 节点,当前没有编号
*/
private Boy first = null;
/**
* 添加小孩节点,构建成一个环形链表
*
* nums表示要加多少个小孩
*/
public void addBoy(int nums){
if (nums < 1){
System.out.println("nums不能小于1");
return;
}
/**
* 辅助指针,帮助构建环形链表
*/
Boy curBoy = null;
//使用 for 创建环形链表
for (int i =1; i <= nums; i++) {
Boy boy = new Boy(i);
if (i == 1){
first = boy;
//构成环状, 让 curBoy指向第一个小孩
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 = curBoy.getNext();
}
}
/**
* 根据用户的输入,计算出小孩出圈的顺序
* @param startNo 表示从第几个小孩开始数数
* @param countNum 表示数几下
* @param nums 表示最初有多少个小孩在圈中
*/
public void countBody(int startNo, int countNum, int nums){
//对数据进行校验
if (first == null || startNo < 1 || startNo > nums ){
System.out.println("参数输入有误,请重新输入");
return;
}
//创建辅助指针
Boy helper = first;
while (true){
//说明, helper 指向了最后小孩节点
if (helper.getNext() == first){
break;
}
helper = helper.getNext();
}
//小孩报数前,先让 first 和 helper 移动 k -1次
for (int j = 0; j < startNo -1 ; j++){
first = first.getNext();
helper = helper.getNext();
}
/*
当小孩报数时,让 fist 和 helper 指针同时移动 m -1 次,然后出圈
这里是一个循环操作,直到圈中只有一个节点
*/
while (true){
if (helper == first){
//说明圈中只有一个节点
break;
}
//让 fist 和 helper 指针同时移动 m -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);
}
//当退出 while 节点之后只有一个节点了
System.out.printf("最后留在圈中的小孩编号 %d \n", helper.getNo());
}
}
/**
* 创建一个目标 boy 类,表示一个节点
*/
class Boy{
/**
* 编号
*/
private int no;
/**
* 指向下一个节点,默认为 null
*/
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) {
Next = next;
}
}