数据结构自学笔记二、栈


(温馨提示:本人是一名正在自学数据结构的小白,如果你是想要得到某个疑问的答案,请另觅高就,不要别我浅薄的学识耽误了)

栈的定义

刚刚我们学了线性表,其实栈在也可以算作是线性表,甚至在进行某些操作的时候要比之前讲的一般的线性表更为简单。

栈是只能在一端进行操作的线性表

小学的时候很喜欢玩《穿越火线》,当时玩终结者模式,一堆人钻在一条狭窄的胡同里,因为幽灵只能从一端进攻,所以只要火力充足,基本都能苟到最后。
但有一个问题,就是胡同太窄了,大家只能排成一列,所以如果前面的人不出去,后面的人也出不去。栈就是像这样的一种数据结构,它是一种线性表,但是只能从线性表的一段进行操作,如添加元素(压入栈push)和删除元素(弹出栈pop)等。

栈中元素的特性是先进后出,后进先出。我们把栈可以进行操作的一端称为栈顶,另一端称为栈底。用户只能对栈顶的元素进行操作。

栈的作用

可能初学者会和我一样想:栈到底有什么用?它是线性表,却比线性表多了很多约束条件,反而不能自由的处理它的任意一个元素,这不是变麻烦了吗?
且慢,我们来看看栈有什么用吧。

我相信很多人在学C语言的函数模块时,一定做过这样一个题:输出斐波那契数列的前多少多少个。
比如:请输出斐波那契数列的前10个数。
而且这个题一定在学循环的时候出现一次,在学函数的时候又会出现一次。
如果我们用循环来写,可以这样:

	int a[10];//由于只输出十个,我们可以声明数组来存放
	a[0]=a[1]=1;
	cout<<a[0]<<endl<<a[1];//输出前两个
	for(int i=2;i<10;i++)//循环八次
	{
		a[i]=a[i-1]+a[i-2];
		cout<<a[i];//输出数字
	}

当然,这并不是一个非常睿智的写法,因为它需要开数组。用两个变量进行循环同样可以达到目的,聪明的你一定能够想到,此处不赘述。
但是,在我们学函数模块时,老师会鼓励我们设计一个递归函数,请看代码:

	int Fibonacci(int n)//一个整型函数,传入一个int型n,返回斐波那契数列的第n个数
	{
		if(n==1||n==2)return 1;
		else return Fibonacci(n-1)+Fibonacci(n-2);
	}

这个代码就非常的简洁而明智。因为C语言是允许函数调用其他函数的(包括自身),这样递归调用函数实现了代码的简化。
这和栈有什么关系吗?

别急,再讲一个故事。还是初学C语言的时候,做了这么一个题:输出斐波那契数列的第10000个数。当时我自信满满的用了刚学的递归,写了一个函数,结果一运行,编译器就报错了。
我蒙了,求救大佬室友。大佬瞅了一眼,“哦,爆栈了,你用循环做吧”
爆栈指的就是函数栈溢出,函数栈用到的就是栈这种数据结构,函数对函数的调用是通过函数栈来完成的。
我们之前提到,栈的特性是后进先出。而程序运行过程中,运行函数时会将函数压入函数栈,在函数运行结束得到返回值后再弹出;如果函数运行没有结束而是调用了另外一个函数,比如像递归函数这样,那么新调用的函数会继续压入函数栈中。如果递归层数太多,也就是说压入函数栈的函数就越多。函数栈的容量是有限的,当压入函数栈的函数达到一定量时,就会发生“溢出”,此时编译器就会抛出一个错误的提示。(听不太明白,别急,我们待会要学到的栈的一些基本操作会解释这一点)
(感兴趣的可以看一看这篇博客:link)

我们可以看到,在递归的过程中,栈的这种先进后出的特性与函数的调用过程十分契合,这就是栈的一个重要用途——用于递归过程。
此外,由于栈的特性,我们只能对栈顶的一个元素进行处理,这种局限性反而缩小了我们思考的范围,让我们可以把精力集中在单个的问题上,而不去考虑其他的元素。这也简化了程序设计。

在介绍完栈的一些基本功能的实现后,我们会再着重讲讲如何用栈实现四则运算,相信届时你会更加理解栈的作用。

栈的顺序存储与随机存储

栈也是线性表的一种,因此同样有顺序存储和随机存储两种类型。优劣也基本相同。
随机存储的栈又叫链栈。

栈的基本功能的实现

