数据结构与算法 | 第三章:栈与队列

news2024/11/27 0:37:11

本文参考网课为 数据结构与算法 1 第三章栈,主讲人 张铭 、王腾蛟 、赵海燕 、宋国杰 、邹磊 、黄群。

本文使用IDE为 Clion,开发环境 C++14

更新:2023 / 11 / 5


数据结构与算法 | 第三章:栈与队列

    • 概念
      • 示例
    • 实现
      • 顺序栈
        • 类定义
        • 进出栈
          • 进栈
          • 出栈
      • 链式栈
        • 类定义
        • 进出栈
          • 进栈
          • 出栈
      • 顺序栈 V.S 链式栈
    • 应用
      • 表达式求值
        • 中缀表达式
          • 概念
        • 后缀表达式
          • 概念
          • 示例
            • 对后缀表达式求值
            • 中缀表达式转换为后缀表达式
      • 递归
        • 概念
          • 递归、迭代
          • 递归函数
            • 尾递归
        • 示例
          • 阶乘函数的调用栈
          • 递归到非递归的转换
        • 通用的机械转换步骤
  • 队列
    • 概念
    • 实现
      • 顺序队列
        • 类定义
      • 链式队列
        • 类定义
      • 顺序队列 V.S 链式队列
    • 应用
      • 宽度优先搜索
        • 示例:人狼羊菜过河
          • 初步分析
          • 最终分析
  • 参考链接


线性表 可以在表的任意位置进行元素的插入、删除等运算。而 Stack )的运算只在表的一端进行,队列Queue )的运算只在表的两端进行,因此可以将 队列 视为操作受限的 线性表


概念

是一种限制访问端口的 线性表,后进先出( Last In First Out )。
的主要操作有 进栈push )和 出栈pop )。

的应用有:

  • 表达式求值
  • 消除递归
  • 深度优先搜索

的抽象数据类型如下:

template <class T>
class stack{
public:                                 // 栈的运算集
    void clear();                       // 清空栈
    bool push(const T item);            // push item入栈。成功推入,返回真;否则返回假
    bool pop(T& item);                  // 返回栈顶内容并弹出。成功弹出,返回真;否则返回假
    bool top(T& item);                  // 返回栈顶但不弹出。成功弹出,返回真;否则返回假
    bool isEmpty();                     // 若栈已空,返回真
    bool isFull();                      // 若栈已满,返回真
};

示例

假如给定一个入栈顺序 1、2、3、4,则出栈的顺序可以有哪些?

k 是最后一个出栈的,那么 k 把序列一分为二。在 k 之前入栈的元素,一定在比在 k 之后入栈的元素要提前出栈。

假设已知出栈顺序为 1、4、2、33 是最后1个出栈的,那么 31、2、3、4 分为 1、24。即 1、2 的出栈要在 4 之前。然而 2 不可能早于 4 出栈,因此 1、4、2、3 是不可能的。

再假设出栈顺序为 1、3、4、22 是最后1个出栈的,那么 21、2、3、4 分为 13、4。显然,1 是在 3、4 之前的,3 也是可以在 4 之前的。


那么,现在给定一个入栈序列,序列长度为 N,请计算有多少种出栈序列。

设有 f(N) 个出栈序列,如果 x 是最后一个出栈的,那么 x 个元素一定在 N-1-x 个元素之前出栈。

前面的 x 个元素有 f(x) 种出栈序列,后面的 N-1-x 个元素有 f(N-1-x) 种出栈序列。xN-1-x 个元素之间的整体出栈顺序是确定的,但是它们内部的出栈顺序是不定的,所以:

在这里插入图片描述
在这里插入图片描述
x 可以从 0N-1


实现

的物理实现有2种,1种称为 顺序栈Array-based Stack ),另1种称为 链式栈Linked Stack )。


顺序栈

使用向量实现,本质是 顺序表 的简化版。
关键是确定哪一端作为栈顶。


类定义
#include <iostream>
#include <iomanip>
using namespace std;

template class <T> class arrStack:public Stack<T>{
private:                                                // 栈的顺序存储
    int mSize;                                          // 栈中最多可存放的元素个数
    int top;                                            // 栈顶位置,应小于mSize
    T *st;                                              // 存放栈元素的数组
public:
    arrStack(int size){                                 // 构造函数,创建一个给定长度的顺序栈实例
        mSize = size; top = -1; st = new T[mSize];
    }
    ~arrStack(){delete [] st;}                          // 析构函数
    void clear(){top = -1;}                             // 清空栈;将栈顶top指向栈底,过后,若有新的元素入栈则会覆盖栈内原有的元素
};

