【数据结构笔记】3.栈和队列

文章目录

  • 第3章 栈和队列
    • 3.1 栈
      • 3.1.1 栈的基本概念
          • 1.栈的定义
          • 2.栈的基本操作
      • 3.1.2 栈的顺序存储结构
          • 1.顺序栈的实现
          • 2.顺序栈的基本运算
          • 3.共享栈
      • 3.1.3 栈的链式存储结构
            • 卡特兰(Catalan)数
            • 【注意】
    • 3.2 队列
      • 3.2.1 队列的基本概念
          • 1.队列的定义
          • 2.队列常见的基本操作
      • 3.2.2 队列的顺序存储结构
          • 1.队列的顺序存储
            • 【注意】
          • 2.循环队列
          • 3.循环队列的操作
      • 3.2.3 队列的链式存储结构
          • 1.队列的链式存储结构
          • 2.链队列的基本操作
      • 3.2.4 双端队列
            • 【题型】
            • 【注意】
    • 3.3 栈和队列的应用
      • 3.3.1 栈在括号匹配中的应用
      • 3.3.2 栈在表达式求值中的应用
      • 3.3.3 栈在递归中的应用
      • 3.3.4 队列在层次遍历中的应用
      • 3.3.5 队列在计算机系统中的应用
            • 【注意】
    • 3.4 特殊矩阵的压缩存储
      • 3.4.1 数组的定义
      • 3.4.2 数组的存储结构
      • 3.4.3 矩阵的压缩存储
          • 1.对称矩阵
          • 2.三角矩阵
          • 3.三对角矩阵
      • 3.4.4 稀疏矩阵
            • 【注意】

第3章 栈和队列

3.1 栈

3.1.1 栈的基本概念

1.栈的定义

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

  • 栈顶。线性表允许进行插入和删除的那一端。

  • 栈底。固定的,不允许进行插入和删除的另一端。

  • 空栈。不含任何元素的空表。

2.栈的基本操作

initStack(&S):初始化一个空栈。

stackEmpty(S):判断一个栈是否为空。

push(&S, x):进栈。

pop(&S, &x):出栈,若栈非空,则弹出栈顶元素。

getTop(S, &x):读栈顶元素。

destoryStack(&S):销毁栈,并释放栈S所占用的空间。

3.1.2 栈的顺序存储结构

1.顺序栈的实现

采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素。同时附设一个指针(top)指示当前栈顶的位置。

#define MaxSize 50
typedef struct SqStack{
    ElemType data[MaxSize];
    int top;
}SqStack;

由于顺序栈的入栈操作受数组上界的约束,当对栈的最大使用空间估计不足时,有可能发生栈上溢。

栈和后面提到的队列的判空和判满条件,会因为实际给的条件不同而变化,下面给出栈顶指针设定的条件下的相应方法,而其他情况需要具体情况具体分析。

2.顺序栈的基本运算
  1. 初始化
void initStack(SqStack &S){
    s.top = -1;
}
  1. 判栈空
bool stackEmpty(SqStack S){
    if(S.top == -1)
        return true;
    else
        return false;
}
  1. 进栈
bool push(SqStack &S, ElemType x){
    if(S.top == MaxSize - 1)
        return false;
    S.data[++S.top] = x;
    return true;
}
  1. 出栈
bool pop(SqStack &S, ElemType &x){
    if(S.top == -1)
        return false;
    x = S.data[S.top--];	//先出栈,指针再减1
    return true;
}
  1. 读栈顶元素
bool getTop(SqStack S, ElemType &x){
    if(S.top == -1)
        return false;
    x = S.data[S.top];
    return true;
}

**栈顶指针做++、–操作的次序视初始化时栈顶的大小而定。**即若栈顶指针初始化为S.top = 0,即入栈操作变为S.data[S.top++] = x,出栈操作变为x = S.data[--S.top]。相应的栈空、栈满条件也会发生变化。

3.共享栈

利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。两个栈的栈顶指针都指向各自的栈顶元素。

  • top0 = -1时,0号栈为空;
  • top1 = MaxSize时1,号栈为空;
  • top1 - top0 = 1时,判断为栈满。

3.1.3 栈的链式存储结构

采用链式存储的栈称为链栈,其优点是便于多个栈共享存储空间和提高效率,且不存在栈满上溢的情况。通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。这里规定链栈没有头结点,Lhead指向栈顶元素。

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

采用链式存储,便于结点的插入与删除。值得注意的是,对于带头结点和不带头结点的链栈,在具体的实现方面有所不同。

卡特兰(Catalan)数

