【数据结构初阶】栈接口实现及经典OJ题超详解

2024-09-24 16:22:25 浏览数 (1)

1. 概念与结构

栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

压栈:栈的插入操作叫做进栈/压栈/入栈入数据在栈顶

出栈:栈的删除操作叫做出栈。出数据也在栈顶

先进后出先进后出

1. 1 栈底层结构选型

栈的实现可以使用数组或者链表,但是相对而言数组的结构实现更优一些

因为栈是一个需要超高强度存取数据的数据结构,而数组尾插数据的代价比较小,比较合适。当然链表也是可以的。

2. 栈实现

2. 1 栈的定义

我们实现的栈应该是一个可以动态增长的栈,不然就会和静态顺序表一样会有许多的限制。

栈的底层是数组,那么也就是说这个数组应该是可以动态增长的,可以参考动态顺序表,我们需要一个capacity来存储数组的容量来判断需不需要扩容。

除此之外,我们应该怎么从栈中取出元素?那就需要把栈中有几个元素存储起来,但在这里,我们将其命名为top(注意top的值就是栈中存储元素的个数,不过我们将其理解为栈顶,其数值为栈中最后一个数据的上面一个位置的下标)。

代码语言:javascript复制
typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;		// 栈顶
	int capacity;	// 容量 
}Stack;

2. 2 栈的初始化

在最开始,我们需要将栈初始化,这里有2个步骤:

  1. a指向NULL
  2. topcapacity都置为0

这个初始化函数是在调用时先创建一个栈,再通过它的地址进行初始化。当然,也可以改成在函数内部动态开辟空间创建栈然后返回的形式,但要注意在销毁时应该将栈本身给free掉。

代码语言:javascript复制
void StackInit(Stack* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

2. 3 入栈

代码语言:javascript复制
void StackPush(Stack* ps, STDataType data);

在栈的最后面插入一个元素

有以下两个步骤:

  1. 判断容量是否足够,如果不够就扩容
  2. 对栈底层的顺序表进行尾插

在实现顺序表时,我们将检查容量这一步单独封装成了函数,因为这一步会被频繁调用,但在栈这里,只有入栈这一步需要用到检查容量,那就没有太大的必要单独封装了。

扩容时,和顺序表相似的,我们将容量扩大至原来的二倍,当然,也要注意容量为0的情况。

代码语言:javascript复制
void StackPush(Stack* ps, STDataType data)
{
	assert(ps);
	//判断容量是否足够
	if (ps->capacity == ps->top)
	{
		//容量不够,扩容至2倍
		int newcapacity = 2 * ps->capacity;
		if (ps->capacity == 0)
			newcapacity = 2;
		STDataType* newa = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
		if (!newa)
		{
			perror("realloc");
			exit(1);
		}
		ps->a = newa;
		ps->capacity = newcapacity;
	}
	//顺序表的尾插,top就是需要插入的位置的下标
	ps->a[ps->top  ] = data;
}

2. 4 判空

代码语言:javascript复制
int StackEmpty(Stack* ps);

判断栈中是否有元素直接判断**top**是否等于0并返回就可以了。

这个函数是为接下来的几个接口做准备。

代码语言:javascript复制
int StackEmpty(Stack* ps)
{
	assert(ps);
	return ps->top == 0;
}

2. 5 出栈

代码语言:javascript复制
void StackPop(Stack* ps);

出栈是从栈顶出栈,也就是删除栈顶元素,也只能从栈顶,对于栈来说,访问除栈顶之外的元素都是违法的

出栈就相当于顺序表的尾删,直接top--就可以了,但是在出栈之前要确保栈不为空

代码语言:javascript复制
void StackPop(Stack* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));	//保证栈不为空
	ps->top--;	
}

2. 6 取栈顶元素

代码语言:javascript复制
STDataType StackTop(Stack* ps);

直接将栈顶元素返回就可以了,注意top是栈顶的下一个元素的下标。

还要注意对栈进行判空,如果栈为空,那么这个函数就没有意义。

代码语言:javascript复制
STDataType StackTop(Stack* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));
	return ps->a[ps->top - 1];
}

