《数据结构 C语言版 严蔚敏 第2版》:栈和队列

一、栈和队列的定义和特点

1、栈的定义和特点 

栈 (stack) 是限定在表尾进行插入或删除操作的线性表


因此,对栈来说,表尾端有其特殊含义,称为栈顶 (top), 相应地, 表头端称为栈底 (bottom)

不含元素的空表称为空栈


栈的修改是按后进先出的原则进行的,因此,栈又称为后进先出 (Last In First Out,LIFO) 的线性表

 

2、队列的定义和特点 

队列(queue) 是一种先进先出(First In First Out, FIFO)的线性表


只允许在表的一端进行插入,而在另一端删除元素


在队列中,允许插入的一端称为队尾 (rear), 允许 删除的一端则称为队头 (front)

 

二、栈的表示和操作的实现 

1、栈的类型定义 

栈也有两种存储表示方法,分别称为顺序栈链栈 

 

 2、顺序栈的表示和实现

顺序栈是指利用顺序存储结构实现的栈,即利用一组地址连续的存储单元依次存放自栈底
到栈顶的数据元素
,同时附设指针 top 指示栈顶元素在顺序栈中的位置 

通常习惯的做法是:
以 top = 0 表示空栈,鉴于 C 语言中数组的下标约定从 0 开始,则当以 C 语言作描述语言时,如此设定会带 来很大不便,因此另设指针 base 指示栈底元素在顺序栈中的位置。当 top 和 base 的值相等时,表示空栈。

顺序栈的存储结构: 

#define MAXSIZE 10 // 顺序栈存储空间的初始分配量
typedef struct{
    SElemType *base; // 栈底指针
    SElemType *top; // 栈顶指针
    int stackszie; // 栈可用的最大容量
}SqStack;

 

base 为栈底指针,初始化完成后,栈底指针 base 始终指向栈底的位置,若 base 的值为
NULL, 则表明栈结构不存在。top 为栈顶指针,其初值指向栈底。每当插入新的栈顶元素
时,指针 top 增 1; 删除栈顶元素时, 指针 top 减 1


栈空时,top 和 base 的值相等,都指向栈底

栈非空时, top 始终指向栈顶元素的上一个位置


由于顺序栈和顺序表一样,受到最大空间容量的限制,虽然可以在“满员 ”时重新分配空
间扩大容量,但工作量较大,应该尽量避免

Ⅰ、初始化 

顺序栈的初始化操作就是为顺序栈动态分配一个预定义大小的数组空间 

步骤:

  1. 为顺序栈动态分配一个最大容量为 MAXSIZE 的数组空间,是 base 指向这段空间的基地址,即栈底
  2. 栈顶指针 top 初始为 base,表示栈为空
  3. stacksize 置为栈的最大容量 MAXSIZE 
Status InitStack(SqStack &S){
    // 构造一个空栈 S
    S.base = new SElemType[MAXSIZE]; //为顺序栈动态分配一个最大容量为 MAXSIZE 的数组空间
    if(!S.base)
        exit(OVERFLOW); //存储分配失败
    S.top = S.base; //top 初始为 base,空栈
    S.stacksize = MAXSIZE; // stacksize置为栈的最大容量 MAXSIZE
    return OK;
}

 

Ⅱ、入栈 

入栈操作是指在栈顶插入一个新的元素 

步骤:

  1. 判断栈是否满,若满则返回 ERROR
  2. 将新元素压入栈顶,栈顶指针加 1 
Status Push(SqStack &S,ElemType e){
    // 插入元素 e 为新的栈顶元素
    if(S.top - S.base == S.stacksize)
        return ERROR; // 栈满
    *S.top++ = e; // 元素 e 压入栈顶,栈顶指针加 1
    return OK;
}

 

Ⅲ、出栈 

出栈操作是将栈顶元素删除 

步骤:

  1. 判断栈是否为空,若空则返回 ERROR
  2. 栈顶指针减 1,栈顶元素出栈 
