《算法笔记》第二章——语言基础 学习记录
浮点型
通俗来讲,浮点型就是小数,一般可以分为单精度(float) 和双精度(double)。
- 对单精度float来说,一个浮点数占用32bit,其中1bit 作为符号位、8bit 作为指数位、23bit作为尾数位(了解即可),可以存放的浮点数的范围是\(-2^{128} \sim +2^{128}\),但是其有效精度只有6 ~ 7位(由23可以得到,读者只需要知道6~ 7位有效精度即可)。这对一些精度要求比较高的题目是不合适的。定义举例:
float fl;
float fl= 3.1415;
- 对双精度double来说,一个浮点数占用64bit,其中依照浮点数的标准,1bit 作为符号位、11bit 作为指数位、52bit 作为尾数位,可以存放的浮点数的范围是\(-2^{1024} \sim +2^{1024}\), 其有效精度有15~16位,比float优秀许多。示例如下:
double db;
double db = 3. 1415926536;
tips
常量后的LL不会被typedef替换。
取模运算符的优先级和除法运算符相同。与除法运算符一样,除数不允许为0。
%f是float和double型的输出格式。
\0代表空字符NULL,其ASCII码为0,注意\0不是空格。
字符串常量可以作为初值赋给字符数组,并使用%s的格式输出。
整型常量在赋值给布尔型变量时会自动转换为true(非零)或者false(零)。注意:“非零”是包括正整数和负整数的,即1和-1都会转换为true。
位运算符的优先级没有算术运算符高,使用时注意加括号。
复合赋值运算符在程序中会被经常使用,并且可以加快编译素的、提高代码可读性。
在scanf中,除了char数组整个输入的情况不加&之外,其他变量类型都需要加&。这是因为数组比较特殊,数组名称本身就代表了这个数组第一个元素的地址,所以不需要再加地址运算符。
除了%c外,scanf对其他格式符(如%d)的输入是以空白符(即空格、换行)为结束判断标志的,因此除非使用%c把空格按字符读入,其他情况都会自动跳过空格。另外,字符数组使用%s读入的时候以空格跟换行为读入结束的标志。
#include<stdio.h>
int main() {
char str[10] ;
scanf("%s",str) ;
printf ("%s",str) ;
return 0;
}
输入数据:
abcd efg
输出结果:
abcd
如果想要输出'%','',则需要在前面再加一个%或\,例如下面的代码:
printf("%%");
printf("\\");
三种实用的输出格式
- %md
%md 可以是不足m位的int型变量以m位进行右对齐输出,其中高位用空格补齐;如果变量本身超过m位,则保持原样。 - %0md
%0md只是在%md中间多加了0。和%md的唯一不同点在于,当变量不足m位时,将在前面补足够数量的0而不是空格。 - %.mf
%.mf可以让浮点数保留m位小数输出,这个“保留”使用的是精度的“四舍六入五成双”规则(具体细节不必掌握)。很多题目都会要求浮点数的输出保留××位小数,或是精确到小数点后××位),就是用这个格式来进行输出(如果是四舍五入,那么需要用到后面会介绍的round函数)。
typedef
typedef 是一个很有用的东西,它能给复杂的数据类型起一个别名,这样在使用中就可以用别名来代替原来的写法。
常用的math函数
- fabs(double x)
该函数用于对double型变量取绝对值。 - floor(double x)和ceil(double x)
这两个函数分别用于double型变量的向下取整和向上取整,返回类型为double型。 - pow(double r,double p)
该函数用于返回\(r^p\),要求r和p都是double型。 - sqrt(double x)
该函数用于返回double型变量的算术平方根。 - log(double x)
该函数用于返回double型变量的以自然对数为底的对数。
使用换底公式来将不是以自然对数为底的对数转换为以e为底的对数,即\(log_ab=\frac{log_eb}{log_ea}\) - sin(double x)、cos(double x)、tan(double x)
这三个函数分别返回double型变量的正弦值、余弦值和正切值,参数要求是弧度制。 - asin(double x)、acos(double x)、atan(double x)
这三个函数分别返回double型变量的反正弦值、反余弦值和反正切值。 - round(double x)
该函数用于将double型变量x四舍五入,返回类型也是double型,需进行取整。
if语句
if(n)表示if(n != 0),if(!n)表示if(n == 0)。
switch语句
每个case下属的语句都没有使用大括号将它们括起来,这是由于case本身默认会把两个case之间的内容全部作为上一个case的内容,因此不用加大括号。
break的作用在于可以结束当前swithc语句,如果将其删去,则程序将会从第一个匹配的case开始执行语句,知道其下面的所有语句都执行完毕才会退出switch。
数组
如果根据一些条件,可以不断让后一位的结果由前一位或前若干位计算得来,那么就把这种做法称为递推。
递推可以分为顺推和逆推两种。
冒泡排序
冒泡排序的本质在于交换,即每次通过交换的方式把当前剩余元素的最大值移动到一端。
整个过程执行\(n-1\)趟,每一趟从左到右依次比较相邻的两个数,如果大的数在左边,则交换这两个数,当该趟结束时,该趟最大数被移动到当前剩余数的最右边。
如果数组大小较大(大概\(10^6\)级别),则需要将其定义在主函数外面,否则会使程序异常退出,原因是函数内部申请的局部变量来自系统栈,允许申请的空间较小;而函数外部申请的全局变量来自静态存储区,允许申请的空间较大。
memset
使用memset需要在程序开头添加string.h文件,且只建议初学者使用memset赋0或-1。
这是因为memset使用的是按字节赋值,即对每个字节赋同样的值,这样组成int型的4个字节就会被赋成相同的值。
而由于0的二进制补码为全0,-1的二进制补码为全1,不容易出错。
如果要对数组赋其他数字(例如1),那么请使用fill函数(但memset的执行速度快)。
gets输入、puts输出
gets用来输入一行字符串(注意:gets识别换行符\n作为输入结束,因此scanf完一个整数后,如果要使用gets,需要先用getchar接收帧数后的换行符),并将其存放于一维数组(或二维数组的一维)中,puts用来输出一行字符串,即将一维数组(或二维数组的一维)在界面上输出,并紧跟一个换行。
字符数组
在一维字符数组(或是二维字符数组的第二维)的末尾都有一个空字符\0,以表示存放的字符串的结尾。
空字符\0在使用gets或scanf输入字符串时会自动添加在输入的字符串后面,并占用一个字符位。
而puts与printf就是通过识别\0作为字符串的结尾来输出的。
特别提醒1:结束符\0的ASCII码为0,即空字符NULL,占用一个字符位,因此开字符数组的时候千万要记得字符数组的长度一定要比实际存储字符串的长度至少多1。注意: int 型数组的末尾不需要加0,只有char型数组需要。还需要注意\0跟空格不是同一个东西,空格的ASCII码是32,切勿混淆。
特别提醒2:如果不是使用scanf函数的%s格式或gets函数输入字符串(例如使用getchar), 请一定要在输入的每个字符串后加入“\0”, 否则printf和puts输出字符串会因无法识别字符串末尾而输出一大堆乱码。
string.h头文件
- strlen()
strlen()函数可以得到字符数组中第一个\0前的字符的个数。 - strcmp()
strcmp函数返回两个字符串大小的比较结果,比较原则是按字典序。所谓字典序就是字符串在字典中的顺序,因此如果有两个字符数组str1和str2,且满足str1[0...k-1] == str2[0...k-1]、str1[k]<str2[k],那么就说str1的字典序小于str2.例如“a”的字典序小于“b”、“aaaa”的字典序小于“aab”。- 如果字符数组1 < 字符数组2,则返回一个负整数(不同编译器处理不同,不一定是1)。
- 如果字符数组1 == 字符数组2,则返回0。
- 如果字符数组1 > 字符数组2,则返回一个正整数(不同编译器处理不同,不一定是+1)。
- strcpy()
strcpy函数可以把一个字符串复制给另一个字符串。
strcpy(字符数组1, 字符数组2)
注意:是把字符数组2复制给字符数组1,这里的复制包括了结束符\0。 - strcat()
strcat()可以把一个字符串接到另一个字符串后面。
strcat(字符数组1, 字符数组2)
注意:是把字符数组2接到字符数组1后面。
sscanf与sprintf
sscanf与sprintf是处理字符串问题的利器,读者很有必要学会它们(sscanf从单词上可以理解为string + scanf, sprintf 则可以理解为string + printf,均在stdio.h头文件下)。
先来回顾一下scanf与printf,如果想要从屏幕输入int型变量n并将int 型变量n输出到屏幕的,则写法是下面这样的:
scanf("%d",&n);
printf("%d",n);
事实上,上面的写法其实可以表示成下面的样子,其中screen表示屏幕:
scanf(screen,"%d",&n);
printf(screen,"%d",n);
可以发现,scanf的输入其实是把screen的内容以"%d"的格式传输到n中(即从左到右),而printf的输出则是把n以"%d"的格式传输到screen上(即从右至左)。
sscanf与sprintf与上面的格式是相同的,只不过把screent换成了字符数组(假设定义了一个char数组str[100]),如下所示:
sscanf(str,"%d",&n);
sprintf(str,"%d",n);
上面sscanf写法的作用是把字符数组str中的内容以"%d"的格式写到n中(还是从左至右),示例如下:
#include<cstdio>
int main() {
int n;
char str[100]="123";
sscanf(str,"%d",&n);
printf("%d\n",n);
return 0;
}
输出结果:
123
而sprintf写法的作用是把n以"%d"的格式写到str字符数组中(还是从右至左),示例如下:
#include<cstdio>
int main() {
int n=233;
char str[100];
sprintf(str,"%d",n);
printf("%s\n",str);
return 0;
}
输出结果:
233
上面只是一些简单的应用,事实上,读者可以像使用scanf与printf那样进行复杂的格式输入和输出。
例如下面的代码使用sscanf将字符数组str中的内容按"%d:%1f,%s"的格式写到int型变量n、double 型变量db、char 型数组str2中。
#include<cstdio>
int main() {
int n;
double db;
char str[100] = "2048:3.14,hello",str2[100];
sscanf(str,"%d:%lf,%s",&n,&db,str2);
printf("n = %d, db = %.2f, str2 = %s\n",n,db,str2);
return 0;
}
类似地,下面的代码使用sprintf将int型变量n、double型变量db、char型数组str2按"%d:%.2f,%s"的格式写到字符数组str中。
#include<cstdio>
int main() {
int n = 12;
double db = 3.1415
char str[100], str2[100] = "good";
sprintf(str, "%d:%.2f,%s", n, db, str2);
printf("str = %s\n",str);
return 0;
}
最后指出,sscanf 还支持正则表达式,如果配合正则表达式来进行字符串的处理,那么很多字符串的题目都将迎刃而解。不过正则表达式不是本书想要讨论的内容,因此不作深入探讨,有兴趣的读者可以自己去了解。
数组作为函数参数
函数的参数也可以是数组,且数组作为参数时,参数中数组的第一维不需要填写长度(如果是二维数组,那么第二维需要填写长度),实际调用时也只需要填写数组名。
最重要的是,数组作为参数时,在函数中对数组元素的修改就等同于是对原数组元素的修改(这与普通的局部变量不同)。
不过,虽然数组可以作为参数,但是却不允许作为返回类型出现。如果想要返回数组,则只能将想要返回的数组作为参数传入。
指针
每个字节都会有一个地址,计算机通过这个地址找到某个变量。
变量的地址一般指它占用的字节中第一个字节的地址,一个int型的变量的地址就是它占用的4Byte当中第一个字节的地址。
在C语言中用“指针”来表示内存地址(或者称指针指向了内存地址),而如果这个内存地址恰好是某个变量的地址,那么又称“这个指针指向该变量”。初学者可以简单理解为指针就是变量的地址(虽然这么说不那么严谨)。
只要在变量前面加上&,就表示变量的地址。
指针变量
指针变量用来存放指针(或者可以理解成地址)。
可以把地址当作常量,然后专门定义了一种指针变量来存放它。
指针变量的定义和普通变量有所区别,它在某种数据类型后加星号*来表示这是一个指针变量。
星号“*”的位置在数据类型之后或是变量名之前都是可以的,编译器不会对此进行区分。
C程序员习惯于把星号放在变量名之前,而C++程序员更习惯于把星号放在数据类型之后。
另外,如果一次有好几个同种类型的指针变量要同时定义,星号只会结合于第一个变量名。也就是说,下面的定义中,只有p1是int*型的,而p2是int型的:
int* p1,p2;
如果要让后面定义的变量也是指针变量,需要在后面的每个变量名之前都加上星号:
int* p1, *p2, *p3;
而为了美观起见,一般会把第一个星号放在变量名p1前面:
int *p1, *p2, *p3;
指针变量存放的是地址,而&则是取地址运算符,因此给指针变量复制的方式一般是把变量的地址取出来,然后赋给对应类型的指针变量:
int a;
int* p = &a;
上面的代码也可以写成:
int a;
int* p;
p = &a;
而如果需要给多个指针变量初始化,方法也是一样:
int a, b;
int *p1 = &a, *p2 = &b;
需要注意的是,int* 是指针变量的类型,而后面的p才是变量名, 用来存储地址,因此地址&a是复制给p而不是*p的。
多个指针变量赋初值时,由于写法上允许把星号放在所有变量名前面,因此容易混淆,其实只要知道星号是类型的一部分就不会记错。
那么,对一个指针变量存放的地址,如何得到这个地址所指的元素呢?其实还是用星号。假设定义了int p = &a,那么指针变量p就存放了a的地址。为了通过p来获得变量a,可以把星号视为一把开启房间的钥匙,将其加在p的前面,这样p就可以把房间打开,然后获得变量a的值。
如果直接对*p进行赋值,也可以起到改变那个保存的元素的功能。
指针变量也可以进行加减法,其中减法的结果就是两个地址偏移的距离。对一个int*型的指针变量p来说,p+1是指p所指的int型变量的下一个int型变量地址。这个所谓的“下一个”是跨越了一整个int型(即4Byte),因此如果是p+i,则说明是跨越到当前int型变量之后的第i个int型变量。
除此之外,指针变量支持自增和自减操作,因此p++等同于p = p+1使用。指针变量的加减法一般用于数组中。
对指针变量来说,把其存储的地址的类型称为基类型,例如定义为int* p的指针变量,int就是它的基类型。基类型必须和指针变量存储的地址类型相同,也就是说,上面定义的指针变量p不能够存放double或char型数据的地址,而必须是int型数据的地址。
指针与数组
数组名称也作为数组的首地址使用,a == &a[0],a+i == &a[i]。
两个int型的指针相减,等价于在求两个指针之间相差了几个int,这个解释对其他类型的指针同样适用。
指针变量作为函数参数
指针变量也可以作为函数参数的类型,这时候视为把变量的地址传入函数。如果在函数中对这个地址中的元素进行改变,原先的数据就会确实地被改变。
函数参数的传送方式是单向一次性的,main函数传给swap函数的“地址”其实是一个“无符号整型”的数,其本身也跟普通变量一样只是“值传递”,swap函数对地址本身进行修改并不能对main函数里的地址修改,能够使main函数里的数据发生变化的只能是swap函数中对地址指向的数据进行的修改。
对地址本身进行修改其实跟之前对传入的普通变量进行交换的函数是一样的作用,都只是副本,没法对数据产生实质性的影响,即相当于把int*看作一个整体,传入的a和b都只是地址的副本。
引用
引用是C++中一个强有力的语法,在编程时极为实用。众所周知,函数的参数是作为局部变量的,对局部变量的操作不会影响外部的变量,如果想要修改传入的参数,那么只能用指针。那么,有没有办法可以不使用指针,也能达到修改传入参数的目的?一个很方便的方法是使用C++中的“引用”。引用不产生副本,而是给原变量起了个别名。例如,假设我本名叫“饭饭”,某天大家给我起了个别名“晴天”,其实这两个名字说的都是同一个人(即这两个名字指向了同一个人)。引用就相当于给原来的变量又取了个别名,这样旧名字跟新名字其实都是指同一个东西,且对引用变量的操作就是对原变量的操作。
引用的使用方法很简单,只需要在函数的参数类型后面加个&就可以了(&加在int后面或者变量名前面都可以,考虑到引用是别名的意思,因此一般写在变量名前面)。
#include<cstdio>
void change(int &x)
{
x=1;
}
int main()
{
int x=10;
change(x);
printf("%d\n",x);
return 0;
}
在上述代码中,在change函数的参数intx中加了&,在传入参数时对参数的修改就会对原变量进行修改。
需要注意的是,不管是否使用引用,函数的参数名和实际传入的参数名可以不同。例如上面这个程序改成下面这样也是可以的:
#include<cstdio>
void change(int &x)
{
x=1;
}
int main()
{
int a=10;
change(a);
printf("%d\n",a);
return 0;
}
要把引用的&跟取地址运算符&区分开来,引用并不是取地址的意思。
指针的引用
之前视图通过将传入的地址交换来达到交换两个变量的效果,但是失败了,这是因为对指针变量本身的修改无法作用到原指针变量上。此处可以通过引用来实现上面的效果:
void swap(int* &p1,int* &p2)
{
int* temp = p1;
p1 = p2;
p2 = temp;
}
这样做的原因是什么?之前说过,指针变量其实是unsigned类型的整数,因此为了理解上的方便,可以“简单”地把int*型理解成unsigned int型,而直接交换这样的两个整型变量是需要加引用的。
需要强调的是,由于引用是产生变量的别名,因此常量不可使用引用。于是上面的代码中不可以写成swap(&a, &b), 而必须用指针变量p1和p2存放&a和&b,然后把指针变量作为参数传入。
结构体
struct studentInfo {
int id;
char gender;
char name[20];
char major[20];
}Alice, Bob, stu[1000];
如果不在次数定义变量或数组,则大括号外直接跟上分号。
定义结构体变量和结构体数组除了可以像上面直接定义外,也可以按照基本数据类型那样定义:
studentInfo Alice;
studentInfo stu[1000];
结构体里面能定义除了自己本身(这样会引起循环定义的问题)之外的任何数据类型。不过虽然不能定义自己本身,但可以定义自身类型的指针变量。
struct node {
node* next;
};
构造函数
所谓构造函数就是用来初始化结构体的一一种函数,它直接定,义在结构体中。构造函数的一个特点是它不需要写返回类型,且函数名与结构体名相同。
一般来说, 对一个普通定义的结构体,其内部会生成一一个默认的构造函数(但不可见)。例如下面的例子中,“studentInfo0{}”就是默认生成的构造函数,可以看到这个构造函数的函数名和结构体类型名相同;它没有返回类型,所以studentInfo 前面没有写东西;它没有参数,所以小括号内是空的;它也没有函数体,因此花括号内也是空的。由于这个构造函数的存在,才可以直接定义studentInfo类型的变量而不进行初始化(因为它没有让用户提供任何初始化参数)。
struct studentInfo{
int id;
char gender;
studentInfo(){}
};
那么,如果想要自己手动提供id和gender的初始化参数,应该怎么做呢?很显然,只需要像下面这样提供初始化参数来对结构体内的变量进行赋值即可,其中_id 和_gender 都是变量名。只要不和已有的变量冲突,用a、b或者其他变量名也可以。
struct studentInfo{
int id;
char gender;
studentInfo(int _id, char _gender)
{
id = _id;
gender = _gender;
}
};
当然,构造函数也可以简化成一行。
struct studentInfo{
int id;
char gender;
studentInfo(int _id, char _gender): id(_id), gender(_gender) {}
};
这样就可以在需要时直接对结构体变量进行赋值了:
studentInfo stt = studentInfo(10086, 'M');
注意:如果自己重新定义了构造函数,则不能不经初始化就定义结构体变量,也就是说,默认生成的构造函数“studentInfoO{}"此时被覆盖了。为了既能不初始化就定义结构体变量,又能享受初始化带来的便捷,可以把“studentInfoO{}”手动加上。这意味着,只要参数个数和类型不完全相同,就可以定义任意多个构造函数,以适应不同的初始化场合,示例如下:
struct studentInfo{
int id;
char gender;
//用以不初始化就定义结构体变量
studentInfo(){}
//只初始化gender
studentInfo(char _gender)
{
gender = _gender;
}
//同时初始化id和gender
studentInfo(int _id, char _gender)
{
id = _id;
gender = _gender;
}
};
下面是一个应用实例,其中结构体Point用于存放平面点的坐标x、y。
struct Point {
int x,y;
Point(){} //用以不经初始化地定义pt[10]
Point(int _x, int _y): x(_x), y(_y) {}
}pt[10];
int main()
{
int num=0;
for(int i=1;i<=3;i++)
for(int j=1;j<=3;j++)
pt[num++]=Point(i,j);
for(int i=0;i<num;i++)
printf("%d,%d\n",pt[i].x,pt[i].y);
return 0;
}
构造函数在结构体内元素比较多的时候会使代码显得精炼,因为可以不需要临时变量就初始化一个结构体,而且代码更加工整。
浮点数的比较
由于计算机中采用有限位的二进制编码,因此浮点数在计算机中的存储并不总是精确的。
例如在经过大量计算后,一个浮点型的数3.14在计算机中就可能存储成3.140000000001,也有可能存储成3.139999999999, 这种情况下会对比较操作带来极大的干扰(因为C/C++中的“=”操作是完全相同才能判定为true)。于是需要引入-一个极小数eps来对这种误差进行修正。
经验表明,eps取\(10^{-8}\)是一个合适的数字——对大多数的情况既不会漏判,也不会误判。
- 等于:fabs(a-b)<eps
- 大于:a-b>eps。如图2-6所示,如果一个数a要大于b,那么就必须在误差eps的扰动范围之外大于b, .因此只有大于b+eps的数才能判定为大于b (也即a减b大于eps)。
- 小于:a-b<-eps。如果一个数a要小于b,那么就必须在误差eps的扰动范围之外小于b,因此只有小于b-eps的数才能判定为小于b (也即a减b小于eps)。
- 大于等于:a-b>-eps。由于大于等于运算符可以理解为大于运算符和等于运算符的结合,于是需要让一个数a在误差扰动范围内能够判定其为大于或者等于b,因此大于b-eps的数都应当判定为大于等于b (也即a减b大于eps)。
- 小于等于:a-b<eps。与大于等于运算符类似,小于等于运算符可以理解为小于运算符和等于运算符的结合,于是需要让一个数a在误差扰动范围内能够判定其为小于或者等于b,因此小于b+eps的数都应当判定为小于等于b (也即a减b小于eps)。
最后需要指出几点:
- 由于精度问题,在经过大量运算后,可能一个变量中存储的0是个很小的负数,这时如果对其开根号sqrt, 就会因不在定义域内而出错。同样的问题还出现在asin(x)当x存放+1、acos(x)当x存放-1时。这种情况需要用eps使变量保证在定义域内。
- 在某些由编译环境产生的原因下,本应为0.00的变量在输出时会变成-0.00。 这个问题是编译环境本身的bug,只能把结果存放到字符串中,然后与-0.00进行比较,如果比对成功,则加上eps来修正为0.00。
黑盒测试
scanf 函数的返回值为其成功读入的参数的个数。这就是说,如果语句scanf"%d", &n)成功读入了一个整数n,那么scanf 的返回值就是1;如果语句sanf("%d%d", &n, &m)成功读入了两个整数n、m,那么scanf的返回值就是2。
于是可能会有读者问,什么时候会读入失败?读入失败时scanf 函数是否返回0?对前一个问题我们需要知道,正常的控制台(屏幕黑框框)中的输入一般是不会失败的,只有在读取文件时到达文件末尾导致的无法读取现象,才会产生读入失败。这个时候,scanf函数会返回-1而不是0,且C语言中使用EOF (即End Of File)来代表-1。
另外,当在黑框里输入数据时,并不会触发EOF状态。因此如果想要在黑框里面手动发EOF,可以按<Ctrl+ Z>组合键,这时就会显示一个^Z,按
还需要指出,如果读入字符串,则有scanf("%s", str)与gets(str)两种方式可用,其对应的输入写法如下所示:
while(scanf("%s“, str) != EOF) {
...
}
while(gets(str) != NULL) {
...
}
最后需要指出,在多点测试中,每-次循环都要重置一下变量和数组,否则在下一组数据来临的时候变量和数组的状态就不是初始状态了。
而重置数组一般使用memset函数或fill函数。