c语言入门后续(6、7、8)

6.流程控制

分支语句(if)

三种结构:

  • if
  • if-else
  • if-else if-else

1.if:判断某个条件,当满足此条件时,才执行某些代码,可以使用if语句来实现:

#include <stdio.h>

int main() {
	int i = 0;
    if(i <10) {    //我们只希望i<10的时候才执行下面的打印语句
        printf("Hello World!\n");
    }
    printf("Hello World?");  //后面的代码在if之外,无论是否满足if条件,都跟后面的代码无关,所以这里的代码任何情况下都会执行
}

if语句的标准格式:

if(判断条件) {
    执行的代码
}

当然如果只需要执行一行代码的话,可以省略花括号:

if(判断条件)
  一行执行的代码   //注意这样只有后一行代码生效,其他的算作if之外的代码了

2.if-else:判断某个条件,当满足此条件时,执行某些代码,而不满足时,我们想要执行另一段代码,可以结合else语句来实现:

#include <stdio.h>

int main() {
   int i = 0;
        if(i < 10) {
            printf("Hello World!");   //满足if条件才执行
        } else {
            printf("王佳乐plus");   //不满足if条件才执行
        }
}

3.if-else if-else:现在希望判断学生的成绩,不同分数段打印的等级不一样,又该如何判断呢?要像这样进行连续判断,我们需要使用else-if来完成:

#include <stdio.h>

int main() {
    int score =  80;
    if(score >= 90) {
        printf("优秀");
    } else if (score >= 80&&score<90) {
        printf("良好");
    } else if(score>=70&&score<80){
        printf("中等");
    }else if (score >= 60&&score<70){
        printf("刚刚及格,老师拉了你一把");
    } else{
        printf("不及格");
    }
}

if这类的语句(switch、for、while)都是支持嵌套使用的,比如现在希望低于60分的同学需要补习,0-30分需要补Java,30-60分需要补C,这时我们就需要用到嵌套:

#include <stdio.h>

int main() {
    int score =  2;
    if(score < 60) {   //先判断不及格
        if(score > 30) {   //在内层再嵌套一个if语句进行进一步的判断
            printf("学习C");
        } else{
            printf("学习Java");
        }
    }
}
分支语句(switch)

if语句,可以通过进行条件判断,然后根据对应的条件,来执行不同的逻辑,当然除了这种方式之外,也可以使用switch语句来实现,它更适用于多分支的情况:(精准匹配某个特定的值)

格式如下:

switch (目标) {   //我们需要传入一个目标,比如变量,或是计算表达式等
  case 匹配值:    //如果目标的值等于我们这里给定的匹配值,那么就执行case后面的代码
    代码...
    break;    //代码执行结束后需要使用break来结束,否则会继续溜到下一个case继续执行代码
}
//eg:
int a=16;
switch (a) {
    case 15:
        printf("我高数考了15分");
        break;
    case 16:
        printf("我高数考了25分");
        break;
}
//我高数考了25分

switch可以精准匹配某个值,但是它不能进行范围判断,比如我们要判断分数段,用switch就很鸡肋了。

当然除了精准匹配之外,其他的情况我们可以用default来表示:

switch (目标) {
    case: ...
    default:
    		其他情况下执行的代码
}

比如:

//eg:
int a=17;
switch (a) {
    case 15:
        printf("我高数考了15分");
        break;
    case 16:
        printf("我高数考了25分");
        break;
    default:
        printf("我高数没过线");
}
//我高数没过线

在switch语句中可以继续嵌套其他的流程控制语句,比如if:

#include <stdio.h>

int main() {
    char c = 'A';
    switch (c) {
        case 'A':
            if(c == 'A') {    //嵌套一个if语句  //这个例子比较鸡肋,已经判断等于A了,进来又判断了一下。理解可以嵌套就行。
                printf("去尖子班!");   
            }
            break;
        case 'B':
            printf("去平行班!");
            break;
    }
}
循环语句 - for

使用分支语句来根据不同的条件执行不同的代码,接着来看第二种重要的流程控制语句,循环语句。

我们在某些时候,可能需要批量执行某些代码:

#include <stdio.h>

int main() {
    printf("伞兵一号卢本伟准备就绪!");   //把这句话给我打印三遍
    printf("伞兵一号卢本伟准备就绪!");
    printf("伞兵一号卢本伟准备就绪!");
}

遇到这种情况,如果还没学习循环语句,那么就只能写N次来实现这样的多次执行。

可以使用for循环语句来多次执行,for语句的标准格式如下:

