Contents

数据结构(第三章)

栈和队列

栈的定义及基本运算

  1. 栈(Stack)的定义

    栈(Stack)是限制在表的一端进行插入和删除运算的线性表,通常称插入、删除的这一端为栈顶(top),另一端为栈底(bottom)。当表中没有元素时称为空栈。

  2. 栈的特点

    栈的修改是按后进先出的原则进行的。因此,栈称为后进先出表(LIFO)。

    https://cdn.jsdelivr.net/gh/adan-ning/images/202403311324102.png

  3. 栈的运算演示

    1. A、B、C、D四个元素依次进入一个栈,再依次出栈,得到一个输出序列DCBA。
  4. 栈的基本运算

    1. InitStack(&S): 初始化栈S
    2. StackEmpty(): 判断栈是否为空
    3. Push(e): 将元素e放入栈顶
    4. Pop(e): 移走栈顶的元素,同时由e带回该元素的值
    5. Gettop(): 获取栈顶的元素,但不从栈中移走

栈的存储结构和实现

  1. 栈的表示和实现

    假设栈$S=(a_1,a_2,a_3,…,a_n)$,则**$a_1$称为栈底元素,$a_n$为栈顶元素**。栈中元素按$a_1,a_2,a_3,…,a_n$的次序进栈,退栈的第一个元素应为栈顶元素。换句话说,栈的修改是按后进先出的原则进行的。因此,**栈称为后进先出表(Last In First Out,LIFO) **

    https://cdn.jsdelivr.net/gh/adan-ning/images/202403311328231.png

  2. 顺序栈

    由于栈是运算受限的线性表,因此线性表的存储结构对栈也适应。栈的顺序存储结构简称为顺序栈,它是运算受限的线性表。因此,可用数组来实现顺序栈。

  3. 顺序栈的类型定义

    // -----栈的顺序存储表示-----
    # define STACK_INIT_SIZE 100;
    # define STACKINCREMENT 10;
    typedef struct {
    	  SElemType  *base;    //栈底指针,栈构造前和销毁后为空
    	  SElemType  *top;    //栈顶指针,指向栈顶元素的下一位置
    	  int  stacksize;    //当前分配的栈的存储空间数
    }SqStack; 
    
  4. 顺序栈运算

    设S是SqStack类型的指针变量。base是栈底指针。Top是栈顶指针。

    1. 栈不存在条件S.base=NULL
    2. 栈空条件S.top=S.base
    3. 插入栈顶元素,栈顶指针S.top=S.top+1
    4. 删除栈顶元素,栈顶指针S.top=S.top-1
    5. 栈满条件S.top-S.base>=S.stacksize

    https://cdn.jsdelivr.net/gh/adan-ning/images/202403311332990.png

  5. 顺序栈的C语言实现

    //初始化
    Status InitStack(SqStack  &S){
    	  //构造一个空栈
    	  S.base = (ElemType *)
            malloc(STACK_INIT_SIZE *sizeof(SElemType)) ;
    	  if(!S.base) exit (OVERFLOW);
    	  S.top = S.base;
    	  S.stacksize = STACK_INIT_SIZE ;
    	  return  OK;
    }//InitStack
    
    //判断栈空
    int StackEmpty(SqStack &S) {
          return(S.base==S.top);
    }
    
    //判断栈满
    int StackFull(SqStack &S) {
          return(S.top-S.base>=S.stacksize);
    }
    
    //元素入栈
    Status Push(SqStack  &S, SElemType e){
    	//插入元素e为新的栈顶元素
    	if (S.top  S.base >= S.stacksize) 
               return OVERFLOW;       //栈满
    	*S.top = e;
    	S.top++;
    	return  OK;
    }//Push
    
    //出栈
    void Pop(SqStack &S, SElemType &e)
    {
        if(S.top==s.base)
            return UNDERFLOW; // 栈空
        S.top--; 
        e=*S.top;   
        return;
    }
    
  6. 链栈

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

    //链栈的类型说明如下:
    typedef struct stacknode
    {
          SElemType data;
          struct stacknode *next;
    } stacknode,*linkstack;
    

    https://cdn.jsdelivr.net/gh/adan-ning/images/202403311339861.png

  7. 链栈的基本操作

    //置空栈
    void InitStack(linkStack &p)
    {
         p=NULL;
    }
    
    //判断栈空
    int StackEmpty(linkstack p)
    {
         return p==NULL;
    }
    
    //进栈
    void Push(linkstack  &p, SElemType e)
    { stacknode *q;
    	q=(stacknode*)malloc(sizeof(stacknode));
    	q>data=e;
    	q>next=p;
    	p=q; //设置栈顶指针
    }
    
    //退栈
    SElemType Pop(linkstack &p)
    {
    	SElemType x;    //临时变量,保存栈顶元素
    	linkstack  q=p;
    	if(p==NULL)
            error(stack underflow.);
    	p=p>next;      //调整栈顶指针
    	x=q>data;
    	free(q);             //删除栈顶元素
    	return x;
    }
    
    //取栈顶元素
    SElemType GetTop(linkstack p)
    {
    	if(p==NULL)
          	error(stack is empty.);
    	return p>data;
    }
    

