初级数据结构(三)——栈

这篇具有很好参考价值的文章主要介绍了初级数据结构(三)——栈。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

  文中代码源文件已上传:数据结构源码

<-上一篇 初级数据结构(二)——链表        |        初级数据结构(四)——队列 下一篇->

1、栈的特性

1.1、函数栈帧简述

        即使是刚入门几天的小白,对栈这个字也应该略有耳闻。在操作系统层面,栈是系统在内存中划分的一整块连续的地址范围。并且系统对于单个程序在栈区的空间使用也是连续的。以一段代码举例:

void FunctionInside()
{
    /* ... */
}

void Function_1()
{
    /* ... */
}

void Function_2()
{
    /* ... */
    FunctionInside();
    /* ... */
}

int main()
{
    Function_1();
    Function_2();
    return 0;
}

        在运行上述代码时,首先调用 main 函数,系统将为 main 函数在栈上单独开辟一块空间供 main 函数使用,这块空间称作 main 函数的栈帧。而当在 main 中调用 Function_1 时,系统又在 main 函数栈帧之上为 Function_1 开辟一块  Function_1 的栈帧。而随着 Function_1 函数调用结束, Function_1 的栈帧也被销毁,该栈帧的空间归还给操作系统。

        而进入 Function_2 函数时,系统同样为 Function_2 开辟一块栈帧。如果如上述代码中的  Function_1 和 Function_2 是连续调用的话, Function_2 栈帧很可能覆盖之前 Function_1 被销毁的栈帧空间。此时进入 Function_2 函数内部,又在内部调用 FunctionInside 函数。此时,系统将在 Function_2 栈帧之上为 FunctionInside 创建栈帧。

        随着 FunctionInside 调用结束 FunctionInside 栈帧也随之归还,今儿回到 Function_2 的栈帧空间。当然, Function_2 调用结束后, Function_2 的栈帧也将归还,并回到 main 函数的栈帧空间。最后随着程序结束,main 函数的栈帧也随之销毁。

初级数据结构(三)——栈,C语言,数据结构与算法,数据结构,链表,c语言,算法

         或许有点绕。但看上图也不难发现其规律,这就是栈的特性:后入先出、先入后出,也称为 LIFO ( Last In First Out )。

1.2、栈结构

        基于某些需求(如通过程序判断记录数学公式字符串的大中小括号是否配对、计算字符串中的数学公式的值、甚至最基本的数组倒序等),程序的数据处理需要用到后入先出的特性,对这类数据进行操作的结构就称为栈结构。

初级数据结构(三)——栈,C语言,数据结构与算法,数据结构,链表,c语言,算法

        栈结构的实现仍然是通过顺序表或者链表实现,只是在存取数据时必须遵循 LIFO 的规则。并且,栈结构不存在改和查的操作。如果要,必须将尾部数据至需要修改的数据之间的元素依次弹出后重新依次写入新数据,至于查,只允许查看尾部元素,一旦查看必须弹出。

        通常栈结构都用顺序表创建较为方便,因此以下便以顺序表的方式进行演示。同时最后附上链表实现栈结构的代码。

2、栈创建

2.1、文件结构

        与之前章节相同,依然创建三个文件,文件名如下:

初级数据结构(三)——栈,C语言,数据结构与算法,数据结构,链表,c语言,算法

        stack.h :用于创建项目的结构体类型以及声明函数;

        stack.c :用于创建栈各种操作功能的函数;

        main.c :仅创建 main 函数,用作测试。

 2.2、前期工作

        stack.h 中内容如下:

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

//存储数据类型的定义及打印占位符预定义
#define DATAPRT "%d"
typedef int DATATYPE;

//栈主体
typedef struct Stack
{
	DATATYPE* data;		//数据段
	size_t top;			//栈顶位置下标
	size_t capacity;	//开辟空间记录
}Stack;

//函数声明---------------------------------

