数据结构--栈与队列

1.栈

栈作为一种限定性线性表,是将线性表的插入和删除操作限制为仅在表的一端进行,通常将表中允许进行插入,删除操作的一端称为栈顶(top),栈顶的当前位置是动态变化的,他由一个称为栈顶指针的位置指示器来指示,将表的另一端称为栈底。由于进栈和出栈只能在一端进行,所以栈中的元素具有先进后出的规则

1.1栈的表现和实现

1.1.1顺序栈

类似于顺序表,顺序栈是用一组地址连续的储存单元依次存放自栈底到栈顶的数据元素,同时附设一个top指针来指示栈顶位置,通常top=-1表示空栈,栈的定义如下

#define Stack_Size 50
typedef struct 
{
    StackElemType  elem[Stack_Size];  //elem是存放栈中元素的一维数组
    int top;
}SeqStack;
(1)初始化顺序栈
void InitStack(SeqStack *S)
{/*构造一个空栈S*/
    S->top=-1;
}
(2)顺序栈进栈操作

值得注意的是,类似于数组元素个数不能超过数组长度,同理在进栈时也要检查是否栈满,否则可能会发生上溢

#define TRUE 1
#define FALSE 0
int Push(SeqStack *S,StackElemType x){
    if(S->top==Stack_Size-1)  return FALSE;
    S->top++;     //修改栈顶指针
    S->elem[S->top]=x;   //x进栈
    return TRUE;
}
(3)顺序栈出栈操作

同样啦,进栈都要检查栈满,出栈肯定要检查栈空啦,不然拿什么来出

int Pop(SeqStack *S,StackElemType *x)
{/**x在这当然不是指栈里面的元素,而是我们要将取出来的元素存入x所指的存储空间里*/
      if(S->top==-1)  return FALSE;
      else{
        *x=S->elem[S->top];
        S->top--;
        return TRUE;
      }
}
(4)顺序栈读栈顶元素操作

顾名思义就是读取栈顶元素是啥,那栈顶指针当然就不变喽

int GetTop(SeqStack *S,StackElemType *x){
    if(S->top==-1)   return FALSE;
    else{
        *x=S->elem[S->top];
        return TRUE;
    }
}

注意:在实现上述操作时,也可以将参数说明*S改为S,也就是传地址方式改为传值方式,但传地址方式会更节省时间和空间(别告诉我你不会访问栈中元素,传值肯定就是" . "运算符啦)

(5)多栈共享技术

为了解决因为对栈空间大小难以准确估计而产生有的栈溢出,有的栈还有空闲空间的情况,可以让多个栈共享一个足够大的数组空间,通过利用栈的动态特性来使其存储空间互相补充,这就是多栈共享技术,最常用的就是两个栈,即双端栈

首先申请一个一维数组空间S[M],将两个栈的栈底分别放在一维数组的两端,初始化为-1,M,这样是真的六,你不用我多了我就可以用,这样不就不浪费空间了,哈哈哈哈哈……

话不多说看图看定义

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

(6)初始化双端栈
void InitStack(DqStack *S)
{
    S->top[0]=-1;  //表示0号栈为空
    S->top[1]=M;   //表示1号栈为空
}
(7)双端栈进栈操作

具体要看进哪个栈,可以用if语句,以下代码用switch-case语句做个示例

int Push(DqStack *S,StackElemType *x,int 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;
}
(8)双端栈出栈操作

基本思想跟进栈差不多也是分两种情况,可以将取出来的数据元素存储起来,也可以不用,这里就不再赘述了

1.1.2链栈

链栈即采用链表作为存储结构实现的栈,链表的表头指针作为栈顶指针,相信学完链表后接下来的内容会轻松很多,话不多说,先来一图便于理解

上图中,top为栈顶指针,始终指向栈顶元素前面的头结点,若top->next==NULL,则代表栈空,采用链栈不必预先估计栈的最大容量,只要系统又可用空间,链栈就不会出现溢出,链栈的各种操作与单链表基本类似,在使用完毕后应该释放相应空间,定义如下

typedef struct node
{
    StackElemType data;
    struct node *next;
}LinkStackNode;
typedef LinkStackNode *LinkStack;  //链栈指针

进栈

