从零实现C++表达式计算库:编译原理实践与性能优化
1. 项目概述为什么我们需要一个自己的表达式计算库在C开发中处理数学表达式是一个高频且棘手的需求。无论是开发一个科学计算器、一个数据可视化工具还是一个游戏中的技能伤害公式引擎你总会遇到需要解析并计算类似(sin(pi/4) 2^3) * max(5, a)这样的字符串。直接使用eval抱歉C标准库没有提供。手动写一个字符串分割和计算那将是一场噩梦尤其是当表达式变得复杂包含函数、变量和括号嵌套时。这就是为什么一个健壮的、自实现的数学表达式计算库会成为你工具箱里的“瑞士军刀”。它不仅仅是把字符串变成数字更是一个将用户输入或配置文件中的公式动态转化为可执行逻辑的桥梁。市面虽有成熟的库如ExprTK、muParser但自己动手实现一遍其价值远超调用一个API。你能彻底掌握从词法分析、语法解析到抽象语法树构建和求值的完整编译原理前端流程这对理解编译器、解释器乃至任何领域特定语言DSL的设计都至关重要。对于面试准备尤其是那些经典的“实现一个计算器”题目和夯实C基础涉及字符串处理、数据结构、递归、面向对象设计、内存管理等这更是一个绝佳的练手项目。接下来我将带你从零开始构建一个支持加减乘除、乘方、括号、常用数学函数如sin, cos, sqrt以及自定义变量的C表达式计算库。我们会遵循清晰的模块化设计并深入每个环节的实现细节与避坑指南。2. 核心架构设计分而治之的编译原理思想一个表达式计算库的核心工作流程可以类比一个微型编译器。它接收源代码表达式字符串经过一系列处理最终输出结果数值。这个过程通常被分解为四个清晰的阶段我们采用经典的“管道”式架构词法分析器将原始的字符串流如3.14 a * 2切割成一个个有意义的“单词”即词法单元。例如3.14数字、运算符、a标识符、*运算符、2数字。语法分析器根据预定义的语法规则如运算符优先级、括号匹配将词法单元序列组织成一棵抽象语法树。这棵树定义了计算的先后顺序。符号表与环境管理表达式中出现的变量如a。它负责在求值时将变量名映射到具体的数值。求值器遍历AST递归或迭代地计算每个节点的值最终得到整个表达式的结果。这种分离关注点的设计好处明显每个模块职责单一易于测试、调试和扩展。例如你想增加一个“取模”运算符%只需在词法分析和语法分析中增加对应的规则即可求值逻辑可以独立修改。2.1 抽象语法树AST节点设计AST是整个过程的核心数据结构。我们使用面向对象的多态来设计节点。首先定义一个基类ExprNode。// 表达式节点的抽象基类 class ExprNode { public: virtual ~ExprNode() default; // 纯虚函数用于求值。参数 env 是变量环境符号表 virtual double evaluate(const std::mapstd::string, double env) const 0; // 可选用于打印调试可视化AST结构 virtual void print(int indent 0) const 0; };然后我们派生出几种具体的节点类型数值节点代表一个常数如3.14。class NumberNode : public ExprNode { double value; public: explicit NumberNode(double val) : value(val) {} double evaluate(const std::mapstd::string, double) const override { return value; // 数值节点求值直接返回存储的值 } void print(int indent) const override { std::cout std::string(indent, ) Number( value )\n; } };变量节点代表一个变量如x。求值时需要从环境env中查找。class VariableNode : public ExprNode { std::string name; public: explicit VariableNode(std::string varName) : name(std::move(varName)) {} double evaluate(const std::mapstd::string, double env) const override { auto it env.find(name); if (it env.end()) { throw std::runtime_error(Undefined variable: name); } return it-second; } void print(int indent) const override { std::cout std::string(indent, ) Variable( name )\n; } };二元运算符节点代表,-,*,/,^等运算。它有两个子节点左操作数和右操作数。class BinaryOpNode : public ExprNode { char op; // 运算符字符 std::unique_ptrExprNode left; std::unique_ptrExprNode right; public: BinaryOpNode(char opChar, std::unique_ptrExprNode l, std::unique_ptrExprNode r) : op(opChar), left(std::move(l)), right(std::move(r)) {} double evaluate(const std::mapstd::string, double env) const override { double lval left-evaluate(env); double rval right-evaluate(env); switch (op) { case : return lval rval; case -: return lval - rval; case *: return lval * rval; case /: if (rval 0) throw std::runtime_error(Division by zero); return lval / rval; case ^: return std::pow(lval, rval); // 乘方 default: throw std::runtime_error(Unknown operator: std::string(1, op)); } } void print(int indent) const override { std::cout std::string(indent, ) BinaryOp( op )\n; left-print(indent 2); right-print(indent 2); } };函数调用节点代表sin(x),sqrt(4)等。它有一个函数名和一个参数子节点我们这里先实现单参数函数。class FunctionCallNode : public ExprNode { std::string funcName; std::unique_ptrExprNode argument; public: FunctionCallNode(std::string name, std::unique_ptrExprNode arg) : funcName(std::move(name)), argument(std::move(arg)) {} double evaluate(const std::mapstd::string, double env) const override { double argVal argument-evaluate(env); if (funcName sin) return std::sin(argVal); else if (funcName cos) return std::cos(argVal); else if (funcName tan) return std::tan(argVal); else if (funcName sqrt) { if (argVal 0) throw std::runtime_error(Square root of negative number); return std::sqrt(argVal); } else if (funcName log) { if (argVal 0) throw std::runtime_error(Log of non-positive number); return std::log(argVal); } else if (funcName exp) return std::exp(argVal); // ... 可以扩展更多函数 else throw std::runtime_error(Unknown function: funcName); } void print(int indent) const override { std::cout std::string(indent, ) FunctionCall( funcName )\n; argument-print(indent 2); } };设计心得使用std::unique_ptrExprNode来管理子节点可以自动处理内存释放避免内存泄漏这是现代C资源管理的核心实践。evaluate方法声明为const因为它不应修改节点状态这符合函数式求值的理念。3. 词法分析器实现将字符串拆解成Token词法分析器也叫扫描器它的任务是把输入字符串转换成一系列Token。每个Token包含类型和值如果是数字或标识符。3.1 定义Token类型enum class TokenType { Number, // 数字如 3.14, 42 Identifier, // 标识符如 x, y, sin, pi Operator, // 运算符如 , -, *, /, ^ LeftParen, // 左括号 ( RightParen, // 右括号 ) Comma, // 逗号 , (用于函数多参数后续扩展) End, // 输入结束 Error // 错误token }; struct Token { TokenType type; std::string value; // 对于Number/Identifier存储其字符串表示对于Operator存储运算符字符 // 可选可以添加行列号信息用于错误定位 };3.2 实现Lexer类Lexer需要逐个字符地读取输入并识别出不同的Token。class Lexer { std::string input; size_t pos; // 当前位置索引 char currentChar() const { return (pos input.size()) ? input[pos] : \0; } void advance() { pos; } void skipWhitespace() { while (std::isspace(static_castunsigned char(currentChar()))) advance(); } public: explicit Lexer(std::string expr) : input(std::move(expr)), pos(0) {} Token getNextToken() { skipWhitespace(); if (currentChar() \0) return {TokenType::End, }; // 处理数字 if (std::isdigit(static_castunsigned char(currentChar())) || currentChar() .) { std::string numberStr; bool hasDot false; while (std::isdigit(static_castunsigned char(currentChar())) || currentChar() .) { if (currentChar() .) { if (hasDot) break; // 遇到第二个小数点停止可以报错 hasDot true; } numberStr currentChar(); advance(); } // 简单验证数字不能以点结尾如 123. 或 . if (numberStr . || numberStr.back() .) { return {TokenType::Error, Invalid number format}; } return {TokenType::Number, numberStr}; } // 处理标识符和函数名 (字母或下划线开头) if (std::isalpha(static_castunsigned char(currentChar())) || currentChar() _) { std::string identStr; while (std::isalnum(static_castunsigned char(currentChar())) || currentChar() _) { identStr currentChar(); advance(); } return {TokenType::Identifier, identStr}; } // 处理运算符和括号 char ch currentChar(); switch (ch) { case : case -: case *: case /: case ^: advance(); return {TokenType::Operator, std::string(1, ch)}; case (: advance(); return {TokenType::LeftParen, (}; case ): advance(); return {TokenType::RightParen, )}; case ,: advance(); return {TokenType::Comma, ,}; default: // 未知字符 std::string errorStr Unexpected character: ; errorStr ch; advance(); return {TokenType::Error, errorStr}; } } };实操要点在skipWhitespace和字符分类函数如isdigit中将char转换为unsigned char再传入是为了避免当char为负值时在某些平台上标准库函数的行为未定义。这是一个容易被忽略但重要的可移植性细节。4. 语法分析器实现构建抽象语法树语法分析是核心中的核心它决定了运算符优先级和结合性。我们将使用递归下降分析法这是手写解析器最直观的方法。其核心思想是为每一种语法规则如表达式、项、因子编写一个函数。我们定义以下语法规则使用巴科斯范式近似表示expression-term( (|-)term)*term-factor( (*|/)factor)*factor-primary(^factor)? // 右结合如 2^3^2 是 2^(3^2)primary-Number|Identifier|(expression)|FunctionCall4.1 实现Parser类Parser持有Lexer实例并消费Token来构建AST。class Parser { Lexer lexer; Token currentToken; void eat(TokenType type) { if (currentToken.type type) { currentToken lexer.getNextToken(); } else { throw std::runtime_error(Syntax error: unexpected token currentToken.value ); } } public: explicit Parser(Lexer l) : lexer(std::move(l)) { currentToken lexer.getNextToken(); // 读取第一个token } // 解析入口 std::unique_ptrExprNode parse() { auto node parseExpression(); if (currentToken.type ! TokenType::End) { throw std::runtime_error(Syntax error: extra characters after expression); } return node; } private: // expression - term ( (|-) term )* std::unique_ptrExprNode parseExpression() { auto node parseTerm(); while (currentToken.type TokenType::Operator (currentToken.value || currentToken.value -)) { char op currentToken.value[0]; eat(TokenType::Operator); auto right parseTerm(); // 注意这里构建左结合的树。对于 a b c会构建成 ((a b) c) node std::make_uniqueBinaryOpNode(op, std::move(node), std::move(right)); } return node; } // term - factor ( (*|/) factor )* std::unique_ptrExprNode parseTerm() { auto node parseFactor(); while (currentToken.type TokenType::Operator (currentToken.value * || currentToken.value /)) { char op currentToken.value[0]; eat(TokenType::Operator); auto right parseFactor(); node std::make_uniqueBinaryOpNode(op, std::move(node), std::move(right)); } return node; } // factor - primary (^ factor)? // 右结合性 std::unique_ptrExprNode parseFactor() { auto node parsePrimary(); if (currentToken.type TokenType::Operator currentToken.value ^) { eat(TokenType::Operator); auto right parseFactor(); // 递归调用parseFactor实现右结合 node std::make_uniqueBinaryOpNode(^, std::move(node), std::move(right)); } return node; } // primary - Number | Identifier | ( expression ) | FunctionCall std::unique_ptrExprNode parsePrimary() { Token token currentToken; if (token.type TokenType::Number) { eat(TokenType::Number); double value std::stod(token.value); return std::make_uniqueNumberNode(value); } else if (token.type TokenType::Identifier) { eat(TokenType::Identifier); std::string name token.value; // 检查后面是不是左括号如果是则是函数调用 if (currentToken.type TokenType::LeftParen) { eat(TokenType::LeftParen); // 解析函数参数目前只支持单参数 auto arg parseExpression(); eat(TokenType::RightParen); return std::make_uniqueFunctionCallNode(name, std::move(arg)); } else { // 否则是普通变量 return std::make_uniqueVariableNode(name); } } else if (token.type TokenType::LeftParen) { eat(TokenType::LeftParen); auto node parseExpression(); eat(TokenType::RightParen); return node; } else { throw std::runtime_error(Syntax error: expected number, identifier, or (); } } };关键解析parseFactor函数中处理乘方^的方式体现了右结合性。当遇到^时它递归调用parseFactor()来解析右边的指数而不是parsePrimary()。这意味着对于表达式2^3^2解析过程是parseFactor(2) - 遇到^ - 递归parseFactor(3) - 遇到^ - 递归parseFactor(2) - 得到2最终构建的树是2^(3^2)符合数学惯例。这是递归下降法处理结合性的一个经典技巧。5. 求值与环境集成让表达式“活”起来有了AST求值就变得直截了当。我们已经在每个ExprNode的子类中实现了evaluate方法。现在需要创建一个顶层的Expression类将解析和求值封装起来并提供变量环境。class Expression { std::unique_ptrExprNode astRoot; std::mapstd::string, double variables; // 符号表存储变量名到值的映射 public: // 从字符串构造表达式 explicit Expression(const std::string exprStr) { Lexer lexer(exprStr); Parser parser(std::move(lexer)); astRoot parser.parse(); } // 设置/修改变量的值 void setVariable(const std::string name, double value) { variables[name] value; } // 批量设置变量 void setVariables(const std::mapstd::string, double vars) { variables.insert(vars.begin(), vars.end()); } // 求值 double evaluate() const { if (!astRoot) { throw std::runtime_error(Expression not parsed successfully); } return astRoot-evaluate(variables); } // 打印AST调试用 void printAST() const { if (astRoot) { astRoot-print(); } else { std::cout (Empty AST)\n; } } };现在我们可以像下面这样使用这个库int main() { try { Expression expr((sin(pi/2) 3^2) * x); expr.setVariable(pi, 3.141592653589793); expr.setVariable(x, 10.0); expr.printAST(); // 可视化AST结构 double result expr.evaluate(); std::cout Result: result std::endl; // 应输出 (1 9) * 10 100 } catch (const std::exception e) { std::cerr Error: e.what() std::endl; } return 0; }6. 性能优化与高级特性探讨一个基础的库已经完成但要投入实用还需要考虑更多。6.1 性能优化避免重复解析与AST编译如果同一个表达式需要被反复求值例如在循环中或用于绘制函数图像每次从字符串开始解析和构建AST是巨大的浪费。我们可以引入“编译”的概念。class CompiledExpression { std::unique_ptrExprNode astRoot; // 可以进一步优化将AST转换为字节码在虚拟机上运行速度更快。 public: CompiledExpression(std::unique_ptrExprNode root) : astRoot(std::move(root)) {} double evaluate(const std::mapstd::string, double env) const { return astRoot-evaluate(env); } }; class ExpressionCompiler { public: static CompiledExpression compile(const std::string exprStr) { Lexer lexer(exprStr); Parser parser(std::move(lexer)); return CompiledExpression(parser.parse()); } };这样用户可以先编译表达式然后传入不同的变量环境快速求值性能提升显著。6.2 支持多参数函数和常量扩展函数调用节点以支持多参数如max(5, 10, x)修改parsePrimary中函数解析部分在(和)之间循环调用parseExpression并用Commatoken分隔将多个参数节点存入std::vector。相应地FunctionCallNode需要存储一个参数节点列表并在evaluate中依次求值。添加常量支持如pi,e可以在VariableNode::evaluate中先检查名称是否在预定义的常量表中是则返回常量值否则再去环境变量中查找。6.3 错误处理与用户友好性目前的错误处理比较简单抛出std::runtime_error。一个工业级的库需要更丰富的错误类型区分词法错误、语法错误、运行时错误除零、未定义变量等。错误定位在Token和Lexer中记录行号、列号在抛出异常时附上位置信息。自定义异常类派生自std::exception携带更多上下文信息。6.4 内存管理与智能指针我们全程使用了std::unique_ptr这是正确的选择它明确了所有权且零开销。在更复杂的场景如可能需要的AST节点缓存、共享子表达式优化可以评估使用std::shared_ptr但绝大多数情况下unique_ptr足矣。7. 常见问题排查与调试技巧实录在实际实现和使用的过程中你几乎一定会遇到下面这些问题。7.1 浮点数精度问题这是所有数学计算库的“阿喀琉斯之踵”。我们的库直接使用Cdouble类型因此继承了所有浮点数精度问题。现象0.1 0.2 ! 0.3或者sin(pi)不等于0而是一个极小的数。应对策略教育用户在文档中明确说明库使用双精度浮点数。比较时使用容差在库内部或用户代码中比较两个浮点数结果时不要用而应使用std::abs(a - b) epsilon例如epsilon 1e-12。谨慎处理临界值例如在sqrt函数中对于负数判断也可以使用容差避免因计算误差导致的误报。7.2 运算符优先级与结合性错误这是语法分析中最容易出错的地方。症状表达式1 2 * 3被计算成9而不是7或者2^3^2结果错误。调试方法打印Token流在Lexer完成后打印出所有识别到的Token确保分词正确。打印AST我们实现的print方法至关重要。对于12*3正确的AST应该先有*节点其子节点是2和3然后节点的左子节点是1右子节点是那个*节点。如果结构不对立刻能发现问题出在parseTerm和parseExpression的优先级处理上。单步调试在递归下降的每个函数入口打印信息观察解析路径。7.3 未定义变量或函数名症状求值时抛出Undefined variable: xxx。处理建议提供默认值可以设计一个“安全求值”模式当变量未定义时返回0或NaN而不是抛出异常。预检查实现一个getVariables()方法遍历AST收集所有出现的标识符让用户提前知道需要提供哪些变量。区分函数和变量在词法分析阶段可以维护一个已知函数名列表如sin, cos, sqrt。当识别到一个标识符时如果它在函数列表中就产生一个Function类型的Token否则产生IdentifierToken。这可以提前发现拼写错误的函数名。7.4 内存泄漏与指针管理如果你使用了原始指针new/delete内存泄漏风险极高。务必使用智能指针我们用了std::unique_ptr。使用valgrind(Linux) 或Dr. Memory(Windows) 等工具进行内存检查确保所有节点都能被正确释放。7.5 处理复杂表达式时的递归深度限制递归下降解析器和递归求值器在解析极深嵌套的表达式如(((((...)))))时可能导致栈溢出。缓解方案将递归算法改为迭代算法如使用显式栈的调度场算法进行语法分析但这会大大增加复杂度。对于求值可以将递归遍历AST改为迭代遍历使用栈或队列。在实践中除非用户故意输入恶意表达式否则正常的数学表达式嵌套深度很难导致栈溢出。可以将其视为一个已知限制在文档中说明。8. 从玩具到实用扩展思路与项目升华至此一个核心可用的表达式计算库已经完成。但如果你想让它更强大、更专业这里有一些进阶方向支持逻辑与比较运算符增加,,,,||等使表达式能返回布尔值可用于条件判断。支持赋值与语句扩展语法支持如x 5 3这样的赋值语句甚至支持多条语句用分号分隔。这会让你的库演变成一个简单的脚本引擎。JIT编译这是性能追求的终极目标。使用LLVM或libgccjit等库将AST直接编译成本地机器码求值速度可提升几个数量级。ExprTK库就以其模板元编程实现的编译时优化而闻名。绑定到其他语言使用Python的C API或pybind11为你的库创建Python绑定让Python用户也能享受高性能的表达式计算。可视化调试工具开发一个图形界面输入表达式后能实时显示Token流、AST图形和求值步骤这对于教学和理解库的工作原理非常有帮助。实现这个库的过程是一次对C核心特性面向对象、智能指针、STL容器和计算机科学基础编译原理、数据结构的深度实践。它麻雀虽小五脏俱全。当你看到自己写的库成功计算出第一个复杂表达式时那种成就感是调用现成API无法比拟的。更重要的是这套词法分析、语法分析、构建AST的模式是处理任何结构化文本配置文件、查询语言、模板引擎的通用利器其价值远超一个计算器本身。

