一. 栈
1. 栈的概念及结构
线性表:相同类型的 n 个数据元素的有限序列
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的元素遵守后进先出的原则。
压栈:栈的插入操作叫做进栈 / 压栈 / 入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈,出数据也在栈顶。


2.入栈出栈顺序
假设入栈顺序为 a1,a2,a3,a4,a5,那么出栈顺序是什么呢?大家最容易想到的是所以数据都入栈以后在出栈就是 a1,a2,a3,a4,a5;实际上有多种出栈顺序,比如每个数据入栈后马上出栈,所以另 ⼀种出栈顺序也可以是a1,a2,a3,a4,a5;还可以是a1,a2,a3⼊栈,出a3,a2,再⼊栈a4,a5,再出栈 a5,a4,a1,也就是说出栈顺序为a3,a2,a5,a4,a1; 通过分析我们可以看到因为出栈时机的不同,可以 有很多种出栈顺序。、

n 个不同元素进栈,出栈顺序组合数是一个卡特兰数 
3. 栈的实现
栈是一种只允许在一端进行插入和删除的线性表,栈插入和删除数据都在栈顶端,插入数据一般叫做入栈,删除数据一般叫做出栈,所以栈最核心的接口函数就是入栈和出栈函数。
1> 顺序栈实现
需要注意的是栈结构中初始化时,top = -1 还是 top = 0, top = -1 代表 top 指向栈顶元素,那么入栈时要先 ++top ,再把入栈数据据放到 top 位置;top = 0 代表 top 指向栈顶元素的下一个位置,那么入栈时要先把入栈数据放到 top 位置,再 ++top。其他接口函数处理细节也要对应调整。这两种方式没有优劣之分,大家任选一种即可。
为什么初始化时 top = 0 ,不能代表 top 指向栈顶元素呢?那我们要思考栈为空和栈只有一个数据时,top 都是 0,如何区分呢?

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int STDataType;
typedef struct
{
STDataType* arr; // 指向栈数组空间的指针
int top; // 栈顶位置
int capacity; // 容量
}Stack;
// 栈的初始化
void StackInit(Stack* s);
// 栈的销毁
void StackDestroy(Stack* s);
// x 元素入栈
void StackPush(Stack* s, STDataType x);
// 将栈顶元素出栈,并返回栈顶元素
STDataType StackPop(Stack* s);
// 获取栈顶元素并返回
STDataType StackPop(Stack* s);
// 获取栈中有效元素个数
int StackSize(Stack* s);
// 检测栈是否为空,如果是空 返回真,否则 返回假
bool StackEmpty(Stack* s);
#include"SeqStack.h"
// 栈的初始化
void StackInit(Stack* s)
{
assert(s);
s->arr = (STDataType*)malloc(4 * sizeof(STDataType));
if (s->arr == NULL)
{
printf("StackInit:空间申请失败!!!\n");
exit(-1);
}
// 初始化时,top = 0,表示top指向的是栈顶元素的下一个位置
// 初始化时,top = -1,表示top指向的是栈顶元素
s->top = 0;
s->capacity = 4;
}
// 栈的销毁
void StackDestroy(Stack* s)
{
assert(s);
if (s->arr)
{
free(s->arr);
s->arr = NULL;
s->top = 0;
s->capacity = 0;
}
}
// x 元素入栈
void StackPush(Stack* s, STDataType x)
{
assert(s);
// 满了
if (s->top == s->capacity)
{
STDataType* tmp = (STDataType*)realloc(s->arr, sizeof(STDataType) * s->capacity * 2);
if (tmp == NULL)
{
printf("StackPush:扩容空间失败!!!\n");
exit(-1);
}
s->arr = tmp;
s->capacity *= 2;
}
s->arr[s->top] = x;
s->top++;
}
// 将栈顶元素出栈,并返回栈顶元素
STDataType StackPop(Stack* s)
{
assert(s);
assert(!StackEmpty(s));
STDataType topE = s->arr[s->top - 1];
--s->top;
return topE;
}
// 获取栈顶元素并返回
STDataType StackTop(Stack* s)
{
assert(s);
assert(!StackEmpty(s));
return s->arr[s->top - 1];
}
// 获取栈中有效元素个数
int StackSize(Stack* s)
{
assert(s);
return s->top;
}
// 检测是否为空,如果是空 返回真,否则 返回假
bool StackEmpty(Stack* s)
{
assert(s);
return s->top == 0;
}
2>链式栈实现
栈的链式存储,我们可以选用单链表结构,也可以选用双链表结构。但是要注意的是,如果选择以用单链表必须用链表头这一端做栈顶,入栈出栈就是对应头插头删,时间复杂度为 O(1);否则用链表尾做栈顶,出栈对应尾删代价太大了,时间复杂度尾 O(N)。
如果选用双向链表,则用表头表尾做栈顶都可以,因为双向链表头尾插入删除效率都是O(1)。当然我们完全没必要选择双向链表,因为单链表就可以高效实现,还省空间一些。双向链表没有优势,每一个结点还要多存储一个前驱指针,使用他纯粹浪费了。
其次我们选择单链表实现,可以带头结点,也可以不带头结点;但是这里入栈出栈就是对应头插头删,单链表带头结点时,头插头删并不会带来什么便利,所以一般我们选择不带头结点实现即可。

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
// 链式栈中存储的数据元素类型
typedef int STDataType;
// 链式栈底层单链表中结点的定义
typedef struct LinkSatckNode
{
struct LinkStackNode* next;
STDataType data;
}LSNode;
typedef struct
{
LSNode* topHead;
int size;
}LinkStack;
// 初始化链式栈 s
void LinkStackInit(LinkStack* s);
// 销毁链式栈 s
void LinkStackDestroy(LinkStack* s);
// x 入栈
void LinkStackPush(LinkStack* s, STDataType x);
// 出栈,并返回栈顶元素
STDataType LinkStackPop(LinkStack* s);
// 获取栈顶元素
STDataType LinkStckTop(LinkStack* s);
// 获取栈中有效元素个数
int LinkStackSize(LinkStack* s);
// 检测栈是否为空,如果是空返回真,否则返回假
bool LinkStackEmpty(LinkStack* s);
#include"LinkStack.h"
// 初始化链式栈 s
void LinkStackInit(LinkStack* s)
{
assert(s);
s->topHead = NULL;
s->size = 0;
}
// 销毁链式栈 s
void LinkStackDestroy(LinkStack* s)
{
assert(s);
LSNode* cur = s->topHead;
while (cur)
{
LSNode* next = cur->next;
free(cur);
cur = next;
}
s->topHead = NULL;
s->size = 0;
}
// x 入栈
void LinkStackPush(LinkStack* s, STDataType x)
{
assert(s);
// 申请新结点
LSNode* newNode = (LSNode*)malloc(sizeof(LSNode));
if (newNode == NULL)
{
printf("LinkStackPush:申请结点失败");
exit(-1);
}
newNode->data = x;
// 头插
newNode->next = s->topHead;
s->topHead = newNode;
s->size++;
}
// 出栈,并返回栈顶元素
STDataType LinkStackPop(LinkStack* s)
{
assert(s);
assert(!LinkStackEmpty(s));
LSNode* delNode = s->topHead;
STDataType topE = delNode->data;
s->topHead = delNode->next;
free(delNode);
delNode = NULL;
s->size--;
return topE;
}
// 获取栈顶元素
STDataType LinStackTop(LinkStack* s)
{
assert(s);
assert(!LinkStackEmpty(s));
return s->topHead->data;
}
// 获取栈中有效元素个数
int LinkStackSize(LinkStack* s)
{
assert(s);
return s->size;
}
// 检测是否为空,如果是空 返回真,否则 返回假
bool LinkStackEmpty(LinkStack* s)
{
assert(s);
return s->size == 0;
}
3> 顺序存储和链式存储对比
栈的顺序存储和链式存储都可以做到入栈和出栈时间复杂度为 O(1),效率都很高。
栈的顺序存储的唯一问题是入栈时空间不够,扩容有一定的消耗,但是这个消耗机会可以忽略不计,因为插入一定量数据才会扩容。
栈的顺序存储相比链式存储 CPU 高速缓存命中率较高,且没有内存碎片。所以栈的实现一般我们使用顺序存储。
CPU缓存:对于CPU来说,它是不会一个字节一个字节的加载的,因为这非常没有效率,一般来说都是要一块一块的数据单位,术语叫"Cache Line",一般来说,一个主流的CPU的Cache Line 是 64 Bytes,Cache Line就是CPU 从内存中捞数据上来的最小数据单位。CPU要读取一个数据时,首先从Cache中查找,如果找到就立即读取并送给CPU处理;如果没有找到,就用相对慢的速度从内存中读取并送给 CPU处理,同时把这个数据所在的数据块调入 Cache 中,可以使得以后对整块数据的读取都从 Cache 中进行,不必再访问内存。
内存碎片是计算机内存管理中的一种现象,指内存中存在足够的总空闲容量,但这些空闲空间被分割成许多不连续的小块,无法分配给需要连续内存空间的程序使用。就像停车场有很多零散的空车位,但因为它们不连续,无法停下一辆需要连续多个车位的大巴车。
顺序表插入删除数据过程中,始终保持一块连续的存储空间,不会产生内存碎片;链表插入删除数据过程中,不断申请和释放小块的存储空间,会导致严重的内存碎片
4. OJ
20. 有效的括号 - 力扣(LeetCode)
https://leetcode.cn/problems/valid-parentheses/description/

