笔记22-2(C语言进阶 动态内存管理练习)

目录

注:

库函数的实现

实现atoi

使用例

模拟实现

寻找只出现了一次的那个数字

分析:


 

注:

        本笔记参考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函数 —— 检查字符是否为十进制数字

当该字符是十进制数字时,返回值为“真”。否则为“假”。

通过这个函数,我们就可以进行数字字符的处理,要求:

  1. 非数字字符需要跳过;
  2. 数字字符需要判断正负。
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++;
}

接下来还需要处理非数字字符的情况。

现在,字符串有两种可能是无法进入上述循环的:

  1. 遇到 '\0' — 程序正常结束。
  2. 遇到非数字字符。
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;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值