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

这篇具有很好参考价值的文章主要介绍了数据结构和算法(4):栈与队列。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

栈 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()
先进先出,后进后出。

队列既然属于序列的特列,故亦可直接基于向量或列表派生。文章来源地址https://www.toymoban.com/news/detail-705831.html

到了这里,关于数据结构和算法(4):栈与队列的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【数据结构】栈与队列

    栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。 进行数据插入和删除操作的一端称为栈顶,另一端称为栈底 。栈中的数据元素遵守后进先出 LIFO (Last In First Out) 的原则。 压栈:栈的插入操作叫做进栈/压栈/入栈, 入数据在栈顶 。 出栈:栈的删除操

    2024年02月13日
    浏览(28)
  • 【数据结构】 栈与队列的相互实现

    队列与栈的操作算法是笔试面试中较为常见的题目。 本文将着重介绍平时面试中常见的关于队列与栈的应用题目,马上要进行秋招了。希望对你们有帮助 _😀 请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。 实现

    2024年02月10日
    浏览(33)
  • 数据结构之栈与队列详解

    栈和队列是一种特殊的线性结构,他与之前学的线性结构不同,栈和队列是拥有一种特殊规则的线性结构,虽然它是用数组或者链表实现,但是只有符合这种规则才能被称作栈或者队列 栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和

    2024年01月16日
    浏览(33)
  • 数据结构例题代码及其讲解-栈与队列

    栈Stack 后进先出 ​ 栈的结构体定义及基本操作。 初始化 ​ 这里初始化时是将栈顶指针指向-1,有些则是指向0,因此后续入栈出栈的代码略微有点区别 判断栈是否为空 压栈操作 由于初始时栈顶指针指向-1,因此需要先变化栈顶指针,然后入栈操作; 且当MaxSize为50时候,数

    2024年02月10日
    浏览(30)
  • 【数据结构】栈与队列经典oj题

    🚀write in front🚀 📜所属专栏:初阶数据结构 🛰️博客主页:睿睿的博客主页 🛰️代码仓库:🎉VS2022_C语言仓库 🎡您的点赞、关注、收藏、评论,是对我最大的激励和支持!!! 关注我,关注我,关注我 , 你们将会看到更多的优质内容!!   栈两种线性表示都能实现

    2024年02月03日
    浏览(26)
  • 【数据结构】栈与队列经典选择题

    🚀write in front🚀 📜所属专栏: 🛰️博客主页:睿睿的博客主页 🛰️代码仓库:🎉VS2022_C语言仓库 🎡您的点赞、关注、收藏、评论,是对我最大的激励和支持!!! 关注我,关注我,关注我 , 你们将会看到更多的优质内容!!   在前面的学习中外面学习了栈与队列。

    2023年04月23日
    浏览(35)
  • (详解)数据结构-----------栈与队列 c语言实现

    本章将会详细讲解以下知识点: 目录 一:栈         1:栈的定义,栈的特点         2:用什么结构来实现栈与原因的分析?         3:  (超详解)栈的常用接口并且附上测试用例 二:队列         1:队列的定义,队列的特点         2:用什么结构来实现队列与原因的分析

    2024年02月11日
    浏览(33)
  • 数据结构基础内容-----第四章 栈与队列

    栈(Stack)是计算机科学中的一种抽象数据类型,它是一个只能在一端进行插入和删除操作的线性数据结构。栈按照后进先出(LIFO)的原则存储数据,即最后放入的元素最先被取出。类比物理世界中的堆叠物品,每次加入的物品都被放在上面,取出时也只能从上面取出,最后

    2024年02月07日
    浏览(32)
  • 【数据结构经典题目】—两个队列实现栈与两个栈实现队列

    ​                                           食用指南:本文在有C基础的情况下食用更佳                                            🔥 这就不得不推荐此专栏了: C语言                                         🍀

    2024年02月13日
    浏览(34)
  • Java------数据结构之栈与队列(简单讲解)

    本篇碎碎念 :时隔n个月,继续写博客,假期落下的进度,在开学后努力追赶, 假期不努力,开学徒伤悲啊,此时此刻真想对自己说一句,活该啊~~~~ 欠下的链表练习题讲解会在下次更新~~~~ 今日份励志文案:  万物皆有裂痕,那是光照进来的地方 栈:一种特殊的线性表,其只允

    2024年04月14日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包