03-栈与队列

3.1 栈

​ 3.1.1 抽象数据类型栈的定义

​ 3.1.2 栈的表示和实现

3.2 栈的应用举例

​ 3.3.1 数制转换

​ 3.3.2 括号匹配的检验

​ 3.3.4 行编辑程序

​ 3.3.5 迷宫求解

​ 3.3.5 表达式求值

3.4 队列

​ 3.4.1 抽象数据类型栈的定义

​ 3.4.2 链队列—队列的链式表示和实现

​ 3.4.3 循环队列—队列的顺序表示和实现

3.1 栈(Stack)

3.1.1 栈(stack)的定义

栈:限定仅在表尾进行插入和删除操作的线性表。

空栈:不含任何数据元素的栈。

允许插入和删除的一端称为栈顶,另一端称为栈底。

1557649832044

栈是一种后进先出(Last In First Out)的线性表,简称为LIFO表。

1557649900193

栈的操作特性:后进先出

栈的概念及实现

例:有三个元素按a、b、c的次序依次进栈,且每个元素只允许进一次栈,则可能的出栈序列有多少种?

1557650331499

1557650487804

注意:栈只是对表插入和删除操作的位置进行了限制,并没有限定插入和删除操作进行的时间。

堆栈的基本操作

  1. 插入( 进栈、入栈)

  2. 删除 (出栈、退栈)

  3. 测试堆栈是否为空

  4. 测试堆栈是否已满

  5. 检索当前栈顶元素

特殊性

  1. 其操作是一般线性表的操作的一个子集。 "

  2. 插入和删除操作的位置受到限制。

栈的基本操作

(1) 初始化栈 initstack(S) : 将栈S置为一个空栈(不含任何元素)。

(2) 求栈长操作 getlen(S) : 求栈中元素的个数。

(3) 取栈顶元素 gettop(S,x) : 取栈S中栈顶元素。

(4) 入栈 push(S,x) : 将元素x插入到栈S中,也称为 “入栈”、“插入”、 “压入”。

(5) 出栈 pop(S,x) : 删除栈S中的栈顶元素,赋给x。也称为“退栈”、 “删除”、 “弹出”。

(6) 判栈空 emptystack (S) : 判断栈S是否为空,若为空,返回值为true,否则返回值为false。

(7) 输出栈操作 list(S) : 依次输出栈S中所有元素

例1:对于一个栈,给出输入项A、B、C,如果输入项序列由ABC组成,试给出所有可能的输出序列。

A进 A出 B进 B出 C进 C出 ABC

A进 A出 B进 C进 C出 B出 ACB

A进 B进 B出 A出 C进 C出 BAC

A进 B进 B出 C进 C出 A出 BCA

A进 B进 C进 C出 B出 A出 CBA

不可能产生输出序列CAB

例2:一个栈的输入序列是12345,若在入栈的过程中允许出栈,则栈的输出序列43512可能实现吗?12345的输出呢?

43512不可能实现,主要是其中的12顺序不能实现;12345的输出可以实现,只需压入一个立即弹出一个即可。

例3:如果一个栈的输入序列为123456,能否得到435612和135426的出栈序列?

435612中到了12顺序不能实现;

135426可以实现。

3.1.2 栈的表示和实现

栈的顺序存储结构及实现

顺序栈——栈的顺序存储结构

如何改造数组实现栈的顺序存储?

1557650905360

确定用数组的哪一端表示栈底。

附设指针top指示栈顶元素在数组中的位置。

一、栈的顺序存储

栈是运算受限的线性表,线性表的存储结构对栈也适用。

1、顺序栈

​ 栈的顺序存储结构简称为顺序栈,它是运算受限的顺序表。即:用一组连续的存储单元(数组)依次存放栈中的每个数据元素。栈底位置是固定不变的,所以可以将栈底位置设置在数组的两端的任何一个端点。

约定:用下标变量记录栈顶的位置,栈顶指针始终指向栈顶元素的上一个单元,用top(栈顶指针)表示。用base(栈底指针)记录栈底位置,为栈空间的起始位置。

1557651044245

栈的顺序存储表示:
#define INITSIZE  100   //栈的存储空间初始分配量
    typedef int ElemType;
    typedef struct
    {   int  top;                      //栈顶指针
         ElemType   *base;    //栈底指针,存放空间起始地址
         int  stacksize;            //当前栈空间的长度
     }sqstack;
顺序栈中“上溢”和“下溢”的概念。

设S是sqstack类型的变量。若栈底位置在向量的低端,即S.base[0]是栈底元素,那么栈顶指针S.top是正向增加的,即进栈时需将S.top加1,退栈时需将S.top 减1。

S.top == 0时表示空栈, S.top == stacksize表示栈满。

