【初阶数据结构】栈与队列:定义、核心操作与代码解析

【往期数据结构回顾】:

/-----------初阶数据结构-----------/

【 时间复杂度+空间复杂度 】

【 顺序表 】

【 单链表 】

【 链表OJ题(上篇)】

【 链表OJ题(下篇)】


栈和队列实现目录:

一、概念:两种"秩序"的本质区别

[1. 栈](#1. 栈)

[2. 队列](#2. 队列)

二、栈的实现

[1. 数组实现(顺序栈)](#1. 数组实现(顺序栈))

【顺序栈优缺点】

[2. 链表实现(链栈)](#2. 链表实现(链栈))

【链式栈优缺点】

三、队列的实现

1、循环队列

2、链式队列

四、总结


在数据结构的学习中,栈和队列是绕不开的基础线性结构。它们本质上都是对数组或链表的"规则化封装"------通过限制数据的存取方式,满足不同场景的需求。栈遵循"先进后出(LIFO)",像弹匣一样只能从顶端操作;队列遵循"先进先出(FIFO)",像排队办事一样有序流转。

一、概念:两种"秩序"的本质区别

栈和队列的核心差异在于数据操作的顺序规则,这一规则直接决定了它们的应用场景和实现逻辑。

1. 栈

栈(Stack)遵循后进先出(Last In First Out, LIFO) 原则,就像我们日常叠放的盘子------最后放上去的盘子,必须最先拿下来。这种结构对数据操作施加了严格限制:所有插入和删除操作都只能在栈的顶端(栈顶)完成,不支持随机访问中间元素。

生活中的"撤销操作"、程序中的函数调用栈,都是栈结构的典型体现:最新的操作或调用始终处于"栈顶",需要时优先被处理。

2. 队列

队列(Queue)遵循先进先出(First In First Out, FIFO) 原则,类似银行柜台前排队的人群------最先排队的人,最先办理业务。与栈不同,队列的操作分为两端:只能在队列的尾部(队尾)插入元素,在队列的头部(队头)删除元素,同样不支持随机访问。

操作系统的进程调度、打印机的任务队列,都依赖队列的"顺序处理"特性,确保任务按提交顺序公平执行。

二、栈的实现

栈的实现有两种经典方案:数组实现(顺序栈)和链表实现(链栈)。两者各有优劣,需根据实际场景选择------数组实现效率高但需预分配空间,链表实现动态扩容但有指针开销。

1. 数组实现(顺序栈)

数组实现的核心是用一个数组(定长的静态栈的结构,实际中一般不实用,所以我们只实现支持动态增长的栈 )存储元素,并通过一个"栈顶指针"(通常是整数索引)标记栈顶位置。初始时栈顶指针为-1或者0(表示栈空),如果是-1代表top始终指向栈顶元素,如果是0表示top始终指向栈顶元素的下一个位置,压栈时指针加1,弹栈时指针减1。

由于这两个数据结构的实现都很简单,我就不分开实现接口了,具体的我在代码里注释

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

// 栈存储的数据类型
typedef int STDataType;

// 栈(基于动态数组实现)
typedef struct stack
{
	STDataType* a;      // 动态数组,存储栈元素
	int top;            // 栈顶指针:指向栈顶元素的下一个位置
	int capacity;       // 当前栈的最大容量
}stack;

// 栈的初始化
void STInit(stack* ps)
{
    // 避免对空指针进行解引用
	assert(ps);
    
    // 为数组开辟初始容量(4个元素)的空间
	ps->a =(STDataType* ) malloc(sizeof(STDataType)*4);
	
    // 内存开辟失败时,打印错误并返回
    if (ps->a == NULL)
	{
		perror("malloc");
		return;
	}
    
	ps->capacity = 4;          // 初始化栈容量为4
	ps->top = 0;               // 初始时栈空,top指向栈顶下一个位置
	//ps->top = -1;// 若需改为"指向栈顶元素",可启用此注释
}

// 栈的销毁
void STDestroy(stack* ps)
{
	assert(ps);         // 避免空指针操作
	free(ps->a);        // 释放动态数组的内存
	ps->a = NULL;       // 指针置空,防止野指针
	ps->capacity = 0;   // 容量重置为0
	ps->top = 0;        // top重置为初始状态
}

// 入栈(压栈)
void STPush(stack* ps, STDataType x)
{
    // 避免空指针操作
	assert(ps);        

	// 检查栈是否已满,满则扩容为原容量的2倍
	if (ps->capacity == ps->top)
	{
		STDataType* tmp =(STDataType*)realloc(ps->a, sizeof(STDataType)*2*ps->capacity );
		
        // 扩容失败时,打印错误并返回
        if (tmp == NULL)  
		{
			perror("realloc");
			return;
		}

		ps->a = tmp;   // 更新数组指针
		ps->capacity = ps->capacity * 2;  // 更新容量
	}

	ps->a[ps->top++] = x;  // 元素存入当前top位置,随后top后移
}


// 出栈(弹栈)
void STPop(stack* ps)
{
	assert(ps);                 // 避免空指针操作
	assert(!STEmpty(ps));       // 栈空时禁止出栈
	ps->top--;                  // top前移,实现出栈(逻辑删除)
}


// 获取栈中有效元素个数
int STSize(stack* ps)
{
	assert(ps);         // 避免空指针操作
	return ps->top;     // top的值等于元素个数(因top指向栈顶下一个位置)
}

// 判断栈是否为空
bool STEmpty(stack* ps)
{
	assert(ps);         // 避免空指针操作
	return ps->top == 0;// top为0时栈空
}

// 获取栈顶元素
STDataType STTop(stack* ps)
{
	assert(ps);                 // 避免空指针操作
	assert(!STEmpty(ps));       // 栈空时无栈顶元素
	return ps->a[ps->top - 1];  // 栈顶元素在top-1位置
}

【顺序栈优缺点】

优点

  • 内存连续,缓存友好,访问速度快;
  • 核心操作(入 / 出栈、查栈顶)均为 O (1);

缺点

  • 扩容依赖连续内存空间,异地扩容需拷贝数据,存在瞬时时间开销(原地扩容无此消耗);
  • 扩容后若实际元素远少于容量(如容量 100 仅用 55 个),易造成闲置内存浪费。

2. 链表实现(链栈)

链表实现的核心是用链表节点存储元素,以链表的头部作为栈顶(便于插入和删除)。无需预分配空间,元素个数可动态增减,从根本上避免了栈溢出问题。

cpp 复制代码
// 链栈的定义 
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include<assert.h>

// 链表节点结构 
typedef struct StackNode 
{
    int data;                  // 节点存储的数据
    struct StackNode *next;    // 指向下一个节点的指针(链表特性)
} StackNode;


// 链栈结构:通过栈顶指针和大小维护整个栈
typedef struct 
{
    StackNode *top;  // 栈顶指针(链栈核心:所有操作围绕栈顶),NULL表示栈空
    int size;        // 栈的元素个数(快速获取大小,避免遍历)
} LinkStack;


// 1. 初始化栈:将栈置为空状态
void StackInit(LinkStack *stack) 
{
    stack->top = NULL;  // 栈顶置空表示空栈
    stack->size = 0;    // 大小初始化为0
}


// 2. 压栈操作(链表头插法):新节点成为新栈顶
bool StackPush(LinkStack *stack, int value)
{   
    assert(stack);  // 防止传入空指针
    // 创建新节点并分配内存
    StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));
    if (newNode == NULL)  // 内存分配失败处理
    {
        printf("内存分配失败,压栈失败!\n");
        return false;
    }
    // 新节点数据赋值
    newNode->data = value;
    // 关键:新节点指向原栈顶(头插法核心)
    newNode->next = stack->top;
    // 栈顶指针更新为新节点
    stack->top = newNode;
    stack->size++;  // 元素个数+1
    return true;
}


// 3. 弹栈操作(链表头删法):移除栈顶节点
bool StackPop(LinkStack *stack) 
{
    // 空栈判断(栈顶为NULL则无法弹栈)
    if (stack->top == NULL) 
    {
        printf("栈为空,弹栈失败!\n");
        return false;
    }
    // 保存原栈顶的下一个节点(防止链表断裂)
    StackNode* next = stack->top->next;
    // 释放原栈顶节点内存
    free(stack->top);
    // 栈顶指针更新为原栈顶的下一个节点
    stack->top = next;
    stack->size--;  // 元素个数-1
    return true;
}


// 4. 查看栈顶元素:仅读取不修改栈
bool StackPeek(LinkStack *stack, int *value) 
{
    // 空栈判断
    if (stack->top == NULL) 
    {
        printf("栈为空,无栈顶元素!\n");
        return false;
    }
    // 通过指针输出栈顶数据
    *value = stack->top->data;
    return true;
}


// 5. 判断栈是否为空:通过栈顶指针直接判断
bool StackIsEmpty(LinkStack *stack) 
{
    return stack->top == NULL;  // 栈顶为NULL即空栈
}


// 6. 获取栈的大小:直接返回维护的size(O(1)效率)
int StackGetSize(LinkStack *stack) 
{
    return stack->size;
}


// 销毁栈:释放所有节点内存(避免内存泄漏)
void StackDestroy(LinkStack *stack) 
{
    StackNode *temp;  // 临时指针保存待释放节点
    // 循环删除栈顶节点,直到栈空
    while (stack->top != NULL) 
    {
        temp = stack->top;          // 保存当前栈顶节点
        stack->top = temp->next;    // 栈顶后移
        free(temp);                 // 释放当前节点
    }
    stack->size = 0;  // 大小重置为0
}

【链式栈优缺点】

优点:

  • 压栈 / 出栈仅需修改节点指针指向(链表头插 / 头删),无需移动大量元素(数组栈扩容时可能需要拷贝数据)。
  • 无需扩容 / 溢出风险:内存动态分配(每个节点独立申请),只要系统有空闲内存就能持续压栈,不存在数组栈的扩容操作或容量限制

缺点:

  • 存在指针空间开销 :每个节点需额外存储next指针(比如 int 型数据占 4 字节,指针占 8 字节时,额外开销达 2 倍),内存利用率低于数组栈。
  • 缓存利用率低:节点内存分散存储,CPU 缓存命中率远低于数组栈的连续内存布局,访问效率受影响。
  • 实际操作效率略低 :压栈需动态分配节点(malloc)、出栈需释放节点(free),比数组栈的 "直接读写内存" 多了调用开销

三、队列的实现

队列的实现同样有数组和链表两种方式。数组实现时,普通队列会出现"假溢出"问题------队尾已满但队头有空闲空间,因此实际中更常用循环队列;链表实现则通过头尾指针轻松完成两端操作。

**假溢出:**普通数组队列里,队尾已经到数组末尾没法再存,但队头因为出队空了不少位置 ------ 看着像满了,其实还有空间能用,就是普通队列用不了这些空位置。

1、循环队列

循环队列的核心是利用"取模运算"让数组首尾相连,形成逻辑上的环形空间。通过两个指针(front指向队头,rear指向队尾的下一个位置)控制操作,同时可引入size变量记录元素个数,capacity变量记录容量大小,这种方式能让判空和判满逻辑更直观------判空条件为size == 0,判满条件为size == capacity,无需通过指针关系间接判断,也避免了预留空间的问题。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>



// 动态循环队列结构体
typedef struct 
{
    int* data;          // 存储队列元素的数组
    int front;          // 队头指针(指向队头元素)
    int rear;           // 队尾指针(指向队尾元素的下一个位置)
    int capacity;       // 当前队列容量
    int size;           // 当前队列元素个数
}CircularQueue;



// 初始化队列
CircularQueue* queueInit(CircularQueue* queue)
{
    // 为队列数据分配初始内存(4个int大小)
    queue->data = (int*)malloc(sizeof(int) * 4);
    if (queue->data == NULL) 
    {
        perror("Failed to allocate memory for queue data");
        exit(1); // 内存分配失败则终止程序
    }

    queue->front = 0;    // 队头指针:指向队列第一个元素的位置
    queue->rear = 0;     // 队尾指针:指向队列最后一个元素的下一个位置(便于入队操作)
    queue->capacity = 4; // 队列初始容量(最多可存储4个元素)
    queue->size = 0;     // 当前队列中的元素个数(初始为空)

    return queue;
}

// 扩容
void expandQueue(CircularQueue* queue) 
{
    if (queue == NULL) 
        return; 

    // 2倍扩容
    int* tmp = realloc(queue->data, sizeof(int) * queue->capacity * 2);
    if (tmp == NULL)
    {
        perror("realloc");
        exit(1); // 扩容失败则终止程序
    }
     
    queue->data = tmp; // 更新数据指针到新内存地址
   
    // 关键逻辑:处理环形队列的"折回"情况
    // 当首尾重合(队列满)时,
    // 扩容后队尾指针需偏移原容量大小,避免新数据覆盖旧数据
   
    queue->rear += queue->capacity; // 队尾指针移到新内存区域的对应位置
    
   
    queue->capacity *= 2; // 更新队列容量为翻倍后的值
}

// 判断队列是否为空:通过元素个数size是否为0判断(比指针比较更直观)
bool isEmpty(CircularQueue* queue) 
{
    return queue->size == 0;
}

// 判断队列是否已满:元素个数等于队列容量时即为满
bool isFull(CircularQueue* queue) 
{
    return queue->size == queue->capacity;
}

// 入队操作:将元素添加到队尾,队列满时自动扩容
// 参数queue为队列指针,value为要入队的元素值,成功返回true,失败返回false
bool enqueue(CircularQueue* queue, int value) 
{
    if (queue == NULL) 
        return false; // 空指针保护

    // 队列满时触发扩容(容量翻倍)
    if (isFull(queue)) 
    {
        expandQueue(queue);
    }

    queue->data[queue->rear] = value; // 将元素存入队尾指针指向的位置
    // 队尾指针循环后移:取模运算实现环形结构,指针到数组末尾后回到开头
    queue->rear = (queue->rear + 1) % queue->capacity;
    queue->size++; // 元素个数加1

    return true;
}

// 出队操作:移除队头元素(仅修改指针,不清理数据)
// 队列为空时直接终止程序(也可根据需求改为返回错误码)
void dequeue(CircularQueue* queue) 
{
    if (queue == NULL || isEmpty(queue)) 
    {  
        printf("Dequeue error: queue is NULL or empty\n");
        exit(1); // 空队列出队属于非法操作,终止程序
    }

    // 队头指针循环后移:指向下一个元素,实现队头元素的"移除"
    queue->front = (queue->front + 1) % queue->capacity;
    queue->size--; // 元素个数减1
}

// 获取队头元素值:返回当前队头指针指向的元素
// 队列为空时终止程序
int getFront(CircularQueue* queue) 
{
    if (queue == NULL || isEmpty(queue)) 
    {
        printf("Queue is empty\n");
        exit(1);
    }
    return queue->data[queue->front]; // 直接返回队头指针指向的元素
}

// 获取队列当前元素个数:直接返回size成员
int getSize(CircularQueue* queue) 
{
    return queue->size;
}

// 销毁队列:释放数据内存并重置所有成员,防止内存泄漏和野指针
void queueDestroy(CircularQueue* queue) 
{
    if (queue != NULL) 
    {
        free(queue->data);   // 释放数据数组的内存
        queue->data = NULL;  // 将指针置空,避免野指针访问
        queue->capacity = 0; // 重置容量为0
        queue->front = -1;   // 重置队头指针(非0值标记已销毁)
        queue->rear = 0;     // 重置队尾指针
        queue->size = 0;     // 重置元素个数
    }
}

// 打印队列所有元素:从队头开始遍历,按顺序输出所有元素
// 处理环形结构,确保遍历顺序正确
void printQueue(CircularQueue* queue) 
{
    if (isEmpty(queue)) 
    {
        printf("Queue is empty\n");
        return;
    }

    printf("Queue elements: ");
    // 遍历逻辑:从队头指针开始,依次取size个元素
    // (queue->front + i) % queue->capacity:实现环形遍历,
    // 即使队头不在数组开头,也能按顺序访问所有元素
    for (int i = 0; i < queue->size; i++) 
    {
        printf("%d ", queue->data[(queue->front + i) % queue->capacity]);
    }
    printf("\n");
}

循环队列解决了普通数组队列的"假溢出"问题,空间利用率高且核心操作时间复杂度均为O(1),但固定容量实现存在溢出风险,动态扩容时需处理元素拷贝,且相比链队列多了数组容量管理的成本。

2、链式队列

链队列的核心是用链表存储元素,维护两个指针:front指向队头节点,rear指向队尾节点。入队时在rear后插入新节点,出队时删除front指向的节点,操作简洁且无容量限制。

cpp 复制代码
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
typedef int QDataType;

// 链式结构:表示队列
typedef struct QListNode
{
	struct QListNode* _pNext;
	QDataType _data;
}QNode;

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

// 初始化队列:将队头、队尾指针置空,有效元素个数置0
void QueueInit(Queue* q)
{
    assert(q); // 防止传入空指针
    q->_front = q->_rear = NULL; // 空队列时头尾指针均指向NULL
    q->size = 0; // 初始元素个数为0
}

// 队尾入队列:创建新节点,处理空队列/非空队列两种情况
void QueuePush(Queue* q, QDataType data)
{
    assert(q);
    // 1. 创建新节点并初始化
    QNode* newNode = (QNode*)malloc(sizeof(QNode));
    if (newNode == NULL) // 内存分配失败处理
    {
        perror("malloc fail\n");
        return;
    }
    newNode->_data = data;
    newNode->_pNext = NULL; // 新节点作为队尾,next置空

    // 2. 处理空队列(首次入队)
    if (q->_front == NULL)
    {
        assert(q->_rear == NULL); // 空队列时头尾指针必须同时为空
        q->_front = q->_rear = newNode; // 头尾指针都指向新节点
    }
    // 3. 处理非空队列(正常入队)
    else
    {
        q->_rear->_pNext = newNode; // 原队尾节点指向新节点
        q->_rear = newNode; // 更新队尾指针为新节点
    }

    q->size++; // 有效元素个数+1
}

// 队头出队列:处理单个节点/多个节点两种情况,避免野指针
void QueuePop(Queue* q)
{
    assert(q);
    assert(!QueueEmpty(q)); // 防止空队列出队

    // 1. 处理队列中只有一个节点的情况
    if (q->_front->_pNext == NULL)
    {
        free(q->_front); // 释放唯一节点
        q->_front = q->_rear = NULL; // 重置头尾指针为空
    }
    // 2. 处理队列中有多个节点的情况
    else
    {
        QNode* next = q->_front->_pNext; // 保存队头的下一个节点
        free(q->_front); // 释放队头节点
        q->_front = next; // 更新队头指针为下一个节点
    }

    q->size--; // 有效元素个数-1
}

// 获取队列头部元素:直接返回队头节点的数据
QDataType QueueFront(Queue* q)
{
    assert(q);
    assert(!QueueEmpty(q)); // 空队列无头部元素
    return q->_front->_data;
}

// 获取队列队尾元素:直接返回队尾节点的数据
QDataType QueueBack(Queue* q)
{ 
    assert(q);
    assert(!QueueEmpty(q)); // 空队列无尾部元素
    return q->_rear->_data;
}

// 获取队列中有效元素个数:直接返回size成员
int QueueSize(Queue* q)
{
    assert(q);
    return q->size;
}

// 检测队列是否为空:通过size是否为0判断(也可通过_front是否为NULL)
int QueueEmpty(Queue* q)
{
    assert(q);
    return q->size == 0;
}

// 销毁队列:遍历释放所有节点,重置指针和size
void QueueDestroy(Queue* q)
{
    assert(q);
    QNode* cur = q->_front; // 从队头开始遍历
    while (cur) // 遍历所有节点
    {
        QNode* next = cur->_pNext; // 保存下一个节点(防止释放后找不到)
        free(cur); // 释放当前节点
        cur = next; // 移动到下一个节点
    }

    q->_front = q->_rear = NULL; // 重置头尾指针
    q->size = 0; // 重置元素个数
}

链队列的优点是动态扩容,无需处理溢出问题;缺点是节点指针带来额外空间开销,适合元素个数不确定的场景。

四、总结

栈和队列是数据结构的"入门基石",它们的核心价值并非复杂的结构,而是通过限制操作顺序,为问题提供清晰、高效的解决思路

  • 栈的LIFO特性,适合处理"需要回溯"或"逆序处理"的场景,所有操作聚焦栈顶,时间复杂度均为O(1)。

  • 队列的FIFO特性,适合处理"需要顺序执行"或"分层遍历"的场景,两端操作同样保证O(1)的高效性能。

  • 实现选择上,数组实现高效紧凑,链表实现动态灵活,需根据实际需求(容量确定性、空间开销)权衡。

理解栈和队列的本质,不仅能解决算法问题,更能在程序设计中建立"秩序化"的思维,让数据操作更可控、更高效。

相关推荐
济宁雪人40 分钟前
Java安全基础——序列化/反序列化
java·开发语言
q***017741 分钟前
Java进阶--IO流
java·开发语言
lsx20240641 分钟前
C语言中的枚举(enum)
开发语言
csbysj20201 小时前
PHP Math
开发语言
小画家~1 小时前
第三十四:golang 原生 pgsql 对应操作
android·开发语言·golang
ulias2121 小时前
初步了解STL和string
开发语言·c++·mfc
二川bro1 小时前
字符串格式化进阶:Python f-string性能优化
开发语言·python
C语言不精1 小时前
一种在 ESP32-S3 上取巧的清晰度检测方案
c语言·stm32·嵌入式硬件·学习
LitchiCheng1 小时前
Mujoco 机械臂 OMPL 进行 RRT 关节空间路径规划避障、绕障
开发语言·人工智能·python