数据结构(七)---C语言实现栈和队列

前言

栈和队列都是数据结构中的链式结构,我们学习过的链式结构还包括顺序表和链表 。

链式结构:在逻辑上是数据依次相连的状态

顺序表和链表可以实现任意位置的数据的插入和删除,但是栈和队列不同:

栈:只允许在线性表的一端进行插入和删除。我们通常将第一个插入的数据称为栈底,数据的插入和删除都是在操作栈顶元素。

队列:只允许在线性表的一端进行插入,另一端进行删除。通常我们将插入的第一个数据称为队列头。新的数据添加在队尾,每一次删除的是队列头的元素。

队列和栈存放数据的方式很相似:

在这里插入图片描述

这两个数据结构的区别是删除数据的方式:

在这里插入图片描述

下面将详细的介绍栈和队列

入栈和出栈的顺序

在栈结构中,出栈的顺序不一定就和入栈顺序完全相反。

例如:我们向栈中先压入数据**1、2、3**,然后**将这三个数据弹出**,
	 再向栈中压入数据**4、5、6**,再将这**三个数据弹出**。
此时我们入栈数据的顺序是:**1、2、3、4、5、6**,而我们出栈的顺序是**3、2、1、6、5、4**。

栈的实现(数组栈)

栈的实现中需要十分注意栈顶位置的查找,如果我们可以高效的找到栈顶的位置,那么我们就可以快捷的实现压栈函数和出栈函数。

栈有两种实现的方式:

  1. 用数组来实现:可以依靠数组元素的下标快速的找到栈顶的位置,入栈和出栈的时间复杂度是O(1)
    数组栈中会在结构中创建一个数据,用来保存栈顶元素的下标。

  2. 用链表来实现:将最后一个插入的节点作为栈顶(头指针指向栈顶),也可以很方便的找到栈顶的位置,入栈和出栈的时间复杂度也是O(1)

在这里插入图片描述

这篇文章将介绍数组栈的实现:

静态数组栈

数组栈又分为静态和动态两种。

	静态数组栈确定了一个栈中可存放元素的个数。
常见的例子就是停车场,停车场的大小是固定的,所以只能停放一定数量的车;
静态数组栈也一样,只能存储一定数量的数据,当数据满的时候就判断满栈
静态数组栈的结构声明
#define MAX 5//栈的空间大小

typedef int STDataType;

typedef struct Stack{
    STDataType data[MAX];
    int top;//栈顶的下一个元素的下标
}Stack;

栈结构中的top到底是栈顶的下标还是栈顶后一个元素的下标?

结果是:不同的初始化方式会导致top 表示不同的意思。

情况1:

初始化top为0,此时top表示的是栈顶后一个元素的下标。

在这里插入图片描述

情况2:

初始化top为-1,此时top 表示的是栈顶元素的下标

在这里插入图片描述

满栈的判断

这里我们初始化top0,所以当top的值等于MAX的时候久代表栈已经满了。

bool StackFull(Stack*ps)
{
    return ps->top == MAX;//初始化top为0。
  //  return ps->top == MAX-1;//初始化top为-1。
}

动态数组栈

动态数组栈相较于静态栈的优点是,动态数据栈不会出现满栈的情况。

动态数组栈的结构声明:

动态数组栈中也是用一个top变量来指向栈顶。
typedef int STDataType ;
 
typedef struct Stack{
    STDataType *data;
    int top;//这篇博客中会初始化为0;
    int capacity;//栈的容量,当栈满的时候可以扩容
};

需要实现的函数接口:

栈的初始化

函数的声明:

void StackInit(Stack *ps);

在初始化栈的时候,我们可以先给这个栈指定一个空间,当然也可以将这个栈指针赋为空指针(等到需要添加数据的时候再申请空间)。

//这里我们在初始化栈的时候将栈赋为空栈
void StackInit(Stack *ps)
{
    assert(ps);//保证ps指针是合法的
    
    ps->data = NULL;
    ps->top = ps->capacity = 0;
}

注意:这里我们将top初识化为0, 就代表我们设置top为栈顶的下一个元素的下标

在这里插入图片描述

向栈中压入一个数据

函数的声明:

void StackPush(Stack *ps);

由于这是一个动态栈,所以我们在添加数据之前需要判断这个栈是否有足够的空间来存放数据,即我们需要判断:

