栈和队列的实现

目录

栈和队列的简要介绍

一、栈

1、栈的初始化

2、入栈

3、出栈

4、获取栈顶元素

5、获取栈中元素个数

6、判断栈是否为空

7、销毁栈

二、队列

1、初始化队列

2、队尾入元素

3、队头出元素

4、获取队头元素

5、获取队尾元素

6、获取队列元素个数

7、判断队列是否为空

8、销毁队列


栈和队列的简要介绍

栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端 称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。

出栈:栈的删除操作叫做出栈。出数据也在栈顶

栈可以用链表实现也可以用顺序表实现,但是压栈和出栈都是对尾部的元素进行操作,即尾插尾删,顺序表的尾插尾删的时间复杂度是O(1),但是对于链表而言,头插头删是O(1),尾插尾删是O(N),所以这里实现栈用顺序表!!

 1、栈的初始化

因为栈是用来存放数据的,所以有存放数据的一块空间设为 _a, 栈的容量_capacity  栈的数据个数(也是栈顶)_top三个属性,定义一个结构体包括这三个属性,就是一个栈的类型了

typedef int STDataType;
typedef struct Stack
{
	STDataType* _a;
	int _top;		// 栈顶
	int _capacity;  // 容量 
}Stack;

定义好栈的结构后,就要先初识化一下栈,让指向存储数据的空间的指针_a为空,容量和数据个数都为0,完成栈的初始化;(再后面数据入栈的时候再来开辟空间,动态改变数据个数和容量)

// 初始化栈 
void StackInit(Stack* ps)
{
	assert(ps);
	ps->_a = NULL;
	ps->_capacity = 0;
	ps->_top = 0;

}

2、入栈

初识化好栈后,就可以把元素压到栈里了,因为栈是一个简化版的顺序表,从下标为零开始插入元素,可以理解为刚插入的元素就是在顺序表的最后边(把顺序表逆时针竖过来 理解成栈的顶),然后元素个数自增1,当元素个数等于栈的容量时就要进行扩容了,这里是两倍两倍地扩容,以此来实现栈。

实现的代码非常简单,只有短短几行。

// 入栈 
void StackPush(Stack* ps, STDataType data)
{
	assert(ps);
	CheckCapacity(ps);
	ps->_a[ps->_top] = data;
	ps->_top++;
}

入栈就是增加元素,每次增加一个元素,_top自加1,增加的元素多了就会使栈的容量不足,这是就要进行扩容了,这里写了一个checkcapacity的函数来检查是否需要扩容,需要则该函数会自动完成扩容。

//检查栈的容量是否满了
void CheckCapacity(Stack* ps)
{
	assert(ps);
	if (ps->_top == ps->_capacity)
	{
		STDataType newtop = ps->_capacity == 0 ? 4 : ps->_capacity * 2;
		STDataType* newStack = (STDataType*)realloc(ps->_a, sizeof(STDataType)*newtop);//这里要注意的是这里的*2总是写错写到sizeof里面 是要写在外面的
		if (newStack == NULL)
		{
			printf("realoc fail!\n");
			exit(-1);
		}
		ps->_a = newStack;

		ps->_capacity = newtop;
	}
	

}

思想:如果栈的容量和元素个数相等了就要进行扩容,因为初始化的时候是设置的容量和元素个数都为0,_a为空,那么扩容是就要判断容量是否为0,如果为0就先开辟4个元素的空间供栈使用,否则就按照原来容量的两倍进行扩容。扩容失败就结束程序,成功就把开辟的空间的地址赋值给原来指向存储数据空间的指针_a,扩容完成后栈的容量变为原来二倍。

3、出栈

实现了入栈就要实现出栈,既然压栈和出栈是类似往弹夹里压子弹出子弹,那么出栈就是把最上面的元素拿出来即可,上面说了栈是一个简单的顺序表,入栈是把元素依次往后放,最后边的元素就是栈顶的元素,我们出栈就是拿栈顶的元素出来,也就是把顺序表最后边的元素拿出来(对应下标为顺序表(或栈 都是一样的 栈就是阉割了的顺序表)的元素个数减1),然后让栈的元素个数自减即可完成出栈操作。

这里是直接让栈的元素个数自减1,这样就让出栈的元素的下面一个元素就是新的栈顶元素了

注意:这里出栈的条件必须是栈的元素个数大于零!!!

// 出栈 
void StackPop(Stack* ps)
{
	assert(ps);
	assert(ps->_top > 0);//只有是大于0才能进行pop
	ps->_top--;


}

4、获取栈顶元素

类似于出栈的逻辑,取出栈顶的元素,但并不是拿出栈,所以不用让栈的元素个数自减。只要返回栈中_a指向的这块空间下标为元素个数减一的元素即可。

// 获取栈顶元素 
STDataType StackTop(Stack* ps)
{
	assert(ps);
	STDataType ret = ps->_a[ps->_top - 1];
	return ret;
}

5、获取栈中元素个数

这个很简单,我们是入一个元素就让(表示栈的元素个数的变量)_top自增1,所以要得到栈的元素个数就返回栈里的_top即可。

// 获取栈顶元素 
STDataType StackTop(Stack* ps)
{
	assert(ps);
	STDataType ret = ps->_a[ps->_top - 1];
	return ret;
}

6、判断栈是否为空

直接返回元素个数是否等于0即可

bool StackEmpty(Stack* ps)
{
	assert(ps);
	return ps->_top == 0;
}

7、栈的销毁

我们把元素入栈是存放在_a指向的空间中的,入栈的过程中可能有多次扩容,我们在销毁栈的时候就把这块空间释放掉,把指向这块空间的指针_a置空防止出现野指针,然后让元素个数和容量都为空即可。

