C语言/C++预习与复习备考这一篇就够了【01】选择、循环、数组排序

1. 基础知识

1.1 程序设计语言

1.1.1 机器语言

计算机能直接识别和接 受的二进制代码称为机 器指令。机器指令的集 合就是该计算机的机器 语言。 特点:难学,难记,难 检查,难修改,难以推 广使用。依赖具体机器 难以移植。

B8 7F 01
BB 21 02
03 D8
B8 1F 04
2B C3

1.1.2 汇编语言

机器语言的符号化。用 英文字母和数字表示指 令的符号语言。 特点:相比机器语言简 单好记,但仍然难以普 及。汇编指令需通过汇 编程序转换为机器指令 才能被计算机执行。依 赖具体机器难以移植。

MOV AX 123
MOV BX 456
ADD BX AX
MOV AX 789
SUB AX BX

1.1.3 高级语言

高级语言更接近于人们 习惯使用的自然语言和 数学语言。 特点:功能强大,不依 赖于具体机器。用高级 语言编写的源程序需要 通过编译程序转换为机 器指令的目标程序。

x = 789 - (123 + 456)

1.2 C与C++语言

  • 1972年,Bell实验室的Dennis Ritchie和Ken Thompson共同发明了C语言。C语言充分结合 了汇编语言和高级语言的优点,高效灵活,且易于移植,很快得到广泛使用。

  • 1979年,Bjarne Stroustrup加入Bell实验室,开始致力于将C改良为带类的C(C with classes)。1983年该语言被正式命名为C++。ANSI和ISO的联合标准化委员会在1994年1 月25曰提出了第一个C++标准化草案,该草案保持了Stroustrup最初定义的所有特征。

  • 在完成C++标准化的第一个草案后不久,Alexander stepanov创建了标准模板库(STL)。 在通过了第一个草案之后,委员会投票并通过了将STL包含到C++标准中的提议。

  • 委员会于1997年11月14日通过了该标准的最终草案,1998年C++的ANSI/ISO标准被投入 使用。通常此版本的C++被认为是标准C++,所有的主流C++编译器都支持该版本的C++, 包括微软的Visual C++和Borland公司的C++Builder

1.3 第一个C++程序

要求:在屏幕上输出“I love c++ programming!” 。

#include <iostream>
using namespace std;
int main(){
    cout << "I love c++ programming!" << endl;
    return 0;
}

1.4 内存和变量

存储器是计算机系统的重要组成部分,是用来存储程序和数据的部件。

  1. 存储器按其用途可分为主存储器和辅助存储器。

  2. 主存储器又称内存储器简称内存,辅助存储器又称外存储器简称外存。

    扫描二维码关注公众号,回复: 15374668 查看本文章
  3. 内存存取速度快,是CPU能够直接寻址的存储空间。

    存储器中每一个字节均对应唯一的编码地址

    通常以8 bit位组成一个字节(byte)为基本单位,每210(1024) byte 为1KB,210KB为1MB,210MB为1GB,210GB为1TB

变量:具有特定属性的内存单元,可通过变量名直接引用该存储单元

在C++程序中,变量必须遵循“先定义,后使用”的原则。变量命名遵守以下规则:

  1. 变量名只能由字母、数字和下划线构成;

  2. 变量名不能以数字开头;

  3. 变量名不能使用系统保留的关键字(见表1.1);

  4. 变量名严格区分大小写。

同时,为了增加程序代码的可读性,给变量取名时应尽可能做到“见名知义”。

image-20220221115141366

定义变量的一般格式为:数据类型变量名1,变量名2,……,变量名n;

int a; //定义一个名称为a的整型变量 
int a, b, c; //定义三个整型变量,名称分别为a, b, c

1.5 cin/cout输入输出

C++的输入和输出均是采用“流”(stream)方式实现,有关流对象cin、 cout和流运算符的信息均存放于C++的输入输出流库中。

因此,若要在程序中使用cin、cout和流运算符,则必须用预处理命令 “#include ”将头文件iostream包含到本文件中。

  1. 使用cin可输入多个变量的值,其一般格式:cin >> 变量1 >> 变量2 …… >> 变量n;

  2. 使用cin可输出多个表达式的值,其一般格式:cout << 表达式1 << 表达式2 …… << 表达式n;

1.6 整数类型

进制由符号集和两个基本因数“基数”与“位权”构成,基数指不同符号的个数,位权 指进制每一固定位对应的权值。十进制是10个符号的排列组合,二进制是2个符号的排列组合。

1.6.1 十进制转二进制

对于整数部分,用被除数反复除以2(第一次取该整数为被除数,以后每次均取前一次商 的整数部分作被除数),依次记下每次的余数并将余数倒序排列,便是转换后的二进制数。

对于小数部分,采用连续乘以基数2并依次取出整数部分,直至其小数部分为0结束。 以十进制数53转换为二进制数为例,53连续六次除以2后,得到的余数依次是:1、0、1、 0、1、1。将所有余数倒序排列为110101。因此十进制数53转换成二进制数为110101。

1.6.2 二进制转十进制

二进制数第0位的权值是2的0次方,第1位的权值是2的1次方,……。 如二进制数0110 0100转换成十进制数为100。

1.6.3 整数类型(原码、反码、补码)

计算机中的有符号数存在原码、反码和补码三种表示方法。

在计算机系统中,数值一律采用补码来表示和存储,其原因在于:

  1. 补码也可将加减运算进行统一处理;

  2. 补码可将符号位和数值位统一处理。

原码、反码与补码的表示规则如下:

  1. 原码表示法的最高位为符号位,正数该位为0,负数该位为1;其余位为数 值位,表示数值的大小,为该数绝对值的二进制形式。
  2. 正数的反码与原码相同,负数的反码为该数原码的符号位不变数值位取反 (0变为1,1变为0)。反码通常用作原码和补码之间的过渡码。
  3. 正数的补码与原码相同,负数的补码为该数反码加1

补码符号位的数学特征体现了补码在计算机系统中数据表示与运算的优势。

以编译系统为短整型数据分配2个字节存储空间为例,十进制数据13的二 进制形式是1101,其在存储单元中数据形式如图1.1。十进制数据-13的原码、 反码和补码如图1.2。

image-20220221120158420

image-20220221120209494

1.7 浮点类型

浮点表示法是目前为止使用最广泛的实数表示方法。相对于定点数而言,浮点数利用指 数使小数点的位置根据需要而上下浮动,从而可以灵活地表达更大范围的实数

在处理浮点型数据时,计算机系统将其分成小数和指数两个部分加以存储。浮点数类型 包括单精度浮点型(float)、双精度浮点型(double)和长双精度浮点型(long double)

1.8 部分函数

image-20220221120424001

image-20220221120437982

1.8.1 程序题

输入一个正整数,输出其位数

#include<iostream>
#include <cmath>
using namespace std;
int main(){
    int num, n;
    cin >> num;
    n = log10(num) + 1;
    cout << n << endl;
    return 0;
}

1.9 字符类型

在C++中,字符类型分为无符号字符类型和有符号字符类型。字符类型数 据占一个字节,以其对应ASCII码的二进制形式存储,因此可看作是存储空间 和取值范围更小的整数类型。

image-20220221120753436

1.9.1 程序题

输入一个小写英文字符,输出其在英文字母表中的排序

