数据结构与算法A 第三章栈与队列
- 栈和队列是典型的线性结构,常被称为“操作受限的线性表”或“限制存取点的线性表”
栈
栈是一种限定仅在限制在一端进行插入和删除的线性表,无论是插入元素还是删除读取栈中的元素,都只能固定在线性表的一断进行。这一端被称为栈顶(top),另一端叫做栈底(bottom)
最后插入栈中的元素是最先被删除或读取的元素,而最先压入的元素则被放在栈的底部。栈的修改是按后进先出的原则进行(Last-In First-Out,LIFO),也称“下推表”
push操作:往栈中插入元素,简称压栈或入栈;pop操作:删除栈顶元素,简称出栈或弹出
栈的ADT
1 | template <class T> // 栈的元素类型为 T |
顺序栈
顺序栈:采用顺序存储结构的栈,需要一块连续的区域来存储栈中的元素
顺序栈本质上简化的顺序表,对元素数目为n的栈,首先需要确定哪一端表示栈顶
- 0位置作为栈顶:插入和删除都在第0个位置进行,push和pop都需要把当前栈的所有元素在数组中后移或前移一个,时间代价O(n)
- n-1位置作为栈顶,只需将新元素添加到表尾,出栈也只需删除表尾,时间代价O(1)
顺序栈实现时,整型变量top既可以只是当前的栈顶位置,也可以表示当前栈中的元素的个数
1 | template <class T> |
top可以有两种定义方式
- 设置为栈中第一个空闲位置,即空栈的top=0
- 把top定义为栈中最上面的那个元素的位置,而非第一个空闲位置,top=-1或任何非自然数
push一个元素首先把top+1,然后把新元素插入到新的栈顶位置。同样pop先删除栈顶元素,再把top-1
顺序栈的溢出:
- 当栈中已有mSize个元素时,进栈操作会产生上溢出(overflow)
- 在空栈上进行出栈操作则会造成下溢出(underflow)
入栈
1 | bool arrStack<T>::push(const T item) |
- 出栈
1 | bool arrStack<T>::pop(T & item) |
- 读取栈顶
1 | bool arrStack<T>:: top(T & item) |
- 清空栈
1 | void arrStack<T>::clear() |
- 判断栈满
1 | bool arrStack<T>:: isFull() |
- 双栈共享一个栈空间
链式栈
链式栈本质上是简化的链表,为了方便读取,栈顶元素是链表头。指针方向:从栈顶到栈底
只能在链表头部进行操作,故链表没有必要像单链表那样附加头结点(head),栈顶指针就是链表的头指针(top)
无栈满问题(但存在栈空约束)
push和pop操作的时间代价均为O(1)
栈的链式实现
1 | template <class T> |
- 压栈
1 | // 入栈操作的链式实现 |
- 出栈
1 | bool lnkStack<T>:: pop(T& item) |
顺序栈和链式栈的比较:
- 时间效率:所有操作都只需常数时间,顺序栈和链式栈在时间效率上难分伯仲
- 空间效率:顺序栈须说明一个固定的长度;链式栈的长度可变,但增加结构性开销
栈的应用:
- 递归
- 表达式求值
表达式求值
表达式是由常量、变量、运算符、函数调用按一定规则组合而成的
表达式的组成:
- 基本符号集:由{0,1,…,9,+,-,*,/,(,)} 等16个基本符号组成
- 语法成分集:由{ <表达式> , <项> , <因子> , <常数> , <数字> } 5个语法成分组成
- 语法公式集 :“::=”是规则定义符
中缀表达式
表达式的分类:
- 中缀表示:<操作数> <操作符> <操作数>,如 A+B
- 前缀表示:<操作符> <操作数> <操作数>,如 +AB
- 后缀表示:<操作数> <操作数> <操作符>,如 AB+
中缀表达式的递归定义:
- <表达式> ∷ = <项> + <项> | <项> - <项> | <项>
- <项> ∷ = <因子> * <因子> |<因子> / <因子> |<因子>
- <因子> ∷ = <常数> | ( <表达式> )
- <常数> ∷ = <数字> |<数字> <常数>
- <数字> ∷ = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
中缀表达式的计算次序
- 先执行括号内的计算,后执行括号外的计算,在具有多层括号时,按层次反复地脱括号
- 在无括号或同层括号时,先乘(*) 、除(/),后作加(+)、减(-)
- 在同一个层次,若有多个乘除(*、/)或加减 (+,-)的运算,那就按自左至右顺序执行
后缀表达式
- 后缀表达式(逆波兰式)不含括号,所有求值计算皆按运算符出现的顺序,严格从左向右进行
中缀表达式到后缀表达式的转换
关键在于如何恰当地去掉中缀表达式中的括号,此过程中用栈储存有关的元素
思路:用栈来存放表达式中的操作数、开括号以及在这个开括号后面的其他暂时不能确定计算次序的内容
- 当输入是操作数,直接输出到后缀表达式序列
- 当输入的是左括号时,也把它压栈
- 当输入的是运算符时,While (以下循环)
If(栈非空 and 栈顶不是左括号 and 输入运算符的优先级 “≤”栈顶运算符的优先级)时
将当前栈顶元素弹栈,放到后缀表达式序列中(此步反复循环,直到上述if条件不成立); 将输入的运算符压入栈中。
Else 把输入的运算符压栈(>当前栈顶运算符才压栈!) - 当输入的是右括号时,先判断栈是否为空
若为空(括号不匹配),异常处理,清栈退出;
若非空,则把栈中的元素依次弹出
遇到第一个左括号为止,将弹出的元素输出到后缀表达式的序列中(弹出的开括号不放到序列中)
若没有遇到开括号,说明括号也不匹配,做异常处理,清栈退出 - 最后,当中缀表达式的符号序列全部读入时,若栈内仍有元素,把它们全部依次弹出,都放到后缀表达式序列尾部
后缀表达式求值
- 思路:利用栈来记录后缀表达式中的两个操作数
- 当遇到的是操作数,则压入栈顶
- 当遇到的是运算符, 就从栈中两次取出栈顶,按照运算符对这两个操作数进行计算。然后将计算结果压入栈顶
- 如此继续,直到遇到符号=, 这时栈顶的值就是输入表达式的值
1 | // Class Declaration 类的说明 |
栈与递归
递归:在调用一个函数的过程中又直接调用或间接调用了函数自身
递归的组成
- 递归基础(递归出口):保证递归结束的前提
- 递归规则:确定了由简单情况构筑复杂情况需要遵循的规则
函数运行时的存储分配
- 静态分配
a. 在非递归情况下,数据区的分配可以在程序运行前进行,一直到整个程序运行结束才释放,这种分配称为静态分配
b. 采用静态分配时,函数的调用和返回处理比较简单,不需要每次分配和释放被调函数的数据区 - 动态分配
a. 在递归(函数)调用的情况下,被调函数的局部变量不能静态地分配某些固定单元,而必须每调用一次就分配一份,以存放当前所使用的数据,当返回时随即释放。【大小不确定,值不确定】
b. 动态分配在内存中开辟一个称为运行栈的足够大的动态区
- 静态分配
动态存储分配:用作动态数据分配的存储区,分为堆(heap)和栈(stack)
- 堆区域则用于不符合LIFO(诸如指针的分配)的动态分配
- 栈用于分配发生在后进先出LIFO风格中的数据(诸如函数的调用)
运行栈又称活动记录栈也称调用栈
- 调用一个函数时,执行进栈操作,把被调函数所对应的活动记录分配在站的顶部
- 从函数返回时,执行出栈操作,释放本次的活动记录,恢复到上次调用所分配的数据区中
一个函数在运行栈上可以有若干不同的活动记录,每个都代表了一个不同的调用
队列
队列也是一种限制访问点的线性表。队列的元素只能从表的一端插入,另一端删除。把只允许删除的一端称为队列的头,简称队头,把删除操作称为出队。称表的另一端为队列的尾,简称队尾,这一端只能进行插入操作,称为入队
队列的特性:先进先出(FIFO, First In First Out)
队列的ADT
1 | template <class T> |
顺序队列
用顺序存储结构实现队列就形成了顺序队列,需要分配一块连续的区域来存储队列的元素,需要事先知道大小
顺序队列也存在溢出问题,队列满时入队会产生上溢,队列为空时出队则会产生下溢
用向量存储队列元素,用两个变量分别指向队列的前端(front)和尾端(rear)
- front:指向当前待出队的元素位置(地址)
- rear: 指向当前待入队的元素位置(地址)
假溢出:当 rear = mSize-1 时,再作插入运算就会产生溢出,如果这时队列的前端还有许多空位置,这种现象称为假溢出
队列的类定义
1 | class arrQueue: public Queue<T> |
- 入队列操作
1 | bool arrQueue<T> :: enQueue(const T item) |
- 出队列操作
1 | bool arrQueue<T> :: deQueue(T& item) |
链式队列
链式队列是队列的链式实现,是对链表的简化。链接的方向从队列的前端指向队列的尾端。成员front和rear是分别指向队首和队尾的指针,增加成员size来表示队列中当前元素的个数
链式队列在进队时无队满问题,但有队空问题。队空条件: front == rear==NULL
队列的类定义
1 | template <class T> |
- 入队
1 | bool lnkQueue <T> :: enQueue(const T item) |
- 出队
1 | // 返回队头元素并从队列中删除 |
- 顺序队列和链式队列的比较
- 由于存储空间固定,顺序队列无法满足队列规模变化很大且最大规模无法预测的情况,而链式队列可以轻松应对这种类型的应用
- 时间效率:顺序队列和链式队列中常用的入队和出队操作都需要常数时间,二者无优劣之分
- 空间效率:顺序队列长度固定,不能再一个数组中存储两个队列
限制存取点的表
- 变种的栈或队列结构
- 双端队列:在队首和队尾都可以插入、删除的队列
- 双栈:两个底部相连的栈,共享一块数据空间
- 超队列:是一种被限制的双端队列,删除操作只允许在一端进行,插入操作却可以在两端同时进行
- 超栈:是一种插入受限的双端队列,即插入限制在一端而删除仍允许在两端进行
第三章 栈与队列 OJ作业
1:滑动窗口
1 | /*描述 |
2:堆栈基本操作
1 | /*描述 |
3:中缀表达式的值
1 | /*描述 |