【数据结构】C语言实现顺序栈 && OJ题 —— 有效的括号

简介: 【数据结构】C语言实现顺序栈 && OJ题 —— 有效的括号

这篇博客为大家带来的是 栈的概念简述、栈的概念选择题、栈的结构选择、C语言实现栈以及栈的一道OJ题。内容相对比较简单。话不多说,我们这就开始。



1. 栈的概念


是一个特殊的 线性表


栈只允许在固定的一段进行插入和删除元素的操作。进行数据插入和删除操作的一端称为栈顶,不进行操作的一端称为栈底。


栈中的元素遵守 后进先出 (LIFO - Last In First Out) 的原则。也就是先进的后出,后进的先出,就像手枪的弹匣一样。后被装入的子弹,也就是弹匣顶端的子弹先被枪打出。


栈对于数据的管理主要有两种操作:


   压栈:栈的插入操作叫做进栈 / 压栈 / 入栈,从栈顶进行压栈。

   出栈:栈的删除操作叫做 出栈,从栈顶进行出栈。


栈的操作流程:


12ddb0f9a4b348fe049231eece2bf3bc.png


栈的概念选择题:


   一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出


   栈的顺序是( )。


   A. 12345ABCDE

   B. EDCBA54321

   C. ABCDE12345

   D. 54321EDCBA

   答案:B


   首先明确栈的原则:后进先出。


   将以上元素依次入栈,那么最入栈的最晚出栈,那么1应该最后一个出栈,直接选出结果:EDCBA54321


   若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是()

   A. 1,4,3,2

   B. 2,3,4,1

   C. 3,1,4,2

   D. 3,4,2,1

   答案:C


   做对这道题目,我们需要知道,栈不是所有数据入栈后才能出栈的,栈可以入栈部分元素,然后出栈,再入栈其他元素。


   下面对每个选项进行分析:


   A:1 先入栈,然后出栈,栈空;随后 2, 3, 4 依次入栈。然后将元素全部出栈,栈空。得到结果为:1, 4, 3, 2


   B:1, 2 先入栈;然后出栈 2,栈中余1;再入栈 3,出栈 3,栈中余1;再入栈 4,出栈 4,栈中余1;最后出栈 1,栈空。得到结果为:2, 3, 4, 1


   C:这种序列答案是绝对不可能的。通过 A、B两个选项和这道题的进栈序列我们也可以找出规律:某个元素的两个相邻元素必定有一个相邻元素与该元素差值为1。否则的话就不符合栈的结构。因为如果第一个元素为3的话,那么就是先入栈 1,2,3,然后出栈。那么无论怎么出栈,第二个元素都不可能为1。2, 4 都有可能,如果还不明白可以画图再想想。


   D:1,2,3,先入栈;然后出栈3,栈中余2,1;再入栈 4,然后出栈 4,栈中余2,1;然后将栈中元素全部出栈。得到结果为:3, 4, 2, 1




2. 栈的结构


栈一般可以使用 数组或链表 实现。让我们分析一下使用这两种方法实现,栈的结构分别是什么样的。



在分析之前,我们要明确的一点是,栈只对 栈顶 的元素进行操作。


数组


对于数组(顺序表)而言,最方便的就是尾插和尾删,所以我们将 顺序表的尾部 当做 栈顶顺序表的头部 则当做 栈底,因为对于顺序表,头部的删除需要挪动大量数据。

01fb73f1c0b18ff161b8b9c454727901.png

链表

对于链表而言,尤其是 单链表,尾部的插入删除是很麻烦的。但是 单链表 的头插和头删就很方便,所以可以把 单链表的头部 作为栈顶,单链表的尾部 作为 栈底


0f277f931894296f9012804f3880045f.png

如果对于双向链表而言,那么就是随便选了,毕竟双向链表无论哪头插入删除数据都很方便。

抉择:


那么对于 顺序栈 和 链式栈 ,那个更加好呢?那必定是 顺序栈,因为使用顺序栈的 尾插尾删非常方便, 且 cpu缓存利用率也更高。而且对于顺序栈实现起来相对简单,所以我们接下来就实现 顺序栈 。




3. 栈的实现


3.1 结构设计


