数据结构 - 第 3 章 栈和队列

考纲内容

(1). 栈和队列的基本概念

(2). 栈和队列的顺序存储结构

(3). 栈和队列的链式存储结构

(4). 栈和队列的应用

知识框架

复习提示

(1). 本章通常以选择题的形式考查,题目不算难,但命题的形式比较灵活

(2). 其中,栈(出入栈的过程 、出栈序列的合法性)和队列的操作及其特征是考查重点

(3). 栈和队列都是线性表的应用和推广,因此也容易出现在算法设计题中

(4). 此外,栈和队列的顺序存储 、链式存储及其特点 、双端队列的特点 、栈和队列的常见应用,以及数组和特殊矩阵的压缩存储都是必须要掌握的内容

3.1 栈

3.1.1 栈的基本概念
1. 栈的定义
栈的概念

栈(Stack)是只允许在一端进行插入或删除操作的线性表

(i). 栈是一种线性表

(ii). 限定只能在线性表的一端进行插入和删除

   (插入和删除在线性表的同一端)

栈顶线性表中允许进行插入和删除操作的一端(瓶口)
栈底固定的,线性表中不允许插入和删除操作的一端(瓶底)
空栈不包含任何元素的空的线性表
栈的表示

假设某个栈 S=(a_1,a_2,a_3,a_4,a_5) ,如下图所示,则 a_1 为栈底元素,

a_5 为栈顶元素

栈的特性

由于栈只能在栈顶进行插入和删除操作,进栈次序依次为 a_1,a_2,a_3,a_4,a_5 ,

而出栈次序为 a_5,a_4,a_3,a_2,a_1

由此可见,栈的操作特性可以明显地概括为后进先出(Last In First Out ,LIFO)

栈的数学性质

n 个不同元素进栈,出栈元素不同排列的个数为 C_{2n}^{n} / (n+1)

上述公式称为卡特兰(Catalan)数,可采用数学归纳法证明

###############################################################################
2. 栈的基本操作
Init(&S)初始化一个空栈 S
Empty(S)判断一个栈是否为空,为空返回 true ,不为空返回 false
Push(&S,x)进栈,若栈 S 未满,则将元素 x 插入,并使元素 x 成为新的栈顶
Pop(&S, &x)出栈,若栈 S 非空,则弹出栈顶元素,并用元素 x 返回
GetTop(S, &x)获取栈顶元素,若栈 S 非空,则用元素 x 返回栈顶元素
Destory(&S)销毁栈,释放栈 S 占用的存储空间

3.1.2 栈的顺序存储结构

    栈是一种操作受限的线性表,类似于线性表,栈也有两种存储方式

1. 顺序栈的实现
概念采用顺序存储的栈,称为顺序栈
顺序栈的实现

(i).  利用一组地址连续的存储单元,存放从栈底到栈顶的数据元素

(ii). 附设一个指针(top)指向当前栈顶元素的位置

所谓指针,其实是一个整数,保存栈顶元素的下标

栈的顺序存储

类型描述

#define  MaxSize  50                       // 定义栈中元素的最大个数

typedef  struct  {

        ElemType  data[MaxSize];    // 存放栈中元素

        int  top;                                   // 栈顶指针

} SqStack;

栈顶指针S->top ,初始状态设置为 S->top = -1
栈顶元素S->data[S->top]
进栈操作

栈不满时,先将栈顶指针 top 加 1 ,再将值插入新的栈顶位置;

S->top++;

S->data[S->top] = x;

出栈操作

栈非空时,先获取栈顶元素,再将栈顶指针减 1 ;

x = S->data[S->top];

S->top--;

栈空条件判断 S->top == -1
栈满条件判断 S->top == MaxSize - 1

栈长

(实际元素个数)

S->top + 1
说明

由于顺序栈的入栈操作受数组上限的约束,当对栈的最大使用空间估计不足时,

有可能发生栈上溢,此时应及时向用户报告消息,以便及时处理,避免出错

注意

栈和队列的判空 、判满条件,会因具体实现而不同,上面提到的方法以及下面的代码

实现只是在栈顶指针设定的条件下的相应方法

top == -1 为栈空条件,这是接口代码实现的前提

2. 顺序栈的基本运算

下面是顺序栈上常用的基本运算的实现

注意:假如存储空间是在栈空间或静态空间上分配的,则无需手动销毁

2.1  顺序栈的 C 描述
#define INIT_SIZE 50
#define INCREASEMENT_SIZE 10

