初阶数据结构【TOP】- 5. 栈的实现(巨详细且易懂)


前言

本篇文章笔者会对数据结构中 “ 栈 ”的知识进行细致讲解 ,从 “ 栈 ” 的介绍 - “ 栈 ” 的选择 - “ 栈 ” 的实现 , 循序渐进 ,希望各位学者认真学习,相信会有所收获!!

一、什么是栈 ?

相信大家在学习编程语言中都听过关于 “ 堆 ” “ 栈 ” “ 队列 ” … 相关名词 , 没听过的也不打紧 , 后续笔者会依次介绍 , 接下来让我们一起探讨 “ 栈 ” 的神奇世界 。


概念:是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底
通俗来讲 :栈的结构只能在栈顶插入和删除。这就好比 “羽毛球桶”
在这里插入图片描述

原则:后进先出 , 也叫LIFO(Last In First Out).
羽毛球桶就遵循这种原则 , 后进的羽毛球肯定是先拿出的 , 那么 栈 就可以理解为这种结构!


流程图:

在这里插入图片描述
注:栈的插入 , 删除操作只会针对栈顶 , 不对栈顶做任何变换。(栈中元素不为空时)

二、栈的选择

在初阶数据结构中我们会学习到: “ 顺序表” 、 “ 单向链表” 、 “双向链表 ” , 对于这三种结构来说各有千秋,那么对于栈的实现到底该选择哪一种结构来实现呢?


栈实现的前提:要先确定栈顶 。


实现笔者给出以下建议:
对比单链表和双链表:
单链表实现
若要用单链表实现 “ 栈 ” 的结构 , 我们先确定栈顶的位置,如图:
在这里插入图片描述

对比 1 , 2 两种方法 ,笔者建议优先选择 1 方法。 方法2要找到栈顶就必须找到栈顶的前一个节点的位置 ,这样会很繁琐 ,找前一个节点也同样很麻烦 , 对比 --> 选择1.


双向链表实现
若要用双向链表实现 “ 栈 ” 的结构 , 我们先确定栈顶的位置,如图:
在这里插入图片描述
我们会发现用双向链表实现虽然解决了前一个节点的问题 , 但是与单向链表方法1对比看来 , 不如选单向链表简单。


选择链表的话 , 笔者更推荐使用单向链表实现 “ 栈 ” 。


对比单链表和顺序表:
在这里插	入图片描述
从图中我们不能明显看出它们的优缺点 , 但是笔者上篇文章讲到过 , 链表和顺序表的具体优缺点 , 我们知道顺序表的一个非常特别的优点是 :CPU 缓存率高 。所以可以选择使用单链表和顺序表实现 。


三、栈的实现 - 顺序表

分析:1.首先确定栈顶的位置 2.实现栈顶的插入 、 删除。
确定栈顶方向图
图中箭头方向确定栈顶方向


栈的初始化

栈实现的基本接口 :

  1. 栈的初始化
  2. 入栈
  3. 出栈
  4. 判空
  5. 取栈顶元素
  6. 获取元素个数
  7. 栈的销毁

注:栈的初始化不同 , 栈顶表示的方式不同 。

初始化的两种方式(必看):

// 栈的声明

typedef int StackDateType;
struct Stack
{
	StackDateType* arr;
	int top;
	int capacity;
};

1.

栈顶位置初始化为 : 0 。 top 表示的是栈顶下一个元素的位置

分析:初始化时给栈置为空 , 如果 对于初始化 top == 0 , top 还想表示栈顶元素 ,就有了冲突 ,栈底层是数组 , 数组中没有元素时下标应置为 <0 的值 ,故:栈顶初始化在 0 的位置 , 那么后续解决就要时刻注意 , top 后续使用表示的是 栈顶下一个元素的位置。
2.

栈顶位置初始化为 : -1 。 top 表示的是栈顶的位置


栈的实现
同样, 实践还是三个文件 :.c 、.h、 .c 。

栈的声明文件 : Stack.h

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



// 栈的声明

typedef int StackDateType;
typedef struct Stack
{
	StackDateType* arr;
	int top;
	int capacity;
}STK;

//栈的初始化
void STKInit(STK* stk);
//入栈
void STKPush(STK* stk , StackDateType x);
//出栈
void STKPop(STK* stk);
//栈的判空
bool STKEmpty(STK* stk);
//取栈顶元素
StackDateType STKTop(STK* stk);
//获取栈的有效元素个数
int STKSize(STK* stk);
//栈的销毁
void STKDestory(STK* stk);

栈的实现文件: Stack.c

#include "Stack.h"

void ChickCapacity(STK* stk)
{
	if (stk->top == stk->capacity)
	{
		//动态增容
		int newcapacity = stk->capacity == 0 ? 4 : (stk->capacity)*2;
		StackDateType*  tmp = (StackDateType* )realloc(stk->arr , newcapacity*sizeof(StackDateType));

		if (tmp == NULL)
		{
			perror("realloc");
			exit(1);
		}
		stk->arr = tmp;
		stk->capacity = newcapacity;
	}
}

