3. 栈和队列

前言:从数据结构的角度看,栈和队列是操作受限的线性表,栈和队列的基本操作是线性表操作的子集。

3.1 栈

3.1.1 栈的概念和特点
  • 栈(stack)是限定在表尾进行插入和删除的线性表。表尾端称为栈顶,表头端成为栈底

  • 栈的特点:后进先出(Last In First Out,缩写为LIFO)。

  • 卡特兰数:n个元素进栈,合理的出栈排列个数为![img](file:///C:\Users\fanze\AppData\Local\Temp\ksohtml\wps63FD.tmp.png)

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 双端队列

​ 双端队列也是一种操作受限的线性表,在两端都可以进行插入删除。在实际使用中,还可以有输入受限的双端队列(即一个断点允许输入、删除,而另一个端点只允许输入)和输出受限的双端队列(即一个断点允许输入、删除,而另一个端点只允许删除)。

​ 如果限定双端队列从某个端点插入的元素只能从该端点删除,那么该双端队列旧蜕变成了两个栈底相邻的栈了。

posted @ 2022-04-10 18:03  某科学的撒把豆子  阅读(90)  评论(0)    收藏  举报