#define OK 1
#define ERROR -1

#define YES 1
#define NO 0

typedef int Status;
typedef int ElemType;

typedef struct {
    ElemType *data;
    int top;
} SeqStack;

/*
 * 初始化
 */
Status Init(SeqStack *S)
{
    S->data = (ElemType*)malloc(sizeof(ElemType) * INIT_SIZE);
    if (S->data == NULL)
    {
        printf("Out of memory !\n");
        return ERROR;
    }

    S->top = -1;
    return OK;
}

/*
 * 判栈空
 */
Status Empty(SeqStack *S)
{
    if (S->top == -1)
        return YES;
    else
        return NO;
}

/*
 * 进栈
 */
Status Push(SeqStack *S, ElemType x)
{
    if (S->top == INIT_SIZE - 1)
        return ERROR;

    S->top++;
    S->data[S->top] = x;
    return OK;
}

/*
 * 出栈
 */
Status Pop(SeqStack *S, ElemType *x)
{
    if (S->top == -1)
        return ERROR;

    *x = S->data[S->top];
    S->top--;
    return OK:
}

/*
 * 获取栈顶元素 
 */
Status GetTop(SeqStack *S, ElemType *x)
{
    if (S->top == -1)
        return ERROR;

    *x = S->data[S->top];
    return OK;
}

/*
 * 销毁栈 
 */
Status Destroy(SeqStack *S)
{
    free(S->data);
    S->data = NULL;
    return OK;
}

2.2 顺序栈的 C++ 描述
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>

using std::cout;
using std::endl;

#define MaxSize 50

class Stack
{
public:
    using ElemType = int;

    Stack() = default;
    Stack(int initsize);
    ~Stack();
    bool empty();
    void push(ElemType x);
    void pop();
    ElemType getop();
    int len();
    int cap();
private:
    ElemType *elem;
    int top;
    int capacity;
};


int main(int argc, char* argv[])
{
    Stack stack(10);
    stack.push(2);
    stack.push(3);
    stack.push(7);
    stack.push(11);
    stack.push(13);

    std::cout << "stack capacity : " << stack.cap() << std::endl;
    std::cout << "stack length : " << stack.len() << std::endl;

    std::cout << stack.getop() << std::endl;
    stack.pop();
    std::cout << "stack length : " << stack.len() << std::endl;
    std::cout << stack.getop() << std::endl;
    stack.pop();
    std::cout << "stack length : " << stack.len() << std::endl;
    std::cout << stack.getop() << std::endl;
    stack.pop();
    std::cout << "stack length : " << stack.len() << std::endl;
    return 0;
}

int Stack::len()
{
    return top+1;
}

int Stack::cap()
{
    return capacity;
}

/* 构造函数 */
Stack::Stack(int initsize)
: top{-1}, elem{new ElemType[initsize]}, capacity{initsize}
{
    std::cout << "Construct" << std::endl;
}

/* 析构函数 */
Stack::~Stack()
{
    delete[] elem;
    elem = nullptr;
    std::cout << "Destroy" << std::endl;
}

/* 判栈空 */
bool Stack::empty()
{
    return top == -1 ? true : false;
}

/* 进栈 */
void Stack::push(ElemType x)
{
    if (top == MaxSize - 1)
    {
        std::cout << "Stack is full !\n" << std::endl;
        return;
    }

    std::cout << "push element is " << x << std::endl;
    elem[++top] = x;
}

/* 出栈 */
void Stack::pop()
{
    if (top == -1)
    {
        std::cout << "Stack is empty !\n" << std::endl;
        return;
    }
    
    std::cout << "pop element is " << elem[top--] << std::endl;
}

/* 获取栈顶元素 */
Stack::ElemType Stack::getop()
{
    if (top == -1)
    {
        std::cout << "Stack is empty !\n" << std::endl;
        return ElemType{};
    }

    return elem[top];
}

3. 共享栈
共享栈
共享栈概念

利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数组空间

将两个栈的栈底分别设置在共享空间的两端两个栈顶向共享空间的中间延伸

栈空

两个栈的栈顶指针都指向栈顶元素;

top1 = -1 时,1 号栈为空;

top2 = MaxSize 时,2 号栈为空;

栈满仅当两个栈顶指针相邻(top1 + 1 = top2 )时,判断为栈满
进栈

当 1 号栈进栈时, top1 先加 1 再插入新元素

s1->top1++;

s1->data[s1->top1] = x;