Status Pop(SqStack &S,SElemType &e){
    // 删除 S 的栈顶元素,用 e 返回其值
    if(S.top == S.base)
        return ERROR; // 栈空
    e = *--S.top; // 栈顶指针减 1,将栈顶元素赋给 e
    return OK;
}

 

 

Ⅳ、取栈顶元素 

当栈非空时, 此操作返回当前栈顶元素的值, 栈顶指针保待不变 

SElemType GetTop(SqStack S){
    // 返回 S 的栈顶元素,不修改栈顶指针
    if(S.top != S.base){ // 栈非空
        return *(S.top - 1); // 返回栈顶元素的值,栈顶指针不变
    }
}

 

3、链表的表示和实现 

 链栈是指采用链式存储结构实现的栈


通常链栈用单链表来表示

链栈的存储结构:

typedef struct StackNode{
    ElemType data;
    struct StackNode *next;
} StackNode,*LinkStack;

 

Ⅰ、初始化 

链栈的初始化操作就是构造一个空栈, 因为没必要设头结点, 所以直接将栈顶指针置空即可 

Status IniyStack(LinkStack &S){
    // 构造一个空栈 S,栈顶指针置空
    S = NULL;
    return OK;
}

Ⅱ、入栈 

链栈在入栈前不需要判断栈是否满,只需要为入栈元素动态分配一个结点空间 

步骤:

  1. 为入栈元素 e 分配空间,用指针 p 指向
  2. 将新结点数据置为 e
  3. 将新结点插入栈顶
  4. 修改栈顶指针为 p 
Statuc Push(LinkStack &S,SElemType e){
    // 在栈顶插入元素 e
    p = new StackNode; // 生成新结点
    p->data = e; // 将新结点数据域置为 e
    p->next = S; // 将新结点插入栈顶
    S = p; // 修改栈顶指针为 p
    return OK;
}

 

 

Ⅲ、出栈 

链栈在出栈前也需要判断栈是否为空,在出栈后需要释放出栈元素的栈顶空间 

步骤:

  1. 判断栈是否为空,若空则返回 ERROR
  2. 将栈顶元素赋给 e
  3. 临时保存栈顶元素的空间,以备释放
  4. 修改栈顶指针,指向新的栈顶元素
  5. 释放原来栈顶元素的空间 
Status Pop(LinkStack &S,SELemType &e){
    // 删除 S 的栈顶元素,用 e 返回其值
    if(S == NULL)
        return ERROR; // 栈空
    e = S->data; // 将栈顶元素赋给 e
    p = S; // 用 p 临时保存栈顶元素空间,以备释放
    S = S->next; // 修改栈顶指针
    delete p; // 释放原栈顶元素的空间
    return OK;
}

 

 

Ⅳ、取栈顶元素 

当栈非空时,此操作返回当前栈顶元素的值,栈顶指针S保持不变 

SElemType GetTop(LinkStack S){
    // 返回 S 的栈顶元素,不修改栈顶指针
    if(S != NULL) // 栈非空
    return S->data; // 返回栈顶元素的值,栈顶指针不变
}

 

三、栈与递归

 1、采用递归算法解决问题 

所谓递归是指,若在一个函数、过程或者数据结构定义的内部又直接(或间接)出现定义本身的应用,则称它们是递归的,或者是递归定义的 

1.1、定义是递归的 

有很多数学函数是递归定义的 , 如大家熟悉的阶乘函数 

Fact(n) = \begin{cases} 1 &\text{若 n = 0} \\ n * Fact(n - 1) &\text{若 n > 0} \end{cases} 

 二阶 Fibonacci 数列

Fib(n) = \begin{cases} 1 &\text{若 n = 1 或 n = 2} \\ Fib(n -1) + Fib(n - 2) &\text{其他情形} \end{cases}

long Fact(long n){
    if(n == 0)
        return 1;
    else
        return n * Fact(n - 1);
}
long Fib(long n){
    if(n == 1 || n == 2)
        return 1;
    else
        return Fib(n - 1) + Fib(n - 2);
}

