前言
仙人指路
[第一章]为入门,讲解数据结构是干什么的
[第二章]到第四章是数据结构基础,主要讲解链表,栈,队列,串等
[第五章]是二叉树
[第六章]是图,此章偏难,非考研或算法从业可粗略看
[第七章]到第八章是查找以及各种排序
[第九章]为补充,教材中没有的部分概念我随机补充上去
墨尘の话
本文内容主要参考王道考研,其中21版本咸鱼学长的课尚可,20版本比较枯燥,可选择性学习
浙大陈越姥姥的课比较精简且侧重于练习,能看懂且时间多的蛋疼可以看这个
因时间关系,后期比较匆促,本人也就是学着玩玩╮(╯▽╰)╭,可多参考链接自主学习
参考视频
第一章
1.1.1 数据机构的基本概念
1. 什么是数据结构
数据:信息的载体,是描述客观事务属性的数,字符,以及所有能输入到计算机种并被计算机程序识别和处理的符号集合
数据对象: 具有相通性质的数据元素的集合,是数据的一个子集
数据元素: 数据的基本单位,通常作为一个整体进行考虑和处理
数据项: 构成数据元素的不可分割的最小单位
举例:一群人,就是一个数据对象,每个人就是一个数据元素,他们的手,头等就是数据项
结构:数据中存在某种关联关系,称为结构
举例:小明是八年级一班中的学生,成绩为100
上例中小明和成绩是数据项,一班是数据元素,八年级是数据对象
2. 数据结构三要素
1. 逻辑结构
线性结构
线性结构: 数据元素是一对一的关系,除了第一个元素,所有都有唯一前驱(前面有一个节点);除了最后一个元素,所有元素都有唯一后继(后面有一个节点)
非线性结构
集合: 一组元素属于同一个集合,除此外别无关系
树形结构: 数据元素之间是一对多的关系
图状结构: 数据元素之间是多对多的关系
2. 存储结构
顺序存储
顺序存储: 把逻辑上相邻的元素存储在物理位置也相邻的存储单元中,元素之间的关系靠存储单元的邻接关系体现
非顺序存储
链式存储: 逻辑上相邻的元素在物理位置上可以不相邻,借助元素存储地址的指针来表示元素之间的逻辑关系
索引存储: 在存储元素信息的同时,还建立附加的索引表。索引表中每项称为索引项,索引项一般为(关键字,地址)
散列存储: 根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储
顺序存储便于查找,非顺序查找便于增删
3. 数据的运算
3. 概念
1.2.1 算法的基本概念
1. 什么是算法
程序 = 数据结构 + 算法
数据结构负责将现实世界的问题转化为信息,然后将信息存入计算机
算法是处理信息的步骤,负责将信息进行处理得到我们想要的结果
2. 算法的特性
- 有穷性: 有限时间里可以执行完
- 可行性: 可以用现有的操作实现算法
好算法的特性
- 正确性:正确解决问题
- 可读性:别人也能很清晰的看明白
- 健壮性(鲁棒性): 可以处理异常情况,比如非法数据等
- 高效率: 省时间,省内存
3. 概念
1.2.2 时间复杂度
1. 如何评价算法的时间开销
1.和机器性能相关,诸如超级计算机 vs 单片机
2.和编程语言相关,越高级的语言效率越低
3.和编译程序产生的机器指令相关
4.有些算法是不能事后统计的
通常情况机器性能和编程语言无法改变,因此考虑如何优化程序性能即可
时间开销与问题规模n之间的关系
2. 如何计算
一般指代循环/迭代,循环的次数越多时间复杂度越高(参考下图)
3. 三种复杂度
1.最坏时间复杂度: 考虑最坏的情况
2.平均时间复杂度: 考虑所有输入情况都处于同等概率
3.最好时间复杂度: 考虑最好的情况
评定一个算法的时间复杂度好坏通常要拿最坏情况去考虑
4. 概念
1.2.3 空间复杂度
通俗理解:每个变量都会在内存中开辟一个新的空间,因此基于空间复杂度优化变量越少越好,递归尤甚
内存开销与问题规模n之间的关系
基于现在计算机内存来讲,通常我们只需要考虑时间复杂度,空间复杂度无需作为首选考虑
1. 概念
第二章
2.1 线性表的定义的基本操作
1. 线性表的定义
线性表是具有相同数据类型的n(n >= 0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表
2. 线性表的基本操作
// 初始化表。构造一个空的线性表L,分配内存空间
InitList(L);
// 销毁操作。销毁线性表,并释放线性表L所占用的内存空间
DestroyList(L);
// 插入操作。在表L中第i个位置上插入指定元素e
ListInsert(L, i ,e);
// 删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值
ListDelete(L, i ,e);
// 按值查找操作。在表L中查找具有给定关键字值的元素。
LocateElem(L, e);
// 按位查找操作。获取表L中第i个位置的元素的值。
GetElem(L, i);
// 其他常用操作
// 求表长。返回线性表L的所有元素值。
Length(L);
// 输出操作。按前后顺序输出线性表L的所有元素值。
PrintList(L);
// 判空操作。若L为空表,则返回true,否则返回false
Empty(L);
Tips:
1.对数据的操作(分析思路) —— 创销,增删改查(适用于所有数据结构)
2.java函数的定义 —— <返回值类型> 函数名(<参数1类型> 参数1,<参数2类型> 参数2,…)
3.实际开发中,可根据实际需求定义其他的基本操作(输出,判空等)
4.函数名和参数的形式,命名都可改变(Reference: 严蔚敏版《数据结构与算法》)
概念
2.2.1 顺序表的定义
1. 顺序表的定义
用顺序存储的方式实现线性表
顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现
2. 顺序表的特点
优点
-
随机访问,既可以在O(1)时间内找到第一个元素
-
存储密度高,每个节点只存储数据元素
缺点
-
拓展容量不方便(采用动态分配时间复杂度较高)
-
插入,删除不方便,需要移动大量元素
3. 概念
注:图上所示函数均为c语言中动态分配相关函数,可根据自身情况选择性学习
代码在2.2.2.2 顺序表的查找一节
2.2.2.1 顺序表的插入删除
概念
2.2.2.2 顺序表的查找
/**
* @Description: 顺序表
* @author: HouBo
* @Date: 2020/11/26 17:52
*/
public class SqList {
static final int MAXSIZE = 10; // 最大长度
int data[] = new int[MAXSIZE]; // 使用静态数组存放数据
int length; // 顺序表的当前长度
// 初始化一个顺序表
void initList(SqList L){
L.length = 8; // 初始长度为0
}
// 添加一个数据
boolean listInsert(SqList L, int i, int e){
if(i < 1 || i > L.length + 1){
// 当前i的值是否有效
return false;
}
if(L.length == MAXSIZE){
// 当前存储空间是否充盈
return false;
}
for(int j = L.length; j >= i; j--){
// 将第i个元素之后的元素后移
L.data[j] = L.data[j - 1];
L.data[j - 1] = e; // 在位置i处放入e
}
L.length++; // 长度+1
return true;
}
// 删除一个数据
boolean listDelete(SqList L, int i, int e){
if(i < 1 || i > L.length + 1){
// 当前i的值是否有效
return false;
}
e = L.data[i - 1];
for(int j = i; j <= L.length; j++){
// 将第i个元素之后的元素后移
L.data[j - 1] = L.data[j]; // 在位置i处放入e
}
L.length--; // 长度-1
return true;
}
// 按位查找
int getElem(SqList L, int i){
if(i < 1 || i > L.length + 1){
return 0;
}
return L.data[i - 1];
}
// 按值查找
int locateElem(SqList L, int e){
for(int i = 0; i < L.length; i++){
if(L.data[i] == e){
return i + 1;
}
}
return 0;
}
}
此段代码非最优解,仅作练习使用
概念
2.3.1 单链表的定义
1. 什么是单链表
相对于顺序表,单链表除了存储数据元素外,还需要存储指向下一个节点的指针
2. 单链表和顺序表的不同
顺序表 | 单链表 | |
---|---|---|
优点 | 随机存取,存储密度高 | 不要求大片连续空间,改变容量方便 |
缺点 | 要求大片连续空间,改变容量不方便 | 不可随机存取,要耗费一定空间存放指针 |
带头结点和不带头结点的区别
创立第一个结点,既为带头结点,反之则为不带
不带头结点,对第一个数据结点和后续数据结点的处理需要用不用的代码逻辑,空表和非空表的处理同理
带头结点通常情况下优于不带
概念
代码放在2.3.2.3 单链表的建立一节中
2.3.2.1 单链表的插入删除
此章中,p通过代指当前结点,s指新插入的结点, a1指下一个结点, e指新值
1.1 按位序插入(带头结点)
思路:插入结点打断链表,上一个结点挂到它身上(指向它),它挂到下一个结点身上。
链表插入删除操作和顺序表相比相对复杂,需多多思考。
1.2 按位序插入(不带头结点)
不带头结点第一个结点需要做特殊处理,创建一个s,将第a1挂到它身上。
2. 指定结点的后插操作
a1挂到s身上,s挂到p身上。
3. 指定结点的前插操作
由于无法直接获取p的上一个结点,因此可采用如下方法实现:
思路:将p变更为s, s变更为p
实现:p的值赋给s,新值e赋给p
4. 按位序删除
思路:找到要删除的结点,断开它并释放空间
5. 指定结点的删除
思路:同上
实现:将p的值和下一个引用都指向下一个,实现删除p的效果
此处如果p是最后一个结点,将会出现异常
单链表的局限性:无法逆向检索,不方便
6. 概念
2.3.2.2 单链表的查找
概念
2.3.2.3 单链表的建立
思路:遍历所有结点,通过头插或者尾插的方式实现循环建立一个单链表。
头插法可以实现反转链表,注意这是个重点
代码
/**
* @Description: 结点
* @Author: MoChen
*/
public class HNode {
public HNode(){
}
public HNode(int data){
this.data = data;
}
// 数据
int data;
// 下一个结点
HNode next;
}
/**
* @Description: 单链表
* @Author: MoChen
*/
public class HLinkerList {
// 链表的头结点
HNode head = null;
// 初始化一个空链表
boolean InitList(HLinkerList L){
L.head = new HNode(); // 分配一个头结点,如果不带头结点此处直接赋空
L.head.next = null; // 头结点之后暂时没有结点
return true;
}
// 在第i个位置插入元素e
boolean ListInsert(HLinkerList L, int i, int e) {
if(i < 1) {
return false;
}
// 以下代码段为不带头结点的操作
// if(i == 1) { // 插入第一个结点的操作与其他结点操作不同
// HNode s = new HNode();
// s.data = e;
// s.next = L.head.next;
// L.head = s; // 头指针指向新结点
// }
// HNode p; // 指针p向前扫描到的结点
// int j = 0; // 当前p指向的是第几个结点,不带的话此处赋1
// p = L.head; // L指向头结点,头结点是第0个结点
// while (p != null && j < i - 1) { // 循环找到 i - 1 个结点
// p = p.next;
// j++;
// }
HNode p = GetElem(L, i - 1);
// if(p == null){ // i值不合法
// return false;
// }
// HNode s = new HNode();
// s.data = e;
// s.next = p.next;
// p.next = s; // 将结点s连到p之后
// return true; // 插入成功
return InsertNextNode(p, e);
}
// 后插操作:在p结点之后插入元素e
boolean InsertNextNode(HNode p, int e){
if(p == null){
return false;
}
HNode s = new HNode();
s.data = e; // 新结点s存储数据e
s.next = p.next;
p.next = s; // 新结点s挂到p身上
return true;
}
// 前插操作:在p结点之前插入元素e
boolean InsertPriorNode(HNode p, int e){
if(p == null){
return false;
}
HNode s = new HNode();
s.next = p.next;
p.next = s; // 新结点s挂到p身上
s.data = p.data; // 将p里面的元素复制到s中
p.data = e; // p中的元素覆盖为e
return true;
}
// 按位序删除
boolean ListDelete(HLinkerList L, int i, int e){
if(i < 1){
return false;
}
// HNode p; // 指针p向前扫描到的结点
// int j = 0; // 当前p指向的是第几个结点,不带的话此处赋1
// p = L.head; // L指向头结点,头结点是第0个结点
// while (p != null && j < i - 1) { // 循环找到 i - 1 个结点
// p = p.next;
// j++;
// }
HNode p = GetElem(L, i - 1);
if(p == null){
// i值不合法
return false;
}
if(p.next == null){
// 第i - 1个结点后面已无其他结点
return false;
}
// HNode q = p.next; // 令q指向被删除的结点
// e = q.data; // 用e返回元素的值
// p.next = q.next; // 将q结点从链中断开
// return true; // 删除成功
return DeleteNode(p);
}
// 删除指定结点p
boolean DeleteNode(HNode p){
if(p == null){
return false;
}
HNode q = p.next; // 令q指向p的后继结点
p.data = p.next.data; // 和后继结点交换数据域
p.next = q.next; // 将q结点从链中断开
return true;
}
// 按位查找,返回第i个元素
HNode GetElem(HLinkerList L, int i){
if(i < 0){
return null;
}
HNode p; // 指针p向前扫描到的结点
int j = 0; // 当前p指向的是第几个结点,不带的话此处赋1
p = L.head; // L指向头结点,头结点是第0个结点
while (p != null && j < i) {
// 循环找到 i 个结点
p = p.next;
j++;
}
return p;
}
// 按值查找
HNode LocateElem(HLinkerList L, int e){
HNode p = L.head.next;
// 从第一个结点开始查找数据域为e的结点
while(p != null && p.data != e){
p = p.next;
}
return p; // 找到后返回该结点,否则返回null
}
// 求表的长度
int Length(HLinkerList L){
int len = 0;
HNode p = L.head;
while(p.next != null){
p = p.next;
System.out.println("链表第" + len + "个数据:" + p.data);
len++;
}
return len;
}
}
此段代码非最优解,仅作练习使用
2.3.3 双链表
下文中,s指代新结点,p指代当前结点,q指代下一个结点
1. 双链表和单链表的概念
单链表:无法逆向检索,有时候不太方便
双链表:可进可退,存储密度更低
双链表整体和单链表差距不大,额外新增了一个指向上一个结点的处理
2. 双链表的插入
思路:和单链表同理,额外增加了prior的处理
① q挂到s身上
② q的prior指向s
③ s的prior指向p
④ s挂到p身上
如果q不存在,第二步需省略
3. 双链表的删除
① q的下一个结点挂到p身上
② q的下一个结点的prior指向p
代码
/**
* @Description: 结点(双链表)
* @Author: MoChen
*/
public class HDNode {
public HDNode(){
}
public HDNode(int data){
this.data = data;
}
// 数据
int data;
// 下一个结点
HDNode next;
// 上一个结点
HDNode prior;
}
/**
* @Description: 双链表
* @Author: MoChen
*/
public class HDLinkedList {
// 链表的头结点
HDNode head = null;
// 初始化一个空链表
boolean InitList(HDLinkedList L){
L.head = new HDNode(); // 分配一个头结点,如果不带头结点此处直接赋空
L.head.prior = null; // 头结点的prior永远指向空
L.head.next = null; // 头结点之后暂时没有结点
return true;
}
// 在p结点之后插入s结点
boolean InsertNextHDNode(HDNode p, HDNode s){
if(p == null || s == null){
// 非法参数
return false;
}
s.next = p.next;
if(p.next != null){
// 如果p后面有结点
p.next.prior = s;
}
s.prior = p;
p.next = s;
return true;
}
// 删除p结点的后继结点
boolean DeleteNextNode(HDNode p){
if(p == null){
return false;
}
HDNode q = p.next; // 找到p的后继结点q
if(q == null){
return false; // p没有后继
}
p.next = q.next;
if(q.next != null){
// q结点不是最后一个结点
q.next.prior = p;
}
return true;
}
// 求表的长度
int Length(HDLinkedList L){
int len = 0;
HDNode p = L.head;
while(p.next != null){
p = p.next;
System.out.println("链表第" + len + "个数据:" + p.data);
len++;
}
return len;
}
}
概念
2.3.4 循环链表
1. 循环链表与普通链表的不同
循环链表的尾结点的指针指向头结点,而普通链表的尾结点指向空。
通常我们无法获取之前的结点,除非拿到头结点,循环单链表可以通过遍历结点的方式从而拿到自己想要的结点。
以前在增加/删除操作时,需要给最后一个结点判空,现在则不需要。
2. 循环双链表
表头的prior指向表尾,表尾的next指向表头。
概念
2.3.5 静态链表
1. 什么是静态链表
单链表:各个结点在内存中随意散落。
静态链表:分配一整片连续的内存空间,各个结点集中安置。
2. 静态链表的概念
静态链表就是用数组的方式实现的链表。
容量固定不可变
本章内容不是很重要,了解其概念即可。
2.3.6 顺序表和链表的比较
1. 线性结构
都属于线性结构
2. 存储结构
顺序表 | 单链表 | |
---|---|---|
优点 | 随机存取,存储密度高 | 不要求大片连续空间,改变容量方便 |
缺点 | 要求大片连续空间,改变容量不方便 | 不可随机存取,要耗费一定空间存放指针 |
3. 基本操作
操作 | 顺序表 | 链表 |
---|---|---|
创 | 需要分配大片连续空间。若分配空间过小,则之后不方便扩展;若分配空间过大,则浪费内存资源 | 只需分配一个头结点(也可不分配),方便扩展 |
销 | 长度赋0即可,本笔记采用java实现,因此无需考虑内存问题,如果使用c实现则需要通过free函数释放内存 | 依次删除每个结点 |
增,删 | 插入/删除元素都要将后续元素全部后移/前移 时间复杂度O(n),时间开销主要来自移动元素 | 插入/删除元素只需修改指针即可 时间复杂度O(n),时间开销主要来自查找元素 |
查 | 按位查找:O(1) | 按值查找:O(n) |
4. 概念
表长无法预估,需要经常进行增/删操作,可使用链表
表长可预估,需要经常进行查询操作,可使用顺序表
第三章
3.1.1 栈的基本概念
1. 栈(Stack)的概念
栈是只允许在一端进行插入或删除的线性表
重要术语:栈顶,栈底,空栈
特点:后进先出(Last In First Out)LIFO
2. 栈的基本操作
InitStack(S); // 初始化栈。构造一个空栈,分配内存空间。
DestroyStack(L); // 销毁栈。销毁并释放栈s所占用的内存空间。
Push(S, x); // 进栈,若栈S未满,则将x加入使之成为新栈顶。
Pop(S, x); // 出栈,若栈S非空,则弹出栈顶元素,并用x返回。
GetTop(S, x); // 读栈顶元素。若栈S非空,则用x返回栈顶元素。
// 其他常用操作
StackEmpty(S); // 判断一个栈是否为空
概念
3.1.2 栈的顺序存储实现
思路
给定一个静态数组,每次操作头或者尾即可
指针用于操纵当前数据
代码
/**
* @Description: 顺序存储实现栈
* @Author: MoChen
*/
public class HSqStack {
final int MAXSIZE = 10; // 定义栈中元素的最大个数
int[] data; // 静态数组中放栈中元素
int top; // 栈顶指针
HSqStack(){
data = new int[MAXSIZE];
}
// 初始化栈
void InitStack(){
top = -1; // 此处如果指向0,则预示已经有一个参数了,所以初始化指向-1
}
// 判断栈空
boolean StackEmpty(){
return top == -1;
}
// 入栈
boolean Push(int x){
if(top == MAXSIZE - 1){
// 栈满
return false;
}
top++; // 指针上移一位
data[top] = x; // 新元素入栈
return true;
}
// 出栈
boolean Pop(int x){
if(top == - 1){
// 空栈
return false;
}
x = data[top]; // 栈顶元素先出栈
top--; // 指针下移
return true;
}
// 读取栈顶元素
int GetTop(int x){
if(top == - 1){
// 空栈
return -1;
}
x = data[top]; // 读取栈顶元素
return x;
}
}
概念
3.1.3 栈的链式存储实现
用链表的方式实现栈几乎等同于单链表,此处直接套用单链表即可
3.2.1 队列的基本概念
1. 什么是队列
栈(Stack)是只允许在一端进行插入或删除的线性表
队列(Queue)是只允许在一端插入,在另一端删除的线性表
队列的特点:先进先出 First In First Out (FIFO)
2. 队列的基本操作
InitQueue(Q); // 初始化队列,构造一个空队列Q
DestroyQueue(Q); // 销毁队列,销毁并释放队列Q所占用的空间
EnQueue(Q, x); // 入队,若队列Q未满,将x加入,使之成为新的队尾
DeQueue(Q, x); // 出队,若队列Q非空,删除队头元素,并用x返回
GetHead(Q, x); // 读队头元素,对队列Q非空,则将队头元素赋值给x
QueueEmpty(Q); // 判断队列是否为空
概念
3.2.2 队列的顺序实现
要点
求队列一共有多少个元素的算法如下:
// 尾指针 + 长度 - 头指针 % 长度
(rear + MAX_SIZE - front) % MAX_SIZE
头尾指针均为长度以内任意一个数字,因此想要求差必须先加上最大长度
尾指针和头指针位置可调换
概念上不会大于长度,保证代码健全性加上模运算
代码
/**
* @Description: 顺序队列
* @Author: MoChen
*/
public class HSqQueue{
private static class SqQueue{
private final int MAX_SIZE = 10;
int[] data; // 静态数组存放元素
int front, rear; // 队头指针和队尾指针
public SqQueue(){
data = new int[MAX_SIZE];
}
}
private SqQueue Q;
// 初始化队列
void initQueue(){
Q = new SqQueue();
Q.front = Q.rear = 0; // 初始时,队头队尾全部指向空
}
// 判断队列是否为空
boolean queueEmpty(){
return Q.front == Q.rear; // 当队头队尾指向一样时,则意味队列为空
}
// 入队
boolean enQueue(int x){
if((Q.rear + 1) % Q.MAX_SIZE == Q.front){
// 如果队头的指针的下一位指向队尾,则表示队满
return false;
}
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % Q.MAX_SIZE; // 队尾指针+1取模,保证指针不会超过长度
return true;
}
// 出队
boolean deQueue(){
if(Q.front == Q.rear){
// 队列为空
return false;
}
Q.data[Q.front] = -1;
Q.front = (Q.rear + 1) % Q.MAX_SIZE; // 队尾指针+1取模,保证指针不会超过长度
return true;
}
// 获取队头元素
int getHead(){
if(Q.front == Q.rear){
// 队列为空
return -1;
}
int x = Q.data[Q.front];
return x;
}
// 获取队列长度
int queueLength(){
return (Q.rear + Q.MAX_SIZE - Q.front) % Q.MAX_SIZE;
}
}
代码不完善,仅作练习使用
概念
3.2.3 队列的链式实现
概念
链式无需考虑内存
代码
/**
* @Description: 链式队列
* @Author: MoChen
*/
public class HLinkQueue {
private class LinkNode{
int data;
LinkNode next;
public LinkNode(){
}
public LinkNode(int data) {
this.data = data;
}
}
private class LinkQueue{
LinkNode front, rear;
}
private LinkQueue Q;
// 初始化队列
void initQueue(){
// 初始时,front,rear都指向头结点
Q.front = Q.rear = new LinkNode();
// 不带头结点的话front和rear都要指向空
Q.front.next = null;
}
// 判断队列是否为空
boolean isEmpty(){
return Q.front == Q.rear;
}
// 入队
boolean enQueue(int x){
LinkNode s = new LinkNode(x);
s.next = null;
// 如果不带头结点则front需要进行非空判断,为空则修改表头指针
Q.rear.next = s; // 新结点挂到rear身上
Q.rear = s; // 修改表尾指针
return true;
}
// 出队
boolean deQueue(){
if(Q.rear == Q.front){
// 空队列
return false;
}
LinkNode p = Q.front.next;
Q.front.next = p.next; // 修改头结点的next指针
if(Q.rear == p){
// 如果是最后一个结点
Q.rear = Q.front;
}
return true;
}
}
概念
3.2.4 双端队列
概念
栈:单端插入和删除的线性表
队列:一端插入,另一端删除的线性表
双端队列:两端插入,两端删除的线性表
双端队列包含输入受限和输出受限两种情况,不同情况具有不同局限性,需根据情况选择
概念
3.3.1 栈在括号匹配中的应用
示例:
一组括号,求出它们能不能完成匹配
思路:
使用栈,将左括号压入,如果遇到右括号就压出,遇到无法匹配的括号或者匹配结束栈里还有值则匹配失败
代码
/**
* @Description: 给定一组括号,判断这组括号能否完成匹配
* @Author: MoChen
*/
public class Brancket {
static String bracketStr = "({}{})";
public static void main(String[] args) {
System.out.println(bracketCheck(bracketStr));
}
static boolean bracketCheck(String bracketStr){
char[] data = new char[bracketStr.length()]; // 栈元素
int top = 0; // 栈顶指针
char[] strs = bracketStr.toCharArray();
for(int i = 0; i < strs.length; i++){
if(strs[i] == '[' || strs[i] == '{' || strs[i] == '('){
// 扫描到左括号,入栈
data[++top] = strs[i];
}else{
if(top == 0){
// 扫描到右括号且当前栈空,匹配失败
return false;
}
char topElem = data[top--]; // 栈顶元素出栈
// 进行匹配,如果匹配失败则返回false
if(strs[i] == ')' && topElem != '('){
return false;
}
if(strs[i] == ']' && topElem != '['){
return false;
}
if(strs[i] == '}' && topElem != '{'){
return false;
}
}
}
return top == 0; // 括号校验完成且栈空则匹配成功
}
}
3.3.2 栈在表达式求值中的应用(上)
三种表达式的不同
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fcNMwH0u-1618913822522)(https://i.loli.net/2021/02/01/2GOCL9hukZWgNse.png)]
中缀表达式
运算符在中间,既为我们常用的数学表达式
后缀表达式
运算符在后面,又叫逆波兰表达式(Reverse Polish notation)
前缀表达式
运算符在前面,因为是波兰人提出,所以又叫波兰表达式(Polish notation)
中缀表达式 | 后缀表达式 | 前缀表达式 |
---|---|---|
a + b | ab + | + ab |
a + b - c | a b + c - | - + ab c |
a + b - c * d | ab + cd * - | - + ab * cd |
核心思想还是一个计算区间一块区域,诸如(a + b) - (c * d) = (ab+)(cd*) -
中缀转后缀
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UCf07h9Q-1618913822523)(https://i.loli.net/2021/02/01/t2GmHCKyBF59gvq.png)]
左优先原则:只要左边的运算符能先计算,就先计算左边的
后缀表达式的手算方法
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应操作,合并为一个操作数
对于计算机而言,后缀表达式的效率往往要更高
中缀表达式转前缀表达式
右优先原则:只要右边的操作符能先计算,就优先计算右边的
中缀
A + B * (C - D) - E / F
常规计算
- + A * B - C D / E F
右优先
+ A - * B - C D / E F
后缀表达式的机算方法
① 从左到右扫描下一个元素,直到处理完所有元素
② 若扫描到操作数则入栈
③ 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果入栈
概念
3.3.2 栈在表达式求值中的应用(下)
中缀转后缀
① 遇到操作数,直接加入后缀表达式
② 遇到界限符。遇到 ( 直接入栈,遇到 ) 则依次弹出栈内运算符并加入后缀表达式,直到弹出 ( 为止。注意( 不加后缀
③ 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到 ( 或栈空则停止。之后再将当前运算符入栈。
中缀的计算
中缀转后缀 + 后缀求值
① 初始化两个栈,操作数栈和运算符栈
② 若扫描到操作数,压入操作数栈
③ 若扫描到运算符或界限符,则按照‘中缀转后缀’相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要在弹出两个操作数栈的栈顶元素并执行相应运算,运算结果在压回操作数栈)
代码
因时间原因,此处仅列出中转后和后计算
import java.util.Stack;
import java.util.regex.Pattern;
/**
* @Description: 后缀表达式
* @Author: MoChen
*/
public class PolishNotation {
// 中缀表达式
private static String notation = "((15 / (7 - (1 + 1))) * 3) - (2 + (1 + 1))";
// 后缀表达式
private static String poNotation = "15 7 1 1 + - / 3 * 2 1 1 + + -";
// 前缀表达式
private static String rpoNotation = "- * / 15 - 7 + 1 1 3 + 2 + 1 1";
// 10的倍数
private static int[] exactNums = {
10, 100, 1000, 10000, 100000};
public static void main(String[] args) {
System.out.println("中计算:" + countNum(notation));
System.out.println("中转后:" + commTransRpo(notation));
System.out.println("后计算:" + poCount(poNotation));
}
/**
* 中缀转后缀
*/
static String commTransRpo(String notation