清晨的阳光透过实验室的窗棂,洒在那些熟悉的化学试剂瓶上。作为一名曾经的化学爱好者,我依然记得第一次看到水分子式H₂O时的震撼------两个氢原子与一个氧原子的精妙组合,竟构成了生命之源。如今,作为算法爱好者,当我看到LeetCode上的"原子的数量"这道题时,那段记忆再次浮现。这道题看似简单,却蕴含着深刻的计算机科学原理,它将化学的精确性与计算机算法的优雅完美结合,让我们一同探索这背后的智慧。
在数字化时代,化学信息处理已成为科研与工业的重要组成部分。从药物设计到材料科学,计算机需要理解和处理复杂的化学表达式。这道"原子的数量"题目,正是这种现实需求在算法世界的缩影,它要求我们设计一个能够解析任意复杂化学式并统计原子数量的程序。
问题背景与现实意义
化学式是化学世界的语言,它以简洁的符号系统描述物质的组成。在计算机化学、药物研发和材料科学领域,自动化处理化学式已成为基础需求。想象一下,当化学家输入一个复杂的分子式时,计算机需要迅速解析并计算出各元素的比例,这对于确定化学反应计量、计算分子量乃至预测化合物性质都至关重要。
"原子的数量"这个问题要求我们解析可能包含嵌套括号的化学式,统计每种原子的数量,并按字典序输出结果。这看似是简单的字符串处理,实则涉及编译原理、栈数据结构、递归算法等计算机科学的核心概念。
题目深度解析
问题形式化定义
给定字符串化学式formula,我们需要识别其中的原子及其数量。原子的命名规则遵循化学惯例:以大写字母开头,后可跟零个或多个小写字母。数量表示则采用简写形式------仅当数量大于1时显示数字。
化学式的构成规则包含:
-
基本原子单元:如"H"、"He"
-
原子与数量的组合:如"H2"、"O2"
-
括号表达式:如"(OH)"、"(SO3)2"
-
复杂组合:如"K4(ON(SO3)2)2"
示例分析
以示例3 "K4(ON(SO3)2)2" 为例,这个化学式的解析过程极为精妙:
-
最外层是K4和(ON(SO3)2)2
-
括号内的ON(SO3)2又包含嵌套结构
-
最终原子计数为:K:4, N:2, O:14, S:4
这个例子展示了多重嵌套括号的处理难点,也揭示了算法需要具备的处理复杂结构的能力。
算法设计思路
核心挑战分析
该问题的核心挑战在于处理嵌套结构和原子数量的累积计算。化学式中的括号类似于程序语言中的表达式,需要按照特定规则解析。主要难点包括:
-
嵌套括号处理:括号可以多层嵌套,每层可能有乘数
-
原子名称识别:需要准确区分单字母与多字母原子名称
-
数量累积计算:括号外的乘数会影响括号内所有原子的数量
-
原子排序输出:结果需要按字典序排列
栈数据结构的选择
面对嵌套结构,栈(Stack)自然成为首选数据结构。栈的LIFO(后进先出)特性完美匹配括号的嵌套关系,使我们能够跟踪当前的处理上下文。
算法基本思路:
-
使用栈来维护不同层次的原子计数
-
遇到左括号时,推入新的计数上下文
-
遇到右括号时,弹出当前上下文,乘以外侧数字后合并到上一层
-
遍历完成后,栈底元素包含最终原子计数
原子解析状态机
解析过程可以视为一个状态机,在不同状态间转换:
-
初始状态:期待原子开始或括号
-
原子名称收集:收集原子名称字符
-
数字收集:收集表示数量的数字
-
括号处理:处理左右括号及后续乘数
时间复杂度分析
设输入字符串长度为n,算法时间复杂度为O(n),原因在于:
-
单次遍历:算法只需要从左到右遍历字符串一次
-
常数时间操作:每个字符的处理均为常数时间操作
-
字符分类判断:O(1)
-
栈操作:平均O(1)
-
哈希表更新:平均O(1)
-
即使考虑最坏情况,算法仍然保持线性时间复杂度,这得益于栈操作和哈希表操作的平均常数时间复杂度特性。
空间复杂度分析
空间复杂度主要来自两个方面:
-
栈空间:最坏情况下,栈深度与嵌套层数成正比。对于长度为n的字符串,最大嵌套深度为O(n),但实际化学式中嵌套深度通常很小
-
哈希表空间:存储原子计数,空间需求与不同原子种类数成正比,最多为O(n)
总体空间复杂度为O(n),在合理范围内。考虑到化学式的实际特性,原子种类数量有限,实际空间需求通常优于最坏情况。
进阶难点与精妙之处
嵌套乘数处理
最精妙的难点在于处理嵌套括号的乘数。以"K4(ON(SO3)2)2"为例:
-
内层(SO3)的乘数为2
-
中层(ON(SO3)2)的乘数也为2
-
因此S原子的最终乘数为2×2=4
这要求算法能够正确累积各层的乘数效应,需要在弹出栈时进行恰当的乘法运算。
原子名称的确定性解析
原子名称解析看似简单,实则暗藏玄机。考虑"Mg(OH)2":
-
遇到'M'时,需要判断是单原子"M"还是"Mg"
-
这要求前瞻一个字符,但不超过小写字母范围
-
解析器必须在看到大写字母时结束当前原子名称
这种前瞻但不消耗的解析策略,体现了编译器设计中词法分析的精髓。
数量缺省值处理
化学式中数量1通常省略,这增加了算法的复杂性。解析器需要区分:
-
显式数字:如"H2"中的2
-
隐式单数:如"O"中的1
这种上下文相关的缺省值处理,要求算法维护完整的状态信息。
算法变体与优化思路
递归下降解析
除了基于栈的迭代方法,还可以采用递归下降解析。这种方法更符合化学式的语法结构,代码可读性更强:
化学式 → 项序列
项 → 原子 [数字] | '(' 化学式 ')' [数字]
原子 → 大写字母 [小写字母]
递归方法虽然栈深度与嵌套深度相关,但更直观地反映了化学式的层次结构。
正则表达式辅助解析
对于原子名称识别,可以使用正则表达式进行匹配,如[A-Z][a-z]*。这种方法简化了解析逻辑,但可能影响性能,需在可读性与效率间权衡。
现实世界应用延伸
该算法的核心思想------处理嵌套结构的表达式------在计算机科学中广泛应用:
-
数学表达式求值:处理包含括号的算术表达式
-
JSON/XML解析:处理标记语言的嵌套结构
-
编程语言解释器:解析代码中的嵌套表达式
-
配置文件处理:解析层次化配置信息
掌握这种栈式处理方法,为理解更复杂的解析算法奠定了基础。
总结
"原子的数量"这个问题完美展示了如何用算法解决现实世界的结构化数据解析问题。它综合运用了栈数据结构、状态机设计和哈希表技术,体现了计算机科学的精髓------用简洁的抽象处理复杂的现实。
通过这道题,我们不仅学会了如何解析化学式,更重要的是掌握了处理嵌套结构的通用方法。这种从具体问题中抽象出通用解决方案的能力,正是算法思维的核心价值。
化学式解析就像是一座桥梁,连接了化学的精确与计算机科学的高效。当我们深入理解这类问题时,我们不仅在解决特定的编程挑战,更在培养一种能够应对各种复杂系统分析的能力。这种能力,无论是在学术研究还是工程实践中,都具有不可估量的价值。
在探索"原子的数量"这道题目的过程中,我们仿佛进行了一场跨学科的思维旅行。从化学实验室到计算机算法,从简单的原子计数到复杂的栈结构应用,这种知识的融合与碰撞正是技术进步的源泉。
算法的魅力在于,它能够将复杂的世界简化为可计算的模型。当我们下次看到化学式时,或许会多一份对背后计算复杂性的欣赏。希望这次的探索不仅帮助你理解这道具体的题目,更激发了你对算法之美的更深层次感悟。
在技术的道路上,每一个看似简单的问题背后,都可能隐藏着值得深入探索的智慧。保持好奇心,继续探索吧!
cpp
#include <stdio.h> // 标准输入输出库,用于printf等函数
#include <stdlib.h> // 标准库,用于malloc、free等内存管理函数
#include <string.h> // 字符串处理库,用于strcmp、strcpy等函数
#include <ctype.h> // 字符类型库,用于isupper、islower、isdigit等函数
// 定义原子结构体,用于存储原子名称和数量
typedef struct {
char name[3]; // 原子名称,最多2个字符(如Mg)加1个结束符
int count; // 原子数量
} Atom;
// 定义栈结构体,用于处理嵌套括号
typedef struct {
Atom* atoms; // 指向原子数组的指针
int size; // 当前栈中原子数量
int capacity; // 栈的容量
} Stack;
// 函数声明开始
Stack* createStack(int capacity); // 创建栈
void pushAtom(Stack* stack, const char* name, int count); // 向栈中添加原子
void multiplyStack(Stack* stack, int multiplier); // 将栈中所有原子数量乘以倍数
void mergeStack(Stack* dest, Stack* src); // 合并两个栈
void freeStack(Stack* stack); // 释放栈内存
int compareAtoms(const void* a, const void* b); // 比较函数,用于排序
char* countOfAtoms(char* formula); // 主函数,解析化学式
// 创建栈的函数
Stack* createStack(int capacity) {
Stack* stack = (Stack*)malloc(sizeof(Stack)); // 分配栈结构体内存
stack->atoms = (Atom*)malloc(sizeof(Atom) * capacity); // 分配原子数组内存
stack->size = 0; // 初始化栈大小为0
stack->capacity = capacity; // 设置栈容量
return stack; // 返回创建的栈指针
}
// 向栈中添加原子的函数
void pushAtom(Stack* stack, const char* name, int count) {
// 遍历栈中现有原子,检查是否已存在同名原子
for (int i = 0; i < stack->size; i++) {
// 如果找到同名原子
if (strcmp(stack->atoms[i].name, name) == 0) {
stack->atoms[i].count += count; // 增加该原子的数量
return; // 直接返回,不添加新原子
}
}
// 如果栈已满,需要扩容
if (stack->size >= stack->capacity) {
stack->capacity *= 2; // 容量翻倍
stack->atoms = (Atom*)realloc(stack->atoms, sizeof(Atom) * stack->capacity); // 重新分配内存
}
// 添加新原子到栈中
strcpy(stack->atoms[stack->size].name, name); // 复制原子名称
stack->atoms[stack->size].count = count; // 设置原子数量
stack->size++; // 栈大小加1
}
// 将栈中所有原子数量乘以倍数的函数
void multiplyStack(Stack* stack, int multiplier) {
// 遍历栈中所有原子
for (int i = 0; i < stack->size; i++) {
stack->atoms[i].count *= multiplier; // 将每个原子的数量乘以倍数
}
}
// 合并两个栈的函数(将src栈合并到dest栈)
void mergeStack(Stack* dest, Stack* src) {
// 遍历源栈中的所有原子
for (int i = 0; i < src->size; i++) {
// 将源栈中的每个原子添加到目标栈
pushAtom(dest, src->atoms[i].name, src->atoms[i].count);
}
}
// 释放栈内存的函数
void freeStack(Stack* stack) {
free(stack->atoms); // 释放原子数组内存
free(stack); // 释放栈结构体内存
}
// 比较函数,用于原子排序(按名称字典序)
int compareAtoms(const void* a, const void* b) {
const Atom* atomA = (const Atom*)a; // 将void指针转换为Atom指针
const Atom* atomB = (const Atom*)b; // 将void指针转换为Atom指针
return strcmp(atomA->name, atomB->name); // 比较原子名称的字典序
}
// 主函数:解析化学式并统计原子数量
char* countOfAtoms(char* formula) {
int len = strlen(formula); // 获取化学式字符串长度
Stack** stacks = (Stack**)malloc(sizeof(Stack*) * (len + 1)); // 创建栈指针数组
int stackTop = -1; // 栈顶指针,初始化为-1
// 创建初始栈并压入栈数组
stacks[++stackTop] = createStack(100); // 创建容量为100的初始栈
int i = 0; // 字符串遍历索引
// 开始遍历化学式字符串
while (i < len) {
// 如果遇到大写字母,表示原子开始
if (isupper(formula[i])) {
char atomName[3] = {0}; // 原子名称缓冲区,初始化为0
atomName[0] = formula[i]; // 添加第一个大写字母
i++; // 移动到下一个字符
// 检查是否有小写字母(多字母原子名)
if (i < len && islower(formula[i])) {
atomName[1] = formula[i]; // 添加小写字母
i++; // 移动到下一个字符
}
int num = 0; // 原子数量,初始化为0
// 检查后面是否有数字
if (i < len && isdigit(formula[i])) {
// 解析数字
while (i < len && isdigit(formula[i])) {
num = num * 10 + (formula[i] - '0'); // 计算数字值
i++; // 移动到下一个字符
}
} else {
num = 1; // 没有数字,默认数量为1
}
// 将原子添加到当前栈顶的栈中
pushAtom(stacks[stackTop], atomName, num);
}
// 如果遇到左括号,创建新栈
else if (formula[i] == '(') {
stacks[++stackTop] = createStack(100); // 创建新栈并压入栈数组
i++; // 移动到下一个字符
}
// 如果遇到右括号,处理栈合并
else if (formula[i] == ')') {
i++; // 移动到下一个字符
int num = 0; // 括号后的乘数,初始化为0
// 检查右括号后是否有数字
if (i < len && isdigit(formula[i])) {
// 解析数字
while (i < len && isdigit(formula[i])) {
num = num * 10 + (formula[i] - '0'); // 计算数字值
i++; // 移动到下一个字符
}
} else {
num = 1; // 没有数字,默认乘数为1
}
// 获取当前栈(括号内的栈)
Stack* current = stacks[stackTop--]; // 弹出栈顶
// 将当前栈中所有原子数量乘以倍数
multiplyStack(current, num);
// 将当前栈合并到前一个栈中
mergeStack(stacks[stackTop], current);
// 释放当前栈内存
freeStack(current);
}
}
// 获取最终结果栈
Stack* resultStack = stacks[stackTop]; // 获取最终栈
// 对原子按名称排序
qsort(resultStack->atoms, resultStack->size, sizeof(Atom), compareAtoms);
// 计算结果字符串所需长度
int totalLen = 0; // 总长度计数器
for (int j = 0; j < resultStack->size; j++) {
totalLen += strlen(resultStack->atoms[j].name); // 添加原子名称长度
if (resultStack->atoms[j].count > 1) {
// 计算数字的位数
int num = resultStack->atoms[j].count; // 获取原子数量
while (num > 0) {
totalLen++; // 每位数占一个字符
num /= 10; // 去掉最后一位
}
}
}
// 分配结果字符串内存(加1用于结束符)
char* result = (char*)malloc(totalLen + 1); // 分配结果字符串内存
int pos = 0; // 结果字符串位置指针
// 构建结果字符串
for (int j = 0; j < resultStack->size; j++) {
// 复制原子名称
strcpy(result + pos, resultStack->atoms[j].name); // 复制原子名到结果
pos += strlen(resultStack->atoms[j].name); // 移动位置指针
// 如果数量大于1,添加数字
if (resultStack->atoms[j].count > 1) {
// 将数字转换为字符串
int num = resultStack->atoms[j].count; // 获取原子数量
char numStr[10] = {0}; // 数字字符串缓冲区
sprintf(numStr, "%d", num); // 将数字转换为字符串
strcpy(result + pos, numStr); // 复制数字到结果
pos += strlen(numStr); // 移动位置指针
}
}
result[pos] = '\0'; // 添加字符串结束符
// 释放内存
free(stacks); // 释放栈指针数组
freeStack(resultStack); // 释放结果栈
return result; // 返回结果字符串
}
// 主函数,用于测试程序
int main() {
// 测试用例数据
char* testCases[] = {
"H2O", // 测试用例1:水分子
"Mg(OH)2", // 测试用例2:氢氧化镁
"K4(ON(SO3)2)2" // 测试用例3:复杂化学式
};
int numTests = sizeof(testCases) / sizeof(testCases[0]); // 计算测试用例数量
printf("化学式原子数量统计程序\n"); // 打印程序标题
printf("====================\n"); // 打印分隔线
// 遍历所有测试用例
for (int i = 0; i < numTests; i++) {
char* formula = testCases[i]; // 获取当前测试用例
char* result = countOfAtoms(formula); // 调用主函数计算结果
printf("测试用例 %d:\n", i + 1); // 打印测试用例编号
printf("输入: %s\n", formula); // 打印输入化学式
printf("输出: %s\n", result); // 打印输出结果
printf("\n"); // 打印空行
free(result); // 释放结果字符串内存
}
// 额外测试用例
printf("额外测试用例:\n"); // 打印额外测试标题
char* extraTest = "Fe3(SO4)2"; // 额外测试用例:硫酸铁
char* extraResult = countOfAtoms(extraTest); // 计算结果
printf("输入: %s\n", extraTest); // 打印输入
printf("输出: %s\n", extraResult); // 打印输出
free(extraResult); // 释放内存
return 0; // 程序正常结束
}