先以顺序存储结构的栈为例。

1.初始化,即生成一个新栈

我们先想想一个栈需要什么。首先,要可以存放数据。顺序存储结构的话,就是定义一个数组,我们不妨设为整型数组,长度为10。此外,我们还需要一个top“指针”,告诉我们栈顶的位置。因为是数组,我们只需要一个整型数来记录栈顶元素在数组中的下标即可。
初始化时,将top默认设为-1,每存入一个元素,则+1;

	struct stack{//定义一个结构体为栈
	int data[10];//也可以是其他长度
	int top;//记录栈顶元素的下标
	};
	
	int main()//初始化一个栈
	{
		stack Stack01;
		Stack01.data[0]=0;
		top++;
	}

2.消除一个栈
由于顺序存储结构的栈占用空间是预先定义好的,所以这个操作不是很需要

3.将栈清空
从top开始,将栈结构体中数组的元素全部改为0

void clear_stack(stack* Stack01ptr,int top)
{
	while(top>=0)
	{
		Stack01ptr->data[top--]=0;
	}
}

4.判断栈是否为空
直接看top的值不就好了?

5.若栈非空,则返回栈顶元素

int get_elem(stack* Stack01ptr,int top)
{
	if(top<0)return ERROR;
	else return Stack01ptr->data[top];
}

6.为栈添加元素

诶嘿,注意这里要判断栈是否满了,否则会发生栈溢出哦

void push(stack* Stack01ptr,int top,int newdata)//假设栈长度为10
{
	if(top==9)return ERROR;
	else
	{
		top++;
		Stack01ptr->data[top]=newdata;
	}
}

7.为栈删除元素

诶嘿,这里要判断栈是否为空

void pop(stack* Stack01ptr,int top)//如果需要知道弹出的元素的值,可以设返回值
{
	if(top==-1)return ERROR;
	else
	{
		Stack01ptr->data[top--]=0;
	}
}

8.返回栈的元素个数

好办!告诉他top+1等于多少就完事了

然后再说说随机存储结构的栈吧

1.初始化,即生成一个新栈

与线性表相同的,我们需要在顺序存储结构的栈的结构体中加入指针域

	struct stack{//定义一个结构体为栈
	int data;//也可以是其他长度
	stack* next;//记录栈顶元素的下标
	};

但是与线性表不同的,由于栈是后进先出,所以栈的“头结点”(只是拿来类比,并不是这么称呼)时刻在变化。
有了线性表的基础,我们很容易写出以下代码

	int main()//初始化一个栈
	{
		stack Stack01;
		Stack01.data=0;
		Stack01.next=NULL;
		int tempdata;
		stack *p;
		while(cin>>tempdata)
		{
			p=(stack*)malloc(sizeof(stack));
			p->data=tempdata;
		    ********
		}
	}

诶,你这代码怎么不全啊?
真不怪我,********处应该是让新的节点的指针指向上一个节点,然后把top指针指向最新的节点。也许你发现了,是不是还没有定义top指针?

由于不是顺序存储结构了,我们如果要找到节点位置就必须靠指针了,但是如果光靠一个指针,我们要得到栈的长度,岂不是又要遍历?干脆,我们构造一个结构体,让它即有top指针,又有栈的长度!

\\定义一个含top指针和栈长度length的结构体
struct sym_stack{
	stack *top;
	int len_stack;
};
\\接上之前*********省略的代码,在主函数中声明这个结构体并让top指向第一个节点的代码略
	p->next=sym_stack01->top;
	sym_stack01->top=p;
	sym_stack01->len_stack++;

2.消除一个栈
emmmm,我觉得和添加元素算法差不多,记得用free就行了嗷

3.将栈清空
从top开始,将栈结构体中数组的元素全部改为0。但是因为你还要找到这个栈,所以top指针不能移动,让一个新的指针取top的值再进行遍历

void clear_stack(stack* Stack01ptr,sym_stack *sym_stack01,int len_stack)
{
	while(len_stack--)
	{
		sym_stack01->topnow->data=0;
		sym_stack01->topnow=sym_stack01->topnow->next;
	}
}

4.判断栈是否为空
直接看len_stack的值不就好了?

5.若栈非空,则返回栈顶元素

int get_elem(sym_stack *sym_stack01)
{
	if(sym_stack01->len_stack==0)return ERROR;
	else return sym_stack->top->data;
}

6.为栈添加元素

