【数据结构与算法】栈与队列

栈与队列

什么是栈?

  1. 栈(stack)是限定仅在表尾进行插入和删除操作的线性表,又被称为后进先出的线性表,简称LIFO结构。

  2. 栈是一个线性表,具有线性关系,我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)。

  3. 栈的插入操作叫作进栈,栈的删除操作叫作出栈,如下动图所示:

在这里插入图片描述

栈的顺序存储结构及实现

栈的结构体定义

typedef int SElemType
typedef struct Stack
{
    
    
    SElemType data[MAXSIZE];
    int top; //栈顶指针
}Stack;

进栈操作

//进栈
bool StackPush(Stack* s, SElemType x)
{
    
    
	if (s->size == MAXSIZE - 1) 
	{
    
    
		printf("栈满!");
		return false;
	}

	s->size++;
	s->data[s->size] = x;
	printf("进栈成功!");
}

出栈操作

//出栈
bool StackPop(Stack* s)
{
    
    
	if (s->size == -1)
	{
    
    
		printf("栈空!");
		return false;
	}
	else
	{
    
    
		s->size--;
	}
}

两栈共享空间

分析

当我们只有一个栈时,必须事先确定储存空间,但如果储存数据过多就会造成溢出,这时,我们可以用两个相同类型的栈,并各自开辟一段数组空间,当一个栈溢出时,溢出的数据储存到另一个栈,但这容易造成一栈满,一栈空的情况。那么,为了充分为调配好空间,我们可以选择用一个数组存储两个栈来解决这一问题。
如下图,top1和top2分别表示栈1和栈2的栈顶,数组长度size=6:

在这里插入图片描述

为了方便理解,这里的数字我用的是数组的下标,数组的两端用两个栈的栈底来表示,从栈1的栈底(0)开始,到栈2的栈底(size-1)结束。但数据进栈时,并不是严格的从左到右进行填充,当向栈2中填充数据时,是从右向左延伸的,即先是5进栈,再有4进栈。

现在我们来讨论两栈的中空栈与满栈的情况:
进行讨论前,我们要记住,top1+1==top2为栈满这个结论是始终适用的

  1. 满栈

    这种情况又可以对栈1和栈2的状态进行细分:

    1. 栈1和栈2都不为空栈

      在这里插入图片描述

      注意,top1和top2不一定在中间相遇才是满栈,只要top1和top2相遇就是满栈

    2. 栈1为空栈==(top1=-1)或者栈2为空栈(top2=size)==

      在这里插入图片描述

      上图是栈1为空的情况
      在这里插入图片描述

      上图是栈2为空的情况

    综上分析 :当满栈时,栈1和栈2可能都不为空栈,但也可能其中一个为空栈,但都满足满栈的结论 top1+1==top2为栈满

  2. 空栈

    当栈1和栈2都为空时,即为空栈

    在这里插入图片描述

下面进行代码的演示

两栈共享空间的结构体定义

typedef int SElemType;
typedef struct DoubleStack
{
    
    
	SElemType data[MAXSIZE];
	int top1;	/*栈1的栈顶指针*/
	int top2;	/*栈2的栈顶指针*/
}DoubleStack;

进栈

//进栈
bool DoubleStackPush(DoubleStack* s, SElemType x, int stackNumber)
{
    
    
	if (s->top1 + 1 == s->top2)  //满栈
	{
    
    
		printf("栈满!");
		return false;
	}
		
	if (stackNumber == 1)  //栈1有元素进栈
		s->data[++s->top1] = x;
	else if (stackNumber == 2)	//栈2有元素进栈
		s->data[--s->top2] = x;

	printf("进栈完成!");
}

出栈

//出栈
bool DoubleStackPop(DoubleStack* s, int stackNumber)
{
    
    
	if (stackNumber == 1)
	{
    
    
		if (s->top1 = -1)
			return false;
		else
			s->top1--;
	}
	else if (stackNumber == 2)
	{
    
    
		if (s->top2 == MAXSIZE)
			return false;
		else
			s->top2++;
	}
}

栈的链式存储结构及实现

栈的链式存储结构

  1. 链栈中有栈顶,不需要头结点。
  2. 不存在满栈的情况。
  3. 空栈也就是top指向NULL的时候。

链栈的结构体

typedef struct StackNode
{
    
    
    SElemType data;
    struct StackNode* next;
}StackNode,*LinkStackPtr;