#include <iostream>
using namespace std;
int main(){
    char c;
    cin >> c;
    cout << c - 'a' + 1 << endl;
    return 0;
}

2. 顺序结构程序设计

2.1 常量

image-20220221121843284

2.2 运算符与表达式

常用的运算符包括算术运算符、比较运算符、位运算符、逻辑运算符、赋值运算符、自增和自减运算符等。由运算符、操作数和括号构成的式子称为表达式。

  1. 算术运算符与算术表达式: 由常量、变量、函数、圆括号和算术运算符+、-、*、/、%等组成的式子。一个常量、一个变量(已赋过值)、一个函数都是合法的表达式。

  2. 赋值运算符与赋值表达式: “=” 的作用是将某一数值赋给某个变量。由 “=” 将一个变量和一个表达式连接起来的式子称为赋值表达式,赋值表达式的末尾加上分号就构成赋值语句。赋值表达式的值就是执行赋值操作后的变量取值。

  3. 自增运算符与自减运算符: “++” 为自增运算符,其作用是使变量的值加1,“--” 为自减运算符,其作用是使变量的值减1。此两种运算符只能用于数值类型的变量,不能用于非数值类型变量、常量、表达式和函数调用。++可以置于变量之前,也可放在变量之后

    “++i”指“i自增1后再参与其它运算”; “i++”则指“i参与运算后其值再自增1”

2.2.1 位运算符

位运算符用来对二进制位进行操作,其操作数只能为整型和字符型数据。位运算符中,除 “~” 以外,其余均为双目运算符。

  1. 按位与运算 &双目运算符,参加运算的两个数以补码形式按二进制位进行 “与” 运算。
    运算规则: 0&O=O ;0&1=0;1&0=0;1&1=1;即两位同时为1结果才为1,否则为0

    例如∶3&5=1,即0000 0011 & 0000 0101 = 0000 0001。常用于保留或清零指定位。

  2. 按位或运算 |: 双目运算符,参加运算的两个数以补码形式按二进制位进行 “或” 运算。
    运算规则: 0|O=0;0|1=1; 1|0=1;1|1=1;即两位只要有一个为1结果为1,否则为0

    例如∶3/5=7,即0000 0011 | 0000 0101 = 0000 0111。常用于对某些指定位置1.

  3. 按位异或运算 ^: 双目运算符,两个数以补码形式按二进制位进行 “异或” 运算。
    运算规则: 0^0=0 ; 0^1=1 ; 1^0=1 ;1^1=0;即两位相异结果为1,否则为0

    例如∶3个5=6,即O000 0011 ^ 0000 0101 = 0000 0110。常用于使特定位翻转(对应位与1异或)。

  4. 按位取反运算 ~: 单目运算符,用于对一个整数以补码形式按二进制位取反,1变为0,O变为1。

    例如 9的运算为︰(0000 1001)= 1111 0110。

  5. 左移运算 <<: 双目运算符,其功能是将运算符 “<<” 左边的数按二进制位左移,参与运算的数以补码形式参加左移运算
    “m<<n” 表示把m按二进制位补码形式左移n位。左移n位的时候,最左边的n位将被丢弃,同时在最右边补上n个0。如:00001010<<2=00101000,10001010<<3=01010000。

  6. 右移运算>>双目运算符,其功能是将运算符“>>”左边的数按二进制位右移,参与运算的数以补码形式参加右移运算。
    “m>>n” 表示把m按二进制位补码形式右移n位。右移n位的时候,最右边的n位将被丢弃。

    但右移时处理最左边位的情形要稍微复杂一点︰若m是一个正数或无符号数,则最左边的n位用O填补;

    若m是一个有符号负数,则最左边的n位补0还是补1取决于所用的计算机系统,补0的称为 “逻辑右移”(不保留符号),补1的称为 “算术右移”(保留符号)。

    以算术右移为例:O0001010 >> 2= 00000010, 10001010 >> 3=11110001

2.2.2 运算符的优先级

image-20220221123445134

2.3 数据类型转换

数据类型转换就是将数据(变量、表达式的值)从一种数据类型转换为另一种数据类型。数据类型转换包含自动类型转换强制类型转换两种情形。

自动类型转换: 在不同数据类型的混合运算中,编译器会隐式地进行数据类型转换,这种隐式进行的类型转换称为自动类型转换。自动类型转换遵循以下原则

  1. 如果参与运算的数据类型不同,那么先转换成同一类型再进行运算。
  2. 转换朝着数据长度增加的方向进行,以保证精度不降低。
  3. 在赋值运算中,赋值号两边的数据类型不相同时,将把右边表达式值的类型转换为左边变量的类型。若右边表达式的数据类型长度比左边长,则会丢失部分数据。
  4. 在赋值语句中,赋值号两边数据类型一定是相兼容的类型,否则编译时会报错。

强制类型转换: 自动类型转换不能实现目的时,可以利用强制类型转换将一个表达式转换成所需类型,这种显式进行的类型转换称为强制类型转换。

一般形式为︰(类型名)(表达式)

2.3.1 程序题

小明的奶奶买了n个苹果,小明在家看电视,看到了奶奶买回来的苹果。假设小明可以一直吃,每x小时能吃一个苹果,在没吃完一个的时候不会取吃另一个,那么经过y小时之后奶奶买的苹果还剩下多少个?

要求:用c/c++编写

#include <iostream>
#include <cmath>
using namespace std;
int main() {
	int n, x, y, eatNum, rest;
	cin >> n >> x >> y;
	eatNum = ceil((double)y / x);
	rest = n - eatNum;
	if (rest < 0) rest = 0;
	cout << rest << endl;
	return 0;
}

2.4 scanf/printf输入输出

C++语言兼容C语言的基本语法语句,C++程序同样可使用C中的scanf/printf语句完成数据的输入输出语句,其效率优于cin/cout方式的输入输出。对于不同数据类型变量和表达式的输入输出, scanf/printf语句有严格对应的配对格式控制,使用前需包含头文件cstdio

  1. printf格式输出函数: 用于向输出设备输出数据, printf函数调用的一般格式为︰

    printf(格式控制字符串,输出列表)
    其中,格式控制字符串是双引号括起来的一个字符串,用于指定输出格式,由格式字符串和非格式字符串组成。非格式字符串是在输出时需要原样输出的字符串。格式字符串是以%开头的字符串,%后跟着各种格式字符以说明输出数据的类型、长度、小数位数等。

    image-20220221125919108

    image-20220221125950892

    image-20220221130005754

  2. scanf格式输入函数: 用于从输入设备输入数据, scanf函数调用的一般格式为︰
    scanf(格式控制字符串,地址列表)
    其中,格式控制字符串的作用与printf函数相同;地址列表中给出各变量的地址,地址是由取地址符“&”后跟变量名组成。

image-20220221130242629

image-20220221130334826

image-20220221130347529

#include <iostream>
#include <cmath>
using namespace std;
int main() {
	double a, b;
	scanf("%lf%lf", &a, &b);
	printf("%.2lf\n",a+b);
	return 0;
}
  1. putchar函数∶ 从计算机向显示器输出一个字符 用putchar函数既可以输出可显示字符,也可以输出控制字符和转义字符。

    putchar©中的c可是字符常量、整型常量、字符变量或整型变量(其值在字符的ASCII代码范围内)。

  2. getchar函数︰向计算机输入一个字符。函数没有参数。函数的值就是从输入设备得到的字符。只能接收一个字符。如果想输入多个字符就要用多次函数。不

    仅可以从输入设备获得一个可显示的字符,而且可以获得控制字符。用getchar函数得到的字符可以赋给一个字符变量或整型变量,也可以作为表达式的一部

    分。如:putchar(getchar());将接收到的字符输出。