//初始化
extern int StackInit(Stack*);
//销毁
extern void StackDestroy(Stack*);
//入栈
extern void StackPush(Stack*, DATATYPE);
//出栈
extern void StackPop(Stack*);

        然后 stack.c 中先创建初始化及销毁的两个函数。这里需要注意的是对结构体中的 top 值的操作。虽然这个值等同于顺序表中的 size ,但需要区分 top 是储存栈结构最后一个数据的下标。顺序表中的空表 size 则是等于 0 ,取顺序表的最后一个元素下标都是以 size - 1 进行操作,而对于栈来说,如果是空栈,top 则置为 -1 :

#include "stack.h"

//初始化
int StackInit(Stack* st)
{
	//参数判断
	if (!st)
	{
		fprintf(stderr, "Illegal Stack Address\n");
		return;
	}
	//初始开辟1个数据位
	st->data = (DATATYPE*)malloc(sizeof(DATATYPE) * 1);
	if (!st->data)
	{
		fprintf(stderr, "Malloc Fail\n");
		return -2;
	}
	st->top = -1;
	st->capacity = 1;
	return 0;
}

//销毁
void StackDestroy(Stack* st)
{
	//参数判断
	if (!st)
	{
		fprintf(stderr, "Illegal Stack Address\n");
		return;
	}
	//释放
	free(st->data);
	st->data = NULL;
	st->top = -1;
	st->capacity = 0;
}

        在 main.c 中则预先创建一个结构体变量,无需进行其他操作。

#include "stack.h"

int main()
{
	Stack st;
	return 0;
}

3、栈的数据操作

3.1、入栈

        入栈实际上就是顺序表的尾插,具体实现过程可以参考第一篇顺序表中的插入数据操作。这个功能实现起来并不复杂。此外跟顺序表一样,如果已开辟的空间不足则需要进行扩容操作。这里可以将扩容封装为一个函数,使代码看起来更简洁。此外,由于扩容函数仅在该代码文件内调用,可以加上 static 修饰。

        在 static.c 中加入以下代码:

//扩容
static void StackExpand(Stack* st)
{
	//参数判断
	if (!st)
	{
		fprintf(stderr, "Illegal Stack Address\n");
		return;
	}
	DATATYPE* temp = (DATATYPE*)realloc(st->data, sizeof(DATATYPE) * st->capacity * 2);
	if (!temp)
	{
		fprintf(stderr, "Realloc Fail\n");
		return;
	}
	st->data = temp;
	st->capacity *= 2;
}

//入栈
void StackPush(Stack* st, DATATYPE data)
{
	//参数判断
	if (!st)
	{
		fprintf(stderr, "Illegal Stack Address\n");
		return;
	}
	st->top++;
	//空间不足则扩容
	if ((st->top) >= (st->capacity))
	{
		StackExpand(st);
	}
	//入栈
	st->data[st->top] = data;
}

        然后在 main.c 中输入以下,并测试:

    StackInit(NULL);
	StackInit(&st);
	StackPush(&st, 1);
	StackPush(&st, 2);
	StackPush(&st, 3);
	StackPush(&st, 4);
	StackPush(&st, 5);

初级数据结构(三)——栈,C语言,数据结构与算法,数据结构,链表,c语言,算法

初级数据结构(三)——栈,C语言,数据结构与算法,数据结构,链表,c语言,算法

        测试无误,进行下一步。

3.2、出栈

        入栈等于尾插,那出栈自然就等同于尾删。对于出栈操作需要注意两点,栈是否为空以及空间回收。是否空栈只需判别 top 是否等于 -1 。至于空间回收,顺序表中也有提到,操作逻辑完全一致。

        这里为了代码可读性,将空栈判断封装为函数,将空间回收也一并封装为函数。 stack.c 中增加如下代码:

//收缩空间
static void StackContract(Stack* st)
{
	//参数判断
	if (!st)
	{
		fprintf(stderr, "Illegal Stack Address\n");
		return;
	}
	DATATYPE* temp = (DATATYPE*)realloc(st->data, sizeof(DATATYPE) * st->capacity / 2);
	if (!temp)
	{
		fprintf(stderr, "Realloc Fail\n");
		return;
	}
	st->data = temp;
	st->capacity /= 2;
}

