数据结构与算法-004-双向链表
双向链表的介绍:
双向链表的应用实例:
使用带head头的双向链表实现 –水浒英雄排行榜
管理单向链表的缺点分析:
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp是待删除节点的前一个节点.
双向链表对象:
//定义一个HeroNode2,每个HeroNode 对象就是一个节点
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;
}
//为了显示方法,我们重写 toString 方法
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
双向链表对象的方法:
//定义 DoubleLinkedList 管理我们的英雄
class DoubleLinkedList {
//初始化一个头结点(不存放具体数据)
private HeroNode2 head = new HeroNode2(0, "", "");
//返回头结点
public HeroNode2 getHeadDoubleList() {
return head;
}
//删除双向链表节点
/*
1.对于双向链表,我们可以直接找到要删除的这个节点
2.找到后,自我删除即可
*/
public void delDoubleListByOlder(int delNo) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空,无法删除 delDoubleListByOlder() ");
return;
}
HeroNode2 temp = head.next;
boolean flag = false;
while (true) {
//已经到链表最后的节点的next,未找到
if (temp == null) {
break;
}
//找到节点
if (temp.no == delNo) {
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("要删除的节点的 no :%d 不存在 \n", delNo);
}
}
//可以看到双项链表的节点修改几乎和单向链表一样
//修改节点信息,根据no编号来修改,即no编号不能改
public void updateDoubleList(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)
public void addDoubleList1(HeroNode2 heroNode) {
//因为head节点不能动,因此需要一个辅助变量 temp
HeroNode2 temp = head;
//遍历链表,找到最后
while (true) {
//判断若找到链表最后
if (temp.next == null) {
break;
}
if (temp.next == heroNode) { //防止重复添加进入死循环
System.out.println("该元素 " + heroNode.toString() + " 已经添加过了不能重复添加。(1)");
return;
}
//若没找到,则 next 链表后移
temp = temp.next;
}
//当退出循环,则temp指向最后
//将这个空节点指向新的节点
System.out.println("add()新添加的节点为:" + heroNode);
temp.next = heroNode;
//将添加的新节点的 pre 指向原来最后一个节点,构成双项链表
heroNode.pre = temp;
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置
//(如果有这个排名,则添加失败,并给出提示信息)
public void addByOrder(HeroNode2 heroNode) {
//因为头结点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
//因为单链表,因为我们找的的 temp 是位于添加位置的前一个节点,否则插入不了
HeroNode2 temp = head.next;
HeroNode2 LinShi = null;
boolean flag = false; //flag标志添加的编号是否存在,默认为false
//遍历链表,找到最后
while (true) {
//说明temp已经在链表最后一个节点的后一个,在其前面插入
if (temp == null) {
break;
}
//找到位置,在temp前面插入
if (temp.no > heroNode.no) {
break;
} else if (temp.no == heroNode.no) { //说明希望添加的元素已经存在
flag = true;
break;
}
LinShi = temp;
temp = temp.next;
}
//添加元素
if (flag) { //说明希望添加的元素已经存在
System.out.printf("该序号:%d 已经存在,添加失败。(2)\n", temp.next.no);
} else { //插入元素,在temp前面插入
if (temp != null) {
heroNode.pre = temp.pre;
temp.pre.next = heroNode;
temp.pre = heroNode;
heroNode.next = temp;
} else {
LinShi.next = heroNode;
heroNode.pre = LinShi;
}
}
}
//遍历双向链表
//显示链表
//正序遍历
public void showDoubleList1() {
//判断链表是否为空
if (head.next == null) {
System.out.println("双向链表为空。");
return;
}
//因为head节点不能动,因此需要一个辅助变量 temp
System.out.println("showdoublelist()显示的节点为:");
HeroNode2 temp = head.next;
while (true) {
if (temp == null) {
break;
}
System.out.println(temp); //temp为指向下一个节点的 next值 || 值
temp = temp.next;
}
System.out.println();
}
//逆序遍历
public void showDoubleList2() {
//判断链表是否为空
if (head.next == null) {
System.out.println("双向链表为空。");
return;
}
//因为head节点不能动,因此需要一个辅助变量 temp
System.out.println("showdoublelist2()显示的节点为:");
HeroNode2 temp = head.next;
HeroNode2 temp_end = null;
while (true) {
while(true){
//找到最后一个节点
if (temp.next == temp_end) {
System.out.println(temp);
temp_end = temp;
break;
}
temp = temp.next;
}
//遍历到第一个节点
if (temp_end.pre == head) {
break;
}
temp = head.next;
}
System.out.println();
}
}
测试双向链表对象的方法:
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, "林冲", "豹子头");
HeroNode2 hero5 = new HeroNode2(5, "公孙胜", "入云龙");
//创建一个双向链表
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
//添加测试
doubleLinkedList.addDoubleList1(hero2);
doubleLinkedList.addDoubleList1(hero3);
doubleLinkedList.addDoubleList1(hero4);
doubleLinkedList.addDoubleList1(hero5);
//doubleLinkedList.addDoubleList1(hero4);
//显示测试|
/*System.out.println("正序显示双向链表:");
doubleLinkedList.showDoubleList1();*/
//System.out.println("逆序显示双向链表:");
//doubleLinkedList.showDoubleList2();
//修改测试
/*HeroNode2 hero5 = new HeroNode2(4, "公孙胜", "入云龙");
doubleLinkedList.updateDoubleList(hero5);
System.out.println("修改后的链表情况为:");
doubleLinkedList.showDoubleList1();*/
//删除测试
/*doubleLinkedList.delDoubleListByOlder(3);
System.out.println("删除后的先标情况为:");
doubleLinkedList.showDoubleList1();*/
//按序号添加测试
doubleLinkedList.addByOrder(hero1);
System.out.println("按序号添加后的先标情况为:");
doubleLinkedList.showDoubleList1();
//测试逆序遍历
doubleLinkedList.showDoubleList2();
}
}
约瑟夫问题(Josephu):
设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
代码如下:
public class Josepfu {
public static void main(String[] args) {
//测试构建环形链表,和显示
CricleSingleLinkedList cricleSingleLinkedList = new CricleSingleLinkedList();
cricleSingleLinkedList.addBoy(5); //加入5个
cricleSingleLinkedList.showBoy();
cricleSingleLinkedList.countBoy(1,3,5);
}
}
//创建一个环形单向列表
class CricleSingleLinkedList {
private Boy first = new Boy(1);
//添加小孩节点,构成一个环形链表
public void addBoy(int nums) {
//nums 做一个数据校验
if (nums < 1) {
System.out.println("nums的值不正确--->addBoy()");
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("没有任何小孩--->showBoy()");
return;
}
//因为first不能动,因此我们仍然使用一个辅助指针完成遍历
Boy curBoy = first;
//链表传参错误
if (curBoy.getNo() == -1 || curBoy.getNext() == null){
System.out.println("传参错误--->showBoy()");
return;
}
while(true){
System.out.printf("小孩的编号是:%d \n",curBoy.getNo());
if (curBoy.getNext() == first ){ //说明已经遍历完了
break;
}
curBoy = curBoy.getNext(); //cueBoy后移
}
}
//根据用户的输入,计算出小孩的顺序
/**
* @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;
while (true){
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();
}
//当小孩报数时,让 first 和 helper 指针同时移动 m-1 次,然后出圈
//这里是一个循环操作,知道圈中只有一个节点
while(true){
if (helper == first){ //说明圈中只有一个节点
break;
}
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;
public Boy(int no) {
this.no = no;
}
public void setNo(int no) {
this.no = no;
}
public void setNext(Boy next) {
this.next = next;
}
public int getNo() {
return no;
}
public Boy getNext() {
return next;
}
}