2.4.1 程序题

输入一个公民身份证号,输出该公民的出生日期。

  • 输入:公民身份证号

  • 输出:年月日(YYYY-MM-DD)

  • 样例输入∶430622197904060023

  • 样例输出∶1979-04-06

  • 注意:使用c/c++

#include <iostream>
#include <cmath>
using namespace std;
int main() {
	int y, m, d;
	scanf("%*6d%4d%2d%2d%*d", &y, &m, &d);
	printf("%04d-%02d-%02d", y, m, d);
	return 0;
}

鸡兔同笼:共有a个头,b条腿,求鸡兔的数量

  • 注意:使用c/c++
#include <iostream>
#include <cmath>
using namespace std;
int main() {
	int a, b, x, y;
	scanf("%d%d", &a, &b);
	y = (b - 2 * a) / 2;
	x = a - y;
	printf("鸡的数量:%d,兔的数量:%d", x, y);
	return 0;
}

小明的家距离学校很远,小明想知道从家里出发到学校耗费了多少时间。

  • 输入:输入用空格隔开的四个整数,分别代表从家出发的时、分和到校的时、分。

  • 输出:输出用空格隔开的两个整数,代表总共花了多少小时多少分钟。

  • 注意:使用c/c++

#include <iostream>
#include <cmath>
using namespace std;
int main() {
	int h1, m1, h2, m2, time, h, m;
	scanf("%d%d%d%d", &h1, &m1, &h2, &m2);
	time =	(h2 * 60 + m2) - (h1 * 60 + m1);
	h = time / 60;
	m = time % 60;
	printf("总共花了%d小时%d分钟", h, m);
	return 0;
}

3. 选择结构程序设计

3.1 if语句

image-20220221145232878

3.1.1 程序题

输入三个整数,按从小到大的顺序输出这三个数

  • 注意:使用c/c++
#include <iostream>
#include <cmath>
using namespace std;
int main() {
	int a, b, c, temp;
	scanf("%d%d%d", &a, &b, &c);
	// 3 1 0
	if (a > b) { //1 3 0
		temp = a;
		a = b;
		b = temp;
	} 
	if (b > c) {//1 0 3
		temp = b;
		b = c;
		c = temp;
	} 
	if (a > b) {//0 1 3
		temp = a;
		a = b;
		b = temp;
	}
	printf("排序后:%d,%d,%d", a, b, c);
	return 0;
}

3.2 嵌套的if语句

if语句还可通过嵌套实现多分支选择,其一般格式如下︰

if(条件1)语句1
else if(条件2)语句2
else if(条件3)语句3
……

注意: if语句可以缺少else选项,但else不能脱离if关键字单独使用。else必须与if关键字配套使用,且else总是与离它最近的还未配对的if匹配。

3.2.1 程序题

输入a、b、c,求一元二次方程ax2+ bx+c=O的解

  • 输入:三个整数a、b、c。
  • 输出:方程ax2+bx+C=O的解(保留两位精度),a=O则输出“This is not a quadratic equation”。若有多个解,则多个解以空格分隔输出。
  • 注意:使用c/c++
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
    
    
	float a, b, c, delta, x1, x2, realpart, imagpart;
	scanf("%f%f%f", &a, &b, &c);
	if (fabs(a) > 1e-6) {
    
    
		delta = b * b - 4 * a * c;
		if (fabs(delta) <= 1e-6)   	printf("%.2f\n", -b / (2 * a)); //有两个相等的实数根
		else if (fabs(delta) > 1e-6) {
    
     //有两个实数根【x1,x2】
			x1 = (-b + sqrt(delta)) / (2 * a);
			x2 = (-b - sqrt(delta)) / (2 * a);
			printf("有两个实数根:x1=%.2f,x2=%.2f\n",  x1, x2);
		} else {
    
    //无实数根,是复数根
			realpart = -b / (2 * a);//复数根的实部
			imagpart = sqrt(-delta) / (2 * a);
			printf("复数根x1=%.2f+%.2fi,", realpart, imagpart);
			printf("复数根x2=%.2f-%.2fi\n", realpart, imagpart);
		}
	} else printf("This is not a quadratic equation\n");

	return 0;
}

3.3 关系运算符与关系表达式

  1. 关系运算符也称比较运算符,关系运算符都是双目运算符,其结合性均为左结合。
  2. 关系运算符的优先级低于算术运算符,高于赋值运算符。
  3. 在六个关系运算符中,“<、<=、>、>=”的优先级相同,“==、!=”的优先级相同,且前四个运算符优先级高于后两个运算符。

3.4 逻辑运算符与逻辑表达式

关系运算符可以方便地构成相对简单的条件判断,而逻辑运算符则可用来连接这些简单条

image-20220221165220900

image-20220221165235218

3.4.1 程序题

输入年份year,判断该年是否为闰年。输入:输入一个整数代表年份。

  • 输出∶若该年是闰年则输出 “year is a leap year”,否则输出 “year is not a leap year”.
  • 注意:使用c/c++
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	int year;
	scanf("%d", &year);
	if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))  printf("year is a leap year");
	else printf("year is not a leap year"); 
	return 0;
}

3.5 条件运算符与条件表达式

“?:”是条件运算符,条件表达式需要3个操作对象,“?”和“”一起出现在表达式中,条件运算符是C++中唯一的一个三目运算符。

使用条件表达式的一般形式为︰

<表达式1>?<表达式2>:<表达式3>

条件表达式的计算过程为︰

  1. 计算表达式1的值;
  2. 若表达式1的值为真(非0),则仅计算表达式2并将其结果作为整个表达式的值;
  3. 若表达式1的值为假(0)则仅计算表达式3并将其结果作为整个表达式的值。

3.6.switch语句

if…else…语句可清晰便捷地实现双分支选择,当需要处理的分支情况较多时,使用switch语句可使程序结构更清晰,执行速度更快。

switch常和关键词case、break、default等一起配合使用。switch语句的一般格式如下︰

switch (表达式)
{
    case常量表达式:[语句1][break;]
    ……
    case常量表达式n:[语句2][break;]
    default:[语句n+1]
}

说明

  1. switch语句中表达式的取值只能是整型、字符型、布尔型或枚举型。
  2. 花括号内是一个复合语句,内包含多个以关键字case开头的语句行和最多一个以default开头的行。
  3. case后面跟一个常量(或常量表达式,其取值类型与表达式类型一致),它们和default都是起标号作用,用来标志一个位置。
  4. 执行switch语句时,先计算switch后面的“表达式”的值,然后将它与各case标号比较,如果与某一个case标号中的常量相同,流程就转到此case标号后面的语句。如果没有与switch表达式相匹配的case常量,流程转去执行default标号后面的语句。
  5. 可以没有default标号,此时如果没有与switch表达式相匹配的case常量,则不执行任何语句。
  6. 各个case标号出现次序不影响执行结果。
  7. 任意两个case后的常量表达式取值必须不同,否则将导致冲突。
  8. case标号只起标记的作用。在执行switch语句时,根据switch表达式的值找到匹配的入口标号,在执行完一个case标号后面的语句后,就从此标号开始执行下去,不再进行判断。因此,一般情况下,在执行一个case子句后,应当用break语句使流程跳出switch结构。最后一个case子句(今为default子句)中可不加break语句。
  9. 在case子句中虽然包含了一个以上执行语句,但可以不必用花括号括起来,会自动顺序执行本case标号后面所有的语句。当然加上花括号也可以。
  10. break语句为可选项,用于终止switch 中的一个case,是否需要视具体情形而定。若某几个case子句后无break语句,则这几个case子句和随后紧跟的第一个带break语句的case子句共用一组执行语句。

