数据结构-顺序栈

  • 顺序栈的描述与表示
  • 顺序栈的操作接口定义
  • 顺序栈的操作实现

顺序栈的描述与表示

栈(Stack)是一种只允许在序列末端进行操作的线性结构。栈实现的是一种后进先出(Last In First Out, LIFO)策略或称先进后出(First In Last Out, FILO),被删除的是最近插入的元素。栈与手枪中使用弹夹相似。在向弹夹装填子弹时,只能逐颗将子弹压入弹夹,最后压入的子弹位于弹夹顶部;射击时,弹夹顶部的子弹将最先被射出。

顺序栈

采用顺序存储结构的栈称为顺序栈,需要用一片地址连续的存储空间来存储栈的元素。根据这个特性,我们很容易想到通过一个简单的一维数组来实现这种结构,并指定栈顶位于序列末端。当有新元素入栈或栈顶元素出栈我们只需要改变栈顶位标即可。 C语言中一维数组的大小是预先确定的,存满后无法继续插入新元素,所以我们将采用动态分配连续一块连续空间来存储栈元素,栈满时,通过重新分配更大空间进行扩容。

顺序栈类型定义如下:

1
2
3
4
5
6
typedef struct {
ElemType * base; // 存储空间的基址
int top; // 栈顶元素的下一个位置,简称栈顶位标
int size; // 当前分配的存储容量
int increment; // 扩容时,增加的存储容量
} SqStack; //顺序栈
说明: 这里我们将栈顶位标top设置为栈顶元素的下一个位置,也有些做法是将栈顶位标设置为栈顶元素的位置,或者将其设置为指向栈顶元素位置的指针。本人查阅了一些资料,似乎没有看到关于这一点孰优孰劣的评论或分析,如有读者知晓,望指教。 ## 本文中预定义的说明 ### 预定义常量和类型
1
2
3
4
5
6
7
8
//函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define OVERFLOW -1

typedef int Status; // 用作函数值类型,表示函数结果状态
### 数据结构(存储结构)的表示 数据结构的表示用类型定义(typedef)描述。数据元素类型约定为ElemType,读者在使用该数据类型时自行定义,如intchar等简单类型。

扩展引用调用

为了便于算法描述,我们使用了 C++ 语言中的引用调用参数传递方式。

顺序栈操作接口定义

作为一个存储结构,自然需要关心元素的存取问题。栈上的 INSERT 操作称为压入(PUSH),而无元素参数的 DELETE 操作称为弹出(POP)。除了入栈、出栈操作,栈的操作还有初始化,销毁,判空等常用操作,我们定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 初始化顺序栈 S
void InitStack_Sq(SqStack &S, int size, int inc);
// 销毁顺序栈
Status DestoryStack_Sq(SqStack &S);
// 判断顺序栈 S 是否为空,若为空则返回 TURE,否则返回 FALSE
Status StackEmpty_Sq(SqStack S);
// 清空顺序栈 S (无元素,空栈,注意与 销毁顺序栈区别)
void ClearStack_Sq(SqStack &S);
// 元素压入顺序栈 S
Status Push_Sq(SqStack &S, ElemType e);
// 顺序栈 S 栈顶元素出栈,并用 e 返回
Status Pop_Sq(SqStack &S, ElemType &e);
// 取栈 S 的栈顶元素,并用 e 返回(与出栈区别)
Status GetTop_Sq(SqStack S, ElemType &e);
# 顺序栈操作的实现 ## 初始化顺序栈 根据顺序栈的类型定义,在顺序栈的初始化中,我们需要进行以下操作: * 分配存储空间 * 置栈为空栈 * 初始化栈的大小 * 初始化栈满时的增量 时间复杂度为\(O(1)\).

1
2
3
4
5
6
7
8
9
10
11
12
Status InitStack_Sq(SqStack &S, int size, int inc)
{
S.base = (ElemType*)malloc(size * sizeof(ElemType));
if (NULL == S.base)
/*allocation failed*/
return OVERFLOW;
S.top = 0;
S.size = size;
S.increment = inc;

return OK;
}