当 2 号栈进栈时,top2 先减 1 再插入新元素

s2->top2--;

s2->data[s2->top2] = x;

出栈

当 1 号栈出栈时,先返回 1 号栈的栈顶元素,再将 1 号栈顶指针减 1

x = s1->data[s1->top1];

s1->top1--;

当 2 号栈出栈时,先返回 2 号栈的栈顶元素,再将 2 号栈顶指针加 1

x = s2->data[s2->top2];

s2->top2++;

总结

共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢;

存取数据的时间复杂度均为 O(1) ,所以对存取效率没什么影响

3.1.3 栈的链式存储结构
概念采用链式存储的栈称为 "链栈"
链栈的优点便于多个栈共享存储空间和提高其效率,不存在栈满上溢的情况
链栈的实现

通常采用单链表实现;

规定栈顶在单链表的表头(即,插入删除元素均在表头)

此处规定链栈没有头结点,链表的头指针指向栈顶元素

栈的链式

存储类型

typedef  struct  Linknode {

        ElemType  data;            // 数据域

        struct Linknode *next;    // 指针域

} *LiStack;                              // 栈类型定义

总结

采用链式存储,便于结点的插入与删除;

链栈的操作与链表类似,入栈和出栈的操作都在链表的表头进行;

(1). 链式栈的描述

#include <stdio.h>
#include <stdlib.h>

typedef int ElemType;

struct Node;
typedef struct Node *PtrToNode;
typedef PtrToNode List;
typedef PtrToNode Position;

struct Node
{
    ElemType Elem;
    Position Next;
};


List InitStack(List L);
int Empty(List L);
void PrintEmpty(List L);
void Push(List L, ElemType E);
void Pop(List L);
ElemType Top(List L);
void PrintTop(List L);
void DestroyStack(List L);



int main(int argc, char* argv[])
{
    List L = NULL;
    L = InitStack(L);
    PrintEmpty(L);

    Push(L, 2);
    Push(L, 3);
    Push(L, 5);
    Push(L, 7);
    Push(L, 11);
    Push(L, 13);

    PrintEmpty(L);
/*
    PrintTop(L);
    Pop(L);
    PrintTop(L);
    Pop(L);
    PrintTop(L);
    Pop(L);
    PrintTop(L);
    Pop(L);
    PrintTop(L);
    Pop(L);
*/


    DestroyStack(L);
    PrintEmpty(L);

    return 0;
}


List InitStack(List L)
{
    L = (List)malloc(sizeof(struct Node));
    if (!L)
        return NULL;

    L->Next = NULL;
    return L;
}

int Empty(List L)
{
    return L->Next == NULL;
}

void PrintEmpty(List L)
{
    if (Empty(L) != 0)
        printf("Empty\n");
    else
        printf("Not Empty\n");
}

void Push(List L, ElemType E)
{
    Position NewCell;

    NewCell = (Position)malloc(sizeof(struct Node));
    if (NewCell == NULL)
    {
        printf("Out of memory!\n");
        exit(0);
    }

    NewCell->Elem = E;
    NewCell->Next = L->Next;
    L->Next = NewCell;
}


void Pop(List L)
{
    Position TmpCell = NULL;

    if (L->Next == NULL)
    {
        printf("Empty Stack!\n");
        return;
    }

    TmpCell = L->Next;
    L->Next = TmpCell->Next;
    free(TmpCell);
    return;
}

ElemType Top(List L)
{
    ElemType data;

    if (L->Next == NULL)
    {
        printf("Empty Stack!\n");
        return data;
    }

    data = L->Next->Elem;
    return data;
}

void PrintTop(List L)
{
    ElemType Elem = Top(L);

    printf("Top Elem is %d\n", Elem);
}

void DestroyStack(List L)
{
    Position P, TmpCell;

    P = L->Next;
    L->Next = NULL;

    while (P != NULL)
    {
        TmpCell = P->Next;
        printf("Current destroyed element is %d\n", P->Elem);
        free(P);
        P = TmpCell;
    }
}

3.2 栈的应用举例

3.2.1 数制转换

3.2.2 括号匹配的检验
初始设置一个空栈

