数据结构 第三章 栈和队列

简介: 数据结构 第三章 栈和队列

栈的定义
栈作为一种限定性线性表,是只允许在同一端进行插入和删除操作的线性表。

栈顶:通常将表中允许进行插入、删除操作的一端称为栈顶 (Top),因此栈顶的当前位置是动态变化的,它由一个称为栈顶指针的位置指示器指示。

栈底:同时表的另一端被称为栈底 (Bottom)。

空栈:当栈中没有元素。

满栈:无法申请到栈区可用空间。

上溢:栈已满仍要入栈。

下溢:栈已空仍要出栈。

栈的插入操作被形象地称为进栈或入栈,删除操作称为出栈或退栈。具有先进后出(FILO)或后进先出(LIFO)的特点。
栈的示意图

栈的顺序实现

结构定义
用一组连续的存储单元依次存放自栈底到栈顶的数据元素。设一个位置指针top(栈顶指针)动态指示栈顶元素在顺序栈中的位置,当top=-1时,表示空栈。

typedef int datatype;  
# define MAXSIZE 100
typedef struct{    
    datatype  data[MAXSIZE];
    int top; 
    } seqstack; 
seqstack *s;

基本操作

(1)判断栈空

int EMPTY(seqstack *s) {
    return (!s–>top>=0);
}

(2)置空栈

void SETNULL(seqstack *s) {
    s–>top=-1;
}

(3)判断栈满

int FULL(seqstack  *s) {
    return (s–>top==MAXSIZE-1);
}

(4)进栈

seqstack * PUSH(seqstack  *s,datatype x) {
    if (FULL(s)){ 
        printf(“stack overflow”); 
        return NULL; 
    }
    s–>top++; 
    s–>data[s–>top]=x;  
    return s;
}

(5)出栈

datatype POP(seqstack *s) {
    if (EMPTY(s)) { 
        printf(“stack underflow”);
        return NULL;
    }
    s–>top--; 
    return(s–>data[s–>top+1]);
}

(6)取栈顶

datatype TOP(seqstack  *s) {
    if (EMPTY(s)) { 
        printf(“stack is empty”);
        return NULL;
    }
    return (s–>data[s–>top]);
} 

两栈共享技术

栈的应用非常广泛,经常会出现在一个程序中需要同时使用多个栈的情况。若使用顺序栈,会因为对栈空间大小难以准确估计,从而产生有的栈溢出、有的栈空间还很空闲的情况。

为了解决这个问题,可以让多个栈共享一个足够大的数组空间,通过利用栈的动态特性来使 其存储空间互相补充,这就是多栈共享技术。

在顺序栈的共享技术中最常用的是两个栈的共享技术即双端栈:它主要利用了“栈底位置不变,而栈顶位置动态变化”的特性。

首先为两个栈申请一个共享的一维数组空间S[M], 将两个栈的栈底分别放在一维数组的两端,分别是 0,M-1。由于两个栈顶动态变化,这样 可以形成互补,使得每个栈可用的最大空间与实际使用的需求有关。

由此可见,两栈共享要比两个栈分别申请M/2的空间利用率要高。

两栈共享的数据结构定义如下:

#define M 100 
typedef struct { 
    StackElementType Stack[M];   
    StackElementType  top[2]; /*top[0]和 top[1]分别为两个栈顶指示器*/
}DqStack; 

两个栈共享空间的示意如下图所示:
双端栈

若栈1的底在v[1],栈2的底在V[N],则栈满的条件是top[1]+1==top[2]。

(1)初始化操作:

void InitStack(DqStack *S) {
    S->top[0]=-1; 
    S->top[1]=M; 
} 

(2)进栈操作:

int Push(DqStack *S, StackElementType x, int i) {
    /*把数据元素 x 压入 i 号堆栈*/  
    if(S->top[0]+1==S->top[1]) /*栈已满*/
           return(FALSE);  
    switch(i)  {  
        case 0:  
            S->top[0]++;
            S->Stack[S->top[0]]=x; 
            break;  
        case 1:   
            S->top[1]--;
            S->Stack[S->top[1]]=x;
            break;    
        default:
            /*参数错误*/             
            return(FALSE)   
    }  return(TRUE); 
} 

(3)出栈操作:

int Pop(DqStack *S, StackElementType *x, int i) {
    /* 从 i 号堆栈中弹出栈顶元素并送到 x 中 */  
    switch(i)  {
        case 0:
            if(S->top[0]==-1)
                return(FALSE);
            *x=S->Stack[S->top[0]];
            S->top[0]--;
            break;
        case 1:
            if(S->top[1]==M)
                return(FALSE);    
            *x=S->Stack[S->top[1]];
            S->top[1]++;
            break;
        default:
            return(FALSE);
    }
    return(TRUE);
} 

