数据结构和算法(4):栈与队列

2023-11-17

栈 ADT 及实现

栈(stack)是存放数据对象的一种特殊容器,其中的数据元素按线性的逻辑次序排列,故也可定义首、末元素。
尽管栈结构也支持对象的插入和删除操作,但其操作的范围仅限于栈的某一特定端。
也就是说,若约定新的元素只能从某一端插入其中,则反过来也只能从这一端删除已有的元素。禁止操作的另一端,称作盲端。

后进先出:从栈结构的整个生命期来看,更晚(早)出栈的元素,应为更早(晚)入栈者。

ADT 功能
size() 返回栈的规模
empty() 判断栈是否为空
push(e) 将 e 插至栈顶
pop() 删除栈顶对象
top() 引用栈顶对象

实现:

#include "../Vector/Vector.h" //以向量为基类,派生出栈模板类
template <typename T> class Stack: public Vector<T> { //将向量首/末端作为栈底/顶
public: //size()、empty()以及其它开放接口,均可直接沿用
	void push ( T const& e ) { insert ( size(), e ); } //入栈:等效于将新元素作为向量末元素插入
	T pop() { return remove ( size() - 1 ); } //出栈:等效于删除向量末元素
	T& top() { return ( *this ) [size() - 1]; } //取顶:直接返向量末元素
};

栈与递归

在 Windows 等 大部分操作系统中,每个运行中的二进制程序都配有一个调用栈(call stack)或执行栈(execution stack)。
借助调用栈可以跟踪属于同一程序的所有函数,记录它们之间的相互调用关系,并保证在每一调用实例执行完毕之后,可以准确地返回。

调用栈的基本单位是帧(frame)
每次函数调用时,都会相应地创建一帧,记录该函数实例在二进制程序中的返回地址,以及局部变量、传入参数等,并将该帧压入调用栈。若在该函数返回之前又发生新的调用,则同样地要将与新函数对应的一帧压入栈中,成为新的栈顶。函数一旦运行完毕,对应的帧随即弹出,运行控制权将被交还给该函数的上层调用函数,并按照该帧中记录的返回地址确定在二进制程序中继续执行的位置。
在任一时刻,调用栈中的各帧,依次对应于那些尚未返回的调用实例,亦即当时的活跃函数实例。特别地,位于栈底的那帧必然对应于入口主函数main(),若它从调用栈中弹出,则意味着整个程序的运行结束,此后控制权将交还给操作系统.

进制转换

进制算法流程:
十进制转二进制: 使用除2取余法,从十进制数中反复除以2,将余数记录下来,然后将余数从下到上排列起来。
二进制转十进制: 从二进制的最右边开始,每个位上的数字乘以2的幂,然后将结果相加。

void convert ( Stack<char>& S,_int64 n, int base ) { //十进制数n到base进制的转换(迭代版)
	static char digit[] //0 <n, 1 < base <m 16,新进制下的数位符号,可视base取值范围适当扩充
	= { '0''1''2''3''4' , '5', '6''7''8', '9''A''B''C''D''E''F');
	while ( n > e ) { //由低到高,逐一计算出新进制下的各数位
		int remainder = ( int ) ( n % base ); S.push(digit[remainder] );	//余数(当前位)入栈
		n/= base; //n 更新为其对 base 的除商
	}
}//新进制下由高到低的各数位,自顶而下保存于栈s中

main(){
	Stack<char> S; convert(S, n, base);	//用栈记录转换得到的各数位
	while(!S.empty())
		printf("%c",S.pop());	//逆序输出
}

括号匹配

括号匹配的任务是,对任一程序块,判断其中的括号是否在嵌套的意义下完全匹配(简称匹配)。
顺序扫描表达式,用栈记录已扫描的部分:凡遇 (,则进栈;凡遇 ),则出栈。

#include <iostream>
#include <stack>
#include <string>

bool isBracketMatch(const std::string& input) {
    std::stack<char> brackets;
    for (char c : input) {
        if (c == '(' || c == '{' || c == '[') {
            brackets.push(c);
        } else if (c == ')' || c == '}' || c == ']') {
            if (brackets.empty()) {
                return false; // 括号不匹配,没有左括号与右括号匹配
            }
            char top = brackets.top();
            brackets.pop();
            if ((c == ')' && top != '(') || (c == '}' && top != '{') || (c == ']' && top != '[')) {
                return false; // 括号不匹配
            }
        }
    }
    return brackets.empty(); // 所有括号都正确匹配
}