进出栈
进栈

在这里插入图片描述

当栈中已经有 maxsize 个元素时,如果再做进栈运算,会产生 上溢overflow )。

因此,压入栈顶时需要做边界条件判定再进行入栈,如下:

bool arrStack<T>::push(const T item){
    if (top == mSize - 1){                  // 满栈
            count << "栈满溢出" << endl;
            return false;
    }else{                                  // 新元素入栈并修改栈顶指针
        st[++top] == item;					// top指针做自增
        return true;
    }
}

出栈

在这里插入图片描述
空栈 进行出栈运算时可能会出现 下溢underflow )。

因此,弹出栈顶时需要做边界条件判定再进行出栈,如下:

bool arrStack<T>::pop(T& item){         // 出栈
    if (top == -1){                     // 空栈
        cout << "空栈,不能出栈" << endl;
        return false;
    }else{
        item = st[top--];               // 返回栈顶,并缩减1
        return true;
    }
}

链式栈

单链表 方式存储,其中指针的方向是从栈顶向下链接。


类定义
template <class T> class lnkStack: public Stack<T>{
private:                                                // 栈的链式存储
    Link<T>* top;                                       // 指向栈顶的指针
    int size;                                           // 存放元素的个数
public:                                                 // 栈运算的链式元素实现
    lnkStack(int defSize){                              // 构造函数
        top = NULL; size = 0;
    }
    ~lnkStack(){                                        // 析构函数
        clear();
    }
};

进出栈
进栈
bool lnkStack<T>::push(const T item){           // 入栈操作的链式实现
    Link<T>* tmp = new Link<T>(item, top);
    top = tmp;
    size ++;
    return true;
}
Link(const T info, Link* nextValue){            // 具有2个参数的Link构造函数
    data = info;
    next = nextValue;
}

出栈
bool lnkStack<T>::pop(T& item){
    Link <T> *tmp;
    if (size == 0){
        cout << "空栈,不能出栈" << endl;
        return false;
    }
    item = top -> data;                     // 将top的数据赋值给item,将item弹出去
    tmp = top -> next;                      // 将top的next指向tmp
    delete top;                             // delete top对应的内存空间
    top = tmp;                              // 将tmp对应的数据赋值给top
    size--;                                 // 元素数量自减1
    return true;
}

顺序栈 V.S 链式栈

  • 时间效率

    • 所有操作都只需要常数时间。二者难分伯仲。
  • 空间效率

    • 顺序栈 须说明一个固定的长度;
    • 链式栈 的长度可变,但是增加结构性开销;
  • 应用范围

  • 实际应用中,顺序栈链式栈 应用范围更广泛

应用

的特点是后进先出。

通常被用来处理具有递归结构的数据:

  • 深度优先搜索
  • 表达式求值
  • 子程序 / 函数调用的管理
  • 消除递归

表达式求值

表达式的递归定义:

  • 基本符号集
    { 01,…,9+-*/() }
  • 语法成分集
    { <表达式><项><因子><常数><数字> }
    • 表达式
      • 中缀表达式
        23 + (34*45)/ (5 + 6 + 7)
      • 后缀表达式
        23 34 45 * 5 6 + 7 + / +

中缀表达式
概念

中缀表达式 的特点是:

  • 运算符在中间
  • 需要括号来改变优先级

中缀表达式 可以使用树状结构表示,例如,以下面的树状结构表达 4 * x * ( 2 * x + a ) - c


也可以使用语法公式表示,

在这里插入图片描述
也可以使用递归图示表示,

在这里插入图片描述


后缀表达式
概念

后缀表达式 的特点是:

  • 运算符在后面
  • 不需要括号

后缀表达式 可以使用树状结构表示,例如,以下面的树状结构表达 4x * 2x * a+ *c -

在这里插入图片描述

示例
对后缀表达式求值

假设待处理后缀表达式为 34 45 * 5 6 + 7 + / +

使用 的概念对该后缀表达式进行求值的算法为:
依次顺序读入表达式的符号序列(假设以 = 作为输入序列的结束 ),并根据读入的元素符号逐一分析:

  1. 当遇到的是一个操作数,则压入栈顶;
  2. 当遇到的是一个运算符,则从栈中两次取出栈顶,按照运算符对这两个操作数进行计算。然后将计算结果压入栈顶。

如此继续,直到遇到符号 =,这时栈顶的值就是输入表达式的值。

以代码来表示上述算法思想,即:

class Calculator{
private:
    Stack<double> s;                                    // 这个栈用于压入、保存操作数
    bool GetTwoOperands(double& opd1, double& opd2);    // 操作1:从栈顶弹出两个操作数 opd1 和 opd2
    void Compute(char op);                              // 操作2:取两个操作数,并按op对两个操作数进行计算
public:
    Calculator(void){};                                 // 创建计算器实例,开辟一个空栈
    void Run(void);                                     // 读入后缀表达式,遇 "=" 符号结束
    void Clear(void);                                   // 清除计算器,为下一次计算做准备
};

template<class ELEM>
bool Calculator<ELEM>::GetTwoOperands(ELEM& opnd1, ELEM& opnd2){
    if (S.IsEmpty()){                                               // 空栈。则无法按预期取出2个操作数。
        cerr << "Missing Operand!" << endl;
        return false;
    }
    opnd1 = S.Pop();                                                // 取出右操作数
    if (S.IsEmpty()){                                               // 取出右操作数后,栈空,则无法取到左操作数。
        cerr << "Missing Operand!" << endl;
        return false;
    }
    opnd2 = S.Pop();                                                // 取出左操作数
    return true;
}

template <class ELEM> void Calculator<ELEM>::Compute(char op){
    bool result; ELEM operand1, operand2;
    result = GetTwoOperands(operand1, operand2);
    if (result == true)
        switch(op){
        case '+': S.Push(operand2 + operand1); break;
        case '-': S.Push(operand2 - operand1); break;
        case '*': S.Push(operand2 * operand1); break;
        case '/': if (operand1 == 0.0){
            cerr << "Divide by 0!" << endl;
            S.ClearStack();
            }else S.Push(operand2 / operand1);
            break;
        }
    else S.ClearStack();
}

template <class ELEM> void Calculator<ELEM>::Run(void){
    char c; ELEM newoperand;
    while (cin >> c, c!='='){
        switch(c){
        case '+': case '-': case '*': case '/':            // 如果碰到的是+-*/这种操作符,则调用compute运算
            Compute(c);
            break;
        default:                                           // 如果碰到的不是+-*/这种运算符,把c这种char类型数据放回栈内并再次读取newoperand这种ELEM
            cin.putback(c); cin >> newoperand;
            S.Push(newoperand);
            break;
        }
    }
    if (!S.IsEmpty())
        cout << S.Pop() << endl;                           // 打印出最后结果
}

中缀表达式转换为后缀表达式

对后缀表达式求值时,向栈内存操作数;

转换中缀表达式为后缀表达式时,向栈内存操作符。因为所有的操作数载顺序上并没有变化,但是操作符的顺序有变化。

  • 当输入是操作数,直接输出到后缀表达式序列;
  • 当输入是左括号时,直接压栈;
  • 当输入是运算符时,
    • while
      • if (栈非空 and 栈顶不是左括号 and 输入运算符的优先级 <= 栈顶运算符的优先级) 时,将当前栈顶元素弹栈,输出到后缀表达式序列中;
        并将输入运算符压栈;
      • else 把输入的运算符压栈

输入运算符的优先级别<=栈顶运算符的优先级,比如输入运算符 +、栈顶运算符 *,则将栈顶 * 弹栈。

  • 当输入是右括号时,先判断栈是否为空:
    • 若栈为空,即无任何左括号已在栈内,则清栈退出;
    • 若栈非空,则把栈中的元素依次弹出:
      • 遇到第一个左括号为止,将弹出的元素输出到后缀表达式的序列中(弹出的开括号不放到序列中)
      • 若未遇到开括号,说明括号不匹配,则清栈退出;
  • 当中缀表达式的符号序列全部读入时,若栈内仍有元素,则把它们全部依次弹出,都放到后缀表达式序列尾部;
    • 若弹出的元素遇到开括号,说明括号不匹配,需要做错误异常处理。

递归

概念
递归、迭代

递归迭代 的异同点:

  • 相同点
    从小规模的相同问题来求解大规模问题
  • 不同点
    • 迭代 先从小问题入手,然后自底向上组合成大问题
    • 递归 先从大问题入手,然后自顶向下,对大问题进行分解

递归 更接近人类思维方式。因此,涉及递归算法比设计非递归算法往往更容易,大多数编程语言支持递归,许多函数式编程语言更是直接以递归为基础。
递归 在完成问题定义时,基本也同时完成了问题求解。


递归函数

在计算机中,我们通常以函数的方式对递归问题进行定义与求解。所谓 递归函数,指的是会直接或间接调用自身的函数。
递归函数 具有2个要素:一是基本的边界情形( 一般是规模小到不需要依赖子问题、可以直接求解的情形 ),二是递归规则( 决定如何将一个递归问题转化为规模更小的子问题 )。