2. 7 栈大小

获取栈中数据个数,其实就是直接返回top的数值。

代码语言:javascript复制
int StackSize(Stack* ps)
{
	assert(ps);
	return ps->top;
}

2. 8 栈销毁

代码语言:javascript复制
void StackDestroy(Stack* ps);

栈中只有数组是通过动态内存管理得到的,只需要将其释放,再将其他的数据都置为0就可以了。

代码语言:javascript复制
void StackDestroy(Stack* ps)
{
	assert(ps);
	free(ps->a);
	ps->capacity = 0;
	ps->top = 0;
}

注意:如果函数的创建是在初始化函数内部动态开辟空间创建栈然后返回的形式,应该传入二级指针,将ps本身给释放掉。

代码语言:javascript复制
void StackDestroy(Stack** pps)
{
	assert(pps && *pps);	//这两个指针都需要解引用,所以都要判空
	free((*pps)->a);
	free(*pps);
}

其实通过这个函数我们就能知道为什么不采用动态开辟的方法创建数组了,正常来讲我们向其它函数传入的都是一级指针,但是如果是动态开辟栈的话就需要传入二级指针了,传入时不统一就会带来不方便。

2. 9 打印

事实上,对栈的直接遍历打印是违法的,因为栈不允许访问除栈顶元素之外的任何元素,所以也就无法遍历,那么自然也就无法打印了。

但是可以通过循环取栈顶元素,出栈这两个步骤来模拟打印

代码语言:javascript复制
void StackPrint(Stack* ps)
{
	assert(ps);
	while (!StackEmpty(ps))
	{
		printf("%d ",StackTop(ps));
		StackPop(ps);
	}
}

当然,在打印之后,栈中就没有元素了。

3. 经典OJ题

3. 1 有效的括号

题目链接

11
代码语言:javascript复制
bool isValid(char* s);

有效的括号是怎样的?

([)]是吗?显然不是,也就是说在这道题中,括号的左右两括号之间没有其他括号或者有完整的其他括号时,才能算是有效的括号

这时其实就可以用到栈这样的结构,如果是左括号就入栈,是右括号就比较栈顶是不是它对应的左括号并出栈,直到遍历结束之后,再判断一下栈是否为空就可以了。

参考代码:

代码语言:javascript复制
//首先定义一个栈
typedef struct Stack
{
    char* data;
    int top;
    int capacity;
}Stack;

bool isValid(char* s) {
    Stack ST;
    //初始化
    ST.data=NULL;
    ST.top=ST.capacity=0;
    //遍历
    char* cur=s; 
    while(*cur)
    {
        if(*cur=='('||*cur=='['||*cur=='{')
        {
            //遇到左括号入栈
            if(ST.capacity==ST.top)
            {
                //扩容
                int newcapacity=2*ST.capacity;
                if(ST.capacity==0)
                    newcapacity=2;
                ST.data=(char*)realloc(ST.data,newcapacity*sizeof(char));
                ST.capacity=newcapacity;
            }
            ST.data[ST.top  ]=*cur;
        }
        else
        {
            if(ST.top==0)	//如果遇到右括号时栈为空,那就是没有匹配的左括号
                return false;
            
            //出栈并匹配(穷举匹配)
            if(*cur==')'&&ST.data[ST.top-1]=='('||
               *cur==']'&&ST.data[ST.top-1]=='['||
               *cur=='}'&&ST.data[ST.top-1]=='{')
                ST.top--;
            else
                return false;
        }
        cur  ;
    }
    //遍历完成后判空,看看有没有多余的左括号
    if(ST.top)
        return false;
    else
        return true;
}

当然,参考代码是在函数内部直接写出需要的栈接口,也可以把需要的栈接口先写到最前面,在这个函数内部直接调用

参考代码没有考虑栈的销毁,当然,为了严谨性,你可以想一下该怎么加上销毁。

栈这个结构的接口比较简单,其OJ题一般也是和其他的数据结构相结合的,所以放到后面的博客中。

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!

我会持续更新更多优质文章

0 人点赞