4. 循环结构程序设计

4.1 while语句

image-20220221171052517

while语句是C/C++语言的一种基本循环模式,若条件为真则执行循环体,当条件为假时跳出循环执行后续语句。while的一般格式为:

while (表达式) 循环体

while语句的功能是:若表达式为真(非0值为真)则执行循环体;然后再重新判断表达式,…;如此周而复始,直至表达式为假(0值为假)则跳出该循环。因此

while语句本质上相当于循环判断执行的单分支if语句。while语句的执行流程图所示。

while语句中,循环体可以是一个简单语句,也可以是一个复合语句(用{}括起的多条语句),甚至是一条空语句;

表达式可以是关系表达式、逻辑表达式,甚至是数值表达式。

4.1.1 程序题

输入多组整数,每组数据包含两个整数a和b,对每组数据输出a+b的结果。

  • 输入:多组整数。
  • 输出:对每组数据输出a+b的结果。
  • 注意:使用c/c++
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	int a, b;
	//EOF其实就是-1
	while (scanf("%d%d", &a, &b) != EOF)
		cout << a + b << endl;
	return 0;
}

4.2 do-while语句

image-20220221172211764

do…while语句首先无条件执行一次循环体,然后再判断循环条件,若条件为真则再次执行循环体,…,直至循环条件为假则跳出循环。

do…while语句的一般格式为:

do 循环体
while (表达式);

do…while语句的循环体可以是一个简单语句,也可以是一个复合语句,甚至是一条空语句。do…while语句的执行流程图所示。

do…while语句和while语句非常相似,但后者是当型循环,前者是直到型循环。do-while语句保证其循环体至少会执行一次;

但是在while语句中,若起始时表达式为假则循环体一次也不会执行。

不过,若开始时表达式为真,则do…while和while效果完全相同。

4.2.1 程序题

输入正整数n,求使1+2+…+i>=n成立的最小i

  • 输入:一个正整数n。
  • 输出:使1+2+…+i>=n成立的最小i。
  • 注意:使用c/c++
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	int i = 0, sum = 0, n;
	scanf("%d", &n);
	do {
		i++;
		sum += i;
	} while (sum < n);
	printf("满足1+2+…+i>=n成立的最小i=%d", i);
	return 0;
}

4.3 for语句

for语句是C/C++的第三种循环控制实现方式。for语句的一般格式为:

for ( [表达式 1]; [表达式 2 ]; [表达式3] ) 
    循环体

表达式1 :是for语句执行的开始部分,通常用于为循环控制变量赋初值,仅在for语句运行开始时执行一次;

表达式2: 是for语句执行的结束部分,与while的判断表达式作用一致,用于设置for语句是否执行循环体的判断条件;

表达式3: 常用于每次执行循环体后改变循环变量值,以使for语句能趋向于结束。

for语句的循环体可以是一条简单语句,也可以是一个复合语句,甚至是一条空语句。for语句中的三个表达式均可省略,但应在其它相应位置有同等效用的功能

实现。

与while和do…while语句相比,for语句将循环变量赋初值、循环条件判断、循环变量修改三个操作浓缩体现在一对小括号中,不仅适用于循环次数确定的情形,

也适用于循环次数不确定的情形,使用起来更为方便灵活,因而是一种使用更为广泛的循环控制语句。

4.3.1 程序题

输入一个正整数n,判定它是否为素数(prime,又称质数)。

  • 输入:一个正整数n。
  • 输出:若n为质数则输出“Yes”,否则输出“No”。
  • 注意:使用c/c++
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	int num, sqNum, flag = 1;
	scanf("%d", &num);
	if (1 == num ) 	flag = 0; 
	sqNum = sqrt(num);
	for (int i = 2; i <= sqNum; i++) {
		if (num % i == 0)  flag = 0;
	}
	if (flag == 1) 	printf("Yse\n");
	else printf("No\n");
	return 0;
}

输入正整数n,求1-2/3+3/5-4/7+5/9-6/11+…的前n项和,结果保留3位小数

  • 输入:一个整数n。

  • 输出:求1-2/3+3/5-4/7+5/9-6/11+…的前n项和。

  • 注意:使用c/c++

#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	int n;
	//分子,当前项,分母,符号
	double item, num, deno, sum = 0, flag = -1;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		num = i;
		flag = -flag;
		deno = 2 * i - 1;
		item = flag * (num / deno);
		sum += item;
	}
	printf("结果为:%.3f", sum);
	return 0;
}

4.4 break/continue语句

break语句continue语句均用于改变循环执行的状态,但两者的含义和执行效用不同

break语句用于强制中断所属循环(若为for语句则表达式3也将不再运行)

continue语句用于跳过本次循环中还未运行的余下语句转而开始下一轮循环判断(若为for语句则执行表达式3后再开始下一轮循环判断

4.5 多重循环

若循环语句的循环体内又包含了另一循环语句,则称为多重循环或循环的嵌套。

4.5.1 程序题

公鸡五文一只,母鸡三文一只,小鸡一文三只,m文钱买m只鸡,三种鸡各买多少只

  • 输入:m。
  • 输出:公鸡、母鸡和小鸡的只数(若有多个解则仅输出字典序最小的解)。
  • 注意:使用c/c++
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	//i,j,k分别并表示的是数量
	int m, i, j, k;
	scanf("%d", &m);
	for (i = 0; i <= m / 5; i++)
		for (j = 0; j <= m / 3; j++) {
			k = m - i - j;//还差k只
			//i*5 + j*3 + k*1/3 = m ==>3*(i*5)+3*(j*3)+k = 3*m
			if (3 * (i * 5) + 3 * (j * 3) + k == 3 * m) {
				printf("结果为公鸡(%d元):%d只,母鸡(%d元):%d只,小鸡(%d元):%d只", i * 5, i, j * 3, j, k / 3, k);
				return 0;
			}
		}
	return 0;
}

4.6 算法执行效率

算法是对特定问题的解决方法的求解步骤描述,程序是依据算法思想采用某种编程语言书写的具体代码。

一个算法具有有穷性、确定性、可行性、输入、输出5大特性

  1. 有穷性:一个算法应在有限步骤之内结束;
  2. 确定性:算法的每一步骤应清晰准确无歧义;
  3. 可行性:算法中的任何步骤均可分解为基本的可执行操作;
  4. 输入:一个算法有0个或多个输入,以从外界获取所需的必要信息;
  5. 输出:一个算法有一个或多个输出,以反映对输入数据加工处理后的结果。

一个好的算法应达到以下目标

  1. 正确性:分为语法正确和逻辑正确两个层面;
  2. 可读性:便于人阅读、有助于阅读者对算法的理解;
  3. 健壮性:对任意非法输入应该有合适的处理策略;
  4. 高效性:花费较少的时间代价和空间代价。

