双向链表
双向链表应用实例
.1双向链表的操作分析和实现使用带head头的双向链表实现–水浒英雄排行榜管理单向链表的缺点分析:1)单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
2)单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp是待删除节点的前一个节点(认真体会)
.3)分析了双向链表如何完成遍历,添加,修改和删除的思路
代码实现
1)遍历方和单链表一样,只是可以向前,也可以向后查找
2)添加(默认添加到双向链表的最
(1)先找到双向链表的最后这个节点
(2)temp.next=newHeroNode
(3)newHeroNode.pre=temp;
3)修改思路和原来的单向链表一样.
4)删除(1)因为是双向链表,因此,我们可以实现自我删除某个节点
(2)直接找到要删除的这个节点,比如temp
(3)temp.pre.next=temp.next
(4)temp.next.pre=temp.pre;
package com.zy.LinkedList;
/**
* @ClassName: DoubleLinkedListDemo
* @Author: Tigger
* @Title: 双向链表测试
* @Datetime: 2020/8/15 20:05
* @Package: com.zy.LinkedList
*/
public class DoubleLinkedListDemo {
public static void main(String[] args) {
System.out.println("双向链表测试");
HeroNode2 node2 = new HeroNode2(1, "宋江", "及时雨");
HeroNode2 heroNode2 = new HeroNode2(2, "卢俊义", "玉麒麟");
HeroNode2 node21 = new HeroNode2(3, "吴用", "智多星");
HeroNode2 node22 = new HeroNode2(4, "林冲", "豹子头");
DoubleLinkedList list = new DoubleLinkedList();
list.add(node2);
list.add(heroNode2);
list.add(node21);
list.add(node22);
list.list();
//修改
HeroNode2 newNode = new HeroNode2(4, "公孙胜", "入云龙");
list.update(newNode);
System.out.println("修改后的英雄信息");
list.list();
//删除
list.delete(3);
System.out.println("删除后的结果是");
list.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;
}
}
//添加节点到双向链表
//思路当不考虑编号顺序时,
//1.找到当前链路的节点,
//将最后节点的next指向新的节点
public void add(HeroNode2 hereNode) {
//因为头节点不能动,所以我们需要一个临时temp去协助遍历
HeroNode2 temp = head;
//遍历链表找到最后
while (true) {
if (temp.next == null) {
break;
}
//如果没有找到,就将temp后移到下一个节点
temp = temp.next;
}
//当退出while循环时就表示,temp指向了链表的尾部
//将最后节点的next指向新的节点
temp.next = hereNode;
hereNode.pre = temp;
}
//修改节点信息,根据编号修改,不能修改编号只能修改节点的数据
public void update(HeroNode2 newHereNode) {
if (head.next == null) {
System.out.println("链表为空");
return;
}
HeroNode2 temp = head.next;
boolean flag = false;//表示是否找到
while (true) {
if (temp == null) {
//表示已经遍历完了链表
break;
}
if (temp.no == newHereNode.no) {
//找到了
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.name = newHereNode.name;
temp.nickName = newHereNode.nickName;
} else {
System.out.println("没有找到角色");
}
}
//删除节点信息 ,双向链表会删除指定的节点,不需要指定前一个辅助节点来帮助删除
public void delete(int no) {
//首先判断链表是否为空
if (head.next == null) {
System.out.println("不好意思,链表为空无法删除");
return;
}
HeroNode2 temp = head;//temp是待删除节点的上一个节点
Boolean flag = false;//判断待删除节点是否存在
while (true) {
if (temp.next == null) {//已经到了链表的最后
break;
}
if (temp.next.no == no) {
flag = true;
break;
}
temp = temp.next;//temp后移,这样我们才能完成循环遍历
}
if (flag) {
temp.pre.next = temp.next;//让被删除的节点的下一个节点的上一个节点指向被删除节点的上一个节点
//如果想要删除的节点是最后一个节点时,最后节点下一节点是空,空的前一个节点也是空,这个时候执行了下面一行代码就会导致
//空指针异常,因此我们需要进行if判断
if (head.next != null) {
temp.next.pre = temp.pre;//让被删除节点的下一个节点的前一位pre指向被删除节点的前一位
}
} else {
System.out.println("不好意思,没有找到想过要被删除的节点");
}
}
}
class HeroNode2 {
public int no;
public String name;
public String nickName;
public HeroNode2 next;//定义指向下一个节点
public HeroNode2 pre;//指向了上一个节点,从而形成双向链表
public HeroNode2() {
}
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 + '\'' +
'}';
}
}
单向环形链表应用场景
约瑟夫环问题
Josephu(约瑟夫、约瑟夫环)问题Josephu问题为:设编号为1,2,…n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示:用一个不带头结点的循环链表来处理Josephu问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
单向循环链表
约瑟夫问题的流程
Josephu问题Josephu问题为:
设编号为1,2,…n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示
用一个不带头结点的循环链表来处理Josephu问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个
创建环形链表的思路图解
小孩出圈的顺序思路分析图
代码:
package com.zy.LinkedList;
/**
* @ClassName: Josepfu
* @Author: Tigger
* @Title: 约瑟夫问题(通过循环链表解决,也可以定义数组取模实现)
* @Datetime: 2020/8/15 21:51
* @Package: com.zy.LinkedList
*/
public class Joseph {
public static void main(String[] args) {
CircleSingleLinkedList linkedList = new CircleSingleLinkedList();
linkedList.addBoys(125);
// linkedList.showBoy();
linkedList.boyGouOut(1,2,125);
}
}
//创建一个环形的单向链表
class CircleSingleLinkedList {
//创建一个first节点
private Boy first = null;
//添加小孩节点构成一个环形链表
public void addBoys(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; // 让curBoy指向第一个小孩
} else {
curBoy.setNext(boy);//插入前的最后一个节点的下一位指向插入节点,
boy.setNext(first);//插入节点的下一位指向first从而形成环形单向链表
curBoy = boy;//让辅助指针右移从而实现不停的插入
}
}
}
//循环遍历出所有的节点
/**
* 功能描述:
*
* @Date: 2020/8/17
* @Param: []
* @Return: void
* @Author: Tiger
*/
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) {
// System.out.println("说明已经遍历完毕");
break;
} else {
curBoy = curBoy.getNext();//让curboy右移从而实现循环遍历节点
}
}
}
/**
* 功能描述:
*
* @Date: 这个方法模拟小孩退出环形单向链表的一个方法
* @Param: startNo:表示从第几个小孩开始数数
* @Param: countNum: 表示数数几次
* @Param: nums: 表示最开始有多少个小孩在这个圈中
* @Return: void
* @Author: Tiger
*/
public void boyGouOut(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和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;
}
//让first和helper同时移动countNum—1
for (int i = 0; i < countNum - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//这个时候first指向的节点,就是要出圈的小孩的节点
System.out.println("小孩出圈的id是" + first.getNo());
//这时将first指向的小孩节点出圈
first = first.getNext();
helper.setNext(first);
}
System.out.println("最后留在圈中小孩的id"+ first.getNo());
}
}
//定义boy类 表示一个节点
class Boy {
private int no;//编号
private Boy next;//指向下一个节点,默认为空
public Boy() {
}
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;
}
}
栈
栈的介绍:
1) 栈的英文叫(strack)
2)栈是一个先入后出的有序列表(FIFO)
3)栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)
4)根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元
素最先删除,最先放入的元素最后删除
5)图解方式说明出栈(pop)和入栈(push)的概念
应用场景
(1)子程序的调用,在跳往子程序之前,会先将下个指令的地址存到堆栈中去,直到子程序执行完毕之后再将其取出,以回到原来的程序中去。
(2)处理递归调用,和子程序的调用类似,只是除了存储下一个指令的地址之外, 也将参数,区域变量等数据都存入到栈中去,表达式的转换{中缀表达式转换成后缀表达式}与求值(实际解决)
(3)二叉树的遍历
(4)图形的深度优先(depth-first)搜索法
入门
(1)用数组模拟栈的使用,由于栈是一种有序列表,当然可以使用数组的的结构存储栈的数据内容,
(2)思路分析:
代码:
package com.zy.strack;
import java.util.Scanner;
public class ArrayStackDemo {
public static void main(String[] args) {
//测试一下这个arrayStck是否正确
ArrayStack stack = new ArrayStack(4);
String key = "";
boolean flag = true;//控制是否跳出菜单
Scanner scanner = new Scanner(System.in);
while (flag) {
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":
flag = false;
System.out.println("程序退出");
break;
case "push":
System.out.println("请输入数字");
int i = scanner.nextInt();
stack.push(i);
break;
case "pop":
try {
int pop = stack.pop();
System.out.println("出栈的数字是:" + pop);
} catch (Exception e) {
e.printStackTrace();
}
break;
}
}
}
}
//定义一个数组模拟栈的类
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()) {
// System.out.println("栈空无法删除");
throw new RuntimeException("Stack is null ,you can't delete any data!!");
}
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.println(i + ":" + stack[i]);
}
}
}
栈实现简单的综合计算器(中缀表达式)
思路分析:
代码实现:
public class Calculator {
public static void main(String[] args) {
//根据之前的思路,完成一串表达式的计算
String expression = "30+22*6-14";//如何处理多位数的情况
//创建两个栈。一个存放数字,一个专门存放运算符
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
//定义需要的变量 代替索引相当于指针 帮助我们去取到表达是中的每一个
int index = 0;
int number1 ;
int number2 ;
int oper ;
int result ;
String keepNum ="";//用于拼接多位数
char ch = ' ';//将每次扫描的char保存到ch中去;
//开始while循环扫描表达式
while (true){
//依次得到表达式 的每一个字符
ch = expression.substring(index,index+1).charAt(0);
//判断ch是什么,然后就行相应的操作
if(operStack.isOper(ch)){//如果是运算符
//判断当前的符号栈是否为空
if(!operStack.isEmpty()){
//不为空的操作,会判断操作符的优先级(如果当前运算符的优先级小于或等于栈中的操作符的优先级
// 就需要从数栈中pop两个数,在运算符栈中pop出一个符号进行运算,将得到的结果入数栈,然后将新添加的符号如符号栈)
if(operStack.priority(ch)<=operStack.priority(operStack.peek())){
//从数栈中弹出两个数字与符号栈弹出的符号进行运算,将结果返回数栈,新加的符号入符号栈
number1 = numStack.pop();
number2 = numStack.pop();
oper = operStack.pop();
result = numStack.cal(number1,number2,oper);
//把运算结果入数栈
numStack.push(result);
//把新加入的运算符入符号栈
operStack.push(ch);
}else{
//如果当前符号的运算符大于栈中的操作符,就直接入栈
operStack.push(ch);
}
}else{
//为空的时候会直接存入到符号栈中去
operStack.push(ch);
}
}else{//如果是数,就直接放入到数栈中去
//这里不能发现它是数就直接入数栈,有可能是多位数
//再处理数的时候要向expression的的index后再看一位,如果是数子就继续扫描,如果是符号才入栈
//因此我们需要定义一个字符串变量,进行数字字符的拼接使用
//处理多位数
keepNum += ch;
//如果ch是最后一个为,直接入栈
if(index ==expression.length()-1){
numStack.push(Integer.parseInt(keepNum));
}else{
//判断下一个字符是不是数字,如果是数字就继续扫描,如果是符号就入栈
if(operStack.isOper(expression.substring(index+1,index+2).charAt(0))){
//这里的keepnum是字符串,所以要转换一下再存进去
numStack.push(Integer.parseInt(keepNum));
//注意 keepnum要清空不然后续都会跟着继续拼接
keepNum = "";
}
}
}
//让index+1,并判断是否扫描到expression的最后
index++;
if(index>=expression.length()){
break;
}
}
//当表达式扫描完毕,就顺序的从数栈,符号栈pop出相应的数和符号,并运行,
while (true){
//如果符号栈为空,则计算到最后的结果,数栈中只有一个最终的 结果说明运算结束
if (operStack.isEmpty()){
break;
}
number1 = numStack.pop();
number2 = numStack.pop();
oper = operStack.pop();
result = numStack.cal(number1,number2,oper);
//把运算结果入数栈
numStack.push(result);
}
// System.out.println("最终的计算结果是:"+numStack.pop());
System.out.printf("表达式%s =%d",expression,numStack.pop());
}
}
class ArrayStack2 {
private int maxsize;//栈的大小
private int[] stack;//数组 数组模拟栈,数据就会存放在该数组种
private int top = -1;//top表示栈顶,初始化值为-1
//构造器
public ArrayStack2(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()) {
// System.out.println("栈空无法删除");
throw new RuntimeException("Stack is null ,you can't delete any data!!");
}
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.println(i + ":" + stack[i]);
}
}
//返回运算符的优先级 ,优先级由程序员选择,优先级使用数字表述,
//返回的数字越大,优先级越高
public int priority(int oper) {
if (oper == '*' || oper == '/') {
return 1;
} else if (oper == '+' || oper == '-') {
return 0;
} else {
return -1;//假定目前的操作符只有加减乘除,
}
}
//判断是操作符还是数字
public boolean isOper(char val) {
return val == '+' || val == '-' || val == '*' || val == '/';
}
//计算方法
public int cal(int number1, int number2, int oper) {
int res = 0;//初始化结果,用来存放最终计算的结果
switch (oper) {
case '+':
res = number1 + number2;
break;
case '-':
res = number2 - number1;
break;
case '*':
res = number2 * number1;
break;
case '/':
res = number2 / number1;
break;
}
return res;
}
//增加一个方法,可以返回当前栈顶的值,但不是真正的出栈pop
public int peek(){
return stack[top];
}
}