//空表判断 空表返回true
static bool StackIsEmpty(Stack* st)
{
	//参数判断
	if (!st)
	{
		fprintf(stderr, "Illegal Stack Address\n");
		return true;
	}
	return (st->top == -1 ? true : false);
}

//出栈
void StackPop(Stack* st)
{
	//参数判断
	if (!st)
	{
		fprintf(stderr, "Illegal Stack Address\n");
		return;
	}
	//数据为空直接返回
	if (StackIsEmpty(st))
	{
		fprintf(stderr, "Stack Empty\n");
		return;
	}
	//出栈
	st->top--;
	//空间过剩则收缩空间
	if ((st->top) < (st->capacity) / 2)
	{
		StackContract(st);
	}
}

3.3、附加功能

        出栈功能写完后,获取栈顶数据及打印输出栈顶数据的功能也一并加上。首先在 stack.h 中进行声明:

//打印
extern void StackPrint(Stack*);
//获取栈顶数据
extern DATATYPE StackGetTopData(Stack*);

        然后继续在 static.c 中补充这两个功能。

        这里需要注意,因为获取栈顶数据是有返回值的,因此如果空表或者传入空指针便不能简单地 return ,容易对返回值的接收产生误解。而不论 return 任何值,均有可能被误以为栈顶数据就是该值,因此这里以 assert 判定最佳:

//获取栈顶数据
DATATYPE StackGetTopData(Stack* st)
{
	//参数判断
	assert(st);
	//空表警告
	assert(!StackIsEmpty(st));
	//取数据并出栈
	DATATYPE data = st->data[st->top];
	StackPop(st);
	return data;
}

//打印
void StackPrint(Stack* st)
{
	//参数判断
	if (!st)
	{
		fprintf(stderr, "Illegal Stack Address\n");
		return;
	}
	//空表打印NULL后返回
	if (StackIsEmpty(st))
	{
		printf("NULL ");
		return;
	}
	//打印栈顶并出栈
	printf(DATAPRT " ", StackGetTopData(st));
}

        至此功能完毕。之后便是测试,同时也顺带测试之前出栈的函数。完整的 main 函数代码:

int main()
{
	Stack st;
	StackInit(NULL);
	StackInit(&st);
	StackPush(&st, 1);
	StackPush(&st, 2);
	StackPush(&st, 3);
	StackPush(&st, 4);
	StackPush(&st, 5);
	StackPrint(&st);        //5
	StackPrint(&st);        //4
	StackPrint(&st);        //3
	StackPrint(&st);        //2
	StackPrint(&st);        //1
	StackPrint(&st);        //NULL
	StackPrint(&st);        //NULL
	StackPush(&st, 1);
	StackPush(&st, 2);
	StackPush(&st, 3);
	StackPrint(&st);        //3
	StackDestroy(&st);
	StackPrint(&st);        //NULL

	return 0;
}

        测试结果如下: 

初级数据结构(三)——栈,C语言,数据结构与算法,数据结构,链表,c语言,算法

        至此栈结构便完成了。 

4、以链表实现栈

        链表实现栈的文件结构与顺序表实现栈的结构一致,根据以下代码自行测试研究。有链表的基础打底,这里实现起来也将十分轻松:

        stack.h :

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

#define DATAPRT "%d"

typedef int DATATYPE;

typedef struct StackNode
{
	DATATYPE data;
	struct StackNode* next;
}StackNode;

typedef struct StackInfo
{
	StackNode* StackHead;
	size_t StackSize;
}StackInfo;

//函数声明---------------------------------
//初始化
extern void StackInit(StackInfo*);
//销毁
extern void StackDestroy(StackInfo*);
//入栈
extern void StackPush(StackInfo*, DATATYPE);
//出栈
extern void StackPop(StackInfo*);
//获取栈顶数据
extern DATATYPE StackGetHead(StackInfo*);
//打印栈顶数据
extern void StackPrint(StackInfo*);

        stack.c :