注意: 当调用内存分配函数时,存在找不到足够大小的内存块的可能性,这时函数将返回空指针。因此在我们分配顺序栈基址时应当测试内存分配函数的返回值,并在返回空指针时采取相应操作,试图通过空指针访问内存的效果是未定义的,程序可能会崩溃或者出现不可预测的行为。

销毁顺序栈

通过调用 free 函数来释放 InitStack_Sq 中分配的空间。

1
2
3
4
void DestoryStack_Sq(SqStack &S)
{
free(S.base);
}
注意: free 函数的实际参数必须是先前由内存分配函数返回的指针,如果这个指针已经被修改,可能将导致未定义的行为。而若参数为空指针,函数free的调用将不起到任何作用。

顺序栈判空

我们在顺序栈的类型定义中定义了栈顶位标,因此判断顺序栈是否为空,只需要判断栈顶位标是否为 0 即可。 时间复杂度为\(O(1)\).

1
2
3
4
5
6
7
Status StackEmpty_Sq(SqStack S)
{
if (S.top <= 0)
return TURE;
else
return FALSE;
}

清空顺序栈

清空顺序栈 S 即将 S 置为空栈,只要对栈顶位标进行修改即可,原来的元素仍在数组里,但已经不在栈里了。 时间复杂度为\(O(1)\).

1
2
3
4
void ClearStack_Sq(SqStack &S)
{
S.top = 0;
}
## 入栈 首先应检查此时是否已经栈满,若栈满,则应该扩容。由于栈顶位标指向的是栈顶元素的下一个位置,因此只需要将新的元素放入栈顶位标 S.top 所指示的位置,并将栈顶位标加一即可。 时间复杂度为\(O(1)\).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Status Push_Sq(SqStack &S, ElemType e)
{
ElemType* newbase;

if (S.top >= S.size) {
newbase = (ElemType*)realloc(S.base, (S.size + S.increment) * sizeof(ElemType));
if (NULL == newbase)
return OVERFLOW;
S.base = newbase;
S.size += S.increment;
}
S.base[S.top] = e;
S.top++;

return OK;
}
注意: * 判断是否栈满实际上只需要使用==即可,这里使用>=是为了防止误操作使栈顶位标大于S.size的情况。 * 如果realloc函数不能按要求扩大内存块,那么它将会返回空指针,而原有的内存块中的数据不会发生改变。 * 一旦realloc函数返回,一定要对内存块的所有指针进行更新,因为realloc函数可能会使内存块移动到了其他地方。在要求扩大内存块大小时,realloc会在「原先的内存块」上直接进行缩减,而不需要移动存储在内存块中的数据。如果无法扩大内存块(内存块后面的字节已经用于其他目的),realloc函数会在别处分配新的内存块,然后将旧的内存块中的内容复制到新的内存块中。

出栈

出栈前应该先检查栈是否非空,如果为空栈,则报错,否则用元素 e 返回,并对栈顶位标减一。 时间复杂度\(O(1)\).

1
2
3
4
5
6
7
8
9
Status Pop_Sq(SqStack &S, ElemType &e)
{
if (S.top <= 0)
return ERROR;
S.top--;
e = S.base[S.top];

return OK;
}
## 取栈顶元素 同样地,在取栈顶元素之前,我们需要先判断栈是否非空,若空则报错,否则用元素 e 返回栈顶元素的值,此时不需要改动栈顶位标。 时间复杂度\(O(1)\).
1
2
3
4
5
6
7
8
Status GetTop_Sq(SqStack S, ElemType &e)
{
if (S.top <= 0)
return ERROR;
e = S.base[S.top - 1];

return OK;
}
# 小结

“栈”的原意是”干草堆积如山“。干草堆时用来临时保存家禽饲料的方式,在取用时,最后堆的干草会被最先取出来。而在程序中也是类似的,为了实现临时保存数据的目的,使用类似于干草推的机制,这种机制体现在内存上,就是栈。当我们需要暂时舍弃当前的数据,随后再原貌还原时,就会使用栈。

参考资料

  • 《数据结构》(高教版,吴伟民,李小妹)
  • 《算法导论》(第三版)
  • 《C语言程序设计现代方法》(第二版)
  • 《程序是怎样跑起来的》