由于现代计算机具有较大的存储容量,因此,算法效率通常以依据该算法所编制程序在计算机上执行消耗的时间来度量。

度量一个程序的执行时间通常有两种方法

  1. 事后统计:依赖于具体的硬件、软件、环境因素,统计数据很难达到客观准确。
  2. 事前分析:计算基本语句执行频度随着问题规模n增长的函数f(n)。
  • 找出语句频度最大的语句作为基本语句;
  • 计算基本语句的频度得到问题规模n的某个函数f(n);
  • 取函数f(n)数量级用符号“O”表示法执行时间关于问题规模n的增长关系。

时间复杂度描述符O 用来描述增长率的上界,表示f(n)的增长速度不快于O。该上界的阶越低就越有价值,称之为“紧凑上界”。

例:f(n)=10n2+4n+2,其时间复杂度为O(n2)。

在估算出算法的时间复杂度之后,将数值可能的最大取值代入复杂度的渐近式中,就能简单地判断算法是否能够满足运行时间限制的要求。

例如

假设题目描述中的限制条件为n≤1000,考虑时间复杂度为O(n2)的算法,将n=1000代入得到106;考虑时间复杂度为O(n3)的算法,将n=1000代入得到109。在结果

数值基础上,可以结合表进行判断。

image-20220221183615273

#include <time.h>
#include <iostream>
using namespace std;
int main() { 
	int i, k = 0;
	int n = 1e8;
	clock_t start, end;
	start = clock();
	for(i = 0; i < n; i++)
		k++;
	end = clock();
	cout << (double)(end - start) / CLOCKS_PER_SEC << endl;
}

4.7 程序题

如果你是在复习或者预习,以下的习题可以放慢节奏,仔细想想还有没有其他的方法,思想很重要,注重实践。

输入两个正整数m和n,求m^n%1007的值(限时1秒)

  • 两个正整数m(1<m<10)和n(1<n≤10^9)
  • 输出:m^n对1007取余后的结果
  • 注意:使用c/c++
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	//i,j,k分别并表示的是数量
	int m, n, result = 1;
	//比如输入的是 3^8%1007     m=3,n=8
	scanf("%d%d", &m, &n); 
	//指数不能为0
	//(3^8)=(3^4*3^4)=(9^4)=(9^2*9^2)=(81^1)这是一个快速幂
	//注意取MOD运算,需要对m = m * m 与 结果都做MOD运算
	while (n != 0) {
		//!=0可以省略,这里判断是奇次方数 比如(81^1)指数就是1了
		if (n % 2) result = result * m % 1007;//可以划分为两步:result = result * m; result = result % 1007;
		n = n / 2;		//等价于n >>= 1;
		m = m * m % 1007;//可以划分为两步:m = m * m; m = m % 1007;
	}
	printf("结果%d", result);
	return 0;
}

给定一个整数,请将该数各个数位上的数字反转得到一个新数。新数也应满足整数的常见形式,即除非给定的原数为零,否则反转后得到的新数的最高位数字不应为零

  • 输入:一个十进制整数。(-690)
  • 输出:对应的反转数。(-96)
  • 注意:使用c/c++
#include<iostream>
using namespace std;
int main(){
	int x, y=0;
	cin >> x;
	while (x != 0)	{
		y = y * 10 + x % 10;
		x = x / 10;
	}
	cout << y;
	return 0;
}

小明买了许多圣诞礼物准备用于班级活动,回家后感觉太累了,便让机器人小灵帮忙数一下礼物一共有多少份。但是小灵不喜欢数字4,因此每次数到4时便跳过该数。例,若小灵数到639时,下一份礼物小灵就会数650

  • 输入:一个整数num,表示小灵给出的礼物的份数,1 < num < 1000,且一定不含整数4
  • 输出:一个整数为礼物的实际份数
  • 注意:使用c/c++
#include<iostream>
using namespace std;
int main() {
	int num, w = 1, sum = 0;
	cin >> num;
	while (num > 0)  	{
		int b = num % 10;
		if (b > 4) b--;
		sum = sum + b * w;
		w = w * 9;
		num = num / 10;
	}
	cout << sum << endl;
	return 0;
}

分解质因数

  • 输入:一个正整数n。(36)
  • 输出:n的质因数的乘积形式。(2*2*3*3)
  • 注意:使用c/c++
#include<iostream>
using namespace std;
int main() {
    int n, i = 2;
    cin >> n;
    while (n != 1)	{
        while (n % i == 0)		{
            cout << i;
            n = n / i;
            if (n != 1) cout << "*";
        }
        i++;
    }
	return 0;
}

输入n和a,求a+aa+aaa+…aa…a(n个a),如当n=3,a=2时,2+22+222=246

  • 输入:包含两个整数,n和a,含义如上述,n和a都是小于10的非负整数
  • 输出:输出前n项和,单独占一行
  • 注意:使用c/c++
#include<iostream>
using namespace std;
int main(){
	int n, a, sum=0;
	cin >> n >> a;
 	int item = 0;
 	for (int i = 1; i <= n; i++) 	{
		item = item * 10 + a;
		sum = sum + item; 
 	}
 	cout << sum << endl;
 	return 0;
}

妈妈给了小明m元零花钱,为了鼓励小明节约,说如果小明每天只消费1元,每花k元就可以得到1元额外奖励,如果听妈妈的话小明最多可以花多少天

  • 输入:输入2个整数m、k(1<k <m<1000)
  • 输出:输出一个整数,表示m元可以消费的天数
  • 注意:使用c/c++
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	//总钱数,花费的,天数
	int m, k, sum = 0;
	cin >> m >> k;
	//排除一次性花完,当花的钱小于总钱数的时候进入循环
	while (m >= k)	{ 
		//天数
		sum = sum + (m - m % k);
		//得到奖励的钱
		m = m / k + m % k;
	}
	sum = sum + m;
	cout << sum << endl;
	return 0;
}

有一辆智能小车,最初(时间为0)的位置为(0,0),我们想知道它最后的位置。

小车以每小时10公里的速度向北移动(北为y轴正向,东为x轴正向)。小车会收到一系列依照时间戳记排序的命令,1表示“向左转”,2表示“向右转”,3表示“停止”。每个命令的前面有一个时间戳记,所以知道该命令是何时发出的。最后一个命令一定是“停止”。另外假设,这辆小车非常灵活,它可以在瞬间转弯

  • 输入:输入包含多个命令,每个命令由整数time和command组成,表示在时刻time发出命令command。command的取值范围1-3,含义如上所述。
  • 输出:输出占一行,包含两个整数,表示小车的最终位置。两个整数之间由空格隔开
#include<iostream>
using namespace std;
int main() {
	int x = 0, y = 0;
	int command; 	//存储命令号
	int preTime = 0, curTime; //preTime和curTime存储上次发命令时间和本次发命令时间
	int direction = 0;	//存储当前方向
	while(cin >> curTime >> command) {
		switch(direction) {
			case 0: y = y + (curTime - preTime) * 10; break; 	//0表示y轴正向
			case 1: x = x - (curTime - preTime) * 10; break; 	//1表示x轴负向
			case 2: y = y - (curTime - preTime) * 10; break; 	//2表示y轴负向
			case 3: x = x + (curTime - preTime) * 10; break; 	//3表示x轴正向
		}
		switch(command) 	{
			case 1: direction++; break;	//向左转,方向增1
			case 2: direction--; break;	//向右转,方向减1
		}  
		direction = (direction + 4) % 4; 	//方向号对4取模,保持在0-3范围内
 		preTime = curTime;  
	}
	cout << x << " " << y;
	return 0;
}

