Bootstrap

数据结构-栈

目录

栈的示意图

入栈、出栈的操作示图

先进栈不一定最后出栈

栈的抽象数据类型定义

顺序栈的表示和实现

空栈和栈满

使用数组作为顺序栈存储方式的特点

顺序栈类型定义

顺序栈的入栈

顺序栈的出栈

链栈的表示和实现

链栈的类型定义

链栈的入栈

链栈的出栈 

递归

递归问题(用分治法求解) 

函数调用过程

求解阶乘n!的过程

递归函数调用的实现 

例:进行fact(4)系统栈的变化状态

递归→非递归

尾递归

单向递归

案例引入

进制转换

括号匹配的检验 

表达式求值 

舞伴问题


  • 栈(stack) 是仅在表尾进行插入和删除操作的线性表
    • 又称为后进先出(Last In First Out)的线性表,简称LIFO结构
  • 表尾(即aₙ端)称为栈顶(Top),表头(即a₁端)称为栈底(Base)
  • 插入元素的栈顶(即表尾)的操作,称为入栈(进栈、压栈,PUSH)
  • 从栈顶(即表尾)删除最后一个元素的操作,称为出栈(弹出,POP)

栈的示意图


入栈、出栈的操作示图



先进栈不一定最后出栈

  • 栈对线性表的插入和删除的位置进行了限制,但并没有对元素进出的时间进行限制

  • 假设有三个元素a,b,c,入栈顺序是a,b,c,则它们的出栈顺序有几种? 
  1. c-b-a:a入栈,b入栈,c入栈。c出栈,b出栈,a出栈
  2. a-b-c:a入栈,a出栈,b入栈。b出栈,c入栈,c出栈
  3. a-c-b:a入栈,a出栈。b入栈,c入栈,c出栈,b出栈
  4. b-a-c:a入栈,b入栈,b出栈,a出栈,c入栈,c出栈
  5. b-c-a:a入栈,b入栈,b出栈,c入栈,c出栈,a出栈

栈的抽象数据类型定义

ADT Stack{

数据对象:

        D:{ ai | ai ∈ ElemSet, i = 1, 2, ...,n, n >= 0  }

数据关系:

        R1 = { < ai - 1, ai >|ai - 1, ai ∈ D, i = 2, ..., n }

        约定 aₙ 端为栈顶, a₁端 为栈底

基本操作:

        InitStack(*S) 

        操作结果:初始化操作,构造一个空栈S

        DestroyStack(*S)

        初始条件:栈S已存在

        操作结果:栈S被销毁

        ClearStack(*S)

        初始条件:栈S已存在

        操作结果:将S清为空栈

        StackEmpty(S)

        初始条件:栈S已存在

        操作结果:若栈S为空栈,则返回TRUE, 否则FALSE

        GetTop(S, *e) 

        初始条件:栈S已存在且非空

        操作结果:用e返回S的栈顶元素

        Push(*S, e) 

        初始条件:栈S已存在

        操作结果:插入元素e为新的栈顶元素

        Pop(*S, *e) 

        初始条件:栈S已存在且非空

        操作结果:删除S的栈顶元素aₙ,并用e返回其值

        StackLength(S)

        初始条件:栈S已存在

        操作结果:返回S的元素个数,即栈的长度

}ADT Stack 

顺序栈的表示和实现

  • 存储方式:同一般线性表的顺序存储结构完全相同
  • 利用一组地址连续的存储单元一次存放自栈底到栈顶的数据元素。栈底一般在低地址端
    • 附设top指针,指示栈顶元素在顺序栈中的位置(为了方便操作,通常top指示真正的栈顶元素之上的下标位置)
    • 另设base指针,指示栈底元素在顺序栈中的位置
    • 用stacksize表示栈可使用的最大容量

空栈和栈满


  • base == top 是栈空的标志
  • top - base == stacksize 是栈满的标志
  • 栈满时的操作
    • 报错,返回操作系统
    • 分配更大的空间,作为栈的存储空间,将原栈的内容移入新栈(操作费时)

使用数组作为顺序栈存储方式的特点

  • 简单、方便、但易产生溢出(数组大小固定) 
    • 上溢(overflow):栈已经满,又要压入元素
    • 下溢(underflow):栈已经空,还要弹出元素
  • 上溢是一种错误,使问题的处理无法进行;而下溢一般认为是一种结束条件可,即问题处理结束

顺序栈类型定义

typedef int SElemType;
#define MAXSIZE 100

typedef struct
{
	SElemType* base; // 栈底指针
	SElemType* top; // 栈顶指针
	int stacksize; // 栈可用最大容量
}SqStack;

顺序栈的入栈


/* 顺序栈的入栈 */
Status Push(SqStack* S, SElemType e)
{
	// 栈满
	if (S->top - S->base == S->stacksize) return ERROR;

	/* 将新元素e赋值给栈顶空间,然后top指针指向下一空间 */
	*S->top = e;
	S->top++;
	// 等同于:*S.top++ = e;

	return OK;
}


顺序栈的出栈


Status Pop(SqStack* S, SElemType* e)
{
	if (S->top == S->base) return ERROR;// 判断是否栈空

	/* 将top指针下移,然后将指的元素复制给变量e */
	S->top--;
	e = *S->top;
	// 等于:e = *--S->top

	return OK;
}

链栈的表示和实现

  • 链栈是运算受限的单链表,只能在链表头部进行操作

链栈的类型定义

/*  
StackNode:栈的结点类型 
*LinkStack:指向这个结点的指针类型
*/

