栈和队列的介绍与实现

终于到数据结构啦~

今天给大家带来的是栈和队列这两个东西。在介绍的时候,我会放在一起对比介绍,这样比较容易理解。

下一期预告:二叉树(那是我目前看来最牛的发明!)

什么是栈?什么是队列?

大家在之前学习循环的时候,应该已经遇到过栈了。如果你写了一个巨大的递归函数,你可能会遇到栈溢出的情况发生。

但是此“栈”非彼“栈”。

栈溢出的栈是对于内存空间中的栈进行讨论的,而我们今天讨论的是数据结构的栈。

同理,在下一期我们也会遇到堆,内存中的堆和数据结构的堆也是不同的两个东西。

但这并非翻译失误,而是两者的原文都是Stack(栈)和heap(堆)。

首先先说栈和队列的特点:

栈:后进先出,一般用顺序表实现(可以用链表实现,但是并不方便)。

        将数据放入栈的操作,我们称为压栈(入栈)。

将数据弹出栈的操作,我们称为弹栈(出栈)。

栈底的数据是我们最先放入的数据,而栈顶的数据是还留在栈里的数据中,最晚放入的数据。

学过最基础的顺序表和链表的同学在这个情况下就已经看出来了,栈的实现用动态顺序表比链表更合适。原因就在于入栈和出栈。

首先是单向链表。当我们执行入栈和出栈操作的时候,我们需要用头指针遍历到最后一个,然后更改最后一个链表的值。

每次执行入栈/出栈操作的时候,我们都需要从头指针遍历到最后一个,大大降低了代码的效率。

这个时候有人说,能不能存一个尾指针。

有了尾指针,我们可以直接进行压栈的操作,但是对于出栈的操作,我们还是束手无策。

所以我们只能举手投降,并且乖乖使用双向链表,存储头指针和尾指针,以便我们的操作。

但这种情况下,空间就被大大浪费了,而且代码不好写(除非你已经有了代码的拷贝)。

队列:先进先出,一般用链表实现。

同时,入队和出队的操作也同样存在,并且会弹出队头的数据,在队尾放入数据。

我们再回到用链表,还是用顺序表的问题。

分析一下数据结构,如果我们需要随时随地的弹出队头数据,显然是用链表更方便。用顺序表时,无论是动态还是静态,我们都需要有移位的操作,将后面所有的数据向前移动,这显然是麻烦的。

而对于链表来说,只需要将链表的头结点指向下一个,并free掉原节点即可,效率极高。

简而言之,栈(LIFO)和队列(FIFO)是两个看上去非常相像的数据结构类型,但是实际上大不相同。在说两者应用之前,先来实现一下吧~

栈的实现

stack.h文件和stack.c文件已放在下方,请自行取用~

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>

// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;		// 栈顶
	int capacity;  // 容量 
}Stack;
// 初始化栈 
void StackInit(Stack* ps);
// 入栈 
void StackPush(Stack* ps, STDataType data);
// 出栈 
void StackPop(Stack* ps);
// 获取栈顶元素 
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数 
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps);
// 销毁栈 
void StackDestroy(Stack* ps);
#define _CRT_SECURE_NO_WARNINGS
#include "stack.h"

typedef int STDataType;
void StackInit(Stack* ps) {
	assert(ps);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}