typedef struct LinkStack
{
    
    
    LinkStackPtr top;	//栈顶指针
    int count;			//计数
}LinkStack;

进栈

  1. 开辟新结点

    LinkStackPtr newNode=(LinkStackPtr)malloc(sizeof(StackNode));
    newNode->data=x;
    
  2. 新结点链接到栈中

    newNode->next=S->top;
    
  3. 将新结点赋值给栈顶指针

    S->top=newNode;
    S->count++;
    

完整代码:

bool  StackPush(LinkStack *S, SElemType x)
{
    
    
    LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
    newNode->data=e;
    newNode->next=S->top;
    S->top=newNode;
    S->count++;
    return true;
}

出栈

bool StackPop(LinkStack *S)
{
    
    
    LinkStackPtr p;
    if(S->top==NULL)
        return false;
    else
    {
    
    
        newtop=S->p;//将栈顶结点赋值给p
        S->top=S->top->next;//栈顶指针后移一位
        free(p);
        S->count--;
        return true;
    }
}

栈的作用

简化程序设计问题,使我们的注意力聚焦于要解决问题的核心。

应用(递归)

斐波那契数列

假设一开始有一对具备繁殖能力的兔子,具有繁殖能力的兔子每个月能生出一对小兔子,且小兔子出生两个月后就具有繁殖能力,假设所有兔子都不死,那么n年后可以得到多少只兔子呢?

依次类推我们得到如下表格

经过时间(月) 1 2 3 4 5 6 7 8 9
兔子对数 1 1 2 3 5 8 13 21 34

用数学公式概括:

