栈的基本概念:栈的 定义和基本操作。
也就是从逻辑结构和运算上来看,栈(stack)是只允许在一端进行插入删除操作的线性表,逻辑结构上与线性表相同,只是运算上有区别。运算有:创销增删改查
特点是后进先出(LIFO= last in first out)
栈的出栈顺序(数学性质):出栈元素不同的排列顺序个数有
栈的顺序存储结构
顺序栈分为动态分配和静态分配。静态分配有一个数组和一个指向栈顶的指针;动态分配有两个指针,分别指向栈顶和栈底。
缺点:顺序栈可能上溢,
栈顶指针:S.top,初始时设置S.top=-1
进栈:栈顶指针加一,值进栈
出栈:取值后指针减一
栈空:S.top==-1 栈满:S.top==Maxsize-1; 栈长:S.top+1
(也可以将S.top定义为0,此时规定top指向栈顶元素下一单元)
共享栈:利用栈底位置相对不变的 特性,可以让两个顺序栈共享一个数组空间,即将两个栈的栈底分别设置在数组的两端,栈顶向中间延伸。
优点是有效利用空间,两空间相互调节,只要空间被占满才会发生上溢
链式存储结构
优点:便于多个栈共享空间和提高效率;不存在上溢
头结点创建单链表==进栈; 对头结点的”后删”操作==出栈
链栈的 实现方式分为带头结点的和不带头结点的,推荐不带头结点的,因为链头作为栈顶,进出站都只能在栈顶一端进行。
队列
队列的基本概念:队列的 定义和基本操作。
也就是从逻辑结构和运算上来看,队列(Queue)是只允许在一端进行插入,而在另一端删除操作的线性表,逻辑结构上与线性表相同,只是运算上有区别。特点:先进先出(FIFO)
运算有:创销增删改查
注意:不是任何对线性表的操作都适用于栈和队列,例如,不可以随便读取其中数据
顺序存储
数组附设两个指针,队头指针指向队头,队尾指针指向队尾的下一位置
缺点:我们注意到,不能用Q.rear==Maxsize作为队满标志,若整个队列只有队尾一个元素仍表示队满,这是一种假溢出
改进:循环队列
对于循环队列中区分队满还是队空有三种解决方式:1、牺牲一个单元来区分队满和队空,这是一种普遍做法,队头指针在队尾指针的下一位置作为队满标志。2、类型中增设表示元素个数的成员,根据Q.size()的大小来判断。3、类型中增设tag作为标记,以0/1区分队满队空,若因插入导致的Q.rear==Q.front则为队满,若因删除导致则为队空。
链式存储结构——链队
实质是一个带有队头指针和队尾指针的单链表
通常将链队设为带头结点的单链表,这样插入和删除操作就统一了。
双端队列
是指允许两端都进行入队和出队的队列
输出受限的双端队列 和 输入受限的 双端队列
例如,end1端输入,输入序列为1 2 3 4;输出序列按照4×3×2×1来说有24种,而普通队列的输出仅有1种,end1端按照栈的输出有14种,还有10种不能输出,但通过end1和end2端混合输出可以实现8种。
栈和队列的应用
栈在括号匹配中应用、栈在表达式求值中应用
- 中缀表达式不不仅要依赖运算符的优先性,还要考虑括号;就是我们日常使用的表达式
- 后缀表达式——逆波兰表达式,是运算符在操作数的后面,已考虑的运算符的优先级,没有括号。
- 中缀和后缀也对应这二叉树的中序遍历和后序遍历。
中缀和后缀的转化——:
1、用两个数组存放。一个数组用来临时存放后缀表达式,另一个临时存放操作数,相当于中转站。如果是数字直接入栈。若是左括号之间入栈
2、将中缀表达式转化为二叉树:叶子结点都是操作数,非叶子结点都是运算符,树根的运算符优先级最低;中缀相当于中序遍历,然后通过后序遍历将其写出来即可得到后缀表达式。
3、括号法:
1. 按照运算符的优先级对所有的运算单位加括号;((a+(b*c))+(((d*e+f)*g))
2. 转换前缀与后缀表达式。后缀:把运算符号移动到对应的括号后面
变成:((a(bc)*)+(((de)*f)+g)*)+
把括号去掉:abc*+de*f+g *+后缀式子出现
后续表达式的求值过程:顺序扫描表达式的每一项,若该项是操作数则压入栈中;若该项是操作符,则连续从栈中退出两个操作数,进行运算,并将结果重新压入栈中;直到遍历完,遇到‘\0’,栈顶即为计算结果。
代码
因为中缀表达式也就是日常使用的表达式,便于我们理解,但是在写算法求值时比较复杂,而后缀表达式更方便计算机的运算,算法相对简单些;所以表达式求值中常用的方法是将中缀表达式转化成后缀表达式(逆波兰表达式),再对后缀表达式进行求值。
1、如何使中缀转换成后缀?
答:
基于栈的算法:遍历表达式,若遇到操作数直接输出;若遇到操作符,分三种情况:若栈为空,直接入栈,若该操作符优先级小于栈顶的操作符,则将栈顶操作符输出,再次比较直至操作符优先级大于栈顶操作符,入栈;若遇到左括号直接入栈,若遇到右括号,将栈中操作符依次输出直到遇到左括号,将左括号弹出(不输出);若输入的表达式遍历完读到‘\0’,将栈中元素依次输出。
括号法:按照运算符的优先级对所有运算符单位加括号;把运算符号移到对应括号的后面,再把括号去掉,即得到后缀表达式。
利用二叉树:将中缀表达式转化为二叉树:叶子结点都是操作数,非叶子结点都是运算符,树根的运算符优先级最低;中缀相当于中序遍历,然后通过后序遍历将其写出来即可得到后缀表达式。
2、后缀表达式如何求值?
答:用栈来辅助存储。先遍历表达式;若该项是操作数则压入栈中,若该项是操作符,则连续从栈中退出两个操作数,进行运算,并将结果重新压入栈中;直到遍历完遇到‘\0’,栈顶即为计算结果。
栈和递归
递归解决的是有依赖顺序关系的多个问题。递归的精髓思想就是把规模大的原始问题转化为规模小的相似的子问题来解决。
递归的本质:在底层对线程栈进行压栈出栈。
递归三要素:递归终止条件、给出递归终止时的处理办法、提取重复逻辑,缩短问题规模
递归的编程模型可分为在递去中解决问题和在归来中解决问题。
function recursion(大规模){
if (end_condition){ // 明确的递归终止条件
end; // 简单情景
}else{ // 在将问题转换为子问题的每一步,解决该步中剩余部分的问题
solve; // 递去
recursion(小规模); // 递到最深处后,不断地归来
}
}
function recursion(大规模){
if (end_condition){ // 明确的递归终止条件
end; // 简单情景
}else{ // 先将问题全部描述展开,再由尽头“返回”依次解决每步中剩余部分的问题
recursion(小规模); // 递去
solve; // 归来
}
}
递归的应用场景:
在我们实际学习工作中,递归算法一般用于解决三类问题:
(1). 问题的定义是按递归定义的(Fibonacci函数,阶乘,…);
(2). 问题的解法是递归的(有些问题只能使用递归方法来解决,例如,汉诺塔问题,…);
(3). 数据结构是递归的(链表、树等的操作,包括树的遍历,树的深度,…)。
递归与循环作比较:
递归是很直白的描述了一个问题的解决过程,是最容易想到的解决方式;循环并不会那么清晰的描述解决问题的步骤。所以递归比较直观,大大减小代码量。而递归因为函数调用的开销,效率比循环低。
将递归转化成非递归一般需要两步:1)借助栈来保存某些内容以便代替系统栈。2)把对递归的调用转化为对循环的处理。
队列在层次遍历中应用
队列在计算机系统中应用
1、解决主机与外部设备之间速度不匹配的问题
例,主机输出数据给打印机打印,输出数据的速度比打印速度快得多。解决方法就是设置一个打印数据的缓冲区,主机把数据写入缓冲区中,写满就暂停输出,转去做其他事情;打印机此时根据队列中的先进先出原则取出数据,打印完后再向主机发出请求。——缓冲区存储数据方式采用队列。
2、解决由多用户引起的资源竞争问题
例,在一个计算机系统上,多个用户需要CPU运行程序,操作系统按照每个请求在时间上的先后顺序,把他们排成队列