练习1:多个字符从两端移动,向中间汇聚。
上述题干的描述内容举个例子更好理解:
很明显需要两个字符串才能实现,一个字符串为"welcome to C!!!!!!!!!!"
,另一个字符串为"######################"
。因为字符串可以放在字符数组中,所以可以利用下标访问操作符访问数组中的字符。定义下标的变量为left
和right
,分别表示数组中最左边和最右边的数组元素的下标,数组下标从0开始,所以对left
初始化为0。如图所示:
那么怎么求下标right
呢?有三种方法:
1.int right = sizeof(arr1) / sizeof(arr1[0]) - 2;
利用sizeof
计算数组元素的个数再减2。这里举个简单的例子更好理解点:字符数组arr[] = "abc"
里的字符c
的下标2
就是right
,下图所示利用VS的调试得知这个数组的大小(数组的元素个数)是4,4减去2即为下标right
。
其实在《初识C语言》中的字符串和\0有讲解字符串的末尾默认有一个\0
字符,怎么调试其中也有讲解。
2.int right = sizeof(arr1) - 2;
是第一种写法的简写,因为字符数组中的每一个数据的数据类型都是字符型,只占1个字节,sizeof(arr1[0])
就为1,可以省略不写。
3.int right = strlen(arr1) - 1;
strlen()
- string length是用来计算字符串长度的,使用时需要包含头文件<string.h>
。计算时,计算的是不包括\0
的字符串长度。所以计算下标right
时要在strlen()
的结果上减1。
初始化后的第一步代码是数组元素赋值并打印:
arr2[left] = arr1[left];
arr2[right] = arr1[right];
printf("%s\n", arr2);
下标right
、left
要发生改变:
left++;
right--;
然后又需要赋值、打印、两个下标改变,这样就会形成循环:
while (判断条件)
{
arr2[left] = arr1[left];
arr2[right] = arr1[right];
printf("%s\n", arr2);
left++;
right--;
}
那么这个循环的判断条件是什么呢?或者说,循环终止的条件是什么呢?
其实能明显地看出下标left
是要小于下标right
的,那么等于行不行呢?还是拿上述的数组arr[] = "abc"
为例子,下面的两张图分别表示初始化和两个下标发生变化,可以发现当两个下标对应的元素是同一个时,其实也不影响正常赋值和打印,所以循环判断条件为left <= right
。
完整代码如下:
#include <string.h>
#include <stdio.h>
int main()
{
char arr1[] = "welcome to C!!!!!!!!!!";
char arr2[] = "######################";
int left = 0;
//int right = sizeof(arr1) / sizeof(arr1[0]) - 2;
//int right = sizeof(arr1) - 2;
int right = strlen(arr1) - 1;
while (left <= right)
{
arr2[left] = arr1[left];
arr2[right] = arr1[right];
printf("%s\n", arr2);
left++;
right--;
}
return 0;
}
我们看到屏幕中打印的结果是一次性全部输出,看不到变化的过程,其实还可以对代码进行改进。
改进1: 利用Sleep()
函数,间隔1000毫秒再输出下一行。
#include <string.h>
#include <stdio.h>
#include <windows.h>//Sleep()的头文件
int main()
{
char arr1[] = "welcome to C!!!!!!!!!!";
char arr2[] = "######################";
int left = 0;
//int right = sizeof(arr1) / sizeof(arr1[0]) - 2;
//int right = sizeof(arr1) - 2;
int right = strlen(arr1) - 1;
while (left <= right)
{
arr2[left] = arr1[left];
arr2[right] = arr1[right];
printf("%s\n", arr2);
Sleep(1000);//休眠1000毫秒
left++;
right--;
}
return 0;
}
最终打印的结果还是跟上面的图一样,不过,若是能在打印下一行的同时删除这一行的内容,那么变化的过程岂不是更明显,请看改进2。
改进2: 利用system()
函数实现清理操作。
在搜索栏中输入cmd打开,先随便输入几组数据(如f、sf、qw、wdf…),再输入cls
后按下回车,实现清理操作,想看到更明显的清理现象可以输入dir,再输入cls
后按下回车,下面几个图就是这句话的过程:
演示完整代码:
#include <string.h>
#include <stdio.h>
#include <windows.h>//Sleep()的头文件
#include <stdlib.h>//system()的头文件
int main()
{
char arr1[] = "welcome to C!!!!!!!!!!";
char arr2[] = "######################";
int left = 0;
//int right = sizeof(arr1) / sizeof(arr1[0]) - 2;
//int right = sizeof(arr1) - 2;
int right = strlen(arr1) - 1;
while (left <= right)
{
arr2[left] = arr1[left];
arr2[right] = arr1[right];
printf("%s\n", arr2);
Sleep(1000);//休眠1000毫秒
system("cls");
left++;
right--;
}
printf("%s\n", arr2);
return 0;
}
练习2:二分查找
题面:
给定一个整型的有序数组,在数组中输入一个值并找到这个指定的值。比如升序数组:1 2 3 4 5 6 7 8 9 10 ,找出7,如果找到了,打印出这个值的下标;找不到的话,就打印找不到。
有序数组与无序数组的区别,举个例子就能一目了然了:
int main()
{
int arr1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//有序数组,这里是升序数组
int arr2[] = { 4, 9, 5, 6, 7, 1 ,2, 5, 7, 8 };//无序数组
return 0;
}
在一个升序的数组中找到一个指定的值,很容易想到的方法是遍历整个数组,上述题面代码演示:
#include <stdio.h>
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//升序
int k = 0;
scanf("%d", &k);
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
if (arr[i] == k)
{
printf("找到了,下标是%d\n", i);
break;
}
}
if (i == sz)
printf("找不到\n");
return 0;
}
代码改进,定义变量flag
并初始化为0,表示一开始没有找到。若找到了,给flag
赋值为1;若没找到,则flag
还是为0。演示如下:
#include <stdio.h>
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//升序
int k = 0;
scanf("%d", &k);
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int flag = 0;//一开始没找到
for (i = 0; i < sz; i++)
{
if (arr[i] == k)
{
flag = 1;
printf("找到了,下标是%d\n", i);
break;
}
}
if (flag == 0)
printf("没找到\n");
return 0;
}
若是有序数组中的元素个数太多了,这样的查找方式肯定太慢了,所以接下来就介绍二分查找(也叫折半查找)。
引入一个例子辅助理解:一双鞋不超过300元,请猜出它的具体价格(假设鞋子的真实价格是175元)。猜的范围是1~300,从1开始猜效率太低了,其实可以对半猜,先猜是150元,被告知猜小了,则150及以下的价格就不用考虑了。再从151到300之间对半猜225,被告知猜大了,这样225及以上的价格就不用再考虑了,以此类推…
像这样每次都能找到被查找范围的中间元素与指定元素进行比较的方法就是二分查找(也叫折半查找)。用这种方法查找的前提是数组必须是有序数组。
那么上述题面的代码思路其实跟猜鞋子价格的思路是一样的:假设指定的元素为k,查找时找到被查找范围的中间元素,再使用中间元素和k比较并去掉一半数据。很明显,被查找范围的中间元素可以用下标来计算,中间元素下标记为mid
,则mid = (left + right) / 2
,再用下标mid
的数组元素arr[mid]
与k
比较
若与指定元素相比小了,则arr[mid]
及以下的元素不用考虑了,此时left = mid + 1
:
若与指定元素相比大了,则arr[mid]
及以上的元素不用考虑了,此时right = mid - 1
:
以上的比较情形可能比较一次会找不到的,这样就需要while
循环,那就要考虑判断条件了,left < right
是一定的,那么left = right
可不可以呢?如下图所示,若被查找的元素是10,执行到最后下标left
和right
一样,还要进行最后一次查找,所以循环的判断条件是left <= right
。
完整的代码演示:
#include <stdio.h>
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int left = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int right = sz - 1;
int k = 0;
scanf("%d", &k);
int flag = 0;//一开始没找到
while (left <= right)
{
int mid = (left + right) / 2;
if (arr[mid] < k)
left = mid + 1;
else if (arr[mid] > k)
right = mid - 1;
else
{
printf("找到了,下标是%d\n", mid);
flag = 1;
break;
}
}
if (flag == 0)
printf("没找到\n");
return 0;
}
计算mid
的时候,mid = (left + right) / 2
这种写法大部分情况下没什么问题,但当left
和right
的值太大了,相加一起会大于整数的最大值,就会有溢出的风险,下图辅助理解:
若把right
比left
多的那一部分的一半和left
的值相加,也就是mid
的值。即mid = left + (right - left) / 2
,下图辅助理解:
#include <stdio.h>
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int left = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int right = sz - 1;
int k = 0;
scanf("%d", &k);
int flag = 0;//一开始没找到
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr[mid] > k)
right = mid - 1;
else if (arr[mid] < k)
left = mid + 1;
else
{
flag = 1;
printf("找到了,下标是%d\n", mid);
break;
}
}
if (flag == 0)
printf("没找到\n");
return 0;
}