5. 数组

5.1 一维数组

5.1.1 定义与引用

在 C语言 中使用数组必须先定义后使用,定义一维数组的一般格式如下:

类型说明符 数组名[常量表达式];

其中,类型说明符是任意一种基本数据类型或构造数据类型,它定义了全体数组成员的数据类型;

数组名是标识数组的名称,与变量命名规则相同;

方括号中的常量表达式代表数组元素的个数,也称为数组的长度。

注意,数组元素的下标从0开始计算。例如:

int a[10];     		//定义一个10个元素的整型数组a,其元素是a[0]~a[9]
double b[10];		//定义一个10个元素的浮点型数组b,其元素分别是b[0]~b[9]
char c[30]; 		//定义一个30个元素的字符数组c,其元素是c[0]~c[19]

5.1.2 初始化

可以在定义数组时为元素赋初值,若定义数组时未赋初值则元素值是无意义的随机值。

若仅给部分元素赋初值,则整型数组中未被赋值的元素自动赋值0,字符数组中未被赋值的元素自动赋值'\0'。如:

int a[10]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9};      // 全体元素赋值,a[0]~ a[9]分别赋值为0~9
int a[10]={1, 2, 3};						//前3个元素赋值为1,2,3,后7个元素值为0
char c[20]={'C', '+', '+'};				     //前3个元素赋值,其余元素值为'\0‘
int a[3]={1, 2, 3, 4};						//赋值数目不允许超过数组容量,否则编译时会报错

5.1.3 程序题

输入一个非负十进制整数,将其转换为二进制形式输出

  • 输入:一个非负整数n (0<=n<2^31)
  • 输出:对应的二进制形式
  • 注意:使用c/c++
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() { 
	int n, i, a[32];
	scanf("%d", &n);
	//!=0可以省略
	if (n) {
		for (i = 0; n != 0; i++) {
			a[i] = n % 2;
			n /= 2;
		}
		for (i--; i >= 0; i--) cout << a[i];
	} else printf("0");
	return 0;
}

一个非递减有序的整型数组有n个元素,给定一个整数num,将num插入该序列的适当位置,使序列仍保持非递减有序

  • 输入:输入有三行。第一行是一个正整数n(n<=1000)。第二行是n个整数,第三行是待插入整数num
  • 输出:输出非递减有序的n+1个整数,数据之间用空格隔开。输出占一行
  • 注意:使用c/c++
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	int n, num, i, arr[1001];
	scanf("%d", &n);
	for (int i = 0; i < n; i++) scanf("%d", &arr[i]);
	scanf("%d", &num);
	for ( i = n - 1; i >= 0 && arr[i] > num ; i--) arr[i + 1] = arr[i];
	arr[i + 1] = num;
	for (i = 0; i <= n; i++) {
		cout << arr[i]<<" ";
	}
	return 0;
}

5.2 数组排序

在实际开发中,经常需要将数组元素按照从大到小(或者从小到大)的顺序排列,这样在查阅数据时会更加方便直观。

数组元素进行排序的方法有很多种,如选择排序、冒泡排序、插入排序、快速排序、归并排序等。

5.2.1 插入排序[稳定]

插入排序,又叫直接插入排序

在待排序的元素中,假设前n-1个元素已有序,现将第n个元素插入到前面已经排好的序列中,使得前n个元素有序。按照此法对所有元素进行插入,直到整个序列有序,即通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

但我们并不能确定待排元素中究竟哪一部分是有序的,所以在代码逻辑上我们一开始认为第一个元素是有序的,然后依次将其后面的元素插入到这个有序序列中来,直到整个序列有序为止。

#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	int n = 6,  a[] = {1, 15, 55, 0, 99, 11};
	for (int i = 0; i < n - 1; i++) {
		//单趟插入
		int end = i;//记录有序序列的最后一个元素的下标
		int tmp = a[end + 1];//待插入的元素
		while (end >= 0) {
			if (tmp < a[end]) {
				a[end + 1] = a[end];
				end--;
			} else 	break;
		}
		//本逻辑包含如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面
		a[end + 1] = tmp;
	}
	for (int i = 0; i < n; i++) {
		cout << a[i] << " ";
	}
	return 0;
}

5.2.2 希尔排序[不稳定]

希尔排序法又称缩小增量法

希尔排序以其设计者希尔的名字命名,他对直接插入排序的时间复杂度进行分析,得出了以下结论:

  • 1.插入排序的时间复杂度最坏情况下为O(n2),此时待排序列为逆序,或者说接近逆序。
  • 2.插入排序的时间复杂度最好情况下为O(n),此时待排序列为升序,或者说接近升序。

若是能先将待排序列进行一次预排序,使待排序列接近有序(接近我们想要的顺序),然后再对该序列进行一次直接插入排序。那么只要控制预排序阶段的时间复杂度不超过O(n2),那么整体的时间复杂度就比直接插入排序的时间复杂度低了,即要比直接插入排序最坏情况更优。

#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
	int n = 6,  a[] = {1, 15, 55, 0, 99, 11};
	int gap = n;
	while (gap > 1) {
		//gap越大,数据挪动得越快;gap越小,数据挪动得越慢。前期让gap较大,可以让数据更快得移动到自己对应的位置附近,减少挪动次数。
		gap = gap / 2; //gap折半
		for (int i = 0; i < n - gap; i++) {
			//单趟
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0) {
				if (tmp < a[end]) {
					a[end + gap] = a[end];
					end -= gap;
				} else
					break;
			}
			a[end + gap] = tmp;
		}
	}
	for (int i = 0; i < n; i++)  cout << a[i] << " ";
	return 0;
}

5.2.3 选择排序[不稳定]

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

步骤:

  • 在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合中剩余1个元素

int main() {
	int num1[10] = { 10, 56, 73, 9, 15, 36, 22, 89, 115, 3 }; // 创建一个数组,存放需要排序的数字
	int min = num1[0];  // 假设第一个元素为最小值
	int min_Index = 0;  // 最小值下标
	int temp;
	for (int i = 0; i < 10; i++) {   // 外层循环控制比较轮数
		min = num1[i];           // 假设第一个元素为最小值,为了方便下面找最小值
		min_Index = i;
		for (int j = i + 1; j < 10; j++) { // 内层循环控制第i轮后需要比较的次数
			if (num1[j] < min) {      // 按照从小到大顺序排序
				min = num1[j];    // 找最小值
				min_Index = j;
			}
		}
		temp = num1[min_Index];     // 把取得的最小值和第一个数相交换
		num1[min_Index] = num1[i];
		num1[i] = temp;
		//第二轮循环从第二个数开始相同原理再找最小值,直到结束!
	}
	for (int i = 0; i < 10; i++) cout << num1[i] << " "; 
	return 0;
}

5.2.4 堆排序[不稳定]

