【数据结构】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;
}



相关文章
|
3天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
45 9
|
2天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
32 16
|
2天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
28 8
|
1天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
20 4
|
24天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
31 3
|
C语言
C语言OJ项目参考(1039) 小球自由下落
(1039) 小球自由下落 Description 一球从M米高度自由下落,每次落地后返回原高度的一半,再落下。它在第N次落地时反弹多高?共经过多少米?保留两位小数 Input M N Output 它在第N次落地时反弹多高?共经过多少米?保留两位小数,空格隔开,放在一行 Sample Input 1000 5 Sample Output 31.25 2875.00
1433 0
|
15天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
30 10
|
8天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
13天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
37 7
|
13天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
25 4