栈的链式实现

链栈即采用链表作为存储结构实现的栈。为便于操作,这里采用带头结点的单链表实现栈。

由于栈的插入和删除操作仅限制在表头位置进行,所以链表的表头指针就作为栈顶指针,如下图所示:链栈

在上图中,top为栈顶指针,始终指向当前栈顶元素前面的头结点,它唯一地确定一个链栈。若top->next=NULL,则代表栈空,该链栈为空栈。

链栈中的结点是动态产生的,无需考虑上溢问题。

采用链栈时,栈的各种基本操作的实现与单链表的操作类似,对于链栈,在使用完毕时,应该释放其空间。

结构定义

typedef  int datatype;
typedef struct node{    
    datatype  data;
    struct node * next;
    }linkstack;
linkstack * top;

基本操作

(1)进栈

linkstack *PUSHLSTACK(linkstack *top, datatype x) { 
    linkstack *p;
    p=(linkstack *)malloc(sizeof(linkstack));
    p->data=x; 
    p->next=top;
    return p;  //返回新栈顶指针
}

(2)出栈

linkstack *POPLSTACK(linkstack *top, datatype *datap) { 
    linkstack *p;
    if (top==NULL) { 
        printf(“under flow”);
    }
    datap=top->data; /*栈顶结点数据存入*datap */
     p=top;
     top= top->next;
     free(p);
    return top;  //返回新栈顶指针
 }

队列

队列的概念
队列(Queue)也是一种运算受限的线性表。只允许在表的一端进行插入,而在另一端进行删除。允许删除的一端称为队头(front),允许插入的一端称为队尾(rear)。具有先进先出(FIFO)的特点。

顺序队列
队列的一种顺序存储称为顺序队列。与顺序栈类似,在队列的顺序存储结构中,用一组 地址连续的存储单元依次存放从队头到队尾的元素,如一维数组data[MAXSIZE]。 由于队列中队头和队尾的位置都是动态变化的,因此需要附设两个指针front和rear。

front:指示队头元素在数组中的位置

rear:指示真实队尾元素相邻的下一个位置

结构定义

#define MAXSIZE 100
typedef  struct{
    datatype data[MAXSIZE];
    int front;
    int rear;
}sequeue;
sequeue * sq;

假溢出
初始化队列时,令front = rear =0;入队时,直接将新元素送入尾指针rear所指的单元, 然后尾指针增1;出队时,直接取出队头指针front所指的元素,然后头指针增1。

显然,在非空顺序队列中,队头指针始终指向当前的队头元素,而队尾指针始终指向真正队尾元素后面的单元。当rear==MAXSIZE时,认为队满。

但此时不一定是真的队满,因为随着部分元 素的出队,数组前面会出现一些空单元,如下图(d)所示。由于只能在队尾入队,致使被删除元素的空间永远无法重新利用。

把这种现象称为假溢出,真正队满的条件是rear - front=MAXSIZE。
假溢出

链式队列

结构定义

typedef struct queuenode{
    datatype  data;
    struct queuenode  *next;
}QueueNode;

typedef struct{
    QueueNode  *front;
    QueueNode  *rear;
}LinkQueue;

基本操作
(1)置空

void InitQueue(LinkQueue  *Q) {
    Q.front=Q.rear=(queuenode*)malloc(sizeof(queuenode )); 
    Q.front->next=Q.rear->next=NULL;}

(2)判断队空

int QueueEmpty(LinkQueue  *Q) {
    return (Q.front->next= =NULL &&Q.rear->next= =NULL);
}

(3)入队

void EnQueue(LinkQueue  *Q, datatype x){    
    QueueNode *p;
    p=(QueueNode * )malloc(sizeof(QueueNode));
    p–>data=x;
    p–>next=NULL;
    Q->rear–>next=p;
    Q->rear=p;
}

(4)出队

DeQueue(linkqueue *Q) {
    linkqueue  *p;
    datatype x;
    if (EMPTY(Q))    
        return NULL;
    p = Q->front->next;      
    Q->front->next = p–>next;
    if (p->next==NULL) 
        Q->rear = Q->front;  
    x = p->data;
    free(p);
    return x;
}

循环队列

为了解决假溢出问题,引入了循环队列的概念。在循环队列中进行出队、入队操作时,头尾指针仍要加1,朝前移动。只不过当头尾指针指向向量上界( MaxSize-1 )时,其加1操作的结果是指向向量的下界0。

判断栈满
入队时:尾指针向前追赶头指针,出队时:头指针向前追赶尾指针,故队空和队满时头尾指针均相等,无法通过sq->front == sq->rear来判断队列“空”还是“满”。
解决此问题的方法至少有两种:

另设一个布尔变量以区别队列的空和满;

少用一个元素空间为代价,入队前,测试尾指针 sq-> rear+1==sq->front ,若相等则认为队满。

基本操作
队空判断: sq->front == sq-> rear

队满判断: sq-> front ==(sq-> rear + 1) % maxSize

入队: sq-> rear = (sq-> rear + 1) % maxSize

出队: sq-> front = (sq-> front + 1) % maxSize

求队长: (sq-> rear - sq-> front+maxSize)%maxSize

(1)入队

检查队列是否已满,若队满,则进行溢出错误处理。

将队尾指针后移一个位置(即加1),指向下一单元。

将新元素赋给队尾指针所指单元。

Status EnQueue (SqQueue *Q, ElemType  e){    
    if ( (Q->rear+1)%MAXQSIZE == Q->front )  
        return(ERROR);   //队满上溢
    Q->rear=(Q->rear+1)%MAXQSIZE;
    Q->data[Q->rear]=e;
    return(True);   }

(2)出队

检查队列是否为空,若队空,则进行下溢错误处理。

将队首指针后移一个位置(即加1)。

取队首元素的值。

Status DeQueue (SqQueue *Q) { 
    if (Q->rear== Q->front)  
        return(NULL);  //队空下溢
    Q->front=(Q->front+1)%MAXQSIZE;
    return(Q->data[Q->front]);
}

(3)置空队

Q->front=Q->rear= MAXQSIZE-1;

(4)取队头

datatype  GetHead(SqQueue *Q ) { 
    if (Q->front==Q->rear)  
        return(NULL);  //队空
    return (Q->data[(Q->front+1)%MAXQSIZE] );
}

(5)判断队空

int QueueEmpty(SqQueue *Q ){
    if (Q->front==Q->rear) 
        return (True);
    else 
        return (False);  
}

栈和队列的应用

递归函数
递归函数又称为自调用函数,它的特点:在函数内部可以直接或间接地调用函数自己。

C语言描述如下:

int  FACT(int n) { 
    if (n==1) 
        return (1);
     else
        return (n*FACT(n-1));
}

算法表达式求值

计算机系统在处理表达式前,先设置两个栈:操作数栈(OPRD):存放处理表达式过程中的操作数;运算符栈(OPTR):存放处理表达式过程中的运算符。开始时,在运算符栈中先在栈底压入一个表达式的结束符“#”。

(1)中缀表示法
计算机系统在处理表达式时,从左到右依次读出表达式中的各个符号(操作数或运算符),每读出一个符号ch后,根据运算规则做如下处理:

如果ch是操作数,则将其压入操作数栈OPRD,并依次读取下一个符号。

若ch是运算符,则:
若读出的运算符的优先级大于OPTR栈顶的运算符优先级,则将其压入OPTR,并依次读下一个符号。
若读出的是“#”,且OPTR栈顶的运算符也是“#”,则表达式处理结束,最后的计算结果在OPRD的栈顶位置。

