目录
(1) 当栈用不带头不循环单向链表去实现时,必须用链表头部当栈顶,用链表尾部当栈底的原因
(2)当栈用不带头不循环单向链表去实现时,栈不用链表尾端当栈顶的原因
(2)销毁函数StackDestroy把栈st从非空栈st变回空栈st的过程
(1)对于压栈函数stackPush在栈st的下标为top位置压入一个元素的理解
四、 初始化函数StackInit把栈st的结构体成员变量top初始化为-1时,整个工程代码。
一、栈
1.栈的概念
(1)栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
注意:①顺序表和链表允许在头、尾、中间等位置进行插入和删除数据,但是栈只允许在栈固定的一端进行插入和删除数据;②栈也是线性表,而且栈中的数据也是依次存储的;③由于栈只允许在栈顶的位置进行插入和删除数据导致栈中的元素必须遵守后进先出的原则。
2.压栈和出栈
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
二、对栈的实现方式进行解析
注意:要实现一个线性结构的数据结构无非用的是数组或者链表,而栈是个线性结构,所以
栈的实现可以使用数组或者链表。
对栈使用数组实现和链表实现进行解析
1.栈使用数组实现的原因:
由于数组在尾插和尾删1个数据很容易实现而且效率高代价小(注意:这里的效率高指的是数组的高速缓存效率高),所以栈的实现可以用数组进行实现即栈可以把数组尾部当做栈顶,而数组头部(即下标为0的位置)当栈底。但是栈用数组实现唯一的缺陷是需要扩容。
图形解析:
栈在数组栈顶位置尾插的图形:
(注意:原数据的顺序时1 2 3 4)
2.栈使用不带头不循环单向链表实现的注意事项
(1) 当栈用不带头不循环单向链表去实现时,必须用链表头部当栈顶,用链表尾部当栈底的原因
由于单链表的头删和头插实现效率高,最终导致栈把链表的头部当栈顶,链表的尾部当栈底。
栈在栈顶位置头插的图形:
(注意:原数据的顺序时1 2 3 4)
栈使用不带头不循环单向链表实现时栈的结构体类型,如下图所示:
(2)当栈用不带头不循环单向链表去实现时,栈不用链表尾端当栈顶的原因
若选不带头不循环单向链表的尾端当成栈顶的话,若对链表进行尾插的话要实现起来还相对简单但是若对链表进行尾删的话,由于在进行尾删1个尾结点之前必须要找到尾结点的上一个结点的地址导致对链表进行尾删比较麻烦,所以栈才不用单链表去实现。
注意:若栈使用不带头不循环双向链表实现时,不管链表的头部还是尾部都可以作为栈顶和栈底,即使用不带头不循环双向链表的尾端做栈顶,由于链表结点的成员变量中有指针prev指向当前结点的上一个结点的位置,若在遍历链表时遍历的当前结点恰好链表尾结点,则就可以很轻松的找到链表尾结点的上一个结点进而方便我们进行尾删。
栈使用不带头不循环双向链表实现时栈的结构体类型,如下图所示:
三、栈使用数组实现
栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。
1.对栈各个功能函数进行解析
初始化函数StackInit
(1)栈的初始化
由于栈一开始最初状态是个空栈导致栈用顺序表(注意:顺序表的本质是个数组)去实现时这个顺序表最初的状态也是个空顺序表,而顺序表最重要的是必须要有一个结构体,所以必须声明一个关于顺序表的结构体类型来表示栈。而初始化函数StackInit本质是把栈初始化为空栈(注意:由于我们用一个顺序表的结构体表示栈,所以这里可以理解为把顺序表初始化为空顺序表)。要想把栈初始化为空栈的话,只需初始化函数StackInit把栈的结构体成员top初始化为0即可,而要不要给结构体成员指针a分配动态内存空间无关紧要,因为压栈函数会按需申请我们需要空间,但是我这里给指针a分配4个元素大小的动态内存空间。
//栈的数据类型
typedef int STDatatype;
//栈的结构体类型
typedef struct Stack
{
STDatatype* a;
int capacity;//容量
int top;//统计栈中存放数据的个数。top表示栈顶元素的下一个元素的下标
}ST;
//把栈st初始化为空栈的写法1:
//初始化函数->把栈st初始化为空栈
void StackInit(ST* ps)
{
//判断指针ps是否是空指针
assert(ps);
ps->a = (STDatatype*)malloc(4 * sizeof(STDatatype));
if (ps->a == NULL)
{
perror("malloc fail");
exit(-1);
}
//对栈st的结构体成员进行初始化
ps->capacity = 4;
ps->top = 0;
}
//把栈st初始化为空栈的写法2:
//void StackInit(ST* ps)
//{
// //判断指针ps是否是空指针
// assert(ps);
//
// //对栈st的结构体成员进行初始化
// ps->a = NULL;
// ps->capacity = ps->top = 0;
//}
(2)下面说明了什么使用assert对指针进行断言
①栈的初始化函数StackInit(ST* ps)中需要用assert(ps)对指针ps指向的顺序表进行断言的原因是:由于在主调函数中创建了一个结构体变量st来表示栈st使得&st不可能是空指针NULL,所以当我们把栈st的地址&st传给初始化函数stackInit(ST* ps)的形参ps接收时可知指针ps的值一定不是空指针NULL,所以在初始化函数stackInit的内部一定要判断指针ps是否是空指针。而判断指针ps是否是空指针的作用是防止程序员自己在传参时不小心传了个空指针NULL给初始化函数sStackInit(ST* ps)而assert(ps)就可以对ps是个空指针进行报错并说明程序报错的位置。
②若功能函数或者函数接口中的形参有指针时,以下的公式是用来判断一个功能函数的内部要不要利用assert函数对功能函数的形参指针进行断言:
公式:一定不为空就要断言。——>对该公式的理解:若功能函数的实参指针的值是个空指针NULL的话使得功能函数的形参指针的值也是空指针NULL的话,则此时功能函数的内部不需要用assert对形参指针进行断言;若功能函数的实参指针的值不是NULL的话使得功能函数的形参指针的值也不是NULL的话(例如,由于主调函数中定义了一个结构体的栈st使得这里的初始化函数StackInit的形参ps一定不为空指针则初始化函数必须对指针ps进行断言),则此时功能函数的内部必须用assert对形参指针进行断言即用assert判读形参指针是否是空指针NULL。
销毁函数StackDestroy
(1)销毁函数StackDestroy功能的理解
栈的删除函数stackDestroy的功能虽然表面上去是删除栈st的,但是栈st的空间类型是静态空间而且栈st的空间随着主调函数被调用完后自动随着主调函数被释放掉,所以栈的删除函数stackDestroy的本质不是删除栈的静态空间。栈的删除函数stackDestroy实际的功能是把栈st从非空栈st变回空栈st。
(2)销毁函数StackDestroy把栈st从非空栈st变回空栈st的过程
利用free函数释放掉栈st的成员变量指针a指向的空间和把栈st的成员变量capacity与top同时设置为0就可以把栈st变回空栈st。
//销毁函数
void StackDestroy(ST* ps)
{
//判断指针ps是否是空指针
assert(ps);
//释放动态空间
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
压栈函数StackPush
(1)对于压栈函数stackPush在栈st的下标为top位置压入一个元素的理解
由于初始化函数StackInit把栈st的结构体成员变量top初始化为0,导致top表示栈顶元素的下一个元素的下标,所以压栈函数stackPush只能在下标为top位置进行压栈。
top初始化为0的压栈图形解析
代码:
//压栈函数->尾插
void StackPush(ST* ps, STDatatype x)
{
//判断指针ps是否为空指针
assert(ps);
//判断插入位置的合法性
assert(ps->top >= 0);
//判断栈st的容量是否充足,若不足则要进行扩容。
if (ps->top == ps->capacity)
{
//扩容
STDatatype* tmp = (STDatatype*)realloc(ps->a, 2 * ps->capacity * sizeof(STDatatype));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->top++] = x;
}
注意:若初始化函数StackInit把栈st的结构体成员变量top初始化为-1,则top表示的是栈顶元素的下标,而top + 1才是压栈函数stackPush要进行压栈位置。若top被初始化为-1的话,则整个栈的各个功能函数的实现又会不一样的,这篇文章的最后会说明当初始化函数StackInit把栈st的结构体成员变量top初始化为-1时整个栈的实现工程如何写。注意,我的这篇栈的实现文章都是围绕初始化函数StackInit把栈st的结构体成员变量top初始化为0来进行解析的。
top初始化为-1的压栈图形解析
出栈函数StackPop
(1)出栈函数StackPop删除栈顶一个元素的原理
由于栈st是用top表示栈中存放元素的个数,而且栈st只通过下标top来访问指针a指向动态空间中存放的栈的数据而top的值减少1就表示栈st中可以访问元素的数量就减少1,所以出栈函数stackPop的内部只需用利用top--让top的值减少1就可以导致栈st中可以访问元素的数量top减少1个最终达到删除栈顶一个元素的效果。总的来说只需用top--就可以完成出栈函数stackPop对栈st的出栈操作。
//出栈函数->尾删
void StackPop(ST* ps)
{
//判断指针ps是否为空指针
assert(ps);
//判断栈st是否是空栈,若是空栈则不能再删除栈顶元素。
assert(!StackEmpty(ps));//或者写成assert(ps->top > 0);//判断栈st中是否还有可以删除的元素
ps->top--;
}
注意:出栈函数StackPop不用free函数删除栈顶元素而是通过top--来减少栈st可以访问元素的数量的原因是栈st用数组实现时,由于数组是个连续的动态内存空间导致无法利用free(top)来单独释放掉这个连续动态空间中的栈顶位置的空间。free函数只能释放掉用molloc申请的整块连续的空间,而free函数做不到删除整块连续空间中的一小部分。
取栈顶元素函数StackTop
(1)由于top表示栈顶元素的下一个元素的下标,若想要取栈顶元素只需访问指针a指向数组下标为top位置的元素即可。(注意:这里把指针a指向的动态空间看成一个数组)
//取栈顶元素
STDatatype StackTop(ST* ps)
{
//判断指针ps是否为空指针
assert(ps);
//判断栈st是否是空栈,若是空栈则没有栈顶元素可取了。
assert(!StackEmpty(ps));//或者写成assert(ps->top > 0);//由于top统计的是栈st中存放的数据个数,所以用assert(ps->top > 0)可以判断栈st中是否还有元素,若有则可以取栈顶元素。
return ps->a[ps->top - 1];
}
2.栈用数组实现的整个代码工程
(1)stack.h头文件
//stack.h头文件
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
//栈用数组实现,数组的尾部作为栈的栈顶
//栈的数据类型
typedef int STDatatype;
//栈的结构体类型
typedef struct Stack
{
STDatatype* a;
int capacity;//容量
int top;//统计栈中存放数据的个数。top表示栈顶元素的下一个元素的下标
}ST;
//初始化函数
void StackInit(ST* ps);
//销毁函数
void StackDestroy(ST* ps);
//压栈函数->尾插
void StackPush(ST* ps, STDatatype x);
//出栈函数->尾删
void StackPop(ST* ps);
//取栈顶元素
STDatatype StackTop(ST* ps);
//判断栈是否是空栈
bool StackEmpty(ST* ps);
//统计栈中存放数据的个数
int StackSize(ST* ps);
//打印函数
void StackPrint(ST* ps);
(2)stack.c源文件
//stack.c源文件
#include "stack.h"
//初始化函数->把栈st初始化为空栈
void StackInit(ST* ps)
{
//判断指针ps是否是空指针
assert(ps);
ps->a = (STDatatype*)malloc(4 * sizeof(STDatatype));
if (ps->a == NULL)
{
perror("malloc fail");
exit(-1);
}
//对栈st的结构体成员进行初始化
ps->capacity = 4;
ps->top = 0;
}
//销毁函数
void StackDestroy(ST* ps)
{
//判断指针ps是否是空指针
assert(ps);
//释放动态空间
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
//压栈函数->尾插
void StackPush(ST* ps, STDatatype x)
{
//判断指针ps是否为空指针。
assert(ps);
//由于压栈函数是在下标为top的位置压入元素的,所以可以利用assert(ps->top >= 0)判断插入位置的合法性。
assert(ps->top >= 0);
//判断栈st的容量是否充足,若不足则要进行扩容。
if (ps->top == ps->capacity)
{
//扩容
STDatatype* tmp = (STDatatype*)realloc(ps->a, 2 * ps->capacity * sizeof(STDatatype));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->top++] = x;//插入元素
}
//出栈函数->尾删
void StackPop(ST* ps)
{
//判断指针ps是否为空指针
assert(ps);
//判断栈st是否是空栈,若是空栈则不能再删除栈顶元素。
assert(!StackEmpty(ps));//或者写成assert(ps->top > 0);//判断栈st中是否还有可以删除的元素
ps->top--;
}
//取栈顶元素
STDatatype StackTop(ST* ps)
{
//判断指针ps是否为空指针
assert(ps);
//判断栈st是否是空栈
assert(!StackEmpty(ps));//或者写成assert(ps->top > 0);//判断栈st中是否还有元素。
return ps->a[ps->top - 1];
}
//判断栈是否是空栈
bool StackEmpty(ST* ps)
{
//判断指针ps是否为空指针
assert(ps);
判断栈st是否是空栈的写法1:
//if (ps->top == 0)
// return true;
//else
// return false;
//判断栈st是否是空栈的写法2:
return ps->top == 0;
}
//统计栈中存放数据的个数
int StackSize(ST* ps)
{
//判断指针ps是否为空指针
assert(ps);
return ps->top;
}
//打印函数
void StackPrint(ST* ps)
{
int i = 0;
for (i = 0; i < ps->top; i++)
printf("%d ", ps->a[i]);
printf("\n");
}
(3)test.c测试代码
//test.c测试代码
#include "stack.h"
//测试函数
void TestStack1()
{
//创建栈st
ST st;
//对栈st进行初始化
StackInit(&st);
//压栈->插入数据
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
//打印
StackPrint(&st);
printf("size:%d\n", StackSize(&st)); //不关心栈的底层实现
printf("栈顶元素:%d\n", StackTop(&st));
//出栈->删除数据
StackPop(&st);
StackPrint(&st);
StackPop(&st);
StackPrint(&st);
StackPop(&st);
StackPrint(&st);
StackPop(&st);
StackPrint(&st);
StackPop(&st);
//StackPop(&st);
printf("size:%d\n", StackSize(&st)); //不关心栈的底层实现
//销毁栈st
StackDestroy(&st);
}
int main()
{
//测试函数
TestStack1();
return 0;
}
四、 初始化函数StackInit把栈st的结构体成员变量top初始化为-1时,整个工程代码。
//初始化函数->把栈st初始化为空栈
void StackInit(ST* ps)
{
//判断指针ps是否是空指针
assert(ps);
ps->a = (STDatatype*)malloc(4 * sizeof(STDatatype));
if (ps->a == NULL)
{
perror("malloc fail");
exit(-1);
}
//对栈st的结构体成员进行初始化
ps->top = -1;
ps->capacity = 4;
}
//销毁函数
void StackDestroy(ST* ps)
{
//判断指针ps是否是空指针
assert(ps);
//释放动态空间
free(ps->a);
ps->a = NULL;
ps->top = -1;
ps->capacity = 0;
}
//压栈函数->尾插
void StackPush(ST* ps, STDatatype x)
{
//判断指针ps是否为空指针
assert(ps);
//判断栈st的容量是否充足,若不足则要进行扩容。
if (ps->top + 1 == ps->capacity)
{
//扩容
STDatatype* tmp = (STDatatype*)realloc(ps->a, 2 * ps->capacity * sizeof(STDatatype));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->top++;
ps->a[ps->top] = x;
}
//出栈函数->尾删
void StackPop(ST* ps)
{
//判断指针ps是否为空指针
assert(ps);
//判断栈st是否是空栈
assert(!StackEmpty(ps));//或者写成assert(ps->top > 0);//判断栈st中是否还有可以删除的元素
ps->top--;
}
//取栈顶元素
STDatatype StackTop(ST* ps)
{
//判断指针ps是否为空指针
assert(ps);
//判断栈st是否是空栈
assert(!StackEmpty(ps));//或者写成assert(ps->top > 0);//判断栈st中是否还有可以删除的元素
return ps->a[ps->top];
}
//判断栈是否是空栈
bool StackEmpty(ST* ps)
{
//判断指针ps是否为空指针
assert(ps);
判断栈st是否是空栈的写法1:
//if (ps->top == -1)
// return true;
//else
// return false;
//判断栈st是否是空栈的写法2:
return ps->top == -1;
}
//统计栈中存放数据的个数
int StackSize(ST* ps)
{
//判断指针ps是否为空指针
assert(ps);
return ps->top + 1;
}