相关新闻

掌握rebel-readline快捷键:提升Clojure REPL效率的10个必备技巧

掌握rebel-readline快捷键:提升Clojure REPL效率的10个必备技巧

掌握rebel-readline快捷键:提升Clojure REPL效率的10个必备技巧 【免费下载链接】rebel-readline Terminal readline library for Clojure dialects 项目地址: https://gitcode.com/gh_mirrors/re/rebel-readline rebel-readline是Clojure方言的终端readline…

2026/10/14 23:23:05 阅读更多 →
Vue CLI4项目配置全解析与优化实践

Vue CLI4项目配置全解析与优化实践

1. Vue CLI4 项目配置核心解析 作为现代前端开发的标配工具,Vue CLI 的配置文件 vue.config.js 承载着项目构建的关键逻辑。这个文件就像是项目的控制中心,从资源路径到编译规则,从开发服务器到生产优化,几乎所有构建相关的配置…

2026/10/1 12:27:17 阅读更多 →
Unity跨场景引用持久化:基于GUID的解决方案与Timeline实战

Unity跨场景引用持久化:基于GUID的解决方案与Timeline实战

1. 项目概述与痛点剖析 在Unity项目开发中,尤其是涉及大型关卡、动态加载或复杂叙事流程的游戏里,跨场景的对象引用问题就像一颗定时炸弹,随时可能引爆你的工作流。想象一下这个场景:你在一个“主菜单”场景里精心编排了一段Timel…

