数据结构之栈以及栈的基本操作

文章目录

    • 前言
    • 进栈出栈的变化形式
    • 栈的实现
      • 栈的顺序存储结构:
      • 栈的链式存储结构:
      • 文件的创建
      • 栈结构的定义
      • 栈的初始化
      • 入栈
      • 出栈
      • 获取栈顶元素
      • 获取栈中有效元素个数
      • 检测栈是否为空
      • 销毁栈
    • 括号匹配问题

前言

栈是限定仅仅在表尾进行插入和删除操作的线性表。

在软件中,栈的这种后进先出数据结构的应用是非常普遍的。比如我们在使用浏览器时,浏览器都有一个“后退键”,你点击后可以按照访问顺序的逆序加载浏览过的网页。很多类似的软件,比如Word、Photoshop等文档或图像编辑软件中,都有撤销的操作,也是用栈这种方式来实现的。

我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何元素的栈称为空栈,栈又被称为后进先出(Last In First Out)的线性表,简称LIFO结构。

如下图:

网站的返回键:

数据结构之栈以及栈的基本操作_第1张图片

word里面的撤销键:

数据结构之栈以及栈的基本操作_第2张图片

还有很多涉及到栈的软件,这里就不一一说了,下面我们来看压栈和出栈

压栈栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈栈的删除操作叫做出栈。出数据也在栈顶。

数据结构之栈以及栈的基本操作_第3张图片

数据结构之栈以及栈的基本操作_第4张图片

动图演示进栈:

数据结构之栈以及栈的基本操作_第5张图片

数据结构之栈以及栈的基本操作_第6张图片

进栈出栈的变化形式

有个问题问问大家?最先进栈的元素,是不是就只能是最后出栈呢?

答案是不一定,栈对线性表的插入和删除的位置进行了限制,并没有对插入和删除的时间进行限制,所以,在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以。

我们举个例子:

现在有3个数字元素1、2、3一次进栈,会有那些的出栈次序呢?

第一种:1、2、3进,再3、2、1出,出栈次序为321

第二种:1进,1出,2进,2出,3进,3出,出栈次序为123

第三种:1进,2进,2出,1出,3进,3出,出栈次序为213

第四种:1进,2进,2出,3进,3出,1出,出战次序为231

第五种:1进,1出,2进,3进,3出,2出,出栈次序为132

有没有可能是312这种出栈顺序呢?答案是不可能的,要让3先出栈,那么123必须全部进栈,那么出栈顺序必须是321。

栈的实现

我们想一下栈的实现我们是使用顺序表实现呢还是链表实现呢?其实相对而言顺序表结构的实现会更优一些,因为数组在尾上插入和删除数据的代价比较小。

栈的顺序存储结构:

数据结构之栈以及栈的基本操作_第7张图片

栈的链式存储结构:

进栈示意图:

数据结构之栈以及栈的基本操作_第8张图片

出栈示意图:

数据结构之栈以及栈的基本操作_第9张图片

注意:

栈的链式结构不能让尾做栈顶,因为栈的操作都在栈顶,在栈顶进行进栈和出栈时,如果让尾做栈顶,那就相当于链表的尾插尾删,而链表的尾插尾删是需要遍历找尾和尾的前一个元素的,而栈这种结构我们是无法进行遍历的,所以我们需要让链表的头做栈顶。

因为栈的顺序表结构的实现会更优一些,因为数组在尾上插入和删除数据的代价比较小,所以我们实现顺序表结构的栈


文件的创建

我们首先创建三个文件:

数据结构之栈以及栈的基本操作_第10张图片

Stack.h对相关头文件的包含,以及实现栈的结构和函数的声明

Stack.c对实现栈的操作的函数进行定义

test.c文件进行栈相关函数和栈功能的测试


栈结构的定义

话不多说,我们开始实现栈,首先实现栈的进栈和出栈操作,我们需要先定义栈这样的一个结构体:

typedef int STDataType;
typedef struct Stack
{
     
	STDataType* a;//指向动态开辟的内存的指针
	int top;
	int capacity;
}Stack;

我们实现顺序表结构的栈,和博主前面讲的顺序表的实现一样,我们使用动态顺序表,这样大小灵活可变,我们这里定义一个指向动态开辟的内存的指针a,我们定义一个top来指示栈顶元素的位置,为了满足动态顺序表我们还需定义一个表示容量的变量。

