前言
本篇文章笔者会对数据结构中 “ 栈 ”的知识进行细致讲解 ,从 “ 栈 ” 的介绍 - “ 栈 ” 的选择 - “ 栈 ” 的实现 , 循序渐进 ,希望各位学者认真学习,相信会有所收获!!
一、什么是栈 ?
相信大家在学习编程语言中都听过关于 “ 堆 ” “ 栈 ” “ 队列 ” … 相关名词 , 没听过的也不打紧 , 后续笔者会依次介绍 , 接下来让我们一起探讨 “ 栈 ” 的神奇世界 。
★ 概念:是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
通俗来讲 :栈的结构只能在栈顶插入和删除。这就好比 “羽毛球桶”
★ 原则:后进先出 , 也叫LIFO(Last In First Out).
羽毛球桶就遵循这种原则 , 后进的羽毛球肯定是先拿出的 , 那么 栈 就可以理解为这种结构!
★ 流程图:
◎ 注:栈的插入 , 删除操作只会针对栈顶 , 不对栈顶做任何变换。(栈中元素不为空时)
二、栈的选择
在初阶数据结构中我们会学习到: “ 顺序表” 、 “ 单向链表” 、 “双向链表 ” , 对于这三种结构来说各有千秋,那么对于栈的实现到底该选择哪一种结构来实现呢?
● 栈实现的前提:要先确定栈顶 。 ●
实现笔者给出以下建议:
● 对比单链表和双链表:
★ 单链表实现
若要用单链表实现 “ 栈 ” 的结构 , 我们先确定栈顶的位置,如图:
◎ 对比 1 , 2 两种方法 ,笔者建议优先选择 1 方法。 方法2要找到栈顶就必须找到栈顶的前一个节点的位置 ,这样会很繁琐 ,找前一个节点也同样很麻烦 , 对比 --> 选择1.
★ 双向链表实现
若要用双向链表实现 “ 栈 ” 的结构 , 我们先确定栈顶的位置,如图:
我们会发现用双向链表实现虽然解决了前一个节点的问题 , 但是与单向链表方法1对比看来 , 不如选单向链表简单。
● 选择链表的话 , 笔者更推荐使用单向链表实现 “ 栈 ” 。 ●
● 对比单链表和顺序表:
从图中我们不能明显看出它们的优缺点 , 但是笔者上篇文章讲到过 , 链表和顺序表的具体优缺点 , 我们知道顺序表的一个非常特别的优点是 :CPU 缓存率高 。所以可以选择使用单链表和顺序表实现 。
三、栈的实现 - 顺序表
★ 分析:1.首先确定栈顶的位置 2.实现栈顶的插入 、 删除。
● 图中箭头方向确定栈顶方向
● 栈的初始化
栈实现的基本接口 :
- 栈的初始化
- 入栈
- 出栈
- 判空
- 取栈顶元素
- 获取元素个数
- 栈的销毁
◎ 注:栈的初始化不同 , 栈顶表示的方式不同 。
◎ 初始化的两种方式(必看):
// 栈的声明
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;
}
测试结果:
当加上出栈部分代码:
总结
相信大家通过以上的学习 ,对栈的实现就不难了 , 笔者建议学者一定要 动手!动手!动手! 来感受数据结构部分的一些知识 ,认真领悟思想是很重要的!希望笔者文章对各位学者有所帮助 ,若学有所获 , 望给笔者关注 , 点赞!!!!!!