// 入栈 
void StackPush(Stack* ps, STDataType data) {
	assert(ps);
	if (ps->top == ps->capacity) {
		int newcapacity = ps->capacity == 0 ? 4 : ps ->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
		if (tmp == NULL) {
			perror(realloc);
			return;
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = data;
	ps->top++;
}
// 出栈 
void StackPop(Stack* ps) {
	assert(ps);
	assert(ps->top);
	ps->top--;
}
// 获取栈顶元素 
STDataType StackTop(Stack* ps) {
	assert(ps);
	assert(ps->top > 0);
	return ps->a[ps->top - 1];
}
// 获取栈中有效元素个数 
int StackSize(Stack* ps) {
	assert(ps);
	return ps->top;
}
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps) {
	assert(ps);
	return ps->top == 0;
}
// 销毁栈 
void StackDestroy(Stack* ps) {
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}

实际上大多数还是动态顺序表的操作,只是可能需要判空。

队列的实现

Queue.h文件和Queue.c文件如下

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
// 链式结构:表示队列
typedef int QDataType;
typedef struct QListNode
{
	struct QListNode* next;
	QDataType data;
}QNode;

// 队列的结构 
typedef struct Queue
{
	QNode* head;
	QNode* tail;
	int size;
}Queue;

// 初始化队列 
void QueueInit(Queue* q);
// 队尾入队列 
void QueuePush(Queue* q, QDataType data);
// 队头出队列 
void QueuePop(Queue* q);
// 获取队列头部元素 
QDataType QueueFront(Queue* q);
// 获取队列队尾元素 
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数 
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
bool QueueEmpty(Queue* q);
// 销毁队列 
void QueueDestroy(Queue* q);
#define _CRT_SECURE_NO_WARNINGS
#include"Queue.h"

// 初始化队列 
void QueueInit(Queue* q) {
	assert(q);
	q->head = NULL;
	q->tail = NULL;
	q->size = 0;
}
// 队尾入队列 
void QueuePush(Queue* q, QDataType data) {
	assert(q);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL) {
		perror(newnode);
		return;
	}
	newnode->next = 0;
	newnode->data = data;
	if (q->tail == NULL) {
		q->head = q->tail = newnode;
	}
	else {
		q->tail->next = newnode;
		q->tail = newnode;
	}
	q->size++;
}
// 队头出队列 
void QueuePop(Queue* q) {
	assert(q);
	assert(q->size!=0);
	if (q->size == 1) {
		free(q->head);
		q->head = q->tail = NULL;
	}
	else {
		QNode* tmp = q->head->next;
		free(q->head);
		q->head = tmp;
	}
	q->size--;
}
// 获取队列头部元素 
QDataType QueueFront(Queue* q) {
	assert(q);
	assert(q->head);
	return q->head->data;
}
// 获取队列队尾元素 
QDataType QueueBack(Queue* q) {
	assert(q);
	assert(q->tail);
	return q->tail->data;
}
// 获取队列中有效元素个数 
int QueueSize(Queue* q) {
	assert(q);
	return q->size;
}
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
bool QueueEmpty(Queue* q) {
	assert(q);
	return q->size==0;
}
// 销毁队列 
void QueueDestroy(Queue* q) {
	assert(q);
	while (q->head != NULL) {
		QNode* next = q->head->next;
		free(q->head);
		q->head = next;
	}
	q->size == 0;
	q->head = q->tail = NULL;
}

不难发现,实际上还是单向链表。

栈和队列的应用

在应用方面,我分别介绍一个我目前遇到的数据结构,肯定还有更多用法,我只是略加介绍。

单调栈

不知道在刷leetcode每日一题的各位,有没有看到过“单调栈”这三个字。

单调栈在栈的基础上,增加了单调性,因而,又分为单调递增栈和单调递减栈。

以单调递增栈为例。

在栈的基础上,保证栈顶到栈底单调递增,如果入栈的元素小于栈顶元素,那么可以直接进栈,否则弹出比它小的所有值后,再进栈,这就是单调递增栈。

听起来很简单,但是很有用。

456. 132 模式 - 力扣(LeetCode)

当初做每日一题的时候,我恰巧做到了这道132模式的问题。

朴素的单调栈只能帮助我们找到最相近的一个更大或更小的数,因此我们需要加以改造后才能得到我们想要的结果。

感兴趣的同学可以去尝试一下~。

广度优先搜索(BFS)

广度优先搜索,顾名思义,就是在每一层都遍历一遍节点后,再到下一层,因而叫广度优先搜索,与之相对的深度优先搜索,就是一条线走到黑以后,再走下一条,直到全走完。

广度优先搜索需要用队列来实现,实现原理如下:

1、队列中先存第一层的结点。

2、每经过一个结点,就将该节点后的所有节点放入队列(即队尾)

3、队列为空后,搜索完毕。

广度优先搜索往往用于计算最短路径,最小次数等问题。

994. 腐烂的橘子 - 力扣(LeetCode)

这道994题也留给看到这篇文章的各位去尝试。

文章就到这里结束,如有问题,请大佬们指正!希望能帮到看到这篇文章的你。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值