数据结构:栈

本文详细介绍了数据结构中的栈,包括栈的基本概念、操作、链表及数组实现方式,以及栈在括号匹配问题和递归中的应用。通过实例展示了如何用C语言实现栈的各种操作,帮助读者掌握栈的精髓。

数据结构:栈

​ 栈是限制在表的一端进行插入和删除操作的一种数据结构。由于插入和删除均在表的一端实现,因此栈具有后进先出LIFO(Last In First Out)的特性。

相关概念及操作

概念
  1. 栈顶(Top):允许进行插入和删除操作的一端,也称作表尾。栈顶指针(top)指示栈顶元素
  2. 栈底(Bottom):为一固定端,又称作表头。
操作
  1. 创建一个栈:CreateStack()
  2. 判断栈是否为空:IsEmpty(Stack S)
  3. 清空栈操作:MakeEmpty(Stack S)
  4. 向栈中插入元素(进栈):Push(Stack S, ElementType data)
  5. 获取栈顶元素:GetTopOfStack(Stack S)
  6. 删除栈顶元素(出栈):Pop(Stack S)

栈的实现

​ 前面提到,栈是一个表,故任何实现表的方式都可以用来实现栈。对于删除和插入操作而言,链表是一种较为合适的实现方式。但我们可以看到,对栈进行的最频繁的操作:插入和删除都发生在表尾,并不涉及对结构中数据的移动。因此我们也可以考虑用数组来实现栈。

栈的链表实现

​ 以下内容参考于《数据结构与算法分析:C语言描述》

相关声明

struct Node;
typedef struct Node *PtrToNode;
typedef PtrToNode Stack;
typedef int ElemntType;     //此处,栈中的数据类型以int为例

struct Node
{
    ElemntType Element;
    PtrToNode Next
};

int IsEmpty( Stack S );
Stack CreateStack( void );
void MakeEmpty( Stack S );
void Push( Stack S, ElemntType X);
ElemntType GetTopOfStack( Stack S);
void Pop( Stack S);
  1. 创建一个栈

    ​ 在这里,S->Next指向的是表尾元素。

    Stack CreateStack( void )
    {
        Stack S;
    
        S = malloc( sizeof( struct Node ));
        if ( S == NULL )
            ERROR( "Out Of sapce" );
        S->Next = NULL;
        MakeEmpty(S);
        return S;
    }
    
  2. 判断栈是否为空

    int IsEmpty( Stack S)
    {
        return S->Next == NULL;
    }
    
  3. 清空栈

    void MakeEmpty( Stack S )
    {
        if ( S == NULL )
            ERROR( "Must create a Stack first" );
        else
        {
            while ( !IsEmpty( S ) )
                Pop( S );
        }
    }
    
  4. 获取栈顶元素

    ElemntType GetTopOfStack( Stack S )
    {
        if ( !IsEmpty( S ))
            return S->Next->Element;
        ERROR( "The Stack is empty!" );
        return 0;
    }
    
  5. 进栈

    ​ 关于栈的插入和删除操作,我能够想到的是:遍历整个栈直到找到栈顶(的直接前驱节点),这在时间上划不来;定义的一个指针指向栈顶的直接前驱节点,那么我们就需要:一个指向栈的指针以便我们能够使用栈,一个指向栈顶的指针以便我们能够获得栈顶元素和进行出栈操作,一个栈顶节点的直接前驱节点以便我们能够实现压栈操作。这在空间上划不来。

    ​ 此处,S是一个指向表尾的指针,仅需一个指针我们就能够实现对栈的任何操作。依据栈的特性,我们并不需要对表头进行任何操作,因此可以省去指向表头的指针。事实上,我们利用S及Next仍可对栈中的任意元素进行访问,虽然我们并不需要这些操作。抽象的表尾其实是实际的表头,抽象的表头是实际的表尾。理解可能略有偏颇。

    void Push( Stack S, ElemntType X )
    {
        PtrToNode TmpCell;
    
        TmpCell = malloc( sizeof( struct Node));
        if (TmpCell == NULL)
            ERROR(" Out of space");
        else
        {
            TmpCell->Element = X;
            TmpCell->Next = S->Next;
            S->Next = TmpCell;
        }
    }
    
  6. 出栈

    void Pop( Stack S )
    {
        PtrToNode FirstCell;
    
        if ( IsEmpty( S ))
            Erro("The Stack is Empty");
        else
        {
            FirstCell = S->Next;
            S->Next = S->Next->Next;
            free( FirstCell );
        }
    }
    