类似这种的复杂问题,若能够分解成几个相对简单且解法相同或类似的子问题来求解,便称作递归求解


这种分解-求解的策略叫做“分治法

采用“分治法”进行递归求解的问题需要满足三个条件:

  1. 能将一个问题转变成一个新问题,而新问题与原问题的解法相同或类同,不同的仅是处理的对象,并且这些处理对象更小且变化有规律
  2. 可以通过上述转化而使问题简化
  3. 必须有一个明确的递归出口,或称递归的边界

“分治法” 求解递归问题算法的一般形式: 

void p(参数表){
    if(递归结束条件成立) // 递归终止条件
        可直接求解;
    else
        p(较小的参数); // 递归步骤
}

 

1.2、数据结构是递归的 

某些数据结构本身具有递归的特性,则它们的操作可递归地描述 

遍历输出链表中各个结点的递归算法 

步骤:

  1. 如果 p 为 NULL,递归结束返回
  2. 否则输出 p->data,p 指向后继结点继续递归 
void TraverseList(LinkList p){
    if(p == NULL)
        return;
    else{
        cout << p->data << end;
        TraverseList(p->next);
    }
}

简化:

void TraverseList(LinkList p){
    if(p){
        cout<<p->data<<end;
        TraverseList(p->next);
    }
}

1.3、问题的解法是递归的 

还有一类问题,虽然问题本身没有明显的递归结构,但用递归求解比迭代求解更简单,如 Hanoi 塔问题、八皇后问题、迷宫问题等 

n 阶 Hanoi 塔问题 

步骤:

  1. 如果 n = 1,则直接将编号为 1 的圆盘从 A 移到 C,递归结束
  2. 否则
    1. 递归,将 A 上编号为 1 至 n - 1 的圆盘移到 B,C 做辅助塔
    2. 直接将编号为 n 的圆盘从 A 移到 C
    3. 递归,将 B 上编号为 1 至 n - 1 的圆盘移到 C,A 做辅助塔 
void Hanoi(int n,char A,char B,char C){
    // 将塔座 A 上的 n 个圆盘按规则搬到 C 上,B 做辅助塔
    if(n == 1)
        move(A,1,C); // 将编号为 1 的圆盘从 A 移到 C
    else{
        Hanoi(n - 1,A,C,B); // 将 A 上编号为 1 至 n - 1 的圆盘移到 B,C 做辅助塔
        move(A,n,C); // 将编号为 n 的圆盘从 A 移到 C
        Hanoi(n - 1,B,A,C); // 将 B 上编号为 1 至 n - 1 的圆盘移到 C,A 做辅助塔
    }
}

 

2、递归过程和递归工作栈 

一个递归函数的运行过程类似于多个函数的嵌套调用,只是调用函数和被询用函数是同一个函数

为了保证递归函数正确执行,系统需设立一个“递归工作栈”作为整个递归函数运行期间使用的数据存储区

每一层递归所需信息构成一个工作记录,其中包括所有的实参、所有的局部变量,以及上一层的返回地址

每进入一层递归,就产生一个新的工作记录压入栈顶 

每退出一层递归, 就从栈顶弹出一个工作记录,则当前执行层的工作记录必是递归工作栈栈顶的工作记录,称这 个记录为“活动记录”

 

3、递归算法的效率分析 

3.1、时间复杂度的分析 

递归计算 Fibonacci 数列和 Hanoi 塔问题递归算法的时间复杂度均为O(2^{n})

 

3.2、空间复杂度的分析 

对于递归算法,空间复杂度 

S(n) = O(f(n)) 

其中,f(n) 为 "递归工作栈" 中工作记录的个数与问题规模 n 的函数关系 

递归解决阶乘间题、 Fibonacci 数列问题、 Hanoi 塔问题的递归算法的空间复杂度均为O(n)

 

4、队列的表示和操作的实现 

