目录
注:
本笔记参考B站up:鹏哥C语言的视频。
库函数的实现
实现atoi
推荐书籍:《剑指offer》
功能:将字符串转换成对应的整型
使用例
#include<stdio.h> //调用printf函数
#include<stdlib.h> //调用atoi函数
int main()
{
char* p = "-1234";
int ret = atoi(p);
printf("%d\n", ret);
return 0;
}
打印结果:
模拟实现
- 在函数执行过程中,不改变传入函数的字符串,故使用 const 修饰形参 char* s 。
int my_atoi(const char* s)
注意:
在上面的要求中,由于const的修饰,传入的字符串本身是不允许被改变的。但是,此时依旧可以通过 指针p 越过const的修饰直接改变字符串,也就是不够严谨。所以推荐在创建字符串时也使用const进行修饰:
const char* p = "-1234";
这样也无法通过 *p 改变字符串了。
处理异常情况
- 空指针
- 空字符串
- 非数字字符
- 字符长度超过范围
- ……
在模拟实现该函数时,我们可以发现许多的异常情况,现在假定我们选择在异常情况下返回 0 ,但是请注意,如果我们输入的正常字符串本身就存在 0 ,这些情况要如何进行区分?
最明显的特征:
正常字符串在遇到末尾的 '\0' 之前都能正常允许。
异常情况则会在遇到这个 '\0' 之前触发问题。
利用上面提到的特点,我们可以定义一个符号,这个符号可以分为“合法”和“不合法”两种状态,接下来只要判断这个符号就可以判断是否合法了。
枚举类型参考:笔记21-1。
此处使用枚举类型来达到上述要求的效果。
enum State
{
INVALID, //规定 0 为“非法”
VALID //规定 1 为“合法”
};
enum State state = INVALID;
//state 记录的是 my_atoi函数 返回的值所处的状态是否是合法的。
在未改变的状态下,变量state 处于“非法”状态,如果下面的返回值是0时,通过检查该变量的状态,未经改变就是“不合法”,即传入的字符串是异常状态下的。
如果传入的字符串合法,只要改变 变量state 记录的值即可。
为了判断字符串的正负号,就需要在跳过空白字符的前提下找到那个代表正负号的字符:
isspace函数 —— 检查空白字符
该函数只有当字符不是空白字符时,才会返回 0 。
通过上述的这个函数,我们就可以检查当前字符是否为空白字符。
//跳过空白字符
while (isspace(*s))
{
s++;
}
//处理正负号问题
if (*s == '+')
{
s++;
}
else if (*s == '-')
{
flag = -1;
s++; // 记得跳过这个负号
}
这里需要注意,对 flag 进行的初始化是初始化为 1 而不是 0 。
int flag = 1;
处理数字字符
isdigit函数 —— 检查字符是否为十进制数字
当该字符是十进制数字时,返回值为“真”。否则为“假”。
通过这个函数,我们就可以进行数字字符的处理,要求:
- 非数字字符需要跳过;
- 数字字符需要判断正负。
int n = 0;
while (isdigit(*s))
{
n = n * 10 + (*s - '0') * flag;
s++;
}
但是,这样还是会存在问题:
- 如果字符串开头就是非数字字符,那么程序就会直接跳过这个循环。
- 如果所处理字符串过大,如何判断并处理?
首先是字符串长度过长的问题,一个解决思路就是变更 变量n 的类型,使用 long long类型 :
long long n = 0;
当然,仅仅使用一个更长的类型是不足以解决根本问题的,这时候还需要一个判断:因为此时采用了 long long类型 的变量,所以 n 能够取到的最大值是超过 INT_MAX 的,依据这一点可以写一个判断:
long long n = 0;
while (isdigit(*s))
{
n = n * 10 + (*s - '0') * flag;
if (n > INT_MAX || n < INT_MIN)
{
return 0; //所处理的值过大,认为非法。
}
s++;
}
接下来还需要处理非数字字符的情况。
现在,字符串有两种可能是无法进入上述循环的:
- 遇到 '\0' — 程序正常结束。
- 遇到非数字字符。
if (*s == '\0') //代表程序正常结束
{
state = VALID; //由于字符串合法,这里需要改变state的值
return (int)n;
}
else //非数字字符的情况
{
return (int)n;
}
打印结果
还记得之前的 state变量 吗?为了正常打印结果,接下来就需要用到对 state值 的判断了。
if(state == VALID)
printf("合法的转换:%d\n", ret);
else
printf("非法的转换:%d\n", ret);
附上源代码
#include<stdio.h>
#include<limits.h>
#include<ctype.h>
enum State
{
INVALID,
VALID
};
enum State state = INVALID;
//state 记录的是 my_atoi函数 返回的值所处的状态是否是合法的。
int my_atoi(const char* s)
{
int flag = 1;
//空指针
if (s == NULL)
{
return 0;
}
//空字符串的情况
if (*s == '\0')
return 0;
//跳过空白字符
while (isspace(*s))
{
s++;
}
//处理正负号问题
if (*s == '+')
{
s++;
}
else if (*s == '-')
{
flag = -1;
s++; // 记得跳过这个负号
}
//处理数字字符
long long n = 0;
while (isdigit(*s))
{
n = n * 10 + (*s - '0') * flag;
if (n > INT_MAX || n < INT_MIN)
{
return 0; //所处理的值过大,认为非法。
}
s++;
}
if (*s == '\0') //代表程序正常结束
{
state = VALID;
return (int)n;
}
else //非数字字符的情况
{
state = VALID;
return (int)n;
}
}
int main()
{
const char* p = " 1234a";
int ret = my_atoi(p);
if (state == VALID)
printf("合法的转换:%d\n", ret);
else
printf("非法的转换:%d\n", ret);
return 0;
}
寻找只出现了一次的那个数字
情景:一个数组内只有两个数字是出现一次,其他所有数字都出现了两次。如:1 2 3 4 5 6 1 2 3 4
要求:编写一个函数找出两个只出现了一次的数字。
分析:
接下来的分析都假设要求数组为:1 2 3 4 5 6 1 2 3 4
考虑到寻找数组的要求,在函数传值时,我们不仅需要传入数组的地址,而且还需要传入数组长度。这里需要注意:在 情景 中,出现了一次的数字有两个,无法返回两个值,所以此时暂定函数的返回类型为 void 。
void Find(int arr[], int sz)
思路:使用异或
如果使用异或,那么根据相同为零可知,相同的数字会互相抵消,而不同的那个数字会留下来。
但是为了完成这个思路,这个数组就只能有一个单独的数字,而 情景 中却有两个单独的数字。那么为了完成这个“异或”的思路,要这么办呢?
假设我们能够将一个数组拆分为两份,即得到:
- 1 3 1 3 5
- 2 4 2 4 6
那么我们就可以使用异或。
所以为了完成这个思路,第一步就是:分组
在该步骤中,我们需要保证两个单独的数字(即:5 和 6),可以被准确分到两个组。这也是最重要的一步。
分组方法一:
5的二进制序列是101
6的二进制序列是110
如果我们将5和6异或,(相同为0,相异为1)我们将得到 011 — 即 3 。
观察011这个序列,我们可以发现011的最后一位和倒数第二位都是1,这就证明一件事:5 和 6 的二进制序列最低位是不同的,它们必定一个为1,一个为0。
所以我们可以将二进制序列最低位为 1 的分为一组:
- 1 3 5 1 3
将二进制序列最低位为 0 的分为一组:
- 2 4 6 2 4
//把所有数字异或
int ret = 0;
int i = 0;
for (i = 0; i < sz; i++)
{
ret ^= arr[i];
}
//计算 变量ret 二进制序列为 1 的那一位
int pos = 0;
for (i = 0; i < 32; i++)
{
if (((ret >> i) & 1) == 1)
{
pos = i;
break;
}
}
分组方法二:
由上述结论可知,我们也可以发现 5 和 6 的二进制序列的倒数第二位是不同的,这同样可以是一个分组依据:
- 1 4 1 4 5
- 2 3 2 3 6
接下来就是分组操作了:
//从低位到高位,把二进制序列第pos位为1的分为一组,为0的分为另一组
int num_1 = 0;
int num_2 = 0;
for ( i = 0; i < sz; i++)
{
if (((arr[i] >> pos) & 1) == 1)
{
num_1 ^= arr[i];
}
else
{
num_2 ^= arr[i];
}
}
现在回到之前的一个话题,如何把两个得到的值传回主函数?
此处提供的思路选择了传址(即使用返回型参数):将两个在主函数内定义完毕的变量的地址传入函数,就可以在函数内部修改这两个变量的内容了。(也可以使用数组)
void Find(int arr[], int sz, int* px, int* py)
*px = num_1;
*py = num_2;
接下来只要在主函数内打印即可。
附源代码
void Find(int arr[], int sz, int* px, int* py)
{
//把所有数字异或
int ret = 0;
int i = 0;
for (i = 0; i < sz; i++)
{
ret ^= arr[i];
}
//计算 变量ret 二进制序列为 1 的那一位
int pos = 0;
for (i = 0; i < 32; i++)
{
if (((ret >> i) & 1) == 1)
{
pos = i;
break;
}
}
//从低位到高位,把二进制序列第pos位为1的分为一组,为0的分为另一组
int num_1 = 0;
int num_2 = 0;
for ( i = 0; i < sz; i++)
{
if (((arr[i] >> pos) & 1) == 1)
{
num_1 ^= arr[i];
}
else
{
num_2 ^= arr[i];
}
}
*px = num_1;
*py = num_2;
}
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 1, 2, 3, 4 };
int sz = sizeof(arr) / sizeof(arr[0]);
int x = 0;
int y = 0;
//找出这两个只出现了一次的数字
Find(arr, sz, &x, &y);
printf("%d %d", x, y);
return 0;
}