int main() {
    std::string input = "{[()]}";
    if (isBracketMatch(input)) {
        std::cout << "Brackets are matched." << std::endl;
    } else {
        std::cout << "Brackets are not matched." << std::endl;
    }
    return 0;
}

在只有一种括号类型时,可以使用计数器起到与栈相同的效果;但存在多种括号类型时,计数器无法使用。

栈混洗

栈混洗是一个经典的问题,涉及到两个栈,其中一个栈包含一组数字,需要确定是否可以通过一系列栈操作将这些数字从一个初始顺序重新排列成另一个目标顺序。

问题的形式如下:

给定两个整数数组,一个代表初始栈的顺序,另一个代表目标栈的顺序,判断是否可以通过以下栈操作将初始栈的元素重新排列成目标栈的顺序:
1.将元素从初始栈的顶部移动到输出栈(可以看作是出栈操作)。
2.将元素从输入栈的底部移动到输出栈(可以看作是将输入栈反转后的出栈操作)。

如果可以,返回 true,否则返回 false

解决这个问题的一种常见方法是使用模拟。可以创建一个辅助栈来模拟操作,并按照目标顺序进行操作。具体步骤如下:
1.初始化一个辅助栈。
2.遍历目标栈的顺序(从左到右):
3.如果目标栈的当前元素与初始栈的顶部元素相同,直接从初始栈弹出元素。
4.否则,从初始栈中弹出元素,并将其推入辅助栈,直到找到与目标栈当前元素相同的元素。
5.继续遍历目标栈,如果辅助栈的栈顶元素与目标栈的当前元素相同,则从辅助栈中弹出元素。
6.最后,如果初始栈为空并且辅助栈也为空,返回 true;否则返回 false

#include <iostream>
#include <stack>
#include <vector>

bool isStackPermutation(const std::vector<int>& input, const std::vector<int>& target) {
    std::stack<int> initialStack;
    std::stack<int> auxStack;

    for (int num : input) {
        initialStack.push(num);
    }

    for (int num : target) {
        while (!initialStack.empty() && initialStack.top() != num) {
            auxStack.push(initialStack.top());
            initialStack.pop();
        }

        if (!initialStack.empty() && initialStack.top() == num) {
            initialStack.pop();
        } else if (!auxStack.empty() && auxStack.top() == num) {
            auxStack.pop();
        } else {
            return false;
        }
    }

    return initialStack.empty() && auxStack.empty();
}

int main() {
    std::vector<int> input = {1, 2, 3};
    std::vector<int> target = {2, 1, 3};

    if (isStackPermutation(input, target)) {
        std::cout << "The permutation is valid." << std::endl;
    } else {
        std::cout << "The permutation is not valid." << std::endl;
    }

    return 0;
}

栈混洗计数为: ( 2 n ! ) ( n + 1 ) ! n ! = c a t a l a n ( n ) \frac{(2n!)}{(n+1)!n!} = catalan(n) (n+1)!n!(2n!)=catalan(n)

甄别栈混洗: B B B A A A 的一个栈混洗,当且仅当对于任意 1 ≤ i < j < k ≤ n 1 \leq i < j < k \leq n 1i<j<kn,P 中都不包含如下模式: { . . . , k , . . . , i , . . . , j , . . . } \{ ..., k, ..., i, ..., j, ...\} {...,k,...,i,...,j,...},例如{3,1,2}.

中缀表达式求值

思路:

将中缀表达式转换为后缀表达式:
    创建一个空栈,用于存储操作符。
    从左到右遍历中缀表达式中的每个字符(数字和操作符)。
    如果遇到数字,直接输出到输出队列。
    如果遇到操作符:
        如果栈为空,将操作符压入栈。
        否则,比较操作符与栈顶操作符的优先级:
            如果操作符优先级高于栈顶操作符,将操作符压入栈。
            否则,弹出栈中较高或相等优先级的操作符,并将它们输出到输出队列,直到遇到更低优先级的操作符或栈为空,然后将当前操作符压入栈。
    如果遇到左括号"(",将其压入栈。
    如果遇到右括号")",弹出栈中的操作符并将它们输出到输出队列,直到遇到左括号"(",然后将左括号从栈中弹出但不输出。
    遍历结束后,将栈中剩余的操作符全部输出到输出队列。