void push(sym_stack *sym_stack01,int newdata)
{
	sym_stack01->len_stack++;
	stack *newstack=(stack*)malloc(sizeof(stack));
	newstack->data=newdata;
	newstack->next=sym_stack01->top;
	sym_stack01->top=newstack;
}

7.为栈删除元素

诶嘿,这里要判断栈是否为空

void push(sym_stack *sym_stack01)
{
	if(sym_stack01->len_stack==0)return ERROR;
	else
	{
		stack *tempptr=sym_stack01->top->next;
		free(sym_stack01->top);
		sym_stack01->top=tempptr;
	}
}

8.返回栈的元素个数

好办!告诉他len_stack等于多少就完事了

两栈共享空间

emmm,在介绍如何用栈实现四则运算之前,我们介绍一种可以节约空间的做法——两栈共享空间。
这个做法主要适用于顺序存储结构的栈,毕竟链栈基本不需要担心空间不够这个问题嘛

大致做法就是,将两个栈放入一个数组中,一个top初始为-1,另一个为数组长度;前者栈没压入一个元素就++,后者–;当两者相差1时,栈满。其他操作基本只需要小修改就可以照搬使用。

这种两栈共享空间的做法,一般用于:1、两栈的数据类型相同;2.两栈存放的元素间存在负相关关系,即这个多另一个就会相应减少,比如两者零和博弈。

栈的一个重要应用——四则运算

怎么说呢,我常常感慨有一些东西前辈们是如何想到的,而这个用栈实现计算机的四则运算就是我很惊讶的一点。

算式的后缀表示法

给一个式子:(89+111)/(240/24),让计算机去算,它会吗?
是的,它会!
但是呢,它并不是直接计算这个式子,而是把这个式子由我们熟悉的中缀表达式换成后缀表达式
转换的原则:从左到右遍历表达式的数字和运算符,如果是数字,直接输出,如果是**(或者是运算符,但是其运算优先级高于栈顶的运算符或者栈顶是(,则压入栈中;如果是运算符且运算优先级不高于栈顶的运算符,则直接输出;如果是),就将栈顶元素一个个弹出,直到弹出与之匹配的(**为止,不过括号不输出到后缀表达式中。

我们来实操一下吧!
s1.栈:(
表达式:无
s2.栈:(+
表达式:89
s3.栈:(+)
表达式:89 111
s4.栈:/(
表达式:89 111 +
s5.栈:/(/
表达式:89 111 + 240
s6.栈:/(/)
表达式:89 111 + 240 24
s7.栈:/
表达式:89 111 + 240 24 /
s8.栈:
表达式:89 111 + 240 24 / /

最终,我们得到原式的后缀表达式为:89 111 + 240 24 / /

如何计算后缀表达式得到结果

后缀表达式的计算原则是:遇到数字就进栈,遇到运算符就弹出两个数字进行该运算,如果是减法和除法,先弹出的作为减数和除数,后弹出的作为被减数和被除数。得到的结果再压入栈中。
好,89,111,进栈;
读到+,弹出89和111,做加法,得到200压入栈中
240,24,进栈;
读到/,弹出240和24,做除法,得到10,进栈;
读到/,弹出10和200,做除法,得到20,进栈;
读完了,弹出20,结果即为20!
此外,你还可以根据运算法则,拓展更广阔的计算!

栈多有趣呀~

写在后面的话

其实我在大一下学期自学C++的时候,就稍微接触了栈,当时mooc上是在讲如何用类模板构造一个栈,不过当时学业压力紧再加上宅家没心思学习,所以听得不很认真。
当时其实有一个很大的疑惑:就是栈到底有什么用?确实,到现在,我也只接触了递归和四则运算这两个用到了栈的实例,哦,突然想起来之前学C的时候有一个检查括号匹配的题目应该也可以用到栈。但是,如果只是这些小小的地方用到栈,栈似乎……没多大用处?
不过现在有点想明白了,就像数组、链表一样,栈只是一种数据结构,它适用于一切需要用到它先进后出的特性的情况。现在觉得没啥用,只是我还没有见到足够多的情况。
这么说来,每一种数据结构都有其独到的用法。我开始自学两天了,分别学了线性表和栈,接下来还有队列、图、树这些在等着我,就像卡池里还有无数SSR在等着我去捞……这样想想,还有些小激动呢。
最后,还是得说一句,看我的博客也就图一乐,真要学知识,还是多看看大牛的博客,或者买本教材系统的学吧。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值