大家好,很高兴又和各位见面了,在上一篇内容结尾有两道题目不知道大家有没有自己去尝试编写代码,今天咱们通过几道题目开始今天的内容。

练习题

1.在一个有序数组中查找具体的某个数字n。

2.编写代码,演示多个字符从两端移动,向中间汇聚。

3.编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码正确则提示登录成功,如果三次均输入错误,则退出程序)

不知道大家有没有跟我一样第一次看到这些题目有点懵圈,作为一个刚刚接触C语言的新手小白,我看到这几道题的第一反应就是“什么呀!!!这啥这应该怎么做呀???”不过别着急,接下来我们就来一起探讨一下这几道题。

折半查找算法(二分法)

在探讨这几道题目之前,我们先来探讨一下二分法。那么什么是二分法呢?对于这个方法我是这样理解的,所谓的二分法顾名思义就是二分嘛,也就是平均分嘛。那这平均分完是来干啥的呢?这个问题很关键,二分法在我看来就是一种通过平均分来达到快速缩小范围的一种方法,举个例子:现在有一组数字1~10,我现在要想找到7在哪里,正常情况下我们是不是应该从头开始,或者从尾开始找,但是二分法呢它确实从中间开始,怎么开始呢?下面就是进入咱们的代码理解环节:

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<string.h>

//查找一个有序数组中的某个数字n
//顺序查找法
int main()
{
	int A[] = { 1,2,3,4,5,6,7,8,9,10 };
	int a = sizeof(A) / sizeof(A[0]);//计算数组中的元素总个数;
	int b = 7;//要查找的目标数字;
	int c = 0;//定义数组下标,通过下标来查找数组对应的元素;
	for (c = 0; c < a; c++)//从第头开始查找
	{
		if (A[c] == b)//判断是否为我们要找的元素;
		{
			printf("找到了,元素下标是:%d\n", c);
			break;//找到后结束循环;
		}
	}
	if (c >= a)//跳出循环后判断c的值有没有超过或等于元素个数;
	{
		printf("没找到,数组中没有该元素。");
	}
	return 0;
}

这个代码的逻辑很简单,我先把数组总的元素总个数确定下来,对应的元素最大的下标就是总个数减一(因为数组的下标是从0开始排序),然后在从第一个元素开始查找也就是下标为0的元素,然后依次增加下标数值,并与目标元素判断是否相等,直到相等,打印结果后跳出循环,再进行判断下标值是否大于等于元素个数,这里如果能找到,肯定是不会比总数大的,如果下标值大于等于元素总个数时,代表数组里面并没有该元素。下面我们分别测试一下7和11两个值:

计算机小白的成长历程——分支与循环(6)_字符串

计算机小白的成长历程——分支与循环(6)_字符串_02

下面我们来看看逆序查找法,也就是从最后一个元素开始往前查找:

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<string.h>

//查找一个有序数组中的某个数字n
//逆序查找法
int main()
{
	int A[] = { 1,2,3,4,5,6,7,8,9,10 };
	int b = 7;//要查找的目标元素;
	int c = sizeof(A) / sizeof(A[0]);//计算数组中的元素总个数;
	int d = c - 1;//最后一个元素的下标
	for (d; d > 0; d--)
	{
		if (A[d] == b)
		{
			printf("找到了,元素下标是:%d\n", d);
			break;
		}
	}
	if (d <= 0)
	{
		printf("没找到,数组里没有该元素。\n");
	}
	return 0;
}

这里的逻辑和上面相同,只不过开始目标由第一个元素换成了最后一个元素,然后开始从后往前一个个审查,审查结果相同,那就找到了,审查结果不同,那就找不到,运行结果如下:

计算机小白的成长历程——分支与循环(6)_二分法_03

计算机小白的成长历程——分支与循环(6)_字符串_04

这里我们可以看到,这两种方法都是可以的,而且也比较简单,容易理解,但是下面我要介绍的二分法,和这两种方法的逻辑是不相同的,它们之间的不同之处就在于,一个是在整个范围中挨个审查,判断是否相等,而二分法则是取整个范围中的中间值来判断是否相等,若不相等,则开始判断是比中间值大还是比中间值小,从而缩小范围,然后一直重复此操作,来更加快捷的查找目标。下面我们来通过代码理解:

