目录
引言
我们在刷题过程中经常会遇到这样的题目:将数组中的内容逆向输出。而根据题目的要求不同,难度也不尽相同。这里列举几种可能。
如这样一段字符串:
char arr[] = "abcde";
经过逆向操作后输出:
edcba
//如果只是逆序打印,可以这么做
void reverse_string1(char* string)
{
if (*string != '\0')
reverse_string1(string + 1);
printf("%c", string[0]);
}
如果不是逆序打印,而是生成一个新的数组存放逆序后的结果,那么代码可以这样改:
char str[20] = "";//用于存放数组逆序后的元素
void reverse_string2(char* string)
{
int i = 0;
static int count = 0;//记录字符串的长度
if (*string != '\0')
{
reverse_string2(string + 1);
count++;
}
for (i = 0; i < count; i++)
{
if (str[i] == 0)//保证放置的元素在相应的位置
str[i] = string[0];
}
}
这样做的好处是原来的数组仍然存在,不会因此丢失原数组,这种写法也比较容易想到,当然这种写法呢会占据更多的内存,有时也并不能满足题目要求;
如果题目要求的是将原数组中的元素逆向,那么这种方法显然是行不通的,这时我们就要寻找其他的方法了。
实例
编写一个函数 reverse_string(char * string)(递归实现)
实现:将参数字符串中的字符反向排列,不是逆序打印。比如 :
char arr[] = "abcdef";
逆序之后数组的内容变成:fedcba
方法一
可以根据上面来理解每一步递归的操作,这里首先把首位两项进行交换:
常见的两个变量交换值的方法有:
//创建临时变量
int tmp = a;
a = b;
b = tmp;
//不创建临时变量:有可能发生栈溢出
a = a + b;
b = a - b;
a = a - b;
//利用位操作符^:这种方法不需要创建临时变量,同时还能有效防止栈溢出
a = a ^ b;
b = a ^ b;
a = a ^ b;
交换首尾项的值后再次调用该函数并将参数+1,可以直接暂时去掉第一项,不过此时最后一项也需要去掉,因此尾项的地址需要减去1(调用几次就需要减去几),这样才能保证新的一轮交换顺利进行。因此可以设置一个全局变量来记录调用了几次,每调用一次+1。
尾项的地址可以通过strlen函数求出:
char string[] = "abcde";
int lens = strlen(string);
//尾项的下标为:
int n = lens - 1;
或者不使用strlen函数:
int count = 0;
while (*string != '\0')
{
count++;
string++;//string为传入的数组
}//count的值即为字符串长度
string = string - count;//返回原字符串首元素地址
此时我们的一次递归就可以进行了:
//封装成函数即为
void reverse_string(char* string)
{
int count = 0;
int tmp = 0;
static time = 1;//记录交换的次数-1
while (*string != '\0')
{
count++;
string++;
}//求字符串长度
string = string - count;
if ()
{
tmp = *string;//*string == string[0]
*string = *(string + count - time);
//这里的*(string + count - time)同样等价于string[count - time]
*(string + count - time) = tmp;
time++;
reverse_string(string + 1);
}
}
到了这里基本上就搞定了,只差最后一个判断条件,易知只有剩余未交换元素的个数为0个时不需要再次递归。那么这里满足交换的条件可以写为:“有效”字符串长度/2与1比较,即:
if (1 <= (count - time + 1) / 2)//注意time的含义是交换次数-1
方法二
上面这种递归方法比较难想,这里介绍一种更简单的递归方法:(注意该方法求字符串的长度一定要在函数外部完成)
void reverse_string(char string[], int left, int right)
//这里边的left和right分别为目标字符串的下标左值和右值
{
if (left < right)
{
char tmp = string[left];
string[left] = string[right];
string[right] = tmp;
reverse_string(string, left + 1, right - 1);
//仍是由外向内一步步交换
}
}
这种方法的确更加简单易懂,判断是否需要交换的条件也比较容易得出,不过这种方法需要传入的参数较多,需要注意。可以对其进行优化:
void reverse_string(char string[], int lens)
//len为字符串长度
{
int left = 0;
int right = lens - 1;
if (left < right)
{
char tmp = string[left];
string[left] = string[right];
string[right] = tmp;
reverse_string(string + 1, lens - 2);//lens - 2表示去掉首位元素
}
}
这样处理后参数进一步减少,不过貌似仍不符合原题reverse_string(char * string)的形式,原题要求只有一个参数,因此该代码还需进一步优化,我们可以借用第一种方法在函数内部求字符串的方法在函数内部得到lens的值来取消传入字符串长度这一参数:
void reverse_string(char string[])
{
int lens = 0;
static int time = 0;//设置全局变量以记录每次需要丢弃的值的个数
while (*string != '\0')
{
lens++;
string++;
}
string = string - lens;
lens -= time;
//由于最后一项没办法直接去掉,
//因此直接让lens的值减去这一部分以保证string[right]的值为要交换的值
int left = 0;
int right = lens - 1;
if (left < right)
{
char tmp = string[left];
string[left] = string[right];
string[right] = tmp;
time++;
reverse_string(string + 1);
}
}
虽然看起来比上一段代码长了不少,其实只加了求字符串长度和引入全局变量来排掉末尾的不需要的值。其实顺下来发现和第一种方法的最终形式差不多,不过这里if条件的获取就容易的多。
总结
解决这类题的递归方法核心思路仍是找递归条件,值得注意的是方法二中创建函数的参数越多是,代码实现的逻辑就越简单,参数越少就意味着越多的变量要在函数内部创建并使用,如果涉及到递归的话这些变量具体如何变化就需要好好考虑一下了,如果实在捋不清,那就调试走一把,调试可以让你快速了解到出错的地方,因而可以“对症下药”。
---------------------------------------------------------------------------------------------------------------------------------
补充一种方法
如前两种方法,在交换内容时都是直接交换,这样就使得后续参与的字符串末尾的内容无用,每次都需要额外考虑这部分,既然如此,交换可以先交换一部分,等到里面也交换完了再把剩余部分交换,具体来说就是我们可以先把第一项元素放到临时变量tmp中,把最后一项放到第一项元素所在的位置,这时不要忙着把tmp中的元素放到最后,而是在最后一项元素的位置放置成'\0',然后直接进入递归计算子串,由于字符串的性质,遇到'0'以后就自动停止,这时字串的内容就自然而然的去掉了首尾元素,这时逻辑就更加清晰了。
新字符串内部直接套用上面的逻辑即可,具体代码实现:
void reverse_string(char string[])
{
int lens = strlen(string);
int left = 0;
int right = lens - 1;
if (lens > 1)
{
char tmp = 0;
tmp = string[left];
string[left] = string[right];
string[right] = '\0';
reverse_string(string + 1);//把内部排好序
string[right] = tmp;
}
}
该方法该方法不容易想到思路,但能想到的话逻辑上就很容易实现,代码量也会简化不少。