化学式的分子量计算——字符转数字

【题目描述】

给出一种物质的分子式(不带括号),求分子量。本题中的分子式只包含4种原子,分别为C, H, O, N,原子量分别为12.01, 1.008, 16.00, 14.01(单位:g/mol)。例如,C6H5OH的分子量为6*12.01 + 6*1.008 + 1*16.00=94.108g/mol。

输入第一行表示有T个分子式,后续是T行分子式。字符串的长度为1~79,元素后面的数字范围为2~99。

【样例输入】

4

C

C6H5OH

NH2CH2COOH

C12H22O11

【样例输出】

12.010

94.108

75.070

342.296

【题目来源】

刘汝佳《算法竞赛入门经典  第2版》习题3-2 分子量(Molar Mass, ACM/ICPC Seoul 2007, UVa1586)

【解析】

本题本质上是一个字符计数问题,只不过每种字符的数量是由其后的数字给定的(数量为1时省略)。

一、老金的算法:用switch语句,每遍历1个字符计算1

因为字符串只有4个字母,其他都是数字,因此老金考虑可以用swich语句,只需要分5种情况分别处理即可。

思路如下:

①设置两个变量:每个原子的分子量weight,每个原子的原子个数n。

②每遍历一个字符,计算一次分子量:wight*n。

代码如下:

#include<stdio.h>
#include<string.h>
char s[85];
int main(){
    int T;
    scanf("%d", &T);
    while(T--){
        scanf("%s", s);
        int len=strlen(s), n=1;
        double weight, sum=0;
        for(int i=0; i<len; i++){
            switch(s[i]){
                case 'C':
                    weight=12.01;
                    break;
                case 'H':
                    weight=1.008;
                    break;
                case 'O':
                    weight=16.00;
                    break;
                case 'N':
                    weight=14.01;
                    break;
                //字符为数字的情况
                default:
                    n=s[i]-49;
                    //通过ASCII值判断下个字符是否为数字
                    if(s[i+1]>=48 && s[i+1]<=57){
                        n=10*(s[i]-48)+(s[i+1]-48)-1;
                        i++;
                    }
            }
            sum += weight*n;
            n=1;
        }
        printf("%.3f\n", sum);
    }
    return 0;
}

代码说明:

1.字母和数字的区分。根据题意,字符串只有4个字母,其他都是数字。这样就可以通过case来区分4个字母,最后用default处理数字。这样做的好处是不用另外写代码去判断每个字符是字母还是数字了。当然,如果不限制只有4个字母,就要写上N个case,显然再用此语句就有点不合适了。

2.算法的3种情形

①字母后没数字。方法是设n的默认值为1,这样如果字母后面没有数字,每次计算的结果自然就是正确的分子量。

②字母后有1位数字。因为前面在遍历字母时已经计算了一次分子量,当遍历的数字是1位数字时,需要将个数减1。

③字母后有2位数字。比如C12,可以先将字符转化为对应的数字(字符的ASCII码值-48),然后用1*10+2算出这个两位数字的大小,最后再将1。

那怎么判断数字是两位数呢?也很简单,就是一旦遍历到数字时,就再判断下一个字符是否也是数字。

一旦判断出是两位数,那么遍历到第2位数字时再计算分子量就会出错了,所以要跳过第二位数字。方法很简单,就是加一行i++即可。

3.多位数字情况处理:字符转数

如果不限制数字的位数呢?实质上这是一个多位数字字符转数的问题:

(1)位值法

此算法需要先用一个循环判断数字是几位数(本题转化为找出最后一位数字的下标),再算出这个数字的大小。

只要将default下的代码替换成如下代码即可:

int j, mod;
j=i+1;
mod=1;
n=0;
//求出最后一位数字在数组中的下标
while(s[j]>=48 && s[j]<=57) j++; //原while(s[++j]>=48 && s[++j]<=57);
//计算数字n的大小
for(int k=j-1; k>=i; k--){
    n += (s[k]-48)*mod;
    mod *= 10;
}
n -= 1;
i = j-1; //更新i的值为第后1位数字的下标