for (表达式1;表达式2;表达式3) {
    循环体
}
  • 表达式1:在循环开始时仅执行一次。
  • 表达式2:每次循环开始前会执行一次,要求为判断语句,用于判断是否可以结束循环,若结果为真,那么继续循环,否则结束循环。表达式2不编写任何内容,默认为真,这样的话循环永远都不会结束
  • 表达式3:每次循环完成后会执行一次。
  • 循环体:每次循环都会执行循环体里面的内容,直到循环结束。

我们来看看按顺序打印的结果:

#include <stdio.h>

int main() {
    //循环4次,打印出来
    for (int i = 0; i < 4; ++i) {
        printf("%d, ", i);
    //int i = 0,首先定义一个变量i用于控制循环结束;在循环外面定义也行,这里就可以直接用。
  	//表达式2在循环开始之前判断是否小于4
  	//表达式3每次循环结束都让i自增一次,这样当自增4次之后不再满足条件,循环就会结束,正好4次循环
    }
}
// 0 1 2 3

想要编写一个无限循环,其实什么都不用写就行了:

#include <stdio.h>

int main() {
    for (;;) {   //什么都不写直接无限循环,但是注意,两个分号还是要写的
        printf("Hello World!\n");   //这里用到了\n表示换行
    }
}

当然,我们也可以在循环过程中提前终止或是加速循环的进行,这里我们需要认识两个新的关键字:

  • break 当满足条件时,会直接通过break跳出循环,循环不再继续下去,直接结束掉
  • continue 当满足条件时,会跳过当前这轮循环继续下面的循环,循环会再继续下去直接结束。
for (int i = 0; i < 10; ++i) {
    if(i == 5) break;   //比如现在我们希望在满足某个条件下(i == 5))提前终止循环,可以使用break关键字来跳出整个循环。
    printf("%d", i);  
}
//01234

也可以加速循环:

for (int i = 0; i < 10; ++i) {
    if(i == 5) continue;   //使用continue关键字会加速循环,无论后面有没有未执行完的代码,都会直接开启下一轮循环
    printf("%d", i);
}
//012346789

虽然使用break和continue关键字能够更方便的控制循环,但是注意在多重循环嵌套下,它只对离它最近的循环生效(就近原则):

for (int i = 1; i < 4; ++i) {
    for (int j = 1; j < 4; ++j) {
        if(i == j) continue;    //当i == j时加速循环
        printf("%d, %d\n", i, j);
    }
}

image-20220613102100374

可以看到,continue仅仅加速的是内层循环,而对外层循环没有任何效果,同样的,break也只会终结离它最近的:

for (int i = 1; i < 4; ++i) {
    for (int j = 1; j < 4; ++j) {
        if(i == j) break;    //当i == j时终止循环
        printf("%d, %d\n", i, j);
    }
}

image-20220613102347086

循环语句 - while

for循环要求我们填写三个表达式,而while相当于是一个简化版本,它只需要我们填写循环的维持条件即可,比如:

#include <stdio.h>

int main() {
    while (1) {   //每次循环开始之前都会判断括号内的内容是否为真,如果是就继续循环
        printf("Hello World!\n");   //这里会无限循环
    }
}

while循环更多的用在不明确具体的结束时机的情况下,而for循环更多用于明确知道循环的情况。

比如我们现在明确要进行循环10次,此时用for循环会更加合适一些,又比如我们现在只知道当i大于10时需要结束循环,但是i在循环多少次之后才不满足循环条件我们并不知道,此时使用while就比较合适了。

#include <stdio.h>

int main() {
    int i = 100;   //比如现在我们想看看i不断除以2得到的结果会是什么,但是循环次数我们并不明确
    while (i > 0) {   //现在唯一知道的是循环条件,只要大于0那么就可以继续除
        printf("%d, ", i);
        i /= 2;   //每次循环都除以2
    }
}
//100 ,50,25,12,6,3,1,

while也支持使用break和continue来进行循环的控制:

int i = 100;
while (i > 0) {
    if(i < 30) break;
    printf("%d, ", i);
    i /= 2;
}
//100,50,

还可以反转循环判断的位置,可以先执行循环内容,然后再做循环条件判断,这里要用到do-while语句:先会打印内容,然后再判断循环条件,满足的话就开启下一轮循环。

#include <stdio.h>

int main() {
    do {  //无论满不满足循环条件,先执行循环体里面的内容
        printf("Hello World!");
    } while (0);   //再做判断,如果判断成功,开启下一轮循环,否则结束
}
//Hello World!
实战:寻找水仙花数