bool isValid(string s) 
{
    if (s.size() % 2 == 1)
    {
        return false;
    }

    std::stack<char> chStack;

    for (auto ch : s)
    {
        switch (ch)
        {
            case '(':
            case '[':
            case '{':
                chStack.push(ch);
                break;
            case ')':
                if (chStack.empty() == true || chStack.top() != '(')
                    return false;
                else
                    chStack.pop();
                break;
            case ']':
                if (chStack.empty() == true || chStack.top() != '[')
                    return false;
                else
                    chStack.pop();
                break;
            case '}':
                if (chStack.empty() == true || chStack.top() != '{')
                    return false;
                else
                    chStack.pop();
                break;
            default:
                return false;
        }
    }
    return chStack.empty();
}
3.2.3 行编辑程序

3.2.4 迷宫求解

3.2.5 表达式求值

3.3 栈与递归的实现

3.4 队列

3.4.1 队列的基本概念
队列的基本概念
队列的定义队列(Queue)简称 "队" ,也是一种操作受限的线性表
只允许在线性表的一端进行插入,在线性表的另一端进行删除
入队向线性表的可以插入元素的一端,插入元素,称为 " 入队或进队 "
出队从线性表的可以删除元素的一端,删除元素,称为 " 出队或离队 "
队列特性

最早入队的元素,也是最早出队

特点就是 " 先进先出 "(First In First Out,FIFO)

队头(Front)允许删除元素的一端,又称队首
队尾(Rear)允许插入元素的一端
空队列不含任何元素的空线性表
队列常见的基本操作
InitQueue(&Q)初始化队列,构造一个空队列
QueueEmpty(Q)判别队列是否为空,队列为空返回 true ,队列不为空返回 false
EnQueue(&Q, x)入队,若队列 Q 未满,将元素 x 插入队尾,并使 x 成为新的队尾元素
DeQueue(&Q, &x)出队,若队列 Q 非空,删除对头元素,并用元素 x 返回
GetHead(Q, &x)读取对头元素,若队列 Q 非空,则将对头元素赋值给元素 x
注意

栈和队列都是操作受限的线性表;

因此,不是任何对线性表的操作都可以作为栈和队列的操作;

比如,不可以随便读取栈或队列中间的某个元素

3.4.2 队列的顺序存储结构
1. 队列的顺序存储
基本概念

队列的顺序存储,是指分配一块地址连续的存储单元来存储队列中的元素

附设两个指针:队头指针 front 指向队头元素,队尾指针 rear 指向队尾元素

队列的顺序

存储类型描述

#define  MaxSize  50                         // 定义队列中元素的最大个数

typedef  struct  {

        ElemType  data[MaxSize];        // 存放队列元素

        int  front;                                    // 队头指针

        int  rear;                                     // 队尾指针

} SqQueue;

队列判空

(初始状态)

Q.front  ==  Q.rear  == 0 
进队操作队列不满时,先将新元素插入队尾位置,再将队尾指针加 1
出队操作队列不空时,先获取队头元素,再将队头指针加 1

顺序队列

存在的问题

图示 (a) 为队列的初始状态,满足 Q.front == Q.rear == 0 ,该条件可以作为队列判空的条件;

但能否用 Q.rear == MaxSize 作为队列满的条件呢???显然不能,图 (d) 中,队列中仅有一个元素,却满足 Q.rear == MaxSize ,这时入队出现 "上溢出" ,但这种溢出并不是真正的溢出,data 数组中依然可以存在可以存放元组的空位置,所以是一种 "假溢出"

队列操作图示
2. 循环队列

解决办法

(循环队列)

前面提到顺序队列的缺点(假溢出),这里引出循环队列的概念

将顺序队列臆造(假想)为一个环状的空间,即把存储队列元素的表,从逻辑上视为一个 "环" ,称为 "循环队列"

当队首指针 Q.front == MaxSize - 1 后,再前进一个位置就自动到 0 ,这可以利用除法取余运算(%)来实现

再强调一下,队尾插入,队头删除

初始状态 :Q.front == Q.rear == 0
队头指针进 1 :Q.front = (Q.front + 1) % MaxSize
队尾指针进 1:Q.rear = (Q.rear + 1) % MaxSize
队列长度:(Q.rear + MaxSize - Q.front) % MaxSize
入队出队时:指针都按顺时针方向进 1
循环队列队空和对满的判断条件是什么呢???
陷阱

若入队元素的速度快于出队元素的速度,则队尾指针很快就会赶上队头指针;

如图 (d1) 所示,可见队满时也有 Q.front == Q.rear

为了区分队空还是队满,有三种处理方式:

方式一

牺牲一个单元来区分队空和队满

入队时少用一个队列单元,这是一种较为普遍的做法;

约定以 "队头指针在队尾指针的下一位置作为队满的标志"