但是如果直接替换,编译时报错:

error: a label can only be part of a statement and a declaration is not a statement

这说明在case和default标签下,只能存在语句,不能有变量声明。因此,第一行的变量声明需要放在swith语句之前。

最好是将代码写成函数:int sntoi(char s , int *i),表示返回从字符串s的第i位开始找到的第一个数字。函数代码如下:

int sntoi(char* s, int* i){
    int j=*i, mod=1, n=0;
    //求出最后一位数字在数组中的下标
    while(s[j]>=48 && s[j]<=57) j++;
    //计算数字n的大小
    for(int k=j-1; k>=*i; k--){
        n += (s[k]-48)*mod;
        mod *= 10;
    }
    n -= 1;
    *i = j-1; //更新i的值为第后1位数字的下标
    return n;
}

如此一来default下只需要一行代码:

n=sntoi(s, &i);

(2)连乘加法

位值法需要先算出这个数是几位数,因而需要遍历两次。如果用连乘加法,只需要遍历一次,因此用这种方法转换效率更高,应优先使用。代码如下:

int sntoi(char* s, int* i){
    int j=*i, n=0;
    while(s[j]>=48 && s[j]<=57) {
        n = n*10 + s[j] - 48;
        j++;
    }
    n -= 1;
    *i = j-1; //更新i的值为第后1位数字的下标
    return n;
}

二、配套书算法:从遇到的第2个字母开始,每遇到一个新字母结算上一个字母的分子量。

分子式中每个原子的分子量=原子量×原子个数。

显然,问题的关键在于确定原子个数,这个值什么时候能确定呢?就是数字结束时,或者说是遇到下一个字母时。但是因为当只有一个原子时字母后面没有数字,所以只能是遇到下一个字母时结算上一个字母的分子量。

配套书即采用这种算法,代码如下:

#include<stdio.h>
#include<string.h>
#include<ctype.h>
#include<assert.h>
#define _for(i, start, end) for (int i = start; i < end; i++)
int main(){
    int T, cnt, sz;
    double W[256], ans;
    char buf[256], c, s;
    W['C'] =  12.01, W['H'] = 1.008, W['O'] = 16.0, W['N'] = 14.01;
    scanf("%d\n", &T);
    while(T--){
        scanf("%s", buf);
        ans = 0;
        s = 0; cnt = -1; sz = strlen(buf);
        _for(i, 0, sz){
            char c = buf[i];
            if(isupper(c)){
                if(i) {
                    if(cnt == -1) cnt = 1;
                    ans += W[s] * cnt;
                }
                s = c;
                cnt = -1;
            } else {
                assert(isdigit(c));
                if(cnt == -1) cnt = 0;
                cnt = cnt*10 + c - '0';
            }
        }
        if(cnt == -1) cnt = 1;
        ans += W[s] * cnt;
        printf("%.3lf\n", ans);
    }
    return 0;
}

代码说明:

1.一个变量代表两种含义。代码中的变量n有两种意义:

①标志变量。当遍历到字母时,n=-1。这本质上起得是标志变量的作用。这个标志变量的作用有两个:

a. 结算时,如果n=-1,说明其前一位是个字母,据此将原子个数置为1。

b. 计数时,如果n=-1,说明其前一位是个字母,此位数字是第一个数字,据此将n的初始值置为0,以便后续使用“连乘加法”计数。

②原子个数。当遇到数字开始计数时,n又变为计数的数字。

虽然用一个变量实现了两种功能,很是牛皮Plus。但这种写法降低了代码可读性,老金认为得不偿失。

2.语句宏替换。代码中另有一处高大上的用法,就是使用宏将for语句进行了简洁替换:

#define _for(i, start, end) for (int i = start; i < end; i++)

这样如果代码中有多个for循环,这种简洁的写法就能减少代码量。

