当前位置:   article > 正文

栈和队列的详解

栈和队列

目录

1. 栈的基本概念

1.1 栈的定义

1.2 栈的存储结构

1.3 栈的数学性质

2. 栈的基本操作

2.1 顺序栈定义

2.2  链式栈结点定义

3 栈输入输出的合理性

4 栈的全部输出结果

 5 栈的相关应用

5.1 括号匹配

5.2 进制转化

6  队列的基本概念

6.1 队列的定义

6.2  队列的存储结构

6.3  循环队列

7 队列的基本操作

7.1 顺序存储队列的基本操作

7.2  链式存储队列的基本操作

 8 队列的相关应用

8.1 杨辉三角(队列形式输出)

 9 总结


1. 栈的基本概念

1.1 栈的定义

        栈(stack) 是限定仅在一端(表尾)进行插入或删除操作的线性表。因此,对栈来说,表尾端有其特殊含义,称为栈顶 (top), 相应地,表头端称为栈底 (bottom) 。不含元素的空表称为空栈。栈的示意图,如下图所示。

        栈的通俗解释:在从上往下堆积木时候,只能从最顶部放置积木,或者从最顶部拿走基本。积木最顶端叫做栈顶,积木最底端叫做栈底。从下往上堆积木的过程叫做堆栈或者压栈,从上往下拿走积木的过程叫做出栈。

1.2 栈的存储结构

        可用顺序表链表来存储栈,栈可以依照存储结构分为两种;顺序栈和链式栈。在栈的定义中已经说明,栈是一种在操作上稍加限制的线性表,即栈本质上是线性表,而线性表有两种主要的存储结构—— 顺序表和链表,因此栈也同样有对应的两种存储结构。

1.3 栈的数学性质

        当n个元素以某种顺序进栈,并且可在任意时刻出栈(在满足先进后出的前提下)时,所获得的元 素排列的数目N恰好满足函数 Catalan()的计算,最终有\frac{1}{n+1} C_{2n}^{n}种排列方法。该公式称为卡特兰数。

2. 栈的基本操作

2.1 顺序栈定义

  1. typedef struct{
  2. int data[maxSize]; //存放栈中元素,maxSize是已定义的常量
  3. int top; //栈顶指针
  4. }SqStack; //顺序栈类型定义

(1) 构造一个空栈:InitStack(&S)

  1. void InitStack(SqStack &S){
  2. s.top=-1;
  3. }

(2)判断栈空:StackEmpty(SqStack S)

  1. bool StackEmpty(SqStack S){
  2. if(S.top==-1) //为空的是top指向-1
  3. return true;
  4. else
  5. return false;
  6. }

(3)进栈: Push(SqStack &S,int x)

  1. bool Push(SqStack &S,int x){
  2. if(S.top==MaxSize-1) //判断是否栈满,满了推出
  3. return false;
  4. S.data[++S.top]=x; //先将指针加一再入栈
  5. return true;
  6. }

 以下是模拟压栈的整个过,以数字3 2 5 6 7为例进行压栈。第一行是栈元素在顺序表中的序号,第二行是栈数据。

 输入元素,左边为顺序表序号,右边是栈数据。

 注意:当栈为空时候,top=-1;每次插入时候,要先判断栈是否已满,就是top是否等于MaxSize-1,如果相等,就不能进行压栈。

(4)出栈:Pop(SqStack &S,int &x)

  1. bool Pop(SqStack &S,int &x){
  2. if(S.top==-1) //判断是否为空
  3. return false;
  4. x=S.data[S.top--]; //先出栈,再指针减一
  5. return true;
  6. }

  以下是模拟出栈的整个过,以上述压栈后栈为例进行出栈。第一行是栈元素在顺序表中的序号,第二行是栈数据。

   输出栈顶元素,左边为顺序表序号,右边是栈数据。

  注意:每次出栈时候,要先判断栈是否为空,就是top是否等于-1,如果相等,就不能进行出栈。

(5) 读栈顶元素:GetTop(SqStack S,int &x)

  1. bool GetTop(SqStack S,int &x){
  2. if(S.top==-1) //判断是否为空
  3. return false;
  4. x=S.data[S.top];
  5. return true;
  6. }

2.2  链式栈结点定义

链式栈的优点:便于多个栈共享存储空间和提高其效率,不存在找满上溢的情况。通常采用单链长实现,并规定所有操作都是在单链表的表头进行的。这里规定链栈没有头结点、 Lhead指向找顶元素,所以在压栈时候采用头插法进行压栈建立链表。

链式栈的存储结构

  1. typedef struct LNode{
  2. int data; //数据域
  3. struct LNode *next; //指针域
  4. }LNode; //链式栈结点定义

(1) 构建一个空栈

  1. void InitStack(LNode *&LStack){
  2. LStack=(LNode*)malloc(sizeof(LNode)); //制造一个头结点
  3. LStack->next=NULL;
  4. }

注意:判断栈是否为空,也是根据初始栈的LStack->next=NULL来判断

 (2)判断栈是否为空

  1. bool IsEmpty(LNode *LStack){
  2. if(LStack->next==NULL)
  3. return true;
  4. return false;
  5. }

(3)进栈

  1. void push(LNode *LStack,int x){
  2. LNode *p;
  3. p=(LNode*)malloc(sizeof(LNode));
  4. p->next=NULL;
  5. //头插法
  6. p->data=x;
  7. p->next=LStack->next;
  8. LStack->next=p;
  9. }

注意:在插入时候,无需和顺序栈一样要判断是否栈满,因为链式栈是链表,只要条件运行,链表就不会存在满的状态。

以下是头插法插入栈顶元素。 

 以下是模拟插入链式栈的运行情况如图。以数字3 2 5 6为例进行压栈。

 以下是该是平台直观显示出来的栈位置,结点左边为数据,右边为该结点地址,靠经Lhead的结点为栈顶,尾部为栈尾。

(4)出栈

  1. bool Pop(LNode *LStack,int &x){
  2. LNode *p;
  3. if(LStack->next==NULL)
  4. return false;
  5. //相当于单链表的删除操作
  6. p=LStack->next;
  7. x=p->data;
  8. LStack->next=p->next;
  9. free(p);
  10. return true;
  11. }

 以下是模拟链式栈出栈的运行情况如图。以上述数字3 2 5 6,进栈后,再出栈为例。

  以下是该是平台直观显示出来的栈位置,结点左边为数据,右边为该结点地址,靠经Lhead的结点为栈顶,尾部为栈尾。

注意:在链式栈中出栈,还是先要判断是否栈为空,如果为空就不能进行出栈。

说明∶对于链栈,和顺序栈一样,只是它们的存储方式不同,但是相关基本操作都说类似的,只需记住,栈如何判空,是否栈满,以及进栈和出栈只能从栈顶操作。

3 栈输入输出的合理性

        栈的输入输出合理主要在于栈的特性,而栈的特性主要是先进后出,每次在栈顶进行出栈和压栈的相关操作,无论怎样输入,每次输出顺序都是和输出相反的顺序。一旦输出的顺序与输入顺序不是完全逆序的,则可以判定该栈的输入输出是不合理的。

       (1) 以输入3 2 6 5 8一串元素,出栈顺序8 5 6 2 3 为例,进行判断栈的合法性。

         接下来是模拟入栈和出栈操作

         从模拟结果可以看出,该入栈顺序和出栈顺序是合法的。

        (2)以输入3 2 6 5 8一串元素为例,出栈顺序8 2 3 5 6 为例,进行判断栈的合法性。

         接下来是模拟入栈和出栈操作

        在出栈时候,可以发现在弹出元素8之后,本应该弹出元素5,但是设定的元素8后面输出2,所以在此可以发现,出栈序列不合法。

4 栈的全部输出结果

(1)栈大小无限制输出结果     

        以4个元素的栈为例,输出全部栈的出栈结果。(在压栈和出栈时候,可以随时选择出栈、压栈,但是都在栈顶操作,不过栈为空时候,就不能进行出栈)

        四个元素进行出栈入栈,总共要产生\frac{1}{4+1} C_{2*4}^{4}=14总合法出栈的方法。但是如果不按照出栈入栈的合法顺序来进行,应该存在A_{4}^{4}种情况产生。所以栈的输入输出方式,很大程度限制了出栈入栈的合法个数。

(2)栈大小有限制输出结果

        在限制栈的大小时候,又将减少合法出栈序列。因为栈里最多存在三个元素,所以必然要等有些元素出栈后,排在后面的元素才能进行入栈。下列将上述栈的大小改为3,所以就减少了四个元素同时在栈中的情况,合法出栈序列从14种减少为13种。

 5 栈的相关应用

5.1 括号匹配

算法思想:在遍历存放字符数组中,如果遇到(、{、[ 等左括号,就将其压入栈中,在遇到 )、}、] 等右括号之后,就需要从栈中出栈判断是否有对应的左括号与之匹配,如果存在,则将其出栈,后继续遍历字符数组。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #define MAXSIZE 100
  4. typedef int datatype;
  5. typedef struct sequence_stack
  6. {
  7. datatype a[MAXSIZE];
  8. int top;
  9. } seq_stack;
  10. void init(seq_stack* st) //初始化栈,生成一个初始栈
  11. {
  12. st->top = 0;
  13. }
  14. int empty(seq_stack st) //判断栈是否为空
  15. {
  16. return( st.top ? 0:1 );
  17. }
  18. datatype top(seq_stack st) //取栈顶元素
  19. {
  20. if (empty(st))
  21. {
  22. printf("\n栈是空的!");
  23. exit(1);
  24. }
  25. return st.a[st.top-1];
  26. }
  27. void push(seq_stack* st, datatype x) //进行压栈
  28. {
  29. if(st->top == MAXSIZE) //判断栈是否已经满了
  30. {
  31. printf("\nThe sequence stack is full!");
  32. exit(1);
  33. }
  34. st->a[st->top]=x;
  35. st->top++;
  36. }
  37. void pop(seq_stack* st) //进行出栈
  38. {
  39. if(st->top==0) //判断栈是否为空
  40. {
  41. printf("\nThe sequence stack is empty!");
  42. exit(1);
  43. }
  44. st->top--;
  45. }
  46. int match_kuohao(char c[]) //括号匹配算法
  47. {
  48. int i=0;
  49. sequence_stack s; //新建一个栈
  50. init(&s);
  51. while(c[i]!='#') //进行输入括号
  52. {
  53. switch(c[i])
  54. {
  55. case '{':
  56. case '[':
  57. case '(': push(&s,c[i]); break; // 开括号全部入栈
  58. case '}': if( !empty(s) && top(s)=='{' ) // 假如 {和}匹配
  59. {pop(&s); break;}
  60. else return 0;
  61. case ']': if( !empty(s) && top(s) == '[' ) // 假如 [和]匹配
  62. {pop(&s); break;}
  63. else return 0;
  64. case ')': if( !empty(s)&& top(s)=='(' ) // 假如 (和)匹配
  65. {pop(&s); break;}
  66. else return 0;
  67. }
  68. i++;
  69. }
  70. return (empty(s)); //栈为空则匹配,否则不匹配
  71. }
  72. int main()
  73. {
  74. char szKuohao[] = "(([(){}]))#";
  75. int result = match_kuohao(szKuohao);
  76. if(result == 1)
  77. printf("匹配成功!\n");
  78. else
  79. printf("匹配不成功!\n");
  80. return 0;
  81. }

5.2 进制转化