栈的数组实现

​ 用链表实现栈“缺点在于对malloc和free的调用的开销是昂贵的,特别是与指针操作的例程相比”,书中如是说。

​ 通过数组我们也可实现栈。对于数组而言,其大小在创建阶段就已经被固定,后续我们不能够对数组的大小进行更改。对于以数组形式实现的栈,可能出现的问题就是”数组不够用“。当然,我们可以声明一个合适的足够大的数组,保证在绝大部分的情况下”数组够用“。对这种形式实现的栈的操作要简单的多。我们可以通过下标TopOfStack实现对栈顶元素的访问,移动TopOfStack(即TopOfStack±1TopOfStack\pm 1TopOfStack±1)即可实现进栈和出栈操作。

​ 嗯,黑皮书果然是黑皮书,字字珠玑。本以为:

#define MaxElements (n) 	//n取任一正整数
ElementType array[MaxElements];
int TopOfStack = -1;

就可以完成栈的初始工作了……对于初学者而言,多接触高阶玩家的观点确实大有裨益:将原以为复杂的东西精简化,将原以为简单的东西细节化。看看Mark Allen WeissMark \ Allen \ WeissMark Allen Weiss如何以数组形式实现栈。

struct StackRecord;
typedef struct StackRecord *Stack;
typedef int ElementType;

int IsEmpty( Stack S );
int IsFull( Stack S );
Stack CreateStack( int MaxElements );
void DisposeStack( Stack S );
void MakeEmpty( Stack S );
void Push( Stack S, ElementType X );
void Pop( Stack S );
ElementType GetTopOfStack( Stack S );

#define EmptyTOS ( -1 )
#define MinStackSize ( n )		//n取任一正整数

//将栈涉及到的各类元素统一规整,形成一个结构整体
struct StackRecord
{
    int Capacity;		//栈的域大小
    int TopOfStack;
    ElementType *Array
};
  1. 栈的创建

    Stack CreateStack( int MaxElements )
    {
        Stack S;
    
        if ( MaxElements < MinStackSize )
            ERROR( "Stack is too small" );
        
        S = malloc( sizeof( struct StackRecord ) );
        if ( S == NULL )
            ERROR( "Out of space " );
    
        S->Array = malloc( sizeof( ElementType ) * MaxElements );
        if (S->Array == NULL )
            EEROR( "Out of space" );
        S->Capacity = MaxElements;
        MakeEmpty( S );
    
        return S;
    }
    
  2. 将栈初始化

    void MakeEmpty( Stack S )
    {
        S->TopOfStack = EmptyTOS;
    }
    
  3. 判断栈是否为空

    int IsEmpty( Stack S )
    {
        return S->TopOfStack == EmptyTOS;
    }
    
  4. 判断栈是否已满

    int IsFull( Stack S )
    {
        return S->TopOfStack == S->Capacity;
    }
    
  5. 获取栈顶元素

    ElementType GetTopOfStack( Stack S )
    {
        if ( !IsEmpty( S ))
            return S->Array[S->TopOfStack];
        ERROR( "The Stack is empty" );
        return 0;
    }
    
  6. 压栈

    void Push( Stack S, ElementType X )
    {
        if ( IsFull( S ) )
            ERROR( "The Stack is full" );
        else
            S->Array[++S->TopOfStack] = X;
    }
    
  7. 退栈

    void Pop( Stack S )
    {
        if ( IsEmpty( S ))
            ERROR( "The Stack is empty" );
        else
            S->TopOfStack--;
    }
    
  8. 释放栈结构体

    void DisposeStack( Stack S )
    {
        if (S != NULL )
        {
            free( S->Array );
            free( S );
        }
    }
    

​ 以链表实现栈,其释放栈的操作与出栈类似,只需使用一个循环将所有节点free()即可。

栈的应用

​ 好了,你已经学会栈的搭建了,快去写题吧!

括号匹配问题

​ 下面以括号匹配问题展示栈的简单应用。

判断包含有4种括号{,[,<(,),>,],}的字符串是否是合法匹配。