对于n个不同元素进栈,出栈序列的个数为:
1 n + 1 C 2 n n = 1 n + 1 ( 2 n ! ) n ! ∗ n ! \frac{1}{n+1} C^n_{2n} = \frac{1}{n+1}\frac{(2n!)}{n!*n!} n+11C2nn=n+11n!n!(2n!)

【注意】
  1. 栈和队列具有相同的逻辑结构,它们都是属于线性结构。
  2. 由于链表的删除操作相对复杂,因此有关链表的问题可以以删除操作为代表。为了不断链,必须获取被删结点的前一个结点,从这个角度思考问题。
  3. 火车车轨问题(P64):入口和出口之间有多个队列,且每个队列可容纳多个元素。

3.2 队列

3.2.1 队列的基本概念

1.队列的定义

队列。也是一种操作受限的线性表,直允许在表的一端进行插入,而在表的另一端进行删除。

  • 队头。允许删除的一端,又称队首。
  • 队尾。允许插入的一端。
  • 空队列。不含任何元素的空表。
2.队列常见的基本操作

initQueue(&Q):初始化队列,构造一个空队列Q。

queEmpty(Q):判队列空。

enQueue(&Q, x):入队。

deQueue(&Q, &x):出队,若队列Q非空,删除队头元素,并用x返回。

getHead(Q, &x):读队头元素。

3.2.2 队列的顺序存储结构

1.队列的顺序存储

队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针front和rear分别指示队头元素和队尾元素的位置。设队头指针指向队头元素,队尾指针指向队尾元素的下一个位置(也可以让rear指向队尾元素,front指向队头元素的前一个位置)。

#define MaxSize 50
typedef struct SqQueue{
    EleType data[MaxSize];
    int front, rear;
}

初始状态:Q.front = Q.rear = 0

进队操作:队不满时,先送值到队尾元素,再将队尾指针加1.

出队操作:队不空时,先取队头元素的值,再将队头指针加1.

【注意】
  • 不能用Q.rear == Q.front作为队列满的条件。
  • 假溢出:在顺序队中,当尾指针已经到了数组的上界,不能再有入队操作,但其实数组中还有空位置,这就叫“假溢出”。解决假溢出的方法是采用循环队列。
2.循环队列

将顺序队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。当队首指针Q.front = MaxSize - 1后,再前进一个位置就自动到0,这可以利用除法取余(%)来实现。

  1. 队首指针进1:Q.front = (Q.front + 1) % MaxSize;

  2. 队尾指针进1:Q.rear = (Q.rear + 1) % MaxSize;

  3. 队列长度:length = (Q.rear + MaxSize - Q.front) % MaxSize;

  4. 队空:Q.front == Q.rear;

  5. 队满

    • 牺牲一个单元来区分队空和队满
      • 队满条件为(Q.rear + 1) % MaxSize == Q.front;
      • 队空条件仍然为Q.front == Q.rear;
    • 类型中增设表示元素个数的数据成员,此时队空和队满都有Q.front == Q.rear;
      • 队空条件为Q.size == 0;
      • 队满条件为Q.size == MaxSize;
    • 类型中增设tag数据成员
      • tag等于0时,若因删除导致Q.front == Q.rear,则为队空
      • tag等于1时,若因插入导致Q.front == Q.rear,则为队满
3.循环队列的操作
  1. 初始化
void initQueue(SqQueue &Q){
    Q.rear = Q.front = 0;
}
  1. 判队空
bool queEmpty(SqQueue Q){
    if(Q.rear == Q.front)
        return ture;
    else
        return false;
}
  1. 入队
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;
}
  1. 出队
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.2.3 队列的链式存储结构

1.队列的链式存储结构