//栈的初始化
void STKInit(STK* stk)
{
	assert(stk);
	stk->arr = NULL;
	stk->top = stk->capacity = 0;
	//top 表示栈顶下一个元素的位置
}
//入栈
void STKPush(STK* stk, StackDateType x)
{
	//类似笔者文章 "顺序表"插入相关知识的讲解 ,请自行查看!!

	assert(stk);
	//检查空间容量
	ChickCapacity(stk);

	stk->arr[stk->top++] = x;
	//stk->arr[stk->top] = x;
	//sk->top++;
}
//出栈
void STKPop(STK* stk)
{
	assert(stk);
	assert(stk->top > 0);
	stk->top--;
}
//栈的判空
bool STKEmpty(STK* stk)
{
	assert(stk);
	return stk->top == 0;
	//if( top == 0)
	//	return ture;
}
//取栈顶元素
StackDateType STKTop(STK* stk)
{
	assert(stk && stk->top > 0);
	return stk->arr[stk->top - 1];
}
//获取栈的有效元素个数
int STKSize(STK* stk)
{
	assert(stk);
	return stk->top;
}
//栈的销毁
void STKDestory(STK* stk)
{
	assert(stk);

	free(stk->arr);
	stk->arr = NULL;
	stk->capacity = stk->top = 0;
}

栈的测试文件 : test.c

#include "Stack.h"

int main()
{
	STK  sk;
	//初始化
	STKInit(&sk);
	return 0;

}

在这里插入图片描述
笔者这里就不一一展示测试用例了, 希望学者能养成自行测试的习惯 , 以防代码出现BUG。


测试总体代码:

#include "Stack.h"


void VistNum(STK* sk)
{
	while (!STKEmpty(sk))
	{
		printf("%d\n", STKTop(sk));
		STKPop(sk);
	}
}

int main()
{
	STK  sk;
	//初始化
	STKInit(&sk);
	
	//入栈
	STKPush(&sk, 1);
	STKPush(&sk, 2);
	STKPush(&sk, 3);
	STKPush(&sk, 4);


	//出栈
	/*STKPop(&sk);
	STKPop(&sk);
	*/


	//判空
	bool ret = STKEmpty(&sk);
	printf("%d\n", ret);
	//取栈顶
	int StackNum = STKTop(&sk);
	printf("栈顶元素 = %d\n", StackNum);
	//获取有效元素的个数
	int size = STKSize(&sk);
	printf("有效元素个数 = %d\n", size);
	 
	 //访问栈的所有元素
	VistNum(&sk);
	//栈的销毁
	STKDestory(&sk);
	return 0;

}

在这里插入图片描述


#include "Stack.h"


void VistNum(STK* sk)
{
	while (!STKEmpty(sk))
	{
		printf("%d\n", STKTop(sk));
		STKPop(sk);
	}
}

int main()
{
	STK  sk;
	//初始化
	STKInit(&sk);
	
	//入栈
	STKPush(&sk, 1);
	STKPush(&sk, 2);
	STKPush(&sk, 3);
	STKPush(&sk, 4);


	//出栈
	STKPop(&sk);
	STKPop(&sk);
	


	//判空
	bool ret = STKEmpty(&sk);
	printf("%d\n", ret);
	//取栈顶
	int StackNum = STKTop(&sk);
	printf("栈顶元素 = %d\n", StackNum);
	//获取有效元素的个数
	int size = STKSize(&sk);
	printf("有效元素个数 = %d\n", size);
	 
	 //访问栈的所有元素
	VistNum(&sk);
	//栈的销毁
	STKDestory(&sk);
	return 0;

}

在这里插入图片描述


顺序表的另一种实现方式( top 表示下标) 希望学者自行练习 , 做到举一反三 !!


四、栈的实现 - 单链表

根据以上分析 , 选择单链表 1 的实现方法 ,符合单链表的 “头插” “头删” , 代码如下:


Stack.h 文件

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


// 单链表实现栈

//栈的声明

typedef int StackDateType;

typedef struct StackNode
{
	StackDateType data;
	struct Stack* next;
	
}STKNode;

typedef struct Stack
{
	STKNode* top; // 表示栈顶 -- 指向头节点 
	int size; // 表示栈中的元素个数
}Stack;

// 初始化
void STKInit(Stack* stack);

//入栈
void STKPush(Stack* stack, StackDateType x);
//出栈
void STKPop(Stack* stack);
//栈的判空
bool STKEmpty(Stack* stack);
//取栈顶元素
StackDateType STKTop(Stack* stack);
//获取栈的有效元素个数
int STKSize(Stack* stack);
//栈的销毁
void STKDestory(Stack* stack);

