数据结构--栈与队列

目录

一、栈

[1.1 栈的基本概念](#1.1 栈的基本概念)

[1.2 栈的代码实现](#1.2 栈的代码实现)

[1.2.1 定义结构体成员以及方法](#1.2.1 定义结构体成员以及方法)

[1.2.2 初始化栈](#1.2.2 初始化栈)

[1.2.3 销毁栈](#1.2.3 销毁栈)

[1.2.4 插入数据](#1.2.4 插入数据)

[1.2.5 判断栈是否存入数据](#1.2.5 判断栈是否存入数据)

[1.2.6 弹出栈顶元素](#1.2.6 弹出栈顶元素)

[1.2.7 查看栈的当前有效长度](#1.2.7 查看栈的当前有效长度)

[1.2.8 查看当前栈顶元素](#1.2.8 查看当前栈顶元素)

[1.2.9 测试](#1.2.9 测试)

二、队列

[2.1 队列的基本概念](#2.1 队列的基本概念)

[2.2 队列的代码实现](#2.2 队列的代码实现)

[2.2.1 定义结构体成员以及方法](#2.2.1 定义结构体成员以及方法)

[2.2.2 初始化队列](#2.2.2 初始化队列)

[2.2.3 销毁队列](#2.2.3 销毁队列)

[2.2.4 插入数据](#2.2.4 插入数据)

[2.2.5 弹出队头元素](#2.2.5 弹出队头元素)

[2.2.6 查看当前有效长度](#2.2.6 查看当前有效长度)

[2.2.7 判断队列是否存入数据](#2.2.7 判断队列是否存入数据)

[2.2.8 查看队头元素](#2.2.8 查看队头元素)

[2.2.9 查看队尾元素](#2.2.9 查看队尾元素)

[2.2.10 测试](#2.2.10 测试)

三、总结


一、栈

1.1 栈的基本概念

(Stack):是只允许在一端进行插入或删除的线性表。首先栈是一种线性表,但限定这种线性 表只能在某一端进行插入和删除操作。

栈顶 (Top):线性表允许进行插入删除的那一端。

栈底 (Bottom):固定的,不允许进行插入和删除的另一端。

空栈:不含任何元素的空表。

栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构

就像图中这样:

1.2 栈的代码实现

了解完上面的概念之后,我们就可以用代码来实现它。

(请注意,下面的代码均是用 C 语言来编写的)

1.2.1 定义结构体成员以及方法

代码如下:

cpp 复制代码
#pragma once

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

typedef int STDataType;

typedef struct Stack
{
	//a:指向动态分配数组的指针,用于存储栈中的元素数据。
	//top:表示栈顶的位置,通常指向栈顶元素的下一个位置(或当前栈顶元素的位置),同时也可以表示栈中元素的个数。
	//capacity:表示当前动态数组的总容量,即最多能存储多少个元素。
	STDataType* a;
	int top;
	int capacity;
}ST;
//初始化栈
void STInit(ST* ps);
//销毁栈
void STDestroy(ST* ps);
//存数据
void STPush(ST* ps, STDataType x);
//取数据
void STPop(ST* ps);
//查看栈的当前长度
int STSize(ST* ps);
//判断栈是否为 NULL
bool STEmpty(ST* ps);
//查看当前栈顶元素
STDataType STTop(ST* ps);

有人看完代码之后,可能会感到疑问,为啥要使用数组来存储栈的元素了?链表不可以吗?

理由如下:

  • 因为栈只在一端,也就是在 栈顶上进行插入和删除,数组的尾部操作时间复杂度为 O(1),且内存连续、缓存命中率高、性能更好。
  • 链表虽然也能实现,但每个节点需要额外存储指针,内存开销大,且频繁申请释放节点效率较低。数组实现更简单高效,所以通常选用数组。

1.2.2 初始化栈

代码如下:

cpp 复制代码
void STInit(ST* ps)
{
	assert(ps);
    // 初始化栈,给 ps->a 分配4个元素空间
	ps->a = (STDataType*)malloc(sizeof(STDataType) * 4 );
	if (ps->a == NULL)
	{
		perror("malloc error");
		return NULL;
	}
	//ps->top = -1; //代表当前栈顶元素  
	ps->top = 0;//这个代表是栈顶元素的下一个位置
    //代表当前可以存放4个元素
	ps->capacity = 4;
}

1.2.3 销毁栈

代码如下:

cpp 复制代码
void STDestroy(ST* ps)
{
    //判断传来的值是否为NULL;
	assert(ps);
    //ps->a 指向的是一整块连续动态分配的内存数组
    //所以直接 free(ps->a) 就可以完成
	free(ps->a);
	ps->top = 0;
	ps->capacity = 0;
}

1.2.4 插入数据

代码如下:

cpp 复制代码
void STPush(ST* ps, STDataType x)
{
	assert(ps);
    //检查当前容量是否满了
	if (ps->top == ps->capacity)
	{
        //满了的话,就扩容两倍,这个是内存扩容
		STDataType* tmp = (STDataType*)realloc(ps->a,sizeof(STDataType) 
                            * ps->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc error");
			return NULL;
		}
        //将新扩容容量的地址,赋值给ps->a
		ps->a = tmp;
        //将原来的容量扩大二倍,改变原来的值,这个是值扩大二倍,不影响内存的
		ps->capacity *= 2;
	}
    //将新元素放入栈顶
	ps->a[ps->top] = x;
    //top往上移动一位,等待下一个栈顶元素的到来
	ps->top++;
}

1.2.5 判断栈是否存入数据

代码如下:

cpp 复制代码
bool STEmpty(ST* ps)
{
	assert(ps);    
    //就看当前有没有栈顶元素
    //top == 0 就表示当前没有元素存入
	return ps->top == 0;
}

1.2.6 弹出栈顶元素

代码如下:

cpp 复制代码
void STPop(ST* ps)
{
	assert(ps);
	assert(!STEmpty(ps)); //防止栈里面没有元素
	ps->top--;//直接让 top 的位置往下移一位
}

1.2.7 查看栈的当前有效长度

代码如下:

cpp 复制代码
int STSize(ST* ps)
{
	assert(ps);
    //如果初始化定义top = 0的话,top的值就是栈的有效长度(也就是当前存入了多少值)
    //如果初始化定义top = -1的话,top + 1的值就是栈的有效长度
    //跟数组下标求长度一个道理
	return ps->top;
}

1.2.8 查看当前栈顶元素

代码如下:

cpp 复制代码
STDataType STTop(ST* ps)
{
	assert(ps);//防止栈里面没有元素
	assert(!STEmpty(ps));
	//因为 top 表示栈顶元素的下一个位置,所以就用 ps->top - 1 来表示栈顶元素
	return ps->a[ps->top - 1];
}

1.2.9 测试

  • 测试代码:
cpp 复制代码
#include"Stack.h"

void test()
{
	ST ps;
	STInit(&ps);
	STPush(&ps, 1);
	STPush(&ps, 22);
	STPush(&ps, 3);
	STPop(&ps);
	STDataType x = STTop(&ps);
	printf("栈顶元素为:%d\n", x);
	int size = STSize(&ps);
	printf("当前有效长度为:%d\n", size);
	STDestroy(&ps);

}

int main()
{
	test();
	return 0;
}

这里不需要将ps置为NULL的原因是:

因为 ps 是在 main 函数中定义的局部变量ST ps,不是指针。STDestroy(&ps) 传入的是 ps 的地址,函数内部释放了 ps.a 指向的动态内存,并将 ps.top 和 ps.capacity 置 0,但 ps 本身作为局部变量在 test 函数返回后会自动销毁,其生命周期结束,所以不需要将 ps 置为 NULL。

  • 测试结果:

执行销毁栈函数之前:

执行销毁栈函数之后:

打印的结果:

二、队列

2.1 队列的基本概念

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。

队头(Front):允许删除的一端,又称队首。

队尾(Rear):允许插入的一端。

空队列:不包含任何元素的空表。

2.2 队列的代码实现

2.2.1 定义结构体成员以及方法

代码如下:

cpp 复制代码
#pragma once

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

typedef int QDatatype;

typedef struct QueueNode
{
	QDatatype data;
	struct QueueNode* next;
}QNode;

typedef struct Queue
{
    //为了确定单链表的头,后面简称:队头
	QNode* head;
    //为了确定单链表的尾,后面简称:队尾
	QNode* tail;
	int size;
}Queue;

//初始化
void QueueInit(Queue* pq);
//销毁队列
void QueueDestroy(Queue* pq);
//插入数据
void QueuePush(Queue* pq, QDatatype x);
//弹出队头元素
void QueuePop(Queue* pq);
//查看当前有效长度
int QueueSize(Queue* pq);
//判断队列是否存入数据
bool QueueEmpty(Queue* pq);
//查看队头元素
QDatatype QueueFront(Queue* pq);
//查看队尾元素
QDatatype QueueBack(Queue* pq);
  • 为啥使用单链表?而不使用双链表或数组?

理由如下:

因为队列只需要在队尾插入,在队头删除,单链表正好满足这个需求。

使用单链表的原因:

  1. 时间复杂度最优:单链表有 head 和 tail 指针,入队在 tail 后插入(O(1)),出队在 head 后删除(O(1)),效率最高。
  2. 不需要双向:队列不需要从后往前遍历或删除尾部节点,所以不需要 `prev` 指针,单链表更节省内存。
  3. 动态扩展方便:链表节点按需分配,无需像数组那样考虑扩容问题。
  4. 结构简单:单链表实现比双链表或循环链表更简洁。
  • 为啥要定义两个结构体?

理由如下:

因为一个结构体只能描述单一对象,而队列需要两种不同的对象:

  1. 节点结构体(QNode):描述每个元素本身,包含数据和指向下一个节点的指针。
  2. 队列结构体(Queue):描述整个队列的管理信息,包含头指针、尾指针和大小。
  3. 如果只用一个结构体,要么只能表示单个节点(无法管理整条链),要么只能表示队列头尾(无法存储每个元素的数据和链接),无法同时满足两个需求。所以必须用两个结构体。

2.2.2 初始化队列

代码如下:

cpp 复制代码
void QueueInit(Queue* pq)
{
	assert(pq);
    //因为此时没有元素插入,就将它们指向 NULL 即可
	pq->head = pq->tail = NULL;
	pq->size = 0;
}

2.2.3 销毁队列

代码如下:

cpp 复制代码
void QueueDestroy(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->head;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
    //因为 free(cur) 释放的是每个节点指向的动态内存,
    //但 pq->head 和 pq->tail 这两个指针变量本身存储的地址值并没有被清空,
    //它们仍然指向已经被释放的内存地址。将 pq->head 和 pq->tail 置为 NULL,
    //是为了防止后续误用这两个野指针。
	pq->head = pq->tail = NULL;
	pq->size = 0;
}

2.2.4 插入数据

代码如下:

cpp 复制代码
void QueuePush(Queue* pq, QDatatype x)
{
	assert(pq);
    //将 x 转换为单链表节点
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc error");
		return;
	}

	newnode->data = x;
	newnode->next = NULL;

    //判断是否为第一个元素插入
	if (pq->head == NULL)
	{
        //是的话,就将队头和队尾指向这个节点
		assert(pq->tail == NULL);
		pq->head = pq->tail = newnode;
	}
	else
	{
        //不是的话,就只需要移动队尾就可以了,队头不需要动
		pq->tail->next = newnode;
		pq->tail = newnode;
	}
    //然后有效长度 +1
	pq->size++;
}

2.2.5 弹出队头元素

代码如下:

cpp 复制代码
void QueuePop(Queue* pq)
{
	assert(pq);
    //这一步判断是防止 pq 这个单链表里面有没有存入数据
	assert(pq->head != NULL);

    //看是否只有队头一个元素
	if (pq->head->next == NULL)
	{
        //如果是,就释放队头元素的内存空间
		free(pq->head);
        //将它俩置为 NULL,防止后面变成野指针
		pq->head = pq->tail = NULL;
	}
	else
	{
        //如果不是
        //就查看队头的下一个元素,将它记录一下
		QNode* next = pq->head->next;
        //然后释放队头元素的内存空间
		free(pq->head);
        //将新队头指向到原来的队头的下一个元素
		pq->head = next;
	}
    //有效长度 -1
	pq->size--;
}

2.2.6 查看当前有效长度

代码如下:

cpp 复制代码
int QueueSize(Queue* pq) 
{
	assert(pq);
	return pq->size;
}

2.2.7 判断队列是否存入数据

代码如下:

cpp 复制代码
bool QueueEmpty(Queue* pq)
{
	assert(pq);
    //有效长度等于0,即为队列里面没有数据
	return pq->size == 0;
}

2.2.8 查看队头元素

代码如下:

cpp 复制代码
QDatatype QueueFront(Queue* pq)
{
	assert(pq);
    //这一步判断是防止 pq 这个单链表里面有没有存入数据
	assert(!QueueEmpty(pq));
    //然后就直接返回 队头 元素就好了
	return pq->head->data;
}

2.2.9 查看队尾元素

代码如下:

cpp 复制代码
QDatatype QueueBack(Queue* pq)
{
	assert(pq);
    //这一步判断是防止 pq 这个单链表里面有没有存入数据
	assert(!QueueEmpty(pq));
    //然后直接返回队尾元素就好了
    //所以这就是定义两个结构体的好处,
    //不然你取队尾元素的话,还需要遍历链表才能取到最后一个节点。
	return pq->tail->data;
}

2.2.10 测试

  • 测试代码:
cpp 复制代码
#include "Queue.h"

void test(Queue* pq)
{
    QueueInit(pq);
    QueuePush(pq, 1);
    QueuePush(pq, 2);
    QueuePush(pq, 3);
    QueuePush(pq, 4);
    QueuePush(pq, 5);
    QueuePop(pq);
    int length = QueueSize(pq);
    QDatatype front =  QueueFront(pq);
    QDatatype tail = QueueBack(pq);
    printf("队列长度为:%d ,队头元素是:%d,队尾元素是:%d", length,front, tail);
    QueueDestroy(pq);

}

int main()
{
    Queue node;
    test(&node);
    return 0;
}
  • 测试结果:

执行销毁队列函数之前:

执行销毁队列函数之后:

三、总结

以上便是我对栈和队列部分的全部理解了。有啥不好的地方也欢迎大家积极指出,希望大家早日被自己喜欢的offer录取。那我们就下一个博客见。

相关推荐
热爱前端的小张1 小时前
第六章 图
数据结构
hold?fish:palm2 小时前
11 滑动窗口最大值
数据结构·算法
xvhao20132 小时前
T750414 【游戏】计算24点 题解
数据结构·c++·算法·游戏
自然常数e3 小时前
顺序表专题
c语言·数据结构·visual studio
小七在进步4 小时前
数据结构:树的遍历
数据结构
j7~4 小时前
【数据结构初阶】队列的实现(链式队列 + 循环队列)--详解
c语言·开发语言·数据结构·学习·队列·queue·c\c++
king_linlin4 小时前
算法基础——算法复杂度
c语言·开发语言·数据结构·算法
AndyHeee5 小时前
Linux 内核底层机制:设备树(Device Tree)的数据结构与内存组织
linux·前端·数据结构
king_linlin5 小时前
数据结构——顺序表(附图文讲解|超详细)
c语言·数据结构·算法