2026/10/10 12:32:53 阅读更多 →

最新新闻

Claude Code 安装配置完全指南:从Node环境到终端AI编程助手

Claude Code 安装配置完全指南:从Node环境到终端AI编程助手

简介:面向开发者的Claude Code安装配置代码包,聚焦智能终端助手在Windows、macOS、Linux三平台下的部署与集成,帮助读者完成Node.js环境准备、全局安装、API密钥配置,并快速接入Visual Studio Code与JetBrains IDE。包体共3个文件…

2026/10/14 23:23:41 阅读更多 →
在线火山图绘制全攻略:从双色到四色的参数与实操

在线火山图绘制全攻略:从双色到四色的参数与实操

1. 先看懂火山图:坐标轴、阈值线、颜色的底层逻辑很多人上来就找按钮,先问“火山图在哪里”。我的建议是,先用两分钟理解这张图的坐标和阈值在说什么。你知道了每个参数背后的含义,在线工具里的选项就不会让你犯迷糊,出…

2026/10/14 23:23:41 阅读更多 →
大模型训练数据管线全拆解:从清洗去重到Tokenize分片

大模型训练数据管线全拆解:从清洗去重到Tokenize分片

1. 从数据视角切入大模型训练:为什么值得深挖做模型训练的人都有一个共识:架构决定上限,数据决定下限。很多人拿到一个开源大模型,第一反应是去看它的网络结构——注意力怎么设计的、位置编码用的什么方案、归一化放在哪里。但真正…

