文章目录
- 1. 数组
- 1.1 数组的创建
- 1.2 数组的初始化
- 1.3 多维数组
- 2. 基础算法
- 2.1 数的运算
- 2.1.1 交换两个数
- 2.1.2 求两数中的较大值或较小值
- 2.1.3 求两个数的最大公因数
- 2.1.4 求取两个数的最小公倍数
- 2.1.5 判断一个数是否是质数
- 2.2 典型的数组处理
- 2.2.1 遍历数组
- 2.2.2 找出数组中最大值或最小值
- 2.2.3 计算数组元素的平均值
- 2.2.4 复制数组
- 2.2.5 数组元素逆序
- 2.2.6 在数组中查找值
- 2.2.7 判断数组是否有序
- 2.2.7.1 数组升序
- 2.2.7.2 数组降序
- 2.2.7.3 判断数组有序
- 2.2.8 对数组中的元素进行排序
- 2.2.8.1 如果验证排序算法的正确性
- 2.4 计时
- 2.4.1 clock()函数
- 2.4.2 time()函数
1. 数组
C/C++中的数组能够顺序存储相同类型的多个数据。除了存储数据,我们也希望能够访问数据。访问数组中的某个元素的方法就是将其编号然后索引。
C/C++中元素的编号是从0开始,而不是从1开始,遵循左闭右开原则。如果我们有个元素,它们的编号则为 到 。这样,对于一个数组 a,我们就能把 到 之间任意的i作为元素的索引,在代码中用a[i]唯一地表示第i+1个元素的值,在 C/C++ 中这种数组被称为一维数组。
1.1 数组的创建
在C/C++中,创建数组有两种方式:
第一种方式是直接创建一个固定大小的数组,它所需要的内存在编译期就会被确定和分配。声明数组时,必须要用一个编译时常量指定数组的长度。
C99标准中的变长数组 允许使用变量来指定数组的长度,但是C++标准并不支持。
int a[16];
第二种方式是使用动态内存分配,可以在运行期分配内存,所需的内存空间大小不需要在编译期确定。
C函数库<stdlib.h>中提供了两个函数:malloc() 和 free(),分别用于执行动态内存的分配和释放。
int* a = (int*)malloc( 16 * sizeof(int));
free(a);
而C++中有分别用于动态内存分配和释放的关键字:new 和delete。
int* a = new int[16];
delete[] a;
当动态分配的内存不再需要使用时,应该将其释放,将其归还给系统内存池,这样它以后可以被重新分配使用。分配内存但在使用完毕后不释放将引起内存泄漏(memory leak)。
1.2 数组的初始化
在上面的示例中,数组创建后并没有被初始化,那么数组元素的值是多少呢?
如果数组在代码块之外被声明或者使用了static关键字进行声明,那么数组将被存储于静态内存中,也就是不属于栈的内存,这时数组属于静态变量,数组元素的初始值将会自动设置为零。
如果数组在代码块内部声明,且没有用static关键字声明为静态变量,那么数组将被存储在栈中,属于自动变量,此时数组中元素的值是未初始化的,元素的值是多少取决于内存原本的值。
就像普通变量可以在声明中进行初始化一样,数组也可以这样做。唯一的区别就是数组的初始化需要一系列的值,这些值位于一对花括号内,每个值之间用逗号分隔。初始化列表给出的值将被逐个赋值给数组的各个元素。
int a[6] = {3, 1, 4, 1, 5, 9};
需要注意的是,声明的数组长度可以比列表长度大,但不能比列表长度小。
int a[3] = {1, 2, 3}; //正确
int a[4] = {1, 2, 3}; //正确,声明的数组长度可以大于列表长度,剩余的元素将被初始化为默认值0
int a[2] = {1, 2, 3}; //错误,声明的数组长度不允许小于列表长度
如果声明中并未给出数组的长度,编译器就把数组的长度设置为刚好能够容纳列表中所有初始值的长度。如果初始值列表经常修改,这个技巧尤其有用。
如果声明中并未给出数组的长度,该如何得知呢?可以使用 sizeof 关键字。
int a[] = {1, 2, 3}; //正确,数组长度不写明,将和列表长度一致,长度为3
int length = sizeof(a) / sizeof(a[0]); //获取数组a的长度
1.3 多维数组
如果某个数组的维数不止一个,那么它将被称为多维数组。例如,下面的声明:
int a[2][3]; //二维数组
int b[10][16][2]; //三维数组
int c[2][3][4][5]; //四维数组
多维数组的内存依然是连续的。多维数组中最常用的是二维数组,是“数组的数组”。
下面来看看一维数组a[3]和二维数组b[2][3]的数组元素的存储顺序(storage order):
在上图中,二维数组b[2][3]优先存储b[0],再存储b[1],说明了数组元素的存储顺序。
在C中,多维数组的元素存储按照最右边的下标率先变化的原则,称为行主序(row major order)。
二维数组的动态内存分配则需要先分配一个指针数组,再为数组里的每个指针元素赋值一个一维数组的首地址。释放时顺序则相反,先释放指针数组里指针所指的内存,再释放指针数组的内存。
int row = 2, col = 3;
//分配内存
//先动态分配一个int*型的指针数组
int** a = new int*[row];
//再为数组里的指针素动态分配一个数组
for (int i = 0; i < row; i++) {
a[i] = new int[col];
}
//释放内存
//释放时释放顺序则相反,先释放指针数组里指针所指的内存
for (int i = 0; i < row; i++) {
delete[] a[i];
}
//再释放指针数组的内存
delete[] a;
2. 基础算法
2.1 数的运算
2.1.1 交换两个数
通常使用三变量法交换两个变量的值,对于 int 型变量 a 和 b ,可以使用如下代码进行交换:
int temp = a;
a = b;
b = temp;
写成函数形式:
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
swap() 函数调用:
swap(&a, &b);
2.1.2 求两数中的较大值或较小值
可以使用三目运算符的简洁写法,比较两个数值,如果 ,则返回 ,否则返回
int max(int a, int b)
{
return a > b ? a : b;
}
int min(int a, int b)
{
return a < b ? a : b;
}
因为上述的函数十分简单,通常定义为宏或者内联来避免函数调用耗时(当然,编译器优化可能会自动帮我们进行这项操作)
inline int max(int a, int b) { return (a > b) ? a : b; }
inline int min(int a, int b) { return (a < b) ? a : b; }
2.1.3 求两个数的最大公因数
最大公因数(Greatest common divisor)也叫做最大公约数,两个整数的最大公因数(Gcd)是能同时整数二者的最大整数。例如 Gcd(50, 15) = 5,Gcd(16, 8) = 8。
求解通常使用欧几里得算法,也叫辗转相除法。
unsigned int gcd(unsigned int m, unsigned int n)
{
unsigned int rem;
while (n > 0) {
rem = m % n; //求m 除以 n 的余数
m = n;
n = rem;
}
return m;
}
这里无需保证,因为如果,那么第一次循环将使他们交换。
2.1.4 求取两个数的最小公倍数
两个数的最小公倍数(Lowest common multiple)是指能同时被两个数整除的整数中的最小值,有如下计算公式。
鉴于分母不能为0,所以需要额外加个判断,防止两数都为0时出错。
unsigned int gcd(unsigned int m, unsigned int n);
unsigned int lcm(unsigned int a, unsigned int b)
{
unsigned gcdValue = gcd(a, b);
return (gcdValue != 0) ? (a * b / gcdValue) : 0;
}
2.1.5 判断一个数是否是质数
质数(Prime)又称素数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数,否则称为合数。
规定1既不是质数也不是合数。最小的质数是2,最小的合数是4。
由定义,判断一个数是否是质数,可以检查这个数能否被从 到 之间的某个整数整除即可。同时考虑到,如果一个数可以被一个数整数,那就能写成 的形式, 或 总有一个不大于 (如果两个都大于,那么两者的乘积就不可能等于),所以只需判断到即可。
bool isPrime(int n)
{
//小于等于1的数都不是质数
if (n <= 1)
return false;
// 计算n的平方根,近似取整
int sqrtn = (int)round(sqrt(n));
//对2 ~ sqrt(n)之间的数进行遍历,分别求余数,存在一个余数不为0则不是质数
for (int i = 2; i <= sqrtn; i++) {
//如果能被其中一个整数,则不是质数
if ( n % i == 0)
return false;
}
//不能被2~ sqrt(n)的任何数整除,则为质数
return true;
}
2.2 典型的数组处理
2.2.1 遍历数组
遍历数组是指按照某种顺序,对数组中的所有元素都进行一次访问,如赋值,输出等。
下面是常见的一维数组和二维数组的遍历输出。
int a[10] = {0};
int b[5][6] = {0};
for (int i = 0; i < 10; i++) {
printf("%d ", a[i]);
}
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
printf("%d ", b[i][j]);
}
printf("\n");
}
为了避免重复编写遍历数组的代码,我们定义一个遍历输出一维数组的 traverse() 函数:
void traverse(int a[], int length)
{
for (int i = 0; i < length; i++)
printf("%d ", a[i]);
printf("\n");
}
有时我们并不是希望输出值,而是进行其它的操作,则可以传入一个函数指针,在遍历时调用。在使用时,先定义一个如 void func(int* x)
之类的函数,在函数中对值进行处理,调用时将函数名传入参数中,如:traverse(a, length, func)
void traverse(int a[], int length, void (*func)(int*))
{
for (int i = 0; i < length; i++)
func(&a[i]);
}
对于二维数组,则可以通过双重循环来遍历。
void traverse(int** a, int row, int col, void (*func)(int*))
{
for (int i = 0; i < row; i++)
for (int j = 0; j < col; j++)
func(&a[i][j]);
}
调用方式如下:
//先定义函数
void printValue(int* x)
{
//访问或修改
printf("%d ", *x);
}
int a[10] = {0};
int b[3][5] = {0};
//调用
traverse(a, 10, printValue);
traverse(b, 3, 5, printValue);
2.2.2 找出数组中最大值或最小值
要求
找出数组中的最大值(最小值),返回其索引。
声明一个变量maxValue来记录数组中元素的最大值,遍历数组,与每一个元素进行比较,当元素的值比当前记录的最大值还要大时,更新maxValue的值。
int max(const int a[], int length)
{
int maxValue = a[0];
for (int i = 1; i < length; i++) {
if (a[i] > maxValue)
maxValue = a[i];
}
return maxValue;
}
最小值则遍历比较,找到更小的值时进行更新。
int min(const int a[], int length)
{
int minValue = a[0];
for (int i = 1; i < length; i++) {
if (a[i] < minValue)
minValue = a[i];
}
return minValue;
}
2.2.3 计算数组元素的平均值
要求
计算数组元素的平均值并返回,当数组长度为0时,返回0。
遍历数组,对数组进行求和,最后判断数组长度是否大于0,数组长度大于0则返回平均值(数组所有元素值之和除以数组长度),否则返回0。
double average(const int a[], int length)
{
double sum = 0.0;
for (int i = 0; i < length; i++)
sum += a[i];
return (length > 0) ? (sum / length) : 0;
}
2.2.4 复制数组
要求
创建一个和给定数组大小一致且对应元素值都相等的数组,返回其首地址。
动态分配一个相同长度的数组,然后逐一复制每个数组元素。
int* copy(const int a[], int length)
{
int* b = new int[length];
for (int i = 0; i < length; i++)
b[i] = a[i];
return b;
}
如果仅要求将元素复制到另一个数组中,则只需要将数组逐一复制即可。
void copy(int dest[], const int src[], int length)
{
for (int i = 0; i < length; i++)
dest[i] = src[i];
}
2.2.5 数组元素逆序
要求
将数组中元素的顺序颠倒,原来位置为顺数第个的元素变成倒数第个。
将数组对应位置上的元素成对交换即可。假设数组长度为,交换的两个元素索引满足关系:。
void inverse(int a[], int length)
{
for (int i = 0; i < length / 2; i++) {
int temp = a[i];
a[i] = a[length - 1 - i];
a[length - 1 - i] = temp;
}
}
2.2.6 在数组中查找值
要求
在数组中查找出给定值相等的元素,返回其索引,如果数组中有多个元素的值和给定值相等,则返回最小的一个索引。如果数组中没有和给定值相等的元素,返回-1。
从头开始遍历数组,直到查找到和给定值相等的元素为止,返回其索引。如果遍历完成后仍未查找到,则返回-1。这种查找方法也叫线性查找。
int search(int value, const int a[], int length)
{
int pos = 0;
while ((pos < length) && (a[pos] != value)) {
pos++;
}
return (pos >= length) ? -1 : pos;
}
2.2.7 判断数组是否有序
2.2.7.1 数组升序
要求
判断数组中的元素是否是按照从小到大的顺序排序(单调递增)。
数组升序即数组中任意两个元素都满足索引值较大的元素大于等于索引值较小的元素(即如果i > j,有a[i] >= a[j])。
注意:
这里并不要求数组元素值 严格单调递增,允许有相等的情况。
遍历数组,比较相邻的两个元素,如果找到不符合升序(索引值较大的元素反而值比较小)的情况,则可以直接判断出数组不是升序,结束循环。循环结束后,如果没有找到不符合的情况,说明数组为升序。
bool isAscending(const int a[], int length)
{
for (int i = 1; i < length; i++) {
if (a[i] < a[i - 1])
return false;
}
return true;
}
2.2.7.2 数组降序
数组降序判断也是类似的处理。
bool isDescending(const int a[], int length)
{
for (int i = 1; i < length; i++) {
if (a[i] > a[i - 1])
return false;
}
return true;
}
2.2.7.3 判断数组有序
要求
判断一个数组是否有序(单调性),升序或降序均可,元素可以相等,不要求严格单调。
数组有序要求数组是升序或者降序均可,那么我们可以从头开始遍历,先找出两个相邻且不相等的值,然后继续遍历数组,根据这两个值的大小关系来对数组剩余部分做升序判断或者降序判断。如果找不到两个不相等的值,则说明数组中所有的元素都相等,数组有序。
//这里需要调用上面的两个函数
bool isAscending(const int a[], int length);
bool isDescending(const int a[], int length);
bool isSorted(const int a[], int length)
{
int i = 1;
while ((i < length) && (a[i-1] == a[i])) {
i++;
}
if (i < length) {
int len = length - i;
if (a[i] > a[i - 1])
return isAscending(&a[i], len);
else
return isDescending(&a[i], len);
}
else
return true;
}
2.2.8 对数组中的元素进行排序
排序是指将一系列元素通过某种方法,将其按照某种顺序排列的过程。对于数组中的值,我们可以直接根据其值的大小,按照从小到大的顺序对它们进行排列。
排序我们可以使用经典的冒泡排序(Bubble sort):从头到尾遍历数组,比较相邻两个数的大小,当遇到顺序不正确的相邻两个值则将它们交换,不断重复遍历直到数组有序。
需要遍历多少次才会使数组有序呢?
答案是对于一个长度为的数组,遍历次即可保证数组有序。
每次遍历都能从剩余未排好序的部分筛选出一个最值放到正确的位置上,当遍历次后,个元素都放在了正确的位置上,由于是交换操作,剩余一个自然也是在其正确位置上。
对于一个长度为的数组,只需遍历 次即可保证数组有序,并且每次遍历后,都会一个最大或最小的元素放在其正确位置上,后续操作就无需对其进行比较,所以一次遍历需要比较的次数分别是:,最多需要比较次。
void bubbleSort(int a[], int length)
{
for (int i = length - 1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
这里我们还可以进行优化,如果一次遍历过后,相邻的两个元素都有序,没有进行交换,这里假设是要按从小到大的顺序进行排序,即 可得:
所以,当遍历一遍后,如果所有相邻的两个元素都不需要交换,那么整个数组就是有序的。这样在数组已经排好序的情况下,不用再进行多余的遍历操作。这在数组本身只有少数几个元素被稍微移动位置的情况下可以节省较多的时间。如果元素本身是随机的,则并不能节省多少。
void bubbleSort(int a[], int length)
{
for (int i = length - 1; i > 0; i--) {
//初始没有发生元素交换
bool exchanged = false;
for (int j = 0; j < i; j++) {
if (a[j] > a[j + 1]) {
//标记发生了元素交换
exchanged = true;
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
//如果遍历后没有发生元素交换,则说明数组有序,无需再遍历
if (!exchanged)
break;
}
}
2.2.8.1 如果验证排序算法的正确性
我们该如何验证我们的排序算法是正确的呢?
对于少量元素(几十个)的情况下,我们可以直接将数组输出,通过人眼观察进行比较。
如果有大量元素,人眼观察的方法就不可行。我们可以如 上一节 判断数组是否有序 中的 isAscending()
和 isDescending()
函数那样,定义用于检测数组有序的函数来对排序后的数组进行检测。
2.3 数学函数
2.3.1 求一个数的绝对值
判断值是否小于0,小于则返回其相反数,否则返回原来的值。
int abs(int x)
{
return (x < 0) ? -x : x;
}
double abs(double x)
{
return (x < 0.0) ? -x : x;
}
2.3.2 计算两数的平方和
double square(double a, double b)
{
return a*a + b*b;
}
2.3.3 计算直角三角形的斜边
由勾股定理可得,直角三角形两条直角边和与斜边的长度关系为:,可得:。
double hypotenuse(double a, double b)
{
return sqrt(a*a + b*b);
}
2.4 计时
C 语言中的 time 函数总结
在程序中时有计时的需要,比如计算一段代码运行的耗时,这时可以通过调用C标准库 <time.h> 中的函数来完成计时工作。
2.4.1 clock()函数
clock() 函数可以返回当前程序运行的时间,单位是(1 / CLOCKS_PER_SEC) 秒,这个宏CLOCKS_PER_SEC通常是1000,所以clock()的时间单位通常是毫秒(ms)。
函数声明:
//头文件包含
clock_t clock(void);
返回值类型为 clock_t,是一个整数类型,表示程序运行时间(单位通常为毫秒)。
typedef long clock_t;
可以通过在一段代码前后分别调用 clock() 函数获取程序运行时间,两者相减即可得到代码的运行时间。
示例如下:
//记录开始时间
clock_t startClock = clock();
//执行一些代码
//记录结束时间
clock_t endClock = clock();
//计算运行时间(结束时间 - 开始时间),单位毫秒
clock_t runTime_ms = (endClock - startClock) * 1000 / CLOCKS_PER_SEC;
printf("代码运行时间为:%ld 毫秒\n", runTime_ms);
因为通常 CLOCKS_PER_SEC 为1000,所以一般计算运行时间时,也使用如下代码即可:
clock_t runTime_ms = endClock - startClock;
2.4.2 time()函数
time() 函数通常用来获取当前时间对应的秒数,作为随机数的种子,参数传入 NULL 即可。
实际是从1970-01-01 00:00:00开始到当前时间的秒数。
函数声明:
time_t time(time_t *timer);
返回值类型是 time_t,整数类型,被定义为 long 或 long long。实际上32位是不够的,只能记录100多年,所以现在用64位的long long。
使用示例如下:
//获取当前时间秒数
time_t curTime = time(NULL);
//作为随机数初始化种子
srand((unsigned int)curTime);