下面我们来完成栈的这些操作:

// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);

栈的初始化

// 初始化栈
void StackInit(Stack* ps)
{
     
	assert(ps);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

数据结构之栈以及栈的基本操作_第11张图片

我们经过调试发现已经将栈已经初始化


入栈

// 入栈
void StackPush(Stack* ps, STDataType data)
{
     
	assert(ps);
	//检测容量
	if (ps->capacity == ps->top)
	{
     
		//增容
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		STDataType* temp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
		if (temp == NULL)
		{
     
			perror("realloc");
			return;
		}
		ps->a = temp;
		ps->capacity = newcapacity;
	}
	//操作入栈
	ps->a[ps->top] = data;
	ps->top++;
}

在入栈元素前,我们需要先考虑增容,仔细想想,当前top值等于我们的当前容量时我们要增容,我们在增容时,如果capacity是0时我们先开辟存储4个数据的内存,不是0时,我们就进行二倍的增容,如果开辟失败了,我们加一个判断pal->a是不是NULL,是NULL则打印错误返回,最后将capacity更新,入栈的话就简单了,因为top就是我们栈里面最后一个元素的下一个元素的下标,所以我们直接赋值即可,最后将top++。

下面我们调试一下入栈操作的代码:

数据结构之栈以及栈的基本操作_第12张图片

数据结构之栈以及栈的基本操作_第13张图片

image-20210806153609312

数据结构之栈以及栈的基本操作_第14张图片

发现没有问题,下面我们继续看接下来的操作:


出栈

// 出栈
void StackPop(Stack* ps)
{
     
	assert(ps);
	assert(!StackEmpty(ps));//栈不能为空
	ps->top--;
}

首先出栈,我们得栈不能为空,所以我们这里断言一下,这个判空得函数会在下面会写。出栈我们直接top–就可以了

我们进行调试代码:

数据结构之栈以及栈的基本操作_第15张图片
数据结构之栈以及栈的基本操作_第16张图片

发现此时top已经改变。


获取栈顶元素

// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
     
	assert(ps);
	return ps->a[ps->top - 1];
}

我们的top是栈中最后一个元素的下一个位置的下标,所以top-1就是最后一个元素的下标,我们将它返回即可


获取栈中有效元素个数

// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
     
	assert(ps);
	return ps->top;
}

top就是我们的元素个数。返回即可


检测栈是否为空

// 检测栈是否为空,如果为空返回true结果,如果不为空返回false
bool StackEmpty(Stack* ps)
{
     
	return ps->top == 0;
}

判断栈是否为空,当ps->top==0时,我们的栈是空的,这句话是真,则返回真,ps->top!=0时,这句话为假,则返回假。

销毁栈

// 销毁栈
void StackDestroy(Stack* ps)
{
     
	assert(ps);
	if (ps->a)
	{
     
		free(ps->a);
		ps->a = NULL;
	}
	ps->capacity = 0;
	ps->top = 0;
}

销毁栈,如果指向存储栈中元素空间的指针不为NULL的话,我们free掉这个指针,并将他置空,防止出现野指针,最好将top以及capacity都清0。

栈的元素的打印我们是没办法进行的,因为栈只能在栈顶操作,我们不能遍历它,我们可以这样打印,但是打印出来的元素只能是逆序的。

while (!StackEmpty(&s))
{
     
	printf("%d ", StackTop(&s));
	StackPop(&s);
}

数据结构之栈以及栈的基本操作_第17张图片

我们每次打印栈顶元素,打印为将它pop,可以打印出逆序的,但是此时栈已经空了

我们接下来来看一个栈的面试题:

括号匹配问题

点击括号匹配问题跳转到做题网站

问题描述:

给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

1、左括号必须用相同类型的右括号闭合。
2、左括号必须以正确的顺序闭合。

示例1:

输入:s = "()"输出:true

示例 2:

输入:s = "()[]{}"输出:true

示例 3:

输入:s = "(]"输出:false

示例 4:

输入:s = "([)]"
输出:false

示例 5:

输入:s = "{[]}"
输出:true

思路:

该题需要用到栈的功能,遍历字符串,遇到左括号就入栈,直到遇到右括号,然后取栈顶元素跟它比较是否匹配,不匹配返回false,匹配则继续