f ( x ) = { 0 , 当 n = 0 1 , 当 n = 1 F ( n − 1 ) + F ( n − 2 ) , 当 n > 1 f(x)=\left\{ \begin{aligned} 0 &,&当n=0\\ 1 & , & 当n=1 \\ F(n-1) & +F(n-2), & 当n>1 \end{aligned} \right. f(x)=01F(n1),,+F(n2),n=0n=1n>1

求经过k个月后兔子的对数代码实现

递归的终止条件非常重要,给了终止条件,计算机才能进行求解子问题并回溯,最终求出f(x)

int Fbi(int n)
{
    
    
    if(n<2)
        return n==0 ? 0 : 1; //若输入的n=0,返回0,否则当通过递归得到n=1时,返回1
    
    return Fbi(n-1)+Fbi(n-2);//不断调用自己,直到i=1
}
int main()
{
    
    
    int k,total;
    scanf("%d",&k);
    printf("第%d个月后兔子的总对数为:%d对\n",k,Fbi(k));
}

对于递归的题目,不要先去想递归的过程,而是去找出对应的公式,当公式出来后,递归的逻辑才会清晰起来。

汉诺塔问题

汉诺塔(Tower of Hanoi)源于印度传说中,大梵天创造世界时造了三根金钢石柱子,其中一根柱子自底向上叠着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

为了方便后续的理解,我们先分析两个盘子的情况:

在这里插入图片描述

要将A上的盘子移动到C上,一共需要3步,过程如下

在这里插入图片描述

下面我们以5个盘子为例进行分析:

在这里插入图片描述

现在我们5个盘子从A移动到为C,移动过程中不能出现大的在上小的在下,那么最少需要多少步才能完成?

分析:

如果我们事先把n-1个盘看成一个整体,那么相当于两个盘,只需要进行三步就可以达到目的。

在这里插入图片描述

但很可惜,这个整体并不只有一个盘子,这个整体里面每个盘子的移动可能是不同的,我们现在也无法确定,那么我们可以暂时将这个整体看作一个未知数,这里我们用F(4)表示,于是我们分为以下步骤:

  1. 将这n-1个盘子借助借助C先移动到B上(忽略转移过程)

    hanoi(n-1,'A','C','B');		//A上的n-1个盘借助C先移动到B上
    

在这里插入图片描述

  1. 然后把A上剩下的一个盘移到C上

    move('A','C');
    

在这里插入图片描述

  1. 把B上的n-1(F(4))个盘借助A移动到C上

    hanoi(n-1,'B','A','C');
    

    但现在B上的F(4),仍然是未知数,因此我们需要进一步把这4个盘看成两个盘,借助A移到C上,如下图,我们把这三个盘组成的整体称为F(3)

在这里插入图片描述

1. 将F(3)转移到A上(不要去想转移过程)

在这里插入图片描述

2. 然后把B上剩余的盘子转移到C上

在这里插入图片描述

3. 将F(3)移动到C上

但现在F(3)里面的每个盘的移动过程仍然是不确定的,我们又需要分为F(2)加一个底盘,这样不断将问题细分,直到得到F(1),也就是n=1的时候,我们终于找到了终止条件,因为最后一个盘的移动是确定的,不再是未知数,那么既然F(1)已经确定了,那么F(2)也就确定了,因为其中一个盘的移动确定,另一个盘的移动也是确定的,F(2)确定,F(3)也就确定了,就这样,通过F(1)回溯,前面的未知数都能确定下来了,也就是说每个盘子的移动过程也就确定下来了。

总结:递归就是不断将问题细分为一个个子问题,也就是如果我们要求解F(n),那就先求解F(n-1),要求解F(n-1),那我先求F(n-2),······,当细分的子问题达到我的终止条件后,就可以通过回溯求解出前面的大问题,最初我们不必纠结于每一个盘子的移动过程,这些过程最终会通过递归进行解决。

汉诺塔问题代码实现
#include<stdio.h>

int total; //计数

void move(char A, char C)
{
    
    
	printf("step %d:%c --> %c\n", ++total, A, C);
}

void hanoi(int n, char A, char B, char C)//n个盘子在A上,借助B,移动到C上
{
    
    
	if (n == 1)//递归终止条件,如果A上只有一个盘子,直接移动到C上
		move(A, C);
	else
	{
    
    
		hanoi(n - 1, A, C, B);//A上的n-1个盘子借助C移动到B上
		move(A, C);			//将A上最后一个盘子移动到C上
		hanoi(n - 1, B, A, C);//将B上的n-1个盘子借助A移动到C上
	}
}

void main()
{
    
    
	int n;

	printf("input the number of diskes:");
	scanf_s("%d", &n);
	printf("The step to moving %d diskes:\n", n);

	hanoi(n, 'A', 'B', 'C');
}

队列

什么是队列?

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表,也就是说是一种先进先出的线性表,简称FIFO。允许插入的一 端称为队尾,允许删除的一端称为队头。

在这里插入图片描述

循环队列

队列顺序储存的不足

队列和栈不一样,栈只在栈顶进行插入和删除操作,不需要挪动元素,因此时间复杂度始终为O(1),虽然进队列操作时间复杂度也为O(1),但出队列时,为了保证队头不为空,后面的元素都要向前挪动一位,时间复杂度为O(n)

那么如果我们队头的位置用指针front来记录,这时候就不一定是下标为0的位置,同时用rear记录队尾元素的后一位,当front等于rear是,就说明队列为空。

在这里插入图片描述

如果我现在继续让新元素入队,当rear移到数组之外时,就会发生溢出

在这里插入图片描述

但现在数组实际存储的元素只有三个,并未达到数组的存储上限,下标为0和1的位置还是空的,为了解决这一情况,引入了循环队列。

什么是循环队列

把队列头尾相接的顺序存储结构

当我们继续入队a6,a6会放在下标为0的位置

在这里插入图片描述

但如果继续入队a7,rear就会等于front,但在前面我们认为rear等于front时,队列为空,为了避免这种情况,我们一般会保留一个空位,这个空位不填充元素

下面我们来设计循环队列的程序:

  1. 满列条件,队列最大长度记为QueueSize

    满列的情况有两种:

    1. rear<front

      在这里插入图片描述

      此时满足 rear+1=front

    2. rear>front

      在这里插入图片描述

      此时满足 (rear+1)% QueueSize=front

    由于以上两种情况均符合第二种判断条件,故将(rear+1)% QueueSize=front作为判断队列满的条件。

  2. 队列长度计算公式(rear-front+QueueSize)% QueueSize

下面我们开始实现代码

循环队列代码实现

结构体
#define MAXSIZE 6
typedef int QElemType;

typedef struct Queue
{
    
    
	QElemType data[MAXSIZE];
	int front;	//头指针,记录队头的下标
	int rear;	//尾指针,记录队尾下一个元素位置的下标
}Queue;
初始化
void InitQueue(Queue* Q)
{
    
    
    Q->front=0;	//将头指针记录的下标初始化为0
    Q->rear=0;	//将尾指针记录的下标初始化为0
}
求队列长度
int QueueLength(Queue Q)
{
    
    
    return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
入队列
void EnterQueue(Queue *Q,QElemType x)
{
    
    
    if(Q->front==Q->rear)
        return 0;
    
    Q->data[Q->rear]=x;				//将x赋值给下标为rear的元素
    Q->rear=(Q->rear+1)%MAXSIZE;	//rear后移一位,若超出数组,则转到数组头部
}
出队列
void DeleteQueue(Queue *Q)
{
    
    
    if(Q->front==Q->rear)
        return 0;
    
    Q->front=(Q->front+1)%MAXSIZE;//front后移一位,若超出数组,则转到数组头部
}

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

队列的链式结构和单链表一样,但只能在表尾进表头出,简称为链队列。

链队列的代码实现

结构体创建
typedef int QElemType;

typedef struct QueueNode
{
    
    
	QElemType data;
	struct QueueNode* next;
}QueueNode,*QueuePtr;

typedef struct
{
    
    
	QueuePtr front, rear;//front是指向头结点的指针,不是队头!队头是头结点的后一位。
}LinkQueue;
初始化
void QueueInit(LinkQueue *Q)
{
    
    
    Q->front=Q->rear=(QueuePtr)malloc(sizeof(QueueNode));
    Q->front->next=NULL;
    Q->rear->next=NULL;
}
入队
void EnQueue(LinkQueue* Q, QElemType x)
{
    
    
    QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));

    newnode->data = x;  //将数据赋给新结点
    newnode->next = NULL;
    Q->rear->next = newnode;    //将新结点链接到队尾结点之后
    Q->rear = newnode;    //队尾指针后移一位
}
出队
void DeQueue(LinkQueue* Q)
{
    
    
    if (Q->front == Q->rear) //队列为空
        return Q;

    QueueNode* p = Q->front->next;    //保存待删除结点
    Q->front->next = p->next;  //头结点与队头结点的后一位链接
    if (Q->rear == p)   // 若队头就是队尾,则删除后将rear指向头结点
        Q->rear = Q->front;
    free(p);
}
打印队列
void PrintQueue(LinkQueue Q)
{
    
    
    QueueNode* cur = Q.front->next;//保存队头结点,准备打印
    
    while (cur)
    {
    
    
        printf("%d-->", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}
链队列完整代码
#include<stdio.h>
#include<stdlib.h>

typedef int QElemType;

typedef struct QueueNode
{
    
    
	QElemType data;
	struct QueueNode* next;
}QueueNode,*QueuePtr;

typedef struct
{
    
    
	QueuePtr front, rear;
}LinkQueue;

//初始化
void QueueInit(LinkQueue* Q)
{
    
    
    Q->front = Q->rear = (QueuePtr)malloc(sizeof(QueueNode));
    Q->front->next = NULL;
    Q->rear->next = NULL;
}

//入队
void EnQueue(LinkQueue* Q, QElemType x)
{
    
    
    QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));

    newnode->data = x;  //将数据赋给新结点
    newnode->next = NULL;
    Q->rear->next = newnode;    //将新结点链接到队尾结点之后
    Q->rear = newnode;    //队尾指针后移一位
}

//出队
void DeQueue(LinkQueue* Q)
{
    
    
    if (Q->front == Q->rear) //队列为空
        return Q;

    QueueNode* p = Q->front->next;    //保存待删除结点
    Q->front->next = p->next;  //头结点与队头结点的后一位链接
    if (Q->rear == p)   // 若队头就是队尾,则删除后将rear指向头结点
        Q->rear = Q->front;
    free(p);
}

//打印队列
void PrintQueue(LinkQueue Q)
{
    
    
    QueueNode* cur = Q.front->next;//保存队头结点,准备打印
    
    while (cur)
    {
    
    
        printf("%d-->", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}

//测试
void test1()
{
    
    
	LinkQueue Queue;
	QueueInit(&Queue);
	EnQueue(&Queue, 1);
	EnQueue(&Queue, 2);
	EnQueue(&Queue, 3);
	EnQueue(&Queue, 4);

	PrintQueue(Queue);

	DeQueue(&Queue);
	DeQueue(&Queue);
	PrintQueue(Queue);
}
int main()
{
    
    
	test1();
	return 0;
}

猜你喜欢

转载自blog.csdn.net/watermelon_c/article/details/122083126