计算后缀表达式的值:
    创建一个空栈,用于存储操作数。
    从左到右遍历后缀表达式中的每个元素(数字和操作符)。
    如果遇到数字,将其压入栈。
    如果遇到操作符,从栈中弹出所需数量的操作数(通常是两个),执行相应的运算,然后将结果压入栈。
    最终,栈中将只剩下一个元素,即表达式的值。

代码实现:

#include <iostream>
#include <stack>
#include <string>
#include <sstream>
#include <cctype>

//返回操作符的优先级,优先级越高,越早进行计算
int precedence(char op) {
    if (op == '+' || op == '-') return 1;
    if (op == '*' || op == '/') return 2;
    return 0;
}
//于执行操作符的运算,根据不同的操作符执行不同的操作,并返回结果
double applyOperator(double operand1, double operand2, char op) {
    switch (op) {
    case '+': return operand1 + operand2;
    case '-': return operand1 - operand2;
    case '*': return operand1 * operand2;
    case '/': return operand1 / operand2;
    default: return 0.0; // 处理未知操作符
    }
}

double evaluateInfixExpression(const std::string& expression) {
    std::stack<char> operators;
    std::stack<double> operands;
    //operators 用于存储操作符,operands 用于存储操作数
    std::istringstream iss(expression);
    //创建了一个 std::istringstream 对象,将中缀表达式字符串 expression 包装为输入流,并准备逐个读取字符串中的标记

    std::string token;
    while (iss >> token) {	//程序检查当前标记 token 是数字还是括号。如果是数字(包括正数和负数),将其转换为 double 类型并压入 operands 栈。如果是左括号 "(",将其压入 operators 栈。
        if (isdigit(token[0]) || (token.length() > 1 && token[0] == '-' && isdigit(token[1]))) {
            double operand = std::stod(token);
            operands.push(operand);
        }
        else if (token == "(") {
            operators.push('(');
        }
        else if (token == ")") {
        //在遇到右括号 ")" 时,程序将执行一系列操作来处理括号内的表达式。它会弹出操作符直到遇到左括号 "(",并对括号内的表达式进行计算,将结果压入 operands 栈。
        //如果在遇到左括号之前就遇到了栈空或其他操作符,表示右括号没有正确匹配,将返回0.0表示错误。
            while (!operators.empty() && operators.top() != '(') {
                char op = operators.top();
                operators.pop();

                if (operands.size() < 2) {
                    // 处理错误:操作数不足
                    return 0.0;
                }

                double operand2 = operands.top();
                operands.pop();
                double operand1 = operands.top();
                operands.pop();

                double result = applyOperator(operand1, operand2, op);
                operands.push(result);
            }

            if (!operators.empty() && operators.top() == '(') {
                operators.pop();
            }
            else {
                // 处理错误:未匹配的右括号
                return 0.0;
            }
        }
        else {
            while (!operators.empty() && precedence(operators.top()) >= precedence(token[0])) {
                char op = operators.top();
                operators.pop();

                if (operands.size() < 2) {
                    // 处理错误:操作数不足
                    return 0.0;
                }

                double operand2 = operands.top();
                operands.pop();
                double operand1 = operands.top();
                operands.pop();

                double result = applyOperator(operand1, operand2, op);
                operands.push(result);
            }

            operators.push(token[0]);
        }
    }

    while (!operators.empty()) {
        char op = operators.top();
        operators.pop();

        if (operands.size() < 2) {
            // 处理错误:操作数不足
            return 0.0;
        }

        double operand2 = operands.top();
        operands.pop();
        double operand1 = operands.top();
        operands.pop();

        double result = applyOperator(operand1, operand2, op);
        operands.push(result);
    }

    if (operands.size() != 1 || !operators.empty()) {
        // 处理错误:操作数和操作符未匹配
        return 0.0;
    }

    return operands.top();
}

