【剑指offer】——根据位运算求解的问题


位运算就是把数字用二进制表示之后,对每一位上0或者1的运算。其总共有5种运算:与、或、异或、左移和右移

一、二进制中1的个数

题目要求:
请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如把9表示成二进制是1001,有2位是1。因此如果输入9,该函数输出2。

题目分析:
方法一:可能引起死循环
每次都判断二进制中的最右边的一位,用n&1看结果是否是1,如果为1则表示该整数的最右边的一位是1,否则为0。判断完毕再把n进行右移操作。
但是这种方法如果遇到负数的情况就会陷入死循环。

方法二:求解效率不高
上一种方法我们是每一次移动数字n,但是这种方法我们的思路则是每一次向左移动数字1。来挨个判断数字n中每个二进制位是否为1。
这种解法循环的次数等于整数二进制的位数。

方法三:最有效的方式
我们来思考这样一个规律:如果一个数不为0 ,那么他的二进制表示里面至少会有一个1存在。那么当数字n减1过后。会有两种情况
第一种:n的最右边为1。减1过后,最后一位变为0,其余所有位都保持不变。
在这里插入图片描述
第二种:n的最右边不为1.如图所示,第二位由1变成了0,第二位之后的0都变成了1.他左边的所有位保持不变。
在这里插入图片描述
总结:把一个整数减去1,再和原整数做与运算,会把该整数最右边的1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。

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

二、数组中数字出现的次数

题目要求:
数组中只出现一次的两个数字。一个整形数组除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度为O(n),空间复杂度为O(1)。

题目分析:
首先拿到这道题,我们可能会感觉比较懵。但是我们这样来思考一下。我们把问题简化一点儿,在一个数组中,只有一个数字出现了一次,其余数字都出现了两次。
这样一想,我们可以利用位运算里面异或的性质来求解问题。**因为任何一个数字异或他自己都等于0 ,**所以这样将数组中的数字挨个儿异或结束过后剩下的数字即为单出来的那个数字。如下:
在这里插入图片描述

那么,接下来,我们就要来思考如何将该数组划分成两个只有一个数字单出来的数组。
我们从头到尾异或数组中的每个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。如数组{2,4,3,6,3,2,5,5}。4^6 = 0010
在这里插入图片描述
根据上述计算,我们可以知道,在这个结果数字的二进制表示中至少有一位为1。于是我们根据倒数第二位是不是1将该数组分成两个子数组。第一个数组是{2,3,6,3,2}。第二个数组是{4,5,5}。接下来分别对这两个数组求异或,就能找到第一个字数组中只出现一次的数字是6,第二个子数组中只出现一次的数字是4。

有了上述的分析。接下来就是代码实现
首先是在整数num的二进制表示中找到最右边是1的位。

unsigned int FindFirstBitIs(int num)
{
	int indexBit = 0;
	while( ((num & 1) == 0) && (indexBit < 8 * sizeof(int)))
	{
		num = num >> 1;
		++indexBit;
	}
	return indexBit;
}

再来判断num的二进制表示中从右边数起的indexBit位是不是1

bool IsBit1(int num, unsigned int indexBit)
{
	num = num >> indexBit;
	return (num & 1);
}

最后代码实现
其中,data是传入的数组,num1和num2分别是分割后的两个数组

void FindNumsAppearOnce(int data[], int length, int* num1, int* num2)
{
	if (data == nullptr || length < 2)
		return;

	int resultExclusiveOR = 0;//数组中所有数字异或过后的结果
	for (int i = 0; i < length; i++)
	{
		resultExclusiveOR ^= data[i];
	}

	unsigned int indexof1 = FindFirstBitIs(resultExclusiveOR);//找到异或后二进制结果中1的位置

	*num1 = *num2 = 0;
	for (int j = 0; j < length; j++)
	{
		if(IsBit1(data[j],indexof1)//数组中该位置为1的数在num1数组中进行异或,最后异或完剩下的值即为单出来的值
			*num1 ^= data[j];
		else
			*num2 ^= data[j];
	}
}

三、数组中唯一只出现一次的数字

题目大意:
在一个数组中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
题目分析:
仔细看看这一道题,我们发现它和上一题有很多相似的地方,但是又有些许的不同。如果该数组中其他数字都只出现了两次,那么异或可得到单出来的那个数字。但是现在其他数字都出现了三次,三个相同的数字异或后的结果还是该数字,所以上述的思路不可行了。
接下来,我们还是用位运算来思考一下。
假设该数组为{5,5,5,2}。我们知道,如果一个数字出现三次,那么他的二进制表示的每一位也出现三次。如果我们把所有出现三次的数字的二进制表示的每一位都分别加起来,那么每一位的和都能被3整除如下:
在这里插入图片描述
此时,我们把所有数字的二进制表示的每一位都加起来会发现什么
在这里插入图片描述
我们发现,如果某一位的和能被3整除,那么那个只出现一次的数字,例如题目中的2,其二进制表示中对应的那一位是0,否则就是1.
有了上述规律,我们就可以着手写代码了。我们需要一个长度为32的辅助数组存储二进制表示的每一位的和。

int FindNumberAppearingOnce(int numbers[], int length)
{
	if (numbers == nullptr || length <= 0)
		throw new std::exception("Invalid input.");

	int bitSum[32] = { 0 };
	//外层循环主要用于遍历数组中的每一个数字
	for (int i = 0; i < length; i++)
	{
		//通过移动bitMask和数字中的每一位进行与操作,判断该位是否为1
		int bitMask = 1;

		//内层循环主要用于遍历该数字中的每一位
		for (int j = 32; j >= 0; j--)
		{
			int bit = numbers[i] & bitMask;
			if (bit & 1 != 0)
				bitSum[j] += 1;
		}
		bitMask << 1;
	}

	//依次输出只出现一次数字的每一位
	int result = 0;
	for (int i = 0; i < 32; i++)
	{
		result = result << 1;//移动每一位
		result += bitSum[i] % 3;//每一位和被3整除的结果即为只出现一次数字的该位
	}
	return result;
}

四、不用加减乘除做加法

题目要求:
写一个函数,求两个整数之和,要求在函数体内不得使用“+”、“-”、“*”、“/”四则运算符。

题目分析:
分析这道题之前,我们先把其简化一下,想象成我们比较常规的一个解法。我们做十进制的加法的时候比如得到5+17=22这个结果。
第一步,只做各位相加,不进位,此时结果是12。
第二步,做进位,5+7中有进位是10。
第三步,把上述两个结果相加12+10 = 22

接下来,还是同样的方法,我们把这个方法运用到二进制加法用位运算来替代。比如5的二进制为101加上7的二进制是10001。
第一步,不考虑进位对每一位相加。因为在二进制运算中0+0=0,1+1=0;0+1=1,1+0=1.。有趣的是,我们发现这样一个相加的结果正好是异或的结果。
在这里插入图片描述
第二步:考虑进位,我们知道对于0+0,0+1,1+0而言都不会产生进位,只有1+1才会产生进位。类比一下位运算,我们立即想到了与操作。进位的过程就好像对这两个二进制数进行与操作,最后把与操作后的结果左移一位即可
在这里插入图片描述
第三步:相加的过程依然重复前面两步,直到不产生进位为止
在这里插入图片描述
代码实现如下:

int Add(int num1, int num2)
{
	int sum, carry;
	do
	{
		sum = num1 ^ num2;
		carry = (num1 & num2) << 1;
		num1 = sum;
		num2 = carry;
	} while (num2 != 0);

	return num1;
}
发布了98 篇原创文章 · 获赞 9 · 访问量 3651

猜你喜欢

转载自blog.csdn.net/qq_43412060/article/details/105359129