数据结构之栈以及栈的基本操作_第18张图片

我们把我们上面实现的栈的操作函数拷贝进去:

typedef char STDataType;
typedef struct StackNode
{
     
	STDataType *a;//存储动态开辟内存的指针
	int top;
	int capacity;
}Stack;

// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);

// 初始化栈
void StackInit(Stack* ps)
{
     
	assert(ps);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}
// 入栈
void StackPush(Stack* ps, STDataType data)
{
     
	assert(ps);
	//检测容量
	if (ps->capacity == ps->top)
	{
     
		//增容
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		STDataType* temp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
		if (temp == NULL)
		{
     
			perror("realloc");
			return;
		}
		ps->a = temp;
		ps->capacity = newcapacity;
	}
	//操作入栈
	ps->a[ps->top] = data;
	ps->top++;
}
// 出栈
void StackPop(Stack* ps)
{
     
	assert(ps);
	assert(!StackEmpty(ps));//栈不能为空
	ps->top--;
}
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
     
	assert(ps);
	return ps->a[ps->top - 1];
}
// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
     
	assert(ps);
	return ps->top;
}
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool StackEmpty(Stack* ps)
{
     
	return ps->top == 0;
}
// 销毁栈
void StackDestroy(Stack* ps)
{
     
	assert(ps);
	if (ps->a)
	{
     
		free(ps->a);
		ps->a = NULL;
	}
	ps->capacity = 0;
	ps->top = 0;
}


bool isValid(char * s)
{
     
    Stack st;
    StackInit(&st);
    while(*s)
    {
     
        if(*s=='('||*s=='['||*s=='{')
        {
     
            StackPush(&st,*s);
            s++;
        }
        else
        {
     
            char ch=StackTop(&st);
            StackPop(&st);
            if((*s==')' && ch!='(')
            ||(*s==']' && ch!='[')
            ||(*s=='}' && ch!='{'))
            {
     
                //不匹配
                return false;
            }
            else
            {
     
                //匹配
                s++;
            }
        }
    }
    return true;
}

我们按逻辑写下来然后提交发现有错误,这里我们就要看它的错误案例:

数据结构之栈以及栈的基本操作_第19张图片

它说只输入"["时输出的是true,但是正确答案是false,于是我们对照代码走一遍,发现我们将[入栈之后循环就结束了,返回了true,所以我们返回时不能这么写,我们应该写return StackEmpty(&st);上面循环走完,栈为空返回true,栈不为空返回false。

我们提交后还有一个错误:

数据结构之栈以及栈的基本操作_第20张图片

我们对照代码走一遍,发现我们栈里面还没有元素时,就取栈顶元素了,于是我们在前面加个判断:

if(StackEmpty(&st))
{
     
	return false;
}

当没有进栈元素时,直接返回false

我们改正后,发现可以通过了:

数据结构之栈以及栈的基本操作_第21张图片

此时别高兴太早,我们的代码还有问题,有什么问题呢?

是不是我们动态开辟的内存还没有释放啊,对,此时造成了内存泄漏。

所以我们最后应该这样改:

bool isValid(char * s)
{
     
    Stack st;
    StackInit(&st);
    bool match=true;
    while(*s)
    {
     
        if(*s=='('||*s=='['||*s=='{')
        {
     
            StackPush(&st,*s);
            s++;
        }
        else
        {
     
            if(StackEmpty(&st))
            {
     
                match=false;
                break;
            }
            char ch=StackTop(&st);
            StackPop(&st);
            if((*s==')' && ch!='(')
            ||(*s==']' && ch!='[')
            ||(*s=='}' && ch!='{'))
            {
     
                //不匹配
                match=false;
                break;
            }
            else
            {
     
                //匹配
                s++;
            }
        }
    }
    if(match==true)
    {
     
        match = StackEmpty(&st);
    }
    StackDestroy(&st);
    return match;
}

我们设定了match来保存真假,初始为真,进去循环那里比较时如果栈为空将match改为false,并且在不匹配那里也要将match改为false,循环出来,判断match是不是为真,为真的话将判断栈是不是空赋给它,是空则match为true,不是空则match为false,最后统一销毁栈,最后返回match。

关于栈就讲解到这里,觉得可以的话点个赞再走吧,欢迎大家学习交流

你可能感兴趣的:(数据结构,栈,数据结构)