int Push(LinkStack top,StackElemType x)
{
    LinkStackNode *temp;
    temp=(LinkStackNode *)malloc(sizeof(LinkStackNode));
    if(temp==NULL)  return FALSE;  //申请空间失败
    temp->data=x;
    temp->next=top->next;
    top->next=temp;   //修改当前栈顶指针
    return TRUE;
}

出栈

int Pop(LinkStack top,StackElemType  *x)
{
    LinkStackNode *temp;
    temp=top->next;
    if(temp==NULL)  return FALSE; //栈为空
    top->next=temp->next;
    *x=temp->data;   //*x用于储存取出的栈顶元素
    free(temp);   //释放储存空间
    return TRUE;
}

多栈运算

算法思想:将多个链栈的栈顶指针放在一个一维指针数组中统一管理,从而实现同时管理和使用多个栈

#define M 10
typedef struct node
{
    StackElemType data;
    struct node *next;
}LinkStackNode,*LinkStack;
LinkStack top[M];

1.1.3栈的应用举例

由于栈的先进后出的特性,使得栈成为程序设计中常用的工具,以下介绍栈应用的两个典型例子:括号匹配问题和表达式求值

1.括号匹配问题

问题描述:设表达式中包含三种括号:圆括号,方括号,和花括号,它们可以相互嵌套如:({[]}),{[()]}等均为正确的格式,而{[]}),{[)]}等均为错误的格式

算法思想:其实括号匹配就跟栈的先进后出很相像了,比如假设这个括号匹配是对的那么对于左括号而言,在最右边的左括号就应该和最左边的右括号匹配,不理解的话可以写一个看看,所以检验算法可以设置一个栈,如果我们读入了一个左边的括号,那我们就让他进去,那我们如果读入的是右括号呢我们就取出栈顶元素与这个右括号进行匹配,如果匹配成功了那栈顶指针就向下降一位,除了不匹配之外,如果栈里面没有元素,但是我们又读到了一个右括号,那很明显右括号就多了呀,同理,如果我们已经读完了,但是栈里面还是有元素就说明左括号多余了噻,假设栈的一些基本运算已经写好了,代码如下:

void BracketMatch(char *str)
{
    Stack S;int i;char ch;
    InitStack(&S);  //将S初始化为空栈
    for(i=0;str[i]!='\0';i++){
        switch(str[i]){
            case '(':
            case '{':
            case '[':
            Push(&S,str[i]);  //进栈
            break;
            case ')':
            case '}':
            case ']':
            if(IsEmpty(S)){printf("右括号多余");return ;}
            else{
                GetTop(&S,&ch);  //将栈顶元素赋给ch
                if(Match(ch,str[i]))
                Pop(&S,&ch);  //匹配好的左括号出栈
                else{
                    printf("左右括号不匹配");return;
                }
            }
        }
    }
    if(IsEmpty(S))
      printf("括号匹配");
    else
      pritnf("左括号多余");
}
Match(char s1,char s2){
    if(s1=='('&&s2==')')  return TRUE;
    else if(s1=='['&&s2==']')  return TRUE;
    else if(s1=='{'&&s2=='}')  return TRUE;
    else return FALSE;
}
2.表达式求值

首先,先来了解几个概念

表达式的组成:运算对象,运算符,定界符

运算对象:既可以是常数,也可以是被说明为变量或者常量的标识符

运算符:分为算数运算符,关系运算符和逻辑运算符三类

定界符:括号和表达式结束符等等

由简到难,我们先看无括号运算和运算对象都是自然数,我们都知道运算符之间具有优先级,比如乘法要比加法先算,^表示幂运算,#是表达式结束符,优先级如下:

^ > *,/ > +,- > #

算法步骤:

(1)规定运算符的优先级表

(2)设置两个栈:OVS(运算数栈),OPTR(运算符栈)

(3)自左向右扫描,进行如下处理:如果遇到运算数就进OVS,若遇到运算符就与OPTR栈的栈顶运算符进行优先级比较:

a.当前运算符优先级>栈顶运算符,则当前运算符进栈

b.当前运算符优先级<=栈顶运算符,则出栈,得到栈顶元素符OVS出栈两次得到两个运算数,将这两个运算数进行相应计算,得到结果,结果进入OVS栈

