数据结构-顺序栈
- 顺序栈的描述与表示
- 顺序栈的操作接口定义
- 顺序栈的操作实现
顺序栈的描述与表示
栈
栈(Stack)是一种只允许在序列末端进行操作的线性结构。栈实现的是一种后进先出(Last In First Out, LIFO)策略或称先进后出(First In Last Out, FILO),被删除的是最近插入的元素。栈与手枪中使用弹夹相似。在向弹夹装填子弹时,只能逐颗将子弹压入弹夹,最后压入的子弹位于弹夹顶部;射击时,弹夹顶部的子弹将最先被射出。
顺序栈
采用顺序存储结构的栈称为顺序栈,需要用一片地址连续的存储空间来存储栈的元素。根据这个特性,我们很容易想到通过一个简单的一维数组来实现这种结构,并指定栈顶位于序列末端。当有新元素入栈或栈顶元素出栈我们只需要改变栈顶位标即可。 C语言中一维数组的大小是预先确定的,存满后无法继续插入新元素,所以我们将采用动态分配连续一块连续空间来存储栈元素,栈满时,通过重新分配更大空间进行扩容。
顺序栈类型定义如下: 1
2
3
4
5
6typedef struct {
ElemType * base; // 存储空间的基址
int top; // 栈顶元素的下一个位置,简称栈顶位标
int size; // 当前分配的存储容量
int increment; // 扩容时,增加的存储容量
} SqStack; //顺序栈top
设置为栈顶元素的下一个位置,也有些做法是将栈顶位标设置为栈顶元素的位置,或者将其设置为指向栈顶元素位置的指针。本人查阅了一些资料,似乎没有看到关于这一点孰优孰劣的评论或分析,如有读者知晓,望指教。
## 本文中预定义的说明 ### 预定义常量和类型 1
2
3
4
5
6
7
8//函数结果状态代码
typedef int Status; // 用作函数值类型,表示函数结果状态typedef
)描述。数据元素类型约定为ElemType,读者在使用该数据类型时自行定义,如int
、char
等简单类型。
扩展引用调用
为了便于算法描述,我们使用了 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);
1 | Status InitStack_Sq(SqStack &S, int size, int inc) |
注意: 当调用内存分配函数时,存在找不到足够大小的内存块的可能性,这时函数将返回空指针。因此在我们分配顺序栈基址时应当测试内存分配函数的返回值,并在返回空指针时采取相应操作,试图通过空指针访问内存的效果是未定义的,程序可能会崩溃或者出现不可预测的行为。
销毁顺序栈
通过调用 free
函数来释放 InitStack_Sq
中分配的空间。 1
2
3
4void DestoryStack_Sq(SqStack &S)
{
free(S.base);
}free
函数的实际参数必须是先前由内存分配函数返回的指针,如果这个指针已经被修改,可能将导致未定义的行为。而若参数为空指针,函数free
的调用将不起到任何作用。
顺序栈判空
我们在顺序栈的类型定义中定义了栈顶位标,因此判断顺序栈是否为空,只需要判断栈顶位标是否为 0 即可。 时间复杂度为\(O(1)\).
1 | Status StackEmpty_Sq(SqStack S) |
清空顺序栈
清空顺序栈 S 即将 S
置为空栈,只要对栈顶位标进行修改即可,原来的元素仍在数组里,但已经不在栈里了。
时间复杂度为\(O(1)\). 1
2
3
4void 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
16Status 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
9Status Pop_Sq(SqStack &S, ElemType &e)
{
if (S.top <= 0)
return ERROR;
S.top--;
e = S.base[S.top];
return OK;
}1
2
3
4
5
6
7
8Status GetTop_Sq(SqStack S, ElemType &e)
{
if (S.top <= 0)
return ERROR;
e = S.base[S.top - 1];
return OK;
}
“栈”的原意是”干草堆积如山“。干草堆时用来临时保存家禽饲料的方式,在取用时,最后堆的干草会被最先取出来。而在程序中也是类似的,为了实现临时保存数据的目的,使用类似于干草推的机制,这种机制体现在内存上,就是栈。当我们需要暂时舍弃当前的数据,随后再原貌还原时,就会使用栈。
参考资料
- 《数据结构》(高教版,吴伟民,李小妹)
- 《算法导论》(第三版)
- 《C语言程序设计现代方法》(第二版)
- 《程序是怎样跑起来的》