4.1、循环队列——队列的顺序表示和实现 

队列的顺序存储结构 

#define MAXSIZE 100 // 队列可能达到的最大长度
typedef struct{
    QElemType *base; // 存储空间的基地址
    int front; // 头指针
    int rear; // 尾指针
} SqQueue;

初始化创建空队列时,令 front = rear = 0 , 每当插入新的队列尾元素时,尾指针 rear 增 1; 每当删除队列头元素时,头指针 front 增 1。因此,在非空队列 中,头指针始终指向队列头元素,而尾指针始终指向队列尾元素的下一个位置 

以上方式可能会导致“假溢出”现象。解决方法:循环队列


头、 尾指针以及队列元素之间的关系不变 ,只是在循环队列中,头、 尾指针“依环状增
1”的操作可用“模”运算来实现。通过取模,头指针和尾指针就可以在顺序表空间内以头
尾衔接的方式“循环”移动

 

Ⅰ、初始化 

循环队列的初始化操作就是动态分配一个预定义大小为 MAXSIZE 的数组空间 

步骤:

  1. 为队列分配一个最大容量为 MAXSIZE 的数组空间,base 指向数组空间的首地址
  2. 头指针和尾指针置为零,表示队列为空 
Status InitQueue(SqQueue &Q){
    // 构造一个空队列 Q
    Q.base = new QElemType[MAXSIZE];// 为队列分配一个最大容量为 MAXSIZE 的数组空间
    if(!Q.front)
        exit(OVERFLOW); // 存储分配失败
    Q.front = Q.rear = 0; // 头指针和尾指针为零,队列为空
    return OK;
}

 

Ⅱ、求队列长度 

对于非循环队列,尾指针和头指针的差值便是队列长度,而对于循环队列,差值可能为负
数,所以需要将差值加上 MAXSIZE, 然后与 MAXSIZE 求余 

int QueueLength(SqQueue Q){
    // 返回 Q 的元素个数,即队列的长度
    return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}

 

Ⅲ、入队 

入队操作是指在队尾插入一个新的元素 

步骤:

  1. 判断队列是否满,若满则返回 ERROR
  2. 将新元素插入队尾
  3. 队尾指针加 1 
Status EnQueue(SqQueue &Q,QElemType e){
    // 插入元素 e 为 Q 的新的队尾元素
    if((Q.rear + 1) % MAXSIZE == Q.front) // 尾指针在循环意义上加 1 后等于头指针,队满
        return ERROR;
    Q.base[Q.rear] = e; // 新元素插入队尾
    Q.rear = (Q.rear + 1) % MAXSIZE; // 队尾指针加 1
    return OK;
}

  

Ⅳ、出队 

出队操作是将队头元素删除 

步骤:

  1. 判断队列是否为空,若空则返回 ERROR
  2. 保存队头元素
  3. 队头指针加 1 
Status DeQueue(SqQueue &Q,QElemType &e){
    // 删除 Q 的队头元素,用 e 返回其值
    if(Q.front == Q.rear) // 队空
        return ERROR;
    e = Q.base[Q.front]; // 保存队头元素
    Q.front = (Q.front + 1) % MAXSIZE; // 队头指针加 1
    return OK;
}

  

Ⅴ、取队头元素 

当队列非空时,此操作返回当前队头元素的值,队头指针保持不变 

SElemeType GetHead(SqQueue Q){
    // 返回 Q 的队头元素,不修改队头指针
    if(Q.front != Q.rear) // 队列非空
    return Q.base[Q.front]; // 返回队头元素的值,队头指针不变
}

 

4.2、链队——队列的链式表示和实现 

链队是指采用链式存储结构实现的队列


一个链队显然需要两个分别指示队头和队尾的指针(分别称为头指针和尾指针)才能唯一
确定 

队列的链式存储结构 

typedef struct QNode{
    QElemType data;
    struct QNode *next;
} QNode,*QueuePtr;