队满条件(Q.rear + 1) % MaxSize == Q.front
队空条件Q.rear == Q.front

队列中

元素个数

(Q.rear - Q.front + MaxSize) % MaxSize
方式二类型中增设表示元素个数的数据成员
队满条件Q.size == MaxSize
队空条件Q.size == 0
这两种情况都有 Q.front == Q.rear
方式三队列中增设 tag 数据成员,以区分是队满还是队空
队满条件tag == 0 时,若因删除导致 Q.front == Q.rear ,则为队空
队空条件tag == 1 时,若因插入导致 Q.front == Q.rear ,则为队满
3. 循环队列的操作
初始化

void InitQueue(SqQueue &Q) {

        Q.rear = Q.front = 0;

}

队列判空

bool isEmpty(SqQueue Q) {

        return Q.rear == Q.front ? true : false;

}        

入队

bool EnQueue(SqQueue &Q,ElemType x) {

        if ((Q.rear + 1) % MaxSize == Q.front)

                return false;

        Q.data[Q.rear] = x;

        Q.rear = (Q.rear + 1) % MaxSize;

        return true;

}

出队

bool DeQueue(SqQueue &Q,ElemType &x) {

        if (Q.rear == Q.front)

                return false;

        x = Q.data[Q.front];

        Q.front = (Q.front + 1) % MaxSize;

        return true;

}

3.4.3 队列的链式存储结构
1. 队列的链式存储

队列的链式表示称为 "链队列"

链队列,实际上是一个同时带有队头指针和队尾指针的单链表

头指针,指向队头结点(第一个存有数据的结点)
尾指针,指向队尾结点,即单链表的最后一个结点()

链式队列

类型描述

typedef  struct {

        ElemType  data;

        struct LinkNode *next;

}LinkNode;

typedef  struct {

        LinkNode *front,*rear;

}LinkQueue;

队列判空当 Q.front == NULL 且 Q.rear == NULL 时,链式队列为空
出队

出队时,首先判断队列是否为空;

队列为空,直接返回;

队列不为空,则取出队头元素,将其从链表中摘除,并让 Q.front 指向下一个结点

(若该结点为最后一个结点,则置 Q.front = Q.rear = 0)

入队

入队时,建立一个新结点,将新结点插入到链表的尾部;

并让 Q.rear 指向这个新插入的结点

(若原队列为空,则令 Q.front 也指向该新结点,)

Tips不难看出,不带头结点的链式队列,在操作上往往比较麻烦,因此通常将链式队列设计成一个带头结点的单链表,这样插入和删除操作就统一了
注意

用单链表表示的链式队列,特别适合于数据元素变动比较大(经常插入删除)的情形;

不存在队列满且产生溢出的问题

另外,假如程序中药使用多个队列,与多个栈的情形一样,最好使用链式队列,这样就不会出现存储分配不合理和 "溢出" 的问题
2. 链式队列的基本操作
初始化

void InitQueue(LinkQueue &Q) {

        Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));

        Q.front->next = NULL;

}

队列判空

bool IsEmpty(LinkQueue Q) {

        return Q.front == Q.rear ? true : false;

}

入队

void EnQueue(LinkQueue &Q,ElemType x) {

        LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));

        s->data = x;

        s->next = NULL;

        Q.rear = s;

}

出队

bool DeQueue(LinkQueue &Q,ElemType &x) {

        if (Q.rear == Q.front)

                return false;

        LinkNode *p = Q.front->next;

        x = p->data;

        Q.front->front = p->next;

        if (Q.rear == p)

                Q.rear = Q.front;

        free(p);

        return true;

}

3.4.4 双端队列
双端队列概念双端队列是指,允许两端都可以进行入队和出队操作的队列
其元素的逻辑结构仍是线性结构
将队列的两端分别称为前端和后端,两端都可以入队和出队
小结

在双端队列入队时,前端入队的元素在后端入队的元素的前面(更靠近前端);

后端入队的元素在前端入队的元素的后面(更靠近后端);

在双端队列出队时,无论是前端出队还是后端出队,先出的元素排列在后出的元素的前面

输出受限的双端队列 :

允许在一端进行插入和删除,另一端只允许插入的双端队列,

称为 "输出受限的双端队列"

输入受限的双端队列 :

允许在一端进行插入和删除,另一端只允许删除的双端队列,

称为 "输入受限的双端队列"

若限定双端队列,从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈

3.5 离散事件模拟

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值