数据结构学习笔记——栈

1.定义
栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
根据定义我们知道栈在本质上也是一种线性表,只是在插入和删除操作上进行了限制。
我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
栈的插入操作,叫做进栈,也称压栈,入栈,类似子弹入弹夹。
栈的删除操作,叫做出栈,也有的叫作弹栈,如同弹夹中的子弹出夹。
应用:浏览器的“后退”键,Word、Photoshop等文档或图像编辑软件中的撤销(undo)操作等。
2.栈的抽象数据类型
1.InitStack(*S):初始化操作,建立一个空栈S。
2.DestroyStack(*S):若栈存在,则销毁它。
3.ClearStack(*S):将栈清空。
4.StackEmpty(S):若栈为空,返回true,否则返回false。
5.GetTop(S, *e):若栈存在且非空,用e返回S的栈顶元素。
6.Push(*S, e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。
7.Pop(*S, *e):删除栈S中栈顶元素,并用e返回其值。
8.StackLength(S):返回栈S的元素个数。
3.栈的存储结构
(1)栈的顺序存储结构
下面来看一下栈的顺序存储结构的代码实现

typedef int SElemType;          //SElemType类型根据实际情况而定,这里假设为int
typedef struct{
    SElemType data[MAXSIZE];
    int top;                    //用于栈顶指针
}SqStack;

进栈操作

Status Push(SqStack *s, SElemType e){  //插入元素e为新的栈顶元素
    if(S->top == MAXSIZE-1) //栈满
        return ERROR;
    S->top++;               //栈顶指针增加一
    S->data[S->top] = e;    //将新插入元素赋值给栈顶空间
    return OK;
}

出栈操作

Status Pop(SqStack *S, SElemType *e){  //若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR
    if(S->top == -1)
        return ERROR;
    *e = S->data[S->top];       //将要删除的栈顶元素赋值给e
    S->top--;                   //栈顶指针减一
    return OK;
}

两栈共享空间
因为栈只准栈顶进出元素,所以不存在线性表插入和删除时需要移动元素的问题。不过它有一个很大的缺陷,就是必须实现确定数组存储空间大小,万一不够用了就需要编程手段来扩展数组的容量,非常麻烦。对于一个栈,我们也只能尽量考虑周全,设计处合适大小的数组来处理,但对于两个相同类型的栈,我们却可以做到最大限度地利用其实现开辟的存储空间来进行操作。
我们都知道使用过程中我们给每个栈分配足够的空间是不太现实的,使用栈的时候,我们也不能保证栈有足够的空间,另外,栈是一个动态的存储结构,各个栈的实际大小在使用的过程中都会发生变化的,有时候其中一个已经上溢了,而另外一个还没用怎么使用。这样必定会造成空间的利用率降低。遇到这种情况我们该怎么办呢?
这时我们引入两站共享空间的概念,我们完全可以用一个数组来存储两个栈,只不过需要点小技巧。
数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为0处,另一个栈为数组的末端,即下标为数组长度n-1处。这样,两个栈如果增加元素,就是两端点向中间延伸。
栈满条件:top1 + 1 == top2
下面是两栈共享空间结构的实现代码

typedef struct{                     //两栈共享空间结构
    SElemType data[MAXSIZE];
    int top1;                       //栈1栈顶指针
    int top2;                       //栈2栈顶指针
}SqDoubleStack;

入栈操作
对于两栈共享空间的push方法,我们除了要插入元素值参数外,还需要有一个判断是栈1还是栈2的栈号参数stackNumber。

//插入e为新的栈顶元素
Status Push(SqDoubleStack *S, SElemType e, int stackNumber){
    if(S->top1+1 == top2)           //栈已满,不能再push新元素了
        return ERROR;
    if(stackNumber == 1)            //栈1有元素进栈
        S->data[++S->top1] = e;     //若栈1则先top1+1后给数组元素赋值
    else if(stackNumber == 2)       //栈2有元素进栈
        S->data[--S->top2] = e;     //若栈2则先top2-1后给数组元素赋值
    return OK;
}

出栈操作

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK否则返回ERROR
Status Pop(SqDoubleStacck *S, SElemType *e, int stackNumber){
    if(stackNumber == 1){
        if(S->top1 == -1)
            return ERROR;                //说明栈1已经是空栈,溢出
        *e = S->data[S->top1--];         //将栈1的栈顶元素出栈
    }
    else if(stackNumber == 2){
        if(S->top2 == MAXSIZE)
            return ERROR;                //说明栈2已经是空栈,溢出
        *e = S->data[S->top2++];         //将栈2的栈顶元素出栈
    }
    return OK;
}

事实上,使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也就是一个栈增长时另一个栈在缩短的情况。
(2)栈的链式存储结构
由于单链表有头指针,而栈顶指针也是必须的,那干嘛不让它俩合二为一呢,所以比较好的办法是把栈顶放在单链表的头部。另外,都已经有了栈顶在头部了,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点呢。
下面是链栈的实现代码

typedef struct StackNode{
    SElemType data;
    struct StackNode *next;
}StackNode, *LinkStackPtr;
typedef struct LinkStack{
    LinkStackPtr top;
    int count;
}LinkStack;

链栈的操作绝大部分都和单链表类似,只是在插入和删除上,特殊一些。
进栈操作

//插入元素e为新的栈顶元素
Status Push(LinkStack *S, SElemType e){
    LinkStackPtr s= (LinkStackPtr)malloc(sizeof(StackNode));
    s->data = e;
    s->next = S->top;                     //把当前的栈顶元素赋值给新结点的直接后继
    S->top = s;                           //将新的结点s赋值给栈顶指针
    S->count++;
    return OK;
}

出栈操作

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR
Status Pop(LinkStack *S, SElemType *e){
    LinkStack p;
    if(StackEmpty(*S))
        return ERROR;
    *e = S->top->data;
    p = S->top;                  //将栈顶结点赋值给p
    S->top = S->top->next;       //使得栈顶指针下移一位,指向后一结点
    free(p);                     //释放结点p
    S->count--;
    return OK;
}

顺序栈和链栈的区别:
对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。
4.栈的作用
栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力去思考数组的下标增减等细节问题,反而掩盖了问题的本质。

你可能感兴趣的:(数据结构,栈)