线性结构和非线性结构
数据结构主要包括线性结构和非线性结构。
基本概念
线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
- 线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的(地址是连续的)。
- 链式存储的线性表称为链表,链表中得存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息
- 线性结构常见的有:数据、队列、链表和栈。
非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构
稀疏数组
基本概念
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法
1.记录数组一共有几行几列,有多少个不同的值
2.把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
举例说明
应用实例
1.使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
2. 把稀疏数组存盘,并且可以重新恢复原来的二维数组。
3. 思路分析:二维数组转稀疏数组的思路
- 遍历原始的二维数组,得到有效数据的个数
- 根据sum创建稀疏数组 sparseArr int[sum+1][3]
- 将二维数组的有效数据存入到稀疏数组
稀疏数组转原始的二维数组的思路
- 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组chessArr2=int[11][11]
2.在读取稀疏数组后几行的数据,并赋给原始的二维数组。
代码实例
//稀疏数组
public class sparsearray {
public static void main(String[] args) {
//创建一个原始的二维数组11*11
// 0:表示没有棋子 1:表示黑子 2:表示白子
int nums[][] = new int[11][11];
nums[1][2] = 1;
nums[2][3] = 2;
//输出原始的二维数组
System.out.println("原始的二维数组:");
for (int[] row : nums) {
for (int data : row) {
System.out.print(data + " ");
}
System.out.println();
}
//将二维数组转稀疏数组的思路
//1. 先遍历二维数组 得到非0数据的个数
int sum = 0;
for (int[] row : nums) {
for (int data : row) {
if (data != 0) {
sum++;
}
}
}
//2. 创建对应的稀疏数组
int sparseArray[][] = new int[sum + 1][3];
//给稀疏数组赋值
sparseArray[0][0] = nums.length;
sparseArray[0][1] = nums[0].length;
sparseArray[0][2] = sum;
//遍历二维数组,将非0数据存放到sparseArray数组中
int count = 1;
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < nums[i].length; j++) {
if (nums[i][j] != 0) {
sparseArray[count][0] = i;
sparseArray[count][1] = j;
sparseArray[count][2] = nums[i][j];
count++;
}
}
}
//输出稀疏数组的形式
System.out.println();
System.out.println("得到稀疏数组为~~~~~");
for (int[] ints : sparseArray) {
for (int data : ints) {
System.out.print(" " + data + " ");
}
System.out.println();
}
//1.先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组
int newNums[][] = new int[sparseArray[0][0]][sparseArray[0][1]];
//2. 在读取稀疏数组后几行的数据,并赋给原始的二维数组即可
for (int i = 1; i < sparseArray.length; i++) {
newNums[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
}
for (int[] row : newNums) {
for (int data : row) {
System.out.print(data + " ");
}
System.out.println();
}
}
}
队列
基本介绍
- 队列是一个有序列表,可以使用数组或是链表来实现。
- 遵循先入先出的原则,即:先存入队列的数据,要先取出。后存入的要后取出
示意图
示意图说明
- rear代表队列的尾部,初始化为-1。
- front代表队列的头部,初始化为-1。
- 当数据加入的时候,rear+1,front不会变化。
- 当数据减少的时候,front-1,rear不会变化。
- 加数据是在队列的尾部添加,而取数据是在队列的头部取出。
数组模拟队列
思路分析
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如上图,其中maxSize是该队列的最大容量。
- 因为队列的输出、输入是分别从前后端来处理的,因此需要两个变量front及rear分别记录队列前后端的下标,front会随着数据输出而改变,而rear泽斯随着数据输入而改变。
- 当我们将数据存入队列时称为"addQueue",addQueue的处理需要有两个步骤:
- 将尾指针往后移:rear+1,当front==rear【空】
- 若尾指针rear小于队列的最大下标maxSize=1,则将数据存入rear所指的数组元素中,否则无法存入数据。rear==maxSize-1【队列满】
代码实现
public class ArrayQueueDemo {
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(3);
queue.addQueue(1);
System.out.println(queue.headQueue());
System.out.println(queue.getQueue());
}
}
//使用数组模拟队列-编写一个ArrayQueue
class ArrayQueue {
private int maxSize; //表示数组的最大容量
private int front; //队列头
private int rear; //队列尾
private int[] arr; //该数组用于存放数据,模拟队列
//创建队列的构造器
public ArrayQueue(int arrMaxSize) {
this.maxSize = arrMaxSize;
this.arr = new int[this.maxSize];
this.front = -1; //指向队列头部,分析出front是指向队列头的前一个位置
this.rear = -1; //指向队列尾,指向队列尾的数据(即就是队列最后一个数据)
}
//判断队列是否满
public boolean isFull() {
return rear == maxSize - 1;
}
//判断队列是否为空
public boolean isEmpty() {
return rear == front;
}
//添加数据到队列
public void addQueue(int n) {
if (isFull()) {
System.out.println("队列已满无法添加新的数据");
return;
}
arr[++rear] = n;
}
//获取队列中的数据,出队列
public int getQueue() {
if (isEmpty()) {
//抛出异常
throw new RuntimeException("队列空,不能取数据");
}
return arr[++front];
}
//显示队列中的所有数据
public void showQueue() {
if (isEmpty()) {
System.out.println("队列为空,没有数据");
}
for (int i = 0; i < arr.length; i++) {
System.out.println("arr[" + i + "]==" + arr[i]);
}
}
//显示队列的头数据,注意不是取数据
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列空,不能取数据");
}
return arr[front + 1];
}
}
数组模拟环形队列
思路分析
- front变量的含义做一个调整:front就指向队列的第一个元素,也就是说arr[front] 就是队列的第一个元素,front的初始值=0
- rear变量的含义做一个调整:rear指向队列的最后一个元素的后一个位置.因为希望空出一个空间作为约定.
- 当队列满时,条件是 (rear+1)%maxSize=front【满】,rear的初始值=0
- 当队列空时,条件是rear==front空
- 队列中有效地数据的个数是 (rear+maxSize-front)%maxSize
代码实现
public class CircleArrayQueueDemo {
public static void main(String[] args) {
System.out.println("测试数组模拟环形队列~");
CircleArray queue = new CircleArray(4);//有效数据为3个,因为希望腾出一个空间作为约定。rear+1为了数组不越界,且永远指向元素的后一个位置。
queue.addQueue(10);
queue.showQueue();
queue.addQueue(20);
queue.showQueue();
queue.addQueue(30);
queue.showQueue();
}
}
class CircleArray {
private int maxSize; //表示数组的最大容量
//front变量的含义做一个调整:front就指向队列的第一个元素,也就是说arr[front] 就是队列的第一个元素,
// front的初始值=0
private int front; //队列头
// rear变量的含义做一个调整:rear指向队列的最后一个元素的后一个位置.因为希望空出一个空间作为约定.
// rear变量的初始值 = 0;
private int rear; //队列尾
private int[] arr; //该数组用于存放数据,模拟队列
public CircleArray(int arrMaxSize) {
this.maxSize = arrMaxSize;
this.arr = new int[this.maxSize];
}
//判断队列是否满
public boolean isFull() {
return (rear + 1) % maxSize == front;
}
public boolean isEmpty() {
return rear == front;
}
//添加数据到队列
public void addQueue(int n) {
if (isFull()) {
System.out.println("队列已满无法添加新的数据");
return;
}
arr[rear] = n;
//将rear后移,这里必须考虑取模
rear = (rear + 1) % maxSize;
}
//获取队列中的数据,出队列
public int getQueue() {
if (isEmpty()) {
//抛出异常
throw new RuntimeException("队列空,不能取数据");
}
//front指向第一个元素
//1. 先把front对应的值保留到一个临时变量
//2. 将front后移
//3. 将临时保存的变量返回
int value = arr[front];
front = (front + 1) % maxSize;
return value;
}
public void showQueue() {
if (isEmpty()) {
System.out.println("队列为空,没有数据");
return;
}
//思路:从front开始遍历,遍历多少个元素
//动脑筋
for (int i = front; i < front + size(); i++) {
System.out.println("arr[" + i % maxSize + "]==" + arr[i % maxSize]);
}
}
//求出当前队列有效数据的个数
public int size() {
return (rear + maxSize - front) % maxSize;
}
//显示队列的头数据,注意不是取数据
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列空,不能取数据");
}
return arr[front];
}
}
链表
链表(Linked List)介绍
- 链表是以节点的方式来存储的
- 每个节点包含data域,next域指向下一个节点.
- 链表的各个节点不一定是连续存储
- 链表分带头节点的链表和没有头结点的链表,根据实际的需求来确定
添加节点-直接在链表尾部添加
思路分析
示意图说明
- 添加(创建)
- 先创建一个head头节点,作用就是表示单链表的头
- 后面每添加一个节点,就直接加入到链表的最后
- 遍历:
- 通过一个辅助变量,来方便遍历整个单链表。
代码实例
public class SingleLinkedListDemo {
public static void main(String[] args) {
//进行测试
//先创建节点
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
SingleLinkedList list = new SingleLinkedList();
list.add(hero1);
list.add(hero2);
list.add(hero3);
list.add(hero4);
list.list();
}
}
//定义SingleLinkedList 管理我们的英雄
class SingleLinkedList {
// 先初始化一个头节点,头节点不要动
// 1.不存放具体的数据
// 2.作用就是表示单链表头next
private HeroNode head = new HeroNode(0, "", "");
//添加节点到单向链表
//思路:当不考虑编号顺序时
//1. 找到当前链表的最后节点
//2. 将最后这个节点的next 指向新的节点
public void add(HeroNode heroNode) {
//因为head节点不能动,因此需要一个辅助指针temp
//相当于有一个temp指针指向头节点.
HeroNode temp = head;
//遍历链表,找到最后
while (true) {
//找到链表的最后
if (temp.next == null) {
break;
}
//如果没有找到,就将temp后移;
temp = temp.next;
}
//当退出while循环后,temp就指向了链表最后一个元素
temp.next = heroNode;
}
//显示链表[遍历]
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,因此我们需要一个辅助变量来遍历
HeroNode temp = head;
while (true) {
if (temp == null) {
break;
}
//输出节点的信息
System.out.println(temp);
//需要注意,将temp后移
temp = temp.next;
}
}
}
//定义HeroNode,每个HeroNode对象就是一个节点
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;
}
//为了显示方便,我们重新toString
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
添加节点-根据id插入到链表中
思路分析
代码实例
public class SingleLinkedListDemo {
public static void main(String[] args) {
//进行测试
//先创建节点
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
SingleLinkedList list = new SingleLinkedList();
//加入按照编号的顺序
list.addByOrder(hero4);
list.addByOrder(hero3);
list.addByOrder(hero1);
list.addByOrder(hero2);
list.list();
}
}
//定义SingleLinkedList 管理我们的英雄
class SingleLinkedList {
// 先初始化一个头节点,头节点不要动
// 1.不存放具体的数据
// 2.作用就是表示单链表头next
private HeroNode head = new HeroNode(0, "", "");
// 第二种方式根据id顺序插入到链表中
// 如果已经有该id,则添加失败,并给出提示
public void addByOrder(HeroNode heroNode) {
//因为head节点不能动,因此需要一个辅助指针temp
//相当于有一个temp指针指向头节点.
HeroNode temp = head;
boolean flag = false; //标识添加的编号是否存在,默认为false
while (true) {
if (temp.next == null) {//说明temp已经在链表的最后
break;
}
if (temp.next.no > heroNode.no) {//位置找到,就在temp的后面插入
break;
} else if (temp.next.no == heroNode.no) {
//说明希望添加的heroNode的编号已然存在
flag = true;
break;
}
temp = temp.next; //后移,遍历当前链表
}
//判断flag的值
if (flag) {//不能添加,说明id已经存在
System.out.println("准备插入的英雄的编号:" + heroNode.no + " 已经存在,不能加入");
} else {
//插入到链表中.
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//显示链表[遍历]
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,因此我们需要一个辅助变量来遍历
HeroNode temp = head;
while (true) {
if (temp == null) {
break;
}
//输出节点的信息
System.out.println(temp);
//需要注意,将temp后移
temp = temp.next;
}
}
}
//定义HeroNode,每个HeroNode对象就是一个节点
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;
}
//为了显示方便,我们重新toString
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
修改节点-根据no修改name和nickname
实例代码
public class SingleLinkedListDemo {
public static void main(String[] args) {
//进行测试
//先创建节点
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
SingleLinkedList list = new SingleLinkedList();
//加入按照编号的顺序
list.addByOrder(hero4);
list.addByOrder(hero3);
list.addByOrder(hero1);
list.addByOrder(hero2);
list.addByOrder(hero2);
list.list();
System.out.println("==================修改前==================");
HeroNode hero5 = new HeroNode(2, "小卢", "麒麟");
list.update(hero5);
list.list();
}
}
//定义SingleLinkedList 管理我们的英雄
class SingleLinkedList {
// 先初始化一个头节点,头节点不要动
// 1.不存放具体的数据
// 2.作用就是表示单链表头next
private HeroNode head = new HeroNode(0, "", "");
// 第二种方式根据id顺序插入到链表中
// 如果已经有该id,则添加失败,并给出提示
public void addByOrder(HeroNode heroNode) {
//因为head节点不能动,因此需要一个辅助指针temp
//相当于有一个temp指针指向头节点.
HeroNode temp = head;
boolean flag = false; //标识添加的编号是否存在,默认为false
while (true) {
if (temp.next == null) {//说明temp已经在链表的最后
break;
}
if (temp.next.no > heroNode.no) {//位置找到,就在temp的后面插入
break;
} else if (temp.next.no == heroNode.no) {
//说明希望添加的heroNode的编号已然存在
flag = true;
break;
}
temp = temp.next; //后移,遍历当前链表
}
//判断flag的值
if (flag) {//不能添加,说明id已经存在
System.out.println("准备插入的英雄的编号:" + heroNode.no + " 已经存在,不能加入");
} else {
//插入到链表中.
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//修改节点的信息,根据no编号修改,即no编号不能修改.
//说明
//1. 根据newHeroNode的no来修改即可
public void update(HeroNode heroNode) {
if (head.next == null) {
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
HeroNode temp = head;
boolean flag = false;
while (true) {
if (temp == null) { //说明temp指向了链表的最后一个节点
break;
}
if (temp.no == heroNode.no) {
flag = true;
break;
}
temp = temp.next;
}
//根据flag 判断是否找到要修改的节点
if (flag) {
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
} else { //没有找到
System.out.println("没有找到编号:" + heroNode.no);
}
}
//显示链表[遍历]
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,因此我们需要一个辅助变量来遍历
HeroNode temp = head;
while (true) {
if (temp == null) {
break;
}
//输出节点的信息
System.out.println(temp);
//需要注意,将temp后移
temp = temp.next;
}
}
}
//定义HeroNode,每个HeroNode对象就是一个节点
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;
}
//为了显示方便,我们重新toString
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
删除节点-根据no删除节点
思路分析
代码实例
public class SingleLinkedListDemo {
public static void main(String[] args) {
//进行测试
//先创建节点
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
SingleLinkedList list = new SingleLinkedList();
//加入按照编号的顺序
list.addByOrder(hero4);
list.addByOrder(hero3);
list.addByOrder(hero1);
list.addByOrder(hero2);
list.addByOrder(hero2);
list.list();
System.out.println("==================删除前==================");
list.del(1);
list.list();
}
}
//定义SingleLinkedList 管理我们的英雄
class SingleLinkedList {
// 先初始化一个头节点,头节点不要动
// 1.不存放具体的数据
// 2.作用就是表示单链表头next
private HeroNode head = new HeroNode(0, "", "");
// 第二种方式根据id顺序插入到链表中
// 如果已经有该id,则添加失败,并给出提示
public void addByOrder(HeroNode heroNode) {
//因为head节点不能动,因此需要一个辅助指针temp
//相当于有一个temp指针指向头节点.
HeroNode temp = head;
boolean flag = false; //标识添加的编号是否存在,默认为false
while (true) {
if (temp.next == null) {//说明temp已经在链表的最后
break;
}
if (temp.next.no > heroNode.no) {//位置找到,就在temp的后面插入
break;
} else if (temp.next.no == heroNode.no) {
//说明希望添加的heroNode的编号已然存在
flag = true;
break;
}
temp = temp.next; //后移,遍历当前链表
}
//判断flag的值
if (flag) {//不能添加,说明id已经存在
System.out.println("准备插入的英雄的编号:" + heroNode.no + " 已经存在,不能加入");
} else {
//插入到链表中.
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//删除节点
//思路
//1. head 不能动,因此我们需要一个temp辅助节点找到待删除节点的前一个节点
//2. 说明在比较时,是temp.next.no 和需要删除的节点的比较
public void del(int no) {
if (head.next == null) {
System.out.println("该链表为空");
return;
}
//指定一个辅助节点
HeroNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) { //说明temp指向了链表的最后一个节点
break;
}
if (temp.next.no == no) {
//找到的待删除节点的前一个节点temp
flag = true;
break;
}
temp = temp.next;
}
//判断flag
if (flag) {
temp.next = temp.next.next;
} else {
System.out.println("要删除的节点no=" + no + "不存在");
}
}
//显示链表[遍历]
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,因此我们需要一个辅助变量来遍历
HeroNode temp = head;
while (true) {
if (temp == null) {
break;
}
//输出节点的信息
System.out.println(temp);
//需要注意,将temp后移
temp = temp.next;
}
}
}
//定义HeroNode,每个HeroNode对象就是一个节点
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;
}
//为了显示方便,我们重新toString
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
单链表基本面试题
求单链表中节点得个数
public class SingleLinkedListDemo {
//方法:获取到单链表得节点得个数(如果是带头节点得链表,需求不统计头节点)
/**
* @param head 链表得头节点
* @return 返回得就是有效节点得个数
*/
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个节点
public class SingleLinkedListDemo {
//查找单链表中得倒数第k个节点
//思路
//1. 编写一个方法,接受head节点,同时接收一个index
//2. index 表示是倒数第index个节点
//3. 先把链表从头到尾遍历,得到链表得总的长度
//4. 得到size后,我们从链表得第一个开始遍历(size-index)个,就可以得到
//5. 如果找到了,则返回该节点,否则返回null
public static HeroNode findLastIndexNode(HeroNode head, int index) {
//判断如果链表为空,返回null
if (head.next == null) {
return null;
}
//第一次遍历得到链表得长度(节点个数)
int size = getLength(head);
//第二次遍历 size-index 位置,就是我们倒数得第K个节点
//先做一个index 得校验
if (index < 0 || index > size) {
return null;
}
//定义辅助变量
HeroNode cur = head.next;
int length = 0;
while (cur != null) {
if (length == size - index) {
break;
}
length++;
cur = cur.next;
}
return cur;
}
}
单链表得反转
示意图
思路分析
- 先定义一个节点 reverseHead=new HeroNode();
- 从头到尾遍历原来得链表,每遍历一个节点,就将其取出,并放在新的链表得最前端。
- 原来得链表得head.next=reverseHead.next
代码实例
//将单链表反转
public class SingleLinkedListDemo {
public static void reversetList(HeroNode head) {
//如果当前链表为空,或者只有一个节点,无需反转,直接返回
if (head.next == null || head.next.next == null) {
return;
}
//定义一个辅助指针(变量),帮助我们遍历原来得链表
HeroNode cur = head.next;
HeroNode next = null; //指向当前节点得下一个节点
HeroNode reverseHead = new HeroNode(0, "", "");
//遍历原来得链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead得最前端
//头插法
while (cur != null) {
next = cur.next;//先暂时保存当前节点得下一个节点,因为后面需要使用
System.out.println(cur.next);
//这里将cur得下一个节点指向reverseHead得第一个节点,此时cur链表已经断开,next=cur.next起到作用,cur=next,则是将链表重新连接
cur.next = reverseHead.next;//将cur得下一个节点指向新的链表得最前端
reverseHead.next = cur;//将 cur连接到新的链表.
System.out.println(reverseHead.next);
cur = next; //让cur后移
}
//将head.next 指向reverseHead.next,实现单链表的反转
head.next = reverseHead.next;
}
}
从尾到头打印单链表
思路分析
- 方式1:先将单链表进行反转操作,然后再遍历即可,这样做会破坏原来的链表结构,不建议。
- 方式2:可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,实现逆序打印的效果
代码实例
//方式2:
//可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
public class SingleLinkedListDemo {
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;
}
while (stack.size()>0){
System.out.println(stack.pop());
}
}
}
合并两个有序单链表,合并之后的链表依然有序
待更新
双向链表
简要分析单链表和双向链表的区别
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要依靠辅助节点,而双向链表则可以自我删除,所以前一章单链表删除节点时,总是找到temp(temp是待删除节点的前一个节点)的下一个节点来删除的。
示意图
思路分析
分析双向链表的遍历,添加,修改,删除的操作思路===> 代码实现
- 遍历方式和单链表一样,只是可以向前,也可以向后查找
- 添加(默认添加到双向链表的最后)
- 先找到双向链表的最后这个节点
- temp.next=lastNode (lastNode 是要插入链表尾部的节点)
- lastNode.pre=temp (lastNode的pre要指向链表的前一个节点)
- 修改思路和原先的单向链表一样
- 删除
- 因为是双向链表,因此,我们可以实现自我删除某个节点。
- 直接找到要删除的这个节点,比如temp
- temp.pre.next=temp.next
- temp.next.pre=temp.pre
代码实例
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);
doubleLinkedList.list();
// 删除
doubleLinkedList.del(4);
doubleLinkedList.list();
}
}
//创建一个双向链表的类
class DoubleLinkedList {
// 先初始化一个头节点,头节点不要动
// 1.不存放具体的数据
// 2.作用就是表示单链表头next
private HeroNode2 head = new HeroNode2(0, "", "");
//返回头节点
public HeroNode2 getHead() {
return head;
}
//显示链表[遍历]
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
}
//因为头节点不能动,因此我们需要一个辅助变量来遍历
HeroNode2 temp = head;
while (true) {
if (temp == null) {
break;
}
//输出节点的信息
System.out.println(temp);
//需要注意,将temp后移
temp = temp.next;
}
}
//添加一个节点到双向链表得最后
public void add(HeroNode2 heroNode) {
//因为head节点不能动,因此需要一个辅助指针temp
//相当于有一个temp指针指向头节点.
HeroNode2 temp = head;
//遍历链表,找到最后
while (true) {
//找到链表的最后
if (temp.next == null) {
break;
}
//如果没有找到,就将temp后移;
temp = temp.next;
}
//当退出while循环后,temp就指向了链表最后一个元素
//让所添加得节点得pre指向前一个节点形成一个双向链表
temp.next = heroNode;
heroNode.pre = temp;
}
//修改一个节点得内容,可以看到双向链表得节点内容修改和单向链表一样
public void update(HeroNode2 heroNode) {
if (head.next == null) {
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
HeroNode2 temp = head;
boolean flag = false;
while (true) {
if (temp == null) { //说明temp指向了链表的最后一个节点
break;
}
if (temp.no == heroNode.no) {
flag = true;
break;
}
temp = temp.next;
}
//根据flag 判断是否找到要修改的节点
if (flag) {
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
} else { //没有找到
System.out.println("没有找到编号:" + heroNode.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) { //说明temp指向了链表的最后一个节点
break;
}
if (temp.no == no) {
//找到的待删除节点的前一个节点temp
flag = true;
break;
}
temp = temp.next;
}
//判断flag
if (flag) {
temp.pre.next = temp.next;
//如果是最后一个节点,就不需要执行下面这句话,否则会出现空指针
if (temp.next != null) {
temp.next.pre = temp.pre;
}
} else {
System.out.println("要删除的节点no=" + no + "不存在");
}
}
}
//定义HeroNode,每个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 + '\'' +
'}';
}
}
单向环形链表
基本介绍
Josephu(约瑟夫环)问题
Josephu 问题为:设编号为1,2,…得n个人围坐在一圈,约定编号为k(1<=k<=n)得人从1开始报数,数到m得那个人出列,它得下一位又从1开始报数,数到m得那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号得序列。
提示:用一个不带头节点得循环链表来处理Josephu问题:先构成一个有n个结点得单循环链表,然后由k节点起从1开始技术,记到m时,对应节点从链表中删除,然后再从被删除节点得下一个节点又从1开始计数,直到最后一个节点从链表中删除算法结束。
示意图
出队列得顺序
2号出队列
4号出队列
1号出队列
5号出队列
思路分析
构建一个单向得环形链表思路
- 先创建第一个节点,让first指向该节点,并形成环形
- 后门当每创建一个新的节点,就把该节点加入到已有得环形链表中即可
遍历环形链表
- 先让一个辅助指针(变量)curBoy,指向first节点
- 然后通过一个while循环遍历该环形链表即可 curBoy.next == first 结束
根据用户得输入,生成一个节点出环形得顺序
- 需求创建一个辅助指针(变量)helper,事先应该指向环形链表得最后这个节点。
补充:节点报数前,先让first和helper移动k-1次
- 当遍历m次时,让first和helper指针同时得移动m-1次
- 这时就可以将first指向得节点出圈
first=first.next
helper.next=first
原来first指向得节点就没有任何引用,就会被回收
代码实例
public class Josephu {
public static void main(String[] args) {
//测试
System.out.println("测试~~~~~~~~~~~");
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5);//加入5个节点
circleSingleLinkedList.showBoy();
circleSingleLinkedList.countBoy(1,2,5);
}
}
//创建一个环形得单向链表
class CircleSingleLinkedList {
//创建一个first节点,当前没有编号
private Boy first = new Boy(-1);
//添加节点,构建成一个环形链表
public void addBoy(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);
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();
}
}
//根据用户得输入,计算出节点出圈得顺序
/**
* @param startNo 表示从第几个节点开始报数
* @param countNum 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();
}
for (int j = 0; j < startNo - 1; j++) {
first = first.getNext();
helper = helper.getNext();
}
//当遍历m次时,让first和helper指针同时得移动m-1次
//这里是一个循环操作,直到圈中只有一个节点
while (true) {
if (helper == first) { //说明圈中只有一个节点
break;
}
//让first和helper指针同事的移动countNum-1
for (int j = 0; j < countNum - 1; j++) {
first = first.getNext();
helper = helper.getNext();
}
//这时first指向的节点,就是要出圈的小孩节点
System.out.println("小孩出圈的编号:" + first.getNo());
//这时将first指向的节点出圈
first = first.getNext();
helper.setNext(first);
}
System.out.println("最后留在圈中的节点编号是:" + first.getNo());
}
}
//创建一个Boy类,表示一个节点
class Boy {
private int no; //编号
private Boy next;//指向下一个节点,默认null
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;
}
}
栈
栈的介绍
- 栈(stack)的特点是先入后出(FILO-First In Last Out)的有序列表
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)
- 通俗的讲,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。
数组模拟栈
实现栈的思路分析
- 使用数组来模拟栈
- 定义一个top来表示栈顶,初始化为-1
- 入栈的操作,当有数据加入到栈时,top++;stack[top]=data;
- 出栈的操作,int value=stack[top];top–;return value;
代码实例
public class ArrayStackDemo {
public static void main(String[] args) {
//测试一下ArrayStack 是否正确
//先创建一个ArrayStack对象->表示栈
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 : 表示从栈取出数据(出栈)");
key = scanner.next();
switch (key) {
case "show":
stack.list();
break;
case "push":
System.out.print("请输入一个数 :");
int value = scanner.nextInt();
stack.push(value);
break;
case "pop":
try {
int res = stack.pop();
System.out.println("出栈的数据是 " + res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case "exit":
scanner.close();
loop = false;
System.exit(0);
break;
default:
break;
}
}
System.out.println("程序退出");
}
}
//定义一个ArrayStack 表示栈
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;
}
//入栈-push
public void push(int value) {
if (isFull()) {
return;
}
stack[++top] = value;
}
//出栈-pop ,将栈顶的数据返回
public int pop() {
//先判断栈是否空
if (isEmpty()) {
throw new RuntimeException("该栈为空,没有数据");
}
return stack[top--];
}
//显示栈的情况[遍历栈],遍历时,需要从栈顶开始显示数据
public void list() {
if (isEmpty()) {
System.out.println("该栈为空,没有数据");
}
for (int i = top; i >= 0; i--) {
System.out.println("stack[" + i + "] = " + stack[i]);
}
}
利用栈实现综合计算器
思路分析
- 通过一个index 值(索引) ,来遍历我们的表达式
- 如果我们发现是一个数字,就直接入数栈
- 如果发现扫描到的是一个符号,就分如下情况解决:
- 如果发现当前的符号栈为空,就直接入栈
- 如果发现当前的符号栈有操作符,就进行比较
- 如果当前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数,再从符号栈中pop出一个符号,进行运算,将得到的结果入数栈,还需将当前的符号入符号栈。
- 如果当前的操作符的优先级大于栈中的操作符, 就直接入符号栈
- 当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运行。
- 最后在数栈只有一个数字,就是表达式的结果。
代码实例
public class Calculator {
public static void main(String[] args) {
String expression = "30+2*6-2";
//创建两个栈,数栈,一个符号栈
ArrayStack2 numStack = new ArrayStack2(10);
ArrayStack2 operStack = new ArrayStack2(10);
//定义需要的相关变量
int index = 0;//用于扫描
int num1 = 0;
int num2 = 0;
int oper = 0;
int res = 0;
String keepNum = "";
char ch = ' ';//将每次扫描得到char保存到ch
//开始while循环的扫描expression
while (true) {
//依次得到expression的每一个字符
ch = expression.charAt(index);
//判断ch是什么,然后做相应的处理
if (operStack.isOper(ch)) { //如果是运算符
//判断当前的符号栈是否为空
if (!operStack.isEmpty()) {
//如果符号栈中有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数
//在从符号栈中pop出一个符号运算,将得到结果,入数栈,然后将当前的操作符入符号栈
if (operStack.priority(ch) <= operStack.priority(operStack.peek())) {
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.cal(num1, num2, oper);
//把运算结果入数栈
numStack.push(res);
//然后把当前运算符入符号栈
operStack.push(ch);
} else {
//如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈
operStack.push(ch);
}
} else {
//如果为空直接入符号栈..
operStack.push(ch);
}
} else {
//如果是数字
// numStack.push(ch - 48);
//分析思路
// 1. 当处理多位数时,不能发现是数字就立即入栈,它可能是多位数
// 2. 在处理数时,需要向expression的表达式的index 后在看一位,如果是树就继续扫描,如果是符号则入栈
// 3. 因此我们需要定义一个变量, 字符串,用于拼接多位数
keepNum += ch;
//如果ch已经是expression的最后一位,则直接入栈
if (index == expression.length() - 1) {
numStack.push(Integer.parseInt(keepNum));
keepNum = "";
} else {
//判断下一个字符是不是数字,如果是数字则继续扫描,如果是运算符就入栈
if (operStack.isOper(expression.charAt(index + 1))) {
numStack.push(Integer.parseInt(keepNum));
keepNum = "";
}
}
}
//index+1,并判断是否扫描到expression最后
index++;
if (index == expression.length()) {
break;
}
}
//当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运行。
while (true) {
//如果符号栈为空,则为计算的最后结果,数栈中只有一个数字
if (operStack.isEmpty()) {
break;
}
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
res = numStack.cal(num1, num2, oper);
numStack.push(res);
}
res = numStack.pop();
System.out.println("计算结果为:" + res);
}
}
//先创建一个栈,直接使用前面创建好的
//定义一个ArrayStack2 表示栈,需要扩展功能
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;
}
//入栈-push
public void push(int value) {
if (isFull()) {
return;
}
stack[++top] = value;
}
//出栈-pop ,将栈顶的数据返回
public int pop() {
//先判断栈是否空
if (isEmpty()) {
throw new RuntimeException("该栈为空,没有数据");
}
return stack[top--];
}
//显示栈的情况[遍历栈],遍历时,需要从栈顶开始显示数据
public void list() {
if (isEmpty()) {
System.out.println("该栈为空,没有数据");
}
for (int i = top; i >= 0; i--) {
System.out.println("stack[" + 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 num1, int num2, int oper) {
int res = 0;//res 用于存放计算的结果
switch (oper) {
case '+':
res = num1 + num2;
break;
case '-':
res = num2 - num1;
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num2 / num1;
break;
default:
break;
}
return res;
}
//查看栈顶的值
public int peek() {
return stack[top];
}
}
前缀、中缀、后缀表达式(逆波兰表达式)
前缀表达式
基本介绍
- 前缀表达式又称为波兰式,前缀表达式的运算符位于操作数之前
- 举例说明:(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,由此得出最终结果
中缀表达式
基本介绍
- 中缀表达式就是常见的运算表达式,如(3+4)× 5 - 6
- 中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作。因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式)
后缀表达式
基本介绍
- 后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后
- 举例说明:(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,由此得到最终结果。
逆波兰计算器
基本介绍
输入一个逆波兰表达式(后缀表达式),使用栈(stack),计算其结果
代码实例
public class PolandNotation {
public static void main(String[] args) {
//先定义一个逆波兰表达式
// (3+4)×5-6 => 3 4 + 5 × 6 -
//说明: 为了方便,逆波兰表达式的数字和符号使用空格隔开
String suffixExpression = "3 4 + 5 × 6 - ";
//思路
//1. 先将"3 4 + 5 × 6 - " => 放入到ArrayList中
//2. 将ArrayList 传递给一个方法,配合栈 完成计算
List<String> rpnList = getListString(suffixExpression);
System.out.println("rpnList=" + rpnList);
int res=calculate(rpnList);
System.out.println("result="+res);
}
//将一个逆波兰表达式,依次将数据和运算符放入到ArrayList中
public static List<String> getListString(String suffixExpression) {
//将suffixExpression分割
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String s : split) {
list.add(s);
}
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(String.valueOf(res));
}
}
//最后留在stack中的数据就是运算结果
return Integer.parseInt(stack.pop());
}
}
中缀表达式转换为后缀表达式
思路分析
后缀表达式适合计算式进行运算,但是由人输入不太容易,尤其是表达式很长的情况下,因此在开发中,我们需要将中缀表达式转化为后缀表达式。
具体步骤如下:
- 初始化两个栈:运算符栈s1和储存中间结果的栈s2;
- 从左至右扫描中缀表达式;
- 遇到操作数栈时,将其压入s2;
- 遇到运算符时,比较其与s1栈顶运算符的优先级;
- 如果s1为空,或栈顶运算符为左括号"(",则直接将此运算符入栈;
- 否则,若优先级比栈顶运算符的高,也将运算符压入s1;
- 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较;
- 遇到括号时:
- 如果是左括号"(",则直接压入s1
- 如果是右括号")",则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
- 重复步骤2至5,直到表达式的最右边
- 将s1中剩余的运算符依次弹出并压入s2
- 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
代码实例
public class PolandNotation {
public static void main(String[] args) {
//完成将一个中缀表达式转成后缀表达式的功能
//说明
//1. 1+((2+3)×4)-5 => 转成 1 2 3 + 4 × + 5 -
//2. 因为直接对str 进行操作不太方便,因此先将1+((2+3)×4)-5 => 中缀的表达式对应的List
//3. 将得到的中缀表达式对应的List => 后缀表达式对应的List
String expression = "1+((2+3)×4)-5";
List<String> infixExpressionList = toInfixExpressionList(expression);
System.out.println("中缀表达式对应的List: " + infixExpressionList); //[1, +, (, (, 2, +, 3, ), ×, 4, ), -, 5]
List<String> suffixExpreesionList = pareseSuffixExpreesionList(infixExpressionList);
System.out.println("后缀表达式对应的List: " + suffixExpreesionList);
System.out.println("expression对应的result="+calculate(suffixExpreesionList));
}
//方法:将得到的中缀表达式对应的List => 后缀表达式对应的List
public static List<String> pareseSuffixExpreesionList(List<String> ls) {
//定义两个栈
Stack<String> s1 = new Stack<>(); //符号栈
//说明:因为s2这个栈,在整个转换过程中,没有pop操作,而且后面我们还需要逆序输出
//因此比较麻烦,这里不使用Stack<String>,使用ArrayList<String>即可完成逆波兰表达式
List<String> s2 = new ArrayList<>(); //储存中间结果的List->s2
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中
//问题:缺少一个比较优先级高低的方法
while (s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) {
s2.add(s1.pop());
}
//还需要将item压入栈中
s1.push(item);
}
}
//将s1中剩余的运算符依次弹出并压入s2
while (!s1.isEmpty()) {
s2.add(s1.pop());
}
return s2;
}
//方法:将中缀表达式转成对应的List
public static List<String> toInfixExpressionList(String s) {
//定义一个List,存放中缀表达式对应的内容
List<String> ls = new ArrayList<>();
int i = 0; //这时是一个指针,用于遍历中缀表达式字符串
String str;//对多位数的拼接
char c; //每遍历到一个字符,就放入到c
do {
//如果c是非数字,就需要加入到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;
}
//完成对逆波兰表达式的运算
/*
*>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(String.valueOf(res));
}
}
//最后留在stack中的数据就是运算结果
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;
}
}