int main() {
    std::string infixExpression = "2 + 3 * 4 - 1";
    double result = evaluateInfixExpression(infixExpression);

    if (result != 0.0) {
        std::cout << "Result: " << result << std::endl;
    }
    else {
        std::cout << "Invalid expression." << std::endl;
    }

    return 0;
}

当前的操作符比栈顶的操作符优先级低时,进行实际的运算。

逆波兰表达式

逆波兰表达式(Reverse Polish Notation,RPN),也称为后缀表达式,是一种数学表达式表示法,其中操作符在操作数之后。这种表示法消除了括号,并且使得表达式的计算顺序更加明确,不需要考虑操作符的优先级。

手工转换
假设:事先未就运算符之间的优先级关系做出过任何约定
1)用括号显式地表示优先级
2)将运算符移到对应的右括号后
3)抹去所有括号,整理。

//原式
( 0 ! + 1 ) * 2 ^ ( 3 ! + 4 ) - ( 5 ! - 67 - ( 8 + 9 ) )
//增添足够多的括号
( ( ( ( 0 ) ! + 1 ) * ( 2 ^ ( ( 3 ) ! + 4 ) ) ) - ( ( ( 5 ) ! - 67 ) - ( 8 + 9 ) ) )
//各运算符后移,使之紧邻于其对应的右括号的右侧:
( ( ( ( 0 ) ! 1 ) + ( 2 ( ( 3 ) ! 4 ) + ) ^ ) * ( ( ( 5 ) ! 67 ) - ( 8 9 ) + ) - ) -
//最后抹去所有括号:
0 ! 1 + 2 3 ! 4 + ^ * 5 ! 67 - 8 9 + - -

操作数之间的相对次序,在转换前后保持不变;而运算符在RPN中所处的位置,恰好就是其对应的操作数均已就绪且该运算可以执行的位置。

队列 ADT 及实现

队列像栈一样,也是受限的序列:
只能在队尾插入(查询):enqueue() + rear()
只能在队头删除(查询):dequeue() + front()
先进先出,后进后出。

队列既然属于序列的特列,故亦可直接基于向量或列表派生。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

数据结构和算法(4):栈与队列 的相关文章

