开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 20 天,点击查看活动详情
数组
现在我们有一个新的需求,我们需要存储2022年每个月都天数,那么此时,为了保存这12个月的天数,我们就得创建12个变量,显然这么做是非常愚蠢的。
#include <stdio.h>
int main() {
int january = 31, february = 28, march = 31 ...
}
数组的创建和使用
为了解决这种问题,我们可以使用数组,什么是数组呢?简单来说,就是存放数据的一个组,所有的数据都统一存放在这一个组中,一个数组可以同时存放多个数据。比如现在我们想保存12个月的天数,那么我们只需要创建一个int类型的数组就可以了,它可以保存很多个int类型的数据,这些保存在数组中的数据,称为“元素”:
int arr[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; //12个月的数据全部保存在了一起
可以看到,数组的定义方式也比较简单:
类型 数组名称[数组大小] = {数据1, 数据2...}; //后面的数据可以在一开始的时候不赋值,并且数组大小必须是整数
注意数组只能存放指定类型的数据,一旦确定是不能更改的,因为数组声明后,会在内存中开辟一块连续的区域,来存放这些数据,所以类型和长度必须在一开始就明确。创建数组的方式有很多种,下面举例:
int a[10]; //直接声明int类型数组,容量为10
int b[10] = {1, 2, 4}; //声明后,可以赋值初始值,使用{}包裹,不一定需要让10个位置都有初始值,比如这里仅仅是为前三个设定了初始值,注意,跟变量一样,如果不设定初始值,数组内的数据并不一定都是0
int c[10] = {1, 2, [4] = 777, [9] = 666}; //我们也可以通过 [下标] = 的形式来指定某一位的初始值,注意下标是从0开始的,第一个元素就是第0个下标位置,比如这里数组容量为10,那么最多到9
int c[] = {1, 2, 3}; //也可以根据后面的赋值来决定数组长度
基本类型都可以声明数组:
#include <stdio.h>
int main() {
char str[] = {'A', 'B', 'C'}; //多个字符
char str2[] = "ABC"; //实际上字符串就是多个字符的数组形式
}
下面介绍数组的使用:
#include <stdio.h>
int main() {
int arr[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
for (int i = 0; i < 12; ++i) {
int days = arr[i]; //直接通过数组 名称[下标] 来访问对应的元素值,再次提醒,下标是从0开始的,不是1
printf("2022年 %d 月的天数是:%d 天\n", (i + 1), days);
}
}
当然我们也可以对数组中的值进行修改:
#include <stdio.h>
int main() {
int arr[] = {666, 777, 888};
arr[1] = 999; //让第二个元素的值变成999
printf("%d", arr[1]);
}
多维数组
数组不仅仅只可以有一个维度,我们可以创建二维甚至多维的数组
int arr[][2] = {{20, 10}, {18, 9}}; //可以看到,数组里面存放的居然是数组
//存放的内层数组的长度是需要确定的,存放数组的数组和之前一样,可以根据后面的值决定
比如现在我们要存放2020-2022年每个月的天数,那么此时用一维数组肯定是不方便了,我们就可以使用二维数组来处理:
int arr[3][12] = {{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
这样,我们就通过二维数组将这三年每个月的天数都保存下来了。
下面展示二维数组的访问
#include <stdio.h>
int main() {
int arr[3][12] = {{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
printf("%d", arr[0][1]); //比如现在我们想要获取2020年2月的天数,首先第一个是[0]表示存放的第一个数组,第二个[1]表示数组中的第二个元素
}
当然除了二维还可以上升到三维、四维:
int arr[2][2][2] = {{{1, 2}, {1, 2}}, {{1, 2}, {1, 2}}};
实战:冒泡排序算法
现在有一个int数组,但是数组内的数据是打乱的,现在请你通过C语言,实现将数组中的数据按从小到大的顺序进行排列:
这里我们使用冒泡排序算法来实现,此算法的核心思想是:
- 假设数组长度为N
- 进行N轮循环,每轮循环都选出一个最大的数放到后面。
- 每次循环中,从第一个数开始,让其与后面的数两两比较,如果更大,就交换位置,如果更小,就不动。
#include <stdio.h>
int main() {
// 冒泡排序
int arr[10] = {3, 5, 7, 2, 9, 0, 6, 1, 8, 4};
for (int i = 0; i < 10; ++i) {
for (int j = 1; j < 10 - i; ++j) {
if (arr[j-1] < arr[j]) {
int tmp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = tmp;
}
}
}
for (int i = 0; i < 10; ++i) {
printf("%d ", arr[i]);
}
}
实战:斐波那契数列(二)
学习了数组,我们来看看如何利用数组来计算斐波那契数列,这里采用动态规划的思想。
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
我们可以在一开始创建一个数组,然后从最开始的条件不断向后推导,从斐波那契数列的规律我们可以得知:
fib[i] = fib[i - 1] + fib[i - 2]
得到这样的一个递推方程就好办了,我们要求解数列第i个位置上的数,只需要知道i - 1和i - 2的值即可,这样,一个大问题,就分成了两个小问题,比如现在我们要求解斐波那契数列的第5个元素:
fib[4] = fib[3] + fib[2]现在我们只需要知道fib[3]和fib[2]即可,那么我们接着来看fib[3] = fib[2] + fib[1]以及fib[2] = fib[1] + fib[0]- 由于
fib[0]和fib[1]我们已经明确知道是1了,那么现在问题其实已经有结果了
#include <stdio.h>
int main() {
// 斐波那契数列2(动态规划)
int target = 7;
int dp[target];
dp[1] = dp[0] = 1;
for (int i = 2; i < 7; ++i) {
dp[i] = dp[i - 1] + dp[i - 2];
}
printf("%d", dp[target - 1]);
}
实战:打家劫舍
来源:力扣(LeetCode)No.198 打家劫舍:leetcode.cn/problems/ho…
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
这道题我们也可以很轻松地按照上面的动态规划思路来处理,首先我们可以将问题分为子问题,比如现在有[2,7,9,3,1]五个房屋,这个问题看起来比较复杂,我们不妨先将大问题先简化成小问题,我们来看看只有N个房屋的情况:
-
假设现在只有
[2]这一个房屋,那么很明显,我可以直接去偷一号房,得到2块钱,所以当有一个房子时最大能偷到2块钱。 -
假设现在有
[2, 7]这两个房屋,那么很明显,我可以直接去偷二号房,得到7块钱,所以当有两个房子时最大能偷到7块钱。 -
假设现在只有
[2, 7, 9]这三个房屋,我们就要来看看了,是先偷一号房再偷三号房好,还是只偷二号房好,根据前面的结论,如果我们偷了一号房,那么就可以继续偷三号房,并且得到的钱就是从一号房过来的钱+三号房的钱,也就是2+9块钱,但是如果只偷二号房的话,那么就只能得到7块钱,所以,三号房能够偷到的最大金额有以下关系(dp是我们求出的第i个房屋的最大偷钱数量,value表示房屋价值,max表示取括号中取最大的一个):dp[i] = max(dp[i - 1], dp[i - 2] + value[i])
-
这样就不难求出:
dp[2] = max(dp[1], dp[0] + value[i])=dp[2] = max(7, 2 + 9)=dp[2] = 11,所以有三个房屋时最大的金额是11块钱。 -
所以,实际上我们只需要关心前面计算出来的盗窃最大值即可,而不需要关心前面到底是怎么在偷。
-
我们以同样的方式来计算四个房屋
[2, 7, 9, 3]的情况:dp[3] = max(dp[2], dp[1] + value[3])=dp[3] = max(11, 7 + 3)=dp[3] = 11
-
所以,当有四个房屋时,我们依然采用先偷一后偷三的方案,不去偷四号,得到最大价值11块钱。
#include <stdio.h>
int main() {
int arr[] = {2,7,9,3,1}, size = 5, result;
int dp[size];
dp[0] = arr[0];
dp[1] = arr[1] > arr[0] ? arr[1] : arr[0];
for (int i = 2; i < size; ++i) {
dp[i] = dp[i - 1] > dp[i - 2] + arr[i] ? dp[i - 1] : dp[i - 2] + arr[i];
}
result = dp[size - 1];
printf("%d", result);
}
字符串
字符串的创建和使用
在C语言中并没有直接提供存储字符串的类型,char类型的数组允许我们存放多个字符,这样的话就可以表示字符串了。
比如我们现在想要存储Hello这一串字符:
char str[] = {'H', 'e', 'l', 'l', 'o', '\0'}; //直接保存单个字符,但是注意,无论内容是什么,字符串末尾必须添加一个‘\0’字符(ASCII码为0)表示结束。
printf("%s", str); //用%s来作为一个字符串输出
这样编写过于麻烦,我们可以使用更加简便的方法:
char str[] = "Hello"; //直接使用双引号将所有的内容囊括起来,并且也不需要补充\0(但是本质上是和上面一样的字符数组)
//也可以添加 const char str[] = "Hello World!"; 双引号囊括的字符串实际上就是一个const char数组类型的值
printf("%s", str);
scanf、gets、puts函数
之前一直在使用的printf是c语言中输出的方式,那么输入该如何实现呢?这时候我们就需要用到scanf函数了。
#include <stdio.h>
int main() {
char str[10];
scanf("%s", str); //使用scanf函数来接受控制台输入,并将输入的结果赋值给后续的变量
//比如这里我们想要输入一个字符串,那么依然是使用%s(和输出是一样的占位符),后面跟上我们要赋值的数组(存放输入的内容)
printf("输入的内容为:%s", str);
}
当然除了能够扫描成字符串之外,我们也可以直接扫描为一个数字:
#include <stdio.h>
int main() {
int a, b;
scanf("%d", &a); //连续扫描两个int数字
scanf("%d", &b); //注意,如果不是数组类型,那么这里在填写变量时一定要在前面添加一个&符号,这里的&不是做与运算,而是取地址操作。
printf("a + b = %d", a + b); //扫描成功后,我们来计算a + b的结果
}
除了使用scanf之外,我们也可以使用字符串专用的函数来接受字符串类型的输入和输出:
#include <stdio.h>
int main() {
char str[10];
gets(str); //gets也是接收控制台输入,然后将结果丢给str数组中
puts(str); //puts其实就是直接打印字符串到控制台
}
当然也有专门用于字符输入输出的函数:
#include <stdio.h>
int main() {
int c = getchar();
putchar(c);
}
实战:回文串判断
“回文串”是一个正读和反读都一样的字符串,请你实现一个C语言程序,判断用户输入的字符串(仅出现英文字符)是否为“回文”串。
ABCBA 就是一个回文串,因为正读反读都是一样的
ABCA 就不是一个回文串,因为反着读不一样
#include <stdio.h>
#include <string.h>
int main() {
char str[64];
gets(str);
int len = strlen(str), left = 0, right = len - 1;
_Bool flag = 1;
while (left < right) {
if (str[left] != str[right]) {
flag = 0;
break;
}
left++;
right--;
}
puts(flag ? "是回文串" : "不是回文串");
}
实战:字符串匹配KMP算法(考研要考)
现在有两个字符串:
str1 = "abcdabbc"
str2 = "cda"
现在请你设计一个C语言程序,判断第一个字符串中是否包含了第二个字符串,比如上面的例子中,很明显第一个字符串包含了第二个字符串。
- 暴力解法
- KMP算法
暴力求解:
#include <stdio.h>
#include <string.h>
int main() {
char str1[64], str2[64];
gets(str1);
gets(str2);
unsigned long len1 = strlen(str1), len2 = strlen(str2);
_Bool flag = 0;
for (int i = 0; i < len1; ++i) {
flag = 0;
for (int j = 0; j < len2; ++j) {
if (str1[i + j] != str2[j]) {
flag = 1;
break;
}
}
if (!flag) {
break;
}
}
puts(flag ? "不包含" : "包含");
}
KMP算法求解:
首先提出一个概念:一个字符串最长相等前缀和后缀。
举个例子:
字符串 abcdab 前缀的集合:{a,ab,abc,abcd,abcda} 后缀的集合:{b,ab,dab,cdab,bcdab}
那么最长相等前后缀为ab
通过上图发现,存在相同前缀后缀时,中间的e可以忽略,直接越过其进行比较,相较于暴力求解,节省了一步操作
我们检查前缀和后缀的目的其实是为了确定匹配串中的下一段开始匹配的位置。
KMP算法中提出一个概念next数组,数组中每个位置的值就是该下标应该跳转的目标位置( next 点)。
计算next数组的推导过程较为复杂,此处只简单介绍结论:
从第一位开始依次推导。
next数组的第一位一定是0。
从第二位开始(用
i表示),将第i-1个字符(也就是前一个)与其对应的next[i - 1] - 1位上的字符进行比较。如果相等,那么
next[i]位置的值就是next[i - 1] + 1如果不相等,则继续向前计算一次
next[next[i-1] - 1] - 1位置上的字符和第i-1个字符是否相同,直到找到相等的为止,并且这个位置对应的值加上1就是next[i]的值了,要是都已经到头了都没遇到相等的,那么next[i]直接等于1。
至此,next数组求解完毕,之后比较只需要多考虑一下next数组即可。
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "adbdaddwada";
char str2[] = "addw";
unsigned long len1 = strlen(str1), len2 = strlen(str2);
int next[len2];
next[0] = 0;
for (int i = 1; i < len2; ++i) {
int j = i - 1;
while (1) {
if (next[j] == 0 || str2[i - 1] == str2[next[j] - 1]) {
next[i] = next[j] + 1;
break;
}
j = next[j] - 1;
}
}
for (int i = 0; i < len2; ++i) {
printf("%d ", next[i]);
}
printf("\n");
int i = 0, j = 0;
while (i < len1) {
if (str1[i] == str2[j]) {
i++;
j++;
} else {
if (j == 0) {
i++;
} else {
j = next[j] - 1;
}
}
if(j == len2) break;
}
printf(j == len2 ? "匹配成功" : "匹配失败");
}