// 销毁栈 
void StackDestroy(Stack* ps)
{
	assert(ps);
	free(ps->_a);
	ps->_a = NULL;
	ps->_capacity = 0;
	ps->_top = 0;
}

二、队列

队列的特性与栈恰恰相反,栈是后入先出,队列是先入先出。

如同排队一样,先来的先进行,后来的后进行;

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出 FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头

 队列的实现线性表和链表都可以,那么到底那种更好呢?

分析一下,根据队列的特性,是先进先出,进是尾插,出是头删,链表的尾插效率低头删效率高,线性表尾插效率高,头删效率低。但是我们可以通过链表结构设一个头和尾,插入数据就让尾移动到队尾更新尾的位置,这样就可以弥补链表尾插效率低的缺陷了。所以这里用链表结构实现队列。

 一、初始化队列

上面分析了为了更好的管理数据,在队列里有管理数据出入的头尾指针,以及存储数据的结点,结点包括一个数据和和下一个结点的地址。

实现:

// 链式结构:表示队列 
typedef int QDataType;
typedef struct QListNode
{
	struct QListNode* _next;
	QDataType _data;
}QNode;

// 队列的结构 
typedef struct Queue
{
	QNode* _head;
	QNode* _tail;
}Queue;

设置好了队列结构后就要对队列进行初始化了->

把头尾指针都置空。

void QueueInit(Queue* q)
{
	assert(q);
	q->_head = NULL;
	q->_tail = NULL;


}

2、队尾入数据

因为这里队列是一个简化的单向链表,队尾入数据就是相当于尾插,只是这里尾插后要记录 尾指针,方便下一次尾插的进行,弥补了链表尾插效率低下的不足;

插入数据先开辟一个结点,把数据放到这个结点中,再链接在尾的后面,如果队列是空(头和尾都是空),就把这个结点当作头和尾,否则就是连在尾的后面,尾更新一下就可以了。

// 队尾入队列 
void QueuePush(Queue* q, QDataType data)
{
	assert(q);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)//判读是否开辟成功
	{
		printf("newnode fail!\n");
		exit(-1);
	}
	newnode->_data = data;
	newnode->_next = NULL;
	if (q->_head == NULL)//如果是第一次插入就让开辟的结点当头和尾
	{
		assert(q->_tail == NULL);
		q->_head = q->_tail = newnode;
	}
	else
	{
		q->_tail->_next = newnode;
		q->_tail = newnode;//or q->_tail->next=q->_tail->next;
	}
}

3、队头出数据

因为是链表的结构,队头出数据就是头删,非常简单,直接保存头结点后面一个结点的地址,销毁掉头结点,让头结点的下一个结点当头结点即可。

但是这里有一个小细节需要注意!

1.常规思想(缺乏严谨性)

// 队头出队列 
void QueuePop(Queue* q)
{
	assert(q);
	assert(q->_head && q->_tail);//如果是头和尾都是空了那么就是没有元素不用删了

	
	QNode* next = q->_head->_next;
	free(q->_head);
	q->_head = next;
	
}

 2.改进后的写法(加一个判断条件)

// 队头出队列 
void QueuePop(Queue* q)
{
	assert(q);
	assert(q->_head && q->_tail);//如果是头和尾都是空了那么就是没有元素不用删了
	//这里有个小细节要注意 如果只是按照普通的头删思想 会导致最后出现尾指针变成野指针 就是释放最后一个结点的时候 被释放后tail还是指向被释放的那块空间的
	if (q->_head->_next == NULL)
	{
		free(q->_head);
		q->_head = q->_tail = NULL;
	}
	else
	{
		QNode* next = q->_head->_next;
		free(q->_head);
		q->_head = next;
	}
}

4、获取队头元素

因为我们保存了头尾指针,想要获取队头元素就直接返回头指针指向结点里的数据即可。

// 获取队列头部元素 
QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(q->_head);
	return q->_head->_data;

}

5、获取队尾元素

因为我们保存了头尾指针,想要获取队尾元素就直接返回尾指针指向结点里的数据即可。

// 获取队列队尾元素 
QDataType QueueBack(Queue* q)
{
	assert(q);
	assert(q->_tail);
	return q->_tail->_data;
}

6、获取队列元素个数

定义一个计数器size,遍历队列,只要结点不为空,size++,当为空结点是就结束,此时的count就是队列元素个数。

拓展:也可以在队列的结构中加一个size 即在Queue中再定义一个size,每次插入元素就size++,最后只需要返回Queue中的size即可,省去了遍历的步骤!

// 获取队列中有效元素个数 
int QueueSize(Queue* q)
{
	assert(q);
	QNode* cur = q->_head;
	int size = 0;
	while (cur)
	{
		size++;
		cur = cur->_next;
	}
	return size;
}

7、判断队列是否为空

只需返回头结点是否为空,如果为空就是没有插入数据,可以以此来判断队列是否为空。

bool QueueEmpty(Queue* q)
{
	assert(q);
	return q->_head == NULL;
}

8、销毁队列

// 销毁队列 
void QueueDestroy(Queue* q)
{
	assert(q);
	QNode* cur = q->_head;
	while (cur)
	{
		QNode* next = cur->_next;
		free(cur);
		cur = next;
	}
	q->_head = q->_tail = NULL;//记得置空 防止出现野指针
}

源码链接:

Queue2 · 张华/c++code - 码云 - 开源中国 (gitee.com) 

Stack2 · 张华/c++code - 码云 - 开源中国 (gitee.com)

此文到此就结束了,各位看官下篇见~~~

  • 43
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 40
    评论
评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值