传送门:C语言-第五章:指针与数组
第一节:冒泡排序
如果定义并初始化了以下的一个数组:
int arr[] = {5,1,2,7,6,8,4};
它的元素顺序是乱的,要让它按照大小顺序排列,就可以使用冒泡排序算法。
冒泡排序是一种简单的算法,它的原理如下:
这样通过一步一步的比较,就可以找到最大的数并把它放到数组的最后。
上述图片中的过程叫做一趟冒泡。
我们先编写代码实现上述一趟冒泡的过程:
#include <stdio.h>
int main()
{
int arr[] = { 5,1,2,7,6,4,8 };
printf("原数组顺序:");
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ",arr[i]);
}
for (int i = 0; i < sizeof(arr) / sizeof(arr[0])-1; i++)
{
if (arr[i] > arr[i + 1])
{
int tmp = arr[i]; // 需要一个中间变量存储arr[i]的值
arr[i] = arr[i + 1]; // 因为这一步会覆盖arr[i]原来的值
arr[i + 1] = tmp;
}
}
printf("\n现数组顺序:");
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
一趟冒泡可以排好一个元素,n个元素不就需要n趟冒泡吗,我们给它加上循环实现n趟冒泡:
#include <stdio.h>
int main()
{
int arr[] = { 5,1,2,7,6,4,8 };
printf("原数组顺序:");
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ",arr[i]);
}
for (int j = 0; j < sizeof(arr) / sizeof(arr[0]); j++) // 冒泡趟数
{
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]) - 1; i++)
{
if (arr[i] > arr[i + 1])
{
int tmp = arr[i]; // 需要一个中间变量存储arr[i]的值
arr[i] = arr[i + 1]; // 因为这一步会覆盖arr[i]原来的值
arr[i + 1] = tmp;
}
}
}
printf("\n现数组顺序:");
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
ps:如果需要降序排列,只需把"arr[i] > arr[i+1]"改为"arr[i] < arr[i+1]"即可
这样数组就全部排好了,但是上述代码还有3个优化点:
第一点:
在进行第一趟冒泡时,最大值8的位置就已经固定了:
那我们就不需要考虑8了,所以我们实际上需要再次排序的数组是:
如果再进行一趟冒泡,最大的7又可以不考虑了,以此类推,每进行一趟冒泡,内层循环的次数就可以-1。
优化的代码如下:
#include <stdio.h>
int main()
{
int arr[] = { 5,1,2,7,6,4,8 };
printf("原数组顺序:");
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ",arr[i]);
}
for (int j = 0; j < sizeof(arr) / sizeof(arr[0]); j++) // 冒泡趟数
{
for (int i = 0; i < sizeof(arr)/sizeof(arr[0])-1-j; i++) // 每循环一次j的值就会+1
{
if (arr[i] > arr[i + 1])
{
int tmp = arr[i]; // 需要一个中间变量存储arr[i]的值
arr[i] = arr[i + 1]; // 因为这一步会覆盖arr[i]原来的值
arr[i + 1] = tmp;
}
}
}
printf("\n现数组顺序:");
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
第二点:
上述代码的冒泡排序是把最大值一个一个的从数组最后向前排好,那么完成倒数第二次排序后,上述数组的情况为:
上述7个数,第6次冒泡后就已经全部排序好了,这是因为6个数的位置都排好了,那么最后一个数只能在它应该在的位置了,不需要再进行冒泡了。
所以把外层循环的次数(即冒泡的趟数)减少1:
#include <stdio.h>
int main()
{
int arr[] = { 5,1,2,7,6,4,8 };
printf("原数组顺序:");
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ",arr[i]);
}
for (int j = 0; j < sizeof(arr) / sizeof(arr[0])-1; j++) // 冒泡趟数减少1
{
for (int i = 0; i < sizeof(arr)/sizeof(arr[0])-1-j; i++) // 每循环一次j的值就会+1
{
if (arr[i] > arr[i + 1])
{
int tmp = arr[i]; // 需要一个中间变量存储arr[i]的值
arr[i] = arr[i + 1]; // 因为这一步会覆盖arr[i]原来的值
arr[i + 1] = tmp;
}
}
}
printf("\n现数组顺序:");
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
第三点:
上述代码遍历了两次数组,代码有重复的部分,一点都不够优雅,我们可以把遍历数组和冒泡排序封装成两个函数:
1、根据函数的作用域,我们用一个文件来专门存放函数:
function.c 用来存放函数的定义和实现,
main.c 用来调用函数。
2、自定义一个名为 TraArray 的函数,一个参数是数组,另一个参数是数组的大小:
// function.c
#include <stdio.h>
void TraArray(int* arr,int size) // 因为数组名是首元素地址,所以用指针接收
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
}
那么为什么我们在函数中不用 sizeof(arr)/sizeof(arr[0]) 计算数组的大小,还需要传入数组大小呢?
这是因为arr 数组的名字 arr 存放的是首元素地址,相当于一个指针,传入函数时也只能被作为一个指针接收,所以在函数中用 sizeof(arr) 计算的就是指针的大小(64位平台上为8字节),而不是整个数组的大小。
函数的参数还可以这样写:
// function.c
#include <stdio.h>
void TraArray(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
这样可以给阅读者强调它是一个数组,但是它依然被函数视为指针 。
3、把冒泡排序自定义成一个函数,需要的参数和 TraArray 相同:
// function.c
void BubbleSort(int* arr, int size)
{
for (int j = 0; j < size; j++)
{
for (int i = 0; i < size - 1-j; i++)
{
if (arr[i] > arr[i + 1])
{
int tmp = arr[i]; // 需要一个中间变量存储arr[i]的值,
arr[i] = arr[i + 1]; // 因为这一步会覆盖arr[i]原来的值
arr[i + 1] = tmp;
}
}
}
}
4、还可以把 TraArray 和 BubbleSort 再定义成一个函数:
// function.c
void TraAndBub(int* arr,int size)
{
printf("原数组顺序:");
TraArray(arr, size); // 遍历并打印数组
BubbleSort(arr, size); // 冒泡排序
printf("现数组顺序:");
TraArray(arr, size); // 遍历并打印数组
}
function.c 完整的代码为:
// function.c
#include <stdio.h>
void TraArray(int* arr, int size) // 因为数组名是首元素地址,所以用指针接收
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
void BubbleSort(int* arr, int size)
{
for (int j = 0; j < size; j++)
{
for (int i = 0; i < size - 1-j; i++)
{
if (arr[i] > arr[i + 1])
{
int tmp = arr[i]; // 需要一个中间变量存储arr[i]的值
arr[i] = arr[i + 1]; // 因为这一步会覆盖arr[i]原来的值
arr[i + 1] = tmp;
}
}
}
}
void TraAndBub(int* arr,int size)
{
printf("原数组顺序:");
TraArray(arr, size); // 遍历并打印数组
BubbleSort(arr, size); // 冒泡排序
printf("现数组顺序:");
TraArray(arr, size); // 遍历并打印数组
}
用函数有什么好处呢?这样做的话我们给多个数组排序就非常简单了:
// main.c
#include <stdio.h>
int main()
{
int arr_1[] = { 5,1,2,7,6,4,8 };
int arr_2[] = { 1,3,1,5,2 };
int arr_3[] = { 12,33,44,11,22,43,10};
int arr_4[] = {101,104,103,102,200};
TraAndBub(arr_1, sizeof(arr_1) / sizeof(arr_1[0]));
printf("\n");
TraAndBub(arr_2, sizeof(arr_2) / sizeof(arr_2[0]));
printf("\n");
TraAndBub(arr_3, sizeof(arr_3) / sizeof(arr_3[0]));
printf("\n");
TraAndBub(arr_4, sizeof(arr_4) / sizeof(arr_4[0]));
return 0;
}
如果不用函数,那么每个数组都需要写很多行重复的代码。
综上所述,函数不仅具有解耦的作用,还可以让代码更美观,最重要的是避免重复写代码。
第二节:二维数组
2-1基本认识
上一期我们说到:数组是用来存放相同类型数据的一种容器。
那么能不能用数组来存放数组呢?可以!
曾经我们讲的数组都是一维数组,而存放一维数组的数组就叫做二维数组,它的定义格式如下:
[一维数组的元素类型] [二维数组名][一维数组个数][一维数组的元素个数];
// 例如:
int Td_arr[5][6];
//表示一个二维数组,它可以存放5个一维数组,每个一位数组可以存放6个元素
2-2.二维数组的初始化
二维数组也有完全初始化和不完全初始化:
1、完全初始化
int Td_arr[3][4] = {{1,2,3,4},{4,5,6,7},{7,8,9,10}};// 每个一维数组也需要用{}初始化
// 上面还可以写成:
int Td_arr[3][4] = {{1,2,3,4},
{4,5,6,7},
{7,8,9,10}};
// 这样二维数组就变成了平面,而且是3行4列,与[]中的数字呼应
// 二维数组定义时只能省略 行,即一维数组数
int Td_arr[][4] = {{1,2,3,4},
{4,5,6,7},
{7,8,9,10}};
// 可以这样理解:其实与一维数组一样,都是省略了元素个数,
// 数组的定义是存储 相同类型 的数据,
// 而一维数组的类型包括一维数组的元素类型和元素个数,
// 所以 列,即一维数组元素个数不能省略。
2、不完全初始化
int Td_arr[3][4] = {{1,2,3,4},{4,5,6}}; // 剩下的位置为0
2-3.二维数组的存储
和一维数组一样,二维数组的元素(即一维数组)存储也是连续的:
例如一个3行4列的二维数组
2-4.二维数组名
我们知道,数组名其实是一个指针,类型与元素类型相同,它指向数组的第一个元素,二维数组的第一个元素是一个一维数组,所以二维数组名的权限是一个一维数组,可以用偏移量证明:
#include <stdio.h>
int main()
{
int Td_arr[3][4] = {{1,2,3,4},
{4,5,6,7},
{7,8,9,10}};
printf("%p\n", Td_arr);
printf("%p\n", Td_arr + 1); // 增加1的偏移量
return 0;
}
差值为16,正好是一个一维数组的大小。
&二维数组名 和 sizeof(二维数组名) 代表整个二维数组,这里就不在演示了。
2-5.二维数组元素的使用
二维数组的元素就是一维数组,和一维数组一样,如果要访问到它的元素,也可以使用下标和[ ]操作符:
#include <stdio.h>
int main()
{
int Td_arr[3][4] = {{1,2,3,4},
{4,5,6,7},
{7,8,9,10}};
Td_arr[0] = {2,3,4,5}; // 修改第一个一维数组
return 0;
}
但是我们拿到整个一维数组是没有用的,因为我们的目的还是使用一维数组里的元素,所以还需要一个[ ]来访问一维数组里的元素:
#include <stdio.h>
int main()
{
int Td_arr[3][4] = {{1,2,3,4},
{4,5,6,7},
{7,8,9,10}};
// 打印第一个一维数组的元素
printf("%d ", Td_arr[0][0]);
printf("%d ", Td_arr[0][1]);
printf("%d ", Td_arr[0][2]);
printf("%d\n",Td_arr[0][3]);
return 0;
}
其他一维数组可以自己打印试试看。
2-6.二维数组的遍历
因为二维数组本质就是数组嵌套数组,所以可以使用循环嵌套的方式打印:
#include <stdio.h>
int main()
{
int Td_arr[3][4] = { {1,2,3,4},
{4,5,6,7},
{7,8,9,10} };
for (int i = 0; i < sizeof(Td_arr) / sizeof(Td_arr[0]); i++) // 二维数组的一维数组个数
{
for (int j = 0; j < sizeof(Td_arr[0]) / sizeof(Td_arr[0][0]); j++)// 一维数组的元素个数
{
printf("%d ", Td_arr[i][j]);
}
printf("\n"); // 打印完一个一维数组就换行
}
return 0;
}
又因为二维数组是连续存储的,所以可以用指针+偏移量的方式打印:
#include <stdio.h>
int main()
{
int Td_arr[3][4] = { {1,2,3,4},
{4,5,6,7},
{7,8,9,10} };
int* ptr = (int*)Td_arr;
// 因为数组名存放的是首元素的地址,
// 所以二维数组名存放了一维数组的地址,
// 一维数组名又存放的是首个 int 的地址
// 即二维数组名存放的地址就是 首个 int 的地址,
// 但是它们的权限不同,即类型不同,所以要用显示类型转换
for (int i = 0;i < sizeof(Td_arr) / sizeof(Td_arr[0][0]); i++) // 二维数组中所有 int 类型数据的数量
{
printf("%d ", *(ptr + i));
}
printf("\n");
return 0;
}
下期预告:
下一期是综合案例,我们将使用之前所学的所有知识写一个扫雷小游戏