计算机程序是一组顺序执行的指令序列。那么怎样用指令序列运行一个 递归函数 呢?
先看程序的内存分布,如下图:


内存被分为好几个区域,比如有内核、动态链接库、存放代码的只读区、存放全局变量的可读写区等等。
程序运行时,有两个区域的内存是不固定的:


  • 主要用于函数调用

  • 用于分配动态内存空间

递归函数 的相关操作主要发生在 内。
在函数调用栈里,元素是一种被称为栈帧的数据结构,每个帧对应1次函数调用,保存当次函数调用时传入的参数、函数返回地址以及局部变量。
比如,调用阶乘函数来求4的阶乘,如下:

在这里插入图片描述
有一个阶乘4对应的帧;有一个阶乘3对应的帧。每个帧内保存了此次函数调用时的相关信息,比如传入的参数、函数的返回地址以及局部变量等。
计算机中的函数调用与退出主要就是对调用栈中的帧进行操作:

  • 函数调用时
    • 计算机在栈中压入一个帧
      • 压入调用参数
      • 压入返回地址
      • 为局部变量分配空间
      • 其它信息(寄存器信息)
    • 计算机通过指令跳转到被调用函数开始的位置,开始执行
  • 函数退出时
    • 记录返回值
    • 释放栈帧(局部变量、返回地址、调用函数及其它信息)
    • 根据返回地址,跳回调用前位置继续执行

递归函数 的运行时的空间、时间开销分析如下:

  • 空间
    • 在每次调用时都需要创建栈帧并进行相应操作。在现实中,许多复杂函数的栈帧都很大,当调用次数很多时可能会把栈的空间全部用完,在一些系统上甚至可能会越界到动态链接库的区域,导致栈溢出的安全风险。
  • 时间
    • 栈操作需要消耗计算资源
    • 函数调用与退出时的跳转指令相比于其他正常指令的开销也是较大的

因此,如果能将递归函数转换为非递归函数,就可以减少程序运行时的开销。


尾递归

尾递归 指的是函数仅有一次自身调用,且该调用时函数退出前的最后一个操作。

举例如下:

long fact(long n){
    if (n<=1)
        return 1;
    else
        return n * fact(n-1);
}

虽然上面的阶乘递归函数只有一次对自身的调用,但是该调用并不是退出前的最后一个操作。因为在调用之后,还有一个乘法操作。所以该递归阶乘并非尾递归。

long fact_tail_rec(long n, long product){
    if (n<=1)
        return product;
    else
        return fact_tail_rec(n-1, product * n)
}

上面的 尾递归 函数多了一个参数用来保存乘积,则原先的乘法结果可以以参数的形式在调用前进行传递,而不再需要在函数内进行乘法操作。这样,自身调用就时函数退出前的最后一个操作,因此,它是一个 尾递归 函数。

尾递归 可以很容易将 递归函数 转化为 非递归函数尾递归 的本质是将单次计算的结果缓存起来以参数的形式传递给下一次调用,因此我们可以很容易地使用循环迭代的方式来保存这个累计的结果。

递归函数 转化为 非递归函数 之后,就可以消除栈开销与函数调用的开销。相比于原先线性增长的栈空间,转换之后只需要常数空间即可。

许多现代编程语言支持对 尾递归 的优化:

  • 编译器/解释器
    GCCLLVM / ClangIntel 编译器、Java 虚拟机
  • 函数式编程语言
    LISPSchemeScalaHaskellErlang

示例
阶乘函数的调用栈
long fact(long n){
    if (n<1)
        return 1;
    else
        return n*fact(n-1);

    int main(){
        int x=4;
        printf("%d\n", fact(x));
        return 0;
    }
}

在这里插入图片描述

递归到非递归的转换

背景:假如有n件物品,物品i的重量为w[i]。如果限定每种物品,要么完全放进背包、要么不放进背包,即物品是不可分割的。

问题:能否从这n件物品中选择若干件放入背包,使其重量之和恰好为s。


通用的机械转换步骤
  1. 定义栈帧,建立调用帧;
  2. 在栈中压入原始问题的帧( rd = 0 );
  3. 根据递归调用数 t,将程序划分为 t+1 个区域;
  4. 创建(t+2)个标签,逐区域翻译(除 return 语句、递归调用);
    ( t+2 )个标签为(t+1)个区域的边界
  5. 用 goto 实现递归调用
    形式 “push stack;goto label 0”,第 i 个调用的 rd=i;
  6. 用 goto 实现 return 语句;
    将所有 “return” 替换为 “goto label (t+1)”
  7. 在标签 t+1 后添加递归出口
    使用 “switch” 语句,根据栈顶的 rd 值判断继续执行的标签