//查找一个有序数组中的某个数字n
// 二分法
int main()
{
	int A[] = { 1,2,3,4,5,6,7,8,9,10 };
	int b = 17;//要查找的目标元素;
	int c = sizeof(A) / sizeof(A[0]);//计算数组中的元素总个数;
	int left = 0;//左边端点元素下标
	int right = c - 1;//右边端点元素的下标
	while (left <= right)
	{
		int mid = (left + right) / 2;//中间元素下标
		if (A[mid] < b)//中间的元素比目标元素小,则目标元素在中间元素的右侧,即新的范围是从中间的元素开始,到右边端点元素为止;
		{
			left = mid + 1;//将中间元素右边的第一个元素变成左边的端点,右侧端点不变;
		}
		else if (A[mid] > b)//中间的元素比目标元素大,则目标元素在中间元素的左侧,即新的范围是从左边端点元素开始,到中间元素为止;
		{
			int right = mid - 1;//将中间元素左边的第一个元素变成右的端点,左侧端点不变;
		}
		else//判断中间元素与目标元素是否相等;
		{
			printf("找到了,目标元素下标为:%d\n", mid);//相等则找到了中间元素的下标;
			break;//找到后跳出循环;
		}
	}
	if (left > right)
	{
		printf("没找到,数组里没有该元素。\n");//如果上述情况都不满足,则表示数组中没有该元素;
	}
	return 0;
}

这样直接去看这个代码是有点难理解的,接下来我们通过下面这张图来进一步理解这个代码:

计算机小白的成长历程——分支与循环(6)_数组_05

首先解释一下下面这个问题:

为什么我们在确定范围后左端点需要+1,右端点需要-1?

我的理解是缩小范围的话我们是需要尽可能多的去掉多余的元素,在中间值与目标值对比时就已经确定了它是否与目标值相等,相等时就说明我们已经找到了,不相等时那这个元素我们就可以直接去掉了,而且去掉中间元素还有一个非常重要的用途,那就是防止陷入死循环,这里我以下面的例子来说明:

计算机小白的成长历程——分支与循环(6)_数组_06

通过这个例子我们可以看到,在查找端点的时候,会陷入最后两个值的循环,因为我们二分法取的是两端平均数的整数部分,如果在一组数中一直取中间值为端点,总是会造成像这里的(8+9)/2=8.5这种陷入死循环的情况,所以为了避免这种情况的产生,我们在进行二分法对比完后,直接舍弃不是目标值的中间值,通过它左右两边的值去确定新的范围,也就是右端点就是中间值-1,左端点就是中间值+1。不知道像这样解释大家能不能理解,如果还有疑问,可以评论、留言或者私信,我都会尽量去解答。

二分法咱们就探讨到这里,我们在探讨的过程中已经把第一题给解决了,接下来我们来看看第二题:

编写代码,演示多个字符从两端移动,向中间汇聚。

这题的意思是如果我想将“hello world!!!”在屏幕上打印出来,题目希望它是从两端开始打印:

//字符从两端移动,向中间汇聚
//hello world!!!
//h            !
//he          !!
//hel        !!!
//……
//hello world!!!

这里我们为了使它看的更加直观一点,我们将中间的空格用#来代替,这里也就变成了:

//字符从两端移动,向中间汇聚
//hello world!!!
//##############
//h############!
//he##########!!
//hel########!!!
//……
//hello world!!!

按照第一题的思路,第一题我们是将需要操作的数放在整型数组里,那这里我们可以将“hello world!!!”和"##############"分别放在两个数组里,通过下标对里面的每个元素进行访问,下面开始编写代码:

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<string.h>
//字符从两端移动,向中间汇聚
int main()
{
	//这里默认不知道具体要打印的字符个数
	char A[] = "hello world!!!";//将需要打印的字符放在字符数组A[]中;
	char B[] = "##############";//将需要打印的字符放在字符数组B[]中;
	int a = sizeof(A) / sizeof(A[0]);//计算数组A[]的元素个数;
	int b = sizeof(B) / sizeof(B[0]);//计算数组B[]的元素个数;
	int i = 0;//定义变量i为数组的第一个字符下标
	int	j = a - 2;//定义变量j作为数组最后一个字符下标;
	for (i, j; i <= j; i++, j--)
	{
		B[i] = A[i];//将数组B[]左边的元素用数组A[]左边的元素依次替换掉;
		B[j] = A[j];//将数组B[]右边的元素用数组A[]右边的元素依次替换掉;
		printf("%s\n", B);//每次替换完,就将数组B[]打印出来;
	}
	return 0;
}

下面我们看看打印结果:

计算机小白的成长历程——分支与循环(6)_二分法_07

从结果中我们可以看到,数组B[]的每一次打印,里面的元素就会从两端被替换掉一个,像这样就实现了字符从两端移动向中间汇聚。接下来我们来解析一下这个代码:

1.在代码中,我即计算了数组A[]的元素个数,也计算了数组B[]的元素个数,其实这里是多余的,因为要实现字符从两端移动,我两个字符的元素个数肯定是相等的,这里是想告诉各位,在计算元素个数时,不管用哪个数组都是可以的;

2.不知道大家有没有注意到我这用a-2来代表数组的最后一个元素的下标,为什么不是a-1?这里涉及到字符串的相关知识点,有兴趣的朋友可以回顾一下初识C语言7,这一篇对字符串的知识点进行了探讨,这里我就给大家做个总结吧:

