栈
今天我们一起学习栈。
代码部分我依旧引用了印度老哥Harsha Suryanarayana的代码,并对学习过程中有问题的点进行描述。
当然,从这篇文章开始,我会说明我的原创点。
(1)原创点:
第二节:
解释了平均时间复杂度的计算过程。
对top变量做范围检查。
第三节:
比较了链表和链栈,解释了链栈的优点。
第四节:
在原视频中,Harsha是用C++,并使用了stack,在这里我用c语言进行描述。
(2) 本文分为5个部分:
1. 栈的介绍
定义:
是一种列表或者集合,并且具有如下的限制——插入或删除元素只能从一端,即栈顶进行。
操作:
(1)Push:向栈顶推入元素。
(2)PoP:从栈上移除最近插入的元素。
(3)Top:简单地返回栈顶的元素。
(4)IsEmpty:判断栈是否为空。
以上操作的时间复杂度都 为O(1)。
2.用数组实现栈
时间复杂度的分析:
(1)原因:
数据结构中有一个重点——时间复杂度,我个人不倾向使用数组,因为数组的规则导致了它不够灵活,相比其它数据结构,最基础的它更容易发生数据溢出,所以在使用数组构建栈之前,分析代码的时间复杂度是很有必要的;
(2)最好情况:
最好情况很容易理解,往栈顶放入数据即可,所以其时间复杂度为O(1);
(3)最坏情况:
数据溢出,即栈空间已经满了,但是系统还要给栈分配数据,那么需要重新建立一个更大的数组,通常建立的新数组的长度是原数组长度的2倍,我们需要把原数组中的元素转移到新的数组中,所以其时间复杂度为O(n);
(4)平均情况:
平均情况下,其时间复杂度为O(1),接下来我对此进行解释:插入的主要情况只有两种,原数组空间未满,进行操作和原数组空间已满,需要创建新的数组;
对两种情况进行细分,有n种情况:长度0~n-1和插满后的长度n,所以插入任意位置的概率为1/n,
那么我们对其进行加权平均法,得出其时间复杂度为O(1)。
定义和全局变量:
#define MAX_SIZE 101
int A[MAX_SIZE];
int top = -1;
代码解释:
第1行和第2行进行创建一个长度为101的数组;
第3行的 int top = -1 指创建一个空栈。
Push函数:
void Push (int x)
{
if (top == MAX_SIZE - 1)
{
printf("出现错误\n");
return;
}
A[++top] = x;
}
构建一个函数,首先考虑会出现错误的情况,所以通过if进行判断,如果 top 在数组的倒数第二个节点之前,可以进行入栈,如果 top >= 100,那么我们为了避免栈的溢出,我们对其进行跳出;
在数组范围内,接下来进行元素的推入。
Pop函数:
void Pop( )
{
if (top == -1)
{
printf("出现错误\n");
return;
}
top--;
}
还是考虑函数会出现错误的情况,所以先通过if进行判断,如果栈为空栈,那么对其进行跳出;
满足上述条件,我们对数组进行自减。
Top函数:
void Top( )
{
if(top<=100&&top>=0)
return A[top];
else
{
printf("对变量进行范围检查\n");
return;
}
}
Top函数不用多说,返回栈顶元素,我对此做了些改动:我在原代码的基础上对 top 变量做了范围检查。
想必大家都没有问题。
3.用链表实现栈
我们把链表实现的栈称为链栈。
(1)使用链栈的原因:
数组实现栈的缺陷我已经在上一章讲解过了,接下来,我们通过链表实现栈。
我们先讲一讲需要链栈的原因,以下都是建立在相比于顺序栈的情况下:
链栈可以利用很多零碎的空间,节省空间且易变化;
也许链栈查询速度不如顺序栈更快,但是链栈添加和删除数据会更快一些。
(2)链栈插入节点的方式:
通过链的学习,我们会插入头节点和尾节点了,且知道头部的时间复杂度为O(1),尾部的时间复杂度为O(1);
而栈中 Push 和 Pop 函数的时间复杂度都为O(1),所以我们不会用尾部插入的方式,而是选择头部插入的方式。
结构体
struct Node
{
int data;
struct Node* next;
};
struct Node* top = NULL;
Push函数:
void Push(int x)
{
struct Node* temp = (strcut Node*)malloc(sizeof(struct Node));
temp->data = x;
temp->link = data;
top = temp;
}
代码解释:
首先在堆上分配空间,关于堆空间的分配,用以下式子表明:
指针自身 = (指针类型*)malloc(sizeof(指针类型)* 数据数量);
其次进行赋值,并且把 top 的地址传入 temp->link 因为 top 的地址是NULL,那么此时 temp->link 的地址因为是NULL;
最后我们把 temp 的地址传入 top 中。
其实这段代码和链的头插入一模一样的,如果对链表很熟悉,那么这段学习起来代码没有压力。
Pop函数:
void Pop()
{
struct Node* temp;
if(top == NULL)
return;
temp = top;
top =top->link;
free(temp);
}
代码解释 :
先建立一个临时的空指针;
判断 top 是否为空栈,如果是,退出,如果不是那么删除该元素;
将栈上第一个元素给到临时性的 temp 指针上,然后再使 top 指针指向栈上的第二个元素;
最后释放 temp 这个临时性的指针。
4.反转字符串和链表
(1)反转字符串
Reverse函数:
void Reverse(char *c,int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
Push(c[i]);
}
for (i = 0; i < n; i++)
{
c[i] = top->data;
Pop();
}
}
代码解释:
这个是有前提的,需要把前面结构体 中的 data 前面的类型 int 改成 char;
首先引入变量 i 进行循环;
其次往栈里面存贮元素;
最后将栈中的元素放回字符串中。
值得注意的是,需要在赋值后,Pop 掉栈顶,以便下一次循环的赋值。
完整代码:
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
struct Node
{
char data;
struct Node* link;
};
struct Node* top = NULL;
void Push(char x)
{
struct Node* temp = (struct Node*)malloc(sizeof(struct Node));
temp->data = x;
temp->link = top;
top = temp;
}
void Pop()
{
struct Node* temp;
if (top == NULL)
{
return;
}
temp = top;
top = temp->link;
free(temp);
}
void Reverse(char *c,int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
Push(c[i]);
}
for (i = 0; i < n; i++)
{
c[i] = top->data;
Pop();
}
}
int main()
{
char c[51];
printf_s("请输入字符串");
gets_s(c);
Reverse(c, strlen(c));
printf_s("输出 = %s",c);
}
(2)反转链表:
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include <iostream>
struct Node
{
int data;
struct Node* link;
};
struct lian
{
int data;
struct lian* link;
};
struct Node* top = NULL;
struct lian* head = NULL;
struct lian* prev = NULL;
void Push(int n)
{
struct Node* temp = (struct Node*)malloc(sizeof(struct Node));
temp->data = n;
temp->link = top;
top = temp;
}
void Pop()
{
struct Node* temp;
if (top == NULL)
{
return;
}
temp = top;
top = temp->link;
free(temp);
}
void Insert(int x)
{
struct lian* temp = (struct lian*)malloc(sizeof(struct lian));
if (head == NULL)
{
head = temp;
prev = temp;
temp->data = x;
temp->link = NULL;
return;
}
prev->link = temp;
temp->data = x;
temp->link = NULL;
prev = temp;
}
void Print()
{
struct lian* temp = head;
while (temp != NULL)
{
printf("%d ", temp->data);
temp = temp->link;
}
printf("\n");
}
void Reverse()
{
lian* temp = head;
while(temp != NULL)
{
Push(temp->data);
temp = temp->link;
}
free(temp);
head = NULL;
while(top != NULL)
{
Insert(top->data);
Pop();
}
}
int main()
{
Insert(2);
Insert(4);
Insert(6);
Insert(8);
printf_s("反转前");
Print();
Reverse();
printf_s("反转后");
Print();
}
代码解释:
以上我直接贴出完整的代码,而本篇代码最重要的就是尾节点插入函数,注意,这里是不能使用头节点插入法,因为这样是不反转链栈的。
这里我讲解一下 Insert 函数:
首先有两个全局变量,head perv ;
如果 head 是空指针,那么就让 两个 指针都指向 temp ;
我们还是自定义:把前一个 temp 称 temp1 ,现在的 temp 称 temp2,接下来的 temp 称 temp3;
接下来,如果 head 已经有指向了,那么对 temp->data 进行赋值, 再让temp->link 指向NULL;而上面的 prev->link 是用来连接 temp1 和 temp2 的;(这一块可能有点难理解,读者多画链表图)
最后让 prev 指向现在的 temp,也就是 temp2 ,为 temp3 的连接做准。
5.前缀、中缀、后缀
我们先来看看什么叫中缀,前缀和后缀,但是这一篇章我不会进行代码的制作,因为C语言并不如C++好实现,所以我进行忽略,等到读者学习C++后,可以学习。
(1)定义:
中缀:人们理解意义上的运算法则,例如——(4+7)* 8;
前缀:* + 4 7 8,从右向左,进行入栈,遇见运算符,将栈顶的两个数字出栈运算,计算后将结果入栈;
后缀:4 7 + 8 * ,从左向右,进行入栈,遇见运算符,将栈顶的两个数字出栈运算,计算后将结果入栈;
对于人们而言,很容易理解中缀的运算方式,而对于计算机而言,它们更容易理解前缀和后缀的方式。
(2)
这里我并未给出代码,是因为C++对此进行操作更方便,而C却要大量的,一系列的自我定义,后续我可能会再出一起如何用C++编程。
*最后以上内容就是这么多,希望读者多加练习,会很容易了解数据结构的。
Harsha Suryanarayana的熟肉
同时也感谢up主fengmuzi2003的翻译。