链队列,实际上是一个同时带有队头指针和队尾指针的单链表。头指针指向队头结点,尾指针指向队尾结点,即单链表的最后一个结点(队列顺序存储结构中的尾指针指向队尾元素的下一个位置

typedef struct LinkNode{
    ElemType data;
    struct LNode * next;
}LinkNode;

typedef struct{
    LNode *front, *rear;
}LinkQueue;
2.链队列的基本操作
  1. 初始化
void initQueue(LinkQueue &Q){
	Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
    Q.front->next = NULL;	//初始为空,看出队操作
}
  1. 判队空
bool queEmpty(LinkNode Q){
    if(Q.front == Q.rear)
        return true;
    else 
        return false;
}
  1. 入队
void enQueue(LinkQueue &Q, ElemType x){
    LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
    s->data = x;
    s->next = NULL;
    Q.rear->next = s;
    Q.rear = s;
}
  1. 出队
bool deQueue(LinkQueue &Q, ElemType &x){
    if(Q.front == Q.rear)
        return false;
    LinkNode *p = Q.front->next;
    x = p->data;
    Q.front->next = p->next;
    if(Q.rear == p)
        Q.rear = Q.front;
    free(p);
    return true;
}

3.2.4 双端队列

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

输出受限的双端队列,允许在一端进行插入和删除,但在另一端只允许插入。

输入受限的双端队列,允许在一端进行插入和删除,但在另一端只允许删除。

【题型】

设有一个双端队列,输入序列为1, 2, 3, 4,试分别求出以下条件的输出序列
1)能由输入受限的双端队列得到,但不能由输出受限的双端队列得到的输出序列;
2)能由输出受限的双端队列得到,但不能由输入受限的双端队列得到的输出序列;
3)既不能由输入受限的双端队列得到,又不能由输出受限的双端队列得到的输出序列;
答:1)4,1,3,2;
2)4,2,1,3;
3)4,2,3,1。

【注意】
  1. 有关队列的操作:入队操作只和尾指针有关(判满与头指针有关),出队操作只和头指针有关(判空与尾指针有关)。
  2. 对于链式队列,由于循环链表还需要在完成入队和出队操作后修改链表为循环的,这个操作显然是冗余的,所以带头尾指针的单链表实际上更适合。需要注意的是我们通常说的循环队列是顺序存储结构
  3. 对于链式队列(单链表),如果出队(即从队头删除元素)发生在链表尾,显然尾指针向前偏移是非常繁琐的,因此队头在链表的链头位置。

3.3 栈和队列的应用

3.3.1 栈在括号匹配中的应用

  1. 初始设置一个空栈,顺序读入括号。
  2. 若是右括号,则或者使置于栈顶的最急迫期待得以消解,或者使不合法的情况(括号序列不匹配,退出程序)。
  3. 若是左括号,则作为一个新的更急迫的期待压入栈中,自然使原有的在栈中的所有未消解的期待的急迫性降了一级。
  4. 算法结束时,栈为空,否则括号序列不匹配。

3.3.2 栈在表达式求值中的应用

中缀表达式不仅依赖运算符的优先级,而且还要处理括号。后缀表达式的运算符在操作数后面,在后缀表达式中已经考虑了运算符的优先级,没有括号。例如:中缀表达式 A+B*(C-D)-E/F(表达式树中序遍历)所对应的后缀表达式是 ABCD-*+EF-(表达式树后序遍历)。

通过后缀表示计算表达式值的过程为:顺序扫描表达式的每一项,然后根据它的类型做如下相应操作:若该项是操作树,则将其压入栈中;若该项是操作符,则连续从栈中退出两个操作数Y和X(显然,第二个操作数先出栈),形成运算指令XY,并将计算结果重新压入栈中。当表达式的所有项都扫描完后,栈顶存放的就是最后的计算结果。

3.3.3 栈在递归中的应用

若一个函数、过程或数据结构的定义中又应用了它自身,则这个函数、过程或数据结构称为是递归定义的,简称递归。

它通常把一个大型的复杂问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的代码就可以描述出解题过程所需要的多次重复计算,大大减少了程序的代码量,但在通常情况下,它的效率并不高。

递归模型不能是循环定义的,其必须满足下面两个条件:

  • 递归表达式(递归体)
  • 边界条件(递归出口)
int Fib(int n){
    if(n == 0)
        return 0;
    else if(n == 1)
        return 1;
    else
        return Fib(n - 1) + Fib(n - 2);
}

在递归调用的过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出等。其效率不高的原因是递归调用过程中包含很多重复的计算。可以将递归算法转换为非递归算法。

3.3.4 队列在层次遍历中的应用

层次遍历二叉树的过程:

  1. 根结点入队。
  2. 若队空(所有结点都已处理完毕),则结束遍历;否则重复3操作。
  3. 队列中第一个结点出队,并访问之。若其有左孩子,则将左孩子入队;若其有右孩子,则将右孩子入队,返回2.

3.3.5 队列在计算机系统中的应用

  1. 解决主机与外部设备之间速度不匹配的问题。
  2. 解决由多用户引起的资源竞争问题。
【注意】
  1. 栈也可以应用于进制转换。例如2进制转换8进制,“11,001,110”,从左向右输入,分组时从右向左三位一组输出。
  2. 调用函数时,系统会为调用者构造一个由参数表和返回地址组成的活动记录,并将记录压入系统提供的栈中,若被调用函数由局部变量,也要压入栈中。

