数据结构–栈、队列、串
栈、队列、串
从数据结构的角度看,栈和队列也是线性表。
栈
- 栈(stack)是限定仅在表尾进行插入和删除操作的线性表。因此,对栈来说,表尾端有其特殊含义,称为栈顶,相应地,表头端称为栈底。不含元素的空表称为空栈。
- 栈又被称为后进先出(last in first out)的线性表(简称LIFO结构)。
栈的表示和实现
- 顺序栈,即栈的顺序存储结构是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针top指示栈顶元素在顺序栈中的位置。
对栈的初始化,一个较合理的做法:先为栈分配一个基本容量,然后在应用过程中,当栈的空间不够使用时再逐段扩大。 为此,可设定两个常量:STACK_INT_SIZE(存储空间初始分配量)和STACKINCREMENT(存储空间分配增量),并以下述类型作为顺序栈的定义。
typedef struct {
SElemType *base;
SElemType *top;
int stacksize;
}
其中,stacksize指示栈的当前可使用最大容量。栈的初始化操作为:按设定的初始分量进行第一次存储分配,base可称为栈底指针,在顺序栈中,它始终指向栈底的位置,若base的值为NULL,则表明表结构不存在。称top为栈顶指针,其初值指向栈底,即top=base可作为栈空的标记,每当有插入新的栈顶元素时,指针top增1;删除栈顶元素时,指针top减1,因此,非空栈中的栈顶指针始终在栈顶元素的下一个位置上。如图:
- 链栈,栈的链式表示。
- 栈的应用
Q1:十进制数 N N N和其他 d d d进制转换。
公式: N N N = ( N N N div d d d) * d d d + N N N mod d d d(其中div为整除运算,mod为求余运算)。
例如:(1348)10 = (2504)8 ,其运算如下:
N N div 8 N mode 8
1348 168 4
168 21 0
21 2 5
2 0 2
利用栈的特性编写程序。
void conversion() {
// 对于输入的任意一个非负十进制整数,打印输出与其等值的八进制
InitStack(S) //创造空栈
scanf("%d",N);
while(N) {
Push(S, N % 8);
N = N / 8;
}
while(!StackEmpty(S)) {
Pop(S,e);
printf("%d",e);
}
}
Q2:括号匹配的检测 [([] [])]
解答:在算法中设置一个栈,接收第一个括号的时候入栈,接收第二个括号的时候判断是否是与第一个括号(栈顶的数据)匹配的括号,如果是,则第一个括号出栈,如果不是第二个继续入栈,依次类推,最后如果栈不是空的,则说明括号不匹配,否则是匹配的。
Q3:迷宫求解(求迷宫中从入口到出口的所有路径–穷举求解法)
Q4:表达式求值(3 * (7 - 2))
算法的基本思想:
- 首先置操作数为空栈,表达式起始符“#”为运算符栈的栈底元素
- 依次读取表达式中的每一个字符,若是操作数则进OPND(存放数字)栈,若是运算符则和OPTR栈的栈顶运算符比较优先级后 做相应的操作,直至整个表达式求值完毕(即OPTR栈的栈顶元素和当前读入的字符均为‘#’)。
算法优先级列表
例如:3 * (7 - 2)
步骤 OPTR栈 OPND栈 输入字符 主要操作
1 # 3 * (7 - 2)# PUSH(OPND,‘3’)
2 # 3 * (7 - 2)# PUSH(OPTR,‘*’)
3 #* 3 (7 - 2)# PUSH(OPTR,‘(’)
4 #*( 3 7 - 2)# PUSH(OPND,‘7’)
5 #*( 3 7 - 2)# PUSH(OPTR,‘-’)
6 #*(- 3 7 2)# PUSH(OPND,‘2’)
7 #*(- 3 7 2 )# operate(‘7’,‘-’,‘2’)
8 #*( 3 5 )# POP(OPTR){消除一对括号}
9 #* 3 5 # operate(‘3’,‘*’,‘5’)
10 # 15 # RETURN 15;
// OPTR栈:存放运算符 OPND栈:存放操作数 operate():运算操作函数
// POP():出栈 PUSH():入栈
// 栈顶运算符优先级 < 读入的运算符 运算符入栈
// 栈顶运算符优先级 = 读入的运算符 栈顶运算符出栈(运算符消去)
// 栈顶运算符优先级 < 读入的运算符 栈顶运算符出栈,并退栈操作数,进行运算操作
栈与递归的实现
栈还有一个重要应用是在程序设计语言中实现递归。
递归是程序设计的一个强有力工具。其一,有很多数学函数是递归定义的。
比如:
阶乘函数
F
a
c
t
(
n
)
=
{
1
若
n
=
0
n
∗
F
a
c
t
(
n
)
若
n
>
0
Fact(n)=\begin{cases} 1 & 若n = 0 \\ n * Fact(n) &若n > 0 \\ \end{cases}
Fact(n)={1n∗Fact(n)若n=0若n>0
2阶Fibonacci数列
F
i
b
(
n
)
=
{
0
若
n
=
0
1
若
n
=
1
F
i
b
(
n
−
1
)
+
F
i
b
(
n
−
2
)
其
他
情
形
Fib(n)=\begin{cases} 0 & 若n = 0 \\ 1 & 若n = 1 \\ Fib(n - 1) + Fib(n - 2) &其他情形 \\ \end{cases}
Fib(n)=⎩⎪⎨⎪⎧01Fib(n−1)+Fib(n−2)若n=0若n=1其他情形
Ackerman函数
A
c
k
(
m
,
n
)
=
{
n
+
1
若
m
=
0
A
c
k
(
m
−
1
,
n
)
若
n
=
0
A
c
k
(
m
−
1
,
A
c
k
(
m
,
n
−
1
)
)
其
他
情
形
Ack(m,n)=\begin{cases} n+1 & 若m = 0 \\ Ack(m - 1,n) & 若n = 0 \\ Ack(m - 1,Ack(m,n-1)) &其他情形 \\ \end{cases}
Ack(m,n)=⎩⎪⎨⎪⎧n+1Ack(m−1,n)Ack(m−1,Ack(m,n−1))若m=0若n=0其他情形
一个递归函数,在函数的执行函数中,需要多次进行自我调用。
两个函数之间调用的情形:
通常,当在一个函数的运行期间调用另一个函数时:
在运行被调用函数之前,系统需先完成3件事
(1)将所有的实在参数、返回地址等信息传递给调用函数保存;
(2)为被调用函数的局部变量分配存储区;
(3)将控制转移到被调用函数的入口;
而从被调用函数返回调用函数之前,系统也要完成3件工作
(1)保存被调函数的计算结果;
(2)释放被调函数的数据区;
(3)依照被调函数保存的返回地址将控制转移到调用函数。
当有多个函数构成嵌套调用的时候,按照"后调用先返回"的原则。
上述函数之间的信息传递和控制转移必须通过"栈"来实现,即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区;每当一个函数退出时,就释放它的存储区,则当前正在运行的函数的数据区必须在栈顶
队列
和栈相反,队列(Queen)是一种先进先出(first in frist out)的线性表。
它只允许在表的一端进行插入,而在另一端进行删除。
在队列中,允许插入的一端叫做队尾(rear),允许删除的一端则称为队头(front)
除了栈和队列之外,还有一种限定性数据结构是双端队列(deque).双端队列是限定插入和删除操作在表的两端(或者一个端点允许插入和删除,另一端点只允许插入/或者一个端点允许插入和删除,另一端点只允许删除)进行的线性表。
和线性表类似,队列也可以有两种存储表示。
- 链队列——队列的链式表示和实现
用链表表示的队列简称链队列,一个链队列需要两个分别指示队头和队尾的指针(分别称为头指针和尾指针)
- 循环队列——队列的顺序表示和实现
和顺序栈类似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列头到队列尾元素之外,尚需附设两个指针front和rear分别指示队列头元素及队列尾元素的位置。
为了在C语言中描述方便起见,在此我们约定:初始化建立队列时,令front = rear = 0每当插入新的队列尾元素时,“尾指针增1”;每当删除队列的头元素时,“头指针增1”。在非空队列中,头指针始终指向队列头元素,而尾指针始终指向队列尾元素的下一个位置。
循环链表
为充分利用向量空间,克服"假溢出"现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。这种循环队列可以以单链表的方式来在实际编程应用中来实现。
串
串(string)(或字符串)是由零个或多个字符组成的有序序列,一般记作s = ‘a1,a2,…an’ (n >= 0)