typedef struct{
    QueuePtr front; // 队头指针
    QueuePtr rear; // 队尾指针
} LinkQueue;

 

Ⅰ、初始化 

链队的初始化操作就是构造一个只有一个头结点的空队 

步骤:

  1. 生成新的结点作为头结点,队头和队尾指针指向此结点
  2. 头结点的指针域置空 
Status InitQueue(LinkQueue &Q){
    // 构造一个空队列 Q
    Q.front = Q.rear = new QNode; // 生成新结点作为头结点,队头和队尾指针指向此结点
    Q.front->next = NULL; // 头结点的指针域置空
    return OK;
}

 

Ⅱ、入队 

链队在入队前不需要判断队是否满,需要为入队元素动态分配一个结点空间 

步骤:

  1. 为入队元素分配结点空间,用指针 p 指向
  2. 将新结点数据域置为 e
  3. 将新结点插入到队尾
  4. 修改队尾指针为 p 
Status EnQueue(LinkQueue &Q,QElemType e){
    // 插入元素 e 为 Q 的新的队尾元素
    p = new QNode; // 为入队元素跟陪结点空间,用指针 p 指向
    p->data = e; // 将新结点数据域置为 e
    p->next = NULL; 
    Q.rear->next = p; // 将新结点插入到队尾
    Q.rear = p; // 修改队尾指针
    return OK;
}

 

Ⅲ、出队 

链队在出队前也需要判断队列是否为空,在出队后需要释放出队头元素的所占空间

步骤:

  1. 判断队列是否为空,若空则返回 ERROR
  2. 临时保存队头元素的空间,以备释放
  3. 修改队头指针,指向下一个结点
  4. 判断出队元素是否为最后一个元素,若是,则将队尾指针重新赋值,指向头结点
  5. 释放原队头元素的空间 
Status DeQueue(LinkQueue &Q,QElemType &e){
    // 删除 Q 的队头元素,用 e 返回其值
    if(Q.front == Q.rear) // 若队列为空,则返回 ERROR
        return ERROR;
    p = Q.front->next; // p 指向队头元素
    e = p->data; // e 保存队头元素的值
    Q.front->next = p->next; // 修改头指针
    if(Q.rear == p)
        Q.rear = Q.front; // 最后一个元素被删,队尾指针指向头结点
    delete p; // 释放原队头元素的空间
    return OK;
}

 

Ⅳ、取队头元素 

当队列非空时,返回当前队头元素的值,队头指针保持不变 

SElemType GetHead(LinkQueue Q){
    // 返回 Q 的队头元素,不修改队头指针
    if(Q.front != Q.rear) // 队列非空
    return Q.front->next->data; // 返回队头元素的值,队头指针不变
}

 

参考资料:《数据结构  C语言版 严蔚敏 第2版》 

