栈与队列是两种重要的抽象数据类型,是一种操作受到限制的线性表,其特殊性质在于限定了插入与输出操作的位置。线性表可见下文(暂时没写,流汗)。
一.栈的定义
栈(Stack)作为一种限定性线性表,是将线性表的插入与删除操作限制为仅在表的一端进行,
通常将表中允许进行插入,删除操作的一端称为栈顶(Top),因此栈顶当前位置是动态变化的,一般由被称作栈顶指针的位置指示器来指示。
同时,表的另一端被称作栈底(Bottom)。
当栈中没有元素时,称为空栈。
栈的插入操作(Push)被形象地称为进栈,入栈或压栈,
删除操作(Pop)称为出栈或退栈。
根据栈的定义,可知每次操作的都是这个栈“最新”的元素,即最后进栈的元素。例如,按照固定顺序依次连续将1,2,3,4,5,这五个元素按照1-5的顺序依次入栈,但其出栈顺序却是5,4,3,2,1;可以发现,最先入栈的元素最后出栈,而最后入栈的元素最先出栈。这种操作原则也就是后进先出原则(Last In First Out , LIFO)。因此栈也被称作后进先出的线性表。生活中也有相关案例,如弹夹中子弹的填充消耗,均在弹夹的一端进行,先装入的子弹后出膛,后装入的子弹先出膛。
不过并非将一组元素按照一定顺序入栈,出栈顺序就是一定的,因为按照一定顺序入栈不代表连续(听起来像个文字游戏,但这就强调了思维的缜密以及对于出入栈的理解),所以可以先入几个元素,再出几个元素,再入,再出。比如,同样按照1,2,3,4,5,的顺序,但不连续将这些元素入栈,比如先1,2,然后出栈2,再入3,4,出4,3,再出1,入5,出5
这样下来,入栈顺序仍是1,2,3,4,5,但出栈顺序变成了2,4,3,1,5.所以出栈的顺序与入栈的顺序以及出栈的时机紧密相连,但只需要注意到出栈出的是top元素,就可以以不变应万变。
二.栈的实现
栈作为一种特殊的线性表,在计算机中主要存在两种基本的存储结构:顺序存储结构(顺序栈)和链式存储结构(链栈)。
01.顺序栈
顺序栈是用顺序存储结构实现的栈,即利用一组连续的存储空间依次存放自栈底到栈顶的数据元素,为了减小时间的开支,可以设置一个位置指针 top (栈顶指针)来动态地指示栈顶元素在顺序栈中的位置。(因为栈总在最后一个位置进行操作,设置指针就可以避免每次遍历链表找到top位置(时间复杂度 O(n)),指针只需指哪打哪(时间复杂度O(1))
通常以 top = -1 来表示空栈。(为什么选择-1?因为-1不但可以起到特殊标记表示空栈,当添加元素时,只需先进行++top操作,就可以令top指向栈顶,即即将进行操作的位置)
其数据结构定义代码如下:
const int Maxsize = 10; // 定义栈中存放数据的空间大小
struct Stack
{
int Date[Maxsize]; // 定义一块连续存储空间--数组来存放数据元素
int top; // 位置指针 top
};
01-01-初始化顺序栈
首先明确初始化就是在堆区开辟一份连续的存储空间分配给栈类型指针,然后将top初始化为-1表示空栈即可,代码实现如下;
Stack Init_Stack() {
Stack* New = (Stack*)malloc(sizeof(Stack)); // 在堆区开辟一份连续的存储空间分配给栈类型指针
New->top = -1; // top初始化为-1
return *New; // 返回此栈
}
01-02-进栈
进栈时,首先需要判断栈是否已满,如果栈满还要强行操作,会发生上溢(未知的不安全操作)。其次就是进行栈顶元素的赋值操作。
具体实现如下:
void Push(Stack& head) {
if (head.top >= Maxsize - 1) { // 判断栈是否已满
printf("此栈已满!!!\n");
return;
}
printf("请输入你要插入的元素:\n");
int date = 0;
scanf("%d", &date);
++head.top; // top先加一指向即将进行操作的位置,即栈顶
head.Date[head.top] = date; // 赋值操作
printf("压入成功\n");
}
01-03-出栈
出栈时,须先判断栈是否为空,如果空,还要执迷不悟就会发生下溢(与上溢一样,不安全,无意义)。其次,只需要将栈顶指针下移就行,原来栈顶的元素无需操作,不动当作随机量就行了。
具体实现如下:
void Pop(Stack& head) {
if (head.top == -1) { // 判断栈是否为空
printf("此栈为空!!!\n");
return;
}
--head.top; // 栈顶指针下移
printf("出栈成功\n");
}
01-04-读取栈中元素
读取操作无非就是将栈遍历一遍,并将每个元素输出一遍,而顺序栈的基本结构又是数组,所以此操作就是数组元素的便利,并且栈中元素个数又可从栈顶指针top上得到,所以一个for循环就可以搞定。
具体实现如下:
void OutPut_Stack(Stack& head) {
if (head.top == -1) { // 判断是否为空栈,空栈便不必输出,
printf("空栈\n"); // 自然不需判断,直接输出也行,不过不如此令操作者清楚栈的状况
return;
}
printf("该栈元素依次如下:\n");
for (int i = 0; i <= head.top; ++i) { // for循环遍历输出
printf("%d\t", head.Date[i]); // top与数组下标相对应,故为 i<=head.top
}
printf("\n");
}
这样,一个简单的顺序栈的定义以及一些基础操作就实现了。是不是很easy呢?快动手实现一下(滑稽+狗头)。
01-05-多栈共享技术
01-05-01-定义
栈的应用非常广泛,经常会出现一个程序中需要同时使用多个栈的情况。若使用顺序栈,需要提前分配栈空间,大小难于确定,从而产生有的栈溢出,有的栈空间还很空闲的情况。为此,可以让多个栈共享一个足够大的数组空间,利用栈的动态特性来使其存储空间相互补充,这便是多栈共享技术。
01-05-02-双端栈
在顺序栈的共享技术中,最常用的是两个栈的共享技术,即双端栈。他主要利用“栈的栈底位置不变,而栈顶位置动态变化”的特性。
首先申请一个共享的一维数组空间S[M],将两个栈的栈底分别放置在一维数组两端,分别是0和M-1。由于两栈顶是动态变化的,这样可以形成互补,使得每个栈的可用空间与实际使用情况有关。这样一来,两栈的空间利用率要比两栈分别申请M/2的空间利用率高。
两端共享的数据结构如下:
const int Maxsize = 10; // 最大空间
enum Direction
{
left=0, right, // 枚举left和right表示两边的栈,直接0,1也行,
}; // 这个看起来清楚,宏定义或者全局常量也行
typedef struct Double_Stack // 嫌名字太长,起个别名 DS
{
int Date[Maxsize]; // 共用一维数组
int Top[2]; // 指向左右两栈的top指针
}DS;
余下的操作,便和顺序栈相差无几,最大的区别在于空栈与满栈的判定,以及右栈top指针的变化方向,会在对应函数前讲解,其余算法便不再描述。
01-初始化
DS* Init_DS() {
DS* L = (DS*)malloc(sizeof(DS)); // 申请一个共享的一维数组空间
L->Top[left] = -1; // 左栈为空操作
L->Top[right] = Maxsize; // 右栈为空
return L;
}
02-压栈
void Push_DS(DS* L) { // 怎样算满?自然是两端栈顶指针相挨,即差一
if (L->Top[left] >= L->Top[right]-1) { // 需要判断两边栈
printf("此表已满!!!\n");
return;
}
int who = 0;
printf("请输入所要输入的方向,left(0),right(1)\n");
scanf("%d", &who);
int x = 0;
printf("请输入所要输入的元素\n");
scanf("%d", &x);
if (who) {
--L->Top[who]; // 且右栈是从右向左压入元素,所以指针减小
} // 这里用switch语句也行,如果是更多栈共享,switch更方便
else
++L->Top[who];
L->Date[L->Top[who]] = x; // 不管那端,赋值即可
}
03-出栈
void Pop_DS(DS* L) {
if (L->Top[left] == -1&&L->Top[right]==Maxsize) { // 同样,判断栈空,两个方向
printf("此栈为空!!!\n");
return;
}
else if (L->Top[left] == -1) {
printf("左栈已空,谨慎操作\n"); // 小提示,可有可无,只在最后显示也行
}
else if (L->Top[right] == Maxsize) {
printf("右栈已空,谨慎操作\n");
}
int Drc = 0;
printf("请输入出栈的方向,left(0),right(1)\n");
scanf("%d", &Drc);
if (Drc && L->Top[right] < Maxsize) { // 因为上述只排除了两端皆空的情况,
++L->Top[Drc]; // 需在此处对余下情况加以分析
} // 操作的那端非空才能操作
else if(!Drc && L->Top[left] > -1) {
--L->Top[Drc];
}
else if (!Drc && L->Top[left] == -1) {
printf("左栈已空,违法操作!!!\n");
}
else if (Drc && L->Top[right] == Maxsize) {
printf("右栈已空,违法操作!!!\n");
}
}
04-输出双端栈
这个可不要一股脑输出,毕竟是两个栈,所以就分左右两栈分别输出其对应元素,无非就是遍历两个顺序表一样
void OutPut_DS(DS* L) {
if (L->Top[left] == -1 && L->Top[right] == Maxsize ) {
printf("此表为空!!!\n");
return;
}
if (L->Top[left] > -1) {
printf("左表依次(left to right)输出如下:\n");
for (int i = 0; i <= L->Top[left]; ++i) {
printf("%d\t", L->Date[i]);
}
printf("\n");
}
if (L->Top[right] <Maxsize) {
printf("右表依次( right to left)输出如下:\n");
for (int i = Maxsize-1; i >= L->Top[right]; --i) {
printf("%d\t", L->Date[i]);
}
printf("\n");
}
}
以上便是双端栈的定义以及一些基本操作,相信你已经跃跃欲试了。动起来(b( ̄▽ ̄)d)!!!
不过各位会不会好奇,如果想让三个及以上的栈共享一块空间,应如何设计,优缺点如何?
欢迎各位在评论区一起讨论。
02.链栈
上述共享栈顶多在一定程度上解决栈区空间分配问题,并且在3个及以上的栈共享一块空间时,会更加复杂。因此在经常变化元素的情况下,动态的链栈便发挥重要作用。
链栈就是采用链表作为存储结构实现的栈。为了方便操作,一般采用带头结点的单链表实现链栈。由于栈的插入和删除操作仅限在一端,不妨采用头插法,在表头位置进行操作,那么链表的表头指针就是栈顶指针啦。
top便是栈顶指针,始终指向当前栈前面的头结点,故真正的栈顶是top->next。若top->next==NULL,则表示栈空。众所周知,使用链栈不必预先分配存储空间,不用为最大容量头大,只要系统有可用空间,就不会上溢。不过链栈使用完毕后,应及时释放相应空间,让系统保持活力。
链栈结构定义:其实就是单向链表
typedef struct Linked_List_Stack
{
int date;
struct Linked_List_Stack* next;
}LS,*Head;
02-01-初始化链栈
相信到这里,初始化对于各位毫无压力,就是链表的初始化。
Head Init_LS() {
Head head = (Head)malloc(sizeof(LS)); // 分配空间
head->date = 0; // 存栈长度
head->next = NULL;
return head;
}
02-02-入栈
void Push_LS(Head head) {
//这玩意没得满
int Date = 0;
printf("请输入所要压入的数据:\n");
scanf("%d",&Date);
LS* node = (Head)malloc(sizeof(LS)); // 申请新的LS空间
node->date = Date; // 数据赋值
node->next = head->next; // 把之前的数据元素连在他后面
head->next = node; // 把他接在头结点后面
++head->date; // 数量加一
}
02-03-出栈
void Pop_LS(Head head) {
if (head->next == NULL) { // 判断是否为空栈,与其余栈一样
printf("此栈为空,无法操作!!!\n");
}
else {
LS* temp = head->next; // 为了最后释放此空间,留个指针指向该空间
head->next = temp->next;
free(temp);
--head->date; // 栈内元素减一
}
}
02-04-输出战栈中元素
void OutPut_LS(Head head) {
if (head->next == NULL)
{
printf("此栈为空!!!\n");
return;
}
LS* node = head->next;
printf("此栈元素如下,从栈顶开始至栈底:\n");
//可以使用head->date作为遍历条件 ,此头结点空间被用来存放栈中元素数目
for (int i = 0; i < head->date; ++i) {
printf("%d\t", node->date);
node = node->next;
}
printf("\n");
// 也可以通过判断是否到达栈底元素来遍历
// 栈底元素标识就在其next指针上,它指向NULL
while (node) {
printf("%d\t", node->date);
node = node->next;
}
printf("\n");
}
至此,链栈的定义与简单的操作完成。
不过在双端栈处留下的问题,如果想令多个栈一起被管理,并且利用同一块空间,应如何更方便?相信各位已有思路。
02-5-多栈运算
将多个链栈的栈顶指针放在一个一维指针数组之中统一管理,从而实现同时管理和使用多个栈。(可能有人会想,为何不用链表存放,这样不是更动态?自然更灵活,但不方便随机读取,链表哪有数组取元素方便,毕竟栈所分配的空间不好确定,但用到的栈的数目还是比较清楚的)
实际上就是把刚才的链栈装在一个数组中分别调用链栈对应的函数,算法上没有新花样。
const int M = 10;
Head top[M] = { NULL };
void Init_Stack() {
for (int i = 0; i < M; ++i) {
top[i] = Init_LS();
}
}
然后就随便调用:
void MutStack() {
int num = 0;
printf("请输入所要操纵的栈号\n");
int Key = 0;
scanf("%d", &num);
while (1) {
LS_Menu();
scanf("%d", &Key);
switch (Key) {
case 1:Push_LS(top[num]);
break;
case 2:Pop_LS(top[num]);
break;
case 3:
break;
case 4:OutPut_LS(top[num]);
break;
case 0:return;
}
system("pause");
system("cls");
}
}
int main()
{
Init_Stack();
int Key = 1;
while (Key!=666) {
MutStack();
printf("是否继续操作,输入666退出,其余继续操作\n");
scanf("%d", &Key);
system("pause");
system("cls");
}
return 10;
}
不过多栈集中管理的思想可以吸收,这和哈希冲突倒是有相似之处。
以上就是我个人对于栈的定义,以及简单的功能实现,包括我个人的一些理解。如有帮助,吾喜之;如有指正,吾更喜。望诸位不吝赐教。o(* ̄▽ ̄*)ブ