水仙花数(Narcissistic number)也被称为超完全数字不变数(pluperfect digital invariant, PPDI)、自恋数、自幂数、阿姆斯壮数或阿姆斯特朗数(Armstrong number),水仙花数是指**一个 3 位数,它的每个位上的数字的 3次幂之和等于它本身。**例如:1^3 + 5^3+ 3^3 = 153。

打印出所有1000以内的水仙花数。

//水仙花数(自己先写的)
    for (int i=100; i <1000 ; ++i) {
        int a=i/100;  //百位数
        int b=(i-a*100)/10;  //十位数
        int c=i-a*100-b*10;  //个位数
        if(i==a*a*a+b*b*b+c*c*c){
            printf("%d\n",i);
        }
    }
    //优化过后的
    for (int i = 100; i < 1000; ++i) {
        int c=i%10; //个位数
        int b=i/10%10;//十位数
        int a=i/100;
        if(i==a*a*a+b*b*b+c*c*c){
                printf("%d\n",i);
            }
    }
实战:打印九九乘法表

打印出一个乘法表出来。

image-20220613105519595

 //九九乘法表(自己先写的)
for (int i = 1; i <= 9; ++i) {
    for (int j = 1; j <= i; ++j) {
        int k=i*j;
            printf("%d*%d=%d\t",j,i,k);
    }
    printf("\n");
}
//优化
for (int i = 1; i < 10; ++i) {
    for (int j = 1; j < 10; ++j) {
        if(i<j) break;
        printf("%d×%d=%-2d ",i,j,i*j); //"%2d"指的是强行占2个位置,"%-2d"占2位居左。
    }
    printf("\n");
}
实战:斐波那契数列解一

波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:**1、1、2、3、5、8、13、21、34、……*在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用。

斐波那契数列:1,1,2,3,5,8,13,21,34,55,89…,规律:从第三个数开始,每个数字的值都是前两个数字的和。

现在获取斐波那契数列上任意一位的数字,比如获取第5个数,–>5。

#include <stdio.h>

int main() {  //输出了斐波那契数列
     //k是结果
    int i=1,j=1,k=i+j;
    while(i<=j){
        i=j;j=k;k=i+j;
        if(k>100000) break;
        printf("%d\n",k);
    }
}

    //优化,获取某个位置上的数。
    int target=7,result;
    int a=1,b=1,c;
    for (int i = 0; i < target; ++i) {
        c=a+b;
        a=b;
        b=c;
    }
    result=c;
    printf("第七个数是:%d",result);

7.数组

现在有了一个新的需求,需要存储2022年12个月每个月的天数,需要创建12个变量???

#include <stdio.h>

int main() {
    int january = 31, february = 28, march = 31 ...
}

太累了?数值大了就不适用了。—>引入数组的概念。

数组的下标是从0开始的;链表中的Ai是第i个元素,表示的是位序。

数组的创建和使用

什么是数组呢?简单来说,就是存放数据的一个组,所有的数据都统一存放在这一个组中,一个数组可以同时存放多个数据。比如现在我们想保存12个月的天数,那么我们只需要创建一个int类型的数组就可以了,它可以保存很多个int类型的数据,这些保存在数组中的数据,称为“元素”:这里的12是从1~12;但数字下标是从0开始的,int arr[12]表示从0~11。

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";  //实际上字符串就是多个字符的数组形式。字符串在下面一小节。
}

那么数组定义好了,如何去使用它呢?比如打印12个月的天数:12个月的天数:

#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[] = {6, 7, 8};
    arr[1] = 9;   //比如我们现在想要让第二个元素的值变成9
    printf("%d", arr[1]);   //打印一下看看是不是变成了9
}
//9

注意,和变量一样,如果只是创建数组但是不赋初始值的话,因为是在内存中随机申请的一块空间,有可能之前其他地方使用过,保存了一些数据(脏数据),所以数组内部的元素值在不赋值的情况下并不一定都是0。

不要尝试去访问超出数组长度位置的数据,可以编译通过,但是会给警告,这些数据是毫无意义的。

多维数组

数组不仅仅只可以有一个维度,我们可以创建二维甚至多维的数组,简单来说就是,存放数组的数组(套娃):

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}, //2020年是闰年,2月有29天
                  {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]表示存放的第一个数组(2020这一年),第二个[1]表示数组中的第二个元素(2月份)。
}

除了二维还有三维、四维:

int arr[2][2][2] = {{{1, 2}, {1, 2}}, {{1, 2}, {1, 2}}};
实战:冒泡排序算法

定义一个int数组,但是数组内的数据是乱序的,通过C语言,实现将数组中的数据按从小到大(大到小也行)的顺序进行排列:

通过冒泡排序算法来实现,此算法的核心思想是:

  • 假设数组长度为N
  • 进行N轮循环,每轮循环都选出一个最大的数放到后面。
  • 每次循环中,从第一个数开始,让其与后面的数两两比较,如果更大,就交换位置,如果更小,就不动。

可以参考王道的数据结构中冒泡排序的算法思想:

//c语言的冒泡排序

//自己写的版本  
int arr[10] = {3, 5, 7, 2, 9, 0, 6, 1, 8, 4};  //乱序的
    int length=sizeof (arr)/sizeof (arr[0]);
    //算法思想:从后往前(从前往后)两两进行比较,若A[i-1]<A[i]则交换,每次排序都将一个最大的数冒到最前面去(最小的冒到最后)
    for (int j = 0; j < 10; ++j) {
        for (int i = 1; i <= length-1; ++i) {
            if (arr[i-1]<arr[i]){  //核心交换两个位置的数据。每次将一个最小的冒到最后
                int temp;
                temp =arr[i-1];
                arr[i-1]=arr[i];
                arr[i]=temp;
            }
        }
    }
    for (int i = 0; i <= length-1; ++i) {
        printf("%d\n",arr[i]);  //打印数组列表。
    }
    //优化后的冒泡排序。
int arr[10] = {3, 5, 7, 2, 9, 0, 6, 1, 8, 4};  //乱序的
int length=sizeof (arr)/sizeof (arr[0]);
//算法思想:从后往前(从前往后)两两进行比较,若A[i-1]<A[i]则交换,每次排序都将一个最大的数冒到最前面去(最小的冒到最后)
for (int j = 0; j < 9; ++j) { //i<9 最后一轮只剩下一个了,不需要比了
    _Bool flag=0;
    for (int i = 1; i <= length-1-j; ++i) {  // i <= length-1-j 优化过后不用每轮都比较10次了
        if (arr[i-1]<arr[i]){  //核心交换两个位置的数据。每次将一个最小的冒到最后
            int temp;
            temp =arr[i-1];
            arr[i-1]=arr[i];
            arr[i]=temp;
            flag=1;
        }
    }
    if(flag==0) break;  //标记一下 某一轮没有发生任何交换,说明已经完全有序不需要再进行比较了。
}
for (int i = 0; i <= length-1; ++i) {
    printf("%d\n",arr[i]);  //打印数组列表。
}
实战:斐波那契数列解二

利用数组来计算斐波那契数列,这里采用动态规划的思想。

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。

我们可以在一开始创建一个数组,然后从最开始的条件不断向后推导,从斐波那契数列的规律我们可以得知:

  • fib[i] = fib[i - 1] + fib[i - 2](这里fib代表斐波那契数列)

得到这样的一个关系(递推方程)就好办了,我们要求解数列第i个位置上的数,只需要知道i - 1i - 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了,那么现在问题其实已经有结果了,把这些小问题的结果组合起来不就能得到原来大问题的结果了吗?

利用动态规划的思想解决斐波那契数列问题:

 //斐波那契数列(数组解法) 动态规划 最优解
 //   1、1、2、3、5、8、13、21、34、
    int arr[10];
    arr[0]=1,arr[1]=1;
    int i;
    for (i = 2; i <10 ; ++i) {
        arr[i]=arr[i-1]+arr[i-2];
    }
    printf("第7个位%d",arr[6]);

    //优化
    int target=8;
    int arr[target];
    arr[0]=1,arr[1]=1;
    for(int i=2;i<target;++i){
        arr[i]=arr[i-1]+arr[i-2];
    }
    printf("%d",arr[target-1]);
实战:打家劫舍

来源:力扣(LeetCode)No.198 打家劫舍:https://leetcode.cn/problems/house-robber/

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 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块钱。

好了,现在思路已经出来了,我们直接上算法吧,现在请你实现下面的C语言程序:

要使用三目运算符,来判断谁大谁小。c语言不想java、python那样方便的直接调用max函数,需要自己max定义函数。

#include <stdio.h>

int main() {
    int arr[]={2,7,9,3,1},size=5,result;
    int dp[size];
    dp[0]=2;
    dp[1]=arr[1]>arr[0]?arr[1]:arr[0]; //三目运算符 如果arr[1]>arr[0]则dp[1]=arr[1],反之则arr[0]
    for (int i = 2; i < size; ++i)
        dp[i]=dp[i-1]>dp[i+1]+arr[i]?dp[i-1]:dp[i-2]+arr[i];
    result=dp[size-1];
    printf("%d",result);
}

8.字符串