1. 这个栈的容量是否为0(第一次添加元素的时候栈的容量就有可能为0)。

2. 是否已经满栈了,如果栈满了,我们就需要扩容。
void StackPush(Stack *ps,STDataType x)
{
	assert(ps);
	
	if(ps->top == ps->capacity)//表明没有多余的空间,需要增大容量
    {
        int newCapacity = ps->capacity == 0 ? 4: ps->capacity*2;
        STDataType *tmp = (STDataType *)realloc(ps->data,sizeof(STDataType)*newCapacity);
        if(tmp == NULL)
        {
            printf("fail :realloc");
            exit(-1);
        }
        
        ps->data = tmp;
        ps->capacity = newCapcity;
    }
    
    ps->data[ps->top] = x;
    ps->top++;
    
}

在这里插入图片描述

从栈中弹出一个数据

函数的声明:

void StackPop(Stack *ps);

对于数组栈,弹出数据的时候我们只需要将top减1,这样就代表了栈顶的数据被弹出了。

需要注意:如果栈是空栈就不需要再弹出了。

void StackPop(Stack *ps)
{
    assert(ps);
    if(ps->top>0)//判断空栈
    {
        ps->top--;
    }
}

在这里插入图片描述

得到栈顶的元素

函数的声明:

STDataType  StackTop(Stack *ps);
//该函数的返回值是对应存储数据的类型

和删除栈顶数据的函数一样,这个函数也只有在栈不为空的时候才会实现

STDataType StackTop(Stack *ps)
{
    assert(ps);
    if(ps->top>0)
    {
        return ps->data[ps->top-1];
    }
}

注意:由于top表示的是栈顶元素的下一个元素的下标,所以如果我们要得到栈顶的元素的时候,我们需要得到数组中下标为top-1的数据。

计算栈的大小
size_t StackSize(Stack *ps);

这个函数的结果就是栈结构中的top的值:

size_t StackSize(Stack *ps)
{
    assert(ps);
    return ps->top;
}

在这里插入图片描述

判断空栈

函数的声明:

bool StackEmpty(Stack *ps);
当栈中没有元素的时候就是空栈,此时top等于0,所以我们可以将top和0的比较结果作为返回值。

如果top == 0,就返回真;如果top !=0 ,就返回假。
bool StackEmpty(Stack *ps)
{
    assert(ps);
    return ps->top == 0;
}
销毁整个栈

函数声明:

void StackDestroy(Stack *ps);

栈结构中有一个动态数组,为了防止内存泄漏,我们需要在不使用栈的时候释放掉这个申请来的空间。

void StackDestroy(Stack *ps)
{
    assert(ps);
    if(ps->capacity>0);
    {
        free(ps->data);//释放动态申请来的空间。
    }
    free(ps);
    ps->top = ps->capacity = 0;
}

队列

队列的结构特点是先进先出(FIFO),生活中一个比较形象的例子就是放羽毛球的球筒,我们从一端将羽毛球放进去,从另一端将球取出,取出球的顺序和放入球的顺序一致。

出队列的顺序:

出队列的顺序是由入队列的顺序决定的,和什么时候出队列无关(每次只能从队列头删除一个数据)。

队列的结构

队列也可以有多种实现方式,但是由于队列的特点,进行一端的插入和另一端的删除,我们一般会使用链式结构来实现队列。

数组结构和链式结构实现队列的区别:

	数组结构:
可以很方便的进行一端的数据插入,但是如果要删除另一端的数据,就需要挪动这个数组中的数据,时间复杂度较高。

	链式结构:
可以用两个指针,一个指针指向队列头,一个指针指向队列尾,这样就可以在时间复杂度为**O(1)**的情况下完成一端
的数据插入和另一端的数据删除。

因此我们会使用链表来实现队列

typedef int QDataType;

typedef struct QueueNode{//每一个节点的结构
    QDataType data;
    struct QueueNode *next;
}QueueNode;

typedef struct Queue{//队列链表中有两个指针,一个指针指向队列头,一个指针指向队列尾。
    QueueNode*head;
    QueueNode*tail;
}Queue;

在这里插入图片描述

需要实现的函数接口;

队列的实现

初始化队列:

void QueueInit(Queue* pq)
{
	assert(pq);
	pq->tail = pq->head = NULL;
}

