【每天学习亿点点系列】——重温栈和队列

栈的实现

在这里插入图片描述

栈的结构并不复杂,但是问题来了,我们应该怎样实现栈了,到底是用顺序表还是链表了,哪个更好了?
顺序表:入栈更加方便,直接在size的位置插入即可,出栈也方便,直接出对应的下标就行
但如果你用单链表就没不是这样了,首先你每次入栈的时候都必须要找到尾节点;出栈的时候出的是尾节点,但此时你还必须记录尾节点的前一个节点,因为如果你要继续出栈的话,就要把栈顶的元素删除,此时你为了保证链表的结构,必须让尾节点的前一个节点指向空
下面来看看栈的代码实现,我分别用顺序表和单链表各实现了一下,大家可以比较一下优劣。

顺序表实现栈

头文件以及测试部分代码

stack.h

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
#include<stdbool.h>

//顺序表实现栈
typedef int SDataType;
typedef struct Stack
{
	SDataType* a;
	int sz;
	int cap;
}Stack;

//将顺序表初始化
void Stackinit(Stack* cc);
//栈的插入
void StackPush(Stack* cc, SDataType x);
//栈的删除
void StackPop(Stack* cc);
//栈顶取出数据
SDataType Stacktake(Stack* cc);
//栈里面元素的打印
void StackPrint(Stack* cc);
//记录栈里面元素的个数
int StackSize(Stack* cc);
//判断是否为空
bool StackEmpty(Stack* cc);
//栈的销毁
void StackDestory(Stack* cc);

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"stack.h"
//顺序表实现栈
void test()
{
	Stack c;
	Stackinit(&c);
	StackPush(&c, 1);
	StackPush(&c, 2);
	StackPush(&c, 3);
	StackPush(&c, 4);
	StackPush(&c, 5);
	printf("%d\n", Stacktake(&c));
	printf("%d\n", Stacktake(&c));
	StackPrint(&c);
	StackPop(&c);
	StackPop(&c);
	StackPop(&c);
	//StackPop(&c);
	StackPrint(&c);
	printf("%d\n", StackSize(&c));
	printf("%d\n", StackEmpty(&c));
}