数据结构》(C语言版)是为“数据结构”课程编写的教材,也可作为学习数据结构及其算法的C程序设计的参数教材。学了数据结构后,许多以前写起来很繁杂的代码现在写起来很清晰明了. 本书的前半部分从抽象数据类型的角度讨论各种基本类型的数据结构及其应用;后半部分主要讨论查找和排序的各种实现方法及其综合分析比较。 全书采用类C语言作为数据结构和算法的描述语言。 本书概念表述谨,逻辑推理密,语言精炼,用词达意,并有配套出的《数据结构题集》(C语言版),便于教学,又便于自学。 本书后附有光盘。光盘内容可在DOS环境下运行的以类C语言描述的“数据结构算法动态模拟辅助教学软件,以及在Windows环境下运行的以类PASCAL或类C两种语言描述的“数据结构算法动态模拟辅助教学软件”。内附 数据结构算法实现(蔚敏配套实现程序) 目录: 第1章 绪论 1.1 什么是数据结构 1.2 基本概念和术语 1.3 抽象数据类型的表现与实现 1.4 算法和算法分析 第2章 线性表 2.1 线性表的类型定义 2.2 线性表的顺序表示和实现 2.3 线性表的链式表示和实现 2.4 一元多项式的表示及相加 第3章 队列 3.1 3.2 的应有和举例 3.3 与递归的实现 3.4 队列 3.5 离散事件模拟 第4章 串 4.1 串类型的定义 4.2 串的表示和实现 4.3 串的模式匹配算法 4.4 串操作应用举例 第5章 数组和广义表 5.1 数组的定义 5.2 数组的顺序表现和实现 5.3 矩阵的压缩存储 5.4 广义表的定义 5.5 广义表的储存结构 5.6 m元多项式的表示 5.7 广义表的递归算法第6章 树和二叉树 6.1 树的定义和基本术语 6.2 二叉树 6.2.1 二叉树的定义 6.2.2 二叉树的性质 6.2.3 二叉树的存储结构 6.3 遍历二叉树和线索二叉树 6.3.1 遍历二叉树 6.3.2 线索二叉树 6.4 树和森林 6.4.1 树的存储结构 6.4.2 森林与二叉树的转换 6.4.3 树和森林的遍历 6.5 树与等价问题 6.6 赫夫曼树及其应用 6.6.1 最优二叉树(赫夫曼树) 6.6.2 赫夫曼编码 6.7 回溯法与树的遍历 6.8 树的计数 第7章 图 7.1 图的定义和术语 7.2 图的存储结构 7.2.1 数组表示法 7.2.2 邻接表 7.2.3 十字链表 7.2.4 邻接多重表 7.3 图的遍历 7.3.1 深度优先搜索 7.3.2 广度优先搜索 7.4 图的连通性问题 7.4.1 无向图的连通分量和生成树 7.4.2 有向图的强连通分量 7.4.3 最小生成树 7.4.4 关节点和重连通分量 7.5 有向无环图及其应用 7.5.1 拓扑排序 7.5.2 关键路径 7.6 最短路径 7.6.1 从某个源点到其余各顶点的最短路径 7.6.2 每一对顶点之间的最短路径 第8章 动态存储管理 8.1 概述 8.2 可利用空间表及分配方法 8.3 边界标识法 8.3.1 可利用空间表的结构 8.3.2 分配算法 8.3.3 回收算法 8.4 伙伴系统 8.4.1 可利用空间表的结构 8.4.2 分配算法 8.4.3 回收算法 8.5 无用单元收集 8.6 存储紧缩 第9章 查找 9.1 静态查找表 9.1.1 顺序表的查找 9.1.2 有序表的查找 9.1.3 静态树表的查找 9.1.4 索引顺序表的查找 9.2 动态查找表 9.2.1 二叉排序树和平衡二叉树 9.2.2 B树和B+树 9.2.3 键树 9.3 哈希表 9.3.1 什么是哈希表 9.3.2 哈希函数的构造方法 9.3.3 处理冲突的方法 9.3.4 哈希表的查找及其分析 第10章 内部排序 10.1 概述 10.2 插入排序 10.2.1 直接插入排序 10.2.2 其他插入排序 10.2.3 希尔排序 10.3 快速排序 10.4 选择排序 10.4.1 简单选择排序 10.4.2 树形选择排序 10.4.3 堆排序 10.5 归并排序 10.6 基数排序 10.6.1 多关键字的排序 10.6.2 链式基数排序 10.7 各种内部排序方法的比较讨论 第11章 外部排序 11.1 外存信息的存取 11.2 外部排序的方法 11.3 多路平衡归并的实现 11.4 置换一选择排序 11.5 最佳归并树 第12章 文件 12.1 有关文件的基本概念 12.2 顺序文件 12.3 索引文件 12.4 ISAM文件和VSAM文件 12.4.1 ISAM文件 12.4.2 VSAM文件 12.5 直接存取文件(散列文件) 12.6 多关键字文件 12.6.1 多重表文件 12.6.2 倒排文件 附录A 名词索引 附录B 函数索引 参考书目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qx_java_1024

祝老板生意兴隆,财源广进!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值