typedef struct StackNode
{
	SElemType data;  // 数据域,存放元素
	struct StackNode* next;  // 指针域,指向下一元素,类型仍为struct
}StackNode, *LinkStack;

  • 链栈中的指针的方向是反过来的
    • 指针域中存放的是前驱元素
  • 链表的头指针就是栈顶
  • 不需要头结点
  • 基本不存在栈满的情况
  • 空栈相当于头指针指向空
  • 插入和删除仅在栈顶处执行

链栈的入栈

Status Push(LinkStack* S, SElemType e)
{
	// 生成新结点p
	LinkStackPtr p = (LinkStackPtr)malloc(sizeof(StackNode));
	
	// 将元素放入新结点数据域
	p->data = e;

	/*
	将当前栈顶元素赋值给新结点的直接后继
	即,让新结点指向当前栈顶元素
	*/
	p->next = S->top;  

	// 让栈顶指针指向新结点
	S->top = p;
	S->cnt++;

	return OK;
}


链栈的出栈 

Status Pop(LinkStack* S, SElemType* e)
{
	LinkStackPtr p;

	/*
	S == NULL 检查链栈对象是否存在。
	S->top == NULL 检查链栈是否为空。
	*/
	if (S == NULL || S->top == NULL) return ERROR; // 检查栈是否为空
	
	*e = S->top->data;
	
	/* 将栈顶结点赋值给p */
	p = S->top;

	/* 让栈顶指针后移,指向后一结点 */
	S->top = S->top->next;

	/* 释放结点 */
	free(p);

	S->cnt--;

	return OK;
}


递归

  • 若一个对象部分地包含它自己,或用它自己给自己定义,则成这更对象是递归的
  • 若一个过程直接地或间接地调用自己,则称这个过程是递归的过程
  • 每个递归定义必须至少有一个条件,满足时递归不在进行,即不再引用自身而是返回值退

递归问题(用分治法求解) 

  • 分治法:对于一个较为复杂的问题,能够分解成几个相对简单且解法相同或类似的子问题来求解
  • 必备的三个条件
    • 能将一个胃转变成一个新问题,而新问题与原问题的解法相同或类同,不同的仅是处理的对象,且这些处理对象是变化有规律的
    • 可以通过上述转化而使问题简化
    • 必须有一个明确的递归出口,或称递归的边界

函数调用过程

  • 调用前,系统完成
    • 将实参、返回地址等传递给被调用函数
    • 为被调用函数的局部变量分配存储区
    • 将控制转移到被调用函数的入口 
  • 调用后,系统完成
    • 保存被调用函数的计算结果
    • 释放被调用函数的数据
    • 依照被调用函数保存的返回地址,将控制转移到调用函数

求解阶乘n!的过程

递归函数调用的实现 

例:进行fact(4)系统栈的变化状态

  • 递归
    • 优点:结构清晰,程序易读
    • 缺点:每次调用要生成工作记录,保存状态信息,入栈;返回时要出栈,恢复状态信息。时间开销大

递归→非递归

  • 递归时间开销大,对时间效率要求高时,要将递归→非递归
  • 方法
    • 尾递归、单向递归→循环结构
    • 自用栈模拟系统的运行时栈

尾递归

单向递归

  • 虽然有一处以上的递归调用语句,但各次递归调用语句的参数只和主调函数有关,相互之间参数无关,并且这些递归调用语句处于算法的最后

案例引入

进制转换

  • 十进制整数 N 向其他进制数d(二、八、十六)转换
  • 转换法则:除以 d 倒取余

  • 使用栈


括号匹配的检验 

  • 假设表达式中允许包含两种括号:圆括号和方括号
  • 其嵌套的顺序随意,即:
    • ( [ ] ( ) )[ ( [ ] []  ) ] 为正确格式
    • [ ( ] ) 为错误格式,交叉了,只能嵌套
    • ( [ ( ) )( ( ) ] )  为错误格式,数量不对
  • 遇到左括号,就放进栈中,遇到右括号,就与左括号匹配,然后把左括号出栈

  • 最后进入右方括号,无法匹配左方括号

表达式求值 

  • 表达式的组成
    • 操作数(operand):常数、变量
    • 运算符(operator):算术运算符、关系运算符和逻辑运算符
    • 界限符(delimiter):左右括弧和表达式结束符

  • 任何一个算术表达式都由操作数(常数、变量)、算术运算符(+ - * /)和界限符(括号、表达式结束符#、虚设的表达式符#)组成
    • 如:# 3 * (7 - 2)  #

  • 为了实现表达式求值。需要设置两个栈:
    • 一个是算法栈OPTR,用于寄存运算符
    • 另一个称为操作数栈OPND,用于寄存运算数和运算结果

  • 求值的处理过程是自左至右扫描表达式的每一个字符
    • 当扫描到的是运算数,则将其压入栈OPND
    • 当扫描到的是运算符时
      • 若这个运算符比OPTR栈顶运算符的优先级高,则入栈OPTR,继续向后处理
      • 若这个运算符比OPTR栈顶运算符有限级低,则从OPND栈中弹出两个运算数,从栈OPTN中弹出栈顶运算符进行运算,并将运算结果压入栈OPND
    • 继续处理当前字符,直到遇到结束符为止

舞伴问题

  • 假设在舞会上,男士和女士各自排成一队。舞会开始时,依次从男队和女队的队头各自出一个配成 舞伴。如果两对初始人数不相同,则较长的那一队中未配对者等待下一轮舞曲
    • 要求写算法模拟上述舞伴配对问题

思路:

  • 先入队的男士或女士先出队配成舞伴。因此该问题具有典型的先进先出特性,可以用队列作为算法的数据结构
    • 首先构造两个队列
    • 依次将队头元素出队配成舞伴
    • 某队为空,则另外一队等待着则是下一舞曲第一个可获得舞伴的人

;