3.查找表的应用。代码中再次用数组W[256]实现了查找表,这样可以通过将字母设为数组的下标,元素值设为原子量的值,从而快速获取字母对应的原子量。

4.字符类型判断函数。函数isupper()判断字符是否为大写字母,isdigit()判断字符是否为数字,它们都定义在<ctype.h>头文件中。字符类型判断函数的用法详见老金之前的文章: 字符类型判断库函数合集-CSDN博客

5.assert宏。assert并不是函数,而是一个宏,定义在 <assert.h> 头文件中。它用于在调试期间捕捉不应该发生的错误情况,比如空指针、越界访问等。

如果指定的条件不满足(即条件为假),assert 会打印一条错误消息并终止程序执行。

三、配套书代码优化:取消标志变量

前面说了,配套书的代码存在一个变量两种用途的问题。如果想增加代码的可读性,另设一个标志变量是一个可行方法。老金这里提出一个不用标志变量的方法。

优化思路:

计数无非分两种情况:无数字、有数字。无数字时代表只有一个原子,因此可以设原子数n的默认值为1,当有数字时,计算数字并更新n。优化后的代码如下:

#include<stdio.h>
#include<string.h>
#include<ctype.h>
#include<assert.h>
#define _for(i, start, end) for (int i = start; i < end; i++)
int main(){
    int T, n, len;
    double W[256], ans;
    char buf[256], c, s; //c是当前字母,s是上一个字母
    W['C'] =  12.01, W['H'] = 1.008, W['O'] = 16.0, W['N'] = 14.01;
    scanf("%d\n", &T);
    while(T--){
        scanf("%s", buf);
        ans = 0;
        s = 0; n = 1; len = strlen(buf);
        _for(i, 0, len){
            char c = buf[i];
            if(isupper(c)){
                //如果不是第一个字母,结算上一个字母的分子量
                if(i) {
                    ans += W[s] * n;
                    n = 1; //结算完成,将数字修改为默认值1
                }

                s = c; //将当前字母赋给s,以备结算
            } else {
                //遇到数字,修改n的值
                assert(isdigit(c));
                if(isupper(buf[i-1])) n = 0;//首次遇到数字,n=0
                n = n*10 + c - '0';
            }
        }
        //最后一个字母单独结算
        ans += W[s] * n;
        printf("%.3lf\n", ans);
    }
    return 0;
}

其实完全说取消了标志变量有些牵强,因为下面这行代码本质上还是标志变量的用法,只不过用数组的形式代替了而已。

if(isupper(buf[i-1])) n = 0;//首次遇到数字,n=0

优化后的代码不但没了标志变量,还减少了两条if语句,而且代码也更容易理解了。

四、我家娃娃的代码:很难懂

最后附上我家娃写的C++代码,老金看了很久,还是没太弄明白,不过这个代码运行结果是正确的。

#include <iostream>
#include <cstring>
using namespace std;
double a[95];
int f(int x){
    int g=1;
    for(int i=1;i<=x;i++){
        g*=10;
    }
    return g;
}
int main (){
    a[67]=12.01;
    a[72]=1.008;
    a[79]=16.00;
    a[78]=14.01;
    int T;
    scanf("%d", &T);
    while(T--){
        int cnt=0,i2,cnt2=0;
        double sum=0;
        string n;
        cin>>n;
        for(int i=0;i<n.size();i++){
            if(n[i]>='A'&&n[i]<='Z'){
                if(i==n.size()-1){
                    sum+=a[n[i]];
                    break;
                }
                i++;
                if(n[i]>='0'&&n[i]<='9'){
                    while(n[i]>='0'&&n[i]<='9'){
                        cnt++;
                        i++;
                    }
                    i2=i-cnt;
                    i-=cnt;
                    while(n[i2]>='0'&&n[i2]<='9'){
                        cnt2+=(n[i2]-48)*f(cnt-(i2-i+1));
                        i2++;
                    }
                    sum+=cnt2*(a[n[i-1]+0]);
                    cnt=0;
                    cnt2=0;
                }
                else{
                    sum+=a[n[i-1]];
                    i--;
                }
            }
        }
        cout<<sum<<endl;
    }
}