队列

概念

队列 是一种限制访问点的 线性表,先进先出( First In First Out )。

  • 按照到达的顺序来释放元素;
  • 所有的插入在表的一端进行,所有的删除都在表的另一端进行;

队列 的主要元素有 队头front )、队尾rear )。

队列 的主要操作:

  • 入队列( enQueue
  • 出队列( deQueue
  • 取队首元素( getFront
  • 判断队列是否为空( isEmpty

队列 的抽象数据类型如下:

template <class T> class Queue{
public:                                 // 队列的运算集
    void clear();                       // 清空队列
    bool enQueue(const T item);         // 将item插入队尾。成功则返回真,否则则返回假
    bool deQueue(T & item);             // 返回队头元素并将其从队列中删除,成功则返回真
    bool getFront(T & item);            // 返回队头元素,但不删除,成功则返回真
    bool isEmpty();                     // 返回真,若队列已空
    bool isFull();                      // 返回真,若队列已满
};

实现

队列 的物理实现有2种,1种称为 顺序队列Array-based Stack ),另1种称为 链式队列Linked Stack )。


顺序队列

在这里插入图片描述
向量 存储队列元素,用两个变量分别指向队列的前端( front )和尾端( rear )。

  • front:指向当前待出队的元素位置(地址)
  • rear:指向当前待入队的元素位置(地址)

然而,这种 顺序队列 会有 溢出 的问题 ——

  • 上溢
    队列 满时,再做进队操作
  • 下溢
    队列 空时,再做删除操作
  • 假溢出
    rear = mSize - 1 时,再作插入运算就会产出 溢出。如果这时 队列 的前端还有许多空位置,这种现象称为 假溢出

为了避免 溢出 的出现,可以将 顺序队列 的首尾相连来变成一个 循环队列


类定义
class arrQueue:public Queue<T>{
private:
    int mSize;                          // 声明队列的数组大小
    int front;                          // 表示队头所在位置的下标
    int rear;                           // 表示待入队元素所在位置的下标
    T   *qu;                            // 存放类型为T的队列元素的数组
public:
    arrQueue(int size){                 // 创建队列的实例
        mSize = size + 1;               // 浪费一个存储空间,以此区别空队列和满队列
        qu = new T [mSize];
        front = rear = 0;
    }
    ~arrQueue(){                        // 消除该实例,并释放其空间
        delete[] qu;
    }
};

bool arrQueue<T>::enQueue(const T item){// item入队,插入队尾
    if (((rear + 1) % mSize == front)){ // rear指针+1,再对mSize取模,如果等于front,说明队列已满
        cout << "队列已满,溢出" << endl;
        return false;
    }
    qu[rear] = item;
    rear = (rear + 1)%mSize;            // 循环后继,将rear指针后移一位
    return true;
}

bool arrQueue<T>::deQueue(T& item){     // 返回队头元素并从队列中删除
    if (front == rear){                 // front指针等于rear指针,说明队列为空,不允许删除元素
        cout << "队列为空" << endl;
        return false;
    }
    item = qu[front];                   // item为队首元素
    front = (front + 1)%mSize;          // 循环后继,将front指针后移一位
    return true;
}

链式队列

链式队列 的本质是 单链表。用 单链表 方式存储,链接指针的方向是从队列的前端向尾端链接。

在这里插入图片描述
front 指向 单链表 的队首。用 rear 指针指向 单链表 的队尾。
我们把入队列和出队列限制在队首和队尾这两部分。不允许在其他部分进行操作。那么这其实就是一个 链式队列


类定义
template <class T>
class lnkQueue:public Queue<T>{
private:
    int size;                                       // 队列中当前元素的个数
    Link<T>* front;                                 // 表示队头的指针
    Link<T>* rear;                                  // 表示队尾的指针
public:
    lnkQueue(int size);                             // 创建队列的实例
    ~lnkQueue();                                    // 消除该实例,并释放其空间
};

bool enQueue(const T item){                         // item入队,插入队尾
    if (rear == NULL){                              // 空队列
            front = rear = new Link<T>(item, NULL);
    }
    else{                                           // 添加新元素
        rear -> next = new Link<T>(item, NULL);     // rear的next指针指向新元素
        rear = rear -> next;                        // rear指针指向最后一个元素
    }
    size ++;
    return true;
}

bool deQueue(T* item){                              // 返回队头元素并从队列中删除
    Link<T> *tmp;
    if (size == 0){                                 // 队列为空,没有元素可出队
        cout << "队列为空" << endl;
        return false;
    }
    *item = front -> data;                          // 将front指针指向的队首元素传给item
    tmp = front;                                    // tmp指针记录front的位置
    front = front -> next;                          // 将front指针指向原队首的下一个元素
    delete tmp;                                     // 删除tmp
    if (front == NULL)                              // 如果front是空的,则rear也为空
        rear = NULL;
    size --;
    return true;
}

顺序队列 V.S 链式队列

  • 空间效率
    • 顺序队列 需要固定的存储空间;
    • 链式队列 可以满足大小无法估计的情况;

应用

队列 满足先来先服务特性的应用,作为其数据组织方式或中间数据结构:

  • 调度或缓冲
    • 消息缓冲器
    • 邮件缓冲器
    • 计算机硬设备之间的通信也需要队列作为数据缓冲
    • 操作系统的资源管理
  • 宽度优先搜索

宽度优先搜索

示例:人狼羊菜过河
初步分析
  • 问题抽象

“人狼羊菜” 乘船过河。只有人能撑船,船只有两个位置(包括人)。狼羊、羊菜不能在没有人时共处。


  • 求解方案

求解该问题最简单的方法是使用试探法,即一步一步进行试探,每一步都搜索所有可能的选择,对前一步合适的选择再考虑下一步的各种方案。

用计算机实现上述求解的搜索过程可以采用两种不同的策略:

  • 队列
    • 宽度优先搜索
      搜索该步的所有可能状态,再进一步考虑后面的各种情况
    • 深度优先搜索
      沿某一状态走下去,不行再回头

假定采用宽度优先搜索解决农夫过河问题:

  • 采用队列作辅助结构,把下一步所有可能达到的状态都放在队列中,然后顺序取出对其分别处理。
  • 由于队列的操作按照先进先出原则,因此只有前一步的所有情况都处理完之后才能进入下一步。

先对数据进行抽象,对每个角色的位置进行描述。人、狼、羊和菜,四个目标依次各用一位,目标在起始岸位置 0、目标岸 1
0110 表示农夫、白菜在起始岸,而狼、羊在目标岸。此状态为不安全状态。
1000 ( 0x08 )表示人在目标岸,而狼、羊、菜在起始岸。
1111 (0x0F)表示人、狼、羊、菜都抵达目标岸。

如何从上述状态中得到每个角色所在位置?

在这里插入图片描述

bool farmer(int status)
{return ((status & 0x08) != 0);}

bool wolf(int status)
{return ((status & 0x04) != 0);}

bool goat(int status)
{return ((status & 0x02) != 0);}

bool cabbage(int status)
{return ((status & 0x01) != 0);}

函数返回值为真,表示所考察人或物在目标岸。否则,所考察人或物在起始岸;

在用以上方法拿到每个角色的所在位置之后可以对安全状态进行判断:

bool safe(int status)                   // 返回true 安全;返回false,不安全
{
    if ((goat(status) == cabbage(status)) && (goat(status) != farmer(status)))  // 羊和白菜共处,但是人不在
        return(false);
    if ((goat(status) == wolf(status) && (goat(status) != farmer(status))))     // 狼和羊共处,但是人不在
        return(false);
    return(true);
}

最终分析
  • 问题抽象

从状态 0000(整数0)出发,寻找全部由安全状态构成的状态序列,以 1111(整数15)为最终目标。
状态序列中每个状态都可以从前一状态通过农夫(可以带一样东西)划船过河的动作到达。
序列中不能出现重复状态。


  • 算法设计

定义一个整数队列 moveTo,它的每个元素表示一个可以安全到达的中间状态。
还需要定义一个数据结构记录已被访问过的各个状态,以及已被发现的能够到达当前这个状态的路径。

  • 顺序表 route 的第i个元素记录状态i是否已被访问过
  • 若route[i] 已被访问过,则在这个 顺序表 元素中记入前驱状态值,-1表示未被访问
  • route的大小(长度)为16

  • 算法实现
void solve(){
    int movers, i, location, newlocation;
    vector<int> route(END+1, -1);
    queue<int> moveTo;                              // 定义初始队列,看它moveTo到哪些队列上去
    moveTo.push(0x00);
    route[0]=0;
}

while (!moveTo.empty() && route[15] == -1){         //
    status = moveTo.front();                        // 拿到moveTo的front指针
    moveTo.pop();                                   // 把当前状态pop出来
    for (movers = 1; movers <= 8; movers << = 1){    // 农夫总是在移动。movers指针逐渐左移
        if (farmer(status)) == (bool)(status & movers){ // farmer(status)获取农夫状态,是1还是0;
                                                        // (status&movers)获取菜的状态,是1还是0;
                                                        // 如果农夫和菜的状态一致
            newstatus = status ^ (0x08 | movers);       // status ^ (0b1001 | 0b0001),即status 和 0b1001作异或操作,得0b0110
            if (safe(newstatus) && (route[newstatus] == -1)){   // 调用safe判断status的下一个状态newstatus是否安全。如果newstatus不安全,则不能变;
                                                                // route[newstatus] == -1,说明newstatus未被访问过
                route[newstatus] = status;
                moveTo.push(newstatus);                         // 将newstatus计入moveTo队列
            }
        }
    }
    if (route[15] != -1){                                           // 如果最后一个状态0b1111对应的不是-1,说明最后一个状态已经达到
        cout << "The reverse path is:" << endl;
        for (int status = 15; status >= 0; status = route[status]){ // 从最后一个状态0b1111开始,
            cout << "The status is:" << status << endl;
            if (status == 0) break;
        }
    }
    else
        cout << "No solution." << endl;
}

参考链接


  1. 数据结构与算法 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1173131.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

谈谈MySQL的底层存储

这个题目启的很大&#xff0c;但其实只是最近在复习MySQL知识的一点心得&#xff0c;比较零散。 更新数据时&#xff0c;底层page的变化 下面这个图&#xff0c;我还需要解释么&#xff1f; 上面的绿色是b数的索引块&#xff0c;分别说明了101号page的最大id是7,102号page的…

ACM MM 2023 | 清华、华为联合提出MISSRec:兴趣感知的多模态序列推荐预训练

©PaperWeekly 原创 作者 | 王锦鹏 单位 | 清华大学深圳国际研究生院 研究方向 | 多模态检索、推荐系统 序列推荐是一种主流的推荐范式&#xff0c;目的是从用户的历史行为中推测用户偏好&#xff0c;并为之推荐感兴趣的物品。现有的大部分模型都是基于 ID 和类目等信息做…

相机滤镜软件Nevercenter CameraBag Photo mac中文版特点介绍

Nevercenter CameraBag Photo mac是一款相机和滤镜应用程序&#xff0c;它提供了一系列先进的滤镜、调整工具和预设&#xff0c;可以帮助用户快速地优化和编辑照片。 Nevercenter CameraBag Photo mac软件特点介绍 1. 滤镜&#xff1a;Nevercenter CameraBag Photo提供了超过2…

【嵌入式 – GD32开发实战指南(ARM版本)】第2部分 外设篇 - 第2章 温湿度传感器AHT10

1 理论分析 1.1 AHT10介绍 AHT10,新一代温湿度传感器在尺寸与智能方面建立了新的标准:它嵌入了适于回流焊的双列扁平无引脚SMD封装,底面4 x 5mm ,高度1.6mm。传感器输出经过标定的数字信号,标准I2C格式。 AHT10 配有一个全新设计的ASIC专用芯片、一个经过改进的MEMS半导体…

难题来了:分库分表后,查询太慢了,如何优化?

说在前面&#xff1a; 尼恩社群中&#xff0c;很多小伙伴反馈&#xff0c; Sharding-JDBC 分页查询的速度超级慢&#xff0c; 怎么处理&#xff1f; 反馈这个问题的小伙伴&#xff0c;很多很多。 而且这个问题&#xff0c;也是面试的核心难题。前段时间&#xff0c;有小伙伴…

一看就懂的java对象内存布局

前言 Java 中一切皆对象&#xff0c;同时对象也是 Java 编程中接触最多的概念&#xff0c;深入理解 Java 对象能够更帮助我们深入地掌握 Java 技术栈。在这篇文章里&#xff0c;我们将从内存的视角&#xff0c;带你深入理解 Java 对象在虚拟机中的表现形式。 学习路线图&…

2023第二届全国大学生数据分析大赛A题思路

某电商平台用户行为分析与挖掘 背景&#xff1a;电商是当今用户最大的交易市场之一&#xff0c;电商行业也逐渐成熟&#xff0c; 所有市场中可售卖的商品全都在平台中存在&#xff0c;并且在网络和疫情的影 响下&#xff0c;在线上的消费行为满足全年龄段用户。 用户的交易行为…

unittest 通过TextTestRunner(buffer=True)打印断言失败case的输出内容

buffer是unittest.TextTestRunner的一个参数&#xff0c;它决定了测试运行时是否将输出结果缓存&#xff0c;并在测试完成后一次性打印。 当buffer设置为True时&#xff0c;测试运行期间的输出结果会被缓存起来&#xff0c;并在测试完成后一次性打印。这对于一些输出频繁的测试…

Lamport Clock算法

Lamport Clock 是一种表达逻辑时间的逻辑时钟&#xff08;logical clock&#xff09;&#xff0c;能够计算得到历史事件的时间偏序关系。 假设 P0进程是分布式集群中心节点中的监控者&#xff0c;用于统一管理分布式系统中事件的顺序。其他进程在发送消息之前和接受事件消息之后…

操作系统——内存映射文件(王道视频p57)

1.总体概述&#xff1a; 2.传统文件访问方式&#xff1a; 我认为&#xff0c;这种方式最大的劣势在于&#xff0c;如果要对整个文件的不同部分进行多次操作的话&#xff0c;这样确实开销可能会大一些&#xff0c;而且程序员还要指定对应的“分块”载入到内存中 3.内存映射文件…

Qt的事件

2023年11月5日&#xff0c;周日上午 还没写完&#xff0c;不定期更新 目录 事件处理函数的字体特点Qt事件处理的工作原理一些常用的事件处理函数Qt中的事件类型QEvent类的type成员函数可以用来判断事件的类型事件的类型有哪些&#xff1f;有多少种事件类 事件处理函数的字体特…

unittest 通过TextTestRunner(failfast=True),失败或错误时停止执行case

failfast是unittest.TextTestRunner的一个参数&#xff0c;它用于控制测试运行过程中遇到第一个失败或错误的测试方法后是否立即停止执行。 当failfast设置为True时&#xff0c;一旦发现第一个失败或错误的测试方法&#xff0c;测试运行就会立即停止&#xff0c;并输出相应的失…

插值表达式 {{}}

前言 持续学习总结输出中&#xff0c;今天分享的是插值表达式 {{}} Vue插值表达式是一种Vue的模板语法&#xff0c;我们可以在模板中动态地用插值表达式渲染出Vue提供的数据绑定到视图中。插值表达式使用双大括号{{ }}将表达式包裹起来。 1.作用&#xff1a; 利用表达式进行…

教你烧录Jetson Orin Nano的ubuntu20.04镜像

Jetson Orin Nano烧录镜像 视频讲解 教你烧录Jetson Orin Nano的ubuntu20.04镜像 1. 下载sdk manager https://developer.nvidia.com/sdk-manager sudo dpkg -i xxxx.deb2. 进入recovery 插上typeC后&#xff0c;短接J14的FORCE_RECOVERY和GND&#xff0c;上电 如下图&#…

【调度算法】单机调度遗传算法

问题描述 工件ABCDEFG工件编号0123456加工时间4765835到达时间3245321交货期10153024141320 目标函数 最小化交货期总延时时间 运算结果 最佳调度顺序&#xff1a; [6, 3, 2, 5, 0, 1, 4] 最小交货期延时时间&#xff1a; 47python代码 import random import numpy as np…

自动驾驶行业观察之2023上海车展-----智驾供应链(3)

智驾解决方案商发展 华为&#xff1a;五项重磅技术更新&#xff0c;重点发布华为ADS 2.0和鸿蒙OS 3.0 1&#xff09;产品方案&#xff1a;五大解决方案都有了全面的升级&#xff0c;分别推出了ADS 2.0、鸿蒙OS 3.0、iDVP智能汽车数字平台、智能车云服务和华为车载光最新 产品…

linux下使用vscode对C++项目进行编译

项目的目录结构 头文件swap.h 在自定义的头文件中写函数的声明。 // 函数的声明 void swap(int a,int b);swap.cpp 导入函数的声明&#xff0c;写函数的定义 #include "swap.h" // 双引号表示自定义的头文件 #include <iostream> using namespace std;// 函…

2023年中国商业密码行业研究报告

第一章 行业概况 1.1 定义及分类 根据《密码法》相关规定&#xff0c;密码是指采用特定变换的方法对信息等进行加密保护、安全认证的技术、产品和服务。 密码产业是指为了保障信息安全&#xff0c;提供加密保护、安全认证相关技术、产品和服务的相关行业总称&#xff0c;主 要…

为机器学习算法准备数据(Machine Learning 研习之八)

本文还是同样建立在前两篇的基础之上的&#xff01; 属性组合实验 希望前面的部分能让您了解探索数据并获得洞察力的几种方法。您发现了一些数据怪癖&#xff0c;您可能希望在将数据提供给机器学习算法之前对其进行清理&#xff0c;并且发现了属性之间有趣的相关性&#xff0c…

python 机器学习 常用函数

一 np.random.randint "randint" 是 "random integer" 的缩写&#xff0c;表示生成随机整数。 np.random.randint 是 NumPy 库中的一个函数&#xff0c;用于生成随机整数。以下是该函数的一般语法&#xff1a; np.random.randint(low, high, size)其中…