算法思想:十进制数 和其他 进制数的转换是计算机实现计算的基本问题,其解决方法很 多,其中一个简单算法基千下列原理:

         N=(N div d)Xd+N mod d (其中: div 为整除运算, mod 为求余运算)

        假设现要编制一个满足下列要求的程序:对于输入的任意一个非负十进制整数,打印 输出与其等值的八进制数。由千上述计算过程是从低位到高位顺序产生八进制数的各个数位,而打印输出,一般来说应从高位到低位进行,恰好和计算过程相反。因此,若将计算 过程中得到的八进制数的各位顺序进栈,则按出栈序列打印输出的即为与输人对应的八进制数。

  1. #include<stdio.h>
  2. void change(int m, int n)
  3. {
  4. int a[100];
  5. int top=0;
  6. while(m)
  7. {
  8. a[top++]=m%n;
  9. m=m/n;
  10. }
  11. top--;
  12. while(top>=0)
  13. {
  14. if(a[top]>9)
  15. printf("%c",a[top]+'A'-10);
  16. else printf("%c",a[top]+'0');
  17. top--;
  18. }
  19. }
  20. int main()
  21. {
  22. int m,n;
  23. printf("输入要转换的十进制数:");
  24. scanf("%d",&m);
  25. printf("要转换的进制:");
  26. scanf("%d",&n);
  27. change(m,n);
  28. printf("\n");
  29. return 0;

6  队列的基本概念

6.1 队列的定义

        队列(queue)是一种先进先出( first in first out,缩写为FIFO)的线性表。它只允许在表的一端进行插入,而在另一端删除元素。这和我们日常生活中的排队是一致的,最早进入队列的元素最早离开。在队列中,允许插入的一端叫做队尾 (rear), 允许删除的一端则称为队头 (front) 。假设队列 q= (a1 ,a2, …, an),那么,a1 就是队头元素, an 则是队尾元素。队列中的元素是按照 a1 ,a2, …, an 的顺序进入的,退出队列也只能按照这个次序依次退出,也就是按照a1 ,a2, …, an顺序,只有当前面的都离开队列后,an+1才能离开队列。

        通俗的解释:在人们排队买东西时候,都是先到收银台的顾客先出超市,只有等排在前面的顾客结完账之后,后面的顾客才能结账。也就是所谓的先进先出。

6.2  队列的存储结构

        可用顺序队链队来存储队列,队列可以依照存储结构分为两种;顺序栈和链式栈。在队列的定义中已经说明,队列是一种在操作上稍加限制的线性表,即栈本质上是线性表,而线性表有两种主要的存储结构—— 顺序表和链表,因此队列也同样有对应的两种存储结构。顺序队又有普通队列和循环队列,是根据队列的特定从普通队列转化形成的循环队列,用于节省存储空间。

        由于普通队列在运行时候,最终会将队头和队尾指向同一个元素,导致虚假的队列满的状态,可是队列中却还有很多在队头和队尾没有包括的地方为空,从而产生空间的浪费。

6.3  循环队列

        循环链表就是将一个顺序队列变成一个环状队列,使其前后可以相连,但是这种环状是人为想象出来的,在计算机实际存储过过程中,还是顺序结构。这种环是一种逻辑结构,方便存储。

① 由空队进队两个元素,此时 front指向0,rear 指向2。

②进队4个元素,出队3个元素,此时 front 指向3,rear指向6。

③进队2个元素,出队4个元素,此时 front指向7,rear指向0。

        由①到③的变化过程可以看出,经过元素的进进出出,即便是 rear和 front都到了数组尾端,依然可以让元素继续入队,因为两指针不是沿着数组下标递增地直线行走,而是沿着一个环行走,走到数组尽头时自动返回数组的起始位置。

队列满条件:(rear+1)%Maxsize==front

队列为空条件:rear=front

队列中元素个数:(rear-front+Maxsize)%Maxsize

7 队列的基本操作

7.1 顺序存储队列的基本操作

  1. #define Maxsize 50
  2. typedef struct{
  3. int data[Maxsize];
  4. int front,rear;
  5. }SqQueue;

(1)构建一个空的队列

  1. void InitQueue(SqQueue &Q){
  2. Q.rear=Q.front=0;
  3. }

(2)判断队列是否为空

  1. bool IsEmpty(SqQueue Q){
  2. if(Q.rear==Q.front)
  3. return true;
  4. return false;
  5. }

(3)入队

  1. bool EnQueue(SqQueue &Q,int x){
  2. if((Q.rear+1)%Maxsize==Q.front) //判断队列是否满了
  3. return false;
  4. Q.data[Q.rear]=x;
  5. Q.rear=(Q.rear+1)%Maxsize;
  6. return true;
  7. }

        以下是模拟入队的整个过,以元素 3 4 1为例进行入队操作,整个顺序队列的Maxsize=10。第一行是队内元素在顺序表中的序号,第二行是队列元素数据。 

        以下是该是平台直观显示出来的队列位置,上方为队列位置,下方为该元素值。

注意:在入队之前,都需要判断队列是否已满,如果已满,则不然进行入队。

(4)出队

  1. bool DeQueue(SqQueue &Q,int &x){
  2. if(IsEmpty(Q)) //判断队列是否为空
  3. return false;
  4. x=Q.data[Q.front];
  5. Q.front=(Q.front+1)%Maxsize;
  6. return true;
  7. }

        以下是模拟出队的整个过,以上述入队之后情况为例进行出队3 4 元素和入队7元素操作,整个顺序队列的Maxsize=10。第一行是队内元素在顺序表中的序号,第二行是队列元素数据。 

         以下是该是平台直观显示出来的队列位置,上方为队列位置,下方为该元素值。

 注意:在出队之前,都需要判断队列是否为空,如果为空,则不然进行出队操作。

7.2  链式存储队列的基本操作

  1. typedef struct LinkNode{ //链式队列结点
  2. int data;
  3. struct LinkNode *next;
  4. }LinkNode;
  5. typedef struct{ //链式队列
  6. LinkNode *front,*rear; //队列的队头和队尾指针
  7. }LinkQueue;

(1)构建一个队列

  1. void InitQueue(LinkQueue &Q){
  2. Q.front==Q.rear=(LinkNode*)malloc(sizeof(LinkNode));
  3. if(!Q.front) //存储分配失败
  4. exit(OVERFLOW);
  5. Q.front->next=NULL;
  6. }

(2)判断队是否为空

  1. bool IsEmpty(LinkQueue Q){
  2. if(Q.front==Q.rear)
  3. return true;
  4. return false;
  5. }

(3)入队

类似于尾接法,把每次进入的插入到rear后面。

  1. void EnQueue(LinkQueue &Q,int x){
  2. LinkNode *p= (LinkNode *)malloc(sizeof(LinkNode));
  3. p->data=x;
  4. p->next=NULL;
  5. Q.rear->next=s;
  6. Q.rear=s;
  7. }

  以下是模拟队列入队的运行情况如图。以数字3  4  1为例进行入队。

   以下是该是平台直观显示出来的队列位置,结点左边为数据,右边为该结点地址,靠近左边的结点为队首front,尾部为队尾rear。

 注意:在链队里面,和链式栈一样,都是不用考虑空间大小的。

(4)出队

类似于删除链表的第一个结点。

  1. bool DeQueue(LinkQueue &Q,int &x){
  2. if(Q.front==Q.rear) //判断是否为空
  3. return false;
  4. LinkNode *p=Q.front->next;
  5. x=p->data;
  6. Q.front->next=p->next;
  7. if(Q.rear==p) //如果队列原来只有一个结点,删除后变成空队列
  8. Q.rear=Q.front;
  9. free(Q);
  10. reurn true;
  11. }

  以下是模拟队列出队的运行情况如图。以上诉3  4  1入队后为例进行出队,再进行一次入队操作,将元素 7 入队。 

    以下是该是平台直观显示出来的队列位置,结点左边为数据,右边为该结点地址,靠近左边的结点为队首front,尾部为队尾rear。

  注意:在链队里面,和链式栈一样,都是需要考虑是否为空的,如果为空,则不能进行出队。

 8 队列的相关应用

8.1 杨辉三角(队列形式输出)

(1)左上的元素,出队列为temp=1,并且打印temp=1, 取出队后现在队首为x=3

(2)求下一行元素temp=temp+x

(3)到最后1前面那个数字也就是3停止遍历

(4)循环外面出队列打印最后一个一个元素 1 ,并且存储下一行最后一个元素数字 1

下面是打印第四行,将第五行存入队列的全过程。

  1. #include<stdio.h>
  2. #define Maxsize 50
  3. typedef struct{
  4. int data[Maxsize];
  5. int front,rear;
  6. }SqQueue;
  7. void InitQueue(SqQueue &Q){
  8. Q.rear=Q.front=0;
  9. }
  10. bool IsEmpty(SqQueue Q){
  11. if(Q.rear==Q.front)
  12. return true;
  13. return false;
  14. }
  15. bool EnQueue(SqQueue &Q,int x){
  16. if((Q.rear+1)%Maxsize==Q.front) //判断队列是否满了
  17. return false;
  18. Q.data[Q.rear]=x;
  19. Q.rear=(Q.rear+1)%Maxsize;
  20. return true;
  21. }
  22. bool DeQueue(SqQueue &Q,int &x){
  23. if(IsEmpty(Q)) //判断队列是否为空
  24. return false;
  25. x=Q.data[Q.front];
  26. Q.front=(Q.front+1)%Maxsize;
  27. return true;
  28. }
  29. bool GetTop(SqQueue Q,int &x){
  30. if(IsEmpty(Q))
  31. return false;
  32. x=Q.data[Q.front];
  33. return true;
  34. }
  35. void YangHuiTriangle(int N){
  36. int n,x,i,temp;
  37. SqQueue Q;
  38. InitQueue(Q);
  39. EnQueue(Q,1);
  40. for(n=2;n<=N;n++){ //每一行进行打印
  41. EnQueue(Q,1); //每行第一个都是数字 1
  42. for(i=1;i<=n-2;i++){ //通过上一行求下一行 (第一行已知,所以可以求出第二行,依次类推)
  43. DeQueue(Q,temp); //先取出最前面的元素,也就是要求下一行的左上方的元素
  44. printf("%d ",temp); //将其打印
  45. GetTop(Q,x); //得到上述出队后,当前最前方元素,也就是要求下一行的上方的元素
  46. temp=temp+x; //得到当前要求元素
  47. EnQueue(Q,temp); //将其入队
  48. }
  49. DeQueue(Q,x); //输出上一行最后一个 1
  50. printf("%d ",x);
  51. EnQueue(Q,1); //存储要求这一行最后一个数字 1
  52. printf("\n"); //输出上一行的换行
  53. }
  54. while(!IsEmpty(Q)){ //输出最后一行
  55. DeQueue(Q,x);
  56. printf("%d ",x);
  57. }
  58. }
  59. int main(){
  60. YangHuiTriangle(7);
  61. return 0;
  62. }

 9 总结

        栈和队列的区别:栈是先进后出, 队列是先进先出,主要还是在逻辑层面,我们认为设定了顺序表或者链表的进出顺序,然后利用这些特性进行专门领域的运用。

        读者也就根据栈和队列的特性,自己总结出相关先进先出,或者先进后出等一系列生活中能运用到的,比如汉诺塔运用栈的先进后出可以解决问题;迷宫不仅可以运用栈的先进后出,也可以运用队列的先进先出进行回溯找寻出口位置。

        我们知道双端队列是两端都能进出,可以自己限制进出方式,使其拥有一定的特性。比如限制左边不能进,右边不能出。

        在双端队列进队时,前端进的元素排列在队列中后端进的元素的前面,后端进的元素排列在队列中前端进的元素的后面。在双端队列出队时,无论是前端还是后端出队,先出的元素排列在后出的元素的前面。思考:如何由入队序列a,b,c,d得到出队序列c,d,a, b?

        那限制右端不能进之后呢,如何由入队序列a,b,c,d得到出队序列c,d,a, b?  这些都说读者值得思考的问题。

参考资料:

[1] 苏小红,孙志刚,陈惠鹏等编著. C语言大学实用教程(第四版)[M]. 北京:电子工业出版社,2017.
[2] 严蔚敏,吴伟民. 数据结构[M]. 北京:清华大学出版社.

作者:

江西师范大学_姜嘉鑫;   江西师范大学_龚俊

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/490082
推荐阅读
相关标签
  

闽ICP备14008679号