先根据数组建立最大堆,然后依次将堆顶元素(最大值)放置在堆的末尾,交换后新的堆顶R[1]可能违反堆的性质,因此重新堆化(忽略已经排序好的数组),再次得到堆顶元素(最大值),放置在堆的末尾,重复n-1次

  • 步骤一:建立大根堆–将n个元素组成的无序序列构建一个大根堆

  • 步骤二:交换堆元素–交换堆尾元素和堆首元素,使堆尾元素为最大元素

  • 步骤三:重建大根堆–将前n-1个元素组成的无序序列调整为大根堆

    重复执行步骤二和步骤三,直到整个序列有序

步骤一:建立大根堆

  1. 无序序列建立完全二叉树

  1. 从最后一个叶子节点开始,从左到右,从下到上调整,将完全二叉树调整为大根堆
    1. 找到第1个非叶子节点6,由于6的右子节点9比6大,所以交换6和9。交换后,符合大根堆的结构。
    2. image-20220222214305033
    3. 找到第2个非叶子节点4,由于的4左子节点9比4大,所以交换4和9。交换后不符合大根堆的结构,继续从右到左,从下到上调整。
    4. image-20220222214354371

步骤二:交换堆元素(交换堆首和堆尾元素--获得最大元素)

image-20220222214433918

步骤三:重建大根堆(前n-1个元素)

image-20220222214459278

重复执行步骤二和步骤三,直到整个序列有序

image-20220222214537273
在这里插入图片描述

#include <iostream> 
#include<vector>
using namespace std;
// 递归方式构建大根堆(len是arr的长度,index是第一个非叶子节点的下标)
void adjust(vector<int> &arr, int len, int index){
	int left = 2*index + 1; // index的左子节点
	int right = 2*index + 2;// index的右子节点
	
	int maxIdx = index;
	if(left<len && arr[left] > arr[maxIdx])     maxIdx = left;
	if(right<len && arr[right] > arr[maxIdx])     maxIdx = right;
	
	if(maxIdx != index)	{
		swap(arr[maxIdx], arr[index]);
		adjust(arr, len, maxIdx);
	} 
}

// 堆排序
void heapSort(vector<int> &arr, int size)
{
	// 构建大根堆(从最后一个非叶子节点向上)
	for(int i=size/2 - 1; i >= 0; i--)	{
		adjust(arr, size, i);
	}
	
	// 调整大根堆
	for(int i = size - 1; i >= 1; i--)	{
		swap(arr[0], arr[i]);           // 将当前最大的放置到数组末尾
		adjust(arr, i, 0);              // 将未完成排序的部分继续进行堆排序
	}
}

int main()
{
	vector<int> arr = {8, 1, 14, 3, 21, 5, 7, 10};
	heapSort(arr, arr.size());
	for(int i=0;i<arr.size();i++)
	{
		cout<<arr[i]<<" ";
	}
	return 0;
}

5.2.5 冒泡排序[稳定]

冒泡排序可能是最简单粗暴的排序算法。冒泡排序不断遍历整个数组比较相邻的两个元素的大小是否符合最终要求的顺序,如果不符合则交换两个元素的位置,一直向后遍历,直到遍历完数组,这个过程就像泡泡浮出水面一样,所以被称为冒泡排序。需要注意循环的终止条件的选择

  • 时间复杂度:O(n2) 空间复杂度:O(1)

