NOIP学习小站
西安交通大学附属中学航天学校

为什么要学习编程

tangyj626阅读(717)

近年来,编程的重要性已成为越来越多的国际意识的主题,从“极客”的狭隘领域扩展到包括K-12教育领域在内的更广阔的世界。从美国在学校强制推行计算机程序设计教育,到全球范围内兴起“每天编程一小时”活动,再到我国教育部公布《2019年教育信息化和网络安全工作要点》,其中指出:今年将启动中小学生信息素养测评,并推动在中小学阶段设置人工智能相关课程,逐步推广编程教育。对于我们普通人来说,编程的能力也正在凸显出前所未有的重要性。

在琳达·卢卡斯的演讲中,她认为“编程就是一个表达自我的方式”。当她了解到,父母可以很轻松的向孩子解释人体的生理构造如何运作,却无法回答关于电脑如何运作的问题。因为不少人也对计算机不够了解,他们“离开精美的用户界面后就不知道如何与计算机交流”。但是演讲者告诉大家,计算机科学并非深奥难懂的学科,反而生活中处处有计算机科学。

教育部于2017年底正式公布了《普通高中信息技术课程标准(2017年版)》,新的课程标准将原来从知识与技能、过程与方法、情感态度与价值观三个维度描述的目标进行了整合,凝练为信息技术学科的四大核心素养:信息意识、计算思维、数字化学习与创新、信息社会责任。计算思维是计算机科学领域的思想方法,是学科本质的体现,是形成问题解决方案的过程中产生的一系列思维活动。

学科核心大概念是学科知识体系的关键核心,有别于学科核心素养指向人的能力、品格与价值观,核心大概念指向知识结构本身。新课程标准明确指出了信息技术学科的四个核心大概念,分别是数据、算法、信息系统、信息社会。其中算法是对特定问题求解步骤的一种描述,是一系列解决问题的清晰指令,精确的算法是计算工具有效计算的前提条件,也是编程的具体指导方法。由此可见,算法指导下的编程是借助计算思维解决问题的一种重要手段。

编程不经意间变得流行起来。越来越多的人发现学会编程是必不可少的,尤其是对年轻一代来说。在一个借助互联网在几毫秒内搜索任何问题答案的世界中,对于学生来说必备的技能不再仅限于传统知识的学习能力。有很多能让人成功的技巧,而这套技能必须包括编程。

一、编程是数字时代的基本素养

当今是一个技术日新月异的时代,要适应这个时代,首先需要知道如何使用这些技术。而要更好地使用这些技术,还需要了解技术背后的逻辑。学习编程时,我们能够理解并摆弄自己所居住的数字世界。编程使技术看起来更像是“魔法”,我们就能够真正理解并控制这项技术的逻辑——只有这样这一过程才更具有魔力。

我们对技术的依赖只会增加。今天的青年一代不仅仅是能够被动地使用新技术,而且要能够理解和控制它,成为这个巨大数字转变的积极组成部分。

二、编程可以改变世界

我们已经看到生活中编程的成果正在改变世界。当互联网开始将当代人的生活由“日用品"变成“必需品",越来越多的人意识到“编程可以改变世界"。编程是最直接的智力高质转换。通过编程计算机语言,一个人在棋牌等娱乐竞技中体现出的智慧也能够转化为具有实际应用价值和产业价值的输出。

编程是为了自己的世界,可以构造和完善自己的世界。AI时代的编程,拥有大量的接口和配件,我们可以根据自己的想象来构建和完善自己的世界。有的编程语言是为了改变世界,所以它复杂而庞大,有的编程语言是为了改变生活,所以我们的编程工作简约而又优美。我们可以制作各种小家电,可以编程实现各种小应用工具,还可以编程丰富娱乐生活。

三、借助编程让创新想法落地

“你为下一个巨大创新有一个想法?太棒了!你怎么落地?”。每个人都有想法,只有少数人能付诸实践让创意成为现实。编程的能力将那些只有想法的人与能把想法付诸实践的人分开。

目前风靡全球的创客运动,就是鼓励人们将个人的想法积极付诸实践。而在实践的过程中不经意就会涉及大量综合学科内容,其中编程往往不可缺少,也是重要的工具。

如果你想成为一个能给生活带来灵感的思考者和创新者,鼓励自己并加入到学习编程的队伍中来吧。编程让你相信自己可以成为设计师和建设者。

四、编程更是对思维的训练

乔布斯说:“每个人都应该花一点时间学习编程,因为一个学过编程的人,他有一种独特的角度去思考世界”。

任何一个程序中都不是相互分割无关的数据组成,相反,一个程序中会存在很多“重复”内容。比如,贪吃蛇中的“吃食物”动作,一个游戏中贪吃蛇会吃到很多次食物,这也就是前面说到的“重复”。编程过程中,我们需要一直做这样的训练,发现程序中会一直持续的动作,然后将它打包起来,让计算机自己重复,以提高编写效率。学会利用这一点,我们就能学会整合讯息的能力。整合并不是简单相加,而是对现状的优化,也是推陈出新的方式之一。

其实编程也是一种语言的学习,只不过和人与人之间沟通不同的是,这种语言是人与计算机的沟通。理性、严谨是计算机的特性,所以与它对话的语言也必须是理性的,严谨的,不能出半点偏差的。"较真",是外界对程序员们的评价,也是每一个程序员所遵守的信念。仍旧以贪吃蛇游戏程序为例,如果某处思考出现漏洞,游戏过程中就可能会出现“贪吃蛇撞了墙没死”或者“贪吃蛇吃到食物没有变大”等bug,那么这就是一个失败的程序。因此,学习编程,就是在对自己的逻辑思维和逻辑判断能力进行训练。

人的一生不可能不犯错。其实犯错也没什么,改了就好。而“改正”就是编程带给我们的逻辑能力中最重要的一项。但凡程序中出现与预期不一样的运行结果,都需要进行调试、修正。这个过程很麻烦,因为有些bug不是一下子就能找到的,常常需要从头梳理,十分考验人的耐心和细心程度。不过也正因此,才更能磨练出个人的品性,同时也能教会我们反思反省意识。

五、信息学竞赛对升学的帮助

2019年伊始,教育部发布了《关于做好2019年高校自主招生工作的通知》,提出了规范自主招生的“十严格”要求。文件中再次强调:降低自招优惠分值,降低自招优惠人数规模;论文、专利、机构的“假奖项”不得作为条件。导致的后果是文科类竞赛在自主招生中彻底出局,科创类竞赛含金量大幅缩水,学科竞赛类一家独大

2019年《国务院办公厅关于新时代推进普通高中育人方式改革的指导意见》要求稳步推进高校招生改革。进一步健全分类考试、综合评价、多元录取的高校招生机制,逐步改变单纯以考试成绩评价录取学生的倾向。这就意味着综合评价政策在未来会成为多元录取的主要形式。但是从2019年采用综合评价的省市招生情况来看,学科竞赛依然是热门。

信息学竞赛是五大学科竞赛(数学、物理、化学、生物、信息学)之一,在信息学竞赛中获得奖励,对于当前自主招生和综合评价政策形式下的升学有很大帮助,至少是一块“敲门砖”。而信息学竞赛注重的更是思维的训练,编程只是解决问题的想法落地的工具!

中国计算机学会(CCF)于1984年创办全国青少年计算机程序设计竞赛(NOI),从此每年一次NOI活动,吸引越来越多的青少年投身其中。几十年来,通过竞赛活动培养和发现了大批计算机爱好者,选拔出了许多优秀的计算机后备人才。当年的许多选手已成为计算机硕士、博士,有的已经走上计算机科研岗位。

为了在更高层次上推动普及,培养更多的计算机技术优秀人才。竞赛及相关活动遵循开放性原则,任何有条件和兴趣的学校和个人,都可以在业余时间自愿参加。 NOI系列活动包括:全国青少年信息学奥林匹克竞赛和全国青少年信息学奥林匹克网上同步赛、全国青少年信息学奥林匹克联赛(NOIP)、冬令营、选拔赛和出国参加 IOI。

CSP-J/S:CCF非专业级软件能力认证(Certified Software Professional Junior/Senior,简称CSP-J/S)创办于2019年,是由CCF统一组织的评价计算机非专业人士算法和编程能力的活动。全国统一大纲、统一认证题目,任何人均可报名参加。CSP-J/S分两个级别进行,分别为CSP-J(入门级,Junior)和CSP-S(提高级,Senior),两个级别难度不同,均涉及算法和编程。CSP-J/S分第一轮和第二轮两个阶段。第一轮考察通用和实用的计算机科学知识,以笔试为主,部分省市以机试方式认证。第二轮为程序设计,须在计算机上调试完成。第一轮认证成绩优异者进入第二轮认证,第二轮认证结束后,CCF将根据CSP-J/S各组的认证成绩和给定的分数线,颁发认证证书。CSP-J/S成绩优异者,可参加NOI省级选拔,省级选拔成绩优异者可参加NOI。

NOIP:全国青少年信息学奥林匹克联赛(National Olympiad in Informatics in Provinces简称NOIP)自1995年至今。每年由中国计算机学会统一组织。 NOIP在 同一时间、不同地点以各省市为单位由特派员组织。全国统一大纲、统一试卷。初、高中或其他中等专业学校的学生可报名参加联赛。联赛分初赛和复赛 两个阶段。初赛考察通用和实用的计算机科学知识,以笔试为主。复赛为程序设计,须在计算机上调试完成。参加初赛者须达到一定分数线后才有资格参加复赛。联赛分普及组和提高组两个组别,难度不同,分别面向初中和高中阶段的学生。

六、编程并不难学

事实上,编程是一个简单的过程。编程的一个最重要的特点是,它提供了即时的反馈(编写代码运行就能马上看到效果),这是学习编程的关键。如果我们编写程序解决了一个问题,然后立即看到自己想要的结果,这样我们就已经知道自己正确地操作了程序代码。这种即时的正面强化是一种令人难以置信的强大工具。

学习如何编程就像学习其他语言一样,必须练习和测试技能。正如语言打开了人与人交流的能力,编程使我们有能力创造影响周围人的技术。只要有一台普通的电脑,我们就可以利用编程技巧来构建能够改变世界的东西。

如果你还没学习如何编程,现在是时候开始了!

七、几个程序(竞赛)网站

  1. NOI官网:http://www.noi.cn/
  2. 中国计算机学会:https://www.ccf.org.cn/
  3. 块语言编程游戏:https://playground.17coding.net/
  4. 洛谷https://www.luogu.com.cn/

八、推荐学习书籍

信息学竞赛对于编程语言的大篇幅学习仅限于初期,后期主要内容是数据结构和算法。因此强烈不建议使用侧重编程语言系统全面学习的书籍(例如《C++从入门到精通》、《C++ Primer》),也不推荐不加甄别地通过网络教程学习(绝大多数网络教程同样偏重于编程语言的学习,并且有大量生僻对于竞赛不实用的内容)。这里推荐几本适合竞赛入门的学习书籍:

  1. 《信息学竞赛一本通》(第五版 C++版)
  2. 《深入浅出程序设计竞赛》(洛谷学术组)
  3. 《CCF中学生计算机程序设计》入门篇、基础篇、提高篇
  4. 《算法竞赛入门经典》与配套的《算法竞赛入门经典训练指导》

程序与程序设计语言

tangyj626阅读(451)

本文首先介绍程序与程序设计语言,这样我们对C++这一程序设计语言有一个初步的认识;最后介绍编写程序解决问题的一般方法和步骤。

一、程序与程序设计语言

计算机程序(Computer Program),是一组计算机能识别和执行的指令,运行于电子计算机上,满足人们某种需求的信息化工具。

计算机程序以某些程序设计语言编写,运行于某种目标结构体系上。打个比方,程序就如同以英语(程序设计语言)写作的文章,要让一个懂得英语的人(编译器)同时也会阅读这篇文章的人(结构体系)来阅读、理解、标记这篇文章。

程序设计语言是用于书写计算机程序的语言。语言的基础是一组记号和一组规则。根据规则由记号构成的记号串就是语言。程序设计语言有3个方面的因素,即语法、语义和语用。

学习编程,至少需要掌握一门程序设计语言才能将自己解决问题的方法步骤(也就是算法)转化为实际的计算机程序来解决问题得出结果。既然称之为程序设计语言,作为一门语言就像英语一样有自己独特的语法,还好,一门成熟的程序设计语言实用的语法并不像英语那样复杂(要坚定学习的信心哦)。

自20世纪60年代以来,世界上公布的程序设计语言已经有上千种之多,但是只有很小的一部分的到了广泛的应用。

C++一直是一个热门的程序设计语言。C++是C语言的继承,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。C++不仅拥有计算机高效运行的实用性特征,同时还致力于提高大规模程序的编程质量与程序设计语言的问题描述能力。目前信息学竞赛推荐使用的程序设计语言就是C++

此外,竞赛编程和软件编程有一定的区别。竞赛编程学习初期会有大篇幅的程序设计语言的学习,后期主要内容就转向数据结构和算法了。程序设计语言只是一个能将我们解决问题的算法实现的工具,所以信息学竞赛的学习不要一味追求全面掌握某种编程语言的使用,只需要掌握对于竞赛实用的内容即可

二、编写程序解决问题的步骤

这里简单介绍编写程序解决问题的步骤,在后续教程中还会结合实例对编程解决问题的方法和步骤展开更深入的分析讲解。
编程解决问题的一般步骤
  1. 分析问题。首先理清要处理什么样的数据,要得出什么样的结果;再进一步理清究竟要求解什么样的问题。就像我们做数学题一样,首先要认真读题、审题,明确题意。
  2. 设计算法。在上一步理清了“究竟要求解什么样的问题”的基础上,进一步研究如何求解问题,找到解决问题的方法和步骤(第一步要干什么,第二步要干什么……)。就像我们做数学题一样,认真读题审题后,将问题与我们所掌握的知识进行关联,得出解决问题的方法和步骤。

举一个例子,“把大象放入冰箱”这一问题的步骤:第一步把冰箱门打开,第二步把大象放进去,第三步把冰箱门关上。像这样的解决问题的具体方法步骤,在程序设计领域称之为算法。只不过程序设计时算法的描述往往是可以用程序代码实现的,而不是像这里“把大象放入冰箱的三部曲”比较原始的自然语言描述。

在竞赛学习的过程中会接触到各种算法,并且我们会发现程序设计的精髓就是算法

  1. 编写程序。你没有看错,到了第3步我们才开始动手编写程序。一个良好习惯的程序员在动手编写程序前,会对问题经过思深熟虑的分析,进而得出解决问题的算法,然后才通过编写程序将算法转化成能够解决问题得出答案的程序代码。
  2. 调试运行。最后,我们要调试运行写好的程序代码,查看运行结果是否和我们期望的结果一致。要解决的问题越复杂,往往程序也越复杂,这个时候调试运行也要做到全面不留死角,往往要经过科学合理的多次测试才能确保程序能够很好地解决问题。

如果程序调试运行时出现编译错误,我们需要去检查、找到并改正程序中的语法错误;如果运行结果与预期结果不一致,那么需要我们去分析修改程序中的逻辑错误;甚至运行结果与预期结果严重不符,那么可能是设计的算法有问题,这个时候还需要进一步去分析问题重新设计算法。

安装Dev C++

tangyj626阅读(930)

目前信息学竞赛推荐使用的程序设计语言是C++,C++编程的集成开发环境(IDE)很多,使用较为广泛的是Dev C++,在正式竞赛时提供给选手使用的计算机上也安装有Dev C++(目前NOI官方指定安装的Dev C++版本是5.9.2)。本图文教程将详细介绍安装Dev C++的过程。

1.点击上方的下载按钮,下载安装包,下载完成后双击安装包文件

2.等待安装程序自动解压加载完毕

3.弹出的对话框中,语言不作修改直接用 English(后续步骤会介绍在安装结束后将语言设置成中文),点击 OK

4.在对话框中点击 I Agree

5.在对话框中点击 Next

6.在对话框中,软件安装位置不作修改用默认即可,点击 Install

7.开始自动安装软件,等待自动安装结束

8.安装完毕后,在对话框中选择 Run Dev-C++ 5.9.2,点击 Finish

安装结束后,第一次运行Dev C++,会自动进入软件配置界面,设置软件的语言、主题等

9.在弹出来的配置对话框中,语言选择 简体中文/Chinese,点击 Next

如果第一次使用的过程中忘记了设置语言或者没有出现设置语言界面,可以参考文末的方法设置语言。

10.在配置对话框中直接用默认主题,点击 Next

11.点击 OK,至此 Dev C++安装并且设置完毕,会自动打开 Dev C++

12.桌面上自动产生了 Dev C++快捷方式, 以后直接双击快捷方式就能打开Dev C++

使用过程中修改界面语言的方法如下:

首先点击 "Tools" → "Environment Options...":

然后在弹出的窗口中设置想要的界面语言即可:

Dev C++的基本使用

tangyj626阅读(770)

本小节介绍Dev C++最基本的使用,包括打开Dev C++、新建源程序、编写程序、保存文件、编译程序、运行程序。大家注意快捷键的使用可以大大提高我们编程的效率。

回顾前面的内容,我们已经知道在Dev C++里编写程序解决问题前需要分析问题并设计算法。这两个步骤非常重要,切记不要看到问题就匆忙编写程序!下面介绍编程和调试运行环节Dev C++的基本使用。

1.打开Dev C++

安装Dev C++后在桌面上会自动创建Dev C++的快捷方式,在“开始”菜单里也能找到它的快捷方式,直接双击快捷方式就能启动Dev C++。

Dev C++快捷方式

