3. 栈和队列
前言:从数据结构的角度看,栈和队列是操作受限的线性表,栈和队列的基本操作是线性表操作的子集。
3.1 栈
3.1.1 栈的概念和特点
-
栈(stack)是限定在表尾进行插入和删除的线性表。表尾端称为栈顶,表头端成为栈底。
-
栈的特点:后进先出(Last In First Out,缩写为LIFO)。
-
卡特兰数:n个元素进栈,合理的出栈排列个数为
3.1.2 顺序栈
对于顺序栈有两个状态和两个操作。
- 两个状态:
- 栈空状态:
S.top == -1;
- 栈满状态:
s.top == MAXSIZE - 1;
- 栈空状态:
- 两个操作
//进栈:
bool Push(SqStack &S,int e){
//插入元素e为新的顶元素
if(s.top == MAXSIZE - 1){//栈满,追加存储空间
return false;//这块可以realloc,如果申请空间失败,再返回失败
}
S.data[++S.top] = e;
return true;
}//Push
//出栈
bool Pop(SqStack &S,int &e){
//插入元素e为新的顶元素
if(s.top == -1){//栈空,返回失败
return false;
}
e = S.data[S.top--];
return true;
}//Pop
3.1.3 链栈
由于栈的操作是线性表操作的特例,则链栈的操作易于实现,在此不做详细讨论。
3.1.4 递归算法中栈的作用
栈还有一个重要作用是在程序设计语言中实现递归。一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。最后调用的函数最先执行结束,符合LIFO的原则。太多层栈,空间复杂度会越来越高,可能导致栈溢出。
递归程序在程序设计中是一个强有力的工具。
- 其一,很多数学函数是递归定义的,比如阶乘、Fibonacci数列、Ackerman函数。
- 其二,有的数据结构,如二叉树、广义表等。由于结构本身固有的递归特性,则他们的操作可递归地描述。
- 其三,还有一类问题,虽然问题没有明显的递归结构,但是递归求解比迭代求解更简单,如八皇后问题、Hanoi塔问题。
3.1.5 栈的典型应用实例
-
括号匹配的检验:这个过程和栈的特点相吻合。在算法中设置一个栈,每读入一个括号,若是右括号,则使栈顶急迫性最高的期待得以消解;若是左边括号,则作为一个新的更急迫的期待压入栈中,自然使得原有的在栈中的所有未消解的期待的急迫性都降了一级。在算法的开始和结束时,栈都应该是空的。
-
行编辑程序:设输入缓冲区为一个栈结构,每当从终端接受了一个字符之后先做如下判别:如果它既不是退格符也不是换行符,则将该字符压入栈顶;如果是一个退格符,则从栈顶删去一个字符;如果是一个换行符,则将字符栈清为空栈。
-
迷宫求解:计算机解迷宫时,通常用的是“穷举求解”,从入口出发,顺着某一方向探索,若能走通,继续往前走;否则原路退回,换一个方向继续探索,直至所有的可能的通路都探索到为止。为了保证在任何位置上都能沿原路退回,显然需要一个后进先出的结构来保存从入口到当前位置的路径。
-
表达式求值:中缀表达式转后缀表达式、后缀表达式求值的结合。
-
中缀转后缀
中缀表达式:A+B*(C-D)-E/F 后缀表达式:ABCD-*+EF/-
-
用栈实现后缀:
a. 从左往右扫描后缀表达式下一个元素,知道处理完所有的元素。
b. 若扫描到操作数则压入栈,并返回到a,否则c(先出栈为右操作数)。
c. 若扫描到运算符,则弹出栈顶两个元素,执行相应的运算,运算结果压入栈顶。
-
3.2 队列
3.2.1 队列的概念和特点
- 队列(queue)只允许在表的一端进行插入,而在另一端删除元素。和我们日常生活中的排队是一致的,最先排队的先离开。允许插入的一端叫做队尾(rear),允许删除的一端叫做队头(front)。
- 队列的特点:先进先出(First In First Out,缩写为FIFO)
3.2.2循环队列
由于顺序队列会产生假溢出,这里引出循环队列。将顺序队列臆造为一个环状的空间,即把存储队列的表从逻辑上视为一个环。当队首指针Q.front = MAXSIZE - 1
后,再前进一个位置就会自动到0。这可以利用除法取余(%)来实现。
- 两个状态
- 初始队空:
Q.front = Q.rear = 0
- 队满状态:
(Q.rear + 1)%MAXSIZE == Q.front
,约定“对头指针在队尾指针的下一个位置作为队满的标志”。
- 初始队空:
- 两个操作
//入队
bool EnQueue(SqQueue &Q,int x){
if((Q.rear + 1)%MAXSIZE == Q.front) return false;//队满不能入队
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1)%MAXSIZE;
}
//出队
bool DeQueue(SqQueue &Q,int &x){
if(Q.front = Q.rear) return false;//队空不能出队
x = Q.data[Q.front];
Q.front = (Q.front + 1)%MAXSIZE;//对头指针加1取模
return true;
}
3.2.3链队
一个链队显然需要两个分别指示队头和队尾的指针才能唯一确定。
- 两个状态
- 初始队空:
Q.front ==NULL
或者Q.rear == NULL
- 队满状态:不存在队满的情况(假设内存无限大的情况)
- 初始队空:
- 两个操作
//入队
void EnQueue(LinkQueue &Q,int x){
LinkNode * s = (LinkNode *) malloc(sizeof(LinkNode));
s.data = x;s->next = NULL;//创建新结点
Q.rear->next = s;//将新结点插入到表尾
Q.rear = s;
}
//出队
bool DeQueue(SqQueue &Q,int &x){
if(Q.front == Q.rear) return false;//队空不能出队
LinkNode * p = Q.fron->next;
x = p.data;
Q.front->next = p->next;
if(Q.rear == p) Q.rear = Q.front;//若队中只有一个结点,删除后变空
free(p);
return true;
}
3.2.4 双端队列
双端队列也是一种操作受限的线性表,在两端都可以进行插入删除。在实际使用中,还可以有输入受限的双端队列(即一个断点允许输入、删除,而另一个端点只允许输入)和输出受限的双端队列(即一个断点允许输入、删除,而另一个端点只允许删除)。
如果限定双端队列从某个端点插入的元素只能从该端点删除,那么该双端队列旧蜕变成了两个栈底相邻的栈了。