数据结构:栈

目录

前言

一、栈概念与结构

栈概念

特性:

栈结构:

二、栈的实现

1.栈的初始化:

2.栈的销毁:

3.栈的入栈:

4.栈的出栈:

5.取栈顶元素:

6.获取栈中元素个数:

7.打印栈内元素:

三、栈测试代码

四、栈的相关例题:

[20. 有效的括号 - 力扣(LeetCode)](#20. 有效的括号 - 力扣(LeetCode))

解题思路

总结

前言

本篇文章将讲解栈的概念和结构,栈的实现,栈的题等知识的相关内容,本章代码知识,与顺序表相关,所以如果还没看过顺序表文章,可以看看:

https://blog.csdn.net/2401_86982201/article/details/153912651?spm=1011.2415.3001.10575&sharefrom=mp_manage_link

一、栈概念与结构

栈概念

栈是一种线性数据结构 ,核心特性为后进先出(LIFO,Last In First Out),所有操作仅允许在栈顶进行。其高效性(基本操作时间复杂度O(1))使其成为算法设计与系统开发的基础工具。

特性:

  • 栈顶(Top):唯一允许插入和删除元素的一端,动态指向最新元素。
  • 栈底(Bottom):固定端,最早入栈的元素存放于此。
  • 基本操作
    • 入栈(Push):在栈顶添加元素。
    • 出栈(Pop):移除并返回栈顶元素。
    • 查看栈顶(Peek/Top):返回栈顶元素但不删除。
    • 判空(IsEmpty):检查栈是否无元素。
    • 判满(IsFull):仅数组实现栈时需判断容量是否用尽。

它具有后进先出的性质:

入栈:栈的插⼊操作叫做进栈/压栈/⼊栈,⼊数据在栈顶。

出栈:栈的删除操作叫做出栈。出数据也在栈顶。

入栈:

出栈

简单来说:

我们可以认为:栈是一个存放东西的罐子,罐子是封闭的,存取物品都在罐子顶部。

图示:

栈结构:

栈的实现可以通过数组或链表来实现栈的代码结构。

但栈的操作都是在栈顶,如果我们选取链表,我们每次操作都需要在链表的尾部操作,即每次都要遍历到链表的尾部O(n),那样的耗费较大,而数组作为随机存储结构,访问值通过下标,时间复杂度为O(1),更适合做为栈的结构。

cpp 复制代码
typedef int type;
typedef struct Stack
{
	type* date;
	int size;
	int capacity;
}Stack;

二、栈的实现

1.栈的初始化:

初始化是栈使用前的必要步骤,需完成内存分配、初始状态定义,由于我们使用的为顺序结构,所以实现与顺序表的大致相同:

void STInit(Stack* h);

代码:

cpp 复制代码
void STInit(Stack* h)
{
	h->date = NULL;
	h->capacity = h->size = 0;
}

本函数为动态数组实现的栈(可自动扩容) 的初始化函数,核心目标是将栈初始化为空状态,为后续动态内存分配奠定基础。

cpp 复制代码
void STInit(Stack* h) {  
    h->date = NULL;       // 初始化为空指针,不分配内存  
    h->capacity = 0;      // 初始容量为0  
    h->size = 0;          // 初始元素个数为0  
}  

2.栈的销毁:

栈的销毁核心是释放已分配的内存空间,避免内存泄漏,它的实现与顺序表的相同。

void STDestory(Stack* h)

代码:

cpp 复制代码
void STDestory(Stack* h)
{
	if (h->date)
	{
		free(h->date);
	}
	h->date = NULL;
	h->size = h->capacity = 0;
}

讲解:

cpp 复制代码
void StackDestroy(Stack* ps) {
    if (ps == NULL) return;
    free(ps->data);       // 释放数组内存
    ps->data = NULL;      // 避免野指针
   h->size = h->capacity = 0;     // 重置容量
}

3.栈的入栈:

入栈(Push)是栈的核心操作之一,遵循"后进先出(LIFO)"原则,需根据存储结构(顺序栈)实现,同时处理容量不足等边界情况。简单来说,其操作可以看成顺序表的尾部插入。

void STPush(Stack* ps, type x)

前面曾说,其操作可以看成顺序表的尾部插入,我们通过顺序表结构可以得知在操作过程中,需要有扩容之处:

所以,需要写出扩容函数:

bool kuo(Stack* h);

代码:

cpp 复制代码
bool kuo(Stack* h)
{
	if (h->capacity == h->size)
	{
		int newcapacity = h->capacity == 0 ? 4 : 2 * h->capacity;
		type* p = (type*)realloc(h->date, newcapacity * sizeof(type));
		if (p)
		{
			h->date = p;
			h->capacity = newcapacity;
		}
		else
		{
			printf("扩容失败\n");
			return false;
		}
	}
	return true;
}

解释:

本函数是栈(Stack)的动态扩容函数,当栈的元素数量(size)达到当前容量(capacity)时触发,通过 realloc 调整内存空间,避免栈溢出。

通过实现了扩容函数,我们接下来,可以实现入栈函数了:

void STPush(Stack* ps, type x)

代码:

cpp 复制代码
bool kuo(Stack* h)
{
	if (h->capacity == h->size)
	{
		int newcapacity = h->capacity == 0 ? 4 : 2 * h->capacity;
		type* p = (type*)realloc(h->date, newcapacity * sizeof(type));
		if (p)
		{
			h->date = p;
			h->capacity = newcapacity;
		}
		else
		{
			printf("扩容失败\n");
			return false;
		}
	}
	return true;
}
void STPush(Stack* h, type x)
{   assert(h);
	if (!kuo(h)) 
	{
		printf("入栈失败:扩容失败,无法添加元素\n");
		return;
	}
	h->date[h->size++] = x;
}

讲解:本代码与顺序表中的尾插入一样,所以不过多讲解:

cpp 复制代码
bool kuo(Stack* h) {  // h 是指向栈结构体的指针,返回 bool 表示扩容是否成功
    if (h->capacity == h->size) {  // 1. 判断是否需要扩容:容量 == 当前元素数
        // 2. 计算新容量:若初始容量为 0(空栈),则扩容至 4;否则翻倍
        int newcapacity = h->capacity == 0 ? 4 : 2 * h->capacity;  
        
        // 3. 申请新内存:用 realloc 调整数据数组大小(h->date 是原数组地址)
        type* p = (type*)realloc(h->date, newcapacity * sizeof(type));  
        
        if (p) {  // 4. 若内存申请成功(p 非 NULL)
            h->date = p;          // 更新栈的数组指针为新地址
            h->capacity = newcapacity;  // 更新容量为新值
        } else {  // 5. 若内存申请失败(如堆空间不足)
            printf("扩容失败\n");  // 打印错误信息
            return false;  // 返回 false 表示扩容失败
        }
    }
    return true;  // 无需扩容或扩容成功,返回 true
}
cpp 复制代码
void STPush(Stack* h, type x) {  // h 是栈指针,x 是待入栈元素
    // 1. 先调用扩容函数:若扩容失败(返回 false),则入栈失败
    assert(h);  //断言:若 h 为 NULL,程序直接终止并报错
    if (!kuo(h)) {  
        printf("入栈失败:扩容失败,无法添加元素\n");  // 提示错误
        return;  // 直接返回,不再执行后续入栈
    }
    // 2. 若扩容成功(或无需扩容),则将 x 存入数组末尾,并更新元素数(size++)
    h->date[h->size++] = x;  
}

4.栈的出栈:

出栈即为删去栈顶的数据,若栈不为空,就直接ps->size--就可以了,不需要其它多余的操作,实现起来很简单的:

函数:

void STPop(Stack* h);

代码实现:

cpp 复制代码
void STPop(Stack* h)
{  assert(h);
	if (h->size)
	{
		h->size--;
}

}

讲解:

移除栈顶元素(通过减少 size 实现,不直接删除内存数据)。

cpp 复制代码
void STPop(Stack* h) {  // h 是指向栈结构体的指针,无返回值
assert(h);   //断言:若 h 为 NULL,程序直接终止并报错
 if (h->size) {  // 1. 判断栈是否非空:若 size > 0(栈内有元素)
        h->size--;  // 2. 栈顶元素索引 = size - 1,size-- 即"逻辑删除"栈顶元素
    }
}

5.取栈顶元素:

由于使用顺序结构顺序表来实现的栈的结构,所以栈访问值的时间复杂度与顺序表是一样的,O(1),直接通过下标访问:

函数:

void STPop(Stack* h);

代码实现:

cpp 复制代码
type STTop(Stack* h)
{
	assert(h);
	return (h->size > 0) ? h->date[h->size - 1] : '\0';
}

讲解:

cpp 复制代码
type STTop(Stack* h) {  // type 为栈元素类型(如 char、int)
    assert(h);  // 1. 断言:确保 h 非空(避免空指针解引用)
    // 2. 条件表达式:栈非空返回栈顶元素,空栈返回 '\0'
    return (h->size > 0) ? h->date[h->size - 1] : '\0';  
}

通过使用三目表达式来实现的。

6.获取栈中元素个数:

功能:返回栈中元素个数.

cpp 复制代码
int STSize(Stack* h)
{
	assert(h);
	return h->size;
}

本功能很简单,直接返回size值就是有效元素个数。

7.打印栈内元素:

因为type我默认为int类型,所以打印为:

cpp 复制代码
void STPrint(Stack* h)
{
	assert(h);
	int i;
	for (i = 0; i < h->size; i++)
	{
		printf("%d  ", h->date[i]);
	}
	printf("\n");
}

三、栈测试代码

接下来,我将展示本次所写的代码以及对应的测试代码:

本代码通过三个文件实现:

1.h

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int type;
typedef struct Stack
{
	type* date;
	int size;
	int capacity;
}Stack;
void STInit(Stack* h);
void STDestory(Stack* h);
void STPush(Stack* h, type x);
bool kuo(Stack* h);
void STPop(Stack* h);
type STTop(Stack* h);
int STSize(Stack* h);
void STPrint(Stack* h);

1.c

cpp 复制代码
#include"1.h"
void STInit(Stack* h)
{
	h->date = NULL;
	h->capacity = h->size = 0;
}
void STDestory(Stack* h)
{
	if (h->date)
	{
		free(h->date);
	}
	h->date = NULL;
	h->size = h->capacity = 0;
}
bool kuo(Stack* h)
{
	if (h->capacity == h->size)
	{
		int newcapacity = h->capacity == 0 ? 4 : 2 * h->capacity;
		type* p = (type*)realloc(h->date, newcapacity * sizeof(type));
		if (p)
		{
			h->date = p;
			h->capacity = newcapacity;
		}
		else
		{
			printf("扩容失败\n");
			return false;
		}
	}
	return true;
}
void STPush(Stack* h, type x)
{
	assert(h);
	if (!kuo(h)) 
	{
		printf("入栈失败:扩容失败,无法添加元素\n");
		return;
	}
	h->date[h->size++] = x;
}
void STPop(Stack* h)
{
	assert(h);
	if (h->size)
	{
		h->size--;
}
}
type STTop(Stack* h)
{
	assert(h);
	if (h->size)
	{
		return h->date[h->size - 1];
	}
}
int STSize(Stack* h)
{
	assert(h);
	return h->size;
}
void STPrint(Stack* h)
{
	assert(h);
	int i;
	for (i = 0; i < h->size; i++)
	{
		printf("%d  ", h->date[i]);
	}
	printf("\n");
}

main.c

cpp 复制代码
#include"1.h"
void test()
{
	Stack h;
	STInit(&h);
	STPush(&h, 10);        //10
	STPrint(&h);
	STPush(&h, 20);        //10  20
	STPrint(&h);
	STPush(&h, 30);        //10  20  30
	STPrint(&h);
	STPop(&h);              //10  20
	STPrint(&h);
	STPop(&h);             //10
	STPrint(&h);    
	printf("%d", STSize(&h));     //1
	STDestory(&h);
}
int main()
{
	test();
}

结果:

四、栈的相关例题:

20. 有效的括号 - 力扣(LeetCode)

括号

根据题的意思,我们可得知:

判断字符串是否有效,核心在于验证括号的匹配顺序类型对应 ,可通过实现高效验证。

解题思路
  1. 栈存储左括号 :遍历字符串,遇到左括号(({[)时入栈。
  2. 右括号匹配检查:遇到右括号时,若栈为空(无对应左括号)或栈顶左括号类型不匹配,则字符串无效;否则弹出栈顶左括号(匹配成功)。
  3. 最终栈为空:遍历结束后,栈若为空表示所有括号均匹配,否则无效。

图例:初始状态:

第一个字符:

第二个字符:

第三个字符:

第四个字符:

栈为空,为true。

代码实现:

cpp 复制代码
typedef char type;
typedef struct Stack
{
	type* date;
	int size;
	int capacity;
}Stack;
void STInit(Stack* h)
{
	h->date = NULL;
	h->capacity = h->size = 0;
}
void STDestory(Stack* h)
{
	if (h->date)
	{
		free(h->date);
	}
	h->date = NULL;
	h->size = h->capacity = 0;
}
bool kuo(Stack* h)
{
	if (h->capacity == h->size)
	{
		int newcapacity = h->capacity == 0 ? 4 : 2 * h->capacity;
		type* p = (type*)realloc(h->date, newcapacity * sizeof(type));
		if (p)
		{
			h->date = p;
			h->capacity = newcapacity;
		}
		else
		{
			printf("扩容失败\n");
			return false;
		}
	}
	return true;
}
void STPush(Stack* h, type x)
{
	assert(h);
	if (!kuo(h)) 
	{
		printf("入栈失败:扩容失败,无法添加元素\n");
		return;
	}
	h->date[h->size++] = x;
}
void STPop(Stack* h)
{
	assert(h);
	if (h->size)
	{
		h->size--;
}
}
type STTop(Stack* h)
{
	assert(h);
	return (h->size > 0) ? h->date[h->size - 1] : '\0';
}
bool isValid(char* s)
{  Stack h;
STInit(&h);
int i=0;
while(s[i])
{
    if(s[i]=='('||s[i]=='['||s[i]=='{')
    {
        STPush(&h,s[i]);
    }
    else
    {   if(h.size==0)
    {
        return false;
    }
        char t=STTop(&h);
        if((s[i]==')'&&t!='(')||(s[i]==']'&&t!='[')||(s[i]=='}'&&t!='{'))
        {
            return false;
        }
        STPop(&h);
    }
    i++;
}
if(h.size==0)
{
    return true;
}
 return false;   
}  

由于题中,只给了函数的代码,我们想使用栈,需要复用栈的代码(做题倒也不是一定这么麻烦,到C++部分,我们是可以直接使用栈所对应的容器来实现我们的目的的):

先介绍下函数:

|-------------|-------------------------------------------------|
| STPush | 入栈:先扩容(若需),再将元素添加到栈顶(size++)。 |
| STPop | 出栈:仅修改 size--(不释放内存,下次入栈可覆盖)。 |
| STTop | 获取栈顶元素:栈非空返回 date[size-1],空栈返回 '\0'(此处未用到)。 |
| STDestroy | 销毁栈:释放动态数组内存,重置 datesizecapacity。 |
| kuo | 扩容:容量不足时,按 2倍 扩容(初始容量为4),失败返回 false。 |

核心思想:利用栈的"先进后出"特性,左括号入栈,右括号与栈顶左括号匹配。

  1. 初始化栈 :创建 Stack h 并调用 STInit(&h) 初始化。
  2. 遍历字符串 :循环处理每个字符 s[i]
    • 情况1:遇到左括号(( [ {
      • 直接入栈(STPush(&h, s[i])),等待后续右括号匹配。
    • 情况2:遇到右括号() ] }
      • 检查栈空 :若栈为空(h.size == 0),说明右括号无匹配的左括号,返回 false
      • 匹配栈顶左括号
        • 获取栈顶元素 t = STTop(&h)(左括号)。
        • 若右括号与 t 不匹配(如 ')' 对应 '['),返回 false
        • 若匹配,出栈(STPop(&h)),继续检查下一个字符。
  3. 遍历结束后检查栈空
    • 若栈为空(h.size == 0),所有左括号均匹配,返回 true
    • 若栈非空,存在未匹配的左括号,返回 false

以文字形式,演示:

(以 s = "({})" 为例)

步骤 字符 操作 栈内元素(size) 结果
1 ( 入栈 ['('](1) -
2 { 入栈 ['(', '{'](2) -
3 } 右括号,匹配栈顶 { ['('](1) 匹配成功
4 ) 右括号,匹配栈顶 ( [](0) 匹配成功
5 结束 栈空,返回 true - 有效

需要注意点:

  1. 如果刚开始就是右侧括号,栈此时无值,要写检查代码检查。

总结

以上就是今天要讲的内容,本篇文章涉及的知识点为:栈的概念和结构,栈的实现,栈的题知识的相关内容 的相关内容,为本章节知识的内容,希望大家能喜欢我的文章,谢谢各位,下篇文章,我将会讲解数据结构:**队列,**接下来的内容我会很快更新。

相关推荐
Bona Sun1 小时前
单片机手搓掌上游戏机(十九)—pico运行doom之硬件连接
c语言·c++·单片机·游戏机
故事不长丨1 小时前
解锁C#编程秘籍:封装、继承、多态深度剖析
开发语言·数据库·c#
远瞻。1 小时前
【环境配置】快速转移conda上的python环境
开发语言·python·conda
缘三水1 小时前
【C语言】5.printf和scanf(新手向详细版)
c语言·开发语言·基础语法
无敌最俊朗@1 小时前
Qt处理tcp数据 粘包 拆包 的简单方法
开发语言
亭上秋和景清1 小时前
数据在内存中的存储
java·开发语言
小二·1 小时前
Java基础教程之网络编程
java·开发语言·网络
乾元1 小时前
多厂商配置对齐器:AI 如何在 Cisco / Huawei / Juniper 间做语义映射
运维·开发语言·网络·人工智能·网络协议·华为·智能路由器
熊文豪1 小时前
使用Python快速开发一个MCP服务器
服务器·开发语言·python·mcp