2.新建源程序(快捷键Ctrl+N

通过“文件”→“新建”→“源代码”来新建源程序,也可以直接使用快捷键Ctrl+N

通过“文件”→“新建”→“源代码”来新建源程序,也可以直接使用快捷键Ctrl+N

3.编写程序

在代码编辑区编写程序代码,如果感觉编辑区字体太小,可以通过鼠标滚轮快速调节,具体做法是:在 Dev C++中按住 Ctrl 键不放,滑动鼠标滚轮可以调整字体大小(上滑鼠标滚轮放大,下滑鼠标滚轮缩小)。

这里给出C++程序的框架,也就是我们在编写程序前需要先输入的最基本的代码。我们不急着了解每句代码的意义和用途,先努力把这个最基本的程序框架记下来。

注意:虽然本网站教程中的代码均可以复制粘贴,但是在初学入门阶段强烈建议参照程序自行输入,不要复制粘贴,只有自己手写代码才能发现编码中的语法问题,通过不断出现问题、寻找发现问题、解决问题,在实际操作中积累经验,自己的编码能力才能得到快速充实的提升!

在Dev C++中编码的时候注意其自动补全的功能(例如输入左尖括号,自动补充右尖括号,并且光标位于两者中间),这明显提高了编码的效率。

#include<iostream>
using namespace std;
int main()
{
	
	return 0;
} 

还要关注Dev C++中代码自动“上色”的功能,代码的不同部分颜色可能不同。观察上面截图,第1行文字是绿色,using namespace int return这些单词被加粗显示(这些是C++的关键字)。通过这些特点可以方便我们查找编码中的错误(例如return拼写错误的话,这部分不会加粗)。

4.保存文件(快捷键Ctrl+S

编写好程序后在编译运行前需要先保存文件(特别是第一次需要保存)。通过“文件”→“保存”来保存程序文件,也可以直接使用快捷键Ctrl+S。文件保存后我们会发现C++源程序的后缀名是.cpp。我们要养成良好的文件保存习惯,注意文件的保存位置和文件名的命名。

通过“文件”→“保存”来保存程序文件,也可以直接使用快捷键Ctrl+S

5.编译程序(快捷键F9

C++源程序需要编译后才能运行。通过“运行”→“编译”来编译程序,也可以直接使用快捷键F9

通过“运行”→“编译”来编译程序,也可以直接使用快捷键F9

编译完成后在Dev C++下方的状态区域编译日志选项卡下会出现编译结果信息。如果提示0错误0警告,表示程序没有任何语法问题,编译成功。在Windows操作系统中编译成功后会在源程序所在的文件夹下生成与源程序同名的exe可执行程序

编译结果提示提示0错误0警告,表示编译成功

如果提示有错误,那么编译失败。说明程序有语法问题,需要修改程序,直到编译无错误提示。

将main错误拼写成mian,导致编译错误,
此时在“编译器”和“编译日志”选项卡下都有错误提示,特别是“编译器”选项卡下会有编译错误原因。

绝大多数情况下,编译出错后除了会给出错误提示信息,还会自动指出出错的位置。

using namespace std语句后少写了英文;,导致编译错误。
可以看出Dev C++将第3行红色背景醒目提示表示这一行有语法问题(其实是上一行),
并且在提示信息中指出[Error] expected ';' before 'int'(在int前应该有一个;)

通过上面编译出错情况的展示,我们应该能意识到编写程序是一件严谨的工作,关键处任何拼写错误或者遗漏必要符号都会导致编译错误!

对于初学者来说,出现编译错误不要怕,这些语法错误是最容易找出来并能较快修改过来的。我们要逐步培养并提升自己查找语法错误并快速改正的能力,最后熟练到不出现语法错误。这是一个需要大量上机操作实践并积累总结经验的过程!

6.运行程序(快捷键F10

编译成功后,在Windows操作系统中编译成功后会在源程序所在的文件夹下生成与源程序同名的exe可执行程序文件。此时可以通过“运行”→“运行”来运行程序,也可以直接使用快捷键F10。其实这里是直接调用运行编译产生的exe可执行文件。

程序运行结果会通过自动打开的黑色控制台呈现。当前我们编写的是一个C++的框架程序,除了最基本的程序代码,没有其它任何语句(特别是输出语句),所以没有任何运行结果(需要注意的是,上图中红色方框内的信息是Dev C++平台给出的程序运行反馈信息,不是程序输出的内容!)。

我们修改之前的程序代码,在return 0;这一句前添加一句输出cout<<"Hello World";

#include<iostream>
using namespace std;
int main()
{
	cout<<"Hello World";
	return 0;
} 

注意,修改代码后不能直接运行查看新程序的执行结果,而是需要先编译再运行。否则运行的仍然是上一次编译产生的exe文件,也就是修改前的程序代码。

运行“Hello World”程序,在控制台窗口,发现在Dev C++给出的运行反馈信息前,出现了我们用代码cout<<"Hello World";输出Hello World这一文字信息。

为了避免我们修改代码后忘记了先编译程序而直接运行程序带来的困惑(修改了代码,却发现运行结果没有任何变化),我们可以使用“运行”菜单下的“编译运行”命令(或者使用快捷键F11),这个命令的作用是先编译再运行程序(如果出现编译错误会提示错误信息就不会再进入到运行环节)。

执行“编译运行”命令,先编译再运行

再试试输出其它内容吧!

#include<iostream>
using namespace std;
int main()
{
	cout<<"Hello World"<<endl;
	cout<<"世界你好"<<endl;
	cout<<"其它你想尝试的内容"<<endl;
	return 0;
} 

如果程序运行结果与预期结果不一致,那么需要我们去分析改正程序中的逻辑错误;甚至运行结果与预期结果严重不符,那么可能是设计的算法有问题,这个时候还需要进一步去分析问题重新设计算法。

再来试试下面这一段程序:

#include<iostream>
using namespace std;
int main()
{
	int n;
	cin>>n;
	cout<<n*n;
	return 0;
}

运行后会发现命令行窗口没有出现任何结果,只是光标一直在不停的闪烁:

这是因为这一段程序运行时需要使用者输入数据(本程序要求输入一个整数),我们尝试输入一个整数然后按下回车键,查看运行结果并猜想程序的功能。


注意:运行编译后的程序时,如果发现有下图中的错误提示“Failed to execute…”

出错原因是不小心将编译模式设置成了 64 位(使用的计算机却是 32 位操作系统),处理方法如下:(使用 TDM-GCC 4.8.1 32-bit Release

此外,还有一种常见的错误,如下图所示:

错误提示信息指出,编译时无法生成可执行文件(没有权限)。出现这个问题的最可能原因是:之前编译运行过程序,且程序还未运行结束(一般是还在等待用户输入数据),这个时候由于操作系统限制不能删除正在使用的程序文件,所以编译无法生成新的可执行文件。此时只需要在任务栏找到并关闭之前的程序运行窗口,就能继续正常编译运行程序了。


7.调试程序

当程序运行发现结果与预期不一致时,我们可以通过Dev C++调试功能去排查程序中的逻辑错误,程序的调试方法会在后期教程中介绍。

在线评测平台(OJ)的使用

tangyj626阅读(716)

为了尽快掌握基础知识,需要通过大量的练习测试。在练习的过程中,我们需要强有力的工具来获取大量的习题并且能够及时检测编写的程序是否正确(能够帮助我们测试编写的程序是否能很好地解决问题,不存在BUG),在线测评平台(OJ)能够满足我们的需求。

目前在线测评平台(OJ,Online Judge)很多,这里推荐几个实用的。

一、洛谷

洛谷创办于2013年,出道名为“洛谷Online Judge”,致力于为oiers/acmers提供清爽、快捷的编程体验。它不仅仅是一个在线测题系统,它拥有强大的社区、在线学习功能。同时,许多教程内容都是由五湖四海的oiers提供的,保证了内容的广泛性。无论是初学oi的蒟蒻,还是久经沙场的神犇,均可从洛谷获益,也可以帮助他人,共同进步。

1.注册账号

在洛谷首页右上角点击“注册”,进入注册页面。

可以选择电子邮件手机号码两种注册账号方式。注意注册后需要按照提示激活账户

2.登录洛谷

在洛谷首页右上角点击“登录”,进入登录页面。

输入用户名、密码和验证码,点击登录按钮登录。

3.题单刷题

点击洛谷网页左侧导航栏中的“题单”,进入到“题单广场”页面。洛谷的题单是按照学习进度来组织的,非常适合初学者。

4.题库刷题

除了上面介绍的题单刷题外,还可以使用题库刷题。点击洛谷网页左侧导航栏中的“题库”,进入到“题目列表”页面。注意:题库中的题不是按照学习进程来排序组织的,对于初学者来说,在题库中依次刷题很容易碰到暂时无法解决的难题。对于初学者,可以选择“入门与面试”类型题库进行练习。

5.提交程序

不管是题单、题库,点击题目名称,会打开题目具体描述页面。题目描述内容包括:题目背景题目描述输入格式输出格式输入输出样例说明/提示

首先要认真读题,明确输入输出(此时要特别关注题目中给出的输入格式和输出格式),明确要解决的问题并确定解决问题的算法,然后在本地(例如Dev C++或者后文介绍的洛谷在线IDE)中编写程序并调试运行,测试编写的程序是否能够解决问题(此时可以借助题目给出的输入输出样例来初步测试)。本地编程并调试运行测试无误后,再提交答案。

在代码提交页面选择程序语言为C++,将程序代码复制(使用右键菜单或者快捷键Ctrl+C)粘贴(使用右键菜单或者快捷键Ctrl+V)到代码输入区域中,然后点击提交(其实也可以提交源程序文件)。

然后会自动跳转到测评结果页面,等待程序测评结束(等待时间要看洛谷平台的负载情况),会给出详细的测评结果(包括是否通过,每个测评点的信息,源代码等)。

所有测试点均通过,会显示通过此题的提示信息
测试点方块颜色为绿色,并标识为AC(Accept),表示通过该测试点。所有测试点均通过,本题才通过。

如果出现编译错误,在测评结果页面会显示编译错误信息。

编译错误信息(上图编译错误原因是将main误写成了mian)
程序运行结果与测试点标准结果不一致,会提示该测试点WA(Wrong Answer)
一般要求1s钟内要出结果,否则测试点会提示TLE(Time Limit Exceeded,运行超时)

6.加入团队

在洛谷中还可以加入学习团队(甚至可以自己组建团队),我们的学习团队里老师会发布作业、举行比赛,团队里也有题单和题目。下面介绍加入团队的方法:

  1. 用个人账号登录洛谷:https://www.luogu.com.cn/auth/login
  2. 登录后点击学习团队链接:https://www.luogu.com.cn/team/3639
  3. 在团队页面点击加入团队按钮
  1. 填写验证信息(真实的年级、班级、姓名,以方便团队管理员审核)
  1. 等待审核通过(老师在QQ群里回复审核通过后自行刷新页面,或者等待一段时间刷新页面查看是否通过审核),审核通过后会出现如下提示:
  1. 进入团队方法:鼠标移动到页面右上角个人头像上,在下浮菜单中点击我的团队。在我加入的团队里点击团队名称进入团队。
注意时常关注团队的“题目”、“作业”、“题单”、“比赛”这些栏目

7.在线IDE

洛谷平台还提供了一个在线IDE,可以直接在网页中编辑程序并提交查看运行结果。如果我们安装Dev C++出现问题,可以直接在在线IDE中编程。并且洛谷的在线IDE提交的程序是在Linux环境下运行的,与信息学竞赛程序判分环境相同。

注意:如果程序需要输入数据,在点击运行按钮前需要将数据输入到左下方的数据输入区

二、NOI OpenJudge

NOI OpenJudge最大的特色是所有题目按照学习进程组织,非常适合于初学者。

三、信息学奥赛一本通在线OJ

《信息学奥赛一本通》是一本影响较大,使用较为广泛的信息学奥赛入门教材(除一本通外,还推出了《训练指导教程》、《初赛篇》、《提高篇》、《高手训练》等系列丛书)。这套教材配套了在线OJ平台,包括基础知识、算法以及历届真题。

四、CCF中学生程序设计在线评测系统

“CCF中学生程序设计在线评测系统”是CCF(中国计算机学会,信息学奥赛官方组织)推出的OJ。

C++程序框架

tangyj626阅读(1416)

编写C++程序,首先需要书写一些固定的内容,这些内容称之为C++程序框架。本小节我们一起来学习C++程序框架,并编写第一个C++程序。

注意:虽然本网站教程中的代码均可以复制粘贴,但是在初学入门阶段强烈建议参照程序自行输入,不要复制粘贴,只有自己手写代码才能发现编码中的语法问题,通过不断出现问题、寻找发现问题、解决问题,在实际操作中积累经验,自己的编码能力才能得到快速充实的提升!

一、C++程序框架

编写C++程序,首先需要书写一些固定的内容,这些内容称之为C++程序框架:

#include<iostream>
using namespace std;
int main()
{
    
    return 0;
} 

现阶段我们还无法完全理解上面每句代码的意义和用途,先努力把这个最基本的程序框架记下来。这里给出一些简单的解释,阅读后能够初步了解语句代码的意义,也能够帮助我们记忆。首先特别强调的是:C++中和语法相关的字母符号都是英文。我们编码的时候往往是在英文半角状态下输入代码。

第一句 #include<iostream> 是引入头文件,使用#include语句引入了iostream这个头文件。程序往往离不开输入数据和输出结果,C++内置了用于输出信息和输入数据的函数(现在可以把函数理解成工具,就像烹饪时使用的锅和铲子一样),这些函数就在iostream这个头文件里,要使用输入输出函数(本文后面要介绍的cin和cout)就需要先引入iostream这个头文件。后面我们还会接触到其它头文件,甚至我们还可以自己组织头文件。

记忆技巧:include——包含,iostreaminput output stream的简写,表示输入输出流。不要忘记include前面的#

include发音
stream发音

第二句 using namespace std; 表示使用std命名空间,命名空间是C++相对于C引入的新内容,暂时不用理解这一句的作用,只需知道使用这一句后可以直接使用cin、cout这些函数,而不需要写的过于复杂(std::cin,std::cout)。

记忆技巧:using——使用,namespace——命名空间(name+space),std是standard(标准)的简写。不要忘记语句末尾的英文分号 ;

接着 int main() 是主函数,可以看到后面有花括号{},将主函数的内容包围了起来。计算机运行我们编写的程序时,会自动从int main后面{}中的第一句开始运行,依次运行每条语句,直到最后一句为止。

主函数最后一条语句 return 0; 意思是主函数返回一个0。其实是向操作系统表明程序正常运行结束。注意,在平时测评和竞赛时千万不要返回一个非0值。

记忆技巧:int——整数数据类型,main——主要的,return——返回。不要忘记main后面的小括号(),return 0;这一句以英文分号 ; 结束。

main发音
return发音

在Dev C++中编写程序框架代码,保存文件并编译运行,运行结果如下图所示:

程序运行结果会通过自动打开的黑色控制台呈现。当前我们编写的是一个C++的框架程序,除了最基本的程序代码,没有其它任何语句(特别是输出语句),所以没有任何运行结果(需要注意的是,上图中红色方框内的信息是Dev C++平台给出的程序运行反馈信息,不是程序输出的内容!,其实我们从反馈信息中可以得知程序运行耗时0.2095秒,返回值是0——这是代码return 0;这一句的作用)。

竞赛的编程关注的是数据的高效处理,所以从头到尾我们编写的程序都是这样的控制台运行效果,不会涉及到窗体编程。

二、Hello World 程序

很多程序设计教材的第一个程序都是“Hello World 程序”(输出文字Hello World ),C++的“Hello World 程序”如下:

#include<iostream>
using namespace std;
int main()
{
    cout<<"Hello World";
    return 0; 
} 

第一次正式地在Dev C++中书写程序,这里特别提出“编码规范”,目前需要注意做到合理缩进

如上图所示,花括号{}中的内容没有顶在最左边写,而是缩进了一个制表符(键盘tab按键的效果就是一个制表符)。这样的好处是能够较好地展示出程序的结构,提高代码的可读性,特别是在程序结构复杂、代码量大的时候。幸运的是,在Dev C++中编码,会自动实现缩进功能(按下左花括号{并回车,会发现自动补齐了右花括号},并且光标自动移动到了{}中的空行,光标所在位置也自动实现了缩进),如果没有自动缩进,可以通过tab键来实现标准的缩进。

对于初学者,养成良好的编码习惯其实很重要,能够确保代码可读性。初学者在编码时大家一定要注意编码规范,习惯成自然,开始时不注意后面就很难纠正。想想需要去阅读大量书写随意、格式乱糟糟的代码,肯定是一件痛苦的事情。你书写的代码或许就是老师或者同学痛苦的根源。


上面的示例程序,在主函数int main中,return 0;语句前添加了一句 cout<<"Hello World"; 一般情况下,代码都书写在return 0;前。cout用于输出信息,这里输出了一句话Hello World,这一句话要用英文双引号括起来。cout与输出的内容间用两个英文左尖括号(小于符号) << 隔开。

我们发现上面的程序中有三句代码:

using namespace std;
cout<<"Hello World";
return 0;

它们都是以英文分号结束的。C++中的语句普遍以英文分号结束(但不是所有)。

编译运行“Hello World 程序”,结果如下:

可以看到程序输出了一句话Hello World。我们可以尝试输出其它内容,甚至是中文内容(竞赛时几乎不会出现输出中文内容的情况),只需要修改英文双引号中间的内容即可。但是注意要输出的内容必须用英文双引号括起来,不能是中文引号(特别是在测试输出中文内容时,容易遗忘切换输入法导致输入了中文引号,此时编译会出现错误提示)。

三、初学者编码容易犯的语法错误

这里总结一下初学者容易犯的语法错误:

  1. 框架中单词拼写错误。例如将main错误输入成mian;
  2. 中英文符号错误。C++中和语法相关的字母符号都是英文。例如cout输出中文信息忘记切换输入法输入了中文的引号导致语法错误;
  3. 遗忘语句结束符号——英文分号。C++中的语句普遍以英文分号结束,不要遗忘;
  4. 遗忘main后面的小括号()和包围main函数内容的{}。

cout输出信息

tangyj626阅读(960)

C++中一般使用cout来输出信息,输出的信息形式是多样的,可以是一句话(文本),也可以是数字(整数或者小数),甚至还可以是一个数学算式。

一、输出信息

回顾上一小节编写了第一个程序,使用cout输出Hello World

#include<iostream>
using namespace std;
int main()
{
    cout<<"Hello World";
    return 0;
} 

我们再通过几段代码来进一步了解cout的使用方法,大家要多练习并仔细观察分析程序运行结果。通过上机实践来积累经验,逐步避免语法错误;通过观察结果分析代码功能,了解语句的作用:

#include<iostream>
using namespace std;
int main()
{
    cout<<"Hello World";
    cout<<"Nice to meet you!";
    return 0;
} 

左侧的代码使用先后的两句cout输出信息,发现输出的两句话在同一行中并且紧挨着(并不是两行哦)。

Hello WorldNice to meet you!

再试试下面的代码:

#include<iostream>
using namespace std;
int main()
{
    cout<<"Hello World"<<endl;
    cout<<"Nice to meet you!";
    return 0;
} 

左侧代码运行结果,可以看到输出了两行文本:

Hello World
Nice to meet you!

使用endl实现了换行效果。cout<<"Hello World"<<endl;这一句是cout输出语句,其实输出了两项内容——"Hello World"这句话和换行符(endl,可以将换行符理解成回车键效果)。可知cout可以一次性输出多项内容,每项内容间用两个英文左尖括号 << 隔开即可。

我们还可以尝试将输出语句修改成:

cout<<"Hello World"<<endl<<"Nice to meet you!";

二、程序注释

再来试试下面的程序,输出数字,甚至是计算式(表达式)。

注意下面的示例程序展示了如何在代码中添加注释,上机时下面程序中的注释信息不用书写,但要了解注释的使用方法和用途:

#include<iostream>
using namespace std;
int main()
{
    cout<<123<<endl;
    cout<<3.14159<<endl;
    //如果行内容以//开头,表示这一行的内容是注释(上机时这段程序时注释内容不用书写)
    //注释往往用来给阅读者一些提示信息或者是备注语句的作用
    //下面的注释就为大家解释了语句的作用,但是实际编程时只需要在必要的地方加上注释

    //输出两个数字(一个整数和一个小数),中间用一个空格隔开
    //cout输出了四项信息,整数123、只有一个空格的一句话、小数3.14159以及换行符
    cout<<123<<" "<<3.14159<<endl; 

    cout<<2*3.14159*12.56<<endl;       //输出半径为12.56的圆的周长(这里也是注释)
    cout<<3.14159*12.56*12.56<<endl;   //输出半径为12.56的圆的面积

    /*
    从上面的注释方法可以看出,一行中如果出现//,那么//之后的内容是注释
    //可能在行开始处出现,那么整行都是注释
    //也可能在行的语句后出现,那么//之后的内容是注释

    发现了吗?这里的内容也都是注释,并且是多行的(大家仔细查看多行注释开始结束的标记符号)
    //是行内注释,仅作用于一行内
    */
    return 0;
} 

注意 行内注释// 和 多行注释/**/的使用方法。良好的注释可以提高程序代码的可读性,也能避免遗忘重要语句的作用(有可能编写的代码过一段时间自己都读不懂,加上必要的注释能避免这样的情况)。

编译时编译器会自动去掉所有的注释信息然后生成exe文件,所以注释信息不会出现在最终的可执行文件中,很显然注释不会影响程序的运行效率。

三、输出信息小结

  1. 使用cout语句输出信息,输出的信息可以是一句话(必须用英文引号括起来)、数字(整数、小数/浮点数)、变量(后续学习内容)、或者计算表达式:
cout<<"Hello World";
cout<<12345;
cout<<3.14159;
cout<<123*456;
  1. 可以只输出一项信息,也可以一次性输出多项信息。cout与输出内容之间、各内容间均用 << 隔开:
cout<<1<<"+"<<2<<"="<<1+2;
  1. 输出一个特殊的内容:endl 可以实现换行效果:
cout<<1<<"+"<<2<<"="<<1+2<<endl;

数学运算

tangyj626阅读(2850)

编写程序解决问题,往往离不开运算,从最简单的数学运算,到复杂的幂、开方、三角、对数,C++都能轻松应对。

一、基本数学运算

数学中最基本的是加(+)、减(-)、乘(×)、除(÷)四则运算,C++中有加(+)、减(-)、乘(*)、除(/)、模(%)五则基本运算。下面通过几个示例程序加以说明:

#include<iostream>
using namespace std;
int main()
{
    cout<<123+456<<endl;            //两个整数相加
    cout<<123.456+456.789<<endl;    //两个小数(浮点数)相加
    cout<<123-456<<endl;            //两个整数相减
    cout<<123.456-456.789<<endl;    //两个小数(浮点数)相减
    cout<<-789<<endl;               //-表示负数
    cout<<-(-789)<<endl;            //-也能表示取反
    return 0;
} 

编写并运行上面的程序,观察运行结果,可知用cout输出一个计算表达式,运行时会先计算结果,再将结果输出。再来看下面这段程序:

#include<iostream>
using namespace std;
int main()
{
    cout<<123*456<<endl;   //注意乘号是米字乘*,不能写成叉乘×,也不能写成点乘·
    cout<<123*-456<<endl;
    
    cout<<12.34*56.78<<endl;
    cout<<123.456*456.789<<endl;
    cout<<1234567.45*456.78<<endl;
    cout<<0.00123*0.004567<<endl;
    cout<<123.000<<endl;
    cout<<123.9999<<endl;
    return 0;
} 

在Dev C++中上面程序运行结果如下:

56088
-56088
700.665
56393.3
5.63926e+008
5.61741e-006
123
124

后面6行输出有点费解,和计算器算出来的结果不一致,原因是:cout输出小数(浮点数)时,默认最多保留6位有效数字(第3行输出700.665,第4行输出56393.3的原因);如果数字太大或太小(绝对值超过\(10^6\)或者绝对值小于\(0.00001\)),会按照科学计算法的方式输出(所以第5行输出5.63926e+008、第6行输出5.61741e-0065.63926e+008表示\(5.63926 \times 10^8\),5.61741e-006表示\(5.61741 \times 10^{-6}\))。cout输出浮点数会忽略掉小数末尾的0,所以第7行输出123,第8行保留6位有效数字并忽略掉小数末尾的0输出124。

也可以指定浮点数的输出格式,例如下面的程序指定浮点数保留5位小数输出(注意引入了一个新的头文件#include<iomanip>):

#include<iostream> 
#include<iomanip>   //这个头文件不能少 
using namespace std;
int main()
{    
    //设置输出小数(浮点数)时保留5位小数
	//固定写法,保留其它小数位数修改setprecision()括号中的数字即可 
    cout<<fixed<<setprecision(5);
    //设置后,后面输出都会遵循输出规则 
    
    cout<<12.34*56.78<<endl;
    cout<<123.456*456.789<<endl;
    cout<<1234567.45*456.78<<endl;
    cout<<0.000009<<endl;
    cout<<123<<endl;      //输出123,setprecision设置的保留小数位数对整数无效
    return 0;
} 

程序输出结果如下:

700.66520
56393.34278
563925719.81100
0.00001
123

分析结果可知,保留小数位数的同时还自动实现了“四舍五入”!小结一下:引入iomanip头文件后,使用cout输出浮点数时可以通过cout<<fixed<<setprecision(要保留的小数位数);设置小数位数,保留小数位数时会自动实现“四舍五入”。其实这个保留小数位数的方法过于复杂,不好记忆,后面会介绍简单的方法。

再来看除法运算和模运算(模运算就是求余数):

#include<iostream>  
using namespace std;
int main()
{    
    cout<<5/2<<endl;		// 整数/整数,注意除号必须用/
    cout<<4/2<<endl;		// 整数/整数 
    cout<<5.0/2<<endl;	  // 浮点数/整数
    cout<<5.0/2.0<<endl;	// 浮点数/浮点数
    cout<<5%2<<endl; 	   // %是模运算,这里计算5除以2的余数 
    return 0;
}

程序输出结果如下:

2
2
2.5
2.5
1

分析运算结果,会发现第1行 5/2 输出2与数学运算结果不一致。C++中的除法运算有一个特点,如果是 整数/整数 ,那么结果是一个整数(小数部分全部被舍弃,结果是除法的商),但是被除数或者除数只要有一个是浮点数,那么结果就是浮点数。

注意模运算仅限于 整数%整数 ,否则会出现编译错误!

小结一下:

  1. 整数/整数 ,结果是除法的商;
  2. 浮点数参与除法(被除数数和除数至少有一个是浮点数),结果是浮点数;
  3. 整数%整数 结果是除法的余数。

最后,和数学混合运算一样,C++复杂的运算也涉及到运算优先级的问题:

#include<iostream>  
using namespace std;
int main()
{    
    cout<<1+2*3<<endl;
    cout<<(1+2)*3<<endl;
    return 0;
} 

大家自行编辑程序并运行,通过分析结果可知:C++中也是优先计算乘除(包括模运算),再计算加减;如果有小括号,那优先计算小括号中的内容。这和数学混合运算一致,有一点不同的是,不管多少层括号,都用小括号

二、使用数学函数

来看一个数学问题,计算直角边为60和91的直角三角形斜边长。已知斜边计算公式 \(c = \sqrt{a^2+b^2}\),这里平方和还容易得到:60*60+91*91,但是接下来的开平方是C++的五则基本运算无法实现的!

好在C++中已经内置了不少函数(目前可以把C++的函数理解成可以实现特定运算的工具),能够完成开平方、幂、三角、对数等数学计算。要解决上述问题,我们可以用到两个函数——pow幂函数和sqrt开平方函数,先编写一个程序来测试一下两个函数的功能(注意需要通过#include<cmath>引入cmath头文件):

#include<iostream>
#include<cmath>  //需要引入cmath头文件  
using namespace std;
int main()
{    
    cout<<pow(60,2)<<endl;  //pow(60,2)计算60的2次方,然后cout输出结果
    cout<<sqrt(9)<<endl;    //sqrt(9)计算9开平方,然后cout输出结果
    return 0;
} 

计算斜边的公式 \(\sqrt{60^2+91^2}\),对应的C++代码可以写成:sqrt( pow(60,2) + pow(91,2) ),这个复杂的计算表达式会首先计算sqrt()括号中的内容pow(60,2) + pow(91,2),这是一个加法运算,那么会先后计算出pow(60,2)pow(91,2)的值,然后相加的结果作为sqrt函数开平方的参数。最终程序代码如下:

#include<iostream>
#include<cmath>  //需要引入cmath头文件  
using namespace std;
int main()
{    
    cout<<sqrt(pow(60,2)+pow(91,2))<<endl;
    return 0;
}

其实pow函数可以计算小数次方,由数学知识可知 sqrt(9)pow(9,0.5) 是等效的。那么还可以只使用pow函数解决问题:

#include<iostream>
#include<cmath>  //需要引入cmath头文件  
using namespace std;
int main()
{    
    cout<<pow(pow(60,2)+pow(91,2),0.5)<<endl;
    return 0;
}

这里将cmath头文件中常用的函数整理如下表:

函数调用方法举例说明
\(pow(x,y)\)\(pow(3,2)\)
\(pow(3,0.5)\)
\(pow(2.5,7.8)\)
计算 \(x^y\),结果是浮点数
\(sqrt(x)\)\(sqrt(2)\)
\(sqrt(2.5)\)
计算 \(\sqrt{x}\),结果是浮点数
\(abs(x)\)\(abs(-10)\)计算\(x\)的绝对值\(|x|\)。如果\(x\)是整数,结果是整数;如果\(x\)是浮点数,结果是浮点数
\(fabs(x)\)\(fabs(-10)\)
\(fabs(-2.5)\)
计算\(x\)的绝对值\(|x|\),不论\(x\)是整数还是浮点数,结果都是浮点数
\(ceil(x)\)\(ceil(2.1)\)
\(ceil(-2.9)\)
计算大于或等于\(x\)的最小整数(向上取整),结果是小数部分为0的浮点数
\(floor(x)\)\(floor(2.9)\)
\(floor(-2.1)\)
计算小于或等于\(x\)的最大整数(向下取整),结果是小数部分为0的浮点数
\(sin(x)\)
\(cos(x)\)
\(sin(3.14159)\)
\(cos(3.14159/2)\)
计算三角函数正弦\(sin(x)\)、余弦\(cos(x)\)值
\(x\)是弧度角度
\(exp(x)\)\(exp(1)\)计算\(e^x\),\(e\)是自然常数
\(log(x)\)\(log(10)\)
\(log(exp(1))\)
计算\(x\)的自然对数\(\ln{x}\)(即\(\log_{e}{x}\))
\(log2(x)\)\(log2(2)\)
\(log2(1024)\)
计算以2为底\(x\)的对数\(\log_{2}{x}\)
\(log10(x)\)\(log10(10)\)
\(log10(100)\)
计算以10为底\(x\)的对数\(\log_{10}{x}\)

建议自己编写程序测试一下上面cmath中的常用函数。

此外algorithm头文件下还有\(max、min\)两个常用函数:

函数调用方法举例说明
\(max(a,b)\)\(max(3,5)\)
\(max(0.1,0.5)\)
计算\(a、b\)的最大值
\(a、b\)的类型必须相同,例如全是整数或者全是浮点数
\(min(a,b)\)\(min(3,5)\)
\(min(0.1,0.5)\)
计算\(a、b\)的最小值
\(a、b\)的类型必须相同,例如全是整数或者全是浮点数

三、浮点数的特点

程序设计语言中一般将小数称为浮点数(这个名称是根据浮点数在计算机内部存储方式得来的),来测试下面的程序:

#include<iostream>
#include<cmath>
using namespace std;
int main()
{
	cout<<ceil(1.01)<<endl;
	cout<<ceil(1.000000000000001)<<endl;
    cout<<ceil(1.0000000000000001);
    return 0;
}

ceil是向上取值,预期应该都输出2,但是实际运行结果如下:

2
2
1

感觉奇怪吧!注意:C++中的浮点数只能保证在一定的有效数字范围内是可靠的,超过这个范围就只能认为是约等于不是精确相等

cout<<1.0000000000000001;会输出1,后面的.0000000000000001超出了可靠的有效数字范围,C++处理时这部分被丢弃了!此时cout<<ceil(1.0000000000000001);其实就相当于 cout<<ceil(1);

变量与常量

tangyj626阅读(1399)

前面学习的示例程序很简单,甚至会认为编写的程序无外乎就是一个“计算器”罢了。但是C++程序能完成的任务不仅仅计算几个表达式这么简单。有些复杂的问题需要经过多个表达式计算才能解决,这个时候需要想办法来存储计算的中间结果,使用变量能够很好应对这样的情况。

一、变量与赋值

小A在玩一个小游戏,玩游戏的过程如下:

  1. 游戏开始时,有5条命
  2. 第一关小A顺利闯过,没有损失命,获得2条命的奖励
  3. 第二关小A顺利闯过,没有损失命,因为耗时比前一关长只获得1条命的奖励
  4. 第三关小A顺利闯过,但太过大意导致损失了3条命,没有获得任何奖励
  5. 第四关小A顺利闯过,损失了2条命,也获得了1条命的奖励
  6. 最后一关较困难,小A损失了所有的命数也未能通关

现在编写程序输出游戏开始前和每关结束后小A的命数。你可能马上想到的是这样输出:

cout<<5<<endl<<7<<endl<<8<<endl<<5<<endl<<4<<endl<<0<<endl;

确实得出了正确的结果,但是这样编程没有体现出“让计算机来计算”,使用计算机就是要让计算机来计算解决问题的,这里却是人计算出了结果然后让计算机输出。借助变量可以这样编程:

#include<iostream>  
using namespace std;
int main()
{
    int n = 5;
    cout<<n<<endl;
    
    n = n+2;
    cout<<n<<endl;

    n += 1;
    cout<<n<<endl;
    
    n -= 3;
    cout<<n<<endl;
    
    n = n-2+1;
    cout<<n<<endl;

    n = 0;
    cout<<n<<endl;
    return 0;
} 

程序第5行 int n = 5; 定义了一个int类型的变量n,同时给变量n赋初值5。int是一种最基本的整数数据类型(后文会介绍),可以存储一个整数。这一句相当于给了一个专门存放整数的盒子,这个盒子的名称是n,最开始往里面存放了5这个整数。

第6行 cout<<n<<endl; 使用cout语句输出变量n的值,此时会输出5。这一句相当于取出了存放在名称为n的盒子的数拿来使用(输出这个数)。

第8行 n = n+2; 是一个赋值语句(第5行其实也是赋值语句,不过还先定义了变量n的数据类型为int)。注意这里的等号 \(=\) 不能认为是判断相等(如果是判断相等,n=n+2是不成立的),这里的 \(=\) 应该读作赋值成\(=\) 赋值符号。这一句赋值语句就相当于向名称为n的盒子里放新的东西,首先会计算赋值符号右边表达式 n+2 的结果(要计算这个结果,首先要使用变量n的值,就和第6行输出n值一样,要取出名称为n的盒子里的数来参加计算,也就是取出来名称为n的盒子里的数+2),然后将计算结果(7)仍然存放到名称为n的盒子里,原来盒子里存放的数据会被丢弃。

这里小结一下,可以通过赋值语句为变量赋值,赋值语句的作用是先计算赋值符号右边表达式的结果,然后将结果赋值给左边的变量。给一个变量重新赋值,变量之前的值会被丢弃(标准说法是被覆盖掉)。在cout输出语句或者计算表达式中可以通过变量名取出变量当前存储的数据。

第11行 n += 1; 也是赋值语句,确切地说是“自赋值”。这一句的效果同 n = n+1; 一致,这里可以简单认为 n += 1; 就是 n = n+1; 的简写形式。作用是将n的值取出来,然后加上1,最后将结果又赋值给变量n。除了+=,还能使用 -=*=/=%=来进行自赋值。读者可以尝试修改第8行和11行原来的赋值语句为自赋值语句。

其实 n += 1; 还可以简写成 n++; 或者 ++n;n++;++n;两者在这里效果相同,但执行过程不同,后面会介绍);同样的n -= 1; 可以简写成 n--; 或者 --n;

统观整个程序代码,会发现在程序运行过程中,变量n的值发生了多次改变(都是通过赋值语句实现的),这也是变量这一程序名词中“”的含义。可以通过赋值语句(当然不仅仅是赋值语句)随时改变变量的值。

再来看一个计算圆的周长和面积的例子:

#include<iostream>  
using namespace std;
int main()
{
	double r;
	r = 12.56;
	cout<<2*3.14*r<<endl;
	cout<<3.14*r*r<<endl;
    return 0;
}

第5行 double r; 声明了一个double类型(浮点数)的变量r。第6行使用赋值语句为变量r赋值。第7、8行使用变量r参加了计算并使用cout输出计算结果。

#include<iostream>  
using namespace std;
int main()
{
	double r,C,S;
	r = 12.56;
	C = 2*3.14*r;
	S = 3.14*r*r;
	cout<<C<<endl<<S<<endl;
    return 0;
}

第5行 double r,C,S; 声明了三个double类型(浮点数)的变量r、C、S。第6行使用赋值语句为变量r赋值。第7、8行使用赋值语句为变量C、S赋值(变量r参加了计算,出现在赋值符号右边的表达式中)。第9行使用cout输出变量C、S的值。


对前面的内容做一个小结:

  1. 变量在使用(为其赋值或者取出值参加计算)前必须先定义,定义变量的格式为:数据类型 变量名;
    可以在定义变量的同时为变量赋初值:数据类型 变量名 = 初始值;
    还可以一次性定义多个相同数据类型的变量:数据类型 变量名1,变量名2,变量名3,...;
    注意:变量名区分大小写!a和A是不同的变量。
  2. 变量取名规范:
    1. 只能由大小写英文字母、数字、下划线(_)组成;
    2. 不能以数字打头;
    3. 不能使用C++的关键字。C++有不少关键字(又称为保留字),例如using、namespace、int、double、if、for、while等,使用关键字作为变量名会出现编译错误;
    4. 在同一作用域不能定义相同名称的变量,作用域的概念后续会接触到。例如:
      int n = 12;
      int n = 0;

      会导致编译错误;
    5. 变量的取名不能太随意,建议做到“见命知义”,或者遵循一些普遍的做法(例如上面示例程序使用变量n来存储整数,后续还会接触到其它变量命名的普遍做法),这样不仅提高代码可读性方便他人阅读,也能使自己在编写程序时思路更加清晰。
  3. 使用变量的值前,需要确保变量已经存储有确定的值。例如下面的语句:
    int n;
    cout<<n<<endl;

    定义了变量n,但是没有为变量n赋值,后面直接取出变量n的值用于输出,这个时候变量的值是不确定的。在使用变量的时候要注意避免使用未赋值的变量的情况。
  4. 赋值语句的格式为:变量名 = 计算表达式; 其中的 \(=\) 是赋值符号,可以读作“赋值为”。赋值语句的作用是先计算赋值符号右边计算表达式的结果,然后将结果赋值给左边的变量。赋值后变量中存储的是新值,原来的旧值会被覆盖(丢弃)。
  5. 自赋值语句的格式为:变量名 自赋值符号 计算表达式; 其中自赋值符号可以是 +=、-=、*=、/=和%=。自赋值语句的作用是先计算自赋值符号右边计算表达式的结果,然后将左边变量当前值与右边计算结果根据自赋值符号中的运算符进行计算,将最后结果赋值给左边的变量。例如 n += 1; 相当于 n = n+1;
  6. n += 1; 还可以简写成 n++; 或者 ++n; ;同样的n -= 1; 可以简写成 n--; 或者 --n;

二、数据类型

上面的讲解已经涉及到数据类型的概念。我们用“盒子存取东西”较为形象地说明变量的使用,那么数据类型可以理解成“盒子的类型”,不同的数据类型对应不同类型的“盒子”,就像现实生活中有用来装水、装油、装化学试剂的不同用途的盒子一样。要处理整数,那么应该用int类型的盒子,也就是要使用int类型的变量;要用变量存储圆的半径,为了提高程序的实用性,最好使用double类型的盒子(整数小数都能装下),也就是要使用double类型的变量。

在计算机内部,数据的处理采用的是二进制,二进制的最小单位是位bit(1个bit只能存放0或者1),最基本的存储单元是字节Byte(1个Byte是连续的8bit)。以存储整数的盒子(整数数据类型)为例,不同的数据类型还可以理解成“盒子的尺寸”,C++中就提供了1Byte、4Byte、8Byte这三个尺寸的盒子,分别对应char、int和long long三种存储整数的数据类型。不同的尺寸就意味着盒子的容量不同,对应地,不同数据类型的变量,能存储的数的范围不同。

以1个字节的char类型为例,其存储数据的方式和存储数据的范围如下图所示:

注意:计算机内部实际处理时,对于负数会采用“补码”的形式编码,char类型的负整数最小值是-128(\(-2^7\))。

同样的分析可知,都可以用来存储整数的char、int、long long三种数据类型的存储范围:

  • 1Byte(8bit)的char类型的存储范围是:\(-2^{7}\) ~ \(2^{7}-1\);
  • 4Byte(32bit)的int类型的存储范围是:\(-2^{31}\) ~ \(2^{31}-1\);
  • 8Byte(64bit)的long long类型的存储范围是:\(-2^{63}\) ~ \(2^{63}-1\)

其实char、int、long long还有对应的无符号数据类型(只能存储非负整数)unsigned char、unsigned int、unsigned long long,也就是最高位不是符号位,所有的位都是数据位:

  • 1Byte(8bit)的unsigned char类型的存储范围是:\(0\) ~ \(2^{8}-1\);
  • 4Byte(32bit)的unsigned int类型的存储范围是:\(0\) ~ \(2^{32}-1\);
  • 8Byte(64bit)的unsigned long long类型的存储范围是:\(0\) ~ \(2^{64}-1\)

常见数据类型:

数据类型说明占用空间存储范围
char整型/字符型1Byte(8bit)-128 ~ 127
unsigned char无符号整型1Byte(8bit)0~255
int
或者long
整型4Byte(32bit)\(-2^{31}\) ~ \(2^{31}-1\),大约能表示绝对值不超过
\(2.1\times10^{9}\)的整数
unsigned int
或者unsigned long
无符号整型4Byte(32bit)\(0\) ~ \(2^{32}-1\),大约能表示不超过
\(4.2\times10^{9}\)的非负整数
long long长整型8Byte(64bit)\(-2^{63}\) ~ \(2^{63}-1\),大约能表示绝对值不超过
\(9.2\times10^{18}\)的整数
unsigned long long无符号长整型8Byte(64bit)\(0\) ~ \(2^{64}-1\),大约能表示不超过
\(1.8\times10^{19}\)的非负整数
float单精度浮点型4Byte(32bit)大约指数绝对值不超过37(\(10^{-37}\)、\(10^{37}\))
6位有效数字
double双精度浮点型8Byte(64bit)大约指数绝对值不超过307(\(10^{-307}\)、\(10^{307}\))
15位有效数字

上表取值范围的“大约”表示保守估计,特别是float和double类型(char、unsigned char、int、unsigned int、long long、unsigned long long都有精确的取值范围,这几种数据类型表中给出的大约是为了给大家一个较为直观的感受)。这里也能看得出来,C++中对于浮点数,只能保证在一定的有效数字范围内是可靠的,不能保证浮点数是绝对精确的。

从表中可知存储整数的数据类型很多,但是long long的范围最大(只考虑有符号数),那是不是存储整数就都采用long long类型呢?答案是否定的。合适的才是最好的。编程时,对于要处理的数据大小范围是可以预估的,应该根据预估情况来选择合适的数据类型。计算机内存有限(竞赛时往往还会设定内存限制),如果都使用long long类型,那么在有限的总空间限制下,存储数据的数量就少了;并且一般来说,大范围的数据运算速度和小范围相比较还是有差异的。

一般情况下,使用int数据类型来存储整数,只有当int无法存储一些绝对值特别大的整数时,才使用long long数据类型;而浮点数一般都使用double,如果空间限制特别严格则使用float。至于long long都无法存储的超大整数,可以使用其它方法来模拟存储(算法部分的“高精度计算”会具体介绍)。

如果强行为变量赋值为超过其存储范围的数据,此时C++按照一定的规则处理,处理后实际存储到变量里的数据是不可靠的,这样的情况称为数据溢出,编程时要避免这样的情况。可以测试下面的程序:

#include<iostream>
using namespace std;
int main()
{
    int n = 2147483647;
    cout<<n<<endl;	//正常输出2147483647 
    n++;			  //n自加1,超出int存储范围,出现数据溢出(上溢) 
    cout<<n<<endl;	//输出-2147483648(不可靠) 
	
	int m = -2147483648;
	cout<<m<<endl;	//正常输出-2147483648
	m--;			  //n自减1,超出int存储范围,出现数据溢出(下溢)
	cout<<m<<endl;	//输出2147483647(不可靠)
    return 0;
} 

三、字符串类型string简介

除了上面介绍的常用的数值类的数据,程序中往往还要处理文本类型的数据,这些文本类型的数据更专业的称谓是“字符串”。C++中可以使用string类型来存储字符串,并且字符串还支持判断相等、加法(两个字符串拼接到一起)等运算:

#include<iostream>
using namespace std;
int main()
{
	string s1,s2;
	s1 = "Hello";
	cin>>s2;

	//这里的if语句后面会学习到
	if(s1=="Hello") cout<<"YES"<<endl;
	else cout<<"NO"<<endl;

	cout<<s1<<endl<<s2<<endl;

	cout<<s1+s2<<endl; 
	return 0;
}

后面会详细介绍字符串的使用。

四、常量的使用

回到前面计算圆的周长和面积的程序:

#include<iostream>  
using namespace std;
int main()
{
	double r,C,S;
	r = 12.56;
	C = 2*3.14*r;
	S = 3.14*r*r;
	cout<<C<<endl<<S<<endl;
    return 0;
}

程序中除了使用double类型的三个变量r,C,S外,还直接使用了一些数字:12.56、2、3.14159。像这样直接书写在程序中的量,称之为字面常量


可以使用sizeof函数来计算常量或者变量所占存储空间的字节数,运行下面的测试程序:

#include<iostream>
using namespace std;
int main()
{
	cout<<sizeof(5)<<endl;	   //输出4,整数字面常量默认是int类型 
	cout<<sizeof(5LL)<<endl;	 //输出8,整数字面常量后加上LL,则是long long类型 
	cout<<sizeof(5.0)<<endl;	 //输出8,浮点数字面常量默认是double类型 
	cout<<sizeof(5.0F)<<endl;	//输出4,浮点数字面常量末尾加上F,则是float类型 
	return 0;
} 

再来看下面一段仍然是计算圆的周长和面积的程序:

#include<iostream>  
using namespace std;
int main()
{
	const double PI = 3.14;
	double r,C,S;
	r = 12.56;
	C = 2 * PI  * r;
	S = PI * r * r;
	cout<<C<<endl<<S<<endl;
    return 0;
}

第5行 const double PI = 3.14; ,是在普通的定义double变量PI并赋初值的语句 double PI = 3.14; 前面额外添加了一个修饰词 const。这样这个PI就成为了一个符号常量。这里的符号常量与变量用法一致,只是不能再修改它的值。一般约定符号常量名全部大写。

#include<iostream>  
using namespace std;
int main()
{
	const double PI = 3.14;
	PI = 3.14159;
    return 0;
}

第6行使用赋值语句尝试修改符号常量PI的值,会出现编译错误。不能修改符号常量的值

#include<iostream>  
using namespace std;
int main()
{
	const double PI;
	PI = 3.14;
    return 0;
}

第5行定义了符号常量但是没有立即赋值,也会出现编译错误。定义符号常量的时候必须赋值

为什么要使用符号常量呢?假设我们要提高计算精度,需要将程序中的圆周率3.14修改成3.14159。使用字面常量修改如下(需要修改两处):

#include<iostream>  
using namespace std;
int main()
{
    double r,C,S;
    r = 12.56;
    C = 2*3.14*r;
    S = 3.14*r*r;
    cout<<C<<endl<<S<<endl;
    return 0;
}

#include<iostream>  
using namespace std;
int main()
{
    double r,C,S;
    r = 12.56;
    C = 2*3.14159*r;
    S = 3.14159*r*r;
    cout<<C<<endl<<S<<endl;
    return 0;
}

如果不小心只修改了一处,那么程序就存在BUG(本例代码量少,排查时很容易发现BUG,如果代码量多,并且使用圆周率的语句也多,那就不好排查了。当然,遇到这样的情况是可以使用编辑器的替换功能来批量修改避免漏改导致的BUG)。

使用符号常量的修改如下(只需要修改一处,真正的“一改全改”):

#include<iostream>  
using namespace std;
int main()
{
	const double PI = 3.14;
	double r,C,S;
	r = 12.56;
	C = 2 * PI  * r;
	S = PI * r * r;
	cout<<C<<endl<<S<<endl;
    return 0;
}
#include<iostream>  
using namespace std;
int main()
{
	const double PI = 3.14159;
	double r,C,S;
	r = 12.56;
	C = 2 * PI  * r;
	S = PI * r * r;
	cout<<C<<endl<<S<<endl;
    return 0;
}

此外,还可以通过C风格的#define语句来定义符号常量,注意语法与const的不同:

#include<iostream>  
using namespace std;
int main()
{
	#define PI 3.14
	double r,C,S;
    r = 12.56;
    C = 2 * PI  * r;
    S = PI * r * r;
    cout<<C<<endl<<S<<endl;
    return 0;
} 

#define语句定义符号常量语法格式:
#define 常量名 常量值。注意#define语句没有体现常量的数据类型,也没有以分号;结束

#include<iostream>
#define PI 3.14  
using namespace std;
int main()
{
	double r,C,S;
    r = 12.56;
    C = 2 * PI  * r;
    S = PI * r * r;
    cout<<C<<endl<<S<<endl;
    return 0;
} 

习惯性地将#define语句写到程序的头部

#define其实是宏定义,编译时宏定义之后的代码中所有的 PI 会被简单粗暴地全部替换成3.14,所以这里不需要指定数据类型。

cin输入数据

tangyj626阅读(810)

编写程序解决问题时,往往需要输入数据。计算的数据由使用者输入,可以大大增强程序的实用性。

一、输入数据

编写程序解决问题时,往往需要输入数据。例如前面示例程序计算圆的周长和面积,计算的是半径为12.56的圆,如果要计算其他半径的圆,需要修改源程序并再次编译运行,这样很不方便并且实用价值低。如果设计程序时,由使用者输入圆的半径,程序根据输入的半径计算圆的周长和面积,那么程序的实用性大大增强。

首先来看一个简单的,计算输入的整数的立方。这里通过cin语句将运行时输入的数据存储到变量中。

#include<iostream>
using namespace std;
int main()
{
    int n; 
    //使用cin语句输入一个整数存储到变量n中,cin和变量间用两个右尖括号(>>)隔开
    cin>>n;
    cout<<n*n*n; 
    return 0;
} 

运行后会发现命令行窗口没有出现任何结果,只是光标一直在不停的闪烁:

这个时候需要我们输入数据,本程序只需要输入一个整数,然后按下回车键,就能看到程序输出结果:


初学者还容易犯的一个错误就是将cin和cout后面的尖括号写错。cin后面是 >> ,cout后面是 << ,不要弄混了。可以这样来帮助记忆:

  • cin是输入,将cin“想象成输入的数据”(其实就是输入流),两个右尖括号>>就像右箭头一样指示了左边输入的数据存储到右边变量中;
  • cout是输出,将cout“想象输出到的位置”(其实就是输出流),两个左尖括号<<就像左箭头一样指示了是将右边的计算表达式结果输出到左边“要输出的位置”。

再来测试一个简单的程序,计算输入的两个整数的和:

#include<iostream>
using namespace std;
int main()
{
    int a,b; 
    cin>>a>>b;        //使用cin语句连续输入2个整数依次存储到变量a、b中
    cout<<a+b<<endl;
    cout<<a<<"+"<<b<<"="<<a+b;
    return 0;
} 

注意体会cin语句输入多个数据以及cout语句输出多项信息的使用方法。cin语句输入多个数据存储到多个变量,输入的多个数据按照cin语句中变量出现的顺序依次存储。

运行本程序,同样会发现光标一直在不停地闪烁,在等待输入数据。本程序需要输入两个整数,输入时可以先输入一个整数并回车,然后再输入另外一个整数并回车;也可以一次性输入两个整数,不过这两个整数间用空格隔开,最后回车。

再测试下面的程序,体会和上一段程序的不同:

#include<iostream>
using namespace std;
int main()
{
    int a,b,c;
    cin>>a>>b;      //使用cin语句连续输入2个整数存储到变量a、b中
    c = a+b;        //计算a+b的值,保存到变量c中
    cout<<c<<endl;
    cout<<a<<"+"<<b<<"="<<c;
    return 0;
} 

再来看一个经典问题:输入两个整数a和b,计算并输出 \(a ÷ b\) 的结果:

#include<iostream>
using namespace std;
int main()
{
    int a,b;
    cin>>a>>b;
    cout<<a/b<<endl;
    return 0;
}

不少初学者不经意就会写出类似左边的代码,测试一下,输入5和2,发现输出结果是2,并不是预期的2.5!

原因在哪里呢?a/b!a、b都是int整数,结果也是int整数!好在我们可以通过测试运行发现程序存在的问题,并及时修改!

#include<iostream>
using namespace std;
int main()
{
    int a,b;
    cin>>a>>b;
    double c = a/b;
    cout<<c<<endl;
    return 0;
} 

再看看左边的代码,感觉没有什么问题,测试一下,输入5和2,发现输出结果是2,也不是预期的2.5!

原因在哪里呢?double c = a/b;仍然先计算a/b!a、b都是int整数,结果也是int整数2,赋值给double类型变量c,c的值是2(小数部分为0)!

#include<iostream>
using namespace std;
int main()
{
    int a,b;
    cin>>a>>b;
    cout<<1.0 * a / b<<endl;
    return 0;
}

上面的程序能够很好地解决问题。1.0 * a / b,先计算1.0 * a,浮点数乘以整数,通过前面一小节数据类型自动转换的内容可知,结果是浮点数;再除以整数b,结果仍然是浮点数。

#include<iostream>
using namespace std;
int main()
{
    int a,b;
    cin>>a>>b;
    cout<<a / b * 1.0<<endl;
    return 0;
}

上面的代码仍然有问题!a / b * 1.0,先计算a / b,整数除以整数,结果是整数,即使再乘以1.0,输出的结果肯定是整数形式(小数部分为0输出时是整数形式)!

#include<iostream>
using namespace std;
int main()
{
    double a,b;
    cin>>a>>b;
    cout<<a/b<<endl;
    return 0;
}

左边的程序也能能够很好地解决问题,但不建议这样做!虽然题目里指出输入的是两个整数,但是整数其实是特殊的浮点数,所以可以用两个double类型的变量来存储输入的整数。

由于浮点数只能保证在一定位数的有效数字范围内是可靠的,不能保证是精确值,所以编程时,能够不用浮点数尽量不用


最后来看计算圆的周长和面积,圆的半径用cin输入:

#include<iostream>
using namespace std;
int main()
{
    const double PI = 3.14159;
    double r;      //为了增强程序的实用性,应该使用double类型来存储输入的半径
    cin>>r;
    cout<<2 * PI  * r<<endl;
    cout<<PI  * r * r<<endl;
    return 0;
} 

最后试一试下面的程序,并和前一段程序比较:

#include<iostream>
using namespace std;
int main()
{
    const double PI = 3.14159;
    double r,C,S;         //声明三个double类型(小数/浮点数)的变量r、C、S
    cin>>r;
    C = 2 * PI  * r;      //计算圆的周长并存储到变量C中
    S = PI  * r * r;      //计算圆的面积并存储到变量S中
    cout<<C<<endl<<S<<endl;
    return 0;
} 

后一段程序是典型的“输入数据——计算结果——输出结果”的结构。

二、输入输出数据小结

1.输入数据

  1. 使用cin语句输入数据,数据存储到变量中;
  1. cin与变量名间用 >> 隔开,格式为:cin>>变量名;
int a,b,c;
cin>>a;
cin>>b;
cin>>c;
  1. cin输入可以一次性输入多个数据依次存储到多个变量中,cin与变量名之间,变量名与变量名间用 >> 隔开,格式为:cin>>变量名1>>变量名2>>变量名3...;
int a,b,c;
cin>>a>>b>>c;
  1. 运行程序时,输入完数据后按回车,才能将数据真正输入到程序处理;多个数据间可以用空格或者回车隔开。

2.输出信息

  1. 使用cout语句输出信息,输出的信息可以是一句话(必须用英文引号括起来)、数字(整数、小数/浮点数)、变量、或者计算表达式:
cout<<"Hello World";
cout<<12345;
cout<<3.14159;
cout<<a;
cout<<2*3.14159*r;
cout<<a*(b+c);
  1. 可以只输出一项信息,也可以一次性输出多项信息。cout与输出内容之间,各内容间用 << 隔开:
cout<<1<<"+"<<2<<"="<<1+2;
cout<<a<<"+"<<b<<"="<<a+b;
  1. 输出一个特殊的内容:endl 可以实现换行效果:
cout<<a<<"+"<<b<<"="<<a+b<<endl;

数据类型转换

tangyj626阅读(948)

C++中数据,不管是常量还是变量都有对应的数据类型,在计算过程中数据的类型可以发生变化。数据类型的变化有些是自动完成的(自动转换),此外还可以使用强制的方法实现数据类型转换(强制转换数据类型)。

一、数据类型自动转换

来看前面尝试过的程序:

#include<iostream>  
using namespace std;
int main()
{
    // 整数/整数    
    cout<<5/2<<endl; 
    return 0;
}

运行输出结果是2。整数/整数结果是整数。

#include<iostream>  
using namespace std;
int main()
{
    // 浮点数/整数   
    cout<<5.0/2<<endl; 
    return 0;
}

运行输出结果是2.5。除法的被除数是浮点数,除数是整数,结果是浮点数。

通过上面的例子可以发现,运算时如果两个数据的类型不一致,会自动按照较大范围的数据类型出结果,也就是结果的数据范围是两个数的数据类型中较大范围的那一个(上面例子5.0是double类型、2是int类型,double类型存储范围大于int,所以结果是double类型)。

如果赋值语句赋值符号左右的数据类型不一致,也会出现自动类型转换:

#include<iostream>  
using namespace std;
int main()
{
	double num = 5;
	cout<<num/2;
    return 0;
} 

输出2.5。第5行 double num = 5; 赋值符号右边是int型常量5,左边是double类型变量,赋值后num的值是5.0(其实直接输出num仍然是5,这里说是5.0强调是一个浮点数),那么第6行输出num/2,num是浮点数,所以结果是浮点数2.5

#include<iostream>  
using namespace std;
int main()
{
	int num = 3.14*5;
	cout<<num;
    return 0;
}

输出15。第5行 int num = 3.14*5;,赋值符号右边计算结果是浮点数15.7,但是左边是int类型变量,赋值后num的值是15(只保留了整数部分,出现了精度丢失)

先判断下面程序的输出结果,然后再编程运行检验判断是否正确。如果判断有误,请结合上面的内容再仔细分析程序的执行情况:

#include<iostream>
using namespace std;
int main()
{
    double d = 5/2;
	cout<<d<<endl; 
    return 0;
} 

来看一个问题, 输入两个整数 \(a,b\),计算并输出 \(a \div b\) 的结果。 我们先尝试编写并运行下面三段程序:

#include<iostream>
using namespace std;
int main()
{
	int a,b;
	cin>>a>>b;
	cout<<a/b;
	return 0;
}

变量a和b都是int类型,整数/整数,结果是除法的商。输入5 2,会输出2

#include<iostream>
using namespace std;
int main()
{
	int a,b;
	cin>>a>>b;
	cout<<1.0*a/b;
	return 0;
}

1.0*a/b,会先计算1.0*a,浮点数乘以整数,结果是浮点数,再除以整数,最后结果是浮点数。 输入5 2,会输出2.5

#include<iostream>
using namespace std;
int main()
{
	int a,b;
	cin>>a>>b;
	cout<<a/b*1.0;
	return 0;
}

a/b*1.0,会先计算a/b,这里是整数/整数,结果是除法的商,再乘以1.0,最后结果是浮点数(小数部分为0)。 输入5 2,会输出2(cout输出小数部分为0的浮点数时不会输出小数部分)。

再来看一个问题,输入两个整数 \(a,b\),计算并输出 \(a^b\) 的结果。这里可以使用 cmath 库文件中的 pow 函数来计算幂次方:

#include<iostream>
#include<cmath>
using namespace std;
int main()
{
    int a,b;
    cin>>a>>b;
    cout<<pow(a,b)<<endl;
    return 0;
} 

下面来测试一下程序:

1.输入\(2\;10\),可以正常输出1024

2. 输入\(35\;12\),输出的是科学计数法形式的结果:3.37922e+018

虽然这里输入的 \(a,b\) 都是整数,数学里计算 \(a^b\) 的结果是整数,但是 pow 函数的结果是小数(可以认为这里 pow 函数的计算结果是小数部分为0的小数),用 cout 直接输出很大的小数会以科学计数法的形式输出,所以出现了上面输出科学计数法形式的情况。考虑到问题里 \(a,b\) 都是整数,那么 \(a^b\) 也是整数,这里我们可以将 pow 的计算结果赋值给一个int(最好是long long)类型的变量,这样就把小数结果自动转换成整数了:

#include<iostream>
#include<cmath>
using namespace std;
int main()
{
    int a,b;
    cin>>a>>b;
    long long ans = pow(a,b);
    cout<<ans<<endl;
    return 0;
} 

但是要注意:这里 pow 的计算结果是double类型,如果结果不太大,可以直接赋值给long long类型的变量;如果计算结果很大(超过了long long的存储范围),直接赋值给long long类型的变量会出现数据溢出情况,结果就不可靠了。例如上面的程序,如果输入\(35\;13\),输出的结果是-9223372036854775808,很明显,这里出现了数据溢出。

二、强制转换数据类型

在必要时,还可以使用强制类型转换来转换数据类型:

#include<iostream>  
using namespace std;
int main()
{
	//注意强制类型转换的语法
	cout<<(double)5/2;
    return 0;
} 

计算(double)5/2,首先处理被除数(double)5,这里5是int常量,前面加上(double)表示将其强制转换成double类型。结果输出2.5

#include<iostream>  
using namespace std;
int main()
{
	double num = 5;
	cout<<(int)num/2;
    return 0;
} 

计算(int)num/2,首先处理被除数(int)num,这里num是double变量,前面加上(int)表示将其强制转换成int类型。结果输出2

从上面的例子可以看出,强制类型转换的优先级高于算术运算:(int)num/2,首先是将num强制转换成int类型,再做除法;而不是先做除法,再将结果强制类型转换成int类型。我们其实可以设计一个程序来验证上面的结论:

#include<iostream>  
using namespace std;
int main()
{
	double num = 5;
	cout<<(int)num/2.0;
    return 0;
} 

如果上面结论成立,也就是强制类型转换的优先级高于算术运算,那么先强制类型转换再做除法,结果应该是2.5;

如果上面结论不成立,那么先计算除法,再强制类型转换,结果应该是2;

实际运行结果是2.5。通过这个试验验证了“强制类型转换的优先级高于算术运算”的结论,看吧——实践出真知!一定要活学活用!

还是上面计算 \(a^b\) (\(a,b\) 都是整数)的问题,也可以使用强制类型转换来将 pow 函数的计算结果(double类型小数)转换成整数。不过也要注意的是,强制类型转换也可能会出现数据溢出的情况:

#include<iostream>
#include<cmath>
using namespace std;
int main()
{
    int a,b;
    cin>>a>>b;
    cout<<(long long)pow(a,b)<<endl;
    return 0;
} 

三、类型转换要注意避免数据溢出

这里总结一下,不管是通过赋值语句的自动类型转换,还是强制类型转换,都要考虑将一个能存储更大范围的数据类型转换成能存储较小范围的数据类型时,可能会出现数据溢出的情况(例如将long long转换成int,将double转换成intlong long)。反之,将一个能存储较小范围的数据类型转换成能存储更大范围的数据类型时,一般不用担心数据溢出问题(例如将int转换成long long,将int转换成double)。解决问题时,只有确保不会出现数据溢出,才能进行数据类型转换。

#include<iostream>
using namespace std;
int main()
{
    long long m = 123;
    //long long类型赋值给int类型
	//因为long long类型变量的值仍在int存储范围内,不会出现数据溢出
    cout<<(int)m<<endl;

    int n = m; 
    cout<<n<<endl;
    return 0;
}
#include<iostream>
using namespace std;
int main()
{
    long long m = 2147483648;
    //long long类型赋值给int类型
	//因为long long类型变量的值超出int存储范围内,会出现数据溢出
    cout<<(int)m<<endl;

    int n = m; 
    cout<<n<<endl;
    return 0;
}

char字符类型

tangyj626阅读(1065)

char类型不仅仅可以存储-128~127范围内的整数,更普遍的用法是存储一个字符。

一、字符串、字符、ASCII表

前面我们输出"Hello World",称"Hello World"是一句话,或者是一段文本,更专业的称谓是字符串。字符串就是有顺序的若干符号“串”在一起组成的文本,字符串中的每个符号就是字符。字符串"Hello World"中的字符依次是'H''e''l''l''o'' '(空格)、'W''o''r''l''d'。字符的书写方法是用英文单引号将符号括起来。

char类型不仅仅可以存储-128~127范围内的整数,更普遍的用法是存储一个字符,包括大小写英文字母、数字符号和英文标点符号以及一些特殊意义的字符(不包括任何汉字),这些字符和0~127的整数一一对应。

这个表展示了字符与十进制数一一对应的关系,称之为ASCII表(American Standard Code for Information Interchange,美国信息交换标准代码)。第0个到第31个字符(也是十进制数0~31对应的字符)是控制字符(不可见字符),从第32个字符开始,每个整数对应一个打印字符(英文标点符号、数字、大小写字母等)。

仔细分析ASCII表,会发现字符与整数对应关系有以下特点:

  1. 大写字母'A'与十进制数65对应,更普遍地,大写字母'A'~'Z'与十进制数65~90连续一一对应
  2. 小写字母'a'与十进制数97对应,更普遍地,大写字母'a'~'z'与十进制数97~122连续一一对应
  3. 数字字符'0'与十进制数48对应,更普遍地,数字字符'0'~'9'与十进制数48~57连续一一对应。需要注意的数字字符'0'与整数0不同,'0'是字符,0是int整数。例如字符串"result is 0",最后一个字符是数字字符'0'

    现在应该清楚'A'"A"A的区别了吧?'A'是字符,"A"是只包含一个字符的字符串、A可以理解为变量。

    同样的,'1'是字符,"1"是只包含一个字符的字符串、1是整数常量。

    二、char字符的本质——char字符与整数的关系

    char类型的本质就是一个-128~127范围的整数,只不过char类型0~127范围内的整数与ASCII表中的字符一一对应。正是这个特点,char字符与0~127范围内的整数可以混合使用(相互赋值),甚至相互运算。参见下面测试程序:

    #include<iostream>
    using namespace std;
    int main()
    {
        char ch1 = 'a';
    	char ch2 = 'b';
        cout<<ch1<<" "<<ch2<<endl; 
        cout<<ch2-ch1<<endl;      //两个char运算,实际是对应的整数参加运算
        return 0;
    } 
    #include<iostream> 
    using namespace std;
    int main()
    {
    	//int整数65赋值给char变量ch,赋值后变量ch就是十进制数65对应的字符'A'
        char ch = 65; 
        cout<<ch<<endl;		//ch是字符'A',但是输出时只会输出A,单引号不会输出
    	
    	//char+int,类型自动转换后结果是int,char变量ch对应十进制数65参加计算,输出66
        cout<<ch+1<<endl;
    	
    	//先计算ch+1,结果是66,赋值给char变量ch,ch变成十进制数66对应的字符'B'  
        ch = ch+1;
        cout<<ch<<endl;		//输出B
        return 0;
    } 
    #include<iostream> 
    using namespace std;
    int main()
    {
    	//char字符'A'赋值给int变量,赋值后变量n就是字符'A'对应的十进制数65
        int n = 'A'; 
        cout<<n<<endl;		 //输出65
    	cout<<(char)n<<endl;   //输出A(将int强制转换成char) 
    	
    	//char-int,类型自动转换后结果是int,'a'对应十进制数97参加计算,输出32
        cout<<'a'-n<<endl;
    	  
        n = n+1;
        cout<<(char)n<<endl;   //输出B 
        return 0;
    }

    三、char数据程序示例

    例1:英文字母。大家知道有26个英文字母,其中A是第一个字母,编写程序求出:

    1. Q是字母表的第几个字母?
    2. 第20个字母是哪一个?

    思路:char字符'A'~'Z'对应的整数是连续的,而且char类型的变量支持与整数直接进行数学计算。

    #include<iostream> 
    using namespace std;
    int main()
    {
    	//'A'是第1个字母,'Q'应该是第('Q'-'A'+1)个字母 
    	int ans1 = 'Q'-'A'+1;
    	
    	//第n个字母是'A'+n-1,赋值给char类型变量 
    	char ans2 = 'A'+20-1;
    	cout<<ans1<<endl<<ans2<<endl; 
        return 0;
    }

    例2:将输入的大写字母转换成对应的小写字母。

    • 输入格式:1个大写字母(竞赛编程时,不需要考虑运行时输入不满足题目输入格式的情况,例如这里不需要考虑万一输入的字符不是大写字母这样的情况;当然,如果是编写实用软件,那就需要考虑这些情况,例如发现输入的字符不是大写字母时给出错误提示)
    • 输出格式:1个字符,也就是输入的大写字母对应的小写字母

    思路:大写字母'A'~'Z'对应的整数是连续的,小写字母'a'~'z'对应的整数也是连续的,并且小写字母对应的整数大于大写字母。

    通过上表分析可知小写字母比对应大写字母大32(也就是'a'-'A'
    #include<iostream> 
    using namespace std;
    int main()
    {
    	char ch1,ch2;
    	cin>>ch1;
    	ch2 = ch1+'a'-'A';
    	cout<<ch2<<endl;
        return 0;
    }
    #include<iostream> 
    using namespace std;
    int main()
    {
    	char ch1,ch2;
    	ch1 = getchar();
    	ch2 = ch1+'a'-'A';
    	cout<<ch2<<endl;
        return 0;
    }

    除了使用cin输入char字符外,还可以通过getchar()函数输入char字符赋值给char变量。

    试一试:编写程序将输入的小写字母转换成对应的大写字母。

    例3:使用转义字符'\n'实现换行效果。

    观察ASCII表,会发现有一些特殊的转义字符,例如'\n''\r''\t'等。转义字符特殊的原因是书写的时候“明明”是两个符号(以\开头再加另外一个符号),其实却是一个字符。如果要表示字符\,需要使用'\\'。其中常用的'\n'是换行符,'\t'是制表符,通过一段程序来测试'\n'的用途:

    #include<iostream> 
    using namespace std;
    int main()
    {
        //字符串"Hello\nWorld\n"中出现了两次换行符'\n'
        cout<<"Hello\nWorld\n";
    
        //字符串"\n"中有且仅有一个字符——换行符'\n'
        cout<<123<<"\n"<<456<<endl;
    
        //直接输出换行符'\n'
        cout<<123<<'\n'<<456<<endl;
        return 0;
    }

    程序运行结果如下:

    Hello
    World
    123
    456
    123
    456

    可知不管是单个'\n'还是字符串中有字符'\n',输出时'\n'都会产生换行的效果。

    试一试:编写并运行下面程序,观察运行效果(确保可以听到计算机发出的声音):

    #include<iostream>
    using namespace std;
    int main()
    {
        cout<<"Listen...\a";    //输出字符'\a'会发出声音
        return 0;
    } 

    scanf输入与printf输出

    tangyj626阅读(1431)

    输入输出除了使用iostream头文件中的cin和cout外,还可以使用cstdio头文件中的scanf和printf函数。两者各有优缺点,scanf和printf在适当的场合使用可能收到意想不到的效果。

    scanf和printf是C语言风格的输入输出,scanf(scan+format)和printf(print+format)表示的是格式化(format)输入输出。这两个函数用来输入输出数据时,最大的特点就是要使用表示格式占位符实现灵活多变的格式化。

    一、printf格式化输出

    回到之前的浮点数保留小数位数的处理方法,使用cout处理很复杂:要引入新的头文件iomanip,使用cout<<fixed<<setprecision(5);这样难以记忆的语句!

    #include<iostream> 
    #include<iomanip>
    using namespace std;
    int main()
    {    
        cout<<fixed<<setprecision(5);    
        cout<<12.3456*34.5678<<endl;
        return 0;
    } 

    下面是使用printf格式化输出的方法,因为不再需要cout,所以不需要引入头文件iostream,不需要使用std命名空间,但是需要引入头文件cstdio:

    #include<cstdio>
    int main()
    {
        printf("%.5f",12.3456*34.5678);  //%.5f是占位符,表示保留5位小数的float数据
        //上面的printf输出:1个占位符,1个要输出的数据(这里是一个计算表达式)
        return 0;
    } 

    经过对比发现使用printf输出指定小数位数的浮点数比起cout简单得多。再通过几个例子来分析printf格式化输出函数的使用方法:

    #include<cstdio> 
    int main()
    {
    	char ch = 'A';
    	printf("%c %d",ch,ch);    //%c和%d都是占位符,分别表示char数据和int数据
    	//上面的printf输出:2个占位符,2个要输出的数据(这里都是变量ch)
    	//程序会输出A 65(占位符中间的空格输出时原样出现)
        return 0;
    } 
    #include<cstdio> 
    int main()
    {
    	int a = 123,b = 456;     //定义两个int型变量a、b并都赋初值
    	printf("%d+%d=%d",a,b,a+b);  //%d是占位符,表示int数据
    	//上面的printf输出:3个占位符(都是%d),3个要输出的数据(int类型变量a、b和计算表达式a+b)
    	//程序会输出123+456=579:变量a的值出现在第一个%d的位置,变量b的值出现在第二个%d的位置,a+b的计算结果出现在第三个%d的位置,其它占位符之外的字符('+'、'=')原样输出。
        return 0;
    } 

    通过上面的测试,可以小结printf函数的使用方法:printf("包含占位符的字符串",变量/表达式列表)

    1. 第二部分变量/表达式列表是用,分隔开的若干个变量或者表达式;
    2. 第一部分包含占位符的字符串中占位符的数量与第二部分变量或者表达式的数量一致;
    3. 输出时包含占位符的字符串中的占位符会被第二部分对应位置的变量或者表达式计算结果按照占位符指定的格式依次替代(占位符“占位”的由来);
    4. 包含占位符的字符串中占位符之外的字符原样输出;
    5. 第一部分"包含占位符的字符串"中可以没有任何占位符,此时没有第二部分:printf("Hello World");

    二、常见输入输出占位符

    占位符说明
    \(\%d\)用于\(int\)类型
    \(\%nd\) (\(n\)是正整数,例如\(\%4d\))至少\(n\)位的\(int\)类型,如果不足\(n\)位,前面补空格直到补齐\(n\)位
    \(\%-nd\) (\(n\)是正整数,例如\(\%-4d\))至少\(n\)位的\(int\)类型,如果不足\(n\)位,后面补空格直到补齐\(n\)位
    \(\%0nd\) (\(n\)是正整数,例如\(\%04d\))至少\(n\)位的\(int\)类型,如果不足\(n\)位,前面补0直到补齐\(n\)位
    \(\%lld\)用于\(long\ long\)类型
    \(\%f\)用于\(float\)类型,输出时默认6位小数
    \(\%lf\)用于输入\(double\)类型;\(double\)类型输出时建议使用\(\%f\)
    \(\%.nf\) (\(n\)是正整数,例如\(\%.2f\))用于输出\(n\)位小数的\(float\)类型,会自动四舍五入
    \(\%m.nf\) (\(m\)、\(n\)是正整数,例如\(\%5.2f\))用于输出\(n\)位小数的\(float\)类型,算上小数点至少占\(m\)位
    \(\%c\)用于\(char\)类型
    \(\%s\)用于字符串
    #include<cstdio>
    int main()
    {
    	printf("Hello World\n");
    	printf("%5d\n",5);
    	printf("%2d\n",12345);
    	printf("%-5dend\n",5);
    	printf("%05d\n",5);
    	printf("%+05d\n",5);
    	printf("%.4f\n",3.14159);
    	printf("%8.4f\n",3.14159);
    	printf("%-8.4fend\n",3.14159);
    	printf("%08.4f\n",3.14159);
    	printf("%c %d\n",'a','a');
    	printf("%s","Hello World");  //后续会学习字符串
        return 0;
    } 

    运行结果如下:

    Hello World
        5
    12345
    5    end
    00005
    +0005
    3.1416
      3.1416
    3.1416  end
    003.1416
    a 97
    Hello World

    三、scanf格式化输入

    scanf格式化输入用来将指定格式的若干数据输入到对应数量的变量中,和printf相似,scanf的使用方法:scanf("包含占位符的字符串",变量列表); 不过要注意的是第二部分是若干用,隔开的变量列表,每个变量名前要添加一个特殊的&运算符号。下面通过几个测试程序来加以说明scanf的使用:

    #include<cstdio>
    int main()
    {
        int n;
        scanf("%d",&n);		//变量名n前必须有&运算符
    	printf("%d",n*n); 
        return 0;
    } 
    #include<cstdio>
    int main()
    {
        int a,b;
        scanf("%d%d",&a,&b);	//占位符%d%d中间也可以加空格:scanf("%d %d",&a,&b);
    	printf("%d+%d=%d",a,b,a+b); 
        return 0;
    } 
    #include<cstdio>
    int main()
    {
    	const double PI = 3.14159;
        double r,C,S;
    	scanf("%lf",&r);
    	C = 2 * PI * r;
    	S = PI * r * r;
    	printf("R=%f\nC=%.5f\nS=%.5f",r,C,S);
        return 0;
    } 

    cin/cout,scanf/printf各有优势。相比较cout,printf更容易通过占位符实现丰富多样的格式化。一般来说,使用scanf读入同样的数据的速度要快于cin,数据量较大(例如要读入百万级别的数据)时差距会相当明显。但大多数情况下程序输入的数据不会很多,cin和scanf输入数据耗时差异不明显。而使用cin、cout输入输出时不用考虑不同数据类型的占位符,相对方便。

    此外,不建议在程序里混合使用cin/cout与scanf/printf。也可以通过关闭同步的方式加快cin的读入速度,不过此时不能再使用scanf输入数据:

    #include<iostream>
    using namespace std;
    int main(){
        ios::sync_with_stdio(false);
    	cin.tie(0);
    	cout.tie(0);
    	//其它程序代码 
        return 0;
    }

    四、输入char注意事项

    通过前面的学习我们已经知道,输入数据时多个数据间可以用若干空格或者回车分隔开(空格和回车是输入数据的分隔符)。但是如果要用scanf来输入本来就包括字符的数据的时候,要格外小心!

    #include<iostream>
    using namespace std;
    int main()
    {
        char c1,c2;
        cin>>c1>>c2;
        cout<<c1<<" "<<c2<<endl;
        return 0;
    }

    使用cin输入char字符时,仍然满足上面输入规则,输入的两个char字符间可以有若干的空格、回车。例如这里输入字符a和b,中间随意输入若干的空格回车,ch2变量都能正常接收到分隔符后的字符,运行结果如下:

    a b

    #include<cstdio>
    int main()
    {
        char c1,c2;
    
        scanf("%c%c",&c1,&c2);
    
        printf("%c %c\n",c1,c2);
        printf("%d %d",c1,c2);
        return 0;
    }

    使用scanf输入char时,和cin有区别,这里使用\(\%c\%c\)作为占位符,输入时两个char间不能有任何分隔符,否则输入到c2中的字符就变成中间的分隔符。使用getchar()函数输入字符与这里的情况相同。

    左边程序,如果输入ab,运行结果如下:

    a b
    97 98

    可知此时ch2接收到了字符'b'


    如果输入:a b,运行结果如下:

    a
    97 32

    此时,ch2接收到的字符是a b中间的空格。

    五、scanf和printf的特殊应用

    #include<cstdio>
    int main()
    {
        int a,b,c;
        scanf("%1d%1d%1d",&a,&b,&c);
        printf("%d %d %d",a,b,c);
        return 0;
    } 

    scanf输入时使用了3个特殊的占位符\(\%1d\),\(\%nd\)用于输出时表示至少占n位。这里用于输入,那么表示输入的整数占n位。输入一个三位数123,运行结果如下所示:

    1 2 3
    #include<cstdio>
    int main()
    {
        double num;  //要处理的浮点数 
        int n;       //输出时保留的位数
    	//输入要处理的浮点数和保留的位数 
        scanf("%lf%d",&num,&n);
        printf("%.*f",n,num);
        return 0;
    }

    输出时使用了\(\%.*f\),表示保留的小数位数值由后面的输出列表对应位置的值决定。

    输入数据:
    3.14159 3
    运行结果:
    3.142

    算法

    tangyj626阅读(510)

    算法(Algorithm)是解决问题的具体方法,是解题方案的准确而完整的描述。简单地说,算法就是准确完整地阐述解决问题的方案,明确给出解决问题的具体步骤(第一步干什么,第二步干什么,……)。

    “算法”即演算法,中文名称出自《周髀算经》;而英文名称Algorithm 来自于9世纪波斯数学家al-Khwarizmi,al-Khwarizmi在数学上提出了算法这个概念。“算法”原为"algorism",意思是阿拉伯数字的运算法则,在18世纪演变为"algorithm"。欧几里得算法被人们认为是史上第一个算法。因为"well-defined procedure"(无歧义的、不会导致矛盾的、符合其应满足的所有要求的程序)缺少数学上精确的定义,19世纪和20世纪早期的数学家、逻辑学家在定义算法上出现了困难。20世纪的英国数学家图灵提出了著名的图灵论题,并提出一种假想的计算机的抽象模型,这个模型被称为图灵机。图灵机的出现解决了算法定义的难题,图灵的思想对算法的发展起到了重要作用。

    算法(Algorithm)是解决问题的具体方法,是解题方案的准确而完整的描述。简单地说,算法就是准确完整地阐述解决问题的方案,明确给出要解决问题的具体步骤(第一步干什么,第二步干什么,……)。算法是程序的灵魂,设计算法是程序设计的核心。我们说编程的过程是对逻辑思维的训练和强化,其实这主要是通过算法来体现的。

    一、算法的特征

    一个算法应该具有以下五个重要的特征:

    1. 有穷性。算法的有穷性是指算法必须能在执行有限个步骤之后终止;
    2. 确切性。算法的每一步骤必须有确切的定义,不能有歧义;
    3. 有输入。一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
    4. 有输出。一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
    5. 可行性。算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个计算步骤都可以在有限时间内完成。

    二、算法的要素

    算法的要素指的是算法内容的组成。算法应该由以下两个部分组成:

    1. 数据对象的运算和操作。计算机可以执行的基本操作是以指令的形式描述的。一个计算机系统能执行的所有指令的集合,成为该计算机系统的指令系统。一个计算机的基本运算和操作有如下四类:
      1. 算术运算:加减乘除等运算
      2. 逻辑运算:或、且、非等运算
      3. 关系运算:大于、小于、等于、不等于等运算
      4. 数据传输:输入、输出、赋值等运算
    2. 算法的控制结构。一个算法的功能结构不仅取决于所选用的操作,还与各操作之间的执行顺序有关。

    三、算法的评定

    同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。一个算法的评价主要从时间复杂度空间复杂度来考虑。

    1.时间复杂度

    算法的时间复杂度是指执行算法所需要的计算工作量。一般来说,计算机算法是问题规模\(n\)的函数\(f(n)\),算法的时间复杂度也因此记做:\(T(n)=Ο(f(n))\)

    一般地,问题的规模\(n\)越大,算法执行的时间的增长率与\(f(n)\)的增长率正相关。

    2.空间复杂度

    算法的空间复杂度是指算法需要消耗的内存空间。其计算和表示方法与时间复杂度类似,一般都用复杂度的渐近性来表示。同时间复杂度相比,空间复杂度的分析要简单得多。

    四、算法的分类

    算法可大致分为基本算法、数据结构的算法、数论与代数算法、计算几何的算法、图论的算法、动态规划以及数值分析、加密算法、排序算法、检索算法、随机化算法、并行算法,厄米变形模型,随机森林算法等。

    常用的算法包括:递推法、递归法、穷举法、贪心算法、动态规划法、迭代法、分支界限法、回溯法等。

    五、算法的描述

    描述算法的方法有多种,常用的有自然语言伪代码流程图PAD图等,其中使用最普遍的是流程图。

    以前面计算圆的周长和面积为例,简单的用自然语言描述的算法可以是:

    1. 输入圆的半径
    2. 利用圆周长公式计算并输出圆的周长
    3. 利用圆面积公式计算并输出圆的面积

    更贴合编程的自然语言描述:

    1. 输入圆的半径\(R\)
    2. 计算圆的周长\(C=2 \times \pi \times R\)
    3. 计算圆的面积\(S= \pi \times R^2\)
    4. 输出周长\(C\)、面积\(S\)

    伪代码描述(\(Input\)输入,\(Output\)输出,\(←\)如同赋值符号一样表示赋值为):

    1. \(Input \ R\)
    2. \(C ← 2 * \pi * R\)
    3. \(S ← \pi * R * R\)
    4. \(Output \ C,S\)

    流程图描述如下:

    流程图常用的符号如下表所示:

    【思考】

    \(n\)(\(1 \le n \le 100\))位选手要参加100米赛跑,标准田径场有8条跑道均可安排选手,如何安排(安排几组,每组每条跑道多少人)可以让每组人数均匀?可以从下图分析每组人数均匀的具体要求。

    每组人数不均匀(\(n=27\))
    每组人数均匀(\(n=27\))

    顺序结构

    tangyj626阅读(658)

    顺序结构的程序设计是最简单的,只要按照解决问题的顺序写出相应的语句就行,它的执行顺序是自上而下,依次执行。顺序结构也是最基础的,其他复杂的程序结构中都有顺序结构的影子。

    一、顺序结构

    顺序结构是程序设计最简单、最基础的结构,顺序结构的特点是按照解决问题的算法的详细步骤书写对应的语句,执行的时候是自上而下,依次执行。

    还是以计算圆的周长和面积为例:

    #include<iostream>
    using namespace std;
    int main()
    {
        const double PI = 3.14159;
        double r,C,S;
        cin>>r;
        C = 2 * PI  * r; 
        S = PI  * r * r;
        cout<<C<<endl<<S<<endl;
        return 0;
    } 

    可以看出右边的代码就是将左边的算法的步骤一一依次转换成程序语句,而程序运行的时候也是按照书写的顺序从上到下依次执行语句——这就是典型的顺序结构!这个算法是按照“输入数据→计算结果→输出结果”的顺序组织的。

    二、赋值语句

    通过前面的学习我们已经知道,通过赋值语句可以为变量赋新值。赋值语句的格式是:\(变量名 = 表达式;\)。赋值符号 \( = \) 的作用是将右边表达式的计算结果赋值给左边的变量。赋值后变量的旧值被新值覆盖。

    其实赋值语句有结果,来看一段代码:

    #include<iostream>
    using namespace std;
    int main()
    {
        int n;
    	cout<<(n=2)<<endl;	//注意()不能少,否则会出现语法错误 
        return 0;
    }

    会发现程序输出了赋值后n的值2,证明了赋值语句的结果就是变量被赋予的新值。

    正因为如此,C++支持多个变量连续赋值。例如:

    int a,b,c;
    a = b = c = 1;

    此外要注意前面已经介绍过的一些特殊的赋值语句:

    int n = 4,m = 6;
    
    n = n + m;
    n += m;          //自赋值(与 n = n + m; 等效,可以认为是简写)
    
    n = n + 1;
    n += 1;          //自赋值(与 n = n + 1; 等效,可以认为是简写)
    n++;             //自赋值( n += 1;的简写)
    ++n;             //自赋值( n += 1;的简写)
    
    n = n - 1;
    n -= 1;
    n--;            //自赋值( n -= 1;的简写)
    --n;            //自赋值( n -= 1;的简写)
    \(n++;\)与\(n++;\)几乎没有区别,特别是单独成句的时候。但是两者的执行流程不同,通过下面两段程序对比测试:

    #include<iostream>
    using namespace std;
    int main()
    {
        int n,m;
    	n = 1;
    	m = n++;
    	cout<<n<<" "<<m;	 
        return 0;
    }

    运行结果如下:

    2 1
    \(n++;\)是先使用\(n\)的值然后\(n\)再自加1;所以\(m = n++;\),赋值语句先执行\(m = n;\),然后再执行\(n = n+1;\),最终\(n = 2\),\(m = 1\)
    #include<iostream>
    using namespace std;
    int main()
    {
        int n,m;
    	n = 1;
    	m = ++n;
    	cout<<n<<" "<<m;	 
        return 0;
    }

    运行结果如下:

    2 2
    \(++n;\)是\(n\)先自加1然后再使用\(n\)的值,所以\(m = ++n;\),赋值语句先执行\(n = n+1;\),然后再执行\(m = n;\),最终\(n = 2\),\(m = 2\)

    三、经典顺序结构

    1.交换器

    如何交换两个变量a、b的值?注意:是真正的在计算机内部的值交换,而不是“输入a、b;输出b、a”这样的障眼法。你可能会想到这样:

    #include<iostream>
    using namespace std;
    int main()
    {
        int a,b;
    	cin>>a>>b;
    	cout<<a<<" "<<b<<endl;
    	a = b;
    	b = a;
    	cout<<a<<" "<<b<<endl;
        return 0;
    }
    #include<iostream>
    using namespace std;
    int main()
    {
        int a,b;
    	cin>>a>>b;
    	cout<<a<<" "<<b<<endl;
    	b = a;
    	a = b;
    	cout<<a<<" "<<b<<endl;
        return 0;
    }

    很显然,上面的两段代码都不能实现交换a、b的值,左边代码导致a、b的值都变成原来b的值,右边的代码导致a、b的值都变成原来a的值。很显然a=b、b=a这样简单的赋值不能实现交换,那试试下面的代码:

    #include<iostream>
    using namespace std;
    int main()
    {
        int a,b,c,d;
    	cin>>a>>b;
    	cout<<a<<" "<<b<<endl;
    	c = a;
    	d = b;
    	a = d;
    	b = c;
    	cout<<a<<" "<<b<<endl;
        return 0;
    }

    思路是用两个变量c、d分别赋值成a、b的值(相当于在接下来要修改a、b的值之前把a、b的值保存到其它的变量中,存了一个副本一样),然后就可以放心大胆地修改a、b的值而不用担心a、b原来的值无法找回来(在变量c、d里保存着),然后执行赋值语句\(a = d;\) \(a\)的值变成了\(d\)(也就是原来的\(b\)),执行赋值语句\(b = c;\) \(b\)的值变成了\(c\)(也就是原来的\(a\))。

    注意体会这里的算法,要修改变量的值,并且修改后还要用到变量修改前的旧值,可以借助额外的变量在修改之前把旧值保存下来。

    其实这里并不需要两个额外的变量来保存a、b的值,用1个变量就行:

    #include<iostream>
    using namespace std;
    int main()
    {
        int a,b,t;
    	cin>>a>>b;
    	cout<<a<<" "<<b<<endl;
    	t = a;
    	a = b;
    	b = t;
    	cout<<a<<" "<<b<<endl;
        return 0;
    }

    再来试试下面的代码,不借助额外的变量,靠简单的加减运算也能实现交换:

    #include<iostream>
    using namespace std;
    int main()
    {
        int a,b;
    	cin>>a>>b;
    	cout<<a<<" "<<b<<endl;
    	a = a+b;
    	b = a-b;
    	a = a-b;
    	cout<<a<<" "<<b<<endl;
        return 0;
    }

    这一个精心设计的算法,没有用到额外的变量,却很巧妙地实现了交换。看吧,这就是算法的魅力!

    2.计数器

    现在想统计一下参加兴趣小组活动的同学的数量,现实生活中最简单的做法是报数,我们也能借助程序来模拟报数的过程:

    #include<iostream>
    using namespace std;
    int main()
    {
        //变量tot用来报数(计数)
        int tot = 0;    //初始值0
    
        //第1个同学报数    
        tot++;
    	cout<<tot<<endl;
    
        //第2个同学报数	
        tot++;
        cout<<tot<<endl;
    
        //第3个同学报数   
        tot++;
        cout<<tot<<endl;
    
        //后面的同学依次报数   
        //...
    	 
        return 0;
    }

    左边的程序“每位同学都报数了”,当然最后报数就是人数。也可以只在最后输出tot变量,实现真正的“计数”,而不是每位同学都报数:

    #include<iostream>
    using namespace std;
    int main()
    {
        int tot = 0;
        
        tot++;
    	
        tot++;
        
        tot++;
        
        //...
        //每一位同学报数其实就是执行一次tot++;
        
        cout<<tot<<endl;	 
        return 0;
    }

    3.累加器

    再来看一个问题,统计同学们捐献的爱心图书数量。我们也可以借助“计数器”的思路实现,不过计数器每次累加的都是1,而这里可能是任意的整数(每位同学捐献的图书数量不一定都是1本):

    #include<iostream>
    using namespace std;
    int main()
    {
        int s = 0;   //初值0 
        
        //第1位同学捐献爱心图书5本 
        s += 5;
    	//第2位同学捐献爱心图书2本
        s += 2;
        //第3位同学捐献爱心图书1本
        s += 1;
        
        //...
        //对于每位同学,s累加上捐献图书本数 
       
        cout<<s<<endl;	 
        return 0;
    }

    左边的程序结构称之为累加器,通过多条赋值(自赋值)语句不停地将数累加到变量s上,最后s就是所有数的和。

    可知,计时器是累加器的特例:每次累加的数都是1。

    不过要记住,对于计数器和累加器变量,最开始一般要赋初值0

    这里还看不出来计数器和累加器的实用价值,等后面学习了循环结构,借助循环语句它们的实用性就能充分体现出来。

    4.累乘器

    和前面的计数器、累加器相似,累乘器实现的效果是重复将一个数放大若干倍数,例如要计算5的阶乘(\(n!=1 \times 2 \times 3 \times ... \times n\)),使用累乘器代码如下:

    #include<iostream>
    using namespace std;
    int main()
    {
        int t = 1;    //初值为1
    	t *= 2;
    	t *= 3;
    	t *= 4;
    	t *= 5;
    	cout<<t<<endl;
        return 0;
    }

    同样地,这里还看不出来累乘器的实用价值,等后面学习了循环结构,借助循环语句它们的实用性就能充分体现出来。

    顺序结构例题

    tangyj626阅读(794)

    本小节通过顺序结构例题解析,更加深入探讨顺序结构。例题问题的解决通过编程解决问题的思路和步骤来组织,希望大家要注意并养成好的编程习惯,注重编码前问题的分析和算法的设计!

    1.海伦公式计算三角形面积

    问题背景:已知三角形的三边长 \(a、b、c\),则三角形的面积可使用海伦公式来计算:

    令 \(p = (a+b+c)/2\),则三角形面积 \(S = \sqrt{p(p-a)(p-b)(p-c)}\)

    问题描述:计算给定三边长的三角形的面积

    输入格式:三角形的三边长,用空格隔开(数据保证能组成三角形)

    输出格式:三角形的面积,保留2位小数

    问题分析:输入三个浮点数存储到double变量中,利用问题背景提供的公式来计算

    算法设计

    1. \(Input \ a,b,c\)
    2. \(p ← (a+b+c)/2\)
    3. \(s ← sqrt(p*(p-a)*(p-b)*(p-c))\)
    4. \(Output \ s\)
    #include<cstdio>
    #include<cmath>
    int main()
    {
    	double a,b,c,p,s;
    	scanf("%lf%lf%lf",&a,&b,&c);
    	p = (a+b+c)/2;
    	//表达式中*不能省略
    	s = sqrt(p*(p-a)*(p-b)*(p-c));
    	printf("%.2lf",s);
    	return 0;
    }

    注意,实际编程时需要考虑代码的可读性,例如下面的代码虽然更加精简,但是阅读起来会有点费解:

    #include<cstdio>
    #include<cmath>
    int main()
    {
    	//不建议编写如下“看似精简,但是可读性不高的代码”
    	double a,b,c;
    	scanf("%lf%lf%lf",&a,&b,&c);
    	printf("%.2lf",sqrt((a+b+c)/2*((a+b+c)/2-a)*((a+b+c)/2-b)*((a+b+c)/2-c)));
        return 0;
    }

    2.数的拆分——求逆序数

    问题描述:求一个三位正整数的逆序数,所谓逆序说就是将整数逆序书写得到的整数,例如123的逆序数是321;120的逆序数是21(去掉高位多余的0)

    输入格式:一个三位正整数n

    输出格式:输出n的逆序数,逆序数高位不能有多余的0

    问题分析:将三位正整数 \(n\) 每位上的数字“拆分”出来,个位、十位、百位上的数字分别存储到变量 \(g、s、b\) 中,然后依次输出\(g、s、b\),不过不用任何符号隔开,那么看上去就是一个三位数,也就是要求的逆序数。那么问题只剩下如何“拆分”了:来看两个运算 \(\%\)和\(/\) ,\(n\%10\)的结果就是个位上的数字\(g\);要计算十位上的数字\(b\),先来看\(n/10\)(整数/整数结果是整数!)的结果是把\(n\)的个位数“砍掉”了,原来十位上的数到了结果的个位数上,那么结果%10,也就是\(n\ / 10\ \%\ 10\)就是十位上的数字\(s\);同样的\(n\ / \ 100\ \%\ 10\)(对于三位数来说\(n/100\)也行)就是百位上的数字\(b\)。

    算法设计:

    1. \(Input \ n\)
    2. \(g \ ← \ n \% 10\)
    3. \(s \ ← \ n/10 \% 10\)
    4. \(b \ ← \ n/100 \% 10\)
    5. \(Output \ g,s,b\)(不用任何符号隔开)
    #include<iostream>
    using namespace std;
    int main()
    {
        int n,g,s,b;
        cin>>n;
        g = n%10;
        s = n/10%10;
        b = n/100%10;
        cout<<g<<s<<b;
        return 0;
    } 

    程序运行测试情况如下:

    输入123,输出:

    321

    输入120,输出:

    021

    输入100,输出:

    001

    分析测试结果,可知右边两种情况(个位是0或者除百位外全是0),直接输出拆分出来的个、十、百位数的数字,不能满足题目“逆序数高位不能为0”的要求。那么我们设计的算法还不能全面地解决问题,需要进行调整。其实拆出来\(g、s、b\)后,可以再重新组合成新整数:\(g*100+s*10+b\),这也是 \(n\) 的逆序数,并且这样做可以自动去掉高位多余的0。我们可以自行修改代码,并再次进行全面的测试。

    其实这个问题还可以利用scanf("%1d%1d%1d",&b,&s,&g);的方式输入数据,或者使用字符方式输入数据,这样就不用拆分整数了。

    #include<cstdio>
    int main()
    {
        int n,g,s,b;
        scanf("%1d%1d%1d",&b,&s,&g);
        n = g*100+s*10+b;
        printf("%d",n);
        return 0;
    } 
    #include<cstdio>
    int main()
    {
        int n;
    	char g,s,b;
        scanf("%c%c%c",&b,&s,&g);
        //b,s,g都是数字字符,减去'0'就是对应的整数 
        n = (g-'0')*100+(s-'0')*10+(b-'0');
        printf("%d",n);
        return 0;
    }

    3.[洛谷]P1425 小鱼的游泳时间

    初步分析:游泳小时数为\(c-a\),分钟数为\(d-b\)

    算法设计:

    1. \(Input \ a,b,c,d\)
    2. \(Output \ c-a\)
    3. \(Output \ d-b\)
    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c,d;
    	cin>>a>>b>>c>>d;
    	cout<<c-a<<" "<<d-b;
    	return 0;
    }

    测试结果如下:

    输入数据:

    8 10 12 20

    输出数据:

    4 10

    输入数据:

    12 50 19 10

    输出数据:

    7 -40

    从测试结果可知,算法有问题!原因是没有考虑\(d<b\)的情况,此时是不能直接用\(d-b\)作为分钟数(不用担心\(a,c\)的大小情况,合法的输入数据肯定满足\(a \le c\))。看来需要重新设计算法。计算00:00到开始时间a时b分的总分钟数:\(t1 = a*60+b\),00:00到结束时间c时d分的总分钟数:\(t2 = c*60+d\),那么游泳持续时间(分钟数)\(t = t2-t1\),换算成小时数为\(t/60\),分钟数为\(t\%60\)。

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c,d,t;
    	cin>>a>>b>>c>>d;
    	t = (c*60+d)-(a*60+b);
    	cout<<t/60<<" "<<t%60;
    	return 0;
    }

    4.[洛谷]P1421 小玉买文具

    问题分析:和上一题类似,这里有元、角2个单位,可以先统一单位,例如全部转换成角,那直接做除法就行(整数/整数结果是整数!)

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b;
    	cin>>a>>b;
    	cout<<(a*10+b)/19;
    	return 0;
    } 

    转换成角来计算,计算简单。

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,ans;
    	cin>>a>>b;
    	ans = (a+0.1*b)/1.9;
    	cout<<ans;
    	return 0;
    } 

    转换成元来计算,计算复杂(除法结果为浮点数,需要转换成整数)。本例因为小数位数仅有一位,浮点数除法计算在可靠范围。但是有些问题用浮点数计算就可能不可靠了。记住:能够避免浮点运算尽量避免!

    5.[洛谷]P5709 Apples Prologue

    问题分析:总时间为\(s\)分钟,吃一个苹果花费\(t\)分钟,那么吃掉的苹果数应该是\(s/t\),要计算剩下的完整苹果数量,而\(s/t\)结果只是除法的整数部分,已经吃过的苹果(包括没有吃完的)的数量是\((int)ceil(1.0*s/t)\)(\(ceil\)向上取整,结果是浮点数,还需要int强制类型转换),剩余苹果数量是\(m-(int)ceil(1.0*s/t)\)。

    #include<iostream>
    #include<cmath>
    using namespace std;
    int main()
    {
    	int m,t,s;
    	cin>>m>>t>>s;
    	cout<<m-(int)ceil(1.0*s/t);
    	return 0;
    } 

    在洛谷平台提交后,评测结果如下:

    三个点错误。鼠标放到错误点上,显示了错误原因:Wrong Answer.wrong answer On line 1 column 1.read -,except 0.


    测评的过程是这样的:每个测试点都设计了标准的输入数据和对应的标准输出答案。测评时会将提交的程序编译后去“跑”每个测试点的输入数据,程序的结果和测试点的标准结果对比,如果相同则认为该点通过,否则未通过。

    这里给出的错误原因指出读到程序输出的第1行(line 1)第1列(column 1)内容是 -,但是该处期望(expected,也就是标准答案)的内容是0。

    注意错误提示不会提示大片区域错误,只会提示在某行某列处第一次匹配不上(内容不同),例如程序运行结果是123,但是标准答案是136,那么只会提醒第一处不匹配的内容,也就是 line 1 column 2,此时错误信息可能是:Wrong Answer.wrong answer On line 1 column 2.read 2,except 3.

    此外,测评系统匹配程序输出结果和标准答案时,对于每一行末尾的空格,以及整个内容后的空行,一般都会忽略不计(包括洛谷平台、竞赛时的测评)。例如题目要求输出1行,我们使用cout<<结果<<endl;cout<<结果;都没有问题。


    结合题目和错误提示信息,可以推测应该是\(m-(int)ceil(1.0*s/t)\)的计算结果出现了负数(提示信息给出“在第一行第一列读到-”的原因),这个时候按照题意,应该输出0。结果应该取\(m-(int)ceil(1.0*s/t)\)和0的最大值,可以借助\(max\)函数实现(后续我们还可以通过选择结构的if语句实现):

    #include<iostream>
    #include<cmath>
    using namespace std;
    int main()
    {
    	int m,t,s;
    	cin>>m>>t>>s;
    	cout<<max(0,m-(int)ceil(1.0*s/t));
    	return 0;
    } 

    再谈变量的使用

    tangyj626阅读(183)

    变量用来存储数据,变量的值在程序中可以通过输入语句或者赋值语句来赋值,并且可以按照需求多次给变量重新赋值,这正是变量一词中“变”的由来。在实际编程时,可以利用变量的这一特征来持续“追踪”变化的状态。

    1.猴子吃桃

    一只猴子有若干桃子。第一天它吃了这些桃子的一半以后,又贪嘴多吃了一个;第二天它也吃了剩余桃子的一半,又贪嘴多吃了一个;第三天它又吃了剩余桃子的一半,并贪嘴多吃了一个。第四天起来一看,只剩下 n 个桃子了。问:猴子最开始有多少个桃子?

    【分析】直接求解问题有一定难度,不过可以从第四天倒推回去,这样就简单多了。第四天剩下 n 个桃子,那么第三天应该是 (n+1)*2 个((n+1)*2/2-1 → n),同理可以继续推算第二天和第一天的情况。

    #include<iostream>
    using namespace std;
    int main()
    {
    	int day4,day3,day2,day1;
    	cin>>day4;				//第4天 
    	day3 = (day4+1)*2;		//第3天 
    	day2 = (day3+1)*2;		//第2天 
    	day1 = (day2+1)*2;		//第1天 
    	cout<<day1<<endl;
        return 0;
    } 

    在倒推的过程中,可以用变量 n 一直来记录这一天猴子吃桃子前的桃子数量,第四天就是输入的 n 值,那么倒推第三天的桃子数应该是 (n+1)*2 ,直接将这个值又赋值给 n(n = (n+1)*2;),这样的话第三天的桃子数量仍然记录在变量 n 中。接着倒推第二天、第一天,仍然用这样的方法。在倒推的过程中,通过连续的赋值语句不断修改变量 n 的值,让 n 一直记录的是倒推到的天数的桃子数量,也就是用 n 来持续“追踪”倒推过程中每天的桃子数量。

    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n;		 //第4天桃子数 
    	n = (n+1)*2;	//第3天桃子数,仍然保存在n中 
    	n = (n+1)*2;	//第2天桃子数,仍然保存在n中 
    	n = (n+1)*2;	//第1天桃子数,仍然保存在n中 
    	cout<<n<<endl;
        return 0;
    } 

    当然,本题也可以用数学解析法求解,可知第一天的桃子数应该是:(((n+1)*2+1)*2+1)*2,程序中直接输出即可,但这样计算表达式比较复杂,不能一下子看清楚中间的计算过程,反而还没有上面的程序可读性强。

    2.分糖果游戏

    【分析】使用5个变量分别记录5位小朋友的糖果数量,然后模拟玩游戏的过程,分糖果的环节中,只要小朋友糖果的数量发生变化,直接用赋值语句对相应的变量重新赋值。游戏结束后,5位小朋友的糖果数量仍然在这5个变量中(注意这里变量使用的技巧,就是用变量“追踪”小朋友的糖果数)。
    计算吃掉的糖果数量,有两种方法。第一种是游戏开始前使用变量s1记录总糖果数,游戏结束后使用变量s2记录总糖果数,那么吃掉的糖果数就是s1-s2。
    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c,d,e,s1,s2;
    	cin>>a>>b>>c>>d>>e;
    	s1 = a+b+c+d+e;		//计算游戏开始前糖果总数 
    
    	//每个小朋友分糖果导致糖果数变化,通过赋值语句重新赋值
    	a/=3; e+=a; b+=a;	//第1位小朋友分糖果,a、e、b要重新赋值,三条语句写到了一行里 
    	b/=3; a+=b; c+=b;
    	c/=3; b+=c; d+=c;
    	d/=3; c+=d; e+=d;
    	e/=3; d+=e; a+=e;
    	
    	s2 = a+b+c+d+e;		//计算游戏结束后糖果总数
    	
    	cout<<a<<" "<<b<<" "<<c<<" "<<d<<" "<<e<<endl;
    	cout<<s1-s2<<endl;
    	return 0;
    }

    第二种方式就是使用累加器来累加计算吃掉的糖果总和,要注意的是累加器先要清零。

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c,d,e,s = 0;	//s用来计算吃掉糖果数,先清零 
    	cin>>a>>b>>c>>d>>e;
    	//每个小朋友分糖果导致糖果数变化,通过赋值语句重新赋值
    	s+=a%3; a/=3; e+=a; b+=a;	//第1位小朋友分糖果,吃掉的糖果数a%3累加到s 
    	s+=b%3; b/=3; a+=b; c+=b;
    	s+=c%3; c/=3; b+=c; d+=c;
    	s+=d%3; d/=3; c+=d; e+=d;
    	s+=e%3; e/=3; d+=e; a+=e;
    	
    	cout<<a<<" "<<b<<" "<<c<<" "<<d<<" "<<e<<endl;
    	cout<<s<<endl;
    	return 0;
    }

    3.分钱游戏

    【分析】使用3个变量来记录三人的钱数,由总钱数n可以计算出分钱结束后三人的钱数都是n/3,然后按照丙、乙、甲的顺序逆推每人分钱之前的钱数,通过赋值语句仍然记录到对应的变量中(用变量来“追踪”钱数),那么三人分钱的环节都逆推结束后,3个变量记录的就是开始前三人的钱数。
    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c,n;
    	cin>>n;
    	a = b = c = n/3;	//结束后三人钱数都是n/3
    	 
    	//逆推丙分钱前三人钱数
    	//对于甲来说,他从丙那里拿到了和他已有钱相等的钱数,两份相加是a
    	//那么在丙分钱之前,甲的钱数应该是a/2。乙的情况类似。
    	a /= 2;			//重新赋值a,赋值后a就是丙分钱前甲的钱数 
    	b /= 2;
    	c = n-a-b;			//丙分钱前丙的钱数是n-a-b.也可以用 c+=a+b; 
    	
    	//逆推乙分钱前三人钱数
    	a /= 2;
    	c /= 2;
    	b = n-a-c;			//也可以用 b+=a+c; 
    	
    	//逆推甲分钱前三人钱数
    	b /= 2;
    	c /= 2;
    	a = n-b-c;			//也可以用 a+=b+c;
    	
    	cout<<a<<" "<<b<<" "<<c<<endl; 
    	return 0;
    }

    也可以使用数学解析的方法来逆推整个过程,得出的三人钱数就是与总钱数相关的计算公式。假设总钱数为n,那么数学解析的逆推过程如下表所示:

    #include<iostream>
    using namespace std;
    int main()
    {
        int n;
        cin>>n;
        cout<<13*n/24<<" ";
        cout<<7*n/24<<" ";
        cout<<n/6<<endl;
        return 0;
    }

    选择结构/分支结构与if语句

    tangyj626阅读(1279)

    现实生活中,我们往往面临选择:今天出门穿什么衣服?中高考结束后填报志愿选择哪所学校?毕业后从事什么样的职业?当然,做出决定之前,我们往往会根据当前的情况来进行分析判断,帮助我们更好地选择。编写程序解决实际问题,往往也要根据不同的情况,完成不同的操作,这就和数学物理学科中的分类讨论一样。

    一、选择结构/分支结构

    我们首先来看最简单的可以分两种情况来处理的问题:口渴了,我们会喝水;不口渴,不喝水。进一步分析如下:

    • 情况1:口渴,那么喝水;
    • 情况2:不口渴,不喝水。

    这是一个典型的分两种对立情况的分类讨论。再进一步分析,按照判断“是否口渴”的结果来描述:

    两种情况对应两个条件判断:

    • 如果口渴,那么喝水;
    • 如果不口渴,那么不喝水。

    左边的描述还可以精简为:

    如果口渴,那么喝水;

    否则,不喝水。

    上面右侧描述的实质就是判断“口渴”这个条件是否成立,成立的话喝水,不成立的话(也就是“否则”的含义)不喝水。对应算法的描述如下:

    伪代码描述如下:

    如果 口渴:
         喝水
    否则:
         不喝水

    流程图描述如右图所示(Y表示YES,条件成立;N表示NO,条件不成立):

    再来看一个例子,判断一个整数\(n\)是否为非负数(0或正整数),判断条件可以用\(n \ge 0\)(这个时候是一个量化的条件,而不是像上一个例子是自然语言描述的条件),算法和程序如下:

    使用C++语言的if语句来实现:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n;
    	if(n>=0){
    		cout<<"YES";
    	}else{
    		cout<<"NO";
    	}
    	return 0;
    } 

    像上面的根据条件判断结果(成立或者不成立)选择执行不同的语句的结构,称为选择结构;从流程图来看,在菱形条件判断处,根据条件成立与否,算法处理流程分成了两条分支,因此也称为分支结构

    二、if语句

    C++中可以用if语句实现选择结构,if语句的使用方法如下:

    if(条件){
        //条件成立时执行的语句(语句组)
    }else{
        //条件不成立时执行的语句(语句组)
    }

    将if读成“如果”,将else读成“否则”,会发现if语句解读下来很容易理解,和自然语言一致。

    此外,if语句还可以没有else部分,此情况用于条件成立时要执行一些操作,条件不成立时什么都不干:

    没有else部分的if语句格式:

    if(条件){
        //条件成立时执行的语句(语句组)
    }

    没有else部分的if语句,只指定条件成立时要执行的if语句(组),条件不成立时什么都不处理。

    需要注意的是,C++书写if语句的时候,if子句部分写在else子句部分前面,但是程序执行的时候if子句和else子句是平级关系,这一点从流程图可以明显看出来。

    完整if语句流程图
    无else部分的if语句流程图

    对于初学者来说,这里有一个容易犯的错误,就是在else后也书写条件,例如下面的代码会出现编译错误:

    #include<iostream>
    using namespace std;
    int main()
    {
        int n;
        cin>>n;
        if(n>=0){
            cout<<"YES";
        }else(n<0){ //编译错误,else后不能写条件
            cout<<"NO";
        }
        return 0;
    } 

    通过前面的分析,我们已经知道else意味着“否则”,也就是if后()括号内条件判断不成立,显然这里是不用再写条件的(画蛇添足!)

    使用if语句解决选择结构的问题,需要我们分析问题后,设计算法构思程序的框架,这个时候最初想到的往往是自然语言描述的“如果某某条件成立,就执行XX;否则就执行YY”,下一步编程时有一个难点就是要将自然语言描述的条件转换为量化的条件。接下来为大家介绍描述简单条件的逻辑运算和描述复杂条件的复合逻辑运算。

    三、简单条件的描述

    最简单的条件就是大小关系比较的判断,例如\(5 > 3\)、\(4 < 5\)、\(a > b\)等,这些称为关系表达式。但是注意C++中的语法与数学书写方法有些是不同的:

    逻辑比较运算符
    大于>
    大于或等于>=
    小于<
    小于或等于<=
    等于==
    不等于!=

    注意:只有>、<与数学书写一致,>=、<=、==、!=和数学书写都不同。特别是判断相等的==运算符,一定要和赋值运算符=区分开

    1. n=0是赋值语句,将0赋值给变量n;
    2. n==0是关系表达式,判断变量n的值是否等于0。

    关系表达式的结果是bool值,条件成立则结果为true,条件不成立则是false

    bool类型(1Byte)是最简单的数据类型,bool类型的值只有true(相当于1)和false(相当于0)。

    例如语句bool b = false;将bool类型变量b赋值为false;又例如语句bool b = 5>3;因为5>3成立,结果是true,赋值后bool类型变量b的值为true。

    其实if语句的条件可以是一个数字或者是变量(一般是整数类型),此时只要作为条件的数字或者变量是非0值,那么条件就成立,例如:

    if(1){    //条件成立
    }
    if(-1){   //条件成立
    }
    if(0){    //条件不成立
    }
    if(n){    //整型变量n的值不为0则条件成立
    }

    四、经典例题

    1.求最值

    求两个整数的最大值(根据情况分别输出):

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b;
    	cin>>a>>b;
    	if(a>b){
    		cout<<a<<endl;
    	}else{
    		cout<<b<<endl;
    	}
    	return 0;
    } 

    求两个整数的最大值(根据情况计算结果,最后输出:输入数据→计算结果→输出结果):

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,max;
    	cin>>a>>b;
    	//if语句{}中如果只有一条语句或者是一个整体
    	//可以不写{},甚至写到一行中
    	if(a>b) max = a;
    	else max = b;	
    	cout<<max<<endl;
    	return 0;
    } 

    2.求绝对值

    if语句的if分支部分和else分支部分,如果只有一条语句(或者是一个完整的整体),那么{}可以省略不写,甚至可以将语句与if(或者else)放在同一行中。

    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n;
    	if(n>=0) cout<<n<<endl;
    	else cout<<-n<<endl;
    	return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n;
    	if(n<0) n = -n;
    	cout<<n<<endl;
    	return 0;
    } 

    3.判断奇偶

    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n;
    	if(n%2==0) cout<<"even";
    	else cout<<"odd";
    	return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n;
    	//下面的语句中的条件n%2=0会出现编译错误
    	if(n%2=0) cout<<"even";
    	else cout<<"odd";
    	return 0;
    } 

    上面右侧的程序会出现编译错误!就是将判断相等的==写成了=(其实是赋值语句),而赋值语句的左边必须是变量,这里是\(n\%2\),不符合语法规则!

    再来看下面的一个求解方法:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int n,mod;
    	cin>>n;
    	mod = n%2;    //计算余数
    	//下面语句中的条件mod=0会出现逻辑错误
    	if(mod=0) cout<<"even";
    	else cout<<"odd";
    	return 0;
    }

    同上面右侧程序一样,也将==写成了=。此时不会出现编译错误(思考原因),但是测试时会发现,不管输入奇数还是偶数,程序的输出都是odd

    什么原因呢?mod=0是赋值语句,赋值语句的结果是mod的值0,这里相当于是用整数0作为if语句的条件。根据上面的知识点可知,此时条件不成立会执行else部分输出odd

    上面给大家演示了一个典型的因为书写问题导致的逻辑错误,大家编程时要注意细节,这样的逻辑错误对于初学者来说还不太容易发现!

    4.正整数除法向上取整

    输入两个正整数a、b,计算输出 a÷b 向上取整的结果。

    因为a、b都是正整数,那么 a/b的结果是商。如果a%b==0(也就是a能被b整除),a÷b就是整数,那么最终结果就是a/b;如果a%b!=0(也就是a不能被b整除),a÷b就是浮点数,那么最终结果就是a/b+1。

    更简单地,结果就是a/b+(a%b!=0)a%b!=0 (判断a不能被b整除)是关系表达式,计算结果是bool,条件成立结果是true(相等于1),条件不成立结果是false(相当于0)。如果a不能被b整除,a%b!=0成立,那么a/b+(a%b!=0)相当于a/b+1;如果a能被b整除,a%b!=0不成立,那么a/b+(a%b!=0)相当于a/b+0

    #include<iostream>
    #include<cmath>
    using namespace std;
    int main()
    {
        int a,b,ans;
        cin>>a>>b;
        ans = ceil(1.0*a/b);
        cout<<ans<<endl;
        return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b;
    	cin>>a>>b;
    	if(a%b==0)
    		cout<<a/b;
    	else
    		cout<<a/b+1;
    	return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,ans;
    	cin>>a>>b;
    	ans = a/b+(a%b!=0);
    	cout<<ans<<endl;
    	return 0;
    } 

    这里给大家介绍一个条件运算符:关系表达式?表达式1:表达式2。作用是判断关系表达式是否成立,成立的话执行表达式1,否则执行表达式2。

    前面几个例子的代码可以使用条件运算符来简化:

    max = a>b?a:b;          //求a、b最大值
    
    n = n<0?-n:n;           //求n的绝对值
    
    cout<<(n%2==0?"even":"odd")<<endl;     //判断奇偶输出结果

    五、复合逻辑运算

    先来看一个问题,判断输入的整数n的绝对值是否小于5。下面给出两种解法:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n;
    	if(n<0) n = -n;//求绝对值 
    	if(n<5) cout<<"YES";
    	else cout<<"NO";
    	return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n;
     	//注意下面条件的写法
    	if(n>-5 && n<5) cout<<"YES";
    	else cout<<"NO";
    	return 0;
    } 

    右边代码中的条件是:n>-5 && n<5&&是复合逻辑运算符,表示并且。此外还有表示或者的||运算符,表示对一个条件取反(否定)的运算符!

    复合逻辑运算符格式用途举例
    &&条件1 && 条件2表示两个条件的并且
    只有两个条件同时成立才成立
    n>-5 && n<5
    a>b && a>c
    ||条件1 || 条件2表示两个条件的或者
    两个条件至少有一个成立就成立
    n<-5 || n>5
    n<0 || n>0
    !!(条件)表示对一个条件的否定
    原条件成立,否定后不成立;反之亦反
    !(n==0)
    !(n>0)

    下面的程序会出现逻辑错误:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n; 
    	//注意:下面的条件描述有逻辑错误
    	if(-5<n<5) cout<<"YES"<<endl;
    	else cout<<"NO"<<endl;
    	return 0;
    }

    左边的程序条件的描述有误,将n>-5 && n<5错误写成数学中的写法-5<n<5。此时编译不会出错,但是测试运行时会发现不管输入什么整数,都会输出YES(也就是作为条件的-5<n<5恒成立)。

    -5<n<5这样的表达式会从左到右依次计算,首先计算-5<n,然后将结果与整数5比较。根据前面内容可知-5<n的结果是bool值,成立时结果为true相当于1,不成立时结果为false相当于0。那么-5<n<5就相当于1<5或者0<5了,当然结果恒成立。

    再来看一个例子,判断输入的正整数n代表的年份是否为闰年。闰年的情况有两种:

    1. n能被4整除,但不能被100整除;
    2. n能被400整除。

    上面两种情况都是闰年,也就是两种情况的或者。对于情况1,条件可以描述为 n%4==0 && n%100!=0。对于情况2,条件可以描述为n%400==0

    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n; 
    	//下面条件中的()可省略,直接用 n%4==0 && n%100!=0 || n%400==0
    	//还可以用 n%400==0 || n%4==0 && n%100!=0
    	if((n%4==0 && n%100!=0) || n%400==0) 
    		cout<<"YES"<<endl;
    	else
    		cout<<"NO"<<endl;
    	return 0;
    } 

    注意,这里的条件可以直接用n%400==0 || n%4==0 && n%100!=0,并不需要使用n%400==0 || (n%4==0 && n%100!=0)。这里就像5+4*2一样,&&运算优先级高于||(就像*运算优先级高于+一样),所以n%400==0 || n%4==0 && n%100!=0会优先计算n%4==0 && n%100!=0。不过为了增强程序的可读性,可以加上小括号n%400==0 || (n%4==0 && n%100!=0),这样阅读者不清楚运算优先级(或者忘记了谁优先计算而产生疑惑)的情况下也能很好理解语句作用。

    注意,!运算其实并不常用,例如!(n==0)可以用相同功能的n!=0替代。!的运算优先级很高,所以n==0这个条件的否定要写作!(n==0),如果写成!n==0,那么会优先计算!n

    if语句的嵌套

    tangyj626阅读(921)

    前面一小节介绍了if语句的基本使用方法,其中条件的描述从最简单的单一数据、简单的关系表达式,再到复杂的多条件复合逻辑运算。本小节我们主要学习if语句的嵌套,也就是if语句的if子句部分或者else子句部分中又出现if语句的结构。

    通过前一小节的学习,我们已经知道,选择结构下的if语句对应的是两种对立情况的分类讨论。如果要处理的问题需要分更多的情况来分类处理,很显然此时简单的if语句是无法满足的,这个时候可以使用本小节介绍的if语句的嵌套结构来解决问题。

    先来看一个简单的问题:输入一个整数\(n\),输出这个整数的符号(0或者+或者-)。

    很明显,这里可以分三种情况讨论:

    1. 如果\(n>0\),输出+
    2. 如果\(n==0\),输出0
    3. 如果\(n<0\),输出-

    这里的三种情况是“互斥”的(对于一个整数\(n\),只能满足三种情况的一个条件,不可能满足这三种情况中的多个条件),所以这里可以使用三个先后顺序的if语句来解决问题:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n; 
    	if(n>0) cout<<"+"<<endl; 
    	if(n==0) cout<<"0"<<endl;
    	if(n<0) cout<<"-"<<endl;
    	return 0;
    }

    算法的流程图如下左图所示:

    三个先后的判断

    其实算法还可以这样设计:

    我们注意上面流程图中用红色虚线标注出来的选择结构,这个选择结构属于外层选择结构条件不成立的部分。像这样的选择结构内部又包含了选择结构的情况,称为选择结构的嵌套。

    使用if语句的嵌套解决上述问题参考代码如下:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n;
    	if(n>0){
    		cout<<"+";
    	}else{	//这里的else意味着n<=0 
    		if(n==0){
    			cout<<"0";
    		}else{
    			cout<<"-";
    		}
    	}
    	return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
    	int n;
    	cin>>n;
    	//下面的嵌套结构和上面流程图不同,请自行分析
    	if(n>=0){
    		if(n==0){
    			cout<<"0";
    		}else{
    			cout<<"+";
    		}
    	}else{
    		cout<<"-";
    	}
    	return 0;
    } 

    一、if语句的嵌套与多分支结构

    再来看一个稍微复杂的问题,计算三个整数\(a,b,c\)的最大值。

    如果使用前面介绍的\(max\)函数,那么可以这样处理:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c,t;
    	cin>>a>>b>>c;
    	t = max(a,b);
    	cout<<max(t,c);
    	return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c;
    	cin>>a>>b>>c;
    	//将max函数的计算结果作为另一个max函数的参数
    	cout<<max(max(a,b),c);
    	return 0;
    } 

    思路:分类讨论,可以分以下三种情况讨论:

    1. a>b && a>c:此时最大值是a;
    2. b>a && b>c:此时最大值是b;
    3. 其它情况:最大值是c。

    从前一小节可知,简单的if语句只能用于两种情况的分类讨论(条件成立或者不成立两种情况),这里有三种情况,怎么用if语句实现呢?

    其实还可以这样分类讨论:

    1. a>b && a>c:此时最大值是a;
    2. 其它情况。这里可以再分类讨论:
      1. b>a && b>c:此时最大值是b;
      2. 其它情况:最大值是c。

    可以看出,思路是将情况1单独拿出来,剩余的情况看成一个整体(此时只有两种情况:情况1、情况2和情况3的组合);然后又可以从剩余的情况中再取出来一种情况(原来的情况2),剩余的情况又可以看成一个整体(此时仍然只有两种情况,不过这里剩余的就是一个简单情况——原来的情况3)。

    流程图如右图所示:

    可以预见,在第一次判断if语句的else部分(图中虚线框部分)又会出现一个完整的if语句。

    完整的代码如下:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c,max;
    	cin>>a>>b>>c; 
    	if(a>b && a>c){
    		max = a;
    	}else{
    		//else部分又是一个if语句
    		//这种情况称为 if语句的嵌套
    		if(b>a && b>c){
    			max = b;
    		}else{
    			max = c;
    		}
    	}
    	cout<<max<<endl;
    	return 0;
    } 

    左边外层if的else部分是一个整体,{}省略并将内容与else写到一行的效果如下:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c,max;
    	cin>>a>>b>>c; 
    	if(a>b && a>c){
    		max = a;
    	}else if(b>a && b>c){
    		max = b;
    	}else{
    		max = c;
    	}
    	cout<<max<<endl;
    	return 0;
    } 

    对于更多情况的分类讨论,思路一致,每次从剩余情况中分出一种,流程图如下:

    可知上面多分支结构的流程图执行过程如下:从左往右(从代码角度看是从上往下)先后判断每个条件是否成立:如果某个条件成立,那么执行该分支的语句,然后退出整个多分支结构(后续的判断也就不会被执行了);如果所有条件都不成立,那么执行最后的否则分支,然后退出整个多分支结构。

    注意上面流程图的执行特点:对于流程图中的任意一个“不成立”,如果能执行到这里,意味着前面所有的条件都不成立;对于最后一个“不成立”,如果能执行到这里,意味着所有的条件都不成立。

    对应的是if...else if...else结构,这里else if可以有多个:

    #include<iostream>
    using namespace std;
    int main()
    {
    	if(条件1){
    		//条件1成立时执行的语句
    
    	}else if(条件2){
    		//条件1不成立、条件2成立时执行的语句
    
    	}else if(条件3){
    		//条件1不成立、条件2不成立、条件3成立时执行的语句
    			
    	}...else if(条件n){
    		//前面条件都不成立、条件n成立时执行的语句
    
    	}else{
    		//前面条件都不成立时执行的语句
    
    	} 
    	return 0;
    } 

    再通过一个例子来巩固if...else if...这样的if嵌套结构和该语句体现的算法策略(每次从剩余情况中分出一种):

    已知分段函数(自变量\(x\)位于不同的取值范围,\(y\)对于\(x\)的函数不同)如下,输入\(x\),计算并输出\(y\):

    $$ y=\left\{ \begin{aligned} x^3+2x^2+1\ (x \ge 10) \\ x+1\ (5 \le x <10) \\ 0\ (-5 < x <5)\\ -x-1\ (-10 < x \le -5)\\-x^3-2x^2-1\ (x \le -10)\end{aligned} \right. $$

    分段函数一共5段,每次分出一种情况(一段)来处理:

    • 如果x>=10,那么...
    • 否则如果x>=5,那么...
    • 否则如果x>-5,那么...
    • 否则如果x>-10,那么...
    • 否则,...
    #include<cstdio>
    int main()
    {
    	double x,y;
    	scanf("%lf",&x);
    	if(x>=10) y = x*x*x+2*x*x+1;
    	else if(x>=5) y = x+1;
    	else if(x>-5) y = 0;
    	else if(x>-10) y = -x-1;
    	else y = -x*x*x-2*x*x-1;
    	printf("%.5lf",y);
    	return 0;
    }

    注意使用if...else if...时要同时联想到它的流程图,大家体会上面程序else if的条件为什么可以简写,例如第一个else if,条件不用写成 x>=5 && x<10,直接用x>=5即可。

    二、“打擂”求极值

    还是上面求三个整数最大值的问题,还可以这样分类讨论:

    1. 如果a>b,那么b肯定不是最大值,不用考虑b,但需要考虑a、c的大小关系,可以再分两种情况讨论:
      1. a>c:最大值是a
      2. 其它情况(a<=c):最大值是c
    2. 其它情况(a<=b),那么a肯定不是最大值,不用考虑a,但需要考虑b、c的大小关系,可以再分两种情况讨论:
      1. b>c:最大值是b
      2. 其它情况(b<=c):最大值是c

    从流程图可以明显看出来,if语句的if子句部分和else子句部分都嵌套了if语句:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c,max;
    	cin>>a>>b>>c; 
    	if(a>b){      //a>b,那么此时只需要考虑a、c 
    		if(a>c) max = a;
    		else max = c;
    	}else{		//a<=b(else),那么此时只需要考虑b、c
    		if(b>c) max = b;
    		else max = c;
    	}
    	cout<<max<<endl;
    	return 0;
    } 

    这里强调代码缩进对于确保程序可读性的重要性,我们对比下面两段代码就能体会到:

    #include<iostream>
    using namespace std;
    int main()
    {
    	int a,b,c,max;
    	cin>>a>>b>>c; 
    	if(a>b){
    		if(a>c)
    			max = a;
    		else
    			max = c;
    	}else{
    		if(b>c)
    			max = b;
    		else
    			max = c;
    	}
    	cout<<max<<endl;
    	return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
    int a,b,c,max;
    cin>>a>>b>>c; 
    if(a>b){
    if(a>c)
    max = a;
    else
    max = c;
    }else{
    if(b>c)
    max = b;
    else
    max = c;
    }
    cout<<max<<endl;
    return 0;
    } 

    大家在编程时,特别是初学阶段,一定要注意良好编码习惯的养成!


    还可以模拟现实生活中“打擂台”的方式来求最大值:

    用一个变量\(max\)记录最大值,首先第一个数\(a\)站到擂台上,那么此时最大值就是\(a\)(\(max=a\)),接下来每次都是一个新数字上到擂台上,和擂台上的留下的数字\(max\)比较大小(两两“打擂”),哪一个数字大就留到擂台上(其实就是更新变量\(max\)的值),那么所有的数都处理结束后,最大值就保存在变量\(max\)中(其实每处理完一个数,已经处理过的所有数的最大值就是变量\(max\))。

    假设\(a=1,b=3,c=2\)

    1. 处理整数\(a\),\(max\)直接赋值成\(a\)。处理后\(max\)的值是1;
    2. 处理整数\(b\),\(max\)值赋值成\(max\)(1)和\(b\)(3)的最大值。处理后\(max\)的值是3,此时\(max\)是\(a,b\)的最大值;
    3. 处理整数\(c\),\(max\)值赋值成\(max\)(3)和\(c\)(2)的最大值。处理后\(max\)的值仍然是3,此时\(max\)是\(a,b,c\)的最大值。
    #include<iostream>
    using namespace std;
    int main()
    {
        int a,b,c,max;
        cin>>a>>b>>c; 
    
        max = a;
        
        if(b>max) max = b;
        
        if(c>max) max = c;
        
        cout<<max<<endl;
        return 0;
    } 

    在上面程序的基础上,添加相似代码很容易实现求更多整数的最大值。

    如果我们再添加一个前提条件:三个整数都是正整数,我们还可以进一步修改代码,让代码更加工整:

    #include<iostream>
    using namespace std;
    int main()
    {
    	//求三个正整数a、b、c的最大值(注意:a、b、c都是正整数) 
        int a,b,c,max;
        cin>>a>>b>>c; 
    
        max = 0;   //体会这里赋值成0的用途 
        
        if(a>max) max = a;
        
        if(b>max) max = b;
        
        if(c>max) max = c;
        
        cout<<max<<endl;
        return 0;
    } 

    再来分析前面的代码,程序结构有if嵌套结构,还有先后执行的多个if语句:

    #include<iostream>
    using namespace std;
    int main()
    {
        int a,b,c,max;
        cin>>a>>b>>c; 
        if(a>b&&a>c){
            max = a;
        }else if(b>a && b>c){
            max = b;
        }else{
            max = c;
        }
        cout<<max<<endl;
        return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
        int a,b,c,max;
        cin>>a>>b>>c; 
    
        max = a;
        
        if(b>max) max = b;
        
        if(c>max) max = c;
        
        cout<<max<<endl;
        return 0;
    } 

    最后来探讨一下程序的效率问题。仍然是计算三个整数最大值,前面说过可以分三种情况讨论(这里的描述和前面稍微不同):

    1. a>=b && a>=c,此时最大值是a;
    2. b>=a && b>=c,此时最大值是b;
    3. c>=a && c>=b,此时最大值是c。

    不少同学自然地使用了先后三个if语句来实现:

    #include<iostream>
    using namespace std;
    int main()
    {
        int a,b,c,max;
        cin>>a>>b>>c; 
        if(a>=b && a>=c){
            max = a;
        }
    	if(b>=a && b>=c){
            max = b;
        }
    	if(c>=a && c>=b){
            max = c;
        }
        cout<<max<<endl;
        return 0;
    } 

    对比前面使用if...else if...语句实现方式:

    #include<iostream>
    using namespace std;
    int main()
    {
        int a,b,c,max;
        cin>>a>>b>>c; 
        if(a>b && a>c){
            max = a;
        }else if(b>a && b>c){
            max = b;
        }else{
            max = c;
        }
        cout<<max<<endl;
        return 0;
    } 

    上面两段程序均能很好地解决问题,但是分析执行流程会发现第2段程序执行效率更高:第1段程序,不管a,b,c大小如何,都要经过3次判断处理;第2段程序,如果a最大,那么只需要判断1次,并且不管a,b,c大小如何最多只判断2次。

    分类讨论时,如果用多个先后的if语句实现,要特别注意一般要保证有一个并且只有一个if语句条件成立。还是以求三个整数的最大值为例,下面的程序当三个整数值相同时,就不能保证这一点,要么程序没有输出,要么程序输出多次:

    #include<iostream>
    using namespace std;
    int main()
    {
        //试试输入三个相同的整数,程序不会有输出
        int a,b,c;
        cin>>a>>b>>c; 
        if(a>b && a>c) cout<<a<<endl; 
        if(b>a && b>c) cout<<b<<endl; 
        if(c>a && c>b) cout<<c<<endl; 
        return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
        //试试输入三个相同的整数,程序会输出三次
        int a,b,c;
        cin>>a>>b>>c; 
        if(a>=b && a>=c) cout<<a<<endl; 
        if(b>=a && b>=c) cout<<b<<endl; 
        if(c>=a && c>=b) cout<<c<<endl; 
        return 0;
    } 

    switch...case多分支结构

    tangyj626阅读(642)

    C++中除了if语句可以实现选择结构(分支结构),还有switch…case结构,可以实现多分支结构。

    先来看一个例子,输入数字(0~6),输出数字对应的星期名称(0表示星期日)。先来看使用if...else if...语句的参考代码:

    #include<iostream>
    using namespace std;
    int main()
    {
        int n;
        cin>>n;
        if(n==0) cout<<"Sunday"<<endl;
        else if(n==1) cout<<"Monday"<<endl;
        else if(n==2) cout<<"Tuesday"<<endl;
        else if(n==3) cout<<"Wednesday"<<endl;
        else if(n==4) cout<<"Thursday"<<endl;
        else if(n==5) cout<<"Friday"<<endl;
        else if(n==6) cout<<"Saturday"<<endl;
        else cout<<"Error"<<endl;
        return 0;
    } 

    再来看使用switch...case语句的参考代码:

    #include<iostream>
    using namespace std;
    int main()
    {
        int n;
        cin>>n;
        switch(n){
        	case 0:cout<<"Sunday"<<endl;break;
        	case 1:cout<<"Monday"<<endl;break;
        	case 2:cout<<"Tuesday"<<endl;break;
        	case 3:cout<<"Wednesday"<<endl;break;
        	case 4:cout<<"Thursday"<<endl;break;
        	case 5:cout<<"Friday"<<endl;break;
        	case 6:cout<<"Saturday"<<endl;break;
        	default:cout<<"Error"<<endl;
    	}
        return 0;
    } 

    switch 语句用来测试一个变量(或表达式)值的情况,判断值是否匹配{}中的某个case,如果匹配到某个case则执行该case冒号后的语句。以上面的程序为例,将输入的变量n的值去匹配{}中case的值,假如我们输入3,那么会匹配到case 3,执行case 3:后面的语句cout<<"Wednesday"<<endl;输出Wednesday

    假如我们这里输入0~6范围外的整数,例如输入7,那么没有任何一个case会被匹配到,这个时候会自动执行default:中的语句。switch语句可以没有default部分,此时如果没有任何case匹配到,那么会自动退出switch语句。

    我们注意到case语句中有break;表示本次case语句执行后不再继续执行后面的case语句直接退出switch语句。假如我们去掉case 3:cout<<"Wednesday"<<endl;break;这里的break;语句,使用case 3:cout<<"Wednesday"<<endl;,此时输入3,那么执行case 3中的输出语句输出Wednesday后,还会继续执行后面case 4中的输出语句(由于case 4有break;,输出Thursday后就会退出switch语句不再继续执行后面的case)。

    注意:switch括号中的变量或者表达式的结果必须是整数(包括char),否则会出现编译错误。case后必须是整型常量(或者结果是整型的常量表达式,包括char),否则也会出现编译错误。case后的执行语句可以写成一行,也可以写成多行(此时注意代码缩进)。

    再来看一个例子,学业水平考试等级成绩为A、B、C、D、E,其中A、B、C、D均通过考试,E未通过考试。输入等级成绩,判断是否通过考试:

    #include<iostream>
    using namespace std;
    int main()
    {
        char degree;
        cin>>degree;
        switch(degree){
        	case 'A':
        		cout<<"Passed";
    			break;
        	case 'B':
        		cout<<"Passed";
    			break;
        	case 'C':
    			cout<<"Passed";
    			break;
        	case 'D':
    			cout<<"Passed";
    			break;
        	case 'E':
    			cout<<"Not Passed";
    			break;
            default:
                cout<<"Input Error"<<endl;
    	}
        return 0;
    } 

    利用case后没有break会自动执行后面case语句的特点,可以将左边的代码精简:

    #include<iostream>
    using namespace std;
    int main()
    {
        char degree;
        cin>>degree;
        switch(degree){
        	case 'A':
        	case 'B':
        	case 'C':
        	case 'D':
    			cout<<"Passed";
    			break;
        	case 'E':
    			cout<<"Not Passed";
    			break;
            default:
                cout<<"Input Error"<<endl;
    	}
        return 0;
    } 

    这里再次强调代码缩进对于确保程序可读性的重要性,我们对比下面两段代码的可读性就能体会到:

    #include<iostream>
    using namespace std;
    int main()
    {
        char degree;
        cin>>degree;
        switch(degree){
        	case 'A':
        	case 'B':
        	case 'C':
        	case 'D':
    			cout<<"Passed";
    			break;
        	case 'E':
    			cout<<"Not Passed";
    			break;
            default:
                cout<<"Input Error"<<endl;
    	}
        return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
    char degree;
    cin>>degree;
    switch(degree){
    case 'A':
    case 'B':
    case 'C':
    case 'D':
    cout<<"Passed";
    break;
    case 'E':
    cout<<"Not Passed";
    break;
    default:
    cout<<"Input Error"<<endl;
    }
    return 0;
    } 

    再来看一个问题,输入年份和月份,输出该月天数。

    分析:只有2月份需要特殊处理(闰年29天,平年28天),1、3、5、7、8、10、12月均为31天,4、6、9、11月均为30天。

    #include<iostream>
    using namespace std;
    int main()
    {
        int y,m,d;
        cin>>y>>m;
        switch(m){
        	case 1:
        	case 3:
        	case 5:
        	case 7:
        	case 8:
        	case 10:
        	case 12:d = 31;break;
        	case 4:
        	case 6:
        	case 9:
        	case 11:d = 30;break;
    		case 2:
    			if(y%4==0 && y%100!=0 || y%400==0) d = 29;
    			else d = 28;
    			break; 
    	}
    	cout<<d<<endl;
        return 0;
    } 

    其实switch...case语句完全可以使用if...else if...结构实现,只是有时候使用switch...case语句可以让结构更加清晰(前提是充分了解switch...case语句的执行流程),如果感觉switch...case语句不容易理解,在实际编程时可以直接使用if...else if...。

    switch...case语句还可以指定case的区间范围:

    #include<iostream>
    using namespace std;
    int main()
    {
        int n;
        cin>>n;
        switch (n) {
            case 1 ... 5:
                cout<<"Weekday"<<endl;
                break;
            case 6 ... 7:
                cout<<"Weekend"<<endl;
                break;
            default:
                cout<<"Input Error"<<endl;
        }
        return 0;
    } 
    #include<iostream>
    using namespace std;
    int main()
    {
        char degree;
        cin>>degree;
        switch(degree){
            case 'A' ... 'D':
                cout<<"Passed"<<endl;
                break;
            case 'E':
                cout<<"Not Passed"<<endl;
                break;
            default:
                cout<<"Input Error"<<endl;
        }
        return 0;
    }