数据结构 - > 栈和队列

一. 栈

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/

解题思路:栈具有后进先出的性质,括号类型和顺序和数量都要匹配,可以遍历字符串,遇到左括号就入栈,遇到右括号就取栈顶的左括号进行匹配。

   如果类型匹配不上说明是类型或顺序不匹配;

   如果栈为空说明右括号多了,数量不匹配;

   如果字符串遍历结束,栈不为空,说明左括号多了,数量不匹配;

 

结论:字符串遍历结束,中途没有出现不匹配,最后栈为空,则说明都匹配,是有效括号

实现版本1:leetcode 支持 C 和 C++,但是 C++ 的函数参数使用了库中 string,我们不会用。C版本呢,使用栈无法使用我们前面实现的 Stack ,因为我们用来 C++的引用 & ,所以下面版本的代码,我们提供了一份纯 C 实现的 Stack ,把引用改成了指针。
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);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值