int main()
{
	test();
	return 0;
}
各个接口的实现
1.初始化
void Stackinit(Stack* cc)
{
	assert(cc);
	cc->cap = 4;
	SDataType* new = (SDataType*)malloc(sizeof(SDataType)*cc->cap);
	if (new == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	else
	{
		cc->a = new;
	}
	cc->sz = 0;
}
2.栈的插入
void StackPush(Stack* cc, SDataType x)
{
	assert(cc);
	if (cc->sz == cc->cap)
	{
		cc->cap *= 2;
		cc->a = BuySList(cc);
		cc->a[cc->sz] = x;
		cc->sz++;
	}
	else
	{
		cc->a[cc->sz] = x;
		cc->sz++;
	}
}
3.栈的删除
void StackPop(Stack* cc)
{
	assert(cc);
	assert(!StackEmpty(cc));
	//任何要删除的地方都要这个断言,因为如果顺序表为空就不能删除了
	cc->sz--;
}

4.栈顶取出数据
SDataType Stacktake(Stack* cc)
{
	assert(cc);
	assert(!StackEmpty(cc));
	return cc->a[cc->sz - 1];
}
5.栈里面元素的打印
void StackPrint(Stack* cc)
{
	assert(cc);
	int i = 0;
	for (i = 0; i < cc->sz; i++)
	{
		printf("%d ", cc->a[i]);
	}
	printf("\n");
}

6.记录栈中元素个数
int StackSize(Stack* cc)
{
	assert(cc);
	return cc->sz;
}
7.判空
bool StackEmpty(Stack* cc)
{
	return cc->sz == 0;
}
8.栈的销毁
void StackDestory(Stack* cc)
{
	assert(cc);
//因为顺序表空间是连续的,所以可以这样释放
	free(cc->a);
	cc->a = NULL;
	cc->sz = cc->cap = 0;
}

单链表实现

头文件以及测试部分代码

stack.h

#include<string.h>
#include<stdlib.h>
#include<stdbool.h>



//单链表实现栈
typedef int stackDataType;
typedef struct stack
{
	struct stack* next;
	stackDataType data;
}stack;

//栈的插入
void stackPush(stack** cc,stackDataType x);

//栈里面元素的打印
void stackPrint(stack* cc);

//栈里面删除栈顶元素
void stackPop(stack** cc);

//取出栈顶的数据
stackDataType stacktake(stack** cc);

//判断栈是否为空
bool stackEmpty(stack* cc);

//记录栈中元素的个数
int stackSize(stack* cc);

//销毁
void stackDestory(stack** cc);

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"stack.h"

void test()
{
	stack* c=NULL;
	stackPush(&c,1);
	stackPush(&c,2);
	stackPush(&c,3);
	stackPop(&c);
	printf("%d\n", stacktake(&c));
	stackPrint(c);
	printf("%d\n", stackSize(c));
	stackDestory(&c);
	stackPrint(&c);
}
int main()
{
	test();
	return 0;
}
各个接口的实现
1.栈的插入
void stackPush(stack** cc,stackDataType x)
{
	assert(cc);
	//一个节点也没有的情况
	if ((*cc) == NULL)
	{
		(*cc) = BuyNode(x);
		(*cc)->next = NULL;
	}
//有节点的情况,注意由于栈是后进先出的,所以头插比较方便,拿出来时
//的时间复杂度为O(1),如果尾插复杂度为O(n)
	else
	{
		stack* newnode = BuyNode(x);
		newnode->next = (*cc);
		(*cc) = newnode;
	}
}
2.栈里面元素的打印
void stackPrint(stack* cc)
{
	while (cc)
	{
		printf("%d ", cc->data);
		cc = cc->next;
	}
	printf("\n");
}
3.删除栈顶的节点
void stackPop(stack** cc)
{
	assert(cc);
	assert(*cc);
    stack* tailpre= *cc;
	while (tailpre->next->next)
	{
		  tailpre = tailpre->next;
	}
	free(tailpre->next);
	tailpre->next = NULL;
}
4.取出栈顶的数据
stackDataType stacktake(stack** cc)
{
	assert(cc);
	assert(*cc);
	return (*cc)->data;
}
5.判空
bool stackEmpty(stack* cc)
{
	return cc == NULL;
}
6.记录栈中元素个数
int stackSize(stack* cc)
{
	int n = 0;
	while (cc)
	{
		n++;
		cc = cc->next;
	}
	return n;
}
7.销毁
void stackDestory(stack** cc)
{
	assert(cc);
	while (*cc)
	{
		struct stack* next = (*cc)->next;
		free(*cc);
		*cc = next;
	}
}

队列

在这里插入图片描述

队列的结构也并不复杂,和栈很像,栈是后进先出,而队列是先进先出,那么问题又来了,队列的实现使用顺序表好还是链表好了?
单链表:插入的话,每次要在尾节点后插入,需要遍历找尾,但是如果出队列的话,只要出头节点就可以了,然后将头节点的下一个节点变为头就可以了。
顺序表:插入的话很简单,每次只要在size为下标的位置插入即可,但如果出队列的话,只能出下标为0的位置,此时出了之后还需要把后面的元素往前移,也并不简单

我们发现其实这两者并没有哪个更优的说法,并不像实现栈的时候有明显的差距,那有没有其他方法可以提高效率了?
肯定是有的:我们发现单链表效率地的原因是它每次都要找尾,那我们能不能不找尾了,我用一个指针来记录尾的位置不就行了吗,前提是这个指针要一直都在,所以我们一开始就将它定义为结构体成员,这种方法叫做结构体包裹。
下面同样,我用两中方法都实现了一下,大家可以对比一下优劣。

单链表(没有用结构体包裹)来实现

头文件以及测试部分

Queue.h

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>

typedef int QueueDataType;
typedef struct Queue
{
	struct Queue* next;
	QueueDataType x;
}Queue;

//因为我是用单链表实现的队列,插入就相当于初始化了,所以没必要去初始化
void QueuePush(Queue** cc,QueueDataType x);
//取出数据,因为队列是先进先出,所以应该取队列顶的数据,也就是头节点
QueueDataType Queuetake(Queue** cc);
//删除,删除取出来的数据,也就是队列头部的数据,也就是头节点
void QueuePop(Queue** cc);
//判断是否为空
bool QueueEmpty(Queue* cc);
//记录队列的节点个数
int QueueSize(Queue* cc);
//打印队列中的节点
void QueuePrint(Queue* cc);

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
void test1()
{
	Queue *c=NULL;
	QueuePush(&c, 1);
	QueuePush(&c, 2);
	QueuePush(&c, 3);
	QueuePush(&c, 4);
	printf("%d\n", Queuetake(&c));
	QueuePop(&c);
	printf("%d\n", Queuetake(&c));
	QueuePop(&c);
	QueuePop(&c);
	QueuePop(&c);
	QueuePop(&c);
	QueuePrint(c);
}

int main()
{
	*test1();
	return 0;
}
各个接口的实现
1.队列的插入
//插入
void QueuePush(Queue** cc, QueueDataType x)
{
	assert(cc);
	//当没有节点的情况
	if (*cc == NULL)
	{
		Queue* newnode = BuyNode(x);
		newnode->next = NULL;
		*cc = newnode;
	}
	//当有节点的情况
	else
	{
		Queue* newnode = BuyNode(x);
		//找尾
		Queue* tail = (*cc);
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
		newnode->next = NULL;
//其实这一步写了等于没写,因为你下一次调用这个接口的时候,你tail又被赋值为*cc了
		tail = newnode; 
	}
}
2.出队列
//取出数据,因为队列是先进先出,所以应该取栈顶的数据,也就是头节点
QueueDataType Queuetake(Queue** cc)
{
	assert(cc);
	assert(*cc);   //这句话和52行起到的是相同的效果
	assert(!QueueEmpty(*cc));
	return (*cc)->x;
}
3.删除
//删除,删除取出来的数据,也就是队列头部的数据,也就是头节点
void QueuePop(Queue** cc)
{
	assert(cc);
	assert(!QueueEmpty(*cc)); //这句话和下一行起到的是相同的效果
	assert(*cc);
	Queue* newnode = (*cc)->next;
	(*cc) = newnode;
}
4.判空
//判断是否为空
bool QueueEmpty(Queue* cc)
{
	return cc == NULL;
}

5.记录节点个数
//记录队列的节点个数
int QueueSize(Queue* cc)
{
	int n = 0;
	while (cc)
	{
		n++;
		cc = cc->next;
	}
	return n;
}
6.打印队列中的节点
void QueuePrint(Queue* cc)
{
	while (cc)
	{
		printf("%d ", cc->x);
		cc = cc->next;
	}
	printf("\n");
}
7.销毁
//销毁
void QueueDestory(Queue** cc)
{
	assert(cc);
	while (*cc)
	{
		Queue* next = (*cc)->next;
		free(*cc);
		*cc = next;
	}
}

单链表实现(结构体包裹的方式)

头文件以及测试部分

Queue.h

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>


typedef int QueueDataType;
typedef int SListDataType;
typedef struct SList
{
	struct SList* next;
	SListDataType x;
}SList;
typedef struct Queue
{
	SList* head;
	SList* tail;
}Queue;

//这种结构体包裹的写法是要初始化的,因为你队列里面包含了两个指针
void Queueinit(Queue* cc);

void QueuePush(Queue** cc, QueueDataType x);
//取出数据,因为队列是先进先出,所以应该取队列顶的数据,也就是头节点
QueueDataType Queuetake(Queue** cc);
//删除,删除取出来的数据,也就是队列头部的数据,也就是头节点
void QueuePop(Queue** cc);
//判断是否为空
bool QueueEmpty(Queue* cc);
//记录队列的节点个数
int QueueSize(Queue* cc);
//打印队列中的节点
void QueuePrint(Queue* cc);
//销毁
void QueueDestory(Queue* cc);

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"

void test2()
{
	Queue c;
	Queueinit(&c);
	QueuePush(&c, 1);
	QueuePush(&c, 2);
	QueuePush(&c, 3);
	QueuePush(&c, 4);
	printf("%d\n", Queuetake(&c));
	QueuePop(&c);
	printf("%d\n", Queuetake(&c));
	QueuePop(&c);
	QueuePop(&c);
	QueuePop(&c);
	//QueuePop(&c);
	QueuePrint(&c);
}
int main()
{
	test2();
	return 0;
}
各个接口的实现
1.初始化
//这种结构体包裹的写法是要初始化的,因为你队列里面包含了两个指针
void Queueinit(Queue* cc)
{
	assert(cc);
	cc->head = cc->tail = NULL;
}

2.入队列
void QueuePush(Queue* cc, QueueDataType x)
{
	assert(cc);
	//没有节点的情况
	if (cc->tail == NULL)
	{
		SList* newnode = BuyNode(x);
		newnode->next = NULL;
		cc->head = cc->tail = newnode;
	}
	//有节点的情况
	else
	{
		//这就是结构体包裹这种写法的好处,这样你就不需要找尾节点了
		//因为我一开始定义队列的时候,就定义了一个指向尾的指针,这样
		//的想法可以大大提高效率
		SList* newnode = BuyNode(x);
		cc->tail->next = newnode;
		newnode->next = NULL;
		cc->tail = newnode;
	}
}
3.判空
bool QueueEmpty(Queue* cc)
{
	assert(cc);
	return cc->head == NULL;
}
4.出队列
QueueDataType Queuetake(Queue* cc)
{
	assert(cc);
	assert(!QueueEmpty(cc));
	return cc->head->x;
}
5.删除队列头部节点
void QueuePop(Queue* cc)
{
	assert(cc);
	assert(!QueueEmpty(cc));
	SList* newhead = cc->head->next;
	cc->head = newhead;
}
6.记录队列节点个数
int QueueSize(Queue* cc)
{
	assert(cc);
	int n = 0;
	while (cc->tail)
	{
		n++;
		cc->tail = cc->tail->next;
	}
	return n;
}
7.打印队列元素
void QueuePrint(Queue* cc)
{
	assert(cc);
	SList* cur = cc->head;
	while (cur)
	{
		printf("%d ", cur->x);
		cur=cur->next;
	}
	printf("\n");
}
8.销毁
void QueueDestory(Queue* cc)
{
	assert(cc);
	while (cc->head)
	{
		SList* next = cc->head->next;
		free(cc->head);
		cc->head = next;
	}
	cc->tail = NULL;
}
  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个数学不怎么好的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值