不得不吐槽一下C++的cout,代码中是没有指定保留几位小数的。实际测试样例数据输出结果正确,但老金随意输入“C3002H9527O21N”输出结果却是46007.2,而正确的结果应该是46007.246。

因为这个问题娃儿和老金反复分析代码也没发现问题,后来老金将最后一条cout语句改用printf输出结果就正确了。真是不知道cout搞的是什么飞机。

当然了,如果用C++的极其考究记忆力的保留小数位数的语法,也是没问题的。

cout<<fixed<<setprecision(3)<<sum<<endl;

真不知道这样的写法有多少孩子能记得住!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Shell的运行原理和Linux的权限

Shell的运行原理 Linux严格意义上说是一个操作系统&#xff0c;我们称之为“核心&#xff08;kernel&#xff09;”&#xff0c;但我们一般用户不能直接使用kernel&#xff0c;而是通过kernel的“外壳程序”&#xff0c;也就是所谓的Shell&#xff0c;来与kernel沟通。 Shell…

自定义类型——结构体、枚举和联合

自定义类型——结构体、枚举和联合 结构体结构体的声明匿名结构体结构体的自引用结构体的初始化结构体的内存对齐修改默认对齐数结构体传参 位段枚举联合 结构体 结构是一些值的集合&#xff0c;这些值被称为成员变量&#xff0c;结构的每个成员可以是不同类型的变量。 数组是…

福建医疗器械展/2024厦门国际医疗器械展览会重磅来袭

2024中国&#xff08;厦门&#xff09;国际医疗器械展览会 时 间&#xff1a;2024年11月1-3日 November 1-3, 2024 地 点&#xff1a;厦门国际会展中心 Xiamen International Conference & Exhibition Center ​ ◆组织机构 主办单位&#xff1a; 中国技术市场协会医…

7B2 PRO主题5.4.2免授权直接安装

B2 PRO 5.4.2 最新免授权版不再需要改hosts&#xff0c;直接在wordpress上传安装即可

【C++】 string类:应用与实践

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

测试项目实战--安享理财2(Jmeter接口测试)

说明&#xff1a; 1.访问地址&#xff1a; 本项目实战使用的是传智播客的安享理财项目&#xff08;找了半天这个项目能免费用且能够满足测试实战需求&#xff09; 前台&#xff1a;http://121.43.169.97:8081/ 后台&#xff1a;http://121.43.169.97:8082/ &#xff08;点赞收藏…

uniapp 使用renderjs的一些详细介绍

一、简介 官方链接&#xff1a;uniapp官网中的renderjs方法的详细介绍 二、renderjs 定义 renderjs是一个运行在视图层的js。它比WXS更加强大。它只支持app-vue和web。 作用&#xff1a; 大幅降低逻辑层和视图层的通讯损耗&#xff0c;提供高性能视图交互能力。在视图层操作d…

2024CCPC郑州站超详细题解(含题面)ABFHJLM(河南全国邀请赛)

文章目录 前言A Once In My LifeB 扫雷 1F 优秀字符串H 随机栈J 排列与合数L Toxel 与 PCPC IIM 有效算法 前言 这是大一博主第一次参加xcpc比赛&#xff0c;虽然只取得了铜牌&#xff0c;但是收获满满&#xff0c;在了解了和别人的差距后会更加激励自己去学习&#xff0c;下面…

OpenHamrony 实战开发——LiteOS-M内核的中断管理

在程序运行过程中&#xff0c;当出现需要由CPU立即处理的事务时&#xff0c;CPU暂时中止当前程序的执行转而处理这个事务&#xff0c;这个过程叫做中断。当硬件产生中断时&#xff0c;通过中断号查找到其对应的中断处理程序&#xff0c;执行中断处理程序完成中断处理。 通过中…

FPGA OSD 方案,应用于XBOX游戏机收费等领域

