数据结构:栈
栈是限制在表的一端进行插入和删除操作的一种数据结构。由于插入和删除均在表的一端实现,因此栈具有后进先出LIFO(Last In First Out)的特性。
相关概念及操作
概念
- 栈顶(Top):允许进行插入和删除操作的一端,也称作表尾。栈顶指针(top)指示栈顶元素
- 栈底(Bottom):为一固定端,又称作表头。
操作
- 创建一个栈:CreateStack()
- 判断栈是否为空:IsEmpty(Stack S)
- 清空栈操作:MakeEmpty(Stack S)
- 向栈中插入元素(进栈):Push(Stack S, ElementType data)
- 获取栈顶元素:GetTopOfStack(Stack S)
- 删除栈顶元素(出栈):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);
-
创建一个栈
在这里,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; } -
判断栈是否为空
int IsEmpty( Stack S) { return S->Next == NULL; } -
清空栈
void MakeEmpty( Stack S ) { if ( S == NULL ) ERROR( "Must create a Stack first" ); else { while ( !IsEmpty( S ) ) Pop( S ); } } -
获取栈顶元素
ElemntType GetTopOfStack( Stack S ) { if ( !IsEmpty( S )) return S->Next->Element; ERROR( "The Stack is empty!" ); return 0; } -
进栈
关于栈的插入和删除操作,我能够想到的是:遍历整个栈直到找到栈顶(的直接前驱节点),这在时间上划不来;定义的一个指针指向栈顶的直接前驱节点,那么我们就需要:一个指向栈的指针以便我们能够使用栈,一个指向栈顶的指针以便我们能够获得栈顶元素和进行出栈操作,一个栈顶节点的直接前驱节点以便我们能够实现压栈操作。这在空间上划不来。
此处,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; } } -
出栈
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
};
-
栈的创建
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; } -
将栈初始化
void MakeEmpty( Stack S ) { S->TopOfStack = EmptyTOS; } -
判断栈是否为空
int IsEmpty( Stack S ) { return S->TopOfStack == EmptyTOS; } -
判断栈是否已满
int IsFull( Stack S ) { return S->TopOfStack == S->Capacity; } -
获取栈顶元素
ElementType GetTopOfStack( Stack S ) { if ( !IsEmpty( S )) return S->Array[S->TopOfStack]; ERROR( "The Stack is empty" ); return 0; } -
压栈
void Push( Stack S, ElementType X ) { if ( IsFull( S ) ) ERROR( "The Stack is full" ); else S->Array[++S->TopOfStack] = X; } -
退栈
void Pop( Stack S ) { if ( IsEmpty( S )) ERROR( "The Stack is empty" ); else S->TopOfStack--; } -
释放栈结构体
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)+(n−1)+……+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;
}
栈与递归
递归其实是“栈+循环”。如果你学过汇编语言,那么你对递归的一定会有更加深的理解。有兴趣可以尝试使用汇编语言编写递归程序,如:斐波那契数列,递归实现阶乘。当然你也可以尝试使用栈+循环的方式编写快速排序。
最后的话
由于知识体系的不完备,目前只是一个知识的搬运工,顺便夹带些许个人想法。技疏学浅,不保证上述代码的正确性。
本文详细介绍了数据结构中的栈,包括栈的基本概念、操作、链表及数组实现方式,以及栈在括号匹配问题和递归中的应用。通过实例展示了如何用C语言实现栈的各种操作,帮助读者掌握栈的精髓。
5012

被折叠的 条评论
为什么被折叠?