3.4 特殊矩阵的压缩存储

在数据结构中考虑的是如何用最少的内存空间来存储同样的一组数据。

3.4.1 数组的定义

数组是由n(n≥1)个相同类型的数据元素构成的有限序列。

数组是线性表的推广。一维数组可视为一个线性表,二位数组可视为元素是线性表的线性表。数组一旦被定义,其维数和维界就不再改变。因此,除结构的初始化和销毁外,数组只会有存取元素和修改元素的操作。

3.4.2 数组的存储结构

一个数组的所有元素在内存中占用一段连续的存储空间。

对于多维数组,有两种映射方法:按行优先按列优先。对于数组A2*3,按行存储在内存中的顺序为:a00,a01,a02,a10,a11,a12;按列存储在内存中的顺序为:a00,a10,a01,a11,a02,a12

3.4.3 矩阵的压缩存储

  • 压缩存储:指为多个值相同的元素指分配一个存储空间,对零元素不分配存储空间。
  • 特殊矩阵的压缩方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间中。
1.对称矩阵

对于n阶对称矩阵,将其存放在一维数组B[n(n+1)/2]中,只存放主对角线和下三角区的元素。
k = { i ( i − 1 ) 2 + j − 1 , i > = j (下三角区和主对角线元素) j ( j − 1 ) 2 + i − 1 , i < j (上三角区元素 a i j = a j i ) k = \begin{cases} \frac{i(i-1)}{2}+j-1, &\text{$i>=j$(下三角区和主对角线元素)} \\\\ \frac{j(j-1)}{2}+i-1, &\text{$ik=2i(i1)+j1,2j(j1)+i1,i>=j(下三角区和主对角线元素)i<j(上三角区元素aij=aji)

2.三角矩阵

下三角矩阵:存储完下三角区和主对角线上的元素之后,紧接着存储对角线上方的常量一次,故可以压缩存储在B[n(n+1)/2+1]中。
k = { i ( i − 1 ) 2 + j − 1 , i > = j (下三角区和主对角线元素) n ( n − 1 ) 2 , i < j (上三角区元素) k= \begin{cases} \frac{i(i-1)}{2}+j-1, &\text{$i>=j$(下三角区和主对角线元素)} \\\\ \frac{n(n-1)}{2}, &\text{$ik=2i(i1)+j1,2n(n1),i>=j(下三角区和主对角线元素)i<j(上三角区元素)
类似的,上三角矩阵只存储主对角线、上三角区上的元素和下三角区的常量一次。
k = { ( i − 1 ) ( 2 n − i + 2 ) 2 + ( j − i ) , i < = j (上三角区和主对角线元素) n ( n + 1 ) 2 , i > j (下三角区元素) k= \begin{cases} \frac{(i-1)(2n-i+2)}{2}+(j-i), &\text{$i<=j$(上三角区和主对角线元素)} \\\\ \frac{n(n+1)}{2}, &\text{$i>j$(下三角区元素)} \end{cases} k=2(i1)(2ni+2)+(ji),2n(n+1),i<=j(上三角区和主对角线元素)i>j(下三角区元素)
以上推到均假设数组的下标从0开始,若题设有具体要求,则应该灵活应对。

3.三对角矩阵

对于n阶方阵A中的任一元素aij,当|i-j|>1时,有aij = 0(1≤i, j≤n),则称为三对角矩阵。所有非零元素都集中在以主对角线为中心的3条对角线的区域,其他元素都为0。三对角矩阵的压缩存储是将3条对角线上的元素按行优先方式存放在一维数组B中。
k = 2 i + j − 3 k=2i+j-3 k=2i+j3

3.4.4 稀疏矩阵

矩阵元素个数s相对于矩阵中非零元素的个数t来说非常多,这样的矩阵称为稀疏矩阵。

通常,稀疏矩阵非零元素的分布没有规律,因此将非零元素及其相应的行和列构成一个三元组(行标,列表,值)A[t+1][3],然后再按照某种规律存储这些三元组。稀疏矩阵压缩存储后便失去了随机存取特性。

【注意】
  1. 本小节有关下标的计算问题要注意题目给定数组的起始下标为0还是1,通常可以利用特殊下标值验证公式,用归纳法推出下标值。

  2. 十字链表将行单链表和列单链表结合起来存储稀疏矩阵;
    邻接矩阵空间复杂度为O(n2),不适合存储稀疏矩阵;
    二叉链表又名左孩子右兄弟表示法,可用于表示树或森林。

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