2026/10/14 23:23:41 阅读更多 →
手写Tool Use的三种实战方案:从字符串解析到AST沙箱

手写Tool Use的三种实战方案:从字符串解析到AST沙箱

1. “手写 Tool Use”到底在考什么:面试官没说破的三层潜台词“面试官让我手写一个 Tool Use,我用了 3 种方案”——这个标题乍看像段程序员日常吐槽,但背后藏着当前大模型应用开发岗位最核心的能力筛选逻辑。我带过十几轮校招和社招技术面&a…

2026/10/14 23:23:41 阅读更多 →
智能体五类伤害与三方责任:风险控制与实操指南

智能体五类伤害与三方责任:风险控制与实操指南

1. 从“智能体闯祸”说起:为什么五类伤害与三方责任值得每个从业者吃透智能体这两年被聊得很多,从自动订票、自动写代码,到在业务系统里替人做决策、调资源、发指令,能力边界一路外扩。但真正在一线做过落地的人都知道&#xff0c…

2026/10/14 23:23:41 阅读更多 →
Delphi中用TPaintBox生成显示二维码,并保存为PNG回读Image的完整实践

Delphi中用TPaintBox生成显示二维码,并保存为PNG回读Image的完整实践

1. 从画布到文件:为什么用PaintBox而不是直接放Image最近在做桌面端工具时,遇到一个挺典型的交互需求:界面某处要根据数据动态生成二维码,当场显示给用户看,同时提供一个“保存图片”的按钮,把二维码落盘。…

2026/10/14 23:22:37 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →