C语言实现计算二进制序列中1的个数的三种算法

前言

在刷编程题时,我们不可避免会碰到有关二进制序列的题目,其中不少与二进制序列中1的个数有关,那么我们能如何实现呢。借着下面牛客网的一道题目,我将会介绍三种算法。

注释:为了方便讲述,本文中二进制的第n位都是从低到高算的。

二进制中1的个数———牛客网


算法1:/与%

 十进制

在十进制中,第n位的权重为10^(n-1)。

 故当一个十进制数字%10时,结果为最低位的数字,当/10时,结果是删去最低位后的数字。

因此,我们通过%和/的计算就能够的到十进制数字中的每一位,从而进行统计或者重排序。


二进制

 与十进制类似,二进制中第n位的权重位2^(n-1)。

 故当一个二进制数字%2时,结果为最低位的数字,当/2时,结果是删去最低位后的数字。

 由此我们写出下面的函数代码来实现

int NumberOf1(int n)
{
	int count = 0;
	while (n)
	{
		if (n % 2 != 0)
		{
			count++;
		}
		n /= 2;
	}
	return count;
}

 以变量count作为函数的返回值,在函数中以n的值是否为0作为判断进行循环,当n%2!=0(或n%2==1)时count增加1,每次循环都通过n/=2删去最后一位。


改进

因为负数/2的商均为0,这段代码存在bug,当输入的数字为负数时,无法得到预期值,故此代码只适用于int范围内的非负整数。 

 若要将适用范围拓展到int范围内的全部整数,需要进行分类讨论。

int NumberOf1(int n)
{
	int count = 0;
	if (n >= 0)
	{
		while (n)
		{
			if (n % 2 != 0)
			{
				count++;
			}
			n /= 2;
		}
	}
	else
	{
		n = ~n;
		while (n)
		{
			if (n % 2 != 0)
			{
				count++;
			}
			n /= 2;
		}
		count = 32 - count;
	}
	return count;
}

 当n>=0时,依旧按照前面的操作计算1的个数,当n<0时,先将n按位取反后计算1的个数,再用32(int占4byte即32bit)减去1的个数即可得到负数的二进制序列中1的个数。

同理,该算法也可以运用于进制。


算法2:<<、>>与&

(a).<<与&

在解决上述问题时,我们不妨通过另一道题来了解>>和<<的作用。

题目:改变10的二进制序列第三位的值

#include <stdio.h>
int main()
{
	int num = 10;
	num ^= 1 << 2;
	printf("%d", num);
	return 0;
}

首先需要定位到二进制序列的第三位,之后再对其进行改变。

这时我们可以把1作为一个定位工具,通过1<<(n-1)定位到第n位。又因为1(index)^1=0、1(index)^0=1,可以通过num ^= 1 << 2直接改变第三位。

从该题目我们可以学习到以1为定位工具,通过移位操作符定位到特定的二进制位。回到原题,根据该经验,我们可以通过循环将1多次移位,定位到二进制序列的各个位。

在定位到各个位之后,我们需要判断该位是否为1。在位操作符&、|和^中,我们需要选出一个来保证得出的结果在未定位的二进制位上只受到定位标志(1<<i)的影响。因为定位标志(1<<i)在未定位的二进制位上均为0,所以在进行按位与&后均为0。而n在定位的二进制位上若为1,按位与的结果为2^i(即1<<i);若为0,按位与的结果为0。因此按位与&满足我们的要求。

n的二进制序列第(i-1)位的值01
102^i(即1<<i)

  n & (1 << i)结果

于是我们便可以得到新的NumberOf1函数,如下。

int NumberOf1(int n)
{
	int count = 0;
	int i = 0;
	for (i = 0; i < 32; i++)
	{
		if ((n & (1 << i)) != 0)
		{
			count++;
		}
	}
	return count;
}

(b).>>与&

既然我们可以通过<<左移1来定位,根据相对运动,我们不妨通过>>来右移n主动将需要定位的二进制位移动到最低位。

int NumberOf1(int n)
{
	int count = 0;
	int i = 0;
	for (i = 0; i < 32; i++)
	{
		count += (n >> i) & 1;
	}
	return count;
}

算法3:n&(n-1)

首先需要认识一下(n-1)在我看来的一种意义。

 依照这张图,我们可以看到,每一次(n-1)都是对二进制序列中为1的最低位进行“下方”(即移至更低位)。当我们想要将第i位完全“下放”(即下一次“下放”的对象为更高位的1)需要进行2^(i-1)次(n-1)的操作,显然是不可能单独依靠(n-1)来计算n的二进制序列中1的个数。

此时我们关注到n与(n-1)的关系。

 我们可以看到每一次“下方”前后,高于被“下方”位(即蓝框内)的序列每位都相同,而被“下方”位与低于被“下方”位的序列每位都不相同。这是因为每次(n-1)操作后高于被“下方”位的序列并不会被影响;被“下方”位原本为1,(n-1)操作后变为0;低于被“下方”位的序列原本都为0,(n-1)操作后均变为1。

因此我们可以通过一次n&(n-1)来取代复杂的2^(i-1)次(n-1)。每一次n&(n-1)都会将二进制序列中为1的最低位完全“下放”。假设原始二进制序列中有m个1,那么经过m次n&=(n-1)操作后n的值就变为0了,所以可以将n的值作为while循环的判断条件,每一次循环都实现count++即可获得二进制序列中1的个数。

int NumberOf1(int n)
{
	int count = 0;
	while (n)
	{
		n &= (n - 1);
		count++;
	}
	return count;
}

应用

当我们能够实现NumberOf1函数后,有很多题目都可以借助NumberOf1函数解决

题目1

输入一个正数num,判断num是否等于2^n(n>=0)

 因为二进制序列中第n为的权重为2^(n-1),所以num=2^n(n>=0),当且仅当num二进制序列中1的个数为1。

int main()
{
	unsigned int num = 0;
	scanf("%u",&num);
	if (NumberOf1(num) == 1)
	{
		printf("%d是2的n次方\(n>=0\)\n",num);
	}
	else
	{
		printf("%d不是2的n次方\(n>=0\)\n", num);
	}
	return 0;
}

题目2

输入两个整数m、n,输出两个数的二进制序列数值不同的位的个数

当数值相同时,按位异或^结果为0;当数值不相同时,按位异或^结果为1。因此我们可以通过计算m^n的二进制序列中1的个数来作为输出结果。

int main()
{
	int m = 0, n = 0;
	scanf("%d%d", &m, &n);
	printf("%d", NumberOf1(m ^ n));
	return 0;
}

结语

这一次写博客尝试了使用更多的图片来帮助读者理解,虽然会增加写博客花费的时间,但是将我的想法更好的表达出来后觉得这都是值得的,之后我也会坚持使用图片来弥补我在语言表达上的不足。

看到这儿还不点关注,现在关注就是老粉了!

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZDJeffrey

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值