若读出的是“(”,则将其压入OPTR。

若读出的是“)”,则:若OPTR栈顶不是“(”,则从OPRD连续退出两个操作数,从OPTR中退出一个运算符,然后作相应的运算,并将运算结果压入OPRD,然后返回a),让ch继续与OPTR栈顶元素进行比较。
若OPTR栈顶为“(”,则从OPTR退出“(”,依次读下一个符号。

若读出的运算符的优先级小于OPTR栈顶运算符的优先级,则从OPRD连续退出两个操作数,从OPTR中退出一个运算符,然后作相应的运算,将运算结果压入OPRD 。返回(2)继续OPTR栈顶元素进行比较。

(2)波兰表示法和逆波兰表示法

以5 + ( 6 – 4 / 2 ) * 3为例:

波兰表示法:+ 5 * - 6 / 4 2 3  

逆波兰表示法:5 6 4 2 / - 3 * +。 

运算时按从左到右的顺序进行,不需要括号。在计算表达式时,可设置一个栈,从左到右扫描后缀表达式,每读到一个操作数就将其压入栈中;每读到一个运算符时,则从栈顶取出两个操作数运算,并将结果压入栈中,一直到后缀表达式读完。最后栈顶就是计算结果。

括号匹配

#include<stdio.h>
#define maxsize 100
typedef int datatype;

typedef struct{
    datatype data[maxsize];
    datatype top;
}seqstack;
seqstack *s;

void build();
void push();
void pop();
int check(char ss[]);

void main(){
    char ss[maxsize];
    build();
    printf("请输入要测试的算数表达式:");
    scanf("%s",ss);
    if(check(ss)==-1)
        printf("算数表达式不匹配!");
    else
        printf("算数表达式匹配!");
}

void build(){
    s=(seqstack*)malloc(sizeof(seqstack));
    s->top=-1;
}
int check(char ss[]){
    int i=0;
    while(ss[i] != '\0'){
        i++;
        if(ss[i] == '(')
            push();
        else if(ss[i] == ')')
            pop();
    }
    return s->top;
}

void push(){
    s->top++;
}
void pop(){
    s->top--;
}

回文串判断

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define maxsize 100

typedef struct {
    char data[maxsize];
    int top;
} stack;

typedef struct {
    char data[maxsize];
    int front;
    int rear;
} queue;

int isempty_queue(queue * stq){
    return stq->front == stq->rear;
}

int isfull_queue(queue *stq){
    return stq->rear >= maxsize -1 ;
}

queue * init_queue(){
    queue * tmp = (queue*) malloc(sizeof(queue));
    tmp->front = tmp->rear = 0;
    return tmp;
}

char dequeue(queue * stq){
    if( isempty_queue(stq) ) {
        printf("队列为空,无法出队\n");
        exit(0);
    }
    return stq->data[stq->front++];
}

void inqueue(queue *stq, char value) {
    if( isfull_queue(stq) ) {
        printf("队列已满,无法入队\n");
        exit(0);
    }
    stq->data[stq->rear++] = value;
}

stack * init_stack(){
    stack * tmp = (stack *) malloc( sizeof(stack) );
    tmp->top = 0;
    return tmp;
}

int isempty_stack(stack *stk) {
    return stk->top == 0 ? 1 : 0;
}

int isfull_stack(stack *stk) {
    return stk->top >= maxsize -1 ? 1: 0;
}

char pop(stack *stk) {
    if( isempty_stack(stk) ) {
        printf("堆栈为空,无法出栈\n");
        exit(0);
    }
    return stk->data[--stk->top];
}

void push(stack *stk, char value) {
    if( isfull_stack(stk) ) {
        printf("堆栈已满,无法入栈\n");
        exit(0);
    }
    stk->data[stk->top++] = value;
}

void compare(stack * stk, queue *stq, char str[], int len) {
    int i;
    int flag = 0;
    char temp_stack;
    char temp_queue;

    for(i = 0; i < len; i++){
        push(stk, str[i]);
        inqueue(stq, str[i]);
    }

    for(i = 0; i < len; i++){
        temp_stack = pop(stk);
        temp_queue = dequeue(stq);

        if(temp_stack != temp_queue) {
            printf("不是回文串\n");
            flag = 1;
            break;
        }
    }

    if( !flag )
        printf("是回文串\n");
}

int main(){
    queue * stq = init_queue();
    stack * stk = init_stack();

    char c[maxsize],s;
    int i=0;
    printf("请输入字符序列,以@结束\n");
    scanf("%c",&s);
    while(s!='@'){
        c[i]=s;
        scanf("%c",&s);
        i++;
    }
    c[i]='\0';
    compare(stk, stq, c, strlen(c)-1);

    return 0;
}

例题
例1
栈和队列是特殊的线性表,其特殊性体现在?为什么要引入循环队列?

和普通线性表相比,对插入、删除运算加以限制。一般的一维数组队列的尾指针已经到了数组的上界,不能再有入队操作,但其实数组中还有空位置,为了解决这种问题,就要引入循环队列。

例2
设一个栈的入栈序列为A、B、C、D,则可能的出栈序列有哪些?

共计14种,分别是:ABCD、ACBD、ACDB、ABDC、ADCB、BACD、BADC、BCAD、BCDA、BDCA、CBAD、CBDA、CDBA、DCBA。

例3
有5个元素,其入栈次序为A、B、C、D、E,在各种可能的出栈次序中,以元素C、D最先出栈(C第一个出栈且D第二个出栈)的次序有哪几种?

共计3种,分别是:CDEBA、CDBEA、CDBAE。

例4
判定一个顺序栈ST(元素个数最多为StackSize)为空/满的条件是【 】。

空:ST->top=0,满:ST->top=MAXSIZE-1。

例5
判定一个循环队列Q(存放元素位置:0至QueueSize-1)队满的条件是【 】。

sq-> front ==(sq-> rear + 1) % maxSize。

例6
若用一个大小为6的数组来实现环形队列,且当前rear和front的值分别是0和3,当从队列中删除一个元素,再加入两个元素后,rear和front的值分别是【 】和【 】。

2 ; 4

目录
相关文章
|
5月前
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
162 1
|
3月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
49 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
8月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
332 77
|
7月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
158 11
|
7月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
8月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
241 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
8月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
188 7
|
10月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
870 9
|
10月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
218 59