1.单链表
(1)简介单链表
有序的列表
实际图:
逻辑图:
小结:
- 链表是以节点的方式来存储的,是链式存储
- 每个节点包含data域、next域(指向下一个节点)
- 链表的各个节点不一定是连续存储的
- 链表可分为有头节点的链表和无头节点的链表
(2)应用案例
需求:使用带head头的单向链表实现 –水浒英雄排行榜管理
-
完成对英雄人物的增删改查操作
-
第一种方法在添加英雄时,直接添加到链表的尾部
-
第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
思路分析:
-
添加(不按顺序):
- 先创建一个head头节点,作用就是表示单链表的头
- 后面我们每添加一个节点,就直接加入到链表的最后
-
添加(按顺序)
- 首先找到新添加节点的位置,通过指针进行遍历
- 新的节点.next = temp.next
- 将temp.next = 新的节点
-
遍历
- 通过一个辅助指针进行遍历
-
删除
- 先找到需要删除的节点的前一个节点temp
- temp.next = temp.next.next
- 被删除的节点,将不会有其它引用指向,会被垃圾回收机制回收
package com.atlige;
/**
* @author Jeremy Li
* @data 2020/12/27 - 11:28
*/
public class SingleLinkedListTest {
public static void main(String[] args) {
// 先创建节点
HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode node3 = new HeroNode(3, "吴用", "智多星");
HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
// 创建链表
SingleLinkedList linkedList = new SingleLinkedList();
linkedList.addNodeByOrder(node3);
linkedList.addNodeByOrder(node4);
linkedList.addNodeByOrder(node2);
linkedList.addNodeByOrder(node1);
linkedList.addNodeByOrder(node4);
// 遍历
linkedList.showList();
// 测试修改
System.out.println("======================");
// HeroNode updateNode = new HeroNode(3, "小吴", "智多星test");
// linkedList.updateNode(updateNode);
// linkedList.deleteNode(3);
// linkedList.deleteNode(4);
linkedList.queryNode("小李");
// 遍历
linkedList.showList();
}
}
// 定义SingleLinkedList管理英雄
class SingleLinkedList{
// 先初始化一个头节点,固定头节点
private HeroNode head = new HeroNode(0, null, null);
/*
当不考虑编号顺序时,思路如下:
1.找到当前链表的最后节点
2.将最后这个节点的next指向新的节点
*/
/**
* 添加节点到单向链表
* @param heroNode 新增节点
*/
public void addNode(HeroNode heroNode){
// 需要一个指针进行遍历
HeroNode temp = head;
while (true){
// 找到链表的最后
if (temp.next == null){
break;
}
// 如果没有找到,将temp后移
temp = temp.next;
}
// 当while循环退出时,temp指向尾节点,将这个节点的next指向新增节点
temp.next = heroNode;
}
/**
* 按顺序添加节点
* @param heroNode 添加的节点
*/
public void addNodeByOrder(HeroNode heroNode){
// 通过指针进行遍历
// 由于是单链表,我们找到位置应该为添加位置的前一个节点,否则不能插入
HeroNode temp = head;
// 标志添加的编号是否存在,默认为false
Boolean flag = false;
while (true){
if (temp.next == null){
// 已经到链表的尾部
break;
}
if (temp.next.no > heroNode.no){
// 找到位置并插入
break;
}else if (temp.next.no == heroNode.no){
// 标号存在
flag = true;
break;
}
// 后移,遍历当前链表
temp = temp.next;
}
// 判断是否存在要插入的节点
if (flag){
System.out.printf("准备插入的英雄编号 %d 已经存在,不能添加\n", heroNode.no);
}else {
// 添加到temp后
heroNode.next = temp.next;
temp.next = heroNode;
}
}
/**
* 根据编号删除节点
* @param no 传入的编号
*/
public void deleteNode(int no){
HeroNode temp = head;
boolean flag = false;
while (true){
if (temp.next == null){
break;
}
if (temp.next.no == no){
flag = true;
break;
}
temp = temp.next;
}
if (flag){
temp.next = temp.next.next;
}else {
System.out.printf("未找到编号为 %d 的英雄,无法删除\n", no);
}
}
public void queryNode(String name){
HeroNode temp = head;
boolean flag = false;
while (true){
if (temp.next == null){
break;
}
if (temp.name == name){
flag = true;
break;
}
temp = temp.next;
}
if (flag){
System.out.printf("已找到姓名为%s的英雄\n", name);
}else {
System.out.println("未找到该英雄!");
}
}
/**
* 修改节点信息,根据no编号取修改
* @param heroNode
*/
public void updateNode(HeroNode heroNode){
// 判断链表是否为空
if (head.next == null){
System.out.println("链表为空!");
return;
}
// 根据指针进行遍历,找到需要修改的节点,根据no编号
HeroNode temp = head.next;
boolean flag = false; // 判断是否找到该节点
while (true){
if (temp == null){
break;// 到达链表尾部
}
if (temp.no == heroNode.no){
// 找到要修改的节点
flag = true;
break;
}
temp = temp.next;
}
// 根据flag判断是否找到要修改的节点
if (flag){
temp.name = heroNode.name;
temp.nikeName = heroNode.nikeName;
}else {
System.out.printf("未找到编号为 %d 的节点,不能进行修改\n", heroNode.no);
}
}
/**
* 遍历单链表
*/
public void showList(){
// 判断链表是否为空
if (head.next == null){
System.out.println("链表为空");
return;
}
// 通过一个指针来遍历
HeroNode temp = head.next;
while (true){
if (temp == null){
break;
}
System.out.println(temp);
// 将指针后移
temp = temp.next;
}
}
}
// 定义HeroNode,每个HeroNode对象就是一个节点
class HeroNode{
public int no;
public String name;
public String nikeName;
public HeroNode next; // 指向下一个节点
public HeroNode(int no, String name, String nikeName) {
this.no = no;
this.name = name;
this.nikeName = nikeName;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nikeName='" + nikeName + '\'' +
'}';
}
}
(3)单向环形链表
Josephu问题:
设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
思路:
用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
- 创建一个辅助指针helper,先将该指针指向环形链表的最后一个节点,first指针指向第一个节点
- 将helper和first移动k-1次,移到k节点的位置
- 当报数时,将first和helper的指针同时的移动m-1次
- 这时就可以将first指向的节点弹出
- first = first.next
- helper.next = first
- 原来first指向的节点就没有任何引用,就会被回收
代码实现:
package com.atlige;
/**
* @author Jeremy Li
* @data 2020/12/29 - 19:22
* 单向环形列表
*/
public class CircleSingleLinkedList {
// 创建一个first节点,当前没有编号
private HeroNode first = new HeroNode(-1);
/**
* 添加节点,构建环形链表
* @param ids
*/
public void addNode(int ids){
if (ids < 1){
System.out.println("id不正确");
return;
}
HeroNode cur = null; // 辅助指针,帮助构建环形列表
// 使用for循环创建环形列表
for (int i = 1; i <= ids; i++) {
// 根据编号创建节点
HeroNode node = new HeroNode(i);
// 如果是第一个小孩
if (i == 1){
first = node;
// 构成环状
first.next = first;
// 让cur指向第一个节点
cur = first;
}else {
cur.next = node;
node.next = first;
cur = node;
}
}
}
/**
* 遍历当前环形列表
*/
public void showList(){
// 判断是否为空
if (first == null){
System.out.println("当前没有节点");
return;
}
// first指针不能动,需要引入一个辅助指针
HeroNode cur = first;
while (true){
System.out.println("当前英雄编号为:" + cur.no);
if (cur.next == first){
// 说明已经遍历结束
break;
}
cur = cur.next;
}
}
/**
* 根据用户的输入,计算出弹出节点的id顺序
* @param startId 从编号为startId开始数
* @param countNum 数几下
* @param ids 一共有多少个人
*/
public void countNode(int startId, int countNum, int ids){
// 先对数据进行校验
if (first == null || startId < 1 || startId > ids){
System.out.println("参数输入有误,请重新输入!");
return;
}
// 创建辅助指针
HeroNode helper = first;
while (true){
if (helper.next == first){
// 此时helper指向最后一个节点
break;
}
helper = helper.next;
}
// 让helper和first移动k-1次
for (int i = 0; i < startId - 1; i++) {
first = first.next;
helper = helper.next;
}
// 开始数,移动m-1次,然后弹出
// 这是一个循环操作,指导圈中还有一个节点
while (true){
if (helper == first){
// 说明环形只有一个节点
break;
}
// 开始移动first和helper
for (int i = 0; i < countNum - 1; i++) {
first = first.next;
helper = helper.next;
}
// 此时将first指向的节点弹出
System.out.println("弹出节点的编号为:" + first.no);
first = first.next;
helper.next = first;
}
System.out.println("最后剩下的节点编号为:" + first.no);
}
}
结果:2->4->1->5->3
2.双向链表
(1)简介双向链表
单向链表存在的局限性:
- 单向链表查找的方向只能是一个方向,而双向链表可以向前或者向后查找
- 单向列表不能自我删除,需要靠辅助节点(待删除节点的上一个节点),而双向链表可以自我删除
结构示意图:
(2)应用案例
思路分析:
- 遍历
- 和单向链表一样,可以向前,也可以向后
- 添加(默认添加到最后一个节点)
- 先找到双向链表的最后一个节点
- temp.next = newNode
- newNode.pre = temp
- 修改
- 和单向链表一样
- 删除
- 直接找到要删除的节点temp
- temp.pre.next = temp.next
- temp.next.pre = temp.pre
3.栈
(1)简介——栈
-
栈的英文为(stack)
-
栈是一个先入后出(FILO-First In Last Out)的有序列表。
-
栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
-
根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
(2)出栈和入栈
(3)应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
- 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth一first)搜索法。
(4)栈——代码实现
思路分析:
- 使用数组模拟栈
- 定义有一个top来表示栈,初始化为-1
- 入栈的操作:当有数据加入到栈,top++; stack[top]=data
- 出栈的操作:int value = stack[top]; top–; return value.
public 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(){
// 数组的length表示数据的尺寸,不表示存放数据的个数
return top == maxSize - 1;
}
// 栈空
public boolean isEmpty(){
return top == -1;
}
// 入栈
public void push(int num){
if (isFull()){
System.out.println("栈已满,无法push");
return;
}
top++;
stack[top] = num;
}
// 出栈
public int pop(){
if (isEmpty()){
throw new RuntimeException("栈空,无法pop");
}
int val = stack[top];
top--;
return val;
}
// 遍历栈(从栈顶往下遍历)
public void showList(){
if (isEmpty()){
System.out.println("栈空,没有数据");
return;
}
for (int i = top; i >= 0; i--){
System.out.printf("stack[%d]=%d\n", i, stack[top]);
}
}
}
(5)使用栈完成计算机表达式
思路:
- 通过指针index,遍历字符串表达式
- 如果发现一个数字就直接入数栈
- 如果发现扫描到一个符号时
- 如果此时符号栈有操作符,就进行比较
- 如果当前操作符的优先级小于或者等于栈中的操作符,就从数栈中pop出两个数,从符号栈中pop出一个符号,进行运算
- 如果当前操作符的优先级大于栈中的操作符,就直接入符号栈
- 如果此时符号栈有操作符,就进行比较
- 当表达式扫描完毕后,就顺序得从数栈和符号栈中pop出对应的数和符号,并运算
- 最后在数栈中只有一个数字,就是表达式结果
(5)前缀表达式
前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前
计算过程:
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果。
举例:(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,由此得出最终结果
(6)后缀表达式
后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后
计算过程:
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
例如: (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,由此得出最终结果
==案例:==设计一个逆波兰计算器
-
输入一个逆波兰表达式(后缀表达式),使用栈(Stack), 计算其结果
-
支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。
public class PolandNotation {
public static void main(String[] args) {
// 定义一个逆波兰表达式
// (3+4)×5-6 ---> 3 4 + 5 × 6 - // 29
// 4 * 5 - 8 + 60 + 8 / 2 ---> 4 5 * 8 - 60 + 8 2 / + // 76
String suffixExpression = "3 4 + 5 × 6 - ";
suffixExpression = "4 5 × 8 - 60 + 8 2 / + ";
// 1.先将表达式放到ArrayList中
// 2.将ArrayList传递给一个方法,遍历 ArrayList ,配合栈完成计算
List<String> stringList = getListString(suffixExpression);
// System.out.println(stringList);
System.out.println("计算结果为:" + calculate(stringList));
}
// 将表达式依次放入ArrayList中
public static List<String> getListString(String suffixExpression){
// 分割
String[] split = suffixExpression.split(" ");
return new ArrayList<>(Arrays.asList(split));
}
// 完成对逆波兰表达式的运算
/*
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> list){
// 创建栈
Stack<String> stack = new Stack<>();
// 遍历list
for (String item : list) {
// 正则表达式取出数字
if (item.matches("\\d+")){// 匹配的是多位数
//入栈
stack.push(item);
}else {
// pop出两个数,并运算,再入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
switch (item){
case "+":
res = num1 + num2;
break;
case "-":
// 注意顺序,num1是后弹出的数字
res = num1 - num2;
break;
case "×":
case "*":
res = num1 * num2;
break;
case "/":
res = num1 / num2;
break;
default:
throw new RuntimeException("运算符号有误");
}
stack.push(res + "");
}
}
// 最后留在stack中的数据就是运算结果
return Integer.parseInt(stack.pop());
}
}
(7)中缀表达式转换为后缀表达式
思路分析:
-
初始化两个栈:运算符栈s1和储存中间结果的栈s2;
-
从左至右扫描中缀表达式;
-
遇到操作数时,将其压s2;
-
遇到运算符时,比较其与s1栈顶运算符的优先级:
(1) 如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈s1;
(2) 否则,若优先级比栈顶运算符的高,也将运算符压入s1;
(3) 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较;
- 遇到括号时:
(1) 如果是左括号“(”,则直接压入s1
(2) 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
-
重复步骤2至5,直到表达式的最右边
-
将s1中剩余的运算符依次弹出并压入s2
-
依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
// 将中缀表达式对应的list转换为后缀表达式对应的list
public static List<String> parseSuffixExpression(List<String> list){
// 定义两个栈
Stack<String> s1 = new Stack<>(); // 符号栈
// 因为s2在整个过程中没有pop操作,且后续还需要逆序输出,故用List<String>代替
// Stack<String> s2 = new Stack<>(); // 储存中间结果的栈
List<String> s2 = new ArrayList<>();
// 遍历list
for (String item : list) {
// 如果是数就入栈
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();
}else {
// 遇到运算符时,要进行优先级比较
// 当item的优先级小于等于s1栈顶运算符,将s1栈顶运算符弹出并加入到s2中,再转到4.1与新的栈顶运算符对比
while (s1.size() != 0 && OperationUtils.getValue(s1.peek()) >= OperationUtils.getValue(item)){
s2.add(s1.pop());
}
// 将item压入栈中
s1.push(item);
}
}
while (s1.size() != 0){
s2.add(s1.pop());
}
return s2;
}