我们既然是实现 顺序栈,那么它的结构肯定就和 顺序表 差不多:

typedef struct Stack
{
  STDatatype* a; // 指向动态开辟的数组
  int capacity; // 栈的容量
  int top; // 标识 栈顶的下一个位置的下标 或 栈顶的下标
}ST;


这里的 top 我们需要好好理解一下。top的初始值不同时,top可以表示 栈顶的下一个位置的下标栈顶下标

  1. top = 0top 表示栈顶的下一个位置的下标:
  2. e36cb45daf939933b2f255fd16c56ff3.png

top 初始值为 0,那么第一次 压栈 就是在0下标插入元素。压栈后,top++。那么当 最后一次压栈后,元素被压在栈顶,那么top 最后的位置就是栈顶的下一个元素的下标处。此时,top就是栈中元素的个数。


  1. top = -1top 表示栈顶的下标:

dd0a7bbd181572e40e8a36b2b92ae717.png


top 初始值为 -1,那么需要先 ++top,再压栈。否则会越界。当 最后一次压栈时,为先 ++top 再压栈,top最后的位置就是栈顶的下标处


这个需要理清楚,否则实现判空、计算大小等接口函数的时候会引起错误。



3.2 接口总览


由于 栈的结构操作规则,栈的接口相对来说比较少,且比较简单:


void StackInit(ST* ps); // 初始化
void StackDestroy(ST* ps); // 销毁
void StackPush(ST* ps, STDatatype x); // 压栈
void StackPop(ST* ps); // 出栈
STDatatype StackTop(ST* ps); // 取栈顶元素
bool StackEmpty(ST* ps); // 判空
int StackSize(ST* ps); // 计算栈的大小


3.3 初始化


我们实现的是顺序栈,那么就和顺序表一样,需要创建结构体变量,传结构体的地址,进行初始化。


在初始化的时候就给栈开上四个单位的空间,并且将起始容量设定为4。


注意了我们这里设定的 top = 0,那么表示 top 为栈顶的下一个位置的下标