当栈满时再做进栈运算必定产生空间溢出,简称“上溢”;

当栈空时再做退栈运算也将产生溢出,简称“下溢”。

上溢是一种出错状态,应该设法避免之;下溢则可能是正常现象。

1557651121969

2、顺序栈上的基本操作实现

(1) 初始化栈S (创建一个空栈S)
void initstack(sqstack *S)     
{     S->base=(ElemType *)malloc(INITSIZE*sizeof(ElemType));  
 
      S->top=0;          /*空栈标志*/
      S->stacksize = INITSIZE;     
}  
(2) 获取栈顶元素
int  gettop(sqstack S,ElemType *e)
{
      if ( S.top==0 )        /* 栈空 */
         {  printf(“Stack is empty!\n”);
             return 0;
          }
      *e= S.base[top-1]; 
      return 1;
}
(3) 进栈 (在栈顶插入新的元素x)
int  push ( sqstack *S , ElemType x )

{    if (S->top == S->stacksize)

​       { S->base=(ElemType *)realloc(S->base,

​                            (S->stacksize+1)*sizeof(ElemType)); 

​          if(!S->base) exit(-1);

​          S->stacksize++; 

​        }

​     S->base[S->top++] = x;   

​     return 1 ;

}
(4) 出栈 (取出S栈顶的元素值交给e)
int pop(sqstack *S, ElemType *e)
{
      if (S.top==0)  
      {  printf(“Stack is empty”);
          return 0;
       }
      *e=S->base [-- S->top ] ;
      return 1;
}

(5) 判断栈S是否为空
int stackempty(sqstack S)
    {   if (S.top==0) 
                  return  1 ;
         else   
                  return  0 ;
     }

结论:

由于栈的插入和删除操作具有它的特殊性,所以用顺序存储结构表示的栈并不存在插入删除数据元素时需要移动的问题,但栈容量难以扩充的弱点仍就没有摆脱。

二、 栈的链式存储

1、链栈

​ 当栈中元素的数目变化范围较大或不清楚栈元素的数目时,应该考虑使用链式存储结构。

​ 栈的链式存储结构称为“链栈” ,是运算受限的单链表,插入和删除操作仅限制在表头位置上进行。由于只能在链表头部进行操作,故链表没有必要像单链表那样附加头结点。栈顶指针就是链表的头指针。

栈的链接存储结构及实现

链栈:栈的链接存储结构

如何改造链表实现栈的链接存储?

1557651430872

将哪一端作为栈顶?

将链头作为栈顶,方便操作。

链栈需要加头结点吗?

链栈不需要附设头结点。

•链式栈无栈满问题,空间可随意扩充

•插入与删除仅在栈顶处执行

•链栈的栈顶在链头

栈的链式存储结构实现:
typedef  int  ElemType;    
typedef struct node      //栈的结点类型
{  ElemType   data;       //栈的数据元素类型
    struct node *next;    //指向后继结点的指针
}linkstack; 
typedef struct stack
{
    linkstack   *top;    /*链栈的头指针*/
}STACK;

2、链栈的部分基本操作实现

(1) 初始化栈S (创建一个不带头结点的空栈S )

void  initstack(STACK  *S)
 {     
       S->top=NULL;
 }

(2) 入栈
void push ( STACK *S , ElemType  e )
{   linkstack *p;
    p=( linkstack *) malloc( sizeof ( linkstack ) );
    if ( !p )  exit(0) ;
    p->data = e; 
    p->next = S->top;
    S->top = p;
}

(3) 出栈
void pop(STACK *S, ElemType *e)
{
      if ( S->top==NULL ) 
      {   printf(“Stack is empty”);
          exit(0); }
      else 
       {  *e=S->top->data;
           p=S->top;
           S->top= p -> next; 
           free(p);
        }
}
(4) 获取栈顶元素内容
void gettop(STACK S , ElemType *e)
{    if ( S.top==NULL ) 
      {   printf(“Stack is empty”);
           exit(0);
       }
       else *e=S.top->data ;
}

(5) 判断栈S是否空
int stackempty(STACK S)
{
         if (S.top==NULL) return 1;
         else return  0;
} 

两种实现方式的比较

v(1)顺序栈:入栈必须判断栈是否满了(上溢)

v(2)出栈和读栈顶元素操作,先判断是否为空。

v(3)链栈不须判断是否栈满。

顺序栈和链栈的比较

时间性能:相同,都是常数时间O(1)。

空间性能:

Ø顺序栈:有元素个数的限制和空间浪费的问题。

Ø链栈:没有栈满的问题,只有当内存没有可用空间时才会出现栈满,但是每个元素都需要一个指针域,从而产生了结构性开销。

