文章目录
线性数据结构和非线性数据结构
数据结构包括 线性数据结构 和 非线性数据结构。
数据结构
- 线性数据结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系。
- 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。顺序存储的线性表称为 顺序表,顺序表中的存储元素是连续的。
- 链式存储的线性表称为 链表,链表中的存储元素不一定是连续的,元素结点中存放数据元素以及相邻元素的地址信息。
- 线性结构常有:数组,队列,链表和栈。
非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构。
稀疏Sparsearray 数组
基本介绍:
- 当一个数组中打部分元素为0,或者同为一个值的数组时,可以用稀疏数组来保存该数组。
- 稀疏数组的处理方法:
- 记录数组一共有几行几列,有多少个不同的值。
- 具有不同值的元素的行和列及值记录在一个小规模的数组中,从而压缩小程序的规模。
案例:
思路:
- 二维数组 ----> 稀疏数组
- 遍历元素的二维数组, 得到有效数据的个数sum。
- 根据sum可以创建稀疏数组 sparseArrary int[sum+1] [3].
- 将二维数组的有效数据存入到稀疏数组。
- 稀疏数组 ----> 二维数组
- 先读取稀疏数组第一行,根据第一行恢复二维数组(稀疏数组第一行存入的是二维数组的行和列)。
- 读取稀疏数组的后几行数据,并赋值给 原始的二维数组即可。
代码实现:
-
public class SparseArray { public static void main(String[] args) { //创建一个二维数组 // 0没,1黑,2,白 int[][] chessArray = new int[11][11]; chessArray[1][2] = 1; chessArray[2][3] = 2; int sum = 0; for (int[] row : chessArray) { for (int data: row) { System.out.printf("%d\t", data); } System.out.println(); } for (int i = 0; i < 11; ++ i) { for (int j = 0; j < 11; ++ j) { if (chessArray[i][j] != 0) { sum ++; } } } //2. 创建对应的稀疏数组 int sparseArr[][] = new int[sum + 1][3]; //给稀疏数组赋值 sparseArr[0][0] = 11; sparseArr[0][1] = 11; sparseArr[0][2] = sum; //遍历二维数组,数据存入稀疏数组。 for (int i = 0; i < 11; ++ i) { for (int j = 0; j < 11; ++ j) { if (chessArray[i][j] != 0) { sparseArr[i][0] = i; sparseArr[i][1] = j; sparseArr[i][2] = chessArray[i][j]; } } } //输出稀疏数组 System.out.println(); System.out.println("得到的稀疏数组"); for (int i = 0; i < sparseArr.length; i ++ ) { System.out.printf("%d\t%d\t%d\n", sparseArr[i][0], sparseArr[i][1], sparseArr[i][2]); } System.out.println(); System.out.println("还原:"); int[][] chessArr2 = new int[sparseArr[0][0]][sparseArr[0][1]]; for (int i = 1; i < sparseArr.length; ++ i) { chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2]; } //输出恢复后的二维数组 for (int[] row : chessArr2) { for (int data: row) { System.out.printf("%d\t", data); } System.out.println(); } } }
课后练习:
-
把稀疏数组保存到磁盘, 读入到数组没写
-
import java.io.*; public class SparseArray { public static void main(String[] args) { //创建一个二维数组 // 0没,1黑,2,白 int[][] chessArray = new int[11][11]; chessArray[1][2] = 1; chessArray[2][3] = 2; int sum = 0; for (int[] row : chessArray) { for (int data: row) { System.out.printf("%d\t", data); } System.out.println(); } for (int i = 0; i < 11; ++ i) { for (int j = 0; j < 11; ++ j) { if (chessArray[i][j] != 0) { sum ++; } } } //2. 创建对应的稀疏数组 int sparseArr[][] = new int[sum + 1][3]; //给稀疏数组赋值 sparseArr[0][0] = 11; sparseArr[0][1] = 11; sparseArr[0][2] = sum; //遍历二维数组,数据存入稀疏数组。 for (int i = 0; i < 11; ++ i) { for (int j = 0; j < 11; ++ j) { if (chessArray[i][j] != 0) { sparseArr[i][0] = i; sparseArr[i][1] = j; sparseArr[i][2] = chessArray[i][j]; } } } //将数据存入磁盘 FileOutputStream fos = null; OutputStreamWriter writer = null; File file = new File("C:\\Users\\25766\\Desktop\\我的学习笔记\\数据结构和算法\\map.data"); try { fos = new FileOutputStream(file);//获取文件输出流 writer = new OutputStreamWriter(fos, "UTF-8");//输出流 for (int i = 0; i < sparseArr.length; ++ i) { writer.append(sparseArr[i][0] + " " + sparseArr[i][1] + " " + sparseArr[i][2] + "\n");//数据写入文件 } } catch (Exception e) { e.printStackTrace(); } finally { try { writer.close(); fos.close(); } catch (IOException e) { e.printStackTrace(); } } //输出稀疏数组 System.out.println(); System.out.println("得到的稀疏数组"); for (int i = 0; i < sparseArr.length; i ++ ) { System.out.printf("%d\t%d\t%d\n", sparseArr[i][0], sparseArr[i][1], sparseArr[i][2]); } System.out.println(); System.out.println("还原:"); FileInputStream fis = null; InputStreamReader reader = null; try{ fis = new FileInputStream(file); reader = new InputStreamReader(fis, "UTF-8"); StringBuffer s = new StringBuffer(); int t = 0; while (reader.ready()) { } }catch (Exception e) { e.printStackTrace(); }finally { try { reader.close(); fis.close(); }catch (IOException e) { e.printStackTrace(); } } int[][] chessArr2 = new int[sparseArr[0][0]][sparseArr[0][1]]; for (int i = 1; i < sparseArr.length; ++ i) { chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2]; } //输出恢复后的二维数组 for (int[] row : chessArr2) { for (int data: row) { System.out.printf("%d\t", data); } System.out.println(); } } }
-
队列
队列:是一种先进先出的数据结构。
队列介绍:
- 队列是一个有序列表,可以用数组或者是链表来实现。
- 遵循 先进先出 的原则。 即:先存入队列的数据,要先取出。后存入的要后取出。
- 示意图:(使用数组模拟队列示意图)
*
数组模拟队列
- 队列是一个有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图,其中maxSize该队列的最大容量。
- 因为队列的输入、输出是分别从前后端来处理,因此需要连个变量front,及rear分别记录队列前后端的下标, front 会随着数据输出而改变,rear 随着数据输入改变而改变:
*
思路分析:
-
将我们的数据存入队列是称为 “addQueue”, addQueue 的处理需要有两个步骤
-
- 将尾指针往后移动:rear + 1, 当front == rear 队列为 空。
- 若尾指针 rear 小于队列的最大下标 maxSize - 1, 则将数据存入rear所指的数组元素中,否则无法存入数据。rear == maxSize - 1(队列满了)
-
-
代码实现:
-
import java.util.Scanner; public class ArrayQueueDemo { public static void main(String[] args) { ArrayQueue q = new ArrayQueue(3); char key = ' '; Scanner cin = new Scanner(System.in); boolean loop = true; while (loop) { System.out.println("s(show): 显示队列"); System.out.println("e(exit): 退出程序"); System.out.println("a(add): 添加数据到队头"); System.out.println("g(get): 从队列取出数据"); System.out.println("h(head): 查看队头的数据"); key = cin.next().charAt(0);//接收一个字符 switch (key) { case 's': q.showQueue(); break; case 'a': System.out.println("输入一个数"); int v = cin.nextInt(); q.addQueue(v); break; case 'g': try { int res = q.getQueue(); System.out.printf("取出数据是%d\n", res); }catch (Exception e) { System.out.println(e.getMessage()); } break; case 'h': try { int res = q.headQueue(); System.out.printf("队列头部的数据为%d\n", res); }catch (Exception e) { System.out.println(e.getMessage()); } break; case 'e': cin.close(); loop = false; break; default: break; } } System.out.println("程序退出"); } } //使用数组模拟 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;//指向队头,分析出前一个位置 this.rear = -1;//指向队尾,具体数据 } //判断队列是否满 public boolean isFull() { return rear == maxSize - 1; } //判断为空 public boolean isEmpty() { return front == rear; } //添加数据 public void addQueue(int n) { if (isFull()) { System.out.println("队列已经满了"); return; } rear ++; arr[rear] = n; } //获得队列的数据 public int getQueue() { if(isEmpty()){ //通过抛出异常 throw new RuntimeException("队列不能为空"); } front++;//后移动 return arr[front]; } //显示队列的所有数据 public void showQueue() { if (isEmpty()) { System.out.println("队列为空"); return; } for (int i = 0; i < arr.length; ++ i) { System.out.printf("arr[%d]=%d\n", i, arr[i]); } } //显示队列的头数据 public int headQueue() { if (isEmpty()) { throw new RuntimeException("队列为空"); } return arr[front + 1]; } }
-
数组模拟环形队列
思路如下:
- front 变量的含义做一个调整:front 就是指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素
- rear 的含义是做一个调整: rear 指向队列的最后一个元素的最后一个位置,因为希望空出一个空间做为约定。
- 当队列满时,条件是(rear + 1)% maxSize = front [满]。
- 当队列为空的条件,rear == front 空
- 当我们这样分析,队列中有效的数据个数 (rear + maxSize - front)% maxSize // rear = 1 front = 0
- 就可以得到环形队列。
import java.util.Scanner;
public class CircleArrayQueue {
public static void main(String[] args) {
CircleArray q = new CircleArray(4);
char key = ' ';
Scanner cin = new Scanner(System.in);
boolean loop = true;
while (loop) {
System.out.println("s(show): 显示队列");
System.out.println("e(exit): 退出程序");
System.out.println("a(add): 添加数据到队头");
System.out.println("g(get): 从队列取出数据");
System.out.println("h(head): 查看队头的数据");
key = cin.next().charAt(0);//接收一个字符
switch (key) {
case 's':
q.showQueue();
break;
case 'a':
System.out.println("输入一个数");
int v = cin.nextInt();
q.addQueue(v);
break;
case 'g':
try {
int res = q.getQueue();
System.out.printf("取出数据是%d\n", res);
}catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int res = q.headQueue();
System.out.printf("队列头部的数据为%d\n", res);
}catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'e':
cin.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出");
}
}
class CircleArray {
private int maxSize;
private int front;
private int rear;
private int[] arr;
public CircleArray(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
front = 0;
rear = 0;
}
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 + 1) % maxSize;
}
public int getQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,不能取数据");
}
int value = arr[front];
front = (front + 1) % maxSize;
return value;
}
public void showQueue() {
if (isEmpty()) {
System.out.println("队列空的,没有数据~~");
return;
}
for (int i = front; i < front + size(); i ++ ) {
System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize]);
}
}
//当前队列的有效个数
public int size() {
return (rear + maxSize - front) % maxSize;
}
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
return arr[front + 1];
}
}
链表
单链表
小结:
- 链表事以结点的方式存储的。
- 每个结点包括data域,next域:指向下一个结点。
- 如图发现不连续。
- 链表分有头节点,和没有头节点的链表,根据实际需求来确定。
带头结点的链表(逻辑结构图):
思路:
- 先创建一个head头结点。作用就事表示单链表的头。
- 后面我们每添加一个结点,就是直接加到链表的后面
-
遍历:
-
代码
-
import java.awt.*; public class SingleLinkedListDemo { public static void main(String[] args) { SingleLinkedList list = new SingleLinkedList(); list.add(new HerNode(1, "董亚", "小旋风")); list.add(new HerNode(2, "王仕", "小宽")); list.add(new HerNode(3, "李", "小琦")); list.add(new HerNode(4, "朱", "小朱")); list.add(new HerNode(5, "肖元", "小肖")); list.add(new HerNode(6, "何杨", "小飞飞")); list.show(); } } //定义 class SingleLinkedList { //初始化一个头结点 private HerNode head = new HerNode(0, "", ""); public void add(HerNode herNode) { HerNode temp = head; //遍历链表 while (temp.next != null) { temp = temp.next; } temp.next = herNode; } public void addByOrder(HerNode herNode) { HerNode temp = head; boolean flag = false; while (temp.next != null) { if (temp.next.no > herNode.no){ break; }else if (temp.next.no == herNode.no) { flag = true; break; } temp = temp.next; } if (flag) { System.out.println("准备插入的英雄的编号已经存在"); } else { //插入到链表中 herNode.next = temp.next; temp.next = herNode; } } //显示 public void show() { if (head.next == null) { System.out.println("链表为空"); return; } HerNode temp = head.next; while (temp != null) { System.out.println(temp); temp = temp.next; } } public void delete(int no) { HerNode temp = head; boolean flag = false; while (temp.next != null) { if (temp.next.no == no) { flag = true; break; } temp = temp.next; } if (flag) { temp.next = temp.next.next; } else { System.out.println("删除结点不存在"); } } } //定义一个HerNode ,每个HerNode就是一个结点 class HerNode { public int no; public String name; public String nickname; public HerNode next; //指向下一个结点 //构造器 public HerNode(int no, String name, String nickname) { this.no = no; this.name = name; this.nickname = nickname; } @Override public String toString() { return "HerNode{" + "no=" + no + ", name='" + name + '\'' + ", nickname='" + nickname + '\'' + '}'; } }
-
单链表中面试题中:展现在代码中
import java.awt.*;
import java.util.Stack;
public class SingleLinkedListDemo {
public static void main(String[] args) {
SingleLinkedList list = new SingleLinkedList();
list.add(new HerNode(1, "董亚", "小旋风"));
list.add(new HerNode(2, "王仕", "小宽"));
list.add(new HerNode(3, "李", "小琦"));
list.add(new HerNode(4, "朱", "小朱"));
list.add(new HerNode(5, "肖元", "小肖"));
list.add(new HerNode(6, "何杨", "小飞飞"));
list.show();
System.out.println();
list.printReverse();
System.out.println();
list.revers();
System.out.println();
list.show();
System.out.println();
}
}
//定义
class SingleLinkedList {
//初始化一个头结点
private HerNode head = new HerNode(0, "", "");
public void add(HerNode herNode) {
HerNode temp = head;
//遍历链表
while (temp.next != null) {
temp = temp.next;
}
temp.next = herNode;
}
public void addByOrder(HerNode herNode) {
HerNode temp = head;
boolean flag = false;
while (temp.next != null) {
if (temp.next.no > herNode.no){
break;
}else if (temp.next.no == herNode.no) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
System.out.println("准备插入的英雄的编号已经存在");
} else {
//插入到链表中
herNode.next = temp.next;
temp.next = herNode;
}
}
//显示
public void show() {
if (head.next == null) {
System.out.println("链表为空");
return;
}
HerNode temp = head.next;
while (temp != null) {
System.out.println(temp);
temp = temp.next;
}
}
public void delete(int no) {
HerNode temp = head;
boolean flag = false;
while (temp.next != null) {
if (temp.next.no == no) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.next = temp.next.next;
} else {
System.out.println("删除结点不存在");
}
}
//不声反转单链表
public void revers() {
if (head.name == null || head.next.next == null) {
return;
}
//定义一个辅助指针变量,帮助我们遍历原来的链表
HerNode cur = head.next;
HerNode next = null;
HerNode reverseHead = new HerNode(0, "", "");
while (cur != null) {
next = cur.next;//先暂时保存当前结点的下一个结点,后面要使用
cur.next = reverseHead.next;//将cur的下一个结点指向新的链表的最前端
reverseHead.next = cur;
cur = next;//让cur后移动
}
head.next = reverseHead.next;
}
//通过栈数据结构,逆序输出单链表
public void printReverse(){
if (head.next == null){
return;
}
Stack<HerNode> stack = new Stack<HerNode>();
HerNode cur = head.next;
//将链表所有结点压入栈中
while(cur != null) {
stack.push(cur);
cur = cur.next;
}
while(!stack.empty()) {
System.out.println(stack.peek());
stack.pop();
}
}
}
//定义一个HerNode ,每个HerNode就是一个结点
class HerNode {
public int no;
public String name;
public String nickname;
public HerNode next; //指向下一个结点
//构造器
public HerNode(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "HerNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
双向链表
-
双向链表的应用示例
-
使用带head头的 双向链表 实现-水浒英雄排行榜
-
管理单向链表的缺点分析:
-
-
单链表,查找的方向只能是一个方向,而双链表可以向前或者向后查找。
-
单链表不能 自我删除,需要靠辅助结点,而双向链表,则可以 自我删除,所以单链表删除结点时,总是找到temp的下一个结点来删除。
-
public void delete(int no) { HerNode temp = head; boolean flag = false; while (temp.next != null) { if (temp.next.no == no) { flag = true; break; } temp = temp.next; } if (flag) { temp.next = temp.next.next; } else { System.out.println("删除结点不存在"); } }
-
-
-
-
-
分析 双向链表的遍历,添加,修改,删除的操作思路:
-
遍历单向链表一样,只是可以向前,也可以向后查找
-
添加(默认添加到双向链表的最后)
-
- 找到双向链表的最后这个节点
- temp.next = newHerNode;
- newHerNode.pre = temp;
-
-
修改思路和单链表一样
-
删除
-
- 因为是双向链表,因此我们课以实现 自我删除 某个节点。
- 直接找到要删除的这个节点,比如temp
- temp.pre.next = temp.next; (当前要删除的节点的 前驱的后继 = 删除节点的后继 )
- temp.next.pre = temp.pre;(当前要删除节点的 后继的前驱 = 删除节点的前驱)
-
-
代码实现:
-
-
public class DoubleLinkedListDemo { public static void main(String[] args) { System.out.println("双向链表测试"); DoubleLinkedList list = new DoubleLinkedList(); list.add(new HerNode1(1, "董亚宁", "小旋风")); list.add(new HerNode1(2, "王仕宽", "小宽")); list.add(new HerNode1(3, "李琦", "小琦")); list.add(new HerNode1(4, "朱瑞", "小朱")); list.add(new HerNode1(5, "肖元彪", "小肖")); list.add(new HerNode1(6, "何杨飞", "小飞飞")); list.show(); System.out.println(); list.delete(5); System.out.println("删除后"); list.show(); System.out.println(); } } class DoubleLinkedList { private HerNode1 head = new HerNode1(0, "", ""); //返回头结点 public HerNode1 getHead() { return head; } //遍历双向链表 public void show() { if (head.next == null) { System.out.println("链表为空"); return; } HerNode1 temp = head.next; while (temp != null) { System.out.println(temp); temp = temp.next; } } //添加到双向链表 public void add(HerNode1 herNode1) { HerNode1 temp = head; //遍历链表 while (temp.next != null) { temp = temp.next; } //形成双向链表 temp.next = herNode1; herNode1.pre = temp; } //双向链表中修改一个结点 public void modify(HerNode1 herNode1) { if (head.next == null) { System.out.println("链表为空"); return; } HerNode1 temp = head; boolean flag = false; while (temp != null) { if (temp.no == herNode1.no) { flag = true; break; } } if (flag) { temp.no = herNode1.no; temp.name = herNode1.name; temp.nickname = herNode1.nickname; } } //双向链表中删除一个结点 public void delete(int no) { HerNode1 temp = head.next; boolean flag = false; while (temp != null) { if (temp.no == no) { flag = true; break; } temp = temp.next; } if (flag) { //注意最后一个结点有点特殊 temp.pre.next = temp.next; //最后一个结点不需要执行下面这段代码 if (temp.next != null) { temp.next.pre = temp.pre; } }else { System.out.println("要删除的结点不存在"); } } } class HerNode1 { public int no; public String name; public String nickname; public HerNode1 next; //指向下一个结点,默认为null public HerNode1 pre; //指向前一个结点 //构造器 public HerNode1(int no, String name, String nickname) { this.no = no; this.name = name; this.nickname = nickname; } @Override public String toString() { return "HerNode{" + "no=" + no + ", name='" + name + '\'' + ", nickname='" + nickname + '\'' + '}'; } }
-
-
约瑟夫问题
问题:
-
设编号为1, 2, … , n的n个人围坐一圈,约定编号k (1 <= k <= n)的人开始从1报数,数到 m 的那个人出列,它的下一位有开始1报数,数到m的那个人出列,依次类推,直到所有人出队列为止,由此产生一个出队编号序列。
-
构建一个单向环形链表思路:
-
-
- 先创建第一个结点,让first指向该结点,并形成环形。
- 后面当我们没创建一个新的结点,就把该节点,加入到已有的环形链表中即可。
-
-
遍历环形链表:
-
- 先让一个辅助指针(变量),指向first结点。
- 然后通过一个while循环遍历该链表就可以了 curBoy.next == first结束到链表尾部。
-
-
实现代码
-
public class Josefa { public static void main(String[] args) { //测试构建和遍历 CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList(); circleSingleLinkedList.addBoy(5); circleSingleLinkedList.showBoy(); circleSingleLinkedList.countBoy(1, 2, 5); } } //创建环形单向链表 class CircleSingleLinkedList { private Boy first; //添加小孩 public void addBoy(int nums) { if (nums < 1) { System.out.println("nums值不正确"); return; } Boy curBoy = null; //帮助构建环形链表 //使用for创建链表 for (int i = 1; i <= nums; ++ i) { Boy boy = new Boy(i); //第一个小孩 if (i == 1) { first = boy; first.setNext(first);//构建环状 curBoy = first; } 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.printf("小孩的编号%d \n", curBoy.getNo()); if (curBoy.getNext() == first) {//遍历完成 break; } curBoy = curBoy.getNext(); } } //出圈,startNo为开始的编号 public void countBoy(int startNo, int countNum, int nums) { //对数据检查 if (first == null || startNo < 1 || startNo > nums) { System.out.println("参数输入有误"); return; } Boy helper = first; //让helper指向first前一个结点 while (helper.getNext() != first) { helper = helper.getNext(); } //从第几个小孩开始 for (int i = 0; i < startNo - 1; ++ i) { first = first.getNext(); helper = helper.getNext(); } //出圈 while (helper != first) { for (int i = 0; i < countNum - 1; ++ i) { first = first.getNext(); helper = helper.getNext(); } //first指向的小孩出 System.out.printf("小孩%d出圈\n", first.getNo()); first = first.getNext(); helper.setNext(first); //小孩出队完成 } System.out.printf("小孩%d出圈\n", first.getNo()); } } //创建要给boy类 class Boy { private int no; private Boy next; 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 是限制线性表中的元素的插入和删除 只能在同一端 进行的一种特殊线性表。允许插入和删除的一端,称为 栈顶。另一端为栈低。
- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。
栈的场景应用:
- 子程序的调用:在跳往子程序前,会将下一个指令的地址存到堆栈中,直到子程序执行完后再将地址,以回到原来的程序中通过。
- 处理递归调用:和子程序的调用类似,只是存储下一个指令地址外,也将参数、区域变量等数据存入堆中。
- 表达式转换[中缀表达式转后缀表达式] 与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth-first)搜索法。
数组模拟栈
数组模拟栈的思路分析:
实现 栈的思路分析
- 使用数组来模拟栈
- 定义一个top来表示栈顶,初始化为-1
- 入栈 的操作当有数据加入时,top++;stack[top] = data;
- 出栈 的操作,int value = stack[top]; top–,return value;
-
代码实现:
-
public class ArrayStackDemo { public static void main(String[] args) { ArrayStack stack = new ArrayStack(3); stack.push(1); stack.push(2); stack.push(3); stack.list(); if (stack.isFull()) System.out.println("Full"); if (stack.isEmpty()) System.out.println("Empty"); } } class ArrayStack { private int maxSize; //栈的大小 private int[] stack; //数组,数组模拟栈,数据就放在该数组 private int top = -1; //top表示栈顶,初始化为 -1 //构造器 public ArrayStack(int maxSize) { this.maxSize = maxSize; this.stack = new int[maxSize]; } //栈满 public boolean isFull() { return top == this.maxSize - 1; } //栈空 public boolean isEmpty() { return top == -1; } //入栈-push public void push(int value) { //先判断栈是否为满 if(isFull()) { System.out.println("栈满"); return; } top++; stack[top] = value; } //出栈 public int pop() { //先判断栈是空 if (isEmpty()) { throw new RuntimeException("栈为空"); } int value = stack[top]; top --; return value; } //显示栈的情况[遍历栈] public void list() { if (isEmpty()) { System.out.println("栈为空"); return; } for (int i = top; i >= 0; -- i) { System.out.print(stack[i] + " "); } System.out.println(); } }
-
作业:
-
用链表模拟栈
-
import javax.management.relation.RoleInfoNotFoundException; public class ArrayLinkedStackDemo { public static void main(String[] args) { LinkedStack stack = new LinkedStack(); stack.push(new stackNode(1)); stack.push(new stackNode(2)); stack.push(new stackNode(3)); System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); } } class LinkedStack { public stackNode head; public LinkedStack() { this.head = new stackNode(0); this.head.next = null; } //满足先加入的结点再后面,参考翻转单链表 public void push(stackNode node) { node.next = head.next; head.next = node; } //删除栈顶元素 public int pop() { if (head.next == null) { throw new RuntimeException("栈为空"); } int value = head.next.data; head = head.next; return value; } //栈是否为空 public boolean isEmpty() { return head.next == null; } } class stackNode { public int data; public stackNode next; public stackNode(int data) { this.data = data; } }
使用栈完成表达式的计算
思路:
- 通过index值(索引),来遍历我们的表达式。
- 如果我们发现式一个数字,就直接入数栈
- 如果发现扫描到是一个符号,就分如下情况
-
- 如果发现当前的符号栈为空,就直接入栈。
- 如果符号栈操作符,就进行比较,如果 当前的操作符的优先级小于或者等于栈中的操作符 ,就需要从数栈中 pop 出两个数,再符号栈中取出一个符号,进行运算,将的到结果,入数栈,然后将当前的操作符入符号栈,如果大于符号栈 栈顶的符号优先级就直接入符号栈。
-
- 表达式扫描完毕,就顺序的 从数栈和符号栈中pop出相应的数和符号,并运行。
- 最后再数栈只有一个数字,就是表达式的结果。
代码实现:
public class Calculator {
public static void main(String[] args) {
//根据前面老师思路,完成表达式的运算
String expression = "7*2*2-5+1-5+3-4"; // 15//如何处理多位数的问题?
//创建两个栈,数栈,一个符号栈
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;
char ch = ' '; //将每次扫描得到char保存到ch
String keepNum = ""; //用于拼接 多位数
//开始while循环的扫描expression
while(true) {
//依次得到expression 的每一个字符
ch = expression.substring(index, index+1).charAt(0);
//判断ch是什么,然后做相应的处理
if(operStack.isOper(ch)) {//如果是运算符
//判断当前的符号栈是否为空
if(!operStack.isEmpty()) {
//如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数,
//在从符号栈中pop出一个符号,进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈
if(operStack.priority(ch) <= operStack.priority(operStack.peek())) {
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); // 1 + 3
}
} else { //如果是数,则直接入数栈
//numStack.push(ch - 48); //? "1+3" '1' => 1
//分析思路
//1. 当处理多位数时,不能发现是一个数就立即入栈,因为他可能是多位数
//2. 在处理数,需要向expression的表达式的index 后再看一位,如果是数就进行扫描,如果是符号才入栈
//3. 因此我们需要定义一个变量 字符串,用于拼接
//处理多位数
keepNum += ch;
//如果ch已经是expression的最后一位,就直接入栈
if (index == expression.length() - 1) {
numStack.push(Integer.parseInt(keepNum));
}else{
//判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则入栈
//注意是看后一位,不是index++
if (operStack.isOper(expression.substring(index+1,index+2).charAt(0))) {
//如果后一位是运算符,则入栈 keepNum = "1" 或者 "123"
numStack.push(Integer.parseInt(keepNum));
//重要的!!!!!!, 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);//入栈
}
//将数栈的最后数,pop出,就是结果
int res2 = numStack.pop();
System.out.printf("表达式 %s = %d", expression, res2);
}
}
//先创建一个栈,直接使用前面创建好
//定义一个 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];
}
//增加一个方法,可以返回当前栈顶的值, 但是不是真正的pop
public int peek() {
return stack[top];
}
//栈满
public boolean isFull() {
return top == maxSize - 1;
}
//栈空
public boolean isEmpty() {
return top == -1;
}
//入栈-push
public void push(int value) {
//先判断栈是否满
if(isFull()) {
System.out.println("栈满");
return;
}
top++;
stack[top] = value;
}
//出栈-pop, 将栈顶的数据返回
public int pop() {
//先判断栈是否空
if(isEmpty()) {
//抛出异常
throw new RuntimeException("栈空,没有数据~");
}
int value = stack[top];
top--;
return value;
}
//显示栈的情况[遍历栈], 遍历时,需要从栈顶开始显示数据
public void list() {
if(isEmpty()) {
System.out.println("栈空,没有数据~~");
return;
}
//需要从栈顶开始显示数据
for(int i = top; i >= 0 ; i--) {
System.out.printf("stack[%d]=%d\n", i, stack[i]);
}
}
//返回运算符的优先级,优先级是程序员来确定, 优先级使用数字表示
//数字越大,则优先级就越高.
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;
}
}
栈实现表达式综合计算
- 前缀(波兰表达式)、中缀、后缀表达式(逆波兰表达式)
-
- 前缀表达式又称为波兰式,前缀表达式的运算符位于操作数之前。
- 举例说明:(3 + 4)X 5 - 6对应的前缀表达式就是 - X + 3 4 5 6.
-
前缀表达式:
- 从右到左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符队他们做对应的计算(栈顶元素 和 次顶元素),并将结果压入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果
- 例如:(3 + 4) X 5 - 6,针对前缀表达式求值步骤如下:
-
- 从右到左扫描,将6、5、4、3压入栈
- 遇到 + 运算符,因此弹出3、4,计算值,得7,将7压入栈中。
- 接下来是X运算符,因此弹出7、5,计算出7 X 5 = 35, 将35压入栈。
- 最后是 - 运算符,计算出值35 - 6的值,29,由次得出结果。
中缀表达式:
-
- 中缀表达式就是 常见的运算表达式,如 (3 + 4) X 5 - 6
- 中缀表达式的求值,是我们最熟悉的,但是对计算机来说却不好操作,因此,再计算结果时,往往将会将中缀表达式转化成其他表达式来操作(一般只转化成后缀表达式)。
后缀表达式:
-
- 后缀表达式又称为 逆波兰表达式 ,与前缀表达式相似,只是运算符的位置位于操作数之后。
- 举例:(3 + 4)* 5 - 6 对应的后缀表达式就式 3 4 + 5 * 6 -
- 再比如:
-
后缀表达式的求值计算
- 从左向右扫描,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶里两个数,用运算符队他们进行计算,并将结果存入栈顶重复上述过程直到表达式最右端,最后运算得出的值即为表达式的计算结果。
*
- 从左向右扫描,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶里两个数,用运算符队他们进行计算,并将结果存入栈顶重复上述过程直到表达式最右端,最后运算得出的值即为表达式的计算结果。
逆波兰计算器
任务:
-
- 输入一个逆波兰表达式,使用栈(stack),计算其结果。
- 支持小括号和多位整数,计算器只是先对整数计算。
- 思路分析
- 代码完成
-
代码实现:
-
import java.util.ArrayList; import java.util.List; import java.util.Stack; public class PolandNotation { public static void main(String[] args) { //定义一个逆波兰表达式 String suffixExpression = "30 4 + 5 * 6 -"; List<String> rpnList = getListString(suffixExpression); System.out.println(rpnList); int res = calculate(rpnList); System.out.println(res); } public static List<String> getListString(String suffixExpression) { String[] split = suffixExpression.split(" "); List<String> list = new ArrayList<String>(); for (String ele : split) { list.add(ele); } return list; } public static int calculate(List<String> ls) { Stack<String> stack = new Stack<String>(); for (String item: ls) { if (item.matches("\\d+")) {//匹配的是多位数 stack.push(item); } else { int n1, n2; n1 = Integer.parseInt(stack.pop()); n2 = Integer.parseInt(stack.pop()); int res = 0; if (item.equals("+")) { res = n1 + n2; } else if (item.equals("-")) { res = n2 - n1; } else if (item.equals("*")) { res = n1 * n2; } else if (item.equals("=")) { res = n2 / n1; } else { throw new RuntimeException("运算符有问题"); } stack.push("" + res); } } return Integer.parseInt(stack.pop()); } }
-
中缀表达式转后缀表达式
- 1.
package com.atguigu.stack;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class PolandNotation {
public static void main(String[] args) {
//完成将一个中缀表达式转成后缀表达式的功能
//说明
//1. 1+((2+3)×4)-5 => 转成 1 2 3 + 4 × + 5 –
//2. 因为直接对str 进行操作,不方便,因此 先将 "1+((2+3)×4)-5" =》 中缀的表达式对应的List
// 即 "1+((2+3)×4)-5" => ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
//3. 将得到的中缀表达式对应的List => 后缀表达式对应的List
// 即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] =》 ArrayList [1,2,3,+,4,*,+,5,–]
String expression = "1+((2+3)*4)-5";//注意表达式
List<String> infixExpressionList = toInfixExpressionList(expression);
System.out.println("中缀表达式对应的List=" + infixExpressionList); // ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
List<String> suffixExpreesionList = parseSuffixExpreesionList(infixExpressionList);
System.out.println("后缀表达式对应的List" + suffixExpreesionList); //ArrayList [1,2,3,+,4,*,+,5,–]
System.out.printf("expression=%d", calculate(suffixExpreesionList)); // ?
/*
//先定义给逆波兰表达式
//(30+4)×5-6 => 30 4 + 5 × 6 - => 164
// 4 * 5 - 8 + 60 + 8 / 2 => 4 5 * 8 - 60 + 8 2 / +
//测试
//说明为了方便,逆波兰表达式 的数字和符号使用空格隔开
//String suffixExpression = "30 4 + 5 * 6 -";
String suffixExpression = "4 5 * 8 - 60 + 8 2 / +"; // 76
//思路
//1. 先将 "3 4 + 5 × 6 - " => 放到ArrayList中
//2. 将 ArrayList 传递给一个方法,遍历 ArrayList 配合栈 完成计算
List<String> list = getListString(suffixExpression);
System.out.println("rpnList=" + list);
int res = calculate(list);
System.out.println("计算的结果是=" + res);
*/
}
//即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] =》 ArrayList [1,2,3,+,4,*,+,5,–]
//方法:将得到的中缀表达式对应的List => 后缀表达式对应的List
public static List<String> parseSuffixExpreesionList(List<String> ls) {
//定义两个栈
Stack<String> s1 = new Stack<String>(); // 符号栈
//说明:因为s2 这个栈,在整个转换过程中,没有pop操作,而且后面我们还需要逆序输出
//因此比较麻烦,这里我们就不用 Stack<String> 直接使用 List<String> s2
//Stack<String> s2 = new Stack<String>(); // 储存中间结果的栈s2
List<String> s2 = new ArrayList<String>(); // 储存中间结果的Lists2
//遍历ls
for(String item: ls) {
//如果是一个数,加入s2
if(item.matches("\\d+")) {
s2.add(item);
} else if (item.equals("(")) {
s1.push(item);
} else if (item.equals(")")) {
//如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
while(!s1.peek().equals("(")) {
s2.add(s1.pop());
}
s1.pop();//!!! 将 ( 弹出 s1栈, 消除小括号
} else {
//当item的优先级小于等于s1栈顶运算符, 将s1栈顶的运算符弹出并加入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较
//问题:我们缺少一个比较优先级高低的方法
while(s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item) ) {
s2.add(s1.pop());
}
//还需要将item压入栈
s1.push(item);
}
}
//将s1中剩余的运算符依次弹出并加入s2
while(s1.size() != 0) {
s2.add(s1.pop());
}
return s2; //注意因为是存放到List, 因此按顺序输出就是对应的后缀表达式对应的List
}
//方法:将 中缀表达式转成对应的List
// s="1+((2+3)×4)-5";
public static List<String> toInfixExpressionList(String s) {
//定义一个List,存放中缀表达式 对应的内容
List<String> ls = new ArrayList<String>();
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++; //i需要后移
} else { //如果是一个数,需要考虑多位数
str = ""; //先将str 置成"" '0'[48]->'9'[57]
while(i < s.length() && (c=s.charAt(i)) >= 48 && (c=s.charAt(i)) <= 57) {
str += c;//拼接
i++;
}
ls.add(str);
}
}while(i < s.length());
return ls;//返回
}
//将一个逆波兰表达式, 依次将数据和运算符 放入到 ArrayList中
public static List<String> getListString(String suffixExpression) {
//将 suffixExpression 分割
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<String>();
for(String ele: split) {
list.add(ele);
}
return list;
}
//完成对逆波兰表达式的运算
/*
* 1)从左至右扫描,将3和4压入堆栈;
2)遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
3)将5入栈;
4)接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
5)将6入栈;
6)最后是-运算符,计算出35-6的值,即29,由此得出最终结果
*/
public static int calculate(List<String> ls) {
// 创建给栈, 只需要一个栈即可
Stack<String> stack = new Stack<String>();
// 遍历 ls
for (String item : ls) {
// 这里使用正则表达式来取出数
if (item.matches("\\d+")) { // 匹配的是多位数
// 入栈
stack.push(item);
} else {
// pop出两个数,并运算, 再入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if (item.equals("+")) {
res = num1 + num2;
} else if (item.equals("-")) {
res = num1 - num2;
} else if (item.equals("*")) {
res = num1 * num2;
} else if (item.equals("/")) {
res = num1 / num2;
} else {
throw new RuntimeException("运算符有误");
}
//把res 入栈
stack.push("" + res);
}
}
//最后留在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("不存在该运算符" + operation);
break;
}
return result;
}
}