FPGA方案&#xff0c;HDMI IN接收原始HDMI 信号&#xff0c;HDMI OUT输出叠加字符/图片后的HDMI信号 客户应用&#xff1a;XBOX游戏机收费 主要特性&#xff1a; 1.支持多分辨率格式显示 2.支持OSD 叠加多个图层 3.支持字体大小随意配置 4.支持字体格式随意配置 5.零延时&…

让墨水屏成为生产力工具,文石做对了什么

文 | 螳螂观察 作者 | 青玥 众所周知&#xff0c;如今&#xff0c;我们的生活中大部分时间都被“屏幕”占据&#xff0c;这一承载着交互与显示功能的介质&#xff0c;出现在我们的手机、平板、汽车等产品上&#xff0c;吞没着我们的工作与生活。 而屏幕的长时间使用势必会对…

【AIGC】Mac Intel 本地 LLM 部署经验汇总(CPU Only)

书接上文&#xff0c;在《【AIGC】本地部署 ollama(gguf) 与项目整合》章节的最后&#xff0c;我在 ollama 中部署 qwen1_5-14b-chat-q4_k_m.gguf 预量化模型&#xff0c;在非 Stream 模式下需要 89 秒才完成一轮问答&#xff0c;响应速度实在是太慢&#xff0c;后续需要想办法…

数据库面试总结

数据库相关 mysql使用的函数 字符相关: concant() 连接字符 trim()去除字符的首尾空格 space(n) 返回n个空格 char_length() 返回字符的个数 ucase()/upper()将字符串 s 的所有字母变成大写字母 lcase()/lower() 将字符串 s 的所有字母变成小写字母 substr/substring/mid(s, …

到底考不考CISP?纠结的看过来

专业认证如CISP&#xff08;注册信息安全专业人员&#xff09;成为了衡量专业水平的重要标准。 CISP的含金量懂的都懂&#xff0c;然而&#xff0c;是否要投入时间、精力和金钱去追求这样一个认证&#xff0c;对于许多人来说&#xff0c;依然是一个值得深思的问题。 那么到底…

PyQt5中的Text Browser常用方法和常用信号

文章目录 1. 简介1.1. 常用方法&#xff1a;1.2 常用信号&#xff1a; 2. Text Browser常用方法使用案例3. Text Browser常用信号使用案例 1. 简介 PyQt5中的QTextBrowser类用于显示富文本内容&#xff0c;并支持与用户交互。以下是一些常用的方法和信号&#xff1a; 1.1. 常…

Vulnhub-wp 获取vulnhub靶机wp搜索工具

项目地址:https://github.com/MartinxMax/vulnhub-wp 简介 搜索Vulnhub平台的解题文章,之过滤返回出正确可访问的页面 使用 $ python3 vulnhubwp.py 支持模糊搜索 [] Query: kiop 进入选项4,获取wp地址 [] Choice options: 4

DML之操作数据表

1. 插入数据 (1). 前言 前文我们实现了如果创建表&#xff0c;接下来我们将学习如何向数据表中插入数据.插入有两种方式. (2). 方式1 : 情况1 : 使用该语法一次只能向表中插入一条记录.为表中的任意字段按默认的顺序插入数据.值列表中需要为表的每一个字段指定值.并且值…

springboot 整合阿里云短信服务

官方sdk示例地址 依赖引入 <!-- https://mvnrepository.com/artifact/com.aliyun/dysmsapi20170525 --><dependency><groupId>com.aliyun</groupId><artifactId>dysmsapi20170525</artifactId><version>2.0.23</version><…

跟TED演讲学英文:How the US is destroying young people‘s future by Scott Galloway

How the US is destroying young people’s future Link: https://www.ted.com/talks/scott_galloway_how_the_us_is_destroying_young_people_s_future? Speaker: Scott Galloway Date: April 2024 文章目录 How the US is destroying young peoples futureIntroductionVoc…

Java中PriorityQueue的用途和性能深度剖析

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…