以下是A/B^C+D*E(为了运算方便,输入表达式时会在末尾添加一个#)运算过程图:

相信看完这张图已经理解了,接下来看代码

int ExpEvaluation()
{
    InitStack(&OPTR);
    InitStack(&OVS);
    Push(&OPTR,'#');   //为了便于操作,首先将#压入栈中,这样如果OVS栈中才进一个数就遇到运算符,不管遇到什么运算符都可以让它直接进OPTR栈
    printf("\n\nPlease input an expression (Ending with #):");
    char ch;
    ch=getchar();
    while(ch!='#'||GetTOP(OPTR)){
        if(!In(ch,OPSet))  //操作数进OVS栈,OPSet为运算符集,需要自己定义
        {
            int n=GetNumber(ch);  //将ch由字符转换为int类型
            Push(&OVS,n);
            ch=getchar();
        }
        else{
            switch(Compare(ch,GetTop(OPTR))){  //compare为比较运算符优先级函数,自己写
                case '>':Push(&OPTR,ch);
                ch=getchar();break;
                case '=':
                case '<':char op;int a,b;
                         Pop(&OPTR,&op);
                         Pop(&OVS,&b);
                         Pop(&OVS,&a);
                         int v=Execute(a,op,b);
                         Push(&OVS,v);
                         break;
            }
        }
    }
   int v=GetTop(OVS);
   return v;
}
1.1.4栈与递归的实现

递归是指在定义自身的同时又出现了对自身的引用,也就是对自身的调用。如果一个函数在其定义体内直接调用自己,则称为直接递归函数;如果一个函数经过一系列的中间调用语句,通过其他函数间接调用自己,则称为间接递归函数

//直接递归 
func(…)
{
  …
  func(…)
}

//间接递归,通过调用别的函数间接调用自己
func1()
{
  …
  func2(…)
}

func2()
{
  …
  func1(…)
}

下面来简单写一个递归的代码吧

阿克曼函数(Ackerman函数)定义为

n+1m=0
Ack(m-1,1)m\neq0,n=0
Ack(m-1,Ack(m,n-1))m\neq0,n\neq0

int Ack(int m,int n)
{
	if(m==0)  return n+1;
	else if(n==0) return Ack(m-1,1);
	else  return Ack(m-1,Ack(m,n-1));
}

显然上述递归的例子与其本身的数学函数表达式有很大的关系,除此之外,在后续会学到的广义表,二叉树,数等等数据结构其本身具有固有的递归特性,因此也可以采用递归法进行处理,还有一种情况就是很多问题的求解过程可以用递归方法描述,一个典型的例子:汉诺(hanoi)塔问题

n阶汉诺塔问题:假设有三个分别命名为X,Y,Z的塔座,在塔座X上插有n个不同直径编号为1……n的圆盘,现要求将塔座X上的n个圆盘移至塔座Z上并按同样顺序叠排,圆盘移动必须遵循下列规则:

1.每次只能移动一个圆盘

2.圆盘可以插在X,Y和Z中的任何一个塔座上

3.任何时刻都不能将一个较大的圆盘压在较小的圆盘之上

首先我们需要先明确我们每一步的目的,这里我们自底向上来进行思考,首先我们我们想到如果我们要将A上的所有盘子移动到C上,又得随时保证大盘子在下面小盘子在上面,那么我们开始思考如何将最大的盘子先放到C上,这样才能进行下一步。

        我们先明确ABC三个柱子的角色,一个作为起始位置、一个作为中转的位置、一个是目的位置;我们开始从最大的盘子开始自底向上考虑,这里我用四个盘子来做例子:

        ①我们要将第四个盘子移动到C,那么前三个盘子就必须先移动到B,这样才能让第四个盘子直接移动到C(因为要移动最下面的盘子的话,必须整个柱子只剩它一个或者它在最上层才能移动);

        ②要将前三个盘子移动到B,第三个盘子就要在B的最下面,现在就将第三个盘子看做底,那么肯定前两个盘子就要先移动到C,这样第三个盘子才能直接移动到B;

        ③要将前两个盘子移动到C,那么第二个盘子就必须在C的最下面,现在将第二个盘子看做底,那么第一个盘子就必须先移动到B,这样第二个盘子才能直接移动到C;

        ④第一个盘子可以直接移动到B;

以上就是有四个盘子的时候第一步的思考,思考过程反过来就是移动步骤;每一步中的移动的思维就差不多;在同一层递归的整体的步骤就是:先将要移动的盘子上面的盘子放到中转的位置去,然后再把要移动的盘子放到目标位置去,最后将中转的位置的盘子再放到目标位置就完成了;每移动一次目标盘子,就需要执行一次以上步骤,因为我们移动的整体思路是自下而上的移动,所以只有当移动的盘子是最顶上的盘子或者只有它一个盘子的时候不需要执行这个三步;所以只要涉及到移动多个盘子就需要进行上述步骤,所以在每层递归中一开始的当前位置到中转,和最后的中转到目标位置,都需要递归执行,只有中间的当前位置直接到目标位置的这一步只移动一个盘子不需要递归。

void Hanoi(int n,char X,char Y,char Z){
    if(n == 1){
        printf("%c-->%c\n",X,Z);   //当只剩一个圆盘时,就开始移动圆盘到Z
    } else{
        Hanoi(n - 1,X,Z,Y);  //将编号为1-n-1的圆盘移到Y,Z为辅助塔
        printf("%c-->%c\n",X,Z);   //将编号为n的圆盘从X移到Z
        Hanoi(n - 1,Y,X,Z);    //将Y上编号为1-n-1的圆盘移到Z,X为辅助塔
    }
}
/*这里还可以将X,Y,Z设为栈,移动的步骤就可以看作是进栈和出栈操作*/

2.队列

队列时另一种限定性的线性表,它只允许在表的一段插入元素,而在表的另一端删除元素,所以队列具有先进先出的特性。这就跟我们日常生活排队是一样的,所以名字叫这个也就不足为奇了。在队列中,允许插入的一端称为队尾,允许删除的一端称为队头

2.1队列的表现与实现

2.1.1链队列

顾名思义,就是用链表来表示队列,为了操作方便,以下采用带头结点的链表结构,并设置一个队头指针front始终指向头结点和一个队尾指针rear始终指向最后一个结点,对于空的链队列,队头和队尾指针均指向头结点

链队列的定义代码如下:

typedef struct Node
{
	QueueElemType data;
	struct Node *next;
}LinkQueueNode;
typedef struct 
{
	LinkQueueNode *front;
	LinkQueueNode *rear;
}LinkQueue;
/*通常将队头指针和队尾指针封装在一个结构体中,并将该结构体类型重新命名为链队列类型*/
1.链队列初始化
int InitQueue(LinkQueue *Q)
{
	Q->front=(LinkQueueNode*)malloc(sizeof(LinkQueueNode));
	if(Q->front!=NULL){  //申请空间成功
		Q->rear=Q->front;
		Q->front->next=NULL;
		return TRUE;
	}
	else return FALSE;
}
/*在初始化链队列时,我们只需要为队头节点分配空间,而不需要为rear指针单独分配空间,因为rear只是用来指向队尾节点的,而队尾节点在初始化时就是队头节点。随着后续入队操作,rear指针会移动到新的队尾节点,但始终指向一个已存在的节点,不需要为它单独分配空间。简而言之,front和rear只是指针,它们指向链队列中的节点,而节点才是需要分配空间的实体。在初始化时,我们只需要为队头节点分配空间,并用front和rear指向它即可。*/
2.链队列入队操作
int EnterQueue(LinkQueue *Q,QueueElemType x)
{
	LinkQueueNode *NewNode;
	NewNode=(LinkQueueNode*)malloc(sizeof(LinkQueueNode));
	if(NewNode!=NULL){
		NewNode->data=x;
		NewNode->next=NULL;
		Q->rear->next=NewNode;   //建立原来队尾结点和新结点之间的联系
		Q->rear=NewNode;         //更新队尾结点
		return TRUE;
	}
	else return FALSE;
}
3.链队列出队操作
int DeleteQueue(LinkQueue *Q,QueueElemType *x)
{
	LinkQueueNode *p;
	if(Q->front==Q->rear)  return FALSE;  //检验是否为空队列
	p=Q->front->next;    //将第一个结点赋给p,为下面新建联系做准备
	Q->front->next=p->next;    //重新建立队头指针与下一个结点之间的联系,从而达到将第一个结点删除(出队列)的效果
	if(Q->rear==p)  Q->rear=Q->front;   //特殊情况:队列中只有一个元素,出来后变空队列
	*x=p->data;   //将原来第一个结点的值赋给x
	free(p);  /*这里的p是作为临时储存第一个结点,当我们进行完删除操作后,这个p结点已经成为了一个孤立的结点,这时候我们可以放心释放它的内存,防止内存泄漏*/
	return TRUE;
}
2.1.2循环队列

循环队列是队列的一种顺序表示和实现方法。与顺序栈类似,它使用的是一组地址连续的储存单元如一维数组Queue[MAXSIZE],我们可能会想当然的认为rear=MAXSIZE的时候队列就满了,但是仔细一想就发现,队列是先进先出的,那万一前面的一些元素出去了呢,那前面不就空了一些空间出来吗,这种情况我们称之为假溢出,真正队满的条件应该是:rear-front=MAXSIZE

那为了解决空间浪费的问题,于是就有了循环队列。我们依旧采用以上的那个一维数组来说明当rear+1==MAXSIZE,令rear=0,我们就求得了Queue[MAXSIZE-1]的后继Queue[0],或者通过取余运算来实现rear=(rear+1)%MAXSIZE我们同样可以得到一样的结果。所以借助取余运算我们可以自动实现队尾指针,队头指针的循环变化。进队操作时,rear=(rear+1)%MAXSIZE;出队操作front=(front+1)%MAXSIZE,理解不了的话请看下图

从上图我们就可以看出队空和队满的情况下都是rear=front那么如何判断队满呢,有两种解决办法:一是损失一个元素的储存空间,这样队尾指针就不会追上队头指针,此时判满(名义上的满)的条件为(rear+1)%MAXSIZE==front,判空的条件依然是rear==front。二是增设一个标志量,以区别队列是空还是满,这种方法就不损失空间。话不多说,看定义:

#define MAXSIZE 50   //定义队列的最大长度
typedef struct 
{
	QueueElemType element[MAXSIZE];
	int front;
	int rear;
}SeqQueue;

循环队列初始化:

void InitQueue(SeqQueue *Q)
{
	Q->front=Q->rear;  //初始化为一个空的循环队列
}

循环队列入队操作:

int EnterQueue(SeqQueue *Q,QueueElemType x)
{
	if((Q->rear+1)%MAXSIZE==Q->front)  return FALSE;  //这里采用的是第一种方法
	Q->element[Q->rear]=x;
	Q->rear=(Q->rear+1)%MAXSIZE;
	return TRUE;
}

循环队列出队操作:

int DeleteQueue(SeqQueue *Q,QueueElemType *x)
{
	if(Q->front==Q->rear)   return FALSE;
	*x=Q->element[Q->front];
	Q->front=(Q->front+1)%MAXSIZE;
	return TRUE;
}

2.3队列的应用举例--打印杨辉三角

特点:第i行元素有第i-1行元素产生,所以可以依次存放i-1行上的元素,然后逐个出队打印,同时生成第i行元素并入队

void YangHuiTriangle()
{   //打印前n行元素,队列长度为N
	SeqQueue Q;
	IintQueue(&Q);
	EnterQueue(&Q,1);  //第一行元素入队
	for(n=2;n<=N;n++) //产生第n行元素并入队,同时打印第n-1行元素
	{   EnterQueue(&Q,1);  //第n行第一个元素入队
	    for(i=1;i<=n-2;i++){  //利用队中第n-1行元素产生第n行的中间n-2个元素并入队
		DeleteQueue(&Q,&temp);
		printf("%d",temp);  //打印第n-1行元素
		GetHead(Q,&x);
		temp=temp+x;  //利用队中第n-1行元素产生第n行元素
		EnterQueue(&Q,temp);
		}
		DeleteQueue(&Q,&x);
		printf("%d",x);   //打印第n-1行的最后一个元素
		EnterQueue(&Q,1);  //第n行的最后一个元素入队
	}
	while(!IsEmpty(Q))  //打印最后一行元素
	{
		DeleteQueue(&Q,&x);
		printf("%d",x);
	}
}

  • 9
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值