由双引号引起的字符串是由单个或多个字符加上看不见的停止标志“\0”组成的,所以字符串的字符个数=看的见的字符个数+1(\0),正常情况下,字符下标=看的见的字符个数-1,但是在字符串中,我们需要吧\0的个数也一并减掉,所以就变成了:字符下标=看的见的字符个数-1(\0)-1=看的见的字符个数-2,这也就是为什么我们这里是a-2而不是a-1。那我们如何让它变成-1呢?在初识C语言7中我们也介绍过计算字符串长度的函数——strlen,这个函数是遇到\0就停止,也就是它只计算在字符串中看的见的字符个数,所以这里我们可以把int j = a - 2;改成int j = strlen(A)-1这两种方式都是可以的;

3.在程序运行时我们会看到这个结果刷一下就全部出来了,那有朋友就会说,你这样去打印,我也体会不到它是从两边移动过来的呀!那我们应该怎么解决这个问题呢?这里我要介绍一个函数——Sleep——睡眠——可以是计算机程序进入睡眠状态,也就是它可以让程序停止运行,但是在使用这个函数时我们需要引用头文件<windows.h>,加入后的展示效果如下:

计算机小白的成长历程——分支与循环(6)_数组_08

(PS:有没有推荐的可以做高清gif的免费软件呀!!!麻烦知道的朋友能够分享一下)

计算机小白的成长历程——分支与循环(6)_二分法_09

因为动图不太清楚,这里大家可以看一下Sleep函数这行的注释。

4.加入Sleep函数后,有朋友可能觉得还不够,想看到它像放动画片一样运动起来,这里我们也可以通过加入system函数和cls指令来实现清屏操作——system(“cls”),这里需要引用头文件<stdlib.h>,下面我们来看看演示效果:

计算机小白的成长历程——分支与循环(6)_字符串_10

我们看一下运行结束后的显示:

计算机小白的成长历程——分支与循环(6)_二分法_11

这里就可以看到,通过加入清屏指令后,屏幕上只显示了循环外的打印指令,循环内就像动态图上显示的那样,可以很直观的看到字符从两侧向中间移动的过程。

第二题探讨到这里就结束了,咱们继续探讨最后一题:

编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码正确则提示登录成功,如果三次均输入错误,则退出程序)

这一题相比于前面的题来说会稍微简单一点,我们直接看代码:

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<string.h>
#include<windows.h>
#include<stdlib.h>

//模拟用户登录情景,并且只能登录三次
int main()
{
	int i = 0;
	char a[20] = { 0 };
	for (i = 0; i < 3; i++)
	{
		printf("请输入密码:>");
		scanf("%s", a);//C语言中字符串为数组形式,而数组名称可以用来表示地址,故输入字符串时不需要加入取地址符号&;
		if (strcmp(a , "123456") == 0)//==不能用来比较两个字符串是否相等,应该使用库函数——strcmp——字符串比较函数;
			//用strcmp函数可以用来比较两个字符串的大小,如果第一个字符串与第二个字符串相等,则函数返回0;
			//如果第一个字符串大于第二个字符串,则返回一个大于0的数值;
			//如果第一个字符串小于第二个字符串,则返回一个小于0的数值;
		{
			printf("登录成功\n");
			break;
		}
		else
		{
			printf("密码错误,请重新输入");
			Sleep(1000);//暂停程序1秒钟;
			system("cls");//清除屏幕内容;
		}
	}
	return 0;
}

接下来我们来看看代码运行的演示:

计算机小白的成长历程——分支与循环(6)_数组_12

我们来看看最后登录成功的界面:


计算机小白的成长历程——分支与循环(6)_二分法_13

下面我们来解析一下这个代码,这个代码涉及到几个知识点,如下所示:

1.在需要使用输入函数scanf输入字符串时,我们需要在定义字符数组时,先确定数组里面的元素个数,不然系统会报错;

2.C语言中字符串为数组形式,而数组名称可以用来表示地址,故在使用scanf函数输入字符串时不需要加入取地址符号&;

3.操作符“==”不能用来比较两个字符串是否相等,应该使用库函数——strcmp——字符串比较函数来比较两个字符串的大小;

4.用strcmp函数来比较两个字符串的大小时分三种情况:

(1)如果第一个字符串与第二个字符串相等,则函数返回0;

(2)如果第一个字符串大于第二个字符串,则返回一个大于0的数值;

(3)如果第一个字符串小于第二个字符串,则返回一个小于0的数值;

5.行代码我们还可以再人性化一点,提示还可输入的次数,如下图所示:

计算机小白的成长历程——分支与循环(6)_数组_14

这里我们通过加入新的变量j来实现告知用户剩余次数。

结语

今天的内容到这里就结束了,通过这三个题目,我们学到的知识点还是挺多的,有二分法、有使字符从两端向中间移动的方法,有Sleep函数,有屏幕清除指令,有字符串比较函数strcmp……希望今天的内容对大家在理解运用这些知识点上能有帮助,接下来随着学习的深入,我会继续给大家分享我在学习过程中的感受,感谢大家的翻阅,咱们下一篇见。