第三章 链表
3.1链表(Linked List)简介
链表是有序的列表,但是它在内存中是存储如下
小结:
1)链表是以节点的方式来存储,是链式存储
2)每个节点包含 data 域, next 域:指向下一个节点.
3)如图:发现链表的各个节点不一定是连续存储.
4)链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
单链表(带头结点) 逻辑结构示意图如下
3.2单链表的应用实例
使用带head头的单向链表实现 –水浒英雄排行榜管理
1)完成对英雄人物的增删改查操作, 注: 删除和修改,查找可以考虑学员独立完成,也可带学员完成
2)第一种方法在添加英雄时,直接添加到链表的尾部
3)第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
class HeroNode {
int no
String name;
String nickName
HeroNode next
}
💧 第一种方法:,直接添加到链表的尾部
添加(创建)
- 先创建一个head 头节点, 作用就是表示单链表的头
- 后面我们每添加一个节点,就直接加入到 链表的最后
//添加数据
// 第一种方法:,直接添加到链表的尾部
public void add(HeroNode heroNode) {
//因为head节点不能移动,定义辅助变量
HeroNode temp = getHead();
//遍历链表,找到末节点
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
}
temp.next = heroNode;
}
💧 遍历
- 通过一个辅助变量遍历,帮助遍历整个链表
//遍历链表
public void show() {
//判断链表是否为空
if(head.next == null) {
System.out.println("链表为空");
return;
}
HeroNode temp = getHead();
//遍历链表,找到末节点
while (true) {
if (temp.next == null) {
break;
}
//将temp后移
temp = temp.next;
System.out.println(temp);
}
}
💧 第二种方式:根据排名将英雄插入到指定位置
需要按照编号的顺序添加
- 首先找到新添加的节点的位置, 是通过辅助变量(指针), 通过遍历来搞定
- 新的节点.next = temp.next
- 将temp.next = 新的节点
//添加数据
//第二种方式:根据排名将英雄插入到指定位置
public void addByOrder(HeroNode heroNode) {
//因为head节点不能移动,定义辅助变量
HeroNode temp = getHead();
boolean flag = false;// flag标志添加的编号是否存在,默认为false
//遍历链表,找到相应节点
while (true) {
if (temp.next == null) {
break;
} else if (temp.next.no > heroNode.no) {
break;
} else if (temp.next.no == heroNode.no) {
flag = true; //说明编号存在
break;
}
temp = temp.next;//后移,遍历当前链表
}
if (flag) {
System.out.println("已存在");
} else {//插入到链表中, temp的后面
heroNode.next = temp.next;
temp.next = heroNode;
}
}
💧 修改
修改单链表中的一个节点
1.通过遍历,先找到节点
2.temp.name = newHeroNode.name;
temp.nickName = newHeroNode.nickName;
//找到需要修改的节点, 根据no编号
//定义一个辅助变量
HeroNode temp = getHead();
boolean flag = false;//表示是否找到该节点
while (true) {
if (temp.next == null) {
break;//已经遍历完链表
}
if (temp.no == heroNode.no) {
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
System.out.println("修改成功");
} else {
System.out.println("没有找到数据");
}
}
💧 删除
从单链表中删除一个节点的思路
- 我们先找到 需要删除的这个节点的前一个节点 temp
- temp.next = temp.next.next
- 被删除的节点,将不会有其它引用指向,会被垃圾回收机制回收
//根据编号删除数据
public void delete(int no) {
HeroNode temp = getHead();
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.no == no) {
//找到的待删除节点的前一个节点temp
temp.next = temp.next.next;
System.out.println("删除成功");
return;
}
temp = temp.next;
}
System.out.println("没有找到相应数据");
}
3.3单链表的常见面试题
1)求单链表中有效节点的个数
//求单链表中有效节点的个数
public static int getLength(HeroNode head) {
if(head.next == null) { //空链表
return 0;
}
int length = 0;
//定义一个辅助的变量, 这里我们没有统计头节点
HeroNode cur = head.next;
while(cur != null) {
length++;
cur = cur.next; //遍历
}
return length;
}
2)查找单链表中的倒数第k个结点 【新浪面试题】
//思路
//1. 编写一个方法,接收head节点,同时接收一个index
//2. index 表示是倒数第index个节点
//3. 先把链表从头到尾遍历,得到链表的总的长度 getLength
//4. 得到size 后,我们从链表的第一个开始遍历 (size-index)个,就可以得到
//5. 如果找到了,则返回该节点,否则返回nulll
public static HeroNode findLastIndexNode(HeroNode head, int k) {
//判断如果链表为空,返回null
if(head.next == null) { //空链表
return null;
}
//第一个遍历得到链表的长度(节点个数)
int length = getLength(head);
//第二次遍历 size-index 位置,就是我们倒数的第K个节点
//先做一个index的校验
if(k <=0 || k > length) {
return null;
}
//定义给辅助变量, for 循环定位到倒数的index
HeroNode temp = head.next;
for (int i = 0; i < length - k; i++) {
temp = temp.next;
}
return temp;
}
3)单链表的反转【腾讯面试题,有点难度】
思路:
- 先定义一个节点 reverseHead = new HeroNode();
- 从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端.
- 原来的链表的head.next = reverseHead.next
详细步骤参考:数据结构 单链表的反转
4)从尾到头打印单链表 【百度,要求方式1:反向遍历 。 方式2:Stack栈】
//方式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.push(cur);
cur = cur.next; //cur后移,这样就可以压入下一个节点
}
//将栈中的节点进行打印,pop 出栈
while (stack.size() > 0) {
System.out.println(stack.pop()); //stack的特点是先进后出
}
}
5)合并两个有序的单链表,合并之后的链表依然有序【课后练习.】
3.4单链表的应用实例及常见面试题完整代码
package com.atguigu.linkedlist;
import java.util.Stack;
public class SingleLinkedListDemo {
public static void main(String[] args) {
HeroNode h1 = new HeroNode(1,"宋江", "及时雨");
HeroNode h2 = new HeroNode(2,"卢俊义", "玉麒麟");
HeroNode h3 = new HeroNode(3,"吴用", "智多星");
HeroNode h4 = new HeroNode(4,"林冲", "豹子头");
//创建一个链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
// singleLinkedList.add(h1);
// singleLinkedList.add(h2);
// singleLinkedList.add(h4);
// singleLinkedList.add(h3);
// singleLinkedList.show();
singleLinkedList.addByOrder(h1);
singleLinkedList.addByOrder(h2);
singleLinkedList.addByOrder(h4);
singleLinkedList.addByOrder(h3);
//测试修改节点的代码
// HeroNode newHeroNode = new HeroNode(2, "小卢", "玉麒麟~~");
// singleLinkedList.update(newHeroNode);
//测试删除节点的代码
// singleLinkedList.delete(2);
singleLinkedList.show();
//测试求单链表中有效节点的个数
// int count = getLength(singleLinkedList.getHead());
// System.out.println(count);
System.out.println("测试逆序打印单链表, 没有改变链表的结构~~");
reversePrint(singleLinkedList.getHead());
//测试查找单链表中的倒数第k个结点 【新浪面试题】
// int k = 1;
// HeroNode heroNode = findLastIndexNode(singleLinkedList.getHead(), k);
// System.out.println("倒数第" + k + "个节点为:" + heroNode);
System.out.println("****************************");
//测试单链表反转方式一
// HeroNode newHead = reverseList(singleLinkedList.getHead());
// singleLinkedList.setHead(newHead);
// singleLinkedList.show();
//测试单链表反转方式二
// reverseList1(singleLinkedList.getHead());
// singleLinkedList.show();
}
//方式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.push(cur);
cur = cur.next; //cur后移,这样就可以压入下一个节点
}
//将栈中的节点进行打印,pop 出栈
while (stack.size() > 0) {
System.out.println(stack.pop()); //stack的特点是先进后出
}
}
//单链表的反转方式二
public static void reverseList1(HeroNode head) {
//链表为空或者链表只有一个节点,无需反转
if (head.next == null || head.next.next == null) {
return;
}
//定义辅助变量
HeroNode cur = head.next;
//存放原链表当前节点的下一个节点
HeroNode next = null;
//反转列表头节点
HeroNode reverseHead = new HeroNode();
while (cur != null) {//遍历原列表将数据根据头插法插入新链表
//保存原链表当前节点的下一个节点
next = cur.next;
//将当前节点插入反转链表
cur.next = reverseHead.next;
reverseHead.next = cur;
//将辅助节点指向原链表下一个节点
cur = next;
}
//将反转链表头节点赋给原头节点
head.next = reverseHead.next;
}
//单链表的反转,【腾讯面试题,有点难度】
public static HeroNode reverseList(HeroNode head) {
if (head.next == null || head.next.next == null) {
return head;
}
//定义辅助变量,遍历链表
HeroNode temp = head;
//新建一个头节点,保存反转链表
HeroNode newHead = new HeroNode();
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
HeroNode newTemp = new HeroNode(temp.no, temp.name, temp.nickName);
if (newHead.next == null) {
newHead.next = newTemp;
newTemp.next = null;
} else {
//temp变量指针不能变,定义辅助变量,给新链表赋值
newTemp.next = newHead.next;
newHead.next = newTemp;
}
}
return newHead;
}
//求单链表中有效节点的个数
public static int getLength(HeroNode head) {
if(head.next == null) { //空链表
return 0;
}
int length = 0;
//定义一个辅助的变量, 这里我们没有统计头节点
HeroNode cur = head.next;
while(cur != null) {
length++;
cur = cur.next; //遍历
}
return length;
}
//查找单链表中的倒数第k个结点 【新浪面试题】
//思路
//1. 编写一个方法,接收head节点,同时接收一个index
//2. index 表示是倒数第index个节点
//3. 先把链表从头到尾遍历,得到链表的总的长度 getLength
//4. 得到size 后,我们从链表的第一个开始遍历 (size-index)个,就可以得到
//5. 如果找到了,则返回该节点,否则返回nulll
public static HeroNode findLastIndexNode(HeroNode head, int k) {
//判断如果链表为空,返回null
if(head.next == null) { //空链表
return null;
}
//第一个遍历得到链表的长度(节点个数)
int length = getLength(head);
//第二次遍历 size-index 位置,就是我们倒数的第K个节点
//先做一个index的校验
if(k <=0 || k > length) {
return null;
}
//定义给辅助变量, for 循环定位到倒数的index
HeroNode temp = head.next;
for (int i = 0; i < length - k; i++) {
temp = temp.next;
}
return temp;
}
}
//定义SingleLinkedList 管理我们的英雄
class SingleLinkedList {
//初始化头节点
private HeroNode head = new HeroNode();
//返回头节点
public HeroNode getHead() {
return head;
}
//给头节点赋值
public void setHead(HeroNode head) {
this.head = head;
}
//添加数据
// 第一种方法:,直接添加到链表的尾部
public void add(HeroNode heroNode) {
//因为head节点不能移动,定义辅助变量
HeroNode temp = getHead();
//遍历链表,找到末节点
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
}
temp.next = heroNode;
}
// public void addByOrder(HeroNode heroNode) { //错误❌ 若链表为空,则无法加入数据
// //因为head节点不能移动,定义辅助变量
// HeroNode temp = getHead();
// //遍历链表,找到相应节点
// while (true) {
// if (temp.next == null) {
// break;
// } else if (temp.next.no > heroNode.no) {
// heroNode.next = temp.next;
// temp.next = heroNode;
// } else if (temp.next.no == heroNode.no) {
// System.out.println("已存在");
// break;
// }
// temp = temp.next;
// }
// }
//添加数据
//第二种方式:根据排名将英雄插入到指定位置
public void addByOrder(HeroNode heroNode) {
//因为head节点不能移动,定义辅助变量
HeroNode temp = getHead();
boolean flag = false;// flag标志添加的编号是否存在,默认为false
//遍历链表,找到相应节点
while (true) {
if (temp.next == null) {
break;
} else if (temp.next.no > heroNode.no) {
break;
} else if (temp.next.no == heroNode.no) {
flag = true; //说明编号存在
break;
}
temp = temp.next;//后移,遍历当前链表
}
if (flag) {
System.out.println("已存在");
} else {//插入到链表中, temp的后面
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//遍历链表
public void show() {
//判断链表是否为空
if(head.next == null) {
System.out.println("链表为空");
return;
}
HeroNode temp = getHead();
//遍历链表,找到末节点
while (true) {
if (temp.next == null) {
break;
}
//将temp后移
temp = temp.next;
System.out.println(temp);
}
}
//根据编号删除数据
public void delete(int no) {
HeroNode temp = getHead();
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.no == no) {
//找到的待删除节点的前一个节点temp
temp.next = temp.next.next;
System.out.println("删除成功");
return;
}
temp = temp.next;
}
System.out.println("没有找到相应数据");
}
//根据编号修改数据
public void update(HeroNode heroNode) {
//判断是否空
if(head.next == null) {
System.out.println("链表为空~");
return;
}
//找到需要修改的节点, 根据no编号
//定义一个辅助变量
HeroNode temp = getHead();
boolean flag = false;//表示是否找到该节点
while (true) {
if (temp.next == null) {
break;//已经遍历完链表
}
if (temp.no == heroNode.no) {
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
System.out.println("修改成功");
} else {
System.out.println("没有找到数据");
}
}
//根据编号查询数据
public HeroNode get(int no) {
//判断是否空
if(head.next == null) {
System.out.println("链表为空~");
return null;
}
HeroNode temp = getHead();
while (true) {
if (temp.next == null) {
break;
}
if (temp.no == no) {
return temp;
}
}
return null;
}
}
//定义HeroNode , 每个HeroNode 对象就是一个节点
class HeroNode {
public int no;
public String name;
public String nickName;
public HeroNode next;//指向下一个节点
//构造器
public HeroNode() {
}
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 + '\'' +
'}';
}
}
3.5双向链表的应用实例
- 使用带head头的双向链表实现 –水浒英雄排行榜
管理单向链表的缺点分析:
1)单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
2)单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp是待删除节点的前一个节点(认真体会).
3)示意图帮助理解删除
分析 双向链表的遍历,添加,修改,删除的操作思路===》代码实现
- 遍历 方和 单链表一样,只是可以向前,也可以向后查找
- 添加 (默认添加到双向链表的最后)
(1) 先找到双向链表的最后这个节点
(2) temp.next = newHeroNode
(3) newHeroNode.pre = temp;- 修改 思路和 原来的单向链表一样.
- 删除
(1) 因为是双向链表,因此,我们可以实现自我删除某个节点
(2) 直接找到要删除的这个节点,比如temp
(3) temp.pre.next = temp.next
(4) temp.next.pre = temp.pre;
3.6双向链表的应用实例完整代码
package com.atguigu.linkedlist;
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("链表为空");
return;
}
// 因为头节点,不能动,因此我们需要一个辅助变量来遍历
HeroNode2 temp = head.next;
while (true) {
// 判断是否到链表最后
if (temp == null) {
break;
}
// 输出节点的信息
System.out.println(temp);
// 将temp后移, 一定小心
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 = heroNode;
heroNode.pre = temp;
}
// 修改一个节点的内容, 可以看到双向链表的节点内容修改和单向链表一样
// 只是 节点类型改成 HeroNode2
public void update(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 对于双向链表,我们可以直接找到要删除的这个节点
// 2 找到后,自我删除即可
public void del(int no) {
// 判断当前链表是否为空
if (head.next == null) {// 空链表
System.out.println("链表为空,无法删除");
return;
}
HeroNode2 temp = head.next; // 辅助变量(指针)
boolean flag = false; // 标志是否找到待删除节点的
while (true) {
if (temp == null) { // 已经到链表的最后
break;
}
if (temp.no == no) {
// 找到的待删除节点的前一个节点temp
flag = true;
break;
}
temp = temp.next; // temp后移,遍历
}
// 判断flag
if (flag) { // 找到
// 可以删除
// temp.next = temp.next.next;[单向链表]
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;
}
// 为了显示方法,我们重新toString
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
}
}
3.7单向环形链表应用场景
- Josephu(约瑟夫、约瑟夫环) 问题
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示:用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
- 单链表示意图
3.8约瑟夫例题
-
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
- n = 5 , 即有5个人
- k = 1, 从第一个人开始报数
- m = 2, 数2下
- 构建一个单向的环形链表思路
- 先创建第一个节点, 让 first 指向该节点,并形成环形
- 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可.
- 遍历环形链表
- 先让一个辅助指针(变量) curBoy,指向first节点
- 然后通过一个while循环遍历 该环形链表即可 curBoy.next == first 结束
- 出圈步骤
- 需求创建一个辅助指针(变量) helper , 事先应该指向环形链表的最后这个节点.使其在first节点前
补充: 小孩报数前,先让 first 和 helper 移动 k - 1次 - 当小孩报数时,让first 和 helper 指针同时 的移动 m - 1 次
- 这时就可以将first 指向的小孩节点 出圈
first = first .next
helper.next = first
原来first 指向的节点就没有任何引用,就会被回收
出圈的顺序
2->4->1->5->3
- 完整代码
package com.atguigu.linkedlist;
public class Josephu {
public static void main(String[] args) {
// 测试一把看看构建环形链表,和遍历是否ok
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(13);// 加入5个小孩节点
circleSingleLinkedList.showBoy();
//测试一把小孩出圈是否正确
circleSingleLinkedList.countBoy(1, 3, 13); // 2->4->1->5->3
//String str = "7*2*2-5+1-5+3-3";
}
}
// 创建一个环形的单向链表
class CircleSingleLinkedList {
// 创建一个first节点,当前没有编号
private Boy first = null;
// 添加小孩节点,构建成一个环形的链表
public void addBoy(int 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 = boy;// 让curBoy指向第一个小孩
} 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.println("小孩的编号:" + 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) { // 说明helper指向最后小孩节点
break;
}
helper = helper.getNext();
}
//开始数数前,将first移动到开始数数的小孩,需要移动 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;
}
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; // 指向下一个节点,默认null
public Boy(int no) {
this.no = no;
}
public void setNo(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
第四章 栈
4.1栈的一个实际需求
请输入一个表达式
计算式:[722-5+1-5+3-3] 点击计算【如下图】
请问: 计算机底层是如何运算得到结果的? 注意不是简单的把算式列出运算,因为我们看这个算式 7 * 2 * 2 - 5, 但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题。-> 栈
4.2栈的介绍
- 栈的英文为(stack)
- 栈是一个先入后出(FILO-First In Last Out)的有序列表。
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
- 出栈(pop)和入栈(push)的概念(如图所示)
4.3栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
- 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth一first)搜索法。
4.4栈的快速入门
- 用数组模拟栈的使用,由于栈是一种有序列表,当然可以使用数组的结构来储存栈的数据内容,下面我们就用数组模拟栈的出栈,入栈等操作。
- 实现思路分析,并画出示意图
4.4.1 代码实现
package com.atguigu.stack;
import java.util.Scanner;
//定义一个ArrayStack表示栈
public class ArrayStackDemo {
;
public static void main(String[] args) {
//创建栈
ArrayStack stack = new ArrayStack(4);
String key = "";
boolean loop = true; //控制是否退出菜单
Scanner scanner = new Scanner(System.in);
while (loop) {
System.out.println("show: 表示显示栈");
System.out.println("exit: 退出程序");
System.out.println("push: 表示添加数据到栈(入栈)");
System.out.println("pop: 表示从栈取出数据(出栈)");
System.out.println("请输入你的选择:");
key = scanner.next();
switch (key) {
case "show":
stack.list();
break;
case "exit":
scanner.close();
loop = false;
break;
case "pop":
try {
int res = stack.pop();
System.out.printf("出战的数据是 %d\n", res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case "push":
System.out.println("请输入一个数:");
int value = scanner.nextInt();
stack.push(value);
break;
}
}
System.out.println("程序退出");
}
}
class ArrayStack {
private int maxSize; //栈的大小
private int[] stack; //数组,数组模拟栈,数据存放在数组中
private int top = -1;//top表示栈顶,初始化为1
//构造器
public ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
//栈满
public boolean isFull() {
return top == maxSize - 1;
}
//栈空
public boolean isEmpty() {
return top == -1;
}
//入栈
public void push(int value) {
//判断栈是否满
if (isFull()) {
System.out.println("栈满");
return;
}
top++;
stack[top] = value;
}
//出栈
public int pop() {
//先判断栈是否为空
if (isEmpty()) {
//抛出异常
throw new RuntimeException("栈空,没有数据");
}
int value = stack[top];
top--;
return value;
}
//显示栈的情况[遍历栈],遍历时,需要从栈顶开始显示数据
public void list() {
if (isEmpty()) {
System.out.println("栈空,没有数据");
return;
}
//从栈顶开始显示数据
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\n", i, stack[i]);
}
}
}
4.4.2 栈实现综合计算器
4.5 前缀、中缀、后缀表达式
4.5.1 前缀表达式(波兰表达式)
- 前缀表达式又称波兰式,前缀表达式的运算符位
- 于操作数之前
举例说明: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6
-
前缀表达式的计算机求值
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果 -
例如: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:
- 从右至左扫描,将6、5、4、3压入堆栈
- 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
- 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈
- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
4.5.2 中缀表达式
-
中缀表达式就是常见的运算表达式,如(3+4)×5-6
-
中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(前面我们讲的案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式.)
4.5.3 后缀表达式(逆波兰表达式)
-
后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后
-
中举例说明: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –
-
再比如:
正常的表达式 | 逆波兰表达式 |
---|---|
a+b | a b + |
a+(b-c) | a b c - + |
a+(b-c)*d | a b c - d * + |
a+d*(b-c) | a d b c - * + |
a=1+3 | a 1 3 + = |
- 后缀表达式的计算机求值
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
- 从左至右扫描,将3和4压入堆栈;
- 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
- 将5入栈;
- 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
- 将6入栈;
- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
4.6 逆波兰计算器
- 我们完成一个逆波兰计算器,要求完成如下任务:
- 输入一个逆波兰表达式(后缀表达式),使用栈(Stack), 计算其结果
- 支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。
- 思路分析
- 代码完成
/**
* 逆波兰表达式计算器
*/
public class PolandNotation {
public static void main(String[] args) {
//先定义逆波兰表达式
//(30+4)x5-6 => 30 4 + 5 x 6 -
String suffixExpression = "30 4 + 5 * 6 -";
/**
* 思路
* 1.先将“3 4 + 5 x 6 -” => 放到ArrayList中
* 2.将ArrayList传递给一个方法,遍历ArrayList配合栈完成计算
*/
List<String> rpnList = getListString(suffixExpression);
System.out.println("rpnList = " + rpnList);
int res = calculate(rpnList);
System.out.println(res);
}
//将一个逆波兰表达式,依次将数据和运算符 放入到 ArrayList中
public static List<String> getListString(String suffixExpression) {
//将 suffixExpression 分割
String[] split = suffixExpression.split(" ");
ArrayList<String> list = new ArrayList<>();
for (String ele : split) {
list.add(ele);
}
return list;
}
/**
* 1. 从左至右扫描,将3和4压入堆栈;
* 2. 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
* 3. 将5入栈;
* 4. 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
* 5. 将6入栈;
* 6. 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
*/
public static int calculate(List<String> ls) {
//创建栈
Stack<String> stack = new Stack<>();
//遍历 ls
for (String item: ls) {
//使用正则表达式取出数
if (item.matches("\\d+")) { //匹配的是多位数
//入栈
stack.push(item);
} else {
//pop出两个数,并运算,在入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if (item.equals("+")) {
res = num1 + num2;
} else if (item.equals("-")) {
res = num1 - num2;
} else if (item.equals("*")) {
res = num1 * num2;
} else if (item.equals("/")) {
res = num1 / num2;
} else {
throw new RuntimeException("运算符有误");
}
//把res入栈
stack.push(res + "");
}
}
//最后留在栈中的数据为运算结果
return Integer.parseInt(stack.pop());
}
}
4.7 中缀表达式转换为后缀表达式
大家看到,后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将 中缀表达式转成后缀表达式。
- 具体步骤如下:
- 初始化两个栈:运算符栈s1和储存中间结果的栈s2;
- 从左至右扫描中缀表达式;
- 遇到操作数时,将其压s2;
- 遇到运算符时,比较其与s1栈顶运算符的优先级:
① 如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
②否则,若优先级比栈顶运算符的高,也将运算符压入s1;
③否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较; - 遇到括号时:
① 如果是左括号“(”,则直接压入s1
② 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃 - 重复步骤2至5,直到表达式的最右边
- 将s1中剩余的运算符依次弹出并压入s2
- 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
- 举例说明:
将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下
因此结果为
“1 2 3 + 4 × + 5 –”
4.7.1 代码实现中缀表达式转为后缀表达式
package com.atguigu.stack;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* 逆波兰表达式计算器
*/
public class PolandNotation {
public static void main(String[] args) {
/**
* 说明
* 1.1+((2+3)x4)-5 => 转成 1 2 3 + 4 x 5 -
* 2.因为丢str进行操作不方便,因此先将“1+((2+3)x4)-5” =>中缀的表达式对应的List
* 即:“1+((2+3)x4)-5” => ArrayList[1,+,(,(,2,+,3,),*,4,),-,5]
*/
String expression = "1+((2+3)*4)-5";
List<String> infixExpressionList = toInfixExpressionList(expression);
System.out.println("中缀表达式对应的List=>" + infixExpressionList);
List<String> parseSuffixExpressionList = parseSuffixExpressionList(infixExpressionList);
System.out.println("后缀表达式对应的List=>" + parseSuffixExpressionList);
System.out.printf("expression=%d", calculate(parseSuffixExpressionList));
/*
//先定义逆波兰表达式
//(30+4)x5-6 => 30 4 + 5 x 6 -
String suffixExpression = "30 4 + 5 * 6 -";
*/
/**
* 思路
* 1.先将“3 4 + 5 x 6 -” => 放到ArrayList中
* 2.将ArrayList传递给一个方法,遍历ArrayList配合栈完成计算
*/
/*
List<String> rpnList = getListString(suffixExpression);
System.out.println("rpnList = " + rpnList);
int res = calculate(rpnList);
System.out.println(res);
*/
}
//即:“1+((2+3)x4)-5” => ArrayList[1,+,(,(,2,+,3,),*,4,),-,5]
//方法:将得到的中缀表达式对应的List => 后缀表达式对应的List
public static List<String> parseSuffixExpressionList(List<String> ls) {
//定义一个栈一个链表存储
Stack<String> s1 = new Stack<>(); //符号栈
//说明:因为s2这个栈,在整个转换过程中,没有pop操作,而且后面我们还需要逆序输出
//因此比较麻烦,故不用Stack直接用List s2
List<String> s2 = new ArrayList<>();//存储中间结果的List s2
//遍历ls
for (String item: ls) {
//如果是一个数,加入s2
if (item.matches("\\d+")) {
s2.add(item);
} else if (item.equals("(")) {
s1.push(item);
} else if (item.equals(")")) {
//如果是右括号")",则一次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
while (!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop();//将 ( 弹出 s1栈,消除小括号
} else {
//当item的优先级小于等于s1栈顶运算符,将s1栈顶的运算符弹出并加入到s2中,再次转到(4,1)与s1中新的栈顶运算符相比较
while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
s2.add(s1.pop());
}
//还需要将item压入栈
s1.push(item);
}
}
//将s1中剩余的运算符依次弹出并加入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2; //存放于List,因此按顺序输出出就是对应的后缀表达式
}
//方法:将 中缀表达式转成对应的List
public static List<String> toInfixExpressionList(String s) {
//定义一个List,存放中缀表达式
List<String> ls = new ArrayList<>();
int i = 0; //用于遍历 中缀表达式
String str = ""; //对多位数的拼接
char c; //每遍历一个字符,就放入c
do {
//若是符号直接加入ls
if ((c=s.charAt(i)) < 48|| (c=s.charAt(i)) > 57) {
ls.add(c + "");
i++;
} else { //是数字,判断下一位是否为数字
str = "";//置空
while (i < s.length() && (c=s.charAt(i)) >= 48 && (c=s.charAt(i)) <= 57) {
str += c;
i++;
}
ls.add(str);
}
} while (i < s.length());
return ls;
}
//将一个逆波兰表达式,依次将数据和运算符 放入到 ArrayList中
public static List<String> getListString(String suffixExpression) {
//将 suffixExpression 分割
String[] split = suffixExpression.split(" ");
ArrayList<String> list = new ArrayList<>();
for (String ele : split) {
list.add(ele);
}
return list;
}
/**
* 1. 从左至右扫描,将3和4压入堆栈;
* 2. 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
* 3. 将5入栈;
* 4. 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
* 5. 将6入栈;
* 6. 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
*/
public static int calculate(List<String> ls) {
//创建栈
Stack<String> stack = new Stack<>();
//遍历 ls
for (String item: ls) {
//使用正则表达式取出数
if (item.matches("\\d+")) { //匹配的是多位数
//入栈
stack.push(item);
} else {
//pop出两个数,并运算,在入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if (item.equals("+")) {
res = num1 + num2;
} else if (item.equals("-")) {
res = num1 - num2;
} else if (item.equals("*")) {
res = num1 * num2;
} else if (item.equals("/")) {
res = num1 / num2;
} else {
throw new RuntimeException("运算符有误");
}
//把res入栈
stack.push(res + "");
}
}
//最后留在栈中的数据为运算结果
return Integer.parseInt(stack.pop());
}
}
//编写一个类Operation 可以返回一个运算符 对应的优先级
class Operation {
private static int ADD = 1;
private static int SUB = 1;
private static int MUL = 2;
private static int DIV = 2;
//携程一个方法,返回对应的优先级数字
public static int getValue(String operation) {
int result = 0;
switch (operation) {
case "+":
result = ADD;
break;
case "-":
result = SUB;
break;
case "*":
result = MUL;
break;
case "/":
result = DIV;
break;
default:
System.out.println("不存在该符号");
break;
}
return result;
}
}