数据结构-顺序栈

顺序栈的描述与表示

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

顺序栈

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

顺序栈类型定义如下:

typedef struct {
  ElemType * base; // 存储空间的基址
  int top; // 栈顶元素的下一个位置,简称栈顶位标
  int size; // 当前分配的存储容量
  int increment; // 扩容时,增加的存储容量
} SqStack; //顺序栈

说明:
这里我们将栈顶位标top设置为栈顶元素的下一个位置,也有些做法是将栈顶位标设置为栈顶元素的位置,或者将其设置为指向栈顶元素位置的指针。本人查阅了一些资料,似乎没有看到关于这一点孰优孰劣的评论或分析,如有读者知晓,望指教。

本文中预定义的说明

预定义常量和类型

//函数结果状态代码
#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)。除了入栈、出栈操作,栈的操作还有初始化,销毁,判空等常用操作,我们定义如下:

// 初始化顺序栈 S
Status 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).

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 中分配的空间。

Status DestoryStack_Sq(SqStack &S)
{
  free(S.base);

  return OK;
}

注意:
free 函数的实际参数必须是先前由内存分配函数返回的指针,如果这个指针已经被修改,可能将导致未定义的行为。而若参数为空指针,函数free的调用将不起到任何作用。

顺序栈判空

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

Status StackEmpty_Sq(SqStack S)
{
  if (S.top <= 0)
    return TURE;
  else
    return FALSE;
}

清空顺序栈

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

void ClearStack_Sq(SqStack &S)
{
  S.top = 0;
}

入栈

首先应检查此时是否已经栈满,若栈满,则应该扩容。由于栈顶位标指向的是栈顶元素的下一个位置,因此只需要将新的元素放入栈顶位标 S.top 所指示的位置,并将栈顶位标加一即可。
时间复杂度为O(1).

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)$.

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).

Status GetTop_Sq(SqStack S, ElemType &e)
{
  if (S.top <= 0)
    return ERROR;
  e = S.base[S.top - 1];

  return OK;
}

参考资料

  • 《数据结构》(高教版,吴伟民,李小妹)
  • 《算法导论》(第三版)
  • 《C语言程序设计现代方法》(第二版)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值