void StackInit(ST* ps)
{
  // 结构体一定不为空,所以需要断言
  assert(ps);
  ps->a = (STDatatype*)malloc(sizeof(STDatatype) * 4);
  if (ps->a == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  ps->capacity = 4;
  ps->top = 0;
}



3.4 销毁


对于栈的销毁,那么我们就只需要释放动态开辟的空间,将指针置空。并将 capacity 和 top 两个变量置 0即可。

void StackDestroy(ST* ps)
{
  assert(ps);
  free(ps->a);
  ps->a = NULL;
  ps->capacity = ps->top = 0;
}



3.5 判断栈是否为空


我们起初设定 top = 0,所以判断栈是否为空,那么只需要看 top 是否为0就可以了。如果为0,返回真 ;不为0,返回假。

bool StackEmpty(ST* ps)
{
  assert(ps);
    // 如果 ps->top == 0,返回真
    // 如果 ps->top !=0,返回假
  return ps->top == 0;
}



3.6 压栈


在压栈之前,需要保证空间足够,所以需要先检查容量,如果 不够,需要扩容,扩容成功后在考虑压栈的步骤。


我们设定 top 的初始值为 0。那么说明我们入栈的步骤为,先将元素放入,再让 top++


479fe7afd74a81e1f11d47cfc063d00c.png

void StackPush(ST* ps, STDatatype x)
{
  assert(ps);
  // 检查容量
  if (ps->top == ps->capacity)
  {
    STDatatype* tmp = (STDatatype*)realloc(ps->a, sizeof(STDatatype) * ps->capacity * 2);
    if (tmp == NULL)
    {
      perror("realloc fail");
      exit(-1);
    }
    ps->a = tmp;
    ps->capacity *= 2;
  }
  // 插入元素
  // top 为栈顶的下一个元素
  // 先插入再 ++ 
  ps->a[ps->top++] = x;
}


3.7 出栈


如果栈中没有元素则不能出栈。所以我们需要调用 StackEmpty 判断是否为空,如果栈空(!StackEmpty(ps)为假),则断言报错。


出栈的操作和顺序表的尾删操作步骤相似,直接将top--即可。


19e0cece5520287282782dacab57d12f.png


void StackPop(ST* ps)
{
  assert(ps);
  // 如果栈空,则不能删除
  assert(!StackEmpty(ps));
  ps->top--;
}



3.8 取栈顶元素


由于我们 top 初值设定为 0,top为栈顶的下一个位置的下标,那么 top - 1 就是栈顶的下标,直接返回即可。


但是请注意:当栈为空时,无法取元素,所以需要判断一下


STDatatype StackTop(ST* ps)
{
  assert(ps);
  assert(!StackEmpty(ps));
  return ps->a[ps->top - 1];
}




3.9 计算栈的大小


如果一开始top = 0,那么栈的大小就直接是最后 top 的值。也非常简单~

int StackSize(ST* ps)
{
  assert(ps);
  return ps->top;
}




4. 完整代码


Stack.h

#pragma once
#include <stdbool.h>
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int STDatatype;
typedef struct Stack
{
  STDatatype* a;
  int capacity;
  int top;   // 初始为0,表示栈顶位置下一个位置下标
           // 初始为-1,表示栈顶位置的下标
}ST;
void StackInit(ST* ps);
void StackDestroy(ST* ps);
void StackPush(ST* ps, STDatatype x);
void StackPop(ST* ps);
STDatatype StackTop(ST* ps);
bool StackEmpty(ST* ps);
int StackSize(ST* ps);



Stack.c


这里我将 top = 0 和 top = -1 的方案都写了一遍,注释部分为 top = 0,未注释部分为 top = -1

#define _CRT_SECURE_NO_WARNINGS 1 
#include "Stack.h"
// top 为栈顶的下一个元素
//void StackInit(ST* ps)
//{
//  // 结构体一定不为空
//  assert(ps);
//
//  ps->a = (STDatatype*)malloc(sizeof(STDatatype) * 4);
//  if (ps->a == NULL)
//  {
//    perror("malloc fail");
//    exit(-1);
//  }
//  ps->capacity = 4;
//  ps->top = 0;
//}
//
//void StackDestroy(ST* ps)
//{
//  assert(ps);
//
//  free(ps->a);
//  ps->a = NULL;
//  ps->capacity = ps->top = 0;
//}
//
//void StackPush(ST* ps, STDatatype x)
//{
//  assert(ps);
//  
//  // 检查容量
//  if (ps->top == ps->capacity)
//  {
//    STDatatype* tmp = (STDatatype*)realloc(ps->a, sizeof(STDatatype) * ps->capacity * 2);
//    if (tmp == NULL)
//    {
//      perror("realloc fail");
//      exit(-1);
//    }
//    ps->a = tmp;
//    ps->capacity *= 2;
//  }
//  // 插入元素
//  // top 为栈顶的下一个元素
//  // 先插入再 ++ 
//  ps->a[ps->top++] = x;
//}
//
//void StackPop(ST* ps)
//{
//  assert(ps);
//
//  // 如果栈空,则不能删除
//  assert(!StackEmpty(ps));
//  ps->top--;
//}
//
//STDatatype StackTop(ST* ps)
//{
//  assert(ps);
//
//  assert(!StackEmpty(ps));
//
//  return ps->a[ps->top - 1];
//}
//
//bool StackEmpty(ST* ps)
//{
//  assert(ps);
//
//  return ps->top == 0;
//}
//
//int StackSize(ST* ps)
//{
//  assert(ps);
//
//  return ps->top;
//}
// top 为栈顶 初识值为 -1
void StackInit(ST* ps)
{
  // 结构体一定不为空
  assert(ps);
  ps->a = (STDatatype*)malloc(sizeof(STDatatype) * 4);
  if (ps->a == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  ps->capacity = 4;
  ps->top = -1;
}
void StackDestroy(ST* ps)
{
  assert(ps);
  free(ps->a);
  ps->a = NULL;
  ps->capacity = ps->top = 0;
}
void StackPush(ST* ps, STDatatype x)
{
  assert(ps);
  // 检查容量
  // 此时 top 一开始为 -1,不能表示栈中元素的数目
  // top + 1 才是正确的
  if (ps->top + 1 == ps->capacity)
  {
    STDatatype* tmp = (STDatatype*)realloc(ps->a, sizeof(STDatatype) * ps->capacity * 2);
    if (tmp == NULL)
    {
      perror("realloc fail");
      exit(-1);
    }
    ps->a = tmp;
    ps->capacity *= 2;
  }
  // 插入元素
  // top 为栈顶元素
  // 先 ++ 再插入
  ps->a[++ps->top] = x;
}
void StackPop(ST* ps)
{
  assert(ps);
  // 如果栈空,则不能删除
  assert(!StackEmpty(ps));
  ps->top--;
}
STDatatype StackTop(ST* ps)
{
  assert(ps);
  assert(!StackEmpty(ps));
  return ps->a[ps->top];
}
bool StackEmpty(ST* ps)
{
  assert(ps);
  return ps->top == -1;
}
int StackSize(ST* ps)
{
  assert(ps);
  return ps->top + 1;
}


test.c

#define _CRT_SECURE_NO_WARNINGS 1 
#include "Stack.h"
void TestST1()
{
  ST st;
  StackInit(&st);
  StackPush(&st, 1);
  StackPush(&st, 2);
  StackPush(&st, 3);
  StackPush(&st, 4);
  StackPush(&st, 5);
  StackPop(&st);
  StackPop(&st);
  StackPop(&st);
  StackPop(&st);
  printf("%d\n", StackTop(&st));
}
int main()
{
  TestST1();
}




5. OJ题 —— 有效的括号


由于我们刚刚学习了栈,而栈实现也比较简单,那么我们就趁热打铁做上一道OJ吧!


链接:20. 有效的括号


描述:


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

有效字符串需满足:


   左括号必须用相同类型的右括号闭合。

   左括号必须以正确的顺序闭合。

   每个右括号都有一个对应的相同类型的左括号。


示例1:

   输入:s = “()”

   输出:true


示例2:

   输入:s = “()[]{}”

   输出:true


示例3:

   输入:s = “(]”

   输出:false


提示:

   1 <= s.length <= 104

   s 仅由括号 '()[]{}' 组成


思路:


既然这道题目出现在这篇博客中,那么一定是用 栈 来解决,而且这道题目的解题思路是十分符合 栈 的。



首先,我们先要实现一个栈,并创建变量和初始化。

题目要求 左括号 需要以正确的顺序闭合,且左右括号成对,那么我们可以遍历字符串 s。


遍历过程中让 左括号入栈,一旦遇到 右括号 便 取栈顶元素 和右括号匹配,并 出栈元素。


一旦匹配失败,便返回 false。如果匹配成功,则让 s++ 往后走。


当字符串遍历结束时,判断栈是否为空,如果栈空,则说明为有效的括号;如果栈非空,则说明有左括号没有匹配,那么返回假。(这里只需要返回栈是否为空的值即可)


de701279f9f1ce1fb3fa3bfd81146525.png


但是也有一些 易错情况,比如字符串遍历结束栈中仍有元素

fd156c9c16227bf610fa37ac64a46783.png


只有右括号,无左括号,栈空,取元素时越界访问:



05f837b43397610922d39de5555e6584.png


只有右括号时为提前返回状况。提前返回需要注意栈的销毁,否则会内存泄漏 !!!


4f179108d322e4b74680c136878b0e57.png



代码

typedef int STDatatype;
typedef struct Stack
{
  STDatatype* a;
  int capacity;
  int top;   // 初始为0,表示栈顶位置下一个位置下标
}ST;
void StackInit(ST* ps);
void StackDestroy(ST* ps);
void StackPush(ST* ps, STDatatype x);
void StackPop(ST* ps);
STDatatype StackTop(ST* ps);
bool StackEmpty(ST* ps);
int StackSize(ST* ps);
void StackInit(ST* ps)
{
  // 结构体一定不为空
  assert(ps);
  ps->a = (STDatatype*)malloc(sizeof(STDatatype) * 4);
  if (ps->a == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  ps->capacity = 4;
  ps->top = 0;
}
void StackDestroy(ST* ps)
{
  assert(ps);
  free(ps->a);
  ps->a = NULL;
  ps->capacity = ps->top = 0;
}
void StackPush(ST* ps, STDatatype x)
{
  assert(ps);
  // 检查容量
  if (ps->top == ps->capacity)
  {
    STDatatype* tmp = (STDatatype*)realloc(ps->a, sizeof(STDatatype) * ps->capacity * 2);
    if (tmp == NULL)
    {
      perror("realloc fail");
      exit(-1);
    }
    ps->a = tmp;
    ps->capacity *= 2;
  }
  // 插入元素
  // top 为栈顶的下一个元素
  // 先插入再 ++ 
  ps->a[ps->top++] = x;
}
void StackPop(ST* ps)
{
  assert(ps);
  // 如果栈空,则不能删除
  assert(!StackEmpty(ps));
  ps->top--;
}
STDatatype StackTop(ST* ps)
{
  assert(ps);
  assert(!StackEmpty(ps));
  return ps->a[ps->top - 1];
}
bool StackEmpty(ST* ps)
{
  assert(ps);
  return ps->top == 0;
}
int StackSize(ST* ps)
{
  assert(ps);
  return ps->top;
}
bool isValid(char * s)
{
    ST st;
    StackInit(&st);
    while (*s)
    {
        // 左括号
        if (*s == '(' || *s == '[' || *s == '{')
        {
            // 入栈
            StackPush(&st, *s);
            s++;
        }
        else
        {
            // 右括号匹配,栈为空
            if (StackEmpty(&st))
            {
                // 防止内存泄漏
                StackDestroy(&st);
                return false;
            }
            // 取栈顶元素,并出栈
            STDatatype top = StackTop(&st);
            StackPop(&st);
            if ((*s == ')' && top != '(')
                || (*s == ']' && top != '[')
                || (*s == '}' && top != '{'))
            {
                return false;
            }
            else
            {
                s++;
            }
        }
    }
    // 判断遍历结束后,栈是否为空
    bool ans = StackEmpty(&st);
    // 销毁栈
    StackDestroy(&st);
    return ans;
}



相关文章
|
21天前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
33 1
|
1月前
|
存储 算法 搜索推荐
【趣学C语言和数据结构100例】91-95
本文涵盖多个经典算法问题的C语言实现,包括堆排序、归并排序、从长整型变量中提取偶数位数、工人信息排序及无向图是否为树的判断。通过这些问题,读者可以深入了解排序算法、数据处理方法和图论基础知识,提升编程能力和算法理解。
45 4
|
1月前
|
存储 机器学习/深度学习 搜索推荐
【趣学C语言和数据结构100例】86-90
本文介绍并用C语言实现了五种经典排序算法:直接插入排序、折半插入排序、冒泡排序、快速排序和简单选择排序。每种算法都有其特点和适用场景,如直接插入排序适合小规模或基本有序的数据,快速排序则适用于大规模数据集,具有较高的效率。通过学习这些算法,读者可以加深对数据结构和算法设计的理解,提升解决实际问题的能力。
43 4
|
23天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
44 5
|
21天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
50 1
|
C语言
C语言OJ项目参考(1039) 小球自由下落
(1039) 小球自由下落 Description 一球从M米高度自由下落,每次落地后返回原高度的一半,再落下。它在第N次落地时反弹多高?共经过多少米?保留两位小数 Input M N Output 它在第N次落地时反弹多高?共经过多少米?保留两位小数,空格隔开,放在一行 Sample Input 1000 5 Sample Output 31.25 2875.00
1447 0
|
19天前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
39 10
|
19天前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
42 9
|
19天前
|
存储 Unix Serverless
【C语言】常用函数汇总表
本文总结了C语言中常用的函数,涵盖输入/输出、字符串操作、内存管理、数学运算、时间处理、文件操作及布尔类型等多个方面。每类函数均以表格形式列出其功能和使用示例,便于快速查阅和学习。通过综合示例代码,展示了这些函数的实际应用,帮助读者更好地理解和掌握C语言的基本功能和标准库函数的使用方法。感谢阅读,希望对你有所帮助!
31 8
|
19天前
|
C语言 开发者
【C语言】数学函数详解
在C语言中,数学函数是由标准库 `math.h` 提供的。使用这些函数时,需要包含 `#include <math.h>` 头文件。以下是一些常用的数学函数的详细讲解,包括函数原型、参数说明、返回值说明以及示例代码和表格汇总。
40 6