例如以下是合法的括号匹配:

(),   [ ],  (()),   ([ ]), ()[ ],   ()[()]

以下是不合法的括号匹配:

(,   [,  ],   )(,   ([ ]},  ([(),{( })

​ 首先我们思考:对于一系列括号,怎样算是匹配成功呢?换个思路,匹配不成功的情形有哪几种呢?

  • 给定的括号数量为奇数
  • 对于相邻的两个括号而言,若右括号左端不是对应的左括号(当然,右括号左端是右括号另说)则匹配不成功。

​ 或许还有其它情形。对于情形2而言,当括号序列中第一个右括号左端不是对应左括号,则匹配不成功;若两括号成功匹配,我们不妨“擦掉”这两个括号,继续对“第一个”右括号及其左端括号进行比对……如此进行下去,最终一定得到结果。诚然,将括号序列保存至数组中,不断进行遍历、比较和删除操作,最终是能够得到结果的。假如给定的括号序列合法,有n对括号,仅查找操作而言,操作次数就达到了(n+1)+(n)+(n−1)+……+2=n(n+3)2次(n+1)+(n)+(n-1)+……+2=\frac{n(n+3)}{2}次(n+1)+(n)+(n1)++2=2n(n+3),这无疑是十分耗费时间的。

​ 在《数据结构和算法分析:C语言描述》中,对此问题,作者给出了较为官方的表达:

1. 做一个空栈
2. 读入字符直到文件尾。如果字符是一个开放符号,则将其推入栈中,如果字符是一个封闭符号,则栈空时报错
3. 否则,将栈元素弹出。如果弹出的符号不是对应的开放符号,则报错
4. 在文件尾,如果栈非空则报错

​ 为解决这一问题,仅仅依靠前文定义的栈是不够的,我们需要定义两个函数:判断给定的括号是左括号还是右括号;右括号和相应的左括号进行匹配。

​ 首先,我们定义两个字符串数组储存左括号和右括号。

char left_brackets[] = "{[<(";
char right_brakcets[] = "}]>)";

​ 定义函数判断给定括号是否在指定的括号集合中

int IsExist( char bracket, char brackets[])
{
    int i = 0;
    for ( i = 0; i < strlen(brackets) ; i++ )
    {
        if ( bracket == brackets[i] )
            return 1;
    }
    return 0;
}

​ 事实上,我们没有办法对弹出的栈元素和给定的括号进行比较,比较结果定然是不等的。换个思路,获得弹出栈元素在对应括号集合中的下标,以此下标找到相匹配括号,与给定括号进行比对。

int GetIndexOfBracket( char bracket, char brackets[])
{
    if ( IsExist( bracket, brackets ) )
    {
        int index = -1;
        int i = 0;
        for ( i = 0; i < strlen(brackets); i++ )
        {
            if ( bracket == brackets[i] )
            {
                index = i;
                return index;
            }
        }
    }
    printf("Wrong");
    return -1;
}

​ 整体代码

int main()
{
    char left_brackets[] = "{[<(";
    char right_brakcets[] = "}]>)";

    char brackets[200] = {'\0'};
    scanf( "%s", brackets );

    Stack S = CreateStack( strlen(brackets) );
    int i = 0;
    for ( i = 0; i < strlen(brackets); i++ )
    {
        if ( IsExist(brackets[i], left_brackets ) )
            Push( S, brackets[i] );
        else
        {
            char tmp_bracket = GetTopOfStack( S );
            Pop( S );
            int tmp_index = GetIndexOfBracket( brackets[i], right_brakcets );
            if ( tmp_bracket != left_brackets[tmp_index] )
            {
                printf("Fail");
                return 0;
            }
        }
    }
    if ( !IsEmpty( S ) )
    {
        printf("Fail");
        return 0;
    }

    printf("Successful");
    return 0;
}
栈与递归

​ 递归其实是“栈+循环”。如果你学过汇编语言,那么你对递归的一定会有更加深的理解。有兴趣可以尝试使用汇编语言编写递归程序,如:斐波那契数列,递归实现阶乘。当然你也可以尝试使用栈+循环的方式编写快速排序。

最后的话

​ 由于知识体系的不完备,目前只是一个知识的搬运工,顺便夹带些许个人想法。技疏学浅,不保证上述代码的正确性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值