分析
在声明文件中笔者多给了一个结构体 , 很多伙伴应该会懵 ,这里给一个简单的 解释: 首先单链表的头插 , 头删根据笔者之前的文章也有讲到具体做法 , 我们要实现这两个接口我们需要传二级指针 , 因为:形参是实参的临时拷贝 , 传址调用才会达到我们想要的结果。
明确目的:我们想要实现栈的结构,Top 栈顶的位置需要指向头指针 ,那么我们就想到创建一个指向头节点的结构体指针。 其次,考虑到栈的接口中有统计有效元素个数的接口 ,我们就不妨在创建一个 Size 变量来记录栈的元素个数 , 在头插,头删中做出相应的 ++ , – 即可 。
这样的想法是怎么来的呢?
1. 栈是用单链表来实现的 , 单链表的统计个数要遍历整个链表 ,时间复杂度是 : O(N) . ,而创建一个 Size 变量便会解决效率的问题 。
2. 有了指向头节点的指针 和 Size 这两个就符合结构体的特征 , 创建结构体 ,想要改变结构体里面变量的值 ,只需传结构体的地址就可以达到效果 , 这样既解决了传参二级指针的问题 ,又解决了效率的问题。那么栈就是由 这个结构体所构成,后续通过结构体便可实现栈。

◆ 这样的思想后续还会用到 , 很巧妙 ,望学者仔细理解!!!!


Stack.c 文件

#include "Stack.h"


// 初始化
void STKInit(Stack* stack)
{
	stack->top = NULL;
	stack->size = 0;
}

//入栈
//单链表的头插
void STKPush(Stack* stack, StackDateType x)
{
	
	assert(stack);

	//申请新节点
	 STKNode* newnode = (STKNode*)malloc(sizeof(STKNode));
	 if (newnode == NULL)
	 {
		 perror("malloc fail !");
		 exit(1);
	 }
	 else
	 {
		 newnode->data = x;
		 newnode->next = NULL;
	 }
	
	 //头插
	
	 //一个节点
	 if (stack->top == NULL)
	 {
		stack->top = newnode;  //成为新的头节点
	 }
	 //多个节点
	 else
	 {
		newnode->next = stack->top;
		 stack->top = newnode;

	 }
	 stack->size++;
}
//出栈

void STKPop(Stack* stack)
{
	assert(stack);
	assert(stack->top);

	//一个节点
	if (stack->top->next == NULL)  // 头节点下一个节点为空
	{
		free(stack->top);
		stack->top = NULL;
	}
	//多个节点
	else
	{
		STKNode* next = stack->top->next;
		free(stack->top);
		stack->top = next;
	}

	stack->size--;
}
//栈的判空
bool STKEmpty(Stack* stack)
{
	assert(stack);
	return stack->top == NULL;
}
//取栈顶元素
StackDateType STKTop(Stack* stack)
{
	assert(stack);
	return stack->top->data;
}
//获取栈的有效元素个数 
int STKSize(Stack* stack)
{
	assert(stack);

	return stack->size;
}
//栈的销毁
void STKDestory(Stack* stack)
{
	assert(stack);
	STKNode* cur = stack->top;

	while(cur)
	{
		STKNode* next = (STKNode*)(cur->next);
		free(cur);

		cur = next;
	}
	stack->top = NULL;
}

test.c 测试文件

#include "Stack.h"



int main()
{
	 //创建一个栈
	Stack sk;
	//栈的初始化
	STKInit(&sk);
	//入栈
	STKPush(&sk, 1);
	STKPush(&sk, 2);
	STKPush(&sk, 3);
	STKPush(&sk ,4);
	出栈
	//STKPop(&sk);
	//STKPop(&sk);
	//栈中的有效个数
	int Size = STKSize(&sk);
	printf("Size =  %d\n", Size);
	printf("\n");
	//取栈顶元素
	int ret = STKTop(&sk);
	printf("Top Stack Element = %d\n", ret);
	printf("\n");

	//判空 -- 0 - 为假 -> 不空
	bool _bool = STKEmpty(&sk);
	printf("bool value = %d\n", _bool);
	printf("\n");

	printf("ALL Element :");
	// 取栈的所有元素
	while (!STKEmpty(&sk))
	{
		printf("%d ", STKTop(&sk));
		STKPop(&sk);
	}
	//栈的销毁
	STKDestory(&sk);

	printf("\n");

	return 0;

}

测试结果:

在这里插入图片描述
当加上出栈部分代码:

测试图片


总结

相信大家通过以上的学习 ,对栈的实现就不难了 , 笔者建议学者一定要 动手!动手!动手! 来感受数据结构部分的一些知识 ,认真领悟思想是很重要的!希望笔者文章对各位学者有所帮助 ,若学有所获 , 望给笔者关注 , 点赞!!!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值