当队列中head和tail都时空指针时,说明队列中没有任何的元素,此时队列是一个空队列,我们可以在初始化的时候将队列初始化为空队列,等需要先队列添加元素的时候再改变队列中的两个指针。

在这里插入图片描述

在队列尾添加元素

向队列添加元素的时候,需要注意的是:添加第一个数据和其他数据的操作不同,

原因:在空队列的时候,head指针指向NULL,在添加第一个元素的时候,我们需要让head指针和tail指针指向第一个元素,

但是在添加后续数据的时候,head指针保持不变,让tail指针向后移动。

void QueuePush(Queue* pq,QDataType x)
{
	assert(pq);
	QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
	assert(newnode);
	newnode->data = x;
	newnode->next = NULL;
	if (pq->tail == NULL)
	{
		pq->head = newnode;
		pq->tail = newnode;
	}
	else
	{
		pq->tail->next = newnode;
		pq->tail = newnode;
	}
}

在这里插入图片描述

删除队列头的元素

我们通过改变头指针的指向达到删除队列头的效果:让头指针指向队列头的下一个节点,就代表删除了队列头。

在这里插入图片描述

但是还需要考虑到删除的是队列中的最后一个数据:
当队列中只剩下最后一个数据的时候,head指针和tail指针指向的是同一个空间,
我们在删除操作的时候释放掉了这块空间,这样就会出现两个情况:tail指针成为野指针,head指针指向NULL.

为了避免这种情况,我们需要将删除函数分为删除最后一个数据和删除其他数据。
利用head->next == NULL来判断是否是最后一个数据,
当删除最后一个数据的时候,就直接将head和tail指向的空间释放掉(选择释放head或者是释放tail,但是不能释放两次),
然后将这两个指针都赋为空指针
void QueuePop(Queue* pq)
{
	assert(pq);
	assert(pq->tail);

	if (pq->head->next == NULL)
	{
		free(pq->head);//此时head和tail指向的是同一个空间,只需要释放一次就行了
		pq->tail = pq->head = NULL;
	}
	else
	{
		QueueNode* next = pq->head->next;
		free(pq->head);
		pq->head = next;
	}
}

判断是否是空队列

当head指针和tail指着都为NULL的时候,表示还没有向队列中添加过元素,或者已经将队列中的数据全部删除。
此时的队列就是一个空队列。
bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->head == NULL && pq->tail == NULL;
}//将两个指针和空指针的比较结果作为返回值,
//如果两个指针都是空指针,就会返回真,表明这是一个空队列。

计算队列中的元素个数

我们可以遍历这个队列得到队列中的数据个数。

注意:如果要计算这个队列中元素的个数,我们就需要保证这不是一个空队列,所以在函数开始就需要检查两个指针是否为空指针。

size_t QueueSize(Queue* pq)
{
	assert(pq);
	assert(pq->tail);//保证这不是一个空队列
	size_t size = 0;
	QueueNode* cur = pq->head;
	while (cur)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

得到队列头和链表尾的元素

同样,如果要取得队列头和尾的数据,我们需要保证这个队列不是一个空队列。

//得到队列头的数据
QDataType QueueFront(Queue* pq)
{
	assert(pq);
	assert(pq->head);//保证这不是一个空队列

	return pq->head->data;
}

//得到队列尾的数据
QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(pq->tail);//保证这不是一个空队列
	return pq->tail->data;
}

在这里插入图片描述

销毁整个队列:

销毁整个队列的时候我们需要先遍历这个队列,把这个队列中每一个节点所占据的空间依次释放,然后再释放掉这个动态申请出来的队列结构。

void QueueDestroy(Queue* pq)
{
	assert(pq);
	QueueNode* cur = pq->head;//找到队列中的第一个数据
	while (cur)
	{
		QueueNode* next = cur->next;//遍历这个队列,并且依次释放申请来的空间。
		free(cur);
		cur = next;
	}

	free(pq->tail);
	free(pq->head);
}

总结

  1. 队列和栈都是线性表类型的数据结构,两个结构都可以从一端存入数据,区别是栈结构只能从存入数据的一端取出数据,而队列结构只能从出入数据的另一端取出数据。
  2. 队列和栈都可以用数组和链表来实现。不管是哪种实现方式,都应该尽可能高效的找到几个特殊的位置(栈顶、队列头、队列尾)。
  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值