#include <iostream>
using namespace std;
int main() {
	int arr[10] = {8, 1, 9, 7, 2, 4, 5, 6, 10, 3}, n = 10;
	for (int i = 0; i < n - 1; i++) {
		for (int j = 0; j < n - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
	for (int j = 0; j < n; j++) cout << arr[j] << " ";
}

5.2.6 快速排序[不稳定]

快排是一种高效的排序方式,是Hoare于1962年提出的一种二叉树结构的交换排序算法。
从无序队列中挑取一个元素作为基准值,把无序队列分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行分割,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
简单来说:挑元素、划分组、分组重复前两步

  1. 选出一个key,一般是最左边或是最右边的。

  2. 定义一个L和一个R,L从左向右走,R从右向左走。

3.(我们选取最左边的值作为key进行分析)在走的过程中,若R遇到小于key的数,则停下,L开始走,直到L遇到一个大于key的数时,将L和R的内容交换,R再次开始走,如此进行下去,直到L和R最终相遇,此时将相遇点的内容与key交换即可。经过一次单趟排序,最终使得key左边的数据全部都小于key,key右边的数据全部都大于key。

需要注意的是:若选择最左边的数据作为key,则需要R先走,这样可以保证相遇点的值比key小,与key交换后满足条件;若选择最右边的数据作为key,则需要L先走,这样可以保证相遇点的值比key大,与key交换后满足条件。

  1. 然后我们在将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,此时序列可以认为是有序的。

#include<iostream>
using namespace std;
void quickSort(int a[], int left, int right) {
	if (left < right) { //判断是否满足排序条件,递归的终止条件
		int i = left, j = right;   //把待排序数组元素的第一个和最后一个下标分别赋值给i,j,使用i,j进行排序;
		int x = a[left];    //将待排序数组的第一个元素作为哨兵,将数组划分为大于哨兵以及小于哨兵的两部分
		while (i < j) {
			while (i < j && a[j] >= x) j--; //从最右侧元素开始,如果比哨兵大,那么它的位置就正确,然后判断前一个元素,直到不满足条件
			if (i < j) a[i++] = a[j]; //把不满足位次条件的那个元素值赋值给第一个元素,(也即是哨兵元素,此时哨兵已经保存在x中,不会丢失)并把i的加1
			while (i < j && a[i] <= x) i++; //换成左侧下标为i的元素开始与哨兵比较大小,比其小,那么它所处的位置就正确,然后判断后一个,直到不满足条件
			if (i < j) a[j--] = a[i]; //把不满足位次条件的那个元素值赋值给下标为j的元素,(下标为j的元素已经保存到前面,不会丢失)并把j的加1
		}
		a[i] = x;   //完成一次排序,把哨兵赋值到下标为i的位置,即前面的都比它小,后面的都比它大
		quickSort(a, left, i - 1); //递归进行哨兵前后两部分元素排序 , low,high的值不发生变化,i处于中间
		quickSort(a, i + 1, right);
	}
}

int main() {
	int a[10] = {8, 1, 9, 7, 2, 4, 5, 6, 10, 3};
	quickSort(a, 0, 9);
	for (int j = 0; j < 10; j++)	cout << a[j] << " ";
}

5.2.7 归并排序[稳定]

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。

时间复杂度为O(nlogn),空间复杂度为 O(n),归并排序比较占用内存,但效率高且稳定。

#include<iostream>
using namespace std;
void Merge(int arr[], int low, int mid, int high) {
	//low为第1有序区的第1个元素,i指向第1个元素, mid为第1有序区的最后1个元素
	int i = low, j = mid + 1, k = 0; //mid+1为第2有序区第1个元素,j指向第1个元素
	int *temp = new (nothrow) int[high - low + 1]; //temp数组暂存合并的有序序列
	if (!temp) { //内存分配失败
		cout << "error";
		return;
	}
	while (i <= mid && j <= high) {
		if (arr[i] <= arr[j]) //较小的先存入temp中
			temp[k++] = arr[i++];
		else
			temp[k++] = arr[j++];
	}
	while (i <= mid) //若比较完之后,第一个有序区仍有剩余,则直接复制到t数组中
		temp[k++] = arr[i++];
	while (j <= high) //同上
		temp[k++] = arr[j++];
	for (i = low, k = 0; i <= high; i++, k++) //将排好序的存回arr中low到high这区间
		arr[i] = temp[k];
	delete []temp;//删除指针,由于指向的是数组,必须用delete []
}

//用递归应用二路归并函数实现排序——分治法
void MergeSort(int arr[], int low, int high) {
	if (low < high) {
		int mid = (low + high) / 2;
		MergeSort(arr, low, mid);
		MergeSort(arr, mid + 1, high);
		Merge(arr, low, mid, high);
	}
}

int main() {
	int a[10] = {5, 1, 9, 3, 7, 4, 8, 6, 0, 2};
	MergeSort(a, 0, 9);
	for (int i = 0; i < 10; i++)
		cout << a[i] << " ";
	return 0;
}

5.2.8 计数排序

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序O(n),计数排序要求输入的数据必须是有确定范围的整数。(直方图统计,再按照顺序扔出来)

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

20190611232645801

#include <iostream>
#include <vector>
using namespace std;
void CountSort(vector<int> &arr, int maxVal) {
	int len = arr.size();
	if (len < 1)
		return;
	vector<int> count(maxVal + 1, 0);
	vector<int> tmp(arr);
	for (auto x : arr)
		count[x]++;
	for (int i = 1; i <= maxVal; ++i)
		count[i] += count[i - 1];
	for (int i = len - 1; i >= 0; --i) {
		arr[count[tmp[i]] - 1] = tmp[i];
		count[tmp[i]]--;				//注意这里要减1
	}
}

int main() {
	vector<int> arr = { 1, 5, 3, 7, 6, 2, 8, 9, 4, 3, 3 };
	int maxVal = 9;
	CountSort(arr, maxVal);
	for (auto x : arr) cout << x << " ";
	return 0;
}

5.3 字符数组与字符串

5.3.1 字符数组的初始化

在C语言中,字符数组是字符类型元素构成的数组。定义字符数组的一般格式如下:
char 数组名[常量表达式];
对于字符数组的初始化,可以在定义时对每个元素逐一初始化,也可以在定义时直接用双引号引起来的一串字符实现初始化。例如:
char a[10]={‘1’ , ‘2’ , ‘3’ , ‘4’ , ‘5’ , ‘6’ };
char a[10]={“123456”};
以上是两个等价的字符数组初始化定义。但是,在使用双引号形式初始化时,字符串常量的字符个数必须比所定义的数组容量少一个以上。//char a[10]=“123456”;
因为此种情况下,系统会自动在有效字符结束时附加一个‘\0’作为结束标志。
在编写程序时,通常依据当前字符与‘\0’是否相等来判断字符串是否结束。

5.3.2 字符串的输入输出

C语言不提供“字符串”数据类型,而是运用一个字符数组来模拟存放一个字符串,只是在字符串的有效字符结束时附加一个字符‘\0’作为结束。
字符数组可以使用格式符“%c”逐个输入或输出一个字符,也可以使用格式符“%s”一次输入或输出整个字符串

例如:

char c[100]={“I love c++ programming!”};
printf(“%s\n”, c);	//从第0个字符起依次逐个输出当前字符,若当前字符为‘\0’则结束

说明:

  1. 使用格式符“%s”输出字符串时,printf的输出项是字符数组名,代表字符串的起始地址。不能写成“printf(“%s\n”, c[0]);”,否则编译时会报错。
  2. 输出时从字符数组的第0个字符起依次逐个输出当前字符,第一次遇到当前字符为‘\0’时输出自动结束,且输出字符不包括‘\0’。
  3. 可以使用scanf函数输入一个字符串,如“scanf(“%s”, c);”,系统检测输入的字符串,遇到换行或空格时自动加一个‘\0’字符作为结束标志。
  4. 若要输入其中含有空格的字符串,则须使用“gets©;”;除此之处,输出字符串时可使用“puts©;”,其功能与语句“printf(“%s\n”, c);”等价。

5.3.3 C语言的字符串处理函数

image-20220222223345711

5.3.4 C++的字符串处理

C++兼容C的字符串表示与处理方式,同时引入了string类表示字符串类型,因此C++中可以直接定义一个字符串变量

C++中的字符串使用方式如下:

string s1 = "c ", s2 = "programming";	//定义字符串变量s1, s2并赋初值
s1 = "c++ ";	// s1重新赋值
s1 = s1+s2;	// s1值更改为"c++ programming ",注意两个字符串常量不能相加

C++支持"cin>>s1;"输入字符串变量的值,同时能够直接利用6种比较关系运算符实现字符串之间的按字典序比较。string类的主要函数与运算见表

image-20220222223520258

5.4 二维数组

5.4.1 定义与引用

二维数组本质上是以一维数组为元素的数组,即“数组的数组”。二维数组又称为矩阵,行列数相等的矩阵称为方阵。二维数组的一般格式为:
类型说明符 数组名[常量表达式1][常量表达式2];
表达式1代表二维数组的行数,表达式2代表二维数组的列数。C++中,二维数组在内存中按行优先顺序存放。注意,元素的行下标和列下标均是从0开始计算。例如:

int a[10][10];     //定义一个10行10列的整型二维数组a,其元素是a[0][0]~a[9][9]

5.4.2 初始化

可以在定义二维数组时给数组元素初始化赋值,若仅给部分元素赋初值,则对int数组而言未被赋值的元素自动赋值0,对char数组而言未被赋值的元素自动赋值’\0’。

如:

int a[2][3]={
   
   {1,2,3},{4,5,6}};	//每个花括号中3个数据对应矩阵的1行
int a[2][3]={1,2,3,4,5,6};		//按行优先给二维数组元素赋值,效果同上
int a[][3]={1,2,3,4,5,6};			//行可缺省,系统据总数目与第2维长度计算第1维长度
int a[][3]={
   
   {1,2,3},{},{4,5,6}};	//行可缺省,系统只对部分元素赋值
int a[2][3]={
   
   {1},{4}};				//部分元素赋初值,除a[0][0]=1, a[1][0]=4外其余元素赋值0

输入一个日期,输出该日期是所在年的第几天

  • 输入:输入一个日期

  • 输出:输出该日期是所在年的第几天

  • 注意:使用c/c++

#include<iostream>
using namespace std;
int main(){
	int year, month, day, leap = 0, shift[2][13] = {
   
   {0,31,28,31,30,31,30,31,31,30,31,30,31},
	                  {0,31,29,31,30,31,30,31,31,30,31,30,31}};
	cin >> year >> month >> day;
	if (year%4 == 0 && year%100 != 0 || year%400 == 0) leap = 1;
	for (int i = 1; i < month; i++) day += shift[leap][i];
	cout << day << endl;
	return 0;
}

输出杨辉三角形

  • 输入:第一行输入一个整数 n (1<=n<=10)
  • 输出:输出杨辉三角形的前n行,每个数字占8格左对齐
  • 注意:使用c/c++
#include <iostream>
using namespace std;
int const N = 10;
int main()
{
	int n, a[N][N];
	cin >> n;
	for (int i = 0; i < n; i++) {
		a[i][0] = 1;
		a[i][i] = 1;
	}
	for (int i = 2; i < n; i++)
		for (int j = 1; j <= i-1; j++)
			a[i][j] = a[i-1][j-1] + a[i-1][j];
	for (int i = 0; i < n; i++) {
		for (int j = 0; j <= i; j++) printf("%-8d", a[i][j]);
		printf("\n");
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_43216714/article/details/123079190
今日推荐