目录
前言
一、读懂题目
二、思路分析
三、代码呈现
总结
作为引入讲讲这道题的由来,在网络编程中,如果 URL 参数中含有特殊字符,如空格、#等,可能导致服务器端无法获得正确的参数值。我们需要将这些特殊符号转换成服务器可以识别的字符。转换的规则是在%后面跟上 ASCI 码的两位十六进制的表示。比如空格的 ASCII 码是 32,即十六进制的 0x20,因此空格被替换成"%20"。
请实现一个函数,把字符串中的每个空格替换成"%20"。
例如输入“We are happy.”,则输出“We%20are%20happy.”
首先我们明白题目要求我们提供一个功能可用于替换空格为“%20”,那么该函数的传入参数应该是字符串类型(char* 或 string),同时因为涉及到字符串修改操作,就必须将参数列表中负责接收字符串部分设定为非const类型。
原来一个空格字符,替换之后变成 “%20” 这 3 个字符,因此字符串会变长。如果是在原来的字符串上做替换,那么就有可能覆盖修改在该字符串后面的内存。如果是创建新的字符串并在新的字符串上做替换,那么我们可以自己分配足够多的内存。如果在允许新创建字符串时我们可以遍历整个字符串,当遇到空格字符时利用%20进行替换。如果不允许,我们就要求原字符串有足够的空间容纳替换后变长的字符串。此处我们实现不利用额外空间(创建新字符串)的情况下进行字符替换。
不妨我们在功能性测试中取字符串 “we are happy.” 进行分析,当我们从左往右遍历,遇见空格就将其换为%,同时后面所有字符向后移动两位,将移位后空开的部分填入 ’2‘ ’0‘ 两个字符,继续遍历,当遇到新空格时,重复上面过程即可,如下图:
我们注意到字符串“happy”在此次操作中一共移动了两次,当字符串很长,空格很多的情况下,这种方式明显在执行效率上有明显缺陷,假设字符串的长度是 n。对每个空格字符,需要移动后面 O (n) 个字符,因此对含有 O (n) 个空格字符的字符串而言总的时间效率是 O (),那有没有什么办法可以减少移动次数甚至一步到位呢?
自然而然地我们想到,如果采用从后向前的遍历方式,把那些需要移动多次的字符直接一步到位地安置在插入后地位置,所以当我们需要置定最后一个字符位置时,我们可以先遍历一次字符串,这样就能统计出字符串中空格的总数,并可以由此计算出替换之后的字符串的总长度。每替换一个空格,长度增加 2,因此替换以后字符串的长度等于原来的长度加上 2 乘以空格数目。我们还是以前面的字符串"we are happy."为例,"we are happy."这个字符串的长度是14(包括结尾符号 \0),里面有两个空格,因此替换之后字符串的长度是18。(这里仅仅用该字符串做演示,其他字符串同理)
接着我们定义两个指针p1, p2分别指向源字符串末尾和替换后最后一个字符应该所在的位置。利用从后向前遍历的方式,如果p1指向的字符不为空格,将p1指针指向的值赋给p2;如果p1指向的字符是空格,那么p1向左一格,p2共计向左三格,p2左移过程中分别赋值 '0', '2', '%' 即可。至此我们已经理清了重要的执行步骤,接着我们只需要判断该步骤在何时终止并返回结果。
当我们定义的p1, p2指针指向同一位置时,代表左侧字符串中不可能再有空格出现,即替换操作处理完毕。或者我们可以加入计数器,每当p1找到一次空格,计数器减一当等于0时,表示所有空格已被替换。
C语言风格:
char* replaceBlank(char* str, size_t length, size_t capacity)
{
assert(str && length != 0);
const char* p1 = (str + length);
const char* index = p1;
int blank_num = 0;
while (index != str)
{
// 注意这里解引用访问字符时,需要先减后访问 类似于a[n]的数组访问最后一位元素应该是*(a+n-1)或a[n-1]
if (*--index == ' ') { blank_num++; }
}
char* p2 = str + (length + blank_num * 2);
if (p2 >= str + capacity) { perror("string length is not long enough to replace!"); return NULL; } // 字符串初始化定义空间不足
while (p1 != p2)
{
if (*p1 != ' ') { *p2-- = *p1--; }
else
{
// 一比三的占比向左移动两指针
*p2-- = '0';
*p2-- = '2';
*p2-- = '%';
p1--;
}
}
return str;
}
测试代码:
1)基础测试
void test1()
{
char str[30] = "we are happy.";
size_t size = strlen(str);
replaceBlank(str, size, 30);
printf("%s\n", str);
}
运行结果:
2)鲁棒性测试
void test1_1()
{
// 测试空指针传入
char* str1 = nullptr;
size_t size1 = 5;
replaceBlank(str1, size1, 30);
printf("%s\n", str1);
// 测试 length 不符合要求
char str2[30] = "we are happy.";
size_t size2 = 0;
replaceBlank(str2, size2, 30);
printf("%s\n", str2);
//测试初始化空间不够容纳替换后的字符串
char str3[15] = "we are happy.";
size_t size3 = strlen(str3);
replaceBlank(str3, size3, 15);
printf("%s\n", str3);
}
由于会发生中断导致后面代码不能正常执行,所以将三种情况分别测试时注释掉另外两个测试段,运行结果如下:
1,2代码段都会发生中断以反映程序中功能函数参数存在错误:
3号代码运行结果:
因为有perror函数的存在,在运行窗口我们可以得知程序在哪方面是存在问题的,最后输出原字符串是因为我们在实现逻辑中如果抛出第一行错误就立即返回原字符串,避免程序继续向下执行。
3)边界测试
void test1_2()
{
// 测试传入字符串全为空格
char str4[30] = " ";
size_t size4 = strlen(str4);
replaceBlank(str4, size4, 30);
printf("%s\n", str4);
// 测试传入字符串仅有单个空格
char str5[30] = " ";
size_t size5 = strlen(str5);
replaceBlank(str5, size5, 30);
printf("%s\n", str5);
}
运行结果:
C++风格:
两者重要步骤代码没什么不同,只是改变了函数异常处理模块,使得外部处理检查更精确。
先附上C++常用异常类:
std::exception
: 所有标准异常类的基类。std::bad_alloc
: 内存分配失败时抛出的异常。std::bad_cast
: 当dynamic_cast
无法将指针或引用转换为目标类型时抛出的异常。std::bad_exception
: 在异常处理过程中抛出的异常。std::bad_typeid
:typeid
操作符无法处理的类型时抛出的异常。std::logic_error
: 逻辑错误相关的异常基类。
std::invalid_argument
: 当函数参数的值不合适时抛出的异常。std::domain_error
: 当参数的值导致函数域错误时抛出的异常。std::length_error
: 当对象的长度超过其最大限制时抛出的异常。std::out_of_range
: 当访问超出有效范围的对象时抛出的异常。std::runtime_error
: 运行时错误相关的异常基类。
std::range_error
: 当参数的值超出有效范围时抛出的异常。std::overflow_error
: 当进行数值溢出操作时抛出的异常。std::underflow_error
: 当进行数值下溢操作时抛出的异常。std::system_error
: 与底层操作系统或库函数相关的异常基类。std::ios_base::failure
: I/O 操作失败时抛出的异常。
代码如下:
char* replaceBlank_Cpp(char* str, size_t length, size_t capacity) // 可以将参数列表换为string类对象,这样可以仅传一个参数
{
if (str == nullptr)
{
throw invalid_argument("str is nullptr!");
}
if (length <= 0) // 在某些平台上,可能存在将 size_t 作为有符号类型处理的情况
{
throw invalid_argument("length is not a positive interger!"); // 非正整数
}
const char* p1 = (str + length);
const char* index = p1;
size_t blank_num = 0;
while (index != str)
{
// 注意这里解引用访问字符时,需要先减后访问 类似于a[n]的数组访问最后一位元素应该是*(a+n-1)或a[n-1]
if (*--index == ' ') { blank_num++; }
}
char* p2 = str + (length + blank_num * 2);
if (p2 >= str + capacity) { throw runtime_error("string length is not long enough to replace!"); return str; } // 字符串初始化定义空间不足
while (p1 != p2)
{
if (*p1 != ' ') { *p2-- = *p1--; }
else
{
// 一比三的占比向左移动两指针
*p2-- = '0';
*p2-- = '2';
*p2-- = '%';
p1--;
}
}
return str;
}
测试代码:
1)基础测试
void test2()
{
char str[30] = "we are happy.";
size_t size = strlen(str);
replaceBlank_Cpp(str, size, 30);
cout << str << endl;
}
运行结果:
2)鲁棒性测试和边界测试
由于C++中的异常捕获使得我们在接收到错误信息后可以展示在屏幕上,不会中断整个程序的运行,所以这里将鲁棒性测试一同进行:
void test2_1() // 鲁棒性测试
{
// 测试空指针传入
try
{
char* str1 = nullptr;
size_t size = 5;
replaceBlank_Cpp(str1, size, 30);
}
catch (invalid_argument& e)
{
cout << e.what() << endl;
}
catch (runtime_error& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Other default!" << endl;
}
// 测试 length 不符合要求
try
{
char str2[30] = "we are happy.";
size_t size = 0;
replaceBlank_Cpp(str2, size, 30);
}
catch (invalid_argument& e)
{
cout << e.what() << endl;
}
catch (runtime_error& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Other default!" << endl;
}
//测试初始化空间不够容纳替换后的字符串
try
{
char str3[15] = "we are happy.";
size_t size = strlen(str3);
replaceBlank_Cpp(str3, size, 15);
}
catch (invalid_argument& e)
{
cout << e.what() << endl;
}
catch (runtime_error& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Other default!" << endl;
}
// 测试传入字符串全为空格
char str4[30] = " ";
size_t size4 = strlen(str4);
replaceBlank_Cpp(str4, size4, 30);
cout << str4 << endl;
// 测试传入字符串仅有单个空格
char str5[30] = " ";
size_t size5 = strlen(str5);
replaceBlank_Cpp(str5, size5, 30);
cout << str5 << endl;
}
运行结果:
在处理替换问题时,该问题完全可以拓展到数组中在指定位置插入新值时,后面所有元素对应都要后移,为了避免覆盖或创建额外空间,我们只好从后往前遍历实现逐个覆盖,最终实现插入或者长度变宽的修改。在编程解决问题甚至构建一个小功能时都要尽可能考虑到所有的边界情况和鲁棒性检查,只有这样才能近乎完美的解决遇到的问题。