栈存储结构
引子 – 栈逻辑结构

栈依然是元素一个前驱,一个后继,典型的线性结构。
但是呢在操作上有了限制,插入删除只能从一端进行(后进先出LIFO),就像洗碗时叠的盘子,叠的时候从下往上,洗的时候从上往下(只能在上面一端出)。别说抽出来一个洗呐,小心砸碎。

n个不同元素进栈,出栈元素的不同排列个数为 1/(n + 1) * C(n,2n)。(阿Q推不出来)

栈的基本操作有:

  • 创建 InitStack(&S)
  • 判空 StackEmpty(S)
  • 判满 StackFull(S)
  • 获取栈顶元素 GetTop(S,&x)
  • 清空栈 StackClear(&S)
  • 销毁栈 DestroyStack(&S)
  • 进栈 Push(&S,x)
  • 出栈 Pop(&S,&x)

顺序栈

用数组来存咯,只不过要用到栈顶指针(可以用真的指针,也可以用逻辑上的指针i)

逻辑指针(静态分配)

存储类型表示

#define ElemType int
// typedef int ElemType;
// 顺序栈(逻辑指针)
#define MaxSize 50

typedef struct
{
    /* data */
    ElemType data[MaxSize];
    int top;
}SqStack;

操作

// C语言实现
#include<stdbool.h> // bool类型在这个头文件里
// 栈:顺序栈、链栈、共享栈
#define ElemType int
// typedef int ElemType;
// 顺序栈(逻辑指针)
#define MaxSize 50

typedef struct
{
    /* data */
    ElemType data[MaxSize];
    int top;
}SqStack;

// 该存储结构下的操作,分为 top从-1 / 0 开始,以下均为从0开始
void InitStack(SqStack *S) // 注意此处无法 SqStack &S,因为只要C++才有引用传递
{
    S->top = 0;
}

bool StackEmpty(SqStack S)
{
    return S.top == 0;
}

bool GetTop(SqStack S,ElemType *x)
{
    if(StackEmpty(S)) return false;
    *x = S.data[S.top - 1];  // 切记:此处top指向的栈顶元素的下一个
    return true;
}

void StackClear(SqStack S)
{
    // 逻辑上的情况即可。和创建栈一样
    S.top = 0;
}

void DestroyStack(SqStack *S)
{
    // 该部分 data[MaxSize]是静态存储,由系统自动释放,则无需操作
}

bool Push(SqStack *S, ElemType x)
{
    if(S->top == MaxSize) return false; //栈满
    S->data[S->top++] = x;  // 注意此处top从0开始(指向栈顶元素的上一个空间)
    return true;
}

bool Pop(SqStack *S, ElemType *x)
{
    if(StackEmpty(*S)) return false;
    *x = S->data[--S->top];
    return true; 
}

测试

int main(){
    SqStack S;
    InitStack(&S);
    Push(&S,1);
    Push(&S,2);
    Push(&S,3);
    Push(&S,4);
    Push(&S,5);
    int x;
    while(!StackEmpty(S))
    {
        Pop(&S,&x);
        printf("%d",x);
    }
    return 0;
}
/*
输出结果为:
54321
*/

阿Q感慨:C语言这也太麻烦了,C++有了引用传递后代码更好读。

咱们用C++快速实现以下(之后还是先用C,毕竟用了才知道C和C++区别还是不小的)

// C++完成
// C++ bool类型自带
typedef int ElemType;
// 顺序栈(逻辑指针)
#define MaxSize 50

typedef struct
{
    /* data */
    ElemType data[MaxSize];
    int top;
}SqStack;

// 该存储结构下的操作,分为 top从-1 / 0 开始,以下均为从0开始
void InitStack(SqStack &S)
{
    S.top = 0;
}

bool StackEmpty(SqStack S)
{
    return S.top == 0;
}

bool GetTop(SqStack S,ElemType &x)
{
    if(StackEmpty(S)) return false;
    x = S.data[S.top - 1];
    return true;
}

void StackClear(SqStack S)
{
    // 逻辑上的情况即可。和创建栈一样
    S.top = 0;
}

void DestroyStack(SqStack &S)
{
    // 该部分 data[MaxSize]是静态存储,由系统自动释放,则无需操作
}

bool Push(SqStack &S, ElemType x)
{
    if(S.top == MaxSize) return false; //栈满
    S.data[S.top++] = x;  // 注意此处top从0开始(指向栈顶元素的上一个空间)
    return true;
}

bool Pop(SqStack &S, ElemType &x)
{
    if(StackEmpty(S)) return false;
    x = S.data[--S.top];
    return true; 
}

真实指针 (动态分配)

存储类型表示

#define ElemType int

typedef struct
{
    /* data */
    ElemType *base; // 栈底指针,用于动态开扩空间
    ElemType *top;  // 栈顶指针,指向栈顶
    int StackSize;  // 可开扩的最大空间
}SqStack;

操作

// 此处直接用C++来写了
// 顺序栈(真实指针)
#include<cstdlib>
#define OVERFLOW -1 // exit状态码
#define MaxSize 50
#define ElemType int

typedef struct
{
    /* data */
    ElemType *base; // 栈底指针,用于动态开扩空间
    ElemType *top;  // 栈顶指针,指向栈顶
    int StackSize;  // 可开扩的最大空间
}SqStack;

bool InitStack(SqStack &S)
{
    // 需要动态分配空间了
    S.base = new ElemType[MaxSize];
    // c语言版:S.base = (ElemType *)malloc(MaxSize * sizeof(ElemType));
    // 判断是否分配成功
    if(!S.base) exit(OVERFLOW);
    S.top = S.base;
    S.StackSize = MaxSize;
    return true;
}

bool StackEmpty(SqStack S)
{
    // 逻辑上 类似于 顺序指针 top从0开始
    return S.top == S.base;
}

bool StackFull(SqStack S)
{
    return (S.top - S.base) == MaxSize;
    // 此处 S.top - S.base 则为当前数组中存的元素个数
}

bool GetTop(SqStack S, ElemType &x)
{
    if(StackEmpty(S)) return false;
    x = *(S.top - 1);
    return true;
}

void StackClear(SqStack &S)
{
    S.top = S.base;
}

void DestroyStack(SqStack &S)
{
    if(S.base){
        // 释放动态分配的空间
        delete S.base;
        // 要注意将 base和 top指针指向空,不然变成野指针了
        S.StackSize = 0;
        S.base = S.top = NULL;
    }
}

bool Push(SqStack &S, ElemType x)
{
    if(StackFull(S)) return false;
    *S.top++ = x;
    return true;
}

bool Pop(SqStack &S, ElemType &x)
{
    if(StackEmpty(S)) return false;
    x = *--S.top;
    return true;
}

共享栈

两个栈放在一起存了,要维护两个栈顶指针(一个是从右往左,一个是从左往右)

存储类型表示(建议用顺序栈存)

当然可以用链栈来存,但不合适,这种主要就是节省空间,两个栈放一块(空间给大点),也减少了任意一个栈上溢的风险。而链栈压根没这上溢顾虑,再拿链栈来存共享栈,阿Q觉得是脱裤子放屁。

// 阿Q只写静态存储(逻辑指针)的共享栈了 - 懒
#define ShareStackSize 100
typedef int Elemtype;

typedef struct
{
    /* data */
    Elemtype data[ShareStackSize];
    int top[2]; // top[0] - 从右往左;  top[1] - 从左往右
}ShareStack;

操作

// #include<cstdlib>
#define ShareStackSize 100
typedef int Elemtype;

typedef struct
{
    /* data */
    Elemtype data[ShareStackSize];
    int top[2]; // top[0] - 从右往左;  top[1] - 从左往右
}ShareStack;

void InitStack(ShareStack &SS)
{
    SS.top[0] = -1;  // 写一次 top 从 -1 开始
    SS.top[1] = ShareStackSize; // 那么接下来那个就是从 MaxSize 开始咯
}

// 对接下来的操作都要设定一个操作数,0 - 表示对 top[0]这边操作, 1 - 表示对 top[1]这边操作
bool StackEmpty(int operate, ShareStack SS)
{
    switch (operate)
    {
    case 0: return SS.top[0] == -1; break;
    case 1: return SS.top[1] == ShareStackSize; break;
    default:
        return false;
        /*
        激进做法(不推荐)
            exit(-1);  // -1状态码就说明操作数输错了
        */
    }
}

bool StackFull(ShareStack SS)
{
    return (SS.top[1] - SS.top[0] == 1); // 若此时 top1就在 top0的右侧,那说明塞不下啦
}

bool GetTop(int operate, ShareStack SS, Elemtype &x)
{
    if(operate < 0 || operate > 1) exit(-1);
    if(StackEmpty(operate,SS)) return false;
    x = SS.data[SS.top[operate]];
    return true;
}

void StackClear(ShareStack &SS)
{
    SS.top[0] = -1;
    SS.top[1] = ShareStackSize;
}

bool Push(int operate, ShareStack &SS, Elemtype x)
{
    if(StackFull(SS)) return false;
    switch (operate)
    {
    case 0:
        /* code */
        SS.data[++SS.top[0]] = x;
        break;
    case 1:
        SS.data[--SS.top[1]] = x;
        break;
    default:
        return false;
    }
    return true;
}

bool Pop(int operate, ShareStack &SS, Elemtype &x)
{
    if(StackEmpty(operate,SS)) return false;
    switch (operate)
    {
    case 0:
        /* code */
        x = SS.data[SS.top[0]--];
        break;
    case 1:
        x = SS.data[SS.top[1]++];
        break;
    // default:
    //     return false;  此处就不需要了,因为 operate错误已经在 StackEmpty条件中
    }
    return true;
}

阿Q感觉没必要这样做吧,分成两个栈分别操作不好嘛。

链栈

阿Q:链式存储有很多(单链表、双链表、循环链表),也可以用这些来存栈。

此处呢为了简单,阿Q选择单链表。那么是怎样一种单链表可以表示为栈呢?最简单的当然是只有头指针的链表,此时存储可以用下图表示:

L为纯指针,an … a1均开辟了空间

存储类型表示

typedef int ElemType;

typedef struct LinkNode
{
    /* data */
    ElemType data;
    struct LinkNode *next;
}LinkNode,*LiStack;

// 头指针确定整个链表

操作

阿Q注:头指针分配的是静态空间(指针);链表的操作结点都需要动态开辟空间,不然导致栈中(栈区)的节点在函数返回后失效(悬空指针)

在Push中存放了这方面的错误代码(详情见C++一览中的内存分配模型)

// C头文件 NULL
// #include<stdio.h>
// #include<stdlib.h>
// C++头文件
#include<cstdlib>

void InitStack(LiStack &L)
{
    L = NULL;
}

bool StackEmpty(LiStack L)
{
    return L == NULL;
}

bool GetTop(LiStack &L, ElemType &x)
{
    if(StackEmpty(L)) return false;
    x = L->data;
    return true;
}


void StackClear(LiStack &L)
{
    LiStack tmp;
    tmp = L;
    while(!StackEmpty(L)){
        L= tmp->next;
        delete tmp;
        tmp = L;
    }
    // L = NULL;
}

// 由于头指针是静态分配的,因此销毁操作只需要清空链表,然后头指针由系统自动释放

bool Push(LiStack &L, ElemType x)
{
    /*
    错误代码:
        LinkNode node;
        node.data = x;
        node.next = L;
        L = &node;
    
    阿Q注:它使用局部变量 node,导致栈中的节点在函数返回后失效(悬空指针)
    */ 
   // 必须要动态分配,这样才能由自己手动释放
   LinkNode *node = new LinkNode;
   if(!node) return false; 
   node->data = x;
   node->next = L;
   L = node;
   return true;
}

bool Pop(LiStack &L, ElemType &x)
{
    if(StackEmpty(L)) return false;
    LinkNode *tmp;
    tmp = L;
    L = tmp->next;
    x = tmp->data;
    delete tmp; // 结点均为动态开辟的,所以需要释放
    return true;
}

题目

非算法题

算法题

1、假设以 I 和 O 分别表示入栈和出栈操作。栈的初态和终态均为空,写出一个算法,判定所给的操作序列是否合法。若合法,返回true,否则返回false(假设被判定的操作序列已存入一维数组中)。

方法一:用栈的逻辑,但不使用栈的具体结构

bool legal(char Operate[])
{
    int i = 0; // 数组下标
    int Input = 0, Output = 0; // 记录出入栈的次数
    while(Operate[i] != '\0')  // 系统会对字符串型自动添加串尾标识符
    {
        switch (Operate[i])
        {
        case 'I':
            /* code */
            Input++;
            break;
        case 'O':
            Output++;
            break;
        default:
            return false;  // 操作序列本身就有误
            break;
        }
        if(Output > Input) return false;  // 出栈次数 > 入栈次数  不合法
        i++;
    }
    if(Output != Input) return false; // 终态栈不为空
    return true;
}

测试

int main(){
    char *operate = (char*)"IIIOIOIO";  // 此处需要强制类型转换
    bool l = legal(operate);
    std::cout << (l == 1? "yes":"no") << std::endl;  // <<运算符的优先级高于三元运算符
    return 0;
}

方法二 直接使用栈

bool legal_with_stack(char operate[])
{
    SqStack S;
    InitStack(S);
    int i = 0;
    char tmp;
    while(operate[i] != '\0')
    {
        if(operate[i] == 'I') Push(S,'I');
        else if(!Pop(S,tmp)) return false;
        i++;
    }
    if(!StackEmpty(S)) return false;
    return true;
}

阿Q:有时候写有关栈算法,不一定需要使用具体的栈结构。


2、设单链表的表头指针为L,结点由data和next两个域组成,其中data为字符型,设计算法判断该链表的全部n个字符是否中心对称,如:xyx,xyyx都是中心对称。

阿Q最先想到的是将单链表遍历一遍,元素分别存入栈和队列中,然后再依次取出进行对比(好笨重的方法)

若关于中心对称,那么将 n/2个元素入栈即可,然后再出栈和 剩余 n/2个 元素 逐一匹配即可。(可以不用具体的栈的结构)

另外此题需要区分 头指针(指向第一个链表元素,静态分配)和 头结点(特殊的结点,结点内不存值,动态分配)

bool symm(LinkList L, int n)
{
    LinkList tmp = L;
    char s[n/2];  // 前 n/2 个元素存入
    int i = 0;    // 栈顶指针(逻辑上)
    // 入栈操作
    while(i < n/2)
    {
        s[i] = tmp->data;
        tmp = tmp->next;
        i++;
    }

    i--; // 此时已经越界 - 指向栈顶元素的上一个位置了哦(容易错)

    // 当 n 为奇数 , n / 2 + 1 就直接跳过
    if(n%2 == 1) tmp = tmp->next;

    // 出栈 和 后续的 n/2 个元素 逐一匹配
    while(i > -1)
    {
        if(s[i] != tmp->data) return false;  // 不匹配即不对称
        i--;
        tmp = tmp->next;
    }
    return true;
}
暂无评论

发送评论 编辑评论


				
上一篇
下一篇