#include "stack.h"

//初始化
void StackInit(StackInfo* info)
{
	//参数有效判断
	if (!info)
	{
		fprintf(stderr, "Illegal StackInformation Address\n");
		return;
	}
	//初始化
	info->StackHead = NULL;
	info->StackSize = 0;
}

//销毁
void StackDestroy(StackInfo* info)
{
	//参数有效判断
	if (!info)
	{
		fprintf(stderr, "Illegal StackInformation Address\n");
	}
	//空链表直接返回
	if (!info->StackSize)
	{
		return;
	}
	//逐节点释放空间
	StackNode* currentNode = info->StackHead;
	while (currentNode)
	{
		StackNode* destroyNode = currentNode;
		currentNode = currentNode->next;
		free(destroyNode);
	}
	info->StackHead = NULL;
	info->StackSize = 0;
}

//判空
static bool StackIsEmpty(StackInfo* info)
{
	//参数有效判断
	if (!info)
	{
		fprintf(stderr, "Illegal StackInformation Address\n");
		return;
	}
	return (info->StackSize == 0 ? true : false);
}

//入栈
void StackPush(StackInfo* info, DATATYPE data)
{
	//参数有效判断
	if (!info)
	{
		fprintf(stderr, "Illegal StackInformation Address\n");
		return;
	}
	StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
	if (!newNode)
	{
		fprintf(stderr, "Malloc Fail\n");
		return;
	}
	newNode->data = data;
	newNode->next = info->StackHead;
	info->StackHead = newNode;
	info->StackSize++;
}

//出栈
void StackPop(StackInfo* info)
{
	//参数有效判断
	if (!info)
	{
		fprintf(stderr, "Illegal StackInformation Address\n");
		return;
	}
	if (StackIsEmpty(info))
	{
		fprintf(stderr, "Stack Empty\n");
		return;
	}
	StackNode* destroyNode = info->StackHead;
	info->StackHead = info->StackHead->next;
	info->StackSize--;
	free(destroyNode);
}

//获取栈顶数据
DATATYPE StackGetHead(StackInfo* info)
{
	//参数有效判断
	assert(info);
	//空表警告
	assert(!StackIsEmpty(info));
	DATATYPE data = info->StackHead->data;
	StackPop(info);
	return data;
}

//打印栈顶数据
void StackPrint(StackInfo* info)
{
	//参数有效判断
	if (!info)
	{
		fprintf(stderr, "Illegal StackInformation Address\n");
		return;
	}
	if (StackIsEmpty(info))
	{
		printf("NULL ");
		return;
	}
	printf(DATAPRT " ", StackGetHead(info));
}

        main.c 的测试用例:文章来源地址https://www.toymoban.com/news/detail-770219.html

#include "stack.h"

int main()
{
	StackInfo info;
	StackInit(NULL);
	StackInit(&info);
	StackPush(&info, 1);
	StackPush(&info, 2);
	StackPush(&info, 3);
	StackPush(NULL, 20);
	StackPush(&info, 4);
	StackPush(&info, 5);
	StackPrint(&info);
	StackPrint(&info);
	StackPrint(&info);
	StackPrint(&info);
	StackPrint(&info);
	StackPrint(&info);
	StackPrint(&info);
	StackPrint(&info);
	StackPush(&info, 1);
	StackPush(&info, 2);
	StackPush(&info, 3);
	StackPrint(&info);
	StackDestroy(&info);
	StackPrint(&info);
	return 0;
}