解题思路:栈具有后进先出的性质,括号类型和顺序和数量都要匹配,可以遍历字符串,遇到左括号就入栈,遇到右括号就取栈顶的左括号进行匹配。
如果类型匹配不上说明是类型或顺序不匹配;
如果栈为空说明右括号多了,数量不匹配;
如果字符串遍历结束,栈不为空,说明左括号多了,数量不匹配;
结论:字符串遍历结束,中途没有出现不匹配,最后栈为空,则说明都匹配,是有效括号
typedef char STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}Stack;
// 初始化
void StackInit(Stack* pst);
// 销毁
void StackDestroy(Stack* pst);
// ⼊栈
void StackPush(Stack* pst, STDataType x);
// 出栈
STDataType StackPop(Stack* pst);
// 取栈顶数据
STDataType StackTop(Stack* pst);
// 判空
bool StackEmpty(Stack* pst);
// 获取数据个数
int StackSize(Stack* pst);
// 初始化和销毁
void StackInit(Stack* pst) {
assert(pst);
pst->a = NULL;
// top指向栈顶数据的下⼀个位置
pst->top = 0;
pst->capacity = 0;
}
void StackDestroy(Stack* pst) {
assert(pst);
free(pst->a);
pst->a = NULL;pst->top = pst->capacity = 0;
}
// ⼊栈 出栈
void StackPush(Stack* pst, STDataType x) {
assert(pst);
// 扩容
if (pst->top == pst->capacity) {
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity *
sizeof(STDataType));
if (tmp == NULL) {
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
STDataType StackPop(Stack* pst) {
assert(pst);
assert(!StackEmpty(pst));
STDataType x = pst->a[pst->top - 1];
pst->top--;
return x;
}
// 取栈顶数据
STDataType StackTop(Stack* pst) {
assert(pst);
assert(!StackEmpty(pst));
return pst->a[pst->top - 1];
}
// 判空
bool StackEmpty(Stack* pst) {
assert(pst);
return pst->top == 0;}
// 获取数据个数
int StackSize(Stack* pst) {
assert(pst);
return pst->top;
}
///////////////////////////////////////////////////////////////////
bool isValid(char* s) {
Stack st;
StackInit(&st);
while (*s) {
// 1、左括号⼊栈
if (*s == '(' || *s == '[' || *s == '{') {
StackPush(&st, *s);
}
else { // 2、右括号取栈顶左括号尝试匹配
// 当前位置是右括号,栈为空代表前⾯没有左括号了
// 则说明括号不匹配
if (StackEmpty(&st)) {
StackDestroy(&st);
return false;
}
char top = StackPop(&st);
// 不匹配
if ((top == '(' && *s != ')')
|| (top == '{' && *s != '}')
|| (top == '[' && *s != ']')) {
StackDestroy(&st);
return false;
}
}
++s;
}
// 栈不为空,说明左括号⽐右括号多,数量不匹配
bool ret = StackEmpty(&st);
StackDestroy(&st);
return ret;
}
实现版本2:版本1的方式还有一个弊端就是我们要实现一个栈,如果遇到在考试中遇到某个题目要用栈,我们可以只提供 Stack 结构定义和接口函数声明并备注接口函数功能即可。但还是很麻烦,且如果是上机考试只能硬着头皮实现,着就很麻烦。所以另一种方式就是直接定义一个静态数组作为简单的栈,再定义一个 top 标记栈顶数据即可。
bool isValid(char* s) {
// 本题中说明s的⻓度<=10000
// 所以这⾥我们直接创建⼀个10000静态数组做栈
int stackArr[10000];
// top标记栈顶数据下⼀个位置
int top = 0;
while (*s) {
// 1、左括号⼊栈
if (*s == '(' || *s == '[' || *s == '{') {
stackArr[top++] = *s;
}
else { // 2、右括号取栈顶左括号尝试匹配
// 当前位置是右括号,栈为空代表前⾯没有左括号了
// 则说明括号不匹配
if (top == 0) {
return false;
}
char topData = stackArr[--top];
// 不匹配
if ((topData == '(' && *s != ')')
|| (topData == '{' && *s != '}')
|| (topData == '[' && *s != ']')) {
return false;
}
}
++s;
}
// 栈不为空,说明左括号⽐右括号多,数量不匹配
r
二. 队列
1. 队列的概念及结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据结构操作的特殊线性表,队列具有先进先出的性质。
入队列:进行插入操作的一端称为队尾。
出队列:进行删除操作的一端称为队头。

2. 队列的实现
队列也可以用数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。

1> 链式队列实现
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
// 初始化 和 销毁
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
// 队尾插入
void QueuePush(Queue* pq, QDataType x);
// 对头删除
void QueuePop(Queue* pq);
// 取队头 和 队尾的数据
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
// 获取数据个数
int QueueSize(Queue* pq);
// 判空
bool QueueEmpty(Queue* pq);
函数实现(Queue.c)
1> 初始化
// 初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
2> 销毁
// 销毁
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
3> 队尾插入
入队就是在表尾插入一个结点,队列设计中我们增加了为指针,所以要实现尾插是很容易的
我们是带头结点的结构,队列空和非空的情况一样的代码就尾插处理了

// 队尾插入
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->next = NULL;
newnode->val = x;
if (pq->ptail == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
4> 队头删除
我们是带头结点的结构,出队就是删除头结点后的第一个结点,需要区分多个结点和最后一个结点的情况,最后一个结点删除后,rear 需要指向哨兵位的头结点了。

// 队头删除
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->size != 0);
// 一个节点
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else // 多个节点
{
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
5> 取队头数据
// 取队头的数据
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
6> 取队尾数据
// 取队尾的数据
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->ptail);
return pq->ptail->val;
}
7> 获取数据个数
// 获取数据个数
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
8> 判空
// 判空
void QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
2> 顺序队列实现
顺序存储结构实现队列,入队对应尾插,出队对应头删,而顺序存储结构直接头删需要挪动数据时间复杂度为 O(N),如果实现队列的顺序存储结构效率太低,就无价值了。所以这里可以调整思路,出队头删时,不在挪动数据,而是移动队头的下标或指针。
#define MAX_SIZE 8
typedef int QDataType; // 循环队列中存储数据元素的类型
typedef struct SqQueue {
QDataType arr[MAX_SIZE];
int front; // 指向队头
int rear; // 指向队尾的下⼀个位置
}SqQueue;

顺序队列的一个经典问题就是假溢出问题,队列的存储空间(数组)中明明还有空闲位置,但由于队尾指针 rear 已经移动到了数组的末尾,导致无法在插入新元素,系统错误了的判断了”队列已满“的现象,这种情况之所以被称为”假“溢出,是因为它并非真正的空间耗尽,而是一种由于数据结构设计缺陷导致的空间浪费
解决假溢出问题的经典方法是使用循环队列: 入队后,rear 指针的更新方式为:rear = ( rear + 1 ) % 数组容量 出队后, front 指针的更新方式为:rear = ( frot + 1 ) % 数组容量
3. OJ
225. 用队列实现栈 - 力扣(LeetCode)
https://leetcode.cn/problems/implement-stack-using-queues/description/
用两个队列实现栈,一个队列设置为空,另一个用来放元素,push 操作直接往有元素的队列放入元素即可。pop 操作要先将有元素的队列 size-1 个元素一次出队列,按照出队列顺序进入另一个队列,然后弹出并返回最后一个元素即可。top 操作直接返回有元素队列的最后一个元素。
typedef struct
{
Queue q1;
Queue q2;
}MYStack;
MYStack* myStackCreate()
{
MyStack* pst = (MyStack)malloc(sizeof(MyStack));
QueueInit(&(pst->q1));
QueueInit(&(pst->q2));
return pst;
}
void myStackPsuh(MyStack* obj, int x)
{
if (!QueueEmpty(&(obj->q1)))
{
QueuePush(&(obj->q1), x);
}
else
{
Queue(&(obj->q2), x);
}
}
int myStackPop(MyStack* obj)
{
// 假设法
Queue* empty = &(obj->q1);
Queue* nonempty = &(obj->q2);
if (!QueueEmpty(&(obj->q1)))
{
nonempty = &(obj->q1);
empty = &(obj->q2);
}
// 不为空前 size-1 导走,删除最后一个就是栈顶数据
while (QueueSize(nonempty) > 1)
{
QueuePush(empty, QueueFront(nonempty));
QueuePop(nonempty);
}
int top = QueueFront(nonempty);
QueuePop(nonempty);
return top;
}
int myStackTop(MyStack* obj)
{
if (!QueueEmpty(&(obj->q1)))
{
return QueueBack(&(obj->q1));
}
else
{
return QueueBack(&(obj->q2));
}
}
bool myStackEmpty(MyStack* obj)
{
return QueueEmpty(&(obj->q1)) && QueueEmpty(&(obj->q2));
}
void myStackFree(MyStack* obj)
{
QueueDestroy(&(obj->q1));
QueueDestroy(&(obj->q2));
free(obj);
}
862

被折叠的 条评论
为什么被折叠?