随机推荐

  • 高等数学——驻点,拐点,极值点

    一 定义不同 1 极值点 若f a 是函数f x 的极大值或极小值 则a为函数f x 的极值点 极大值点与极小值点统称为极值点 极值点是函数图像的某段子区间内上极大值或者极小值点的横坐标 极值点出现在函数的驻点 导数为0的点 或不可导点处
  • 活体检测的几种手段分析

    人脸识别是判断你是否是你 而活体检测则为了确定人脸识别的你是不是活得你 基于这样的特性 活体检测可以有效的避免视频 图片的技术BUG 活体检测的手段比较多 目前比较通用的是人脸活体检测 但是实际应用中的还有指纹识别 虹膜识别 静脉识别 通过
  • 生成一个6位数的随机密码,且需要包括字符、数字、特殊符号

    实现思路 第一步 6位数的密码 且需要包括字符 数字 特殊符号这三个元素 将三个元素组成6位时每个元素的排列组合列举出来 第二步 从第一步的排列组合中随机抽取一个排列组合类型 i j k 第三步 从所有的字符 数字 特殊符号中随机抽取i个字
  • Zookeeper到底是干嘛的

    在Zookeeper的官网上有这么一句话 ZooKeeper is a centralized service for maintaining configuration information naming providing distr
  • 数据集笔记:杭州 & 上海 地铁客流数据

    数据集地址 PVCGN data at master liuwj2000 PVCGN github com 1 数据集介绍 从5 15到23 30的地铁乘客流量预测 使用前四个时间间隔 15分钟 x 4 60分钟 的地铁乘客流量 进 出流量
  • Python(1)--Python安装目录介绍

    DLLs Python 自己使用的动态库 Doc 自带的 Python 使用说明文档 include 包含共享目录都是 h的文件 Lib 库文件 放自定义模块和包 pip 安装下载的包会放这Lib site packages 这个路径可以修
  • 云原生微服务应用的平台工程实践

    作者 纳海 01 微服务应用云原生化 微服务是一个广泛使用的应用架构 而如何使得微服务应用云原生化却是近些年一直在演进的课题 国内外云厂商对云原生概念的诠释大同小异 基本都会遵循 CNCF 基金会的定义 云原生技术有利于各组织在公有云 私有
  • 形态学的图像处理

    数字形态学是图像处理与分析领域的重要工具之一 数学形态学可以用来解决抑制噪声 特征提取 边缘检测 图像分割 形状识别 纹理分析 图像恢复与重建 图像压缩等图像处理问题 本文将会对形态学的图像处理进行一些通俗的原理解释和Matlab代码验证
  • 初始C语言——数组的行和列互换

    define CRT SECURE NO WARNINGS 1 防止visual studio2013以上版本scanf报错 vc6 0环境可忽略 include
  • 小案例:页面滚动事件以及导航栏点击

    HTML html实现方法一 导航栏a标签href 要与下列div中id属性对应 点击a标签即可滑动到对应id的div div class navbar 导航栏 ul class rightheader li a class page sc
  • 5G承载网络技术发展趋势

    导读 随着5G建设的日渐加快 5G与云网融合共生互促 推动承载网络技术不断发展演进 云网融合必将成为行业高质量发展的必然趋势 当前云网融合面临着新需求与新挑战 5G承载网络技术在确定性保障 定制化服务和智能管控运维等技术方面也面临着新的发展
  • 5个最流行的可用于移动开发的嵌入式数据库简介

    嵌入式数据库是轻量级的 独立的库 没有服务器组件 无需管理 一个小的代码尺寸 以及有限的资源需求 目前有几种嵌入式数据库 你可以在移动应用程序中使用 让我们来看看这些最流行的数据库 数据库 数据类型存储 License 支持平台 Berke
  • 【2019.11.12】C语言中求最大值和最小值的两种方法

    C语言中求最大值和最小值的两种方法 编写完整的程序 输入三个数 输出其中的最大数 最小数 输入说明 两个整数N1 N2 N3 输出说明 最大数 最小数 输入样例 5 4 9 输出样例 9 4 方法一 include
  • C++学习教程大纲

    以下是C 学习教程的大纲 第一部分 基础知识 C 简介 什么是C C 的历史 C 的特点和优势 开发环境的搭建 安装C 编译器 配置开发环境 第一个C 程序 Hello World程序 程序的结构 编译和运行程序 数据类型和变量 基本数据类
  • jQuery的三种$()

    号是jQuery 类 的一个别称 构造了一个jQuery对象 所以 可以叫做jQuery的构造函数 个人观点 呵呵 1 可以是 expresion 即css选择器 Xpath或html元素 也就是通过上述表达式来匹配目标元素 比如 a 构造
  • 应急响应篇:windows入侵排查

    前言 应急响应 Incident Response Service IRS 是当企业系统遭受病毒传播 网络攻击 黑客入侵等安全事件导致信息业务中断 系统宕机 网络瘫痪 数据丢失 企业声誉受损 并对组织和业务运行产生直接或间接的负面影响时 急
  • 《码上行动:零基础学会Python编程》书籍分享

    Python是一种高级的 面向对象的编程语言 由Guido van Rossum于1991年开发 它具有简洁 易读和可维护的语法 被广泛用于科学计算 Web开发 数据分析 人工智能等领域 以下是Python的一些特点和优势 简洁易读 Pyt
  • 还对Flutter理解不透?看完这些迟早成为大佬~

    Flutter是什么 Flutter简介 Flutter是谷歌的移动UI框架 可以快速在iOS和Android上构建高质量的原生用户界面 一份代码可以同时生成iOS和Android两个高性能 高保真的应用程序 Flutter目标是使开发人员
  • 2023年最火副业:Python爬虫兼职,一周赚7800元,一天只要两小时 !

    现在学习python的人越来越多了 跟大家简单如何利用python搞副业赚钱的 想要利用 Python 赚钱的方式还是比较多的 其中接单和投稿算是两种比较简单的方式了 如果你是业余学python爬虫 可以去淘宝上加了找了几个店铺直接问需要爬
  • 数据结构和算法(4):栈与队列

    栈 ADT 及实现 栈 stack 是存放数据对象的一种特殊容器 其中的数据元素按线性的逻辑次序排列 故也可定义首 末元素 尽管栈结构也支持对象的插入和删除操作 但其操作的范围仅限于栈的某一特定端 也就是说 若约定新的元素只能从某一端插入其