终于到数据结构啦~
今天给大家带来的是栈和队列这两个东西。在介绍的时候,我会放在一起对比介绍,这样比较容易理解。
下一期预告:二叉树(那是我目前看来最牛的发明!)
什么是栈?什么是队列?
大家在之前学习循环的时候,应该已经遇到过栈了。如果你写了一个巨大的递归函数,你可能会遇到栈溢出的情况发生。
但是此“栈”非彼“栈”。
栈溢出的栈是对于内存空间中的栈进行讨论的,而我们今天讨论的是数据结构的栈。
同理,在下一期我们也会遇到堆,内存中的堆和数据结构的堆也是不同的两个东西。
但这并非翻译失误,而是两者的原文都是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每日一题的各位,有没有看到过“单调栈”这三个字。
单调栈在栈的基础上,增加了单调性,因而,又分为单调递增栈和单调递减栈。
以单调递增栈为例。
在栈的基础上,保证栈顶到栈底单调递增,如果入栈的元素小于栈顶元素,那么可以直接进栈,否则弹出比它小的所有值后,再进栈,这就是单调递增栈。
听起来很简单,但是很有用。
当初做每日一题的时候,我恰巧做到了这道132模式的问题。
朴素的单调栈只能帮助我们找到最相近的一个更大或更小的数,因此我们需要加以改造后才能得到我们想要的结果。
感兴趣的同学可以去尝试一下~。
广度优先搜索(BFS)
广度优先搜索,顾名思义,就是在每一层都遍历一遍节点后,再到下一层,因而叫广度优先搜索,与之相对的深度优先搜索,就是一条线走到黑以后,再走下一条,直到全走完。
广度优先搜索需要用队列来实现,实现原理如下:
1、队列中先存第一层的结点。
2、每经过一个结点,就将该节点后的所有节点放入队列(即队尾)
3、队列为空后,搜索完毕。
广度优先搜索往往用于计算最短路径,最小次数等问题。
这道994题也留给看到这篇文章的各位去尝试。
文章就到这里结束,如有问题,请大佬们指正!希望能帮到看到这篇文章的你。