到了这里,关于初级数据结构(三)——栈的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C语言数据结构——链表

    目录 前言 一、什么是链表 1.1链表的结构和概念 1.2 链表的分类 二、无头单向非循环链表 2.1 创建结构体 2.2 动态申请一个节点 2.3 单链表打印 2.4 单链表尾插/尾删 2.4.1 单链表尾插  2.4.2 单链表尾删 2.5 单链表头插/头删 2.5.1 头插 2.5.2 头删 2.6 单链表查找 2.7 单链表中间插入/中

    2024年02月16日
    浏览(40)
  • 【算法与数据结构】链表

    链表是由一串节点串联在一起的,链表的每个节点存储两个信息:数据+下一个节点的地址 分清楚两个概念:什么是内存内部,什么是程序内部 内存内部: 信息存储在内存空间里的 程序内部: 通过什么信息,去操作结构 如果想操作链表的话,我们依靠的是程序内部的信息,

    2024年02月03日
    浏览(37)
  • 【数据结构与算法】链表

    对于顺序表存在一些缺陷: 中间/头部的插入删除,时间复杂度为O(N) 。头部插入需要挪动后面的元素 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插

    2023年04月09日
    浏览(33)
  • 数据结构:链表(Python语言实现)

    链表分为单链表、双链表、循环单链表和循环双链表。 本文以单链表为例,用python创建一个单链表数据结构,同时定义链表节点的增加、删除、查询和打印操作。 创建一个名为Node的节点类,节点类里面包含2个属性和1个方法。 分别为data数据域属性和next指针域属性。 has_va

    2024年02月16日
    浏览(44)
  • 02-链表 (数据结构和算法)

    3.1 链表的基本概念 前面我们在学习顺序表时,线性表的顺序存储结构的特点是逻辑关系上相邻的两个数据元素在物理位置上也是相邻的。我们会发现虽然顺序表的查询很快,时间复杂度为O(1),但是增删的效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。为

    2024年02月16日
    浏览(32)
  • Python数据结构与算法-数据结构(列表、栈、队列、链表)

    数据结构是指相互之间存在这一种或者多种关系的数据元素的集合和该集合中元素之间的关系组成。 简单来说,数据结构就是设计数据以何种方式组织并存储在计算机中。 比如:列表、集合与字典等都是一种数据结构。 N.Wirth:“程序=数据结构+算法” 数据结构按照其 逻辑结

    2024年02月08日
    浏览(38)
  • C语言实现链表--数据结构

    魔王的介绍:😶‍🌫️一名双非本科大一小白。 魔王的目标:🤯努力赶上周围卷王的脚步。 魔王的主页:🔥🔥🔥大魔王.🔥🔥🔥 ❤️‍🔥大魔王与你分享:很喜欢宫崎骏说的一句话:“不要轻易去依赖一个人,它会成为你的习惯当分别来临,你失去的不是某个人而是你精

    2024年02月02日
    浏览(30)
  • C语言数据结构-双向链表

    带头链表的头结点,实际是\\\"哨兵位\\\",哨兵位节点不存储任何有效元素,只是站在这里\\\"放哨的\\\". 哨兵位的意义:遍历循环链表避免死循环. 笔者在删除,插入数据时,画好图后,也好了代码,但是在调试中多次出现 代码位置出错 ,导致写的代码的含义不符合预期. 所以说思路一定要清晰

    2024年02月04日
    浏览(33)
  • 双向链表(数据结构)(C语言)

    目录 概念 带头双向循环链表的实现 前情提示 双向链表的结构体定义 双向链表的初始化 关于无头单向非循环链表无需初始化函数,顺序表、带头双向循环链表需要的思考 双向链表在pos位置之前插入x 双向链表的打印 双链表删除pos位置的结点 双向链表的尾插 关于单链表的尾

    2024年02月04日
    浏览(50)
  • 数据结构链表(C语言实现)

            机遇对于有准备的头脑有特别的亲和力。本章将讲写到链表其中主要将写到单链表和带头双向循环链表的如何实现。    话不多说安全带系好,发车啦 (建议电脑观看) 。 附:红色,部分为重点部分;蓝颜色为需要记忆的部分(不是死记硬背哈,多敲);黑色加粗

    2024年02月10日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包