线性结构和非线性结构
- 线性结构:
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
- 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的(地址连续)
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的(地址不一定连续),元素节点中存放数据元素以及相邻元素的地址信息
- 线性结构常见的有:数组、队列、链表和栈
- 非线性结构
- 非线性结构包括:二维数组, 多维数组,广义表,树结构,图结构
稀疏数组和对列
1. 引例
-
问题描述:编写的五子棋程序中,有存盘退出和续上盘的功能。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SwBArbhO-1589021541023)(E:\Java学习资料\Java进阶笔记\图片\001.png)]
-
问题分析:因为该二维数组的很多值是默认值0,因此记录了很多没有意义的数据,所以我们应该用稀疏数组。
2. 稀疏数组SparseArray
基本介绍
-
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
-
稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
- 图例:
应用实例
使用稀疏数组,来保留类似引例中的二维数组(棋盘、地图等)。把稀疏数组存盘,并且可以重新恢复原来的二维数组。
-
思路:
-
二维数组转稀疏数组的思路
- 遍历原始的二维数组,得到有效数据的个数sum
- 根据sum就可以创建稀疏数组
sparseArr int[sum+ 1][3]
- 将二维数组的有效数据数据存入到稀疏数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ztGFl51X-1589021541025)(E:\Java学习资料\Java进阶笔记\图片\004.png)]
-
稀疏数组转原始的二维数组的思路
- 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的
chessArr2 = int[11][11]
- 在读取稀疏数组后几行的数据,并赋给原始的二维数组即可
- 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的
-
-
代码实现:
public class SparseArrayMain {
public static void main(String[] args) {
//这里我们的棋盘大小为11*11,1代表黑子,2代表蓝子,设置三个初始值
int[][] arr1 = new int[11][11];
arr1[1][2] = 1;
arr1[2][3] = 2;
arr1[4][5] = 2;
//打印原来的数组
System.out.println("原来的数组:");
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++){
System.out.printf("%d\t", arr1[i][j]);
}
System.out.println();
}
//计算有效数据个数
int sum = 0;
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) {
if(arr1[i][j] != 0){
sum++;
}
}
}
//创建稀疏数组
int[][] sparseArr = new int[sum + 1][3];
//给稀疏数组赋值
//这里我们设置一个计数器来控制稀疏数组行数的自增
int count = 0;
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) {
if(arr1[i][j] != 0){
count++;
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = arr1[i][j];
}
}
}
//打印稀疏数组
System.out.println("稀疏数组:");
for (int i = 0; i < sum + 1; i++) {
for (int j = 0; j < 3; j++) {
System.out.printf("%d\t", sparseArr[i][j]);
}
System.out.println();
}
//把稀疏数组还原为原来的数组
int[][] arr2 = new int[11][11];
//遍历稀疏数组进行赋值,给新数组进行赋值从第一行开始遍历
for (int i = 1; i < sum + 1; i++) {
for (int j = 0; j < 3; j++) {
arr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][j];
}
}
//打印还原后的数组
System.out.println("还原后的数组:");
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++){
System.out.printf("%d\t", arr2[i][j]);
}
System.out.println();
}
}
}
-------------------------------
运行结果:
原来的数组:
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 2 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
稀疏数组:
0 0 0
1 2 1
2 3 2
4 5 2
还原后的数组:
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 2 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
Process finished with exit code 0
-
补充练习
要求:
- 在前面的基础上,将稀疏数组保存到磁盘上,比如map.data
- 恢复原来的数组时,读取map.data进行恢复
3. 队列
队列介绍
队列是一个有序列表,可以用数组或是链表来实现。
遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
数组模拟队列
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图,其中maxSize是该队列的最大容量。因为队列的输出、输入是分别从前后端来处理,因此需要两个变量front及rear分别记录队列前后端的下标,front 会随着数据输出而改变,而rear则是随着数据输入而改变,如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ZC8nJZP-1589021541026)(E:\Java学习资料\Java进阶笔记\图片\005.png)]
-
思路分析:当我们将数据存入队列时称为”addQueue",addQueue 的处理需要有两个步骤:
- 将尾指针往后移: rear+1, 当front== rear [空]
- 若尾指针
rear
小于队列的最大下标maxSize-1
,则将数据存入rear所指的数组元素中,否则无法存入数据。
rear == maxSize - 1
[队列满]
-
代码实现:
import java.util.Scanner;
public class ArrayQueueMain {
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(3);
Scanner sc = new Scanner(System.in);
//为了方便演示,我们首先制作一个菜单
boolean b = true;
while (b){
System.out.println("s(show): 打印队列");
System.out.println("a(add): 添加元素");
System.out.println("o(out): 取出元素");
System.out.println("f(first): 获取队首元素");
System.out.println("e(exit): 退出程序");
System.out.println("请输入您要执行的操作:");
char c = sc.next().charAt(0);
switch (c){
case 's':
System.out.println("队列元素如下:");
queue.show();
break;
case 'a':
try {
System.out.println("请输入您要添加的元素值:");
int i = sc.nextInt();
queue.addQueue(i);
} catch (Exception e) {
e.printStackTrace();
}finally {
break;
}
case 'o':
try {
int i = queue.outQueue();
System.out.println("取出的元素值为:" + i);
} catch (Exception e) {
e.printStackTrace();
}finally {
break;
}
case 'f':
try {
int first = queue.getFirst();
System.out.println("对首元素为:" + first);
} catch (Exception e) {
e.printStackTrace();
}finally {
break;
}
case 'e':
b = false;
break;
default :
System.out.println("输入错误");
b = false;
break;
}
}
System.out.println("退出程序");
}
}
class ArrayQueue{
private int numMax;//数组队列的最多存储的元素个数
private int fount;//队头指针
private int rear;//队尾指针
private int[] arr;//队列数组
//创建队列的构造器
public ArrayQueue(int numMax){
arr = new int[numMax];
this.numMax = numMax;
fount = -1;
rear = -1;
}
//判断队列是否为空
public boolean isEmpty(){
return fount == rear;
}
//判断队列是否满
public boolean isFull(){
return rear == (numMax - 1);
}
//入队列
public void addQueue(int value) throws Exception {
//判断队列是否为满,如果为满就抛出异常
if(isFull()){
throw new Exception("队列已满,无法加入数据");
}
//否则,加入数据
rear++;
arr[rear] = value;
}
//出队列,并返回取出元素的值
public int outQueue() throws Exception {
//判断队列是否为空,如果为空抛出异常
if(isEmpty()){
throw new Exception("队列为空,无法进行出队操作");
}
//否则,出对列
fount++;
return arr[fount];
}
//返回队头元素
public int getFirst() throws Exception {
//判断队列是否为空,如果为空抛出异常
if(isEmpty()){
throw new Exception("队列为空,无法进行出队操作");
}
//否则返回队首元素
return arr[fount + 1];
}
//打印队列
public void show(){
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
- 上面的代码中出现的问题及其优化方案
- 目前数组使用一次就不能使用了,没有达到复用的效果
- 将这个数组使用相关算法,改进成一个环形数组(使用取模的方法(%))
数组模拟环形队列
目的:解决上一节中代码出现的问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gfOSKCPQ-1589021541028)(E:\Java学习资料\Java进阶笔记\图片\005.png)]
思路分析:
- front 变量的含义做一个调整:front就指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素,front的初始值为0
- rear变量的含义做一个调整:rear执行队列的最后一个元素的后一个位置,因为我们希望空出一个空间作为约定(这个空出来的位置不放置队列元素,也就是我们定义了一个有n个元素的数组,但队列中最多只能存放n-1个元素).rear的初始值为0。
- 当队列满时,条件是(rear + 1)%maxSize = front【满】
- 当队列为空的条件:rear == front【空】
- 当我们这样分析后,队列中有效的数据的个数:(rear + maxSize-front)%maxSize
- 这样我们就可以在原来的队列上修改得到一个环形队列
代码实现:
import java.util.Scanner;
public class ArrayCircleQueueMain {
public static void main(String[] args) {
CircleQueue queue = new CircleQueue(4);
Scanner sc = new Scanner(System.in);
//为了方便演示,我们首先制作一个菜单
boolean b = true;
while (b){
System.out.println("s(show): 打印队列");
System.out.println("a(add): 添加元素");
System.out.println("o(out): 取出元素");
System.out.println("f(first): 获取队首元素");
System.out.println("e(exit): 退出程序");
System.out.println("请输入您要执行的操作:");
char c = sc.next().charAt(0);
switch (c){
case 's':
System.out.println("队列元素如下:");
try {
queue.show();
} catch (Exception e) {
e.printStackTrace();
}
break;
case 'a':
try {
System.out.println("请输入您要添加的元素值:");
int i = sc.nextInt();
queue.add(i);
} catch (Exception e) {
e.printStackTrace();
}finally {
break;
}
case 'o':
try {
int i = queue.out();
System.out.println("取出的元素值为:" + i);
} catch (Exception e) {
e.printStackTrace();
}finally {
break;
}
case 'f':
try {
int first = queue.getFirst();
System.out.println("对首元素为:" + first);
} catch (Exception e) {
e.printStackTrace();
}finally {
break;
}
case 'e':
b = false;
break;
default :
System.out.println("输入错误");
b = false;
break;
}
}
System.out.println("退出程序");
}
}
class CircleQueue{
private int maxSize;
private int front;
private int rear;
private int[] arr;
//创建队列
public CircleQueue(int maxSize){
this.maxSize = maxSize;
arr = new int[maxSize];
front = 0;
rear = 0;
}
//判断队列是否为空
public boolean isEmpty(){
return front == rear;
}
//判断队列是否为满
public boolean isFull(){
return ((rear + 1) % maxSize) == front;
}
//入队
public void add(int num) throws Exception {
//判断队列是否为满
if(isFull()){
throw new Exception("队列为满,不能添加元素!!!");
}
//如果不为满,就添加元素到队列尾
arr[rear] = num;
rear = (rear + 1) % maxSize;
}
//出队
public int out() throws Exception {
//判断队列是否为空
if(isEmpty()){
throw new Exception("队列为空,不能取出元素");
}
//如果不为空,就取出队首元素
int value = arr[front];
front++;
return value;
}
//查看队首元素
public int getFirst() throws Exception {
//判断队列是否为空
if(isEmpty()){
throw new Exception("队列为空,不能取出元素");
}
//如果不为空,返回队首元素
return arr[front];
}
//打印队列
public void show() throws Exception {
//判断队列是否为空,如果为空,就无法打印
if(isEmpty()){
throw new Exception("队列为空");
}
//打印队列
for(int i = front; i < front + queueSize(); i++){
System.out.printf("arr[%d] = %d\n", i % maxSize, arr[i % maxSize]);
}
}
//求出当前队列有效数据的个数
public int queueSize(){
return (rear + maxSize - front) % maxSize;
}
}
链表(Linked List)
单向链表
- 链表是以节点的方式来存储的,是链式存储
- 每个节点包含data域,next域(指向下一个节点)
- 链表的各个节点不一定是连续存储
- 链表分带头结点的链表和没有头节点的链表,根据实际的需求来确定
带头节点单链表的逻辑结构示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qiPoEqlG-1589021541029)(E:\Java学习资料\Java进阶笔记\图片\006)]
head节点:
- 不存放具体的数据
- 作用就是表示单链表的表头next
应用实例
使用带head头的单向链表实现水浒英雄排行榜管理
- 完成对英雄人物的增删改查操作
- 第一种方式,在添加英雄时,直接添加到链表的尾部
- 第二种方式,在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
第一种方式
- 添加(创建)
- 先创建一个head头节点,作用就是表示单链表的头
- 后面我们每添加一个节点,就直接加入到链表的最后
- 遍历
- 通过一个辅助变量帮助遍历整个链表
代码实现:
public class SingleLinkedListDemo {
public static void main(String[] args) {
//创建一个单链表
SingleLinkedList list = new SingleLinkedList();
//创建4个节点
HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
HeroNode node3 = new HeroNode(3, "吴用", "智多星");
HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
//给链表添加节点
list.add(node1);
list.add(node3);
list.add(node4);
list.add(node2);
//打印链表
list.show();
}
}
class SingleLinkedList{
//创建头节点
HeroNode head = new HeroNode(0, null, null);
//在链表尾部添加元素
public void add(HeroNode heroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
//遍历链表, 如果遇到指针为空的节点,就在它后面添加元素,否则就把指针后移一位
while(true){
if(temp.next == null){
temp.next = heroNode;
break;
}
temp = temp.next;
}
}
//打印链表
public void show(){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
while(true){
if(temp.next == null){
break;
}
System.out.println(temp.next);
temp = temp.next;
}
}
}
class HeroNode{
public int no;
public String name;
public String nickname;
public HeroNode next;
//创建节点
public HeroNode(int no, String name, String nickname){
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "[" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
']';
}
}
-----------------------
运行结果:
[[no=1, name='宋江', nickname='及时雨']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
[no=2, name='卢俊义', nickname='玉麒麟']
Process finished with exit code 0
第二种方式
需要按照编号的顺序添加
- 首先找到新添加的节点的位置(通过辅助指针temp ),通过遍历
- 新的节点.next=temp.next
- 将temp.next=新的节点
代码实现:
public class SingleLinkedListDemo {
public static void main(String[] args) {
//创建一个单链表
SingleLinkedList list = new SingleLinkedList();
//创建4个节点
HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
HeroNode node3 = new HeroNode(3, "吴用", "智多星");
HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
//给链表添加节点
list.addByOrder(node1);
list.addByOrder(node3);
list.addByOrder(node4);
list.addByOrder(node2);
list.addByOrder(node2);
//打印链表
list.show();
}
}
class SingleLinkedList{
//创建头节点
HeroNode head = new HeroNode(0, null, null);
//按no的顺序添加元素,如果已经存在,相同的no,就不添加
public void addByOrder(HeroNode heroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
//定义一个flag来标记是否存在相同的节点,默认为false
boolean flag = false;
//遍历链表,如果找到指定的位置,就跳出循环
while(true){
if(temp.next == null){//说明已经到达链表的尾部
break;
}
if(temp.next.no > heroNode.no){//说明此时的temp是新元素的前一个位置,跳出循环,得到temp
break;
} else if(temp.next.no == heroNode.no){//说明该节点已经存在,不能添加,改变flag=false
flag = true;
break;
}else {
temp = temp.next;
}
}
//跳出循环之后,我们要做的第一件事就是对flag进行判断,如果flag=false,说明新节点可以添加,否则,不可添加
if(flag){
System.out.printf("no为%d的节点已经存在,不可重复添加\n", heroNode.no);
}else{
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//打印链表
public void show(){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
while(true){
if(temp.next == null){
break;
}
System.out.println(temp.next);
temp = temp.next;
}
}
}
class HeroNode{
public int no;
public String name;
public String nickname;
public HeroNode next;
//创建节点
public HeroNode(int no, String name, String nickname){
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "[" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
']';
}
}
------------------
运行结果:
no为2的节点已经存在,不可重复添加
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
Process finished with exit code 0
修改节点的信息
根据no编号来修改,即no编号不能改
代码实现及测试
public class SingleLinkedListDemo {
public static void main(String[] args) {
//创建一个单链表
SingleLinkedList list = new SingleLinkedList();
//创建4个节点
HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
HeroNode node3 = new HeroNode(3, "吴用", "智多星");
HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
//给链表添加节点
list.addByOrder(node1);
list.addByOrder(node3);
list.addByOrder(node4);
list.addByOrder(node2);
//list.addByOrder(node2);
//打印链表
list.show();
HeroNode node5 = new HeroNode(2, "小卢", "小玉");
System.out.println("修改后的链表为:");
list.update(node5);//修改编号为2的节点的信息
list.show();
}
}
class SingleLinkedList{
//创建头节点
HeroNode head = new HeroNode(0, null, null);
/* //在链表尾部添加元素
public void add(HeroNode heroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
//遍历链表, 如果遇到指针为空的节点,就在它后面添加元素,否则就把指针后移一位
while(true){
if(temp.next == null){
temp.next = heroNode;
break;
}
temp = temp.next;
}
}
*/
//按no的顺序添加元素,如果已经存在,相同的no,就不添加
public void addByOrder(HeroNode heroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
//定义一个flag来标记是否存在相同的节点,默认为false
boolean flag = false;
//遍历链表,如果找到指定的位置,就跳出循环
while(true){
if(temp.next == null){//说明已经到达链表的尾部
break;
}
if(temp.next.no > heroNode.no){//说明此时的temp是新元素的前一个位置,跳出循环,得到temp
break;
} else if(temp.next.no == heroNode.no){//说明该节点已经存在,不能添加,改变flag=false
flag = true;
break;
}else {
temp = temp.next;
}
}
//跳出循环之后,我们要做的第一件事就是对flag进行判断,如果flag=false,说明新节点可以添加,否则,不可添加
if(flag){
System.out.printf("no为%d的节点已经存在,不可重复添加\n", heroNode.no);
}else{
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//修改节点
public void update(HeroNode newHeroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head.next;
//定义一个flag来标识找到与否,默认为false
boolean flag =false;
//遍历链表,在指定情况下跳出循环
while (true){
if(temp == null){//到达链表末尾
break;
}
if(temp.no == newHeroNode.no){//说明找到了
flag = true;
break;
}else{
temp = temp.next;
}
}
//跳出循环后,对flag进行判断
if(flag){//找到了
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}else{
System.out.printf("没有找到编号%d的节点,无法修改!!!\n", newHeroNode.no);
}
}
//打印链表
public void show(){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
while(true){
if(temp.next == null){
break;
}
System.out.println(temp.next);
temp = temp.next;
}
}
}
class HeroNode{
public int no;
public String name;
public String nickname;
public HeroNode next;
//创建节点
public HeroNode(int no, String name, String nickname){
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "[" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
']';
}
}
------------------
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
修改后的链表为:
[no=1, name='宋江', nickname='及时雨']
[no=2, name='小卢', nickname='小玉']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
Process finished with exit code 0
单链表节点的删除
思路:
- 我们先找到需要删除的这个节点的前一个节点temp
- temp.next = temp.next.next
- 被删除的节点,将不会有其他引用指向,会被垃圾回收机制回收
代码示例:
public class SingleLinkedListDemo {
public static void main(String[] args) {
//创建一个单链表
SingleLinkedList list = new SingleLinkedList();
//创建4个节点
HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
HeroNode node3 = new HeroNode(3, "吴用", "智多星");
HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
//给链表添加节点
list.addByOrder(node1);
list.addByOrder(node3);
list.addByOrder(node4);
list.addByOrder(node2);
//list.addByOrder(node2);
//打印链表
list.show();
list.delete(1);
list.delete(4);
list.delete(2);
//打印删除后的
System.out.println("删除后的链表为:");
list.show();
}
}
class SingleLinkedList{
//创建头节点
HeroNode head = new HeroNode(0, null, null);
/* //在链表尾部添加元素
public void add(HeroNode heroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
//遍历链表, 如果遇到指针为空的节点,就在它后面添加元素,否则就把指针后移一位
while(true){
if(temp.next == null){
temp.next = heroNode;
break;
}
temp = temp.next;
}
}
*/
//按no的顺序添加元素,如果已经存在,相同的no,就不添加
public void addByOrder(HeroNode heroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
//定义一个flag来标记是否存在相同的节点,默认为false
boolean flag = false;
//遍历链表,如果找到指定的位置,就跳出循环
while(true){
if(temp.next == null){//说明已经到达链表的尾部
break;
}
if(temp.next.no > heroNode.no){//说明此时的temp是新元素的前一个位置,跳出循环,得到temp
break;
} else if(temp.next.no == heroNode.no){//说明该节点已经存在,不能添加,改变flag=false
flag = true;
break;
}else {
temp = temp.next;
}
}
//跳出循环之后,我们要做的第一件事就是对flag进行判断,如果flag=false,说明新节点可以添加,否则,不可添加
if(flag){
System.out.printf("no为%d的节点已经存在,不可重复添加\n", heroNode.no);
}else{
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//修改节点
public void update(HeroNode newHeroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head.next;
//定义一个flag来标识找到与否,默认为false
boolean flag =false;
//遍历链表,在指定情况下跳出循环
while (true){
if(temp == null){//到达链表末尾
break;
}
if(temp.no == newHeroNode.no){//说明找到了
flag = true;
break;
}else{
temp = temp.next;
}
}
//跳出循环后,对flag进行判断
if(flag){//找到了
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}else{
System.out.printf("没有找到编号%d的节点,无法修改!!!\n", newHeroNode.no);
}
}
//打印链表
public void show(){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
if(head.next == null){
System.out.println("链表为空");
}
while(true){
if(temp.next == null){
break;
}
System.out.println(temp.next);
temp = temp.next;
}
}
//删除节点
public void delete(int no){
HeroNode temp = head;
boolean flag = false;
while(true){
if(temp.next == null){
break;
}
if(temp.next.no == no){
flag = true;
break;
}else{
temp = temp.next;
}
}
if(flag){
temp.next = temp.next.next;
}else{
System.out.printf("要删除的%d节点不存在\n", no);
}
}
}
class HeroNode{
public int no;
public String name;
public String nickname;
public HeroNode next;
//创建节点
public HeroNode(int no, String name, String nickname){
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "[" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
']';
}
}
----------------------------
运行结果:
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
删除后的链表为:
[no=3, name='吴用', nickname='智多星']
Process finished with exit code 0
练习题
- 计算单链表的有效节点个数(如果是带头节点的链表,不统计头节点)
- 查找单链表中的倒数第k个节点
- 单链表的反转
- 从位到头打印单链表(用栈的方式)
- 合并两个有序的单链表,合并之后的链表依然有序(以后再实现)
代码实现及测试:
import java.util.Stack;
public class SingleLinkedListDemo {
public static void main(String[] args) {
//创建一个单链表
SingleLinkedList list = new SingleLinkedList();
//创建4个节点
HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
HeroNode node3 = new HeroNode(3, "吴用", "智多星");
HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
//给链表添加节点
list.addByOrder(node1);
list.addByOrder(node3);
list.addByOrder(node4);
list.addByOrder(node2);
//list.addByOrder(node2);
//打印链表
list.show();
list.delete(1);
//打印删除后的
System.out.println("删除后的链表为:");
list.show();
System.out.printf("节点的有效个数为:%d\n", getLength(list.getHead()));
//查找倒数第二个元素
HeroNode lastIndexNode = findLastIndexNode(list.getHead(), 2);
System.out.println("倒数第二个元素为:" + lastIndexNode);
//反转链表
reverse(list.getHead());
//打印反转后的链表
System.out.println("打印反转后的链表:");
list.show();
//在不改变链表结构的前提下,从尾到头打印单链表
System.out.println("从尾到头打印单链表,不改变链表结构");
reverseShow(list.getHead());
}
//计算单链表节点的个数
public static int getLength(HeroNode head) {//参数为头节点
HeroNode temp = head.next;
if (temp == null) {
return 0;
}
int length = 0;
while (temp != null) {
length++;
temp = temp.next;
}
return length;
}
//查找倒数第k个节点并返回
//1.编写一个方法,接收head节点,同时接收一个index
//2.index 表示的是倒数第index个节点
//3.先把链表从头到尾遍历,得到链表的总长度getLength()
//4.得到size后我们从链表的第一个开始遍历(size - index)个,就可以得到我们要找的节点
//5.如果找到了,则返回该节点,否则返回null
public static HeroNode findLastIndexNode(HeroNode head, int index){
if(head.next == null){
return null;
}
HeroNode temp = head.next;
if(index <= 0 || index > getLength(head)){
return null;
}
for(int i = 0; i < getLength(head) - index; i++){
temp = temp.next;
}
return temp;
}
//单链表的反转
//1.先定义一个节点 reverseHead = new HeroNode();
//2.从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前端
//3.原来的链表的head.next = reverseHead.next
public static void reverse(HeroNode head){
//如果该链表为空或者只有一个节点,那么就不用反转
if(head.next == null || head.next.next == null){
return;
}
HeroNode reverseHead = new HeroNode(0, "", "");
HeroNode cur = head.next;//指向当前节点
HeroNode next = null;//指向当前节点的下一个节点
while(cur != null){
next = cur.next;
cur.next = reverseHead.next;
reverseHead.next = cur;
cur = next;
}
head.next = reverseHead.next;
}
//逆序打印单链表,不改变链表的结构(使用栈)
public static void reverseShow(HeroNode head){
//如果链表为空,则不打印
if(head.next == null){
return;
}
Stack<HeroNode> heroNodes = new Stack<>();
HeroNode cur = head.next;
while(cur != null){
heroNodes.push(cur);
cur = cur.next;
}
//出栈,并打印
while(heroNodes.size() > 0){
System.out.println(heroNodes.pop());
}
}
}
class SingleLinkedList{
//创建头节点
private HeroNode head = new HeroNode(0, null, null);
public HeroNode getHead() {
return head;
}
/* //在链表尾部添加元素
public void add(HeroNode heroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
//遍历链表, 如果遇到指针为空的节点,就在它后面添加元素,否则就把指针后移一位
while(true){
if(temp.next == null){
temp.next = heroNode;
break;
}
temp = temp.next;
}
}
*/
//按no的顺序添加元素,如果已经存在,相同的no,就不添加
public void addByOrder(HeroNode heroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
//定义一个flag来标记是否存在相同的节点,默认为false
boolean flag = false;
//遍历链表,如果找到指定的位置,就跳出循环
while(true){
if(temp.next == null){//说明已经到达链表的尾部
break;
}
if(temp.next.no > heroNode.no){//说明此时的temp是新元素的前一个位置,跳出循环,得到temp
break;
} else if(temp.next.no == heroNode.no){//说明该节点已经存在,不能添加,改变flag=false
flag = true;
break;
}else {
temp = temp.next;
}
}
//跳出循环之后,我们要做的第一件事就是对flag进行判断,如果flag=false,说明新节点可以添加,否则,不可添加
if(flag){
System.out.printf("no为%d的节点已经存在,不可重复添加\n", heroNode.no);
}else{
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//修改节点
public void update(HeroNode newHeroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head.next;
//定义一个flag来标识找到与否,默认为false
boolean flag =false;
//遍历链表,在指定情况下跳出循环
while (true){
if(temp == null){//到达链表末尾
break;
}
if(temp.no == newHeroNode.no){//说明找到了
flag = true;
break;
}else{
temp = temp.next;
}
}
//跳出循环后,对flag进行判断
if(flag){//找到了
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}else{
System.out.printf("没有找到编号%d的节点,无法修改!!!\n", newHeroNode.no);
}
}
//打印链表
public void show(){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode temp = head;
if(head.next == null){
System.out.println("链表为空");
}
while(true){
if(temp.next == null){
break;
}
System.out.println(temp.next);
temp = temp.next;
}
}
//删除节点
public void delete(int no){
HeroNode temp = head;
boolean flag = false;
while(true){
if(temp.next == null){
break;
}
if(temp.next.no == no){
flag = true;
break;
}else{
temp = temp.next;
}
}
if(flag){
temp.next = temp.next.next;
}else{
System.out.printf("要删除的%d节点不存在\n", no);
}
}
}
class HeroNode{
public int no;
public String name;
public String nickname;
public HeroNode next;
//创建节点
public HeroNode(int no, String name, String nickname){
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "[" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
']';
}
}
----------------------------
测试结果:
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
删除后的链表为:
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
节点的有效个数为:3
倒数第二个元素为:[no=3, name='吴用', nickname='智多星']
打印反转后的链表:
[no=4, name='林冲', nickname='豹子头']
[no=3, name='吴用', nickname='智多星']
[no=2, name='卢俊义', nickname='玉麒麟']
从尾到头打印单链表,不改变链表结构
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
Process finished with exit code 0
双向链表
分析:
- 遍历:和单链表一样,只是可以向前,也可以向后查找
- 添加(默认添加到双向链表的最后):
- 先找到双向链表的最后这个节点
- temp.next = newHeroNode
- newHeroNode.pre = temp
- 修改:思路和原理与单链表一样
- 删除:
- 因为是双向链表,因此,我们可以实现自我删除某个节点
- 直接找到要删除的节点,比如temp
- temp.pre.next = temp.next
- temp.next.pre = temp.pre(要考虑temp是否是最后一个节点)
- 按顺序添加节点(以后实现)
代码实现:
public class DoubleLinkedListDemo {
public static void main(String[] args) {
DoubleLinkedList list = new DoubleLinkedList();
HeroNode2 node1 = new HeroNode2(1, "宋江", "及时雨");
HeroNode2 node3 = new HeroNode2(3, "吴用", "智多星");
HeroNode2 node4 = new HeroNode2(4, "林冲", "豹子头");
HeroNode2 node2 = new HeroNode2(2, "卢俊义", "玉麒麟");
list.add(node1);
list.add(node2);
list.add(node3);
list.add(node4);
System.out.println("初始的链表: ");
list.show();
//修改
HeroNode2 newHeroNode = new HeroNode2(4, "公孙胜", "入云龙");
list.update(newHeroNode);
System.out.println("修改之后的链表:");
list.show();
//删除
list.delete(1);
System.out.println("删除之后的链表:");
list.show();
}
}
class DoubleLinkedList {
//创建头节点
private HeroNode2 head = new HeroNode2(0, null, null);
//返回头节点
public HeroNode2 getHead() {
return head;
}
//打印链表
public void show(){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode2 temp = head;
if(head.next == null){
System.out.println("链表为空");
return;
}
while(true){
if(temp.next == null){
break;
}
System.out.println(temp.next);
temp = temp.next;
}
}
//遍历链表并在末尾添加元素
public void add(HeroNode2 heroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode2 temp = head;
//遍历链表, 如果遇到指针为空的节点,就在它后面添加元素,否则就把指针后移一位
while(true){
if(temp.next == null){
temp.next = heroNode;
break;
}
temp = temp.next;
}
temp.next = heroNode;
heroNode.pre = temp;
}
//修改节点,和单链表的方式一样
public void update(HeroNode2 newHeroNode){
//因为头节点不能动,所以我们需要定义一个变量来辅助我们遍历链表
HeroNode2 temp = head.next;
//定义一个flag来标识找到与否,默认为false
boolean flag =false;
//遍历链表,在指定情况下跳出循环
while (true){
if(temp == null){//到达链表末尾
break;
}
if(temp.no == newHeroNode.no){//说明找到了
flag = true;
break;
}else{
temp = temp.next;
}
}
//跳出循环后,对flag进行判断
if(flag){//找到了
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}else{
System.out.printf("没有找到编号%d的节点,无法修改!!!\n", newHeroNode.no);
}
}
//删除某个节点
public void delete(int no){
HeroNode2 temp = head.next;
boolean flag = false;
while(true){
if(temp == null){
break;
}
if(temp.no == no){
flag = true;
break;
}else{
temp = temp.next;
}
}
if(flag){
temp.pre.next = temp.next;
if(temp.next != null){
temp.next.pre = temp.pre;
}
}else{
System.out.printf("要删除的%d节点不存在\n", no);
}
}
}
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;
}
@Override
public String toString() {
return "[" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
']';
}
}
---------------------
测试结果:
初始的链表:
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='林冲', nickname='豹子头']
修改之后的链表:
[no=1, name='宋江', nickname='及时雨']
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='公孙胜', nickname='入云龙']
删除之后的链表:
[no=2, name='卢俊义', nickname='玉麒麟']
[no=3, name='吴用', nickname='智多星']
[no=4, name='公孙胜', nickname='入云龙']
Process finished with exit code 0
单向环形链表
以约瑟夫环为例。
约瑟夫环问题:
Josephu 问题为:设编号为1, 2, … n的n个小孩围坐一圈,约定编号为k (1<=k<=n) 的小孩从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到m的那个小孩又出列,依次类推,直到所有小孩出列为止,由此产生一个出队编号的序列。
提示:
用一个不带头结点的循环链表来处理Josephu问题:先构成个有n个结 点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
思路分析:
- 先创建第一个节点,让first执行该节点,并形成环形
- 后面当我们每创建一个新的节点,就把该节点加入到已有的环形链表中即可
遍历环形链表:
- 先让一个辅助指针(变量)curBoy,指向first节点
- 然后通过一个while循环遍历该环形链表即可,curBoy.next == first表示结束
定义一个方法,生成指定的出列顺序,步骤如下:
-
需要创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点
补充:小孩报数前,先让first和helper移动k - 1次
-
当小孩报数时,让first和helper指针同时移动m - 1次
-
这是就可以将first指向的小孩节点出圈
first = first.next
helper.next = first
原来first指向的节点就会没有任何引用,会被回收
代码实现:
public class Jusephu {
public static void main(String[] args) {
//创建一个环形链表
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
//添加五个小孩
circleSingleLinkedList.addBoys(5);
//遍历
circleSingleLinkedList.show();
//我们指定有5个小孩,从第2个小孩开始报数,m = 2
circleSingleLinkedList.jusephuNum(2, 2, 5);
}
}
//创建环形链表
class CircleSingleLinkedList{
private BoyNode first = null;
//在这个环形链表中添加一定数量的小孩,参数为小孩的数量
public void addBoys(int num){
if(num < 1){
System.out.println("输入不合法");
return;
}
BoyNode curBoy = null;
for (int i = 1; i <= num; i++) {
BoyNode boyNode = new BoyNode(i);
if(i == 1){
first = boyNode;
curBoy = boyNode;
first.setNext(first);
}else{
curBoy.setNext(boyNode);
boyNode.setNext(first);
curBoy = curBoy.getNext();
}
}
}
//遍历环形链表
public void show(){
//判断链表是否为空
if(first == null){
System.out.println("链表为空");
return;
}
//定义一个辅助变量
BoyNode temp = first;
while(true){
System.out.println(temp.getNo());
temp = temp.getNext();
if(temp == first){
return;
}
}
}
public void jusephuNum(int startNo, int countNum, int nums) {
//对数据的正确性进行校验
if (startNo < 1 || startNo > nums || countNum < 1 || countNum > nums) {
return;
}
//创建一个helper指针,指向最后一个节点
BoyNode helper = null;
BoyNode temp = first;
while (true) {
if (temp.getNext() == first) {
helper = temp;
break;
} else {
temp = temp.getNext();
}
}
//在进行报数前,我们需要把helper和first同时移动startNo - 1次
for (int i = 0; i < startNo - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//报数过程
while (true) {
if(first == helper){
//此时链表中还有一个节点,跳出循环
break;
}
//报数的过程,helper和first同时移动countNum - 1次,移动之后,first所指向的节点即是要出列的节点
for (int i = 0; i < countNum - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//让指定的小孩出链表
System.out.printf("编号为%d的小孩出列\n", first.getNo());
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后出列的小孩的编号为:%d", first.getNo());
}
}
//创建小孩节点
class BoyNode{
private int no;
private BoyNode next;
public BoyNode(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
@Override
public String toString() {
return "BoyNode{" +
"no=" + no +
'}';
}
public BoyNode getNext() {
return next;
}
public void setNext(BoyNode next) {
this.next = next;
}
}
-----------------------
测试结果:
1
2
3
4
5
编号为3的小孩出列
编号为5的小孩出列
编号为2的小孩出列
编号为1的小孩出列
最后出列的小孩的编号为:4
Process finished with exit code 0
栈(Stack)
- 栈的英文为(stack)
- 栈是一个先入后出(FILO-First In Last Out)的有序列表。
- 栈(stack)是限 制线性表中元素的插入和删除只能在线性表的同一端进 行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一 端, 称为栈底(Bottom)。
- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈项,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
数组模拟栈
思路:
- 定义一个top来表示栈顶,初始化为-1
- 入栈的操作,当有数据加入到栈时,top++;stack[top] = data
- 出栈的操作,int value = stack[top]; top–; return value
代码实现:
import java.util.Scanner;
public class ArrayStackDemo {
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(4);
String key = "";
boolean loop = true;
Scanner sc = new Scanner(System.in);
while(loop){
System.out.println("exit: 退出程序");
System.out.println("show: 显示栈数据");
System.out.println("pop: 出栈");
System.out.println("push: 入栈");
System.out.println("请输入您要执行的操作:");
key = sc.next();
switch (key){
case "exit":
loop = false;
break;
case "show":
stack.list();
break;
case "pop":
try {
int value = stack.pop();
System.out.println("出栈的数据为" + value);
}catch (Exception e){
e.printStackTrace();
}
break;
case "push":
System.out.println("请输入您要入栈的数据:");
int num = sc.nextInt();
stack.push(num);
break;
default:
break;
}
}
System.out.println("退出程序");
}
}
class ArrayStack{
private int maxSize;
private int top = -1;
private int[] arr;
//构造方法
public ArrayStack(int maxSize){
this.maxSize = maxSize;
arr = new int[this.maxSize];
}
//判断栈是否为空
public boolean isEmpty(){
return top == -1;
}
//判断栈是否满
public boolean isFull(){
return top == maxSize - 1;
}
//入栈
public void push(int num){
//判断栈是否满
if(isFull()){
System.out.println("栈满,无法入栈");
return;
}
top++;
arr[top] = num;
}
//出栈
public int pop(){
//判断栈是否为空
if(isEmpty()){
throw new RuntimeException("栈为空,无法出栈");
}
int v = arr[top];
top--;
return v;
}
//遍历栈(从栈顶开始显示数据)
public void list(){
//判断栈是否为空
if(isEmpty()){
System.out.println("栈为空");
return;
}
for(int i = top; i >= 0; i--){
System.out.printf("arr[%d] = %d\n", i, arr[i]);
}
}
}
链表模拟栈
(略)
栈实现综合计算器
只有加减乘除。
思路:
- 通过一个index值(索引),来遍历我们的表达式
- 如果我们发现是一个数字, 就直接入数栈
- 如果发现扫描到是一个符号,就分如下情况
- 如果发现当前的符号栈为空,就直接入栈
- 如果符号栈有操作符,就进行比较,如果当 前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数在从符号栈中pop出一个符号, 进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈,如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈.
- 当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运行.
- 最后在数栈只有一个数字,就是表达式的结果
代码实现:
public class Calculator {
public static void main(String[] args) {
String expression = "100*2+3*3*5-3/1";
//创建两个栈,一个数字栈,一个符号栈
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
//定义一些操作数
int index = 0;//用来扫描字符串
int num1;
int num2;
char ch;
int oper;
String k = "";
String p = "";//用来对表达式进行展望
while (true){
ch = expression.substring(index, index + 1).charAt(0);
//判断是否为运算符
if(operStack.isOper(ch)){
//判断符号栈是否为空
if(!operStack.isEmpty()){
//如果不为空
//判断当前操作符的优先级是否小于等于栈顶优先级
if(operStack.charClass(ch) <= operStack.charClass(operStack.peek())){
//数字栈中弹出两个数,符号栈中弹出栈定符号,进行运算
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
int res = numStack.cal(num1, num2, oper);
numStack.push(res);
//运算符入栈
operStack.push(ch);
}else{
//直接入栈
operStack.push(ch);
}
}else {
//符号栈为空,直接入栈
operStack.push(ch);
}
}else{
//不是运算符
k += ch;//如果下一位不是运算符,就会一直拼接下去
if(index == expression.length() - 1){
numStack.push(Integer.parseInt(k));
}else{
if(operStack.isOper(expression.substring(index + 1, index + 2).charAt(0))){
numStack.push(Integer.parseInt(k));
//清空k
k = "";
}
}
}
index++;
if(index >= expression.length()){
break;
}
}
//对最后两个栈中的元素进行判断,如果运算符栈为空,那么数字栈就只剩一个数字,即为结果
while(true){
if(operStack.isEmpty()){
break;
}
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
int res = numStack.cal(num1, num2, oper);
numStack.push(res);
}
//打印运算结果
System.out.println(expression + "=" + numStack.pop());
}
}
class ArrayStack2{
private int maxSize;
private int top = -1;
private int[] arr;
//构造方法
public ArrayStack2(int maxSize){
this.maxSize = maxSize;
arr = new int[this.maxSize];
}
//判断栈是否为空
public boolean isEmpty(){
return top == -1;
}
//判断栈是否满
public boolean isFull(){
return top == maxSize - 1;
}
//入栈
public void push(int num){
//判断栈是否满
if(isFull()){
System.out.println("栈满,无法入栈");
return;
}
top++;
arr[top] = num;
}
//出栈
public int pop(){
//判断栈是否为空
if(isEmpty()){
throw new RuntimeException("栈为空,无法出栈");
}
int v = arr[top];
top--;
return v;
}
//遍历栈(从栈顶开始显示数据)
public void list(){
//判断栈是否为空
if(isEmpty()){
System.out.println("栈为空");
return;
}
for(int i = top; i >= 0; i--){
System.out.printf("arr[%d] = %d\n", i, arr[i]);
}
}
//查看栈定元素
public int peek(){
return arr[top];
}
//判断是否为运算符
boolean isOper(char ch){
return ch == '+' || ch == '-' || ch == '*' || ch == '/';
}
//返回运算符的优先级,优先级越高,返回值越大
public int charClass(int ch){
if(ch == '*' || ch == '/'){
return 1;
}else if(ch == '+' || ch == '-'){
return 0;
}else {
return -1;
}
}
//表达式运算
public int cal(int num1, int num2, int oper){
if(oper == '+'){
return num1 + num2;
}else if(oper == '-'){
return num2 - num1;
}else if(oper == '*'){
return num1 * num2;
}else if(oper == '/'){
return num2 / num1;
}else {
return -1;
}
}
}
前缀、中缀、后缀表达式
前缀表达式(波兰表达式)
前缀表达式的计算机求值:
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个
数,用运算符对它们做相应的计算(栈项元素和次项元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果。
例如:(3+4)X5-6对应的前缀表达式就是- X +3456,针对前缀表达式求值步骤如下:
- 从右至左扫描,将6、5、4、3压入堆栈
- 遇到+运算符, 因此弹出3和4 (3为栈项元素,4为次项元素),计算出3+4的值,得7,再将7入栈
- 接下来是X运算符,因此弹出7和5,计算出7X5=35, 将35入栈
- 最后是-运算符,计算出35-6的值,即29,由此得出最终结果
中缀表达式
- 中缀表达式就是常见的运算表达式,如(3+4)X5-6
- 中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(前面我们讲的案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式)
后缀表达式
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素和栈项元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
例如:(3+4)X5-6对应的前缀表达式就是34+5 X 6-,针对后缀表达式求值步骤如下:
- 从左至右扫描, 将3和4压入堆栈;
- 遇到+运算符,因此弹出4和3 (4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
- 将5入栈;
- 接下来是X运算符, 因此弹出5和7,计算出7X5=35,将35入栈;
- 将6入栈;
- 最后是-运算符,计算出35-6的值, 即29, 由此得出最终结果
逆波兰计算器的实现
- 输入一个逆波兰表达式,使用栈(Stack), 计算其结果
- 支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。
- 思路分析
- 代码完成
代码实现:
import java.util.ArrayList;
import java.util.Stack;
public class PolandNotation {
public static void main(String[] args) throws Exception {
//先给定一个逆波兰表达式
String notation = "31 4 + 5 * 6 -";
//把给定的表达式存放在一个集合中
ArrayList<String> list = toList(notation);
System.out.println(list);
//给定一个集合,返回计算结果
int result = getResult(list);
System.out.println("计算结果为:" + result);
}
public static ArrayList<String> toList(String notation){
ArrayList<String> list = new ArrayList<>();
String[] s = notation.split(" ");
for(String e : s){
list.add(e);
}
return list;
}
public static int getResult(ArrayList<String> list) throws Exception {
Stack<String> stack = new Stack<>();
int num1;
int num2;
for(String s : list){
int res = 0;
if(s.matches("\\d+")){//匹配多位数(正则表达式)
stack.push(s);
}else if(s.equals("+")){
num1 = Integer.parseInt(stack.pop());
num2 = Integer.parseInt(stack.pop());
res = num2 + num1;
stack.push("" + res);
}else if(s.equals("-")){
num1 = Integer.parseInt(stack.pop());
num2 = Integer.parseInt(stack.pop());
res = num2 - num1;
stack.push("" + res);
}else if(s.equals("*")){
num1 = Integer.parseInt(stack.pop());
num2 = Integer.parseInt(stack.pop());
res = num2 * num1;
stack.push("" + res);
}else if(s.equals("/")){
num1 = Integer.parseInt(stack.pop());
num2 = Integer.parseInt(stack.pop());
res = num2 / num1;
stack.push("" + res);
}else {
throw new Exception("表达式有误!!!");
}
}
return Integer.parseInt(stack.pop());
}
}
中缀表达式转后缀表达式
思路分析:
-
初始化两个栈:运算符栈s1和储存中间结果的栈s2
-
从左至右扫描中缀表达式
-
遇到操作数时,将其压入s2
-
遇到运算符时,比较其与s1栈顶的优先级:
- 如果s1为空,或栈顶运算符为左括号”(“,则直接将次运算符入栈
2)否则,若优先级比栈顶运算符高,也将运算符压入s1
3)否则,将s1栈顶的运算符弹出并压入到s2中,再次转到1)与s1中新的栈顶运算符相比较
-
遇到括号时:
1)如果是左括号”(“,则直接压入s1
2)如果是右括号”)“,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
-
重复步骤2至5,直到表达式的最右边
-
将s1中剩余的运算符依次弹出并压入s2
-
依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
代码实现:
import java.util.ArrayList;
import java.util.Stack;
public class PolandNotationPlus {
public static void main(String[] args) throws Exception {
String expression = "1+((2+3)*4)-5";
//把中缀表达式字符串转换为一个存放各个数字和运算符的ArrayList
ArrayList<String> infixList = infixToList(expression);
System.out.println(infixList);
//把存放中缀表达式的集合转换为后缀表达式集合
ArrayList<String> list = infixToSuffixList(infixList);
System.out.println(list);
int result = getResult(list);
System.out.println(result);
}
//把中缀表达式字符串转换为一个存放各个数字和运算符的ArrayList
//方法如下:
public static ArrayList<String> infixToList(String expression){
ArrayList<String> list = new ArrayList<>();
int i = 0;
String key;
char c;
do{
if((c = expression.charAt(i)) < 48 || (c = expression.charAt(i)) > 57){
list.add("" + c);
i++;
}else{
key = "";
while(i < expression.length() && (c = expression.charAt(i)) >= 48 && (c = expression.charAt(i)) <= 57){
key += c;
i++;
}
list.add(key);
}
}while (i < expression.length());
return list;
}
//把存放中缀表达式的集合转换为后缀表达式集合
//方法如下
public static ArrayList<String> infixToSuffixList(ArrayList<String> list){
ArrayList<String> ls = new ArrayList<>();
Stack<String> s1 = new Stack<>();
for(String item : list){
if(item.matches("\\d+")){
ls.add(item);
}else if(item.equals("(")){
s1.push(item);
}else if(item.equals(")")){
while(!s1.peek().equals("(")){
ls.add(s1.pop());
}
s1.pop();
}else{
while (true){
if(s1.size() == 0 || s1.peek().equals("(")){
s1.push(item);
break;
}else if(Operation.oper(item) > Operation.oper(s1.peek())){
s1.push(item);
break;
}else{
ls.add(s1.pop());
}
}
}
}
while (s1.size() != 0){
ls.add(s1.pop());
}
return ls;
}
//输入后缀表达式,得出结果
public static int getResult(ArrayList<String> list) throws Exception {
Stack<String> stack = new Stack<>();
int num1;
int num2;
for(String s : list){
int res = 0;
if(s.matches("\\d+")){//匹配多位数(正则表达式)
stack.push(s);
}else if(s.equals("+")){
num1 = Integer.parseInt(stack.pop());
num2 = Integer.parseInt(stack.pop());
res = num2 + num1;
stack.push("" + res);
}else if(s.equals("-")){
num1 = Integer.parseInt(stack.pop());
num2 = Integer.parseInt(stack.pop());
res = num2 - num1;
stack.push("" + res);
}else if(s.equals("*")){
num1 = Integer.parseInt(stack.pop());
num2 = Integer.parseInt(stack.pop());
res = num2 * num1;
stack.push("" + res);
}else if(s.equals("/")){
num1 = Integer.parseInt(stack.pop());
num2 = Integer.parseInt(stack.pop());
res = num2 / num1;
stack.push("" + res);
}else {
throw new Exception("表达式有误!!!");
}
}
return Integer.parseInt(stack.pop());
}
}
//比较运算符优先级的类
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 oper(String s){
if(s.equals("+")){
return ADD;
}else if(s.equals("-")){
return SUB;
}else if(s.equals("*")){
return MUL;
}else{
return DIV;
}
}
}
逆波兰计算器完整版实现
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;
public class ReversePolishMultiCalc {
/**
* 匹配 + - * / ( ) 运算符
*/
static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";
static final String LEFT = "(";
static final String RIGHT = ")";
static final String ADD = "+";
static final String MINUS= "-";
static final String TIMES = "*";
static final String DIVISION = "/";
/**
* 加減 + -
*/
static final int LEVEL_01 = 1;
/**
* 乘除 * /
*/
static final int LEVEL_02 = 2;
/**
* 括号
*/
static final int LEVEL_HIGH = Integer.MAX_VALUE;
static Stack<String> stack = new Stack<>();
static List<String> data = Collections.synchronizedList(new ArrayList<String>());
/**
* 去除所有空白符
* @param s
* @return
*/
public static String replaceAllBlank(String s ){
// \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
return s.replaceAll("\\s+","");
}
/**
* 判断是不是数字 int double long float
* @param s
* @return
*/
public static boolean isNumber(String s){
Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
return pattern.matcher(s).matches();
}
/**
* 判断是不是运算符
* @param s
* @return
*/
public static boolean isSymbol(String s){
return s.matches(SYMBOL);
}
/**
* 匹配运算等级
* @param s
* @return
*/
public static int calcLevel(String s){
if("+".equals(s) || "-".equals(s)){
return LEVEL_01;
} else if("*".equals(s) || "/".equals(s)){
return LEVEL_02;
}
return LEVEL_HIGH;
}
/**
* 匹配
* @param s
* @throws Exception
*/
public static List<String> doMatch (String s) throws Exception{
if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");
s = replaceAllBlank(s);
String each;
int start = 0;
for (int i = 0; i < s.length(); i++) {
if(isSymbol(s.charAt(i)+"")){
each = s.charAt(i)+"";
//栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
if(stack.isEmpty() || LEFT.equals(each)
|| ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
stack.push(each);
}else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
//栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
if(calcLevel(stack.peek()) == LEVEL_HIGH){
break;
}
data.add(stack.pop());
}
stack.push(each);
}else if(RIGHT.equals(each)){
// ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
if(LEVEL_HIGH == calcLevel(stack.peek())){
stack.pop();
break;
}
data.add(stack.pop());
}
}
start = i ; //前一个运算符的位置
}else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
if(isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException("data not match number");
}
}
//如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));
System.out.println(data);
return data;
}
/**
* 算出结果
* @param list
* @return
*/
public static Double doCalc(List<String> list){
Double d = 0d;
if(list == null || list.isEmpty()){
return null;
}
if (list.size() == 1){
System.out.println(list);
d = Double.valueOf(list.get(0));
return d;
}
ArrayList<String> list1 = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
list1.add(list.get(i));
if(isSymbol(list.get(i))){
Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
list1.remove(i);
list1.remove(i-1);
list1.set(i-2,d1+"");
list1.addAll(list.subList(i+1,list.size()));
break;
}
}
doCalc(list1);
return d;
}
/**
* 运算
* @param s1
* @param s2
* @param symbol
* @return
*/
public static Double doTheMath(String s1,String s2,String symbol){
Double result ;
switch (symbol){
case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
default : result = null;
}
return result;
}
public static void main(String[] args) {
//String math = "9+(3-1)*3+10/2";
String math = "12.8 + (2 - 3.55)*4+10/5.0";
try {
doCalc(doMatch(math));
} catch (Exception e) {
e.printStackTrace();
}
}
}
.pop());
}
}
start = i ; //前一个运算符的位置
}else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
if(isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException(“data not match number”);
}
}
//如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));
System.out.println(data);
return data;
}
/**
* 算出结果
* @param list
* @return
*/
public static Double doCalc(List<String> list){
Double d = 0d;
if(list == null || list.isEmpty()){
return null;
}
if (list.size() == 1){
System.out.println(list);
d = Double.valueOf(list.get(0));
return d;
}
ArrayList<String> list1 = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
list1.add(list.get(i));
if(isSymbol(list.get(i))){
Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
list1.remove(i);
list1.remove(i-1);
list1.set(i-2,d1+"");
list1.addAll(list.subList(i+1,list.size()));
break;
}
}
doCalc(list1);
return d;
}
/**
* 运算
* @param s1
* @param s2
* @param symbol
* @return
*/
public static Double doTheMath(String s1,String s2,String symbol){
Double result ;
switch (symbol){
case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
default : result = null;
}
return result;
}
public static void main(String[] args) {
//String math = "9+(3-1)*3+10/2";
String math = "12.8 + (2 - 3.55)*4+10/5.0";
try {
doCalc(doMatch(math));
} catch (Exception e) {
e.printStackTrace();
}
}
}