总之,当栈的使用过程中元素个数变化较大时,用链栈是适宜的,反之,应该采用顺序栈。

3.2 栈的应用举例

【例1】将从键盘输入的字符序列逆置输出

比如,从键盘上输入:“tset a si sihT”,算法将输出:“This is a test”

以下是解决这个问题的完整算法。

#include “stdio.h”
typedef char SElemType;
void ReverseRead( )
             {STACK S; //定义一个栈结构S
                char ch;
                initstack(&S);             //初始化栈
       while ((ch=getchar())!=’\n’) 
               //从键盘输入字符,直到输入换行符为止 
         push(&S,ch);        //将输入的每个字符入栈
   while (!stackempty(S))   
                       //依次退栈并输出退出的字符
         {pop( &S , ch ) ; putchar ( ch ) ;         }
    putchar(‘\n’);}

【例2】数制转换

使用辗转相除法可将一个十进制数值转换成非十进制数值。即用该十进制数值除以非十进制数的基数(R进制数的基数为R),并保留其余数;重复此操作,直到该十进制数值为0为止。最后将所有的余数反向输出就是所对应的非十进制数值。

该算法基于下列原理:

N = ( N / d ) * d + N % d

( 其中: / 为整除运算, % 为求余运算)

例如 (1348)10=(2504)8,其运算过程如下:

​ N N/8(整除) N%8(取余)

​ 1348 168 4

​ 168 21 0

​ 21 2 5

​ 2 0 2

我们看到所转换的8进制数按低位到高位的顺序产生的,而通常的输出是从高位到低位的,恰好与计算过程相反,因此转换过程中每得到一位8进制数则进栈保存,转换完毕后依次出栈则正好是转换结果。

算法思想如下:当N>0时重复1,2

​ 1. 若 N≠0,则将N % r 压入栈s中 ,执行2;若N=0,将栈s的内容依次出栈,算法结束。

  1. 用N / r 代替 N

以下是解决这个问题的完整算法: m

void Decimal_Binary ( )
{     int N;      STACK S;        //定义栈结构S
      initstack ( &S ) ;    //初始化栈S
      scanf(“%d”,&N);  //输入十进制正整数
      scanf(“%d”,&r);  //输入 正整数r (进制)
      while (N>0) 
        {    push( &S , N%r );     //余数入栈
             N /= r; //被除数整除以r,得到新的被除数
         }
     while ( !stackempty(S) ) 
              //依次从栈中弹出每一个余数,并输出之
       {   pop( &S , N );       
            printf(“%d”, N );       }
}

【例3】检验表达式中的括号匹配情况

​ 假设在一个算术表达式中,可以包含三种括号:圆括号“(”和“)”,方括号“[”和“]”和花括号“{”和“}”,并且这三种括号可以按任意的次序嵌套使用。比如 ,[ { } [ ] ] [ ] ( ) 。现在需要设计一个算法,用来检验在输入的算术表达式中所使用括号的合法性。

​ 算术表达式中各种括号的使用规则为:出现左括号,必有相应的右括号与之匹配,并且每对括号之间可以嵌套,但不能出现交叉情况。

可以利用一个栈结构保存每个出现的左括号,当遇到右括号时,从栈中弹出左括号,检验匹配情况。在检验过程中,若遇到以下几种情况之一,就可以得出括号不匹配的结论。

(1) 当遇到某一个右括号时,栈已空,说明到目前为止,右括号多于左括号;

​ (2) 从栈中弹出的左括号与当前检验的右括号类型不同,说明出现了括号交叉情况;

​ (3) 算术表达式输入完毕,但栈中还有没有匹配的左括号,说明左括号多于右括号。