前面学习了数组,而对于字符类型的数组,比较特殊,它实际上可以作为一个字符串(String)表示,字符串就是一个或多个字符的序列,比如我们在一开始认识的"Hello World!",像这样的多个字符形成的一连串数据,就是一个字符串,而printf函数接受的第一个参数也是字符串。

字符串的创建和使用

在C语言中并没有直接提供存储字符串的类型,我们熟知的能够存储字符的只有char类型,但是它只能存储单个字符,而一连串的字符想要通过变量进行保存,那么就只能依靠数组了,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函数,实际上这个函数就是用于打印字符串到控制台,只需要填入一个字符串和参数即可。

#include <stdio.h>

int main() {
    const char str[] = "Hello World!";   //注意printf需要填写一个const char数组进去,也就是字符串
    printf(str);
}

printf用来输出,那么输入该如何实现呢?比如我们现在希望将我们想要说的话告诉程序,让程序从控制台读取我们输入的内容,这时我们就需要使用到scanf函数了:

#include <stdio.h>

int main() {
    char str[5];
    scanf("%s", str);   //使用scanf函数来接受控制台输入,并将输入的结果按照格式,分配给后续的变量
  	//比如这里我们想要输入一个字符串,那么依然是使用%s(和输出是一样的占位符),后面跟上我们要赋值的数组(存放输入的内容)
    printf("输入的内容为:%s", str);
}

//yyds
//输入的内容为:yyds

当然除了能够扫描成字符串之外,也可以直接扫描数字:

#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);
}
实战:回文串判断

“回文串”是一个正读和反读都一样的字符串,判断用户输入的字符串(仅出现英文字符)是否为“回文”串。

ABCBA 就是一个回文串,正读反读都是一样的

ABCA 就不是一个回文串,反着读不一样

//回文串判定
char str[64];
gets(str);
int length= strlen(str);
int left=0,right=length-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算法

暴力解法基本思想:从主串(str)的第一个字符开始和子串(pattern)的第一个字符进行比较,若相等,则继续比较;否则子串退回第一个字符,重新和主串的第二个字符进行比较。反复如此,直到主串完毕。

暴力解法虽然很好理解,但是可能会做一些毫无意义的比较:

image-20220624105606429

当发生不匹配时,又会重新开始比较下一个:

image-20220624105650078

//暴力解法
char str1[64],str2[64];

gets(str1);
gets(str2);

unsigned long length1= strlen(str1);
unsigned long length2= strlen(str2);

_Bool flag=0;
for (int i = 0; i < length1; ++i) {
    flag=0;
    for (int j = 0; j < length2; ++j) {
        if(str1[i+j]!=str2[j]){
            flag=1;
            break;
        }
    }
    if(!flag) break;
}
puts(flag?"不包含":"包含");

但是我们不难发现,因为不匹配的位置发生在第三个字符,而前面是a,b两个字符都匹配,显然完全没有必要再继续挨着去比较ab了,因为很明显不可能匹配。

image-20220624142802170

实际上我们可以直接跳过b,因为我们一眼就能看出肯定不匹配,所以直接跳过从后面继续判断,能节省不少的时间。我相信如果让你通过自己的人脑去进行匹配,也是按照这样的方式去比较的吧?

不过关键点就在于怎么在程序中得知该不该跳过呢,又该跳过多少个字符不判断呢?所以我们在拿到子串的时候,就需要根据子串来计算一个叫做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

比如:

image-20220624113812730

首先一二位明确是0和1,这里我们从第三位开始计算,根据我们前面的规则:

  1. 首先判断str[next[1] - 1] == str[1],显然不相等。
  2. 此时无法继续向前走了,next[2]直接等于1即可。

image-20220624114135711

我们接着来看第四位:

  1. 首先判断str[next[2] - 1] == str[2],发现相等。
  2. 此时next[2]直接等于next[2 - 1] + 1即可。

image-20220624114743673

最后一位也是一样的判断方式:

  1. 首先判断str[next[3] - 1] == str[3],发现相等。
  2. 此时next[3]直接等于next[3 - 1] + 1即可。

image-20220624115015885

至此,next数组求解完毕,之后比较只需要多考虑一下next数组即可:

image-20220624115417358

当不匹配发生在第三位时,此时next[2] = 1, 所以我们将第一个元素移动到c的位置重新开始比较:

image-20220624142642394

发现不匹配,直接继续向后比较,重复上述操作,像这样这样跳着去比较就大大节省了时间。

OK,理论差不多结束了,上代码。

//kmp算法
char str1[]="abababcabb";
char str2[]="ababc";

int len1= strlen(str1);
int len2= strlen(str2);
int next[len2];
next[0]=0;

//求next数组
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]);
}
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?"匹配成功!":"匹配失败");
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值