栈的应用举例

根据栈的FILO特性,用作某些处理问题的工具。

数制转换

https://cdn.jsdelivr.net/gh/adan-ning/images/202403312226417.png

void conversion( ) {
InitStack(S);  //构造空栈
scanf (%d,&n);
while(n){
Push(S,n%2);
n=n/2;
  }
while(! StackEmpty(S)){
Pop(S,e);
printf(%d,e);
  }
}

括号匹配

设一个表达式中可以包含三种括号:“(”和“)”、“[”和“]”、“{”和“}”,并且这三种括号可以按照任意的次序嵌套使用,考查表达式中的括号是否匹配。例如: …[…{…[…}…]…]…[…]…(…)…)…

例:

a=b+(c-d)*(e-f));
while (m<(a[8]+t) 
{
    m=m+1;  t=t-1;
}

实现方法--利用栈进行表达式中的括号匹配

自左至右扫描表达式,若遇左括号,则将左括号入栈,若遇右括号,则将其与栈顶的左括号进行匹配,若配对,则栈顶的左括号出栈,否则出现括号不匹配错误。

迷宫问题

寻找一条从入口到出口的通路

https://cdn.jsdelivr.net/gh/adan-ning/images/202403312229579.png

  1. 迷宫的表示

    const int N=8;
    struct PosType{
           int x, y;
    };
    char maze[N][N];  //位置上的标识,是否可通过
    
  2. 迷宫初始化

    用二层嵌套循环对迷宫赋值

  3. 迷宫求解

    #define _CRT_SECURE_NO_WARNINGS 1
    
    #include <stdio.h>
    #include <stdlib.h>
    
    #define STACK_INIT_SIZE 100   //存储空间初始分配量
    #define STACKINCREMENT 10     //存储空间分配增量
    #define MAXLENGTH 25		  //迷宫最大行列值
    #define OK 1
    #define ERROR 0
    #define TRUE 1
    #define FALSE 0
    
    typedef struct {
    	int x;
    	int y;
    }PosType; //坐标
    
    typedef struct {
    	int		 ord;  //通道块在路径上的序号
    	PosType  seat; //通道块在迷宫中的坐标
    	int		 di;   //从此通道块走行下一通道块的方向
    }SElemType; //栈元素类型
    
    typedef struct Stack{
    	SElemType* base; //栈底指针
    	SElemType* top;  //栈顶指针
    	int stacksize;   //当前已分配的存储空间
    }SqStack;
    
    typedef int Status;
    typedef int MazeType[MAXLENGTH][MAXLENGTH];
    
    //声明全局变量
    int x, y; //迷宫的行列数
    int curstep = 1; //足迹路线,开始在入口处,每找到一个足迹递增1
    PosType begin; //迷宫入口坐标
    PosType end;   //迷宫出口坐标
    PosType direc[4] = { {0,1},{1,0},{0,-1},{-1,0} }; //移动方向依次位东南西北
    MazeType maze; //迷宫数组(等价于maze[MAXLENGTH][MAXLENGTH]),此时未初始化值全为0
    
    Status InitStack(SqStack* S);
    Status StackEmpty(SqStack S);
    Status Push(SqStack* S, SElemType e);
    Status Pop(SqStack* S, SElemType* e);
    Status ClearStack(SqStack* S);
    Status DestroyStack(SqStack* S);
    
    void Init();
    void Print();
    void MarkPrint(PosType e);
    Status Pass(PosType b);
    void FootPrint(PosType b);
    PosType NextPos(PosType b, int di);
    Status MazePath(PosType start, PosType end);
    
    int main()
    {
    	Init(); //初始化迷宫
    	if (MazePath(begin, end))
    	{
    		printf("此迷宫的一条路径如下\n");
    		Print();
    	}
    	else
    	{
    		Print();
    		printf("没有通道\n");
    	}
    	return 0;
    }
    
    //栈的基本操作部分
    Status InitStack(SqStack* S)
    {
    	//申请一个动态数组,为栈创建存储空间
    	S->base = (SElemType*)malloc(STACK_INIT_SIZE*sizeof(SElemType));
    	if (!S->base)
    		exit(EXIT_FAILURE);
    	S->top = S->base; //此时栈顶指针和栈底指针相同
    	S->stacksize = STACK_INIT_SIZE;
    
    	return OK;
    }
    
    Status StackEmpty(SqStack S)
    {
    	if (S.base == S.top)
    		return TRUE;
    	else
    		return FALSE;
    }
    
    Status Push(SqStack* S, SElemType e)
    {
    	if (S->top - S->base >= S->stacksize)
    	{
    		S->base = (SElemType*)realloc(S->base, (S->stacksize + STACKINCREMENT) * sizeof(SElemType));
    		if (!S->base)
    			exit(EXIT_FAILURE);
    		S->top = S->base + S->stacksize;
    		S->stacksize += STACKINCREMENT;
    	}
    	*S->top++ = e; //等同*S->top == e; S->top++;
    
    	return OK;
    }
    
    Status Pop(SqStack* S, SElemType* e)
    {
    	if (!S->base)
    
    		return ERROR;
    	else if (S->base == S->top)
    		return ERROR;
    	--(S->top);
    	*e = *(S->top);
    
    	return OK;
    }
    
    Status ClearStack(SqStack* S)
    {
    	S->base = S->top;
    
    	return OK;
    }
    
    Status DestroyStack(SqStack* S)
    {
    	S->top = NULL;
    	S->stacksize = 0;
    	free(S->base);
    
    	return OK;
    }
    
    //迷宫实现部分
    void Init()
    {
    	int i, j;
    	int x1, y1;
    	printf("请输入迷宫的行列数:\n"); 
    	scanf("%d %d", &x, &y);
    	//赋值时将最外面一圈设置为墙,所以x-1 y-1
    	for (i = 1; i < x - 1; i++) 
    		for (j = 1; j < y - 1; j++)
    			maze[i][j] = 1;
    	printf("请设置迷宫内有几个墙:\n");
    	scanf("%d", &j);
    	printf("请输入迷宫内每个墙的位置:\n");
    	for (i = 0; i < j; i++)
    	{
    		scanf("%d %d", &x1, &y1);
    		maze[x1][y1] = 0;
    	}
    	printf("迷宫结构如图\n");
    	Print();
    	printf("请输入入口的行列数\n");
    	scanf("%d %d", &begin.x, &begin.y);
    	printf("请输入出口的行列数\n");
    	scanf("%d %d", &end.x, &end.y);
    }
    
    void Print()
    {
    	int i, j;
    	for (i = 0; i < x; i++)
    	{
    		for (j = 0; j < y; j++)
    			printf("%3d", maze[i][j]);
    		puts("");
    	}
    }
    
    void MarkPrint(PosType e)
    {
    	maze[e.x][e.y] = -1;
    }
    
    Status Pass(PosType b)
    {
    	if (maze[b.x][b.y] == 1)
    		return OK;
    	else
    		return ERROR;
    }
    
    void FootPrint(PosType b)
    {
    	maze[b.x][b.y] = curstep;
    }
    
    PosType NextPos(PosType b, int di)
    {
    	b.x += direc[di].x;
    	b.y += direc[di].y;
    
    	return b;
    }
    
    //书上有参数MazeType maze,我们声明了全局变量所以不需要此参数
    Status MazePath(PosType start, PosType end)
    {
    	SqStack S;
    	PosType curpos;
    	SElemType e;
    	InitStack(&S);
    	curpos = start; //入口位置
    	//书上有curpos = 1,我们声明了全局变量所以不用此代码
    	do
    	{
    		if (Pass(curpos))//当前位置可以通过,未曾走到过的通道快
    		{
    			FootPrint(curpos); //在此位置留下足迹
    			e.ord = curstep;
    			e.seat = curpos; //位置
    			e.di = 0; //direc[0]是东所以先让e.di =0
    			Push(&S, e);
    			if (curpos.x == end.x && curpos.y == end.y)
    				return TRUE;
    			curpos = NextPos(curpos, e.di); //下一位置是当前元素的东邻
    			curstep++; //探索下一步
    		}
    		else
    		{
    			if (!StackEmpty(S))
    			{
    				Pop(&S, &e); //退栈到前一位置,改变前一位置中的方向
    				curstep--;
    				while (e.di == 3 && !StackEmpty(S)) //前一位置处于最后一个方向
    				{
    					//此循环将栈中元素退回到还有被方向没有使用的坐标处
    					MarkPrint(e.seat); //留下不能通过的标记
    					Pop(&S, &e); //退回1步
    					curstep--; //足迹减1
    				}
    				if (e.di < 3)
    				{
    					e.di++; //换下一个方向探索
    					Push(&S, e); //入栈该位置的下一方向
    					curstep++;
    					curpos = NextPos(e.seat, e.di);
    				}
    			}
    		}
    	} while (!StackEmpty(S));
    	return FALSE; 
    }
    
  4. 输出栈中的路径

    Status MazePath(maze, start, end) {
    //若迷宫中存在一条从入口start到出口end的通道,则求出这样的一条通路
      InitStack(S);  curpos = start;   curstep = 1;
      do {
           if (pass(curpos))  //当前位置可以通过
              Mark(maze,curpos); //留下记号
              e = (curstep,curpos,1);      push(S,e); //加入路径
              if (curpos==end)  return true; //到达出口
              curpos = NextPos(curpos,1) ;//下一个位置
              curstep++;
          }
         else {//当前位置不能通过
            if (!StackEmpty(S)){  pop(S,e); //退回一步
                while(e.di==4 && ! StackEmpty(S)) {//当前位置是死胡同
                     Markdead(maze,e.seat)pop(S,e); //留下记号,沿来路返回
                }
                if (e.di<4) { //当前位置还有其他方向没有探索,继续试探
                    e.di++;  push(S,e); curpos =NextPos(e.seat, e.di); 
                }
            }
      }while (!StackEmpty(S));
      return false;
    }
    

表达式求值—算符优先法

4+2×3-10/5

– 先乘除,后加减;

– 从左算到右

– 先括号内,后括号外

– 4+2×3-10/5=4+6-10/5=10-10/5=10-2=8

  1. 表达式由操作数(operand)、运算符(operator)和界限符(delimiter)组成的。
  2. 将运算符和界限符统称为算符。它们构成的集合命名为OP

表达式求值

  1. 算符优先算法:用两个工作栈。一个称作OPTR,用于存放运算符;另一个称作OPND,用以存放操作数或运算结果。
  2. 首先置操作数栈为空栈,表达式起始符“#”为运算符的栈底元素;
  3. 依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符,则和OPTR栈的栈顶运算符比较优先权后作相应操作,直到整个表达式求值完毕(OPTR栈的栈顶元素和当前读入的字符均为“#”)
OperandType EvaluateExpression() {
   InitStack(OPTR); Push(OPTR,’#’);
   InitStack(OPND); c = getchar();
   while(c != ‘#’||GetTop(OPTR)!=‘#’){
      if(!In(c,OP)) {Push(OPND,c); c=getchar();}
      else                              //不是运算符则进栈
          switch(Precede(GetTop(OPTR),c)){
              case <:              //栈顶元素优先权低
                   Push(OPTR,c); c=getchar();
                   break;
              case =:              //脱括号并接收下一字符
                   Pop(OPTR,x); c=getchar();
                   break;
          	  case >:            //退栈并将运算结果入栈
    				Pop(OPTR,theta);
    				Pop(OPND,b); Pop(OPND,a);
    				Push(OPND,Operate(a,theta,b));
    				break;
		    	} //switch
			}//while
	 	return GetTop(OPND);
	}//EvaluateExpression

栈与递归的实现

用栈结构实现程序设计语言中函数的嵌套调用和递归调用

例:

long  f(int n)
{
    if (n>1)  return n*f(n-1);
    else  return 1;
}
void  main( )
{
    int n=4;
    printf(%ld,f(n));
}

n阶Hanoi塔 问题

假设有三个分别命名为X、Y和Z的塔座,在塔座X上插有n个直径大小各不相同、依小到大编号为1,2,…,n的圆盘。现要求将X轴上的n个圆盘移至塔座Z上并仍按同样顺序叠排。

  1. 每次只能移动一个圆盘;
  2. 圆盘可以插在X、Y和Z中的任一塔座上
  3. 任何时刻都不能将一个较大的圆盘压在较小的圆盘之上。

https://cdn.jsdelivr.net/gh/adan-ning/images/202404082138019.png

https://cdn.jsdelivr.net/gh/adan-ning/images/202404082139462.png

https://cdn.jsdelivr.net/gh/adan-ning/images/202404082139636.png

https://cdn.jsdelivr.net/gh/adan-ning/images/202404082139824.png

https://cdn.jsdelivr.net/gh/adan-ning/images/202404082140449.png

https://cdn.jsdelivr.net/gh/adan-ning/images/202404082140881.png

https://cdn.jsdelivr.net/gh/adan-ning/images/202404082141769.png

队列(Queue)的定义

队列是仅限定在表尾进行插入和表头进行删除操作的线性表

  1. 术语
    1. 队头(front)--队列的表头,即只允许删除的一端。
    2. 队尾(rear) --队列的表尾,即只允许插入的一端。
    3. 入队(EnQueue) --向队尾插入元素
    4. 出队(DeQueue) --从队头删除元素

https://cdn.jsdelivr.net/gh/adan-ning/images/202404082143382.png

队列的特点

队列的修改是按先进先出的原则进行的。因此,队列称为先进先出表(FIFO)。

队列的基本运算

  1. InitQueue(&Q): 初始化队列Q
  2. QueueEmpty(): 判断队列是否为空
  3. EnQueue(e): 将元素e放入队尾
  4. DeQueue(e): 移走队头元素,由e带回该元素的值
  5. GetFront(): 获取队头元素的值,但不从队列中移走该元素
  6. Length(): 计算并返回队列中元素的个数

链队列--队列的链式存储结构

https://cdn.jsdelivr.net/gh/adan-ning/images/202404082145367.png

链队列的C语言实现

  //-----单链队列的存储结构-----
typedef struct QNode{  //链表结点类型
              QElemType data;
               struct QNode *next;
}QNode,*QueuePtr;
  
typedef struct {     //队列类型
            QueuePtr   front;    //队头指针
            QueuePtr   rear;     //队尾指针
}LinkQueue;

链队列基本操作的实现

  1. 初始化

    Status  InitQueue(LinkQueue &Q) {
    	//构造一个空队列Q
    	Q.front= Q.rear = 
                  (QueuePtr)malloc(sizeof(QNode));
    	if(!Q.front)  exit(OVERFLOW);
    	Q.front->next = NULL;
    	return  OK;
    }
    
  2. 入队

    Status  EnQueue(LinkQueue &Q, QElemType e){
    		//将元素e插入到队列Q中
    		p = (QueuePtr)malloc(sizeof(QNode));
    		if (!p) exit(OVERFLOW);
    		p->data = e;	p->next=NULL;
    		Q.rear->next = p;     Q.rear = p;
    		return  OK;
    	}
    
  3. 出队列

    Status  DeQueue(LinkQueue &Q, QElemType &e){
    	//若队列不空,则队头元素出队列,用e返回其值,
       //返回OK,否则返回ERROR
    	   if (Q.rear == Q.front)
                   return ERROR;
    	   p = Q.front -> next;
          e = p -> data;
    	  Q.front->next = p -> next;
          if (Q.rear == p)
              Q.rear = Q.front;
          free(p);
    	   return  OK;
    	}
    

循环队列--队列的顺序存储结构

https://cdn.jsdelivr.net/gh/adan-ning/images/202404082148002.png

循环队列的C语言实现

//----循环队列的存储结构----
#define   MAXSIZE   100
typedef  struct {
                QElemType  *base;
                int  front;   
		       int  rear;
   }SqQueue;

循环队列基本操作的实现

  1. 初始化

    Status InitQueue(SqQueue &Q){
    	Q.base=(QElemType *)
                     malloc(MAXSIZE*sizeof(QElemType));
    	if (!Q.base) exit (OVERFLOW);
    	Q.front = Q.rear = 0;
    	return  OK;
    }
    
  2. 入队

    Status EnQueue(SqQueue &Q,QElemType e) {
    	//将元素e插入队列Q的队尾
    	if ((Q.rear+1) % MAXSIZE == Q.front) return ERROR;
    	Q.base[Q.rear] = e;
    	Q.rear = (Q.rear+1) % MAXSIZE;
    	return OK;
    }
    
  3. 出队

    Status DeQueue(SqQueue &Q,QElemType &e) {
    	//删除队列Q的队头元素并用e带回
    	if (Q.front == Q.rear) return ERROR;
    	e = Q.base[Q.front];
    	Q.front = (Q.front+1) % MAXSIZE;
    	return OK;
    }
    

双端队列

  1. 双端队列

    https://cdn.jsdelivr.net/gh/adan-ning/images/202404082151989.png

  2. 输出受限的双端队列

    https://cdn.jsdelivr.net/gh/adan-ning/images/202404082151296.png

  3. 输入受限的双端队列

    https://cdn.jsdelivr.net/gh/adan-ning/images/202404082151005.png

优先队列

  1. 在许多情况下,简单的队列结构是不够的,需要使用某些优先规则来完善先入先出机制
  2. 优先队列的问题是如何找到一种实现优先的方法,使得入队和出队列操作得以相对容易实现。
  3. 优先队列可以通过两种修正的链表结构来实现。
    1. 一种结构是元素仍然依次进入(即加入元素时,时间复杂度为O(1)),而取出元素时则需遍历队列(即出队时的时间复杂度为O(n)),
    2. 另一种是根据元素的优先级决定其插入的位置(即入队时的时间复杂度为O(n),出队时的时间复杂度为O(1))

队列的应用

同栈一样,队列也是一种应用广泛的线性表,在日常生活和计算机科学中很常见:

离散事件模拟

排队问题

作业控制

广度优先搜索