以下是解决这个问题的完整算法。

            typedef char ElemType;
            int Check( )
            {STACK S;        //定义栈结构S
             char ch;
            initstack(&S);      //初始化栈S
while ((ch=getchar())!=’\n’) //以字符序列的形式输入表达式
   if  ( ch==‘(’ || ch== ‘[’ || ch== ‘{’ ) 
           push(&S,ch); //遇左括号入栈
else  if( ch== ‘)’ )   
                 //在遇到右括号时,分别检测匹配情况
                if (stackempty ( S ) )  retrun 0;     
                else {  pop(&S,&ch);
                            if ( ch!= ‘(’  )  return 0; 
                         }
            else if (ch== ‘]’) 
                        if ( stackempty(S) ) retrun0;
                        else {  pop( &S,&ch) ;
                                    if ( ch!= ‘[’ ) return 0;  
                                 }
else if ( ch== ‘}’ ) 
                          if (stackempty(S) ) retrun 0;
                          else {  pop(&S,&ch); 
                                      if ( ch!= ‘{’ ) return 0; 
                                   }
                       else continue;
  if ( stackempty(S) ) return 1;
  else return 0;
}

【例4】表达式求值( 这是栈应用的典型例子 )

例如:3*(7 – 2 )

(1)要正确求值,首先了解算术四则运算的规则:

​ a. 从左算到右

​ b. 先乘除,后加减

​ c. 先括号内,后括号外

​ 由此,此表达式的计算顺序为:

​ 3*(7 – 2 )= 3 * 5 = 15

v(2)根据上述三条运算规则,在运算的每一步中,对任意相继出现的算符q1和q2 ,都要比较优先权关系。

v算符优先法所依据的算符间的优先关系

​ 见教材P53表3.1

v(是提供给计算机用的表!)

(3)算法思想:

•设定两栈:操作符栈 OPTR ,操作数栈 OPND

•栈初始化:设操作数栈 OPND 为空;操作符栈 OPTR 的栈底元素为表达式起始符 ‘#’;

•依次读入字符:操作数入OPND栈,操作符则判断:

​ 栈顶元素<操作符:压入OPTR栈。

​ 栈顶元素 =操作符且不为‘#’:弹出左括号;

​ 栈顶元素>操作符:退栈、计算,将结果压入

​ OPND栈;

求解表达式 3*(7-2) 的求值过程如下:

1557651904743

算法如下:

Status    EvaluateExpression( OperandType  &result)   {
  InitStack(OPND); InitStack(OPTR);    
  Push(OPTR ,’#’);c=getchar();
  while( (c!=‘#’) ||(GetTop(OPTR)!=‘#’) )
   {   if (!In(c,OP)  {    Push(OPND,c);   c=getchar();}  
       else   switch(compare(GetTop(OPTR) ,c))
                 {case ‘<’ :    Push(OPTR , c); c=getchar();break; 
                   case ‘=’:   Pop(OPTR);c=getchar();break;
                  case ‘>’ : temat=Pop(OPTR); b=Pop();a=Pop();
                                  result=Operate(a,temat,b);  
                                  Push(OPND,result);
                                  break;  
                } //switch  
     }//while   
  result=GetTop(OPND);}//EvaluateExpression

3.3  栈与递归

n递归的定义 若一个对象部分地包含它自己, 或用它自己给自己定义, 则称这个对象是递归的;若一个过程直接地或间接地调用自己, 则称这个过程是递归的过程。

当多个函数构成嵌套调用时, 遵循 后调用先返回

以下三种情况常常用到递归方法

Ø递归定义的数学函数

Ø具有递归特性的数据结构

Ø可递归求解的问题

1,递归定义的数学函数:

•阶乘函数: 1557652085709

•2阶Fibonaci数列: 1557652112376

  1. 具有递归特性的数据结构 1557652173893
  2. 可递归求解的问题:

迷宫问题 Hanoi塔问题

用分治法求解递归问题

分治法:对于一个较为复杂的问题,能够分解成几个相对简单的且解法相同或类似的子问题来求解

必备的三个条件

•1、能将一个问题转变成一个新问题,而新问题与原问题的解法相同或类同,不同的仅是处理的对象,且这些处理对象是变化有规律的

•2、可以通过上述转化而使问题简化

•3、必须有一个明确的递归出口,或称递归的边界

【举例5】栈与递归的实现

在高级语言编制的程序中,调用函数与被调用函数之间的链接和信息交换必须通过栈进行。在一个函数的运行期间调用另一个函数时,运行被调用函数之前,系统需先完成三件事:

1)将所有的实在参数、返回地址等信息传递给被调用函数保存;

2)为被调用函数的局部变量分配存储区;

3)将控制转移到被调用函数的入口。

从被调用函数返回调用函数之前,应该完成:

1)保存被调函数的计算结果;

2)释放被调函数的数据区;

3)依照被调函数保存的返回地址将控制转移到调用函数。

​ 多个函数嵌套调用的规则是:后调用先返回,此时的内存管理实行“栈式管理”

【例】栈与递归

​ 栈的一个重要应用是在程序设计语言中实现递归过程。现实中,有许多实际问题是递归定义的,这时用递归方法可以使许多问题的结果大大简化,以 n!为例:

n!的定义为:

1557652290288

根据定义可以很自然的写出相应的递归函数:

        int fact (int n) 

         {   if (n==0)  return 1 ;

              else return  (n* fact (n-1) ) ;

         }

分治法求解递归问题算法的一般形式:

void p (参数表) {

​ if (递归结束条件)可直接求解步骤;-----基本项

​ else p(较小的参数);------归纳项

​ }

long Fact ( long n ) {
    if ( n == 0) return 1;//基本项
    else return n * Fact (n-1); //归纳项}

非递归算法:

long fac(int n)
{  
     long f=1;
      ElemType x;sqstack S;
      initstack(&S);
     while(n>0)   {   push(&S,n); n-- ;    }
     while(S.top!=0)   {   pop(&S,&x);  f=f*x;   }
      return f;
}

栈和递归的应用

​ —— 汉诺( Hanoi)塔

​ 传说在创世纪时,在一个叫Brahma的寺庙里,有三个柱子,其中一柱上有64个盘子从小到大依次叠放,僧侣的工作是将这64个盘子从一根柱子移到另一个柱子上。

移动时的规则:

• 每次只能移动一个盘子;

• 只能小盘子在大盘子上面;

• 可以使用任一柱子。

Hanoi塔问题 1557652435032

n = 1,则直接从 A 移到 C。否则

(1)用 C 柱做过渡,将 A 的(n-1)个移到 B

(2)将 A 最后一个直接移到 C

(3)用 A 做过渡,将 B 的 (n-1) 个移到 C

分析:

​ 设三根柱子分别为 x,y, z , 盘子在 x 柱上,要移到 z 柱上。

1、当 n=1 时,盘子直接从 x 柱移到 z 柱上;

2、当 n>1 时, 则

①设法将前n–1个盘子借助 z ,从 x 移到 y 柱上,把 盘子 n 从 x 移到z柱上;

② 把n –1 个盘子 从 y 移到 z 柱上。

算法如下:

Void Hanoi ( int n, char x, char y, char z )

1{ //将 n 个 编号从上到下为 1…n 的盘子从 x 柱,借助 y 柱移到 z 柱

2 if ( n = = 1 )

3 move ( x , 1 , z ) ;

​ //将编号为 1 的盘子从 x 柱移到 z 柱

4 else {

​ //将 n -1个 编号从上到下为1到n-1的盘子从 x 柱,借助 z 柱移到 y 柱

5 Hanoi ( n-1 , x , z , y ) ;

6move ( x , n, z) ;

​ //将编号为 n 的盘子从 x 柱移到 z 柱

7 Hanoi ( n-1 , y , x , z );

​ //将 n -1个 编号从上到下为 1…n-1的盘子从 y 柱,借助 x 柱移到 z 柱 }} //Hanoi

递归的优缺点

优点:结构清晰,程序易读

缺点:每次调用要生成工作记录,保存状态信息,入栈;返回时要出栈,恢复状态信息。时间开销大。

3.4 队列(Queue)

3.4.1 队列的定义及基本操作

v例1:排队购物。

v例2:操作系统中的作业排队。

v例3:乘坐公共汽车,应该在车站排队,车来后,按顺序上车。

​ 队列(Queue)也是一种运算受限的线性表。

队列的逻辑结构

队列:只允许在一端进行插入操作,而另一端进行删除操作的线性表。 a

空队列:不含任何数据元素的队列。

允许插入(也称入队、进队)的一端称为队尾,允许删除(也称出队)的一端称为队头。

队列的逻辑结构

1557652598281

队列的操作特性:先进先出

练习

设栈S和队列Q的初始状态为空,元素e1、e2、e3、e4、e5和e6依次通过S,一个元素出栈后即进入Q,若6个元素出队的序列是e2、e4、e3、e6、e5和e1,则栈S的容量至少应该是( )。

A)2 (B)3 (C)4 (D)6

队列的基本运算:

1.初始化队列 initqueue(Q) :将队列Q设置成一个空队列。

3.求队列长度 getlen(Q) : 返回队列的元素个数。

3.取队头元素 gettop(Q, e) :得到队列Q的队头元素之值,并用e返回其值。

4.入队列 enqueue(Q,x) :将元素x插入到队尾中,也称“进队” ,“插入”。

5.出队列 dequeue(Q,e) :将队列Q的队头元素删除,并用e返回其值,也称“退队”、“删除”。

6.判队空 emptyqueue(Q) :判断队列Q是否为空,若为空返回1,否则返回0。

7.输出队列 list(Q) :依次输出从队头到队尾的所有元素

3.4.2 链队列的表示和实现

链队列概念

队列的链式存储结构简称为链队列,它是限制仅在表头删除和表尾插入的单链表。

​ 显然仅有单链表的头指针不便于在表尾做插入操作,为此再增加一个尾指针,指向链表的最后一个结点。即:一个链队列由头指针和尾指针唯一确定。

队头指针即为链表的头指针

队列的链接存储结构及实现

非空链队列 1557652722384

空链队列 1557652763004

链队列示意图

非空队列

1557652841041

空队列 Q.front==Q.rear

1557652886238

将这两个指针封装在一起,链队列的类型linkqueue定义为一个结构类型: *

typedef int ElemType;
typedef  struct  node
     {    ElemType  data;
           struct node  *next;
     }qlink;  //链队列的结点的类型
typedef struct
     {   qlink  *front;   //队头指针
          qlink  *rear;    //队尾指针
     }linkqueue;  //队列类型

链队列上基本运算的实现

(1)初始化操作

void initqueue(linkqueue  *LQ)
{
    LQ->front=LQ->rear=(qlink *) malloc(sizeof(qlink));
    if(!LQ->front)  exit (0); 
    LQ->front->next=LQ->rear->next=NULL;  //初始化队头队尾指针
}

(2)队列的判空

int emptyqueue (linkqueue  LQ)
{
   return(LQ.front->next==NULL&&LQ.rear->next==NULL);
}

如果没有头结点会怎样?

front=rear=NULL

算法描述:

p->next=NULL;

rear=p; 

front=p;

(3)入队操作

void enqueue(linkqueue *LQ, ElemType  x)
{    qlink *p;
      p=(qlink * )malloc(sizeof(qlink));
      p–>data=x;
      p–>next=NULL;
     LQ->rear–>next=p;
     LQ->rear=p;
}

注:入队只需修改队尾指针。

链队列的实现——出队 lue

算法描述:

p=front->next; 

front->next=p->next

如何判断边界情况? tml> ����*@(zN�}N

算法描述:

if (p->nextNULL)(或者rearp)

​ rear=front;

(4)出队操作

int  dequeue ( linkqueue *LQ, ElemType *e)
  {   linkqueue *p;
       if( emptyqueue(LQ)  )   return 0;
       p=LQ->front->next;
       *e=p–>data;
       LQ->front->next=p–>next;
       if( LQ->rear == p )
            LQ->rear=LQ->front;
       free(p);
       return OK;
    }

注意:在出队算法中,一般只需修改队头指针。但当原队中只有一个结点时,该结点既是队头也是队尾,故删去此结点时亦需修改尾指针,且删去此结点后队列变空。

3.4.3 队列的顺序表示和实现

1、顺序队列和循环队列

​ 队列的顺序存储结构称为顺序队列,顺序队列实际上是运算受限的顺序表,和顺序表一样,顺序队列也是必须用一个向量空间来存放当前队列中的元素。

队列的顺序存储结构及实现

顺序队列:队列的顺序存储结构

如何改造数组实现队列的顺序存储?

例:a1a2a3a4依次入队

1557653222342

入队操作时间性能为O(1)

例:a1a2依次出队

1557653269339

出队操作时间性能为O(n)

如何改进出队的时间性能?

放宽队列的所有元素必须存储在数组的前n个单元这一条件 ,只要求队列的元素存储在数组中连续的位置

设置队头、队尾两个指针

头指针始终指向队头元素

尾指针始终指向队尾元素的下一位置。

v入队:将新元素插入所指的位置,然后尾指针加1。

v出队:删去所指的元素,队头指针加1并返回被删元素。

由此可见,当头尾指针相等时队列为空。

1557653380112

队列的顺序存储结构定义:

#define MAXQSIZE 100  //队列的最大长度
typedef int ElemType;
typedef  struct
{
  ElemType *base;  //队列空间的起始位置
   int front;   //队头指针,即头元素在数组中的位序
   int rear;    //队尾指针,指向队尾元素的下一位置
}cqueue;    

顺序队列

队空:Q.front==Q.rear

队满:Q.rear=MAXQSIZE(有假溢出)

求队长: Q.rear-Q.front

入队:新元素按 rear 指示位置加入,再将队尾指针加一 ,即 rear = rear + 1

出队:将front指示的元素取出,再将队头指针加一,即front = front + 1,

继续入队会出现什么情况?

1557653485139

假溢出:当元素被插入到数组中下标最大的位置上之后,队列的空间就用尽了,尽管此时数组的低端还有空闲空间,这种现象叫做假溢出。

因为在入队和出队的操作中,头尾指针只增加不减小,致使被删除元素的空间永远无法重新利用。

因此,尽管队列中实际的元素个数远远小于向量空间的规模,但也可能由于尾指针巳超出向量空间的上界而不能做入队操作。该现象称为假上溢。

如何解决假溢出?

1557653533266

循环队列:将存储队列的数组头尾相接。

   为充分利用向量空间,克服上述假上溢现象,可以将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量,存储在其中的队列称为循环队列(Circular Queue)。 

1557653579421

在循环队列中进行出队、入队操作时,头尾指针仍要加1,朝前移动。只不过当头尾指针指向向量上界(MAXQSIZE-1)时,其加1操作的结果是指向向量的下界0。

​ 由于入队时尾指针向前追赶头指针,出队时头指针向前追赶尾指针,故队空和队满时头尾指针均相等。 无法通过Q.front=Q.rear 来判断队列“空”还是“满”。

循环队列

队空:Q.front =Q. rear

队满:Q.front =(Q.rear + 1) % MAXCSIZE

入队: Q.rear = (Q.rear + 1) % MAXCSIZE

出队: Q.front = (front + 1) % MAXCSIZE

求队长: (Q.rear-Q.front+ MAXCSIZE)% MAXCSIZE

循环队列的基本运算实现

1、进队列

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

2)将新元素赋给队尾指针所指单元;

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

int enqueue (cqueue *cq, ElemType  x)
     { if((cq->rear+1)%MAXCSIZE == cq->front)  return 0;
        cq->base[cq->rear]=x;  //插入x值到队尾
        cq->rear=(cq->rear+1)%MAXCSIZE;  //队尾
  1. 出队列

(1) 检查队列是否为空,若队空,进行下溢错误处理;

(2) 取队首元素的值。

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

int dequeue (cqueue *cq, ElemType *e)
{   if ( cq->rear== cq->front )  return 0 ; //队空
     *e=cq->base[cq->front] ;  //e值带回出队元素的值
     cq->front=(cq->front+1)%MAXCSIZE; //队头指针循环后移
     return 1 ; } 

循环队列和链队列的比较

时间性能:

循环队列和链队列的基本操作都需要常数时间

空间性能:

循环队列:必须预先确定一个固定的长度,所以有存储元素个数的限制和空间浪费的问题。

链队列:没有队列满的问题,只有当内存没有可用空间时才会出现队列满,但是每个元素都需要一个指针域,从而产生了结构性开销。

双端队列

v限定插入和删除操作在表的两端进行的线性表。也可以限定为输出受限(输入受限)的双端队列。

3.5 队列的应用举例

【举例1】模拟打印机缓冲区。

​ 在主机将数据输出到打印机时,会出现主机速度与打印机的打印速度不匹配的问题。这时主机就要停下来等待打印机。显然,这样会降低主机的使用效率。为此人们设想了一种办法:为打印机设置一个打印数据缓冲区,当主机需要打印数据时,先将数据依次写入这个缓冲区,写满后主机转去做其他的事情,而打印机就从缓冲区中按照先进先出的原则依次读取数据并打印,这样做即保证了打印数据的正确性,又提高了主机的使用效率。由此可见,打印机缓冲区实际上就是一个队列结构。

【举例2】CPU分时系统

​ 在一个带有多个终端的计算机系统中,同时有多个用户需要使用CPU运行各自的应用程序,它们分别通过各自的终端向操作系统提出使用CPU的请求,操作系统通常按照每个请求在时间上的先后顺序,将它们排成一个队列,每次把CPU分配给当前队首的请求用户,即将该用户的应用程序投入运行,当该程序运行完毕或用完规定的时间片后,操作系统再将CPU分配给新的队首请求用户,这样即可以满足每个用户的请求,又可以使CPU正常工作。

【举例3】汽车加油站

​ 随着城市里汽车数量的急速增长,汽车加油站也渐渐多了起来。通常汽车加油站的结构基本上是:入口和出口为单行道,加油车道可能有若干条。每辆车加油都要经过三段路程,第一段是在入口处排队等候进入加油车道;第二段是在加油车道排队等候加油;第三段是进入出口处排队等候离开。实际上,这三段都是队列结构。若用算法模拟这个过程,就需要设置加油车道数加2个队列。

小结

线性表、栈与队的异同点

相同点:

逻辑结构相同,都是线性的;都可以用顺序存储或链表存储;栈和队列是两种特殊的线性表,即受限的线性表(只是对插入、删除运算加以限制)。

不同点:

① 运算规则不同,线性表为随机存取,而栈是只允许在一端进行插入和删除运算,因而是后进先出表LIFO;队列是只允许在一端进行插入、另一端进行删除运算,因而是先进先出表FIFO。

② 用途不同,线性表比较通用;堆栈用于函数调用、递归和简化设计等;队列用于离散事件模拟、多道作业处理和简化设计等。

假设正读和反读都相同的字符序列为“回文”,例如,‘abba’和‘abcba’是回文,‘abcde’ 和‘ababab’则不是回文。假设一字符序列已存入计算机,请分析用线性表、堆栈和队列等方式正确输出其回文的可能性?

答:线性表是随机存储,可以实现,靠循环变量(j–)从表尾开始打印输出;

堆栈是后进先出,也可以实现,靠正序入栈、逆序出栈即可;

队列是先进先出,不易实现。

哪种方式最好,要具体情况具体分析。

(1)若正文在机内已是顺序存储,则直接用线性表从后往前读取即可,或将堆栈栈顶开到数组末尾,然后直接用出栈操作实现。

若正文是单链表形式存储,则等同于队列,需开辅助空间,可以从链首开始入栈,全部压入后再依次输出。

写出下列程序段的输出结果(栈的元素类型SElem Type为char)。

void main( ){
Stack S;   Char x,y;
InitStack(S);  x=’c’;y=’k’;
Push(S,x); Push(S,’a’);  Push(S,y);
Pop(S,x); Push(S,’t’); Push(S,x);
Pop(S,x); Push(S,’s’);
while(!StackEmpty(S)){ Pop(S,y);printf(y); };
Printf(x);}

答:输出为“stack”。

如果用一个循环数组q[0…m-1]表示队列时,该队列只有一个队列头指针front,不设队列尾指针rear,而改置计数器count用以记录队列中结点的个数。

编写实现队列的三个基本运算:

判空、入队、出队

判空:

int Empty(cqnode  cq)   //cq是cqnode类型的变量  
{if(cq.count==0) return(1);
     else return(0);  //空队列}

入队:

int EnQueue(cqnode cq,elemtp x)
{if(count==m){printf(“队满\n”);exit(0); }
             cq.q[(cq.front+count)%m]=x;    //x入队
             count++; return(1);   
                  //队列中元素个数增加1,入队成功。
}

出队:

int DelQueue(cqnode cq)
{if (count==0){printf(“队空\n”);return(0);}
 printf(“出队元素”,cq.q[cq.front]);
x=cq.q[cq.front];
cq.front=(cq.front+1)%m;  //计算新的队头指针。
return(x)
}

假设以带头结点的循环链表表示队列,并且只设一个指针指向队尾结点,不设头指针,请写出相应的入队列和出队列算法。

void EnQueue (LinkedList rear, ElemType x)
// rear是带头结点的循环链队列的尾指针,本算法将元素x插入到队尾。
{ s= (LinkedList) malloc (sizeof(LNode)); 
                                         //申请结点空间
 s->data=x;  s->next=rear->next;         
                                          //将s结点链入队尾
  rear->next=s;  rear=s;                 
   
   //rear指向新队尾
   void DeQueue (LinkedList  rear)
     // rear是带头结点的循环链队列的尾指针,本算法执行出队操作,操作成功输出队头元素;否则给出出错信息。
     { if (rear->next==rear)  { printf(“队空\n”); exit(0);}
       s=rear->next->next;        //s指向队头元素
       rear->next->next=s->next;    //队头元素出队。
       printf (“出队元素是”,s->data);
       if (s==rear) rear=rear->next;   //空队列
      free(s);
     }

已知一个带有表头结点的单链表,结点结构为: data|link

假设该链表只给出了头指针list,在不改变链表的前提下,请设计一个尽可能高效的算法,查找链表中倒数第K个位置上的结点 (K为正整数),若查找成功,算法输出该结点的data域的值,并返回1;否则只返回0。要求:

(1)描述算法的基本设计思想;

(2)描述算法的详细实现步骤,关键之处请给出简要注释。

基本设计思想:

增加P和P1两个指针变量和一个整型变量K。

从链表头向后遍历,其中指针p指向当前遍历的结点,指针p1指向p所指向结点的前k个结点,如果p之前没有k个结点,那么p1指向表头结点。用整型变量i表示当前遍历了多少个结点,当i>k时,p1随着每次的遍历,也向后移动一个结点。则遍历完成时,p1或者指向表头结点,或者指向链表中倒数第k个位置上的结点。

算法:

算法1:空间3,时间n
p=list->link;p1=list;i=1;
while(p)
{    p=p->link;
    i++;
    if (i> k) p1=p1->next;//保证了p1指向从p开始倒数的第k个结点
}
if (p1==list) return 0;//p之前没有k个结点
else 
{    printf("%d\n", p1->data);
    return 1;
}

算法2:空间3,时间n
p=list->link;   p1=list;  i=1;
while(p&&i<k)
{    p=p->link;
    i++;}
if (!p) return 0;//没有k 个结点
while(p)
{
    p=p->link;
    p1=p1->next;//保证了p1指向从p开始倒数的第k个结点

}
printf("%d\n", p1->data);
return 1;

算法3:首先遍历整个链表,获取链表的长度n。再重新遍历到链表的第n-k+1个结点,即为所求。
p=list->link;i=0;
while(p)
{    p=p->link;    i++;}    //i统计结点总数n
if (i<k) return 0;   //倒数也没有第k个节点
p=list->link
while(i>k)
{    p=p->link;       i--;} //刚好走过了n-k+1个结点
printf("%d\n", p->data);
return 1;

比较:

空间复杂度:算法3最好。

时间复杂度:算法2最好

猜你喜欢

转载自blog.csdn.net/qq_44621510/article/details/90143761
03-