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


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

一、二进制中1的个数

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

示例1
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。

示例2
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 ‘1’。

示例3
输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 ‘1’。

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

int numberofN1(uint32_t n)
{
	int count = 0;
	while (n)
	{
		if(n&1)
		count++;
		n = n >> 1;
	}
	return count;
}

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

int numberofN2(uint32_t n)
{
	int count = 0;
	unsigned int flag = 1;
	while (flag)
	{
		if (n & flag)
			count++;
		flag = flag << 1;
	}
	return count;
}

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

int numberofN3(uint32_t n)
{
	int count = 0;
	while (n)
	{
		n = n & (n - 1);
		count++;
	}
	return count;
}
int main()
{
	cout << numberofN3(00110011) << endl;
	return 0;
}

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

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

示例1:
输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

示例2:
输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]
限制:2 <= nums.length <= 10000

题目分析:
首先拿到这道题,我们可能会感觉比较懵。但是我们这样来思考一下。我们把问题简化一点儿。在一个数组中,只有一个数字出现了一次,其余数字都出现了两次。
这样一想,我们可以利用位运算里面异或的性质来求解问题。

  1. 因为任何一个数字异或他自己都等于0 所以这样将数组中的数字挨个儿异或结束过后剩下的数字即为单出来的那个数字。
    在这里插入图片描述

  2. 接下来,我们就要来思考如何将该数组划分成两个只有一个数字单出来的数组

    我们从头到尾异或数组中的每个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。如数组{2,4,3,6,3,2,5,5}。4^6 = 0010
    在这里插入图片描述
    根据上述计算,我们可以知道,在这个结果数字的二进制表示中至少有一位为1

  3. 于是我们根据倒数第二位是不是1将该数组分成两个子数组

    第一个数组是{2,3,6,3,2}。第二个数组是{4,5,5}。接下来分别对这两个数组求异或,就能找到第一个字数组中只出现一次的数字是6,第二个子数组中只出现一次的数字是4。

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

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

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

bool IsBit(int num,unsigned int Bitindex)
{
	num = num >> Bitindex;
	return(num & 1);
}

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

vector<int> singleNumbers(vector<int>& arr)
{
	int num1 = 0;
	int num2 = 0;
	vector<int> res;

	if (arr.size() == 0)
		return vector<int>();

	int result = 0;
	for (int i = 0; i < arr.size(); i++)
	{
		result ^= arr[i];
	}

	unsigned int index = FindFirstBitIs(result);
	for (int j = 0; j < arr.size(); j++)
	{
		if (IsBit(arr[j], index))
		{
			num1 ^= arr[j];
		}
		else
			num2 ^= arr[j];
	}
	res.push_back(num1);
	res.push_back(num2);
	return res;
}

int main()
{
	vector<int> a;
	vector<int> b;
	a.push_back(4);
	a.push_back(1);
	a.push_back(4);
	b = singleNumbers(a);
	for (int i = 0; i < b.size(); i++)
	{
		cout << b[i] << endl;
	}
	return 0;
}

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

题目大意:
在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例1:
输入:nums = [3,4,3,3]
输出:4

示例2:
输入:nums = [9,1,7,9,7,9,7]
输出:1

题目分析:
方法一:采用排序 O(nlogn )S(1)
这个思路非常简单,只需要先将数组金进行排序,然后在排了序之后的数组中,将该数字和前后两个相邻的数字进行比较,如果不等,就返回该数字

bool flag = false;
int singleNumber(vector<int>& arr) 
{
	if (arr.size() == 0)
	{
		flag = true;
		return 0;
	}

	flag = false;
	sort(arr.begin(), arr.end());
	if (arr[0] != arr[1])
		return arr[0];

	for (int i = 1; i < arr.size()-1; i++)
	{
		if (arr[i - 1] != arr[i] && arr[i] != arr[i + 1])
			return arr[i];
	}
	return arr[arr.size() - 1];
}
int main()
{
	vector<int> res;
	res.push_back(5);
	res.push_back(5);
	res.push_back(5);
	res.push_back(2);
	cout << singleNumber(res) << endl;
	return 0;
}

方法二:采用位运算 O(n )S(1)

仔细看看这一道题,我们发现它和上一题有很多相似的地方,但是又有些许的不同。如果该数组中其他数字都只出现了两次,那么异或可得到单出来的那个数字。但是现在其他数字都出现了三次,三个相同的数字异或后的结果还是该数字,所以上述的思路不可行了。这道题详细的解答请见博客数组中唯一只出现一次的数字
接下来,我们还是用位运算来思考一下。假设该数组为{5,5,5,2}。

  1. 如果一个数字出现三次,那么他的二进制表示的每一位也出现三次。如果我们把所有出现三次的数字的二进制表示的每一位都分别加起来,那么每一位的和都能被3整除

如下:
在这里插入图片描述

  1. 把所有数字的二进制表示的每一位都加起来

在这里插入图片描述

  1. 判断每一位的和是否能被3整除。如果能,那么只出现一次的数字的二进制中的哪一位为0,否则为1.
    在这里插入图片描述
    代码实现如下:
int singleNumber1(vector<int>& arr)
{
	if (arr.size() == 0)
		return 0;

	int b[32] = { 0 };
	for (int i = 0; i < arr.size(); i++)
	{
		int flag = 1;
		for (int j = 0; j < 32; j++)
		{
			if (arr[i] & flag)
			{
				b[j] += 1;
			}
			flag <<= 1;
		}
	}

	int res = 0;
	for (int i = 31; i >= 0; i--)
	{
		res <<= 1;
		res += (b[i] % 3);
	}
	return res;
}
int main()
{
	vector<int> res;
	res.push_back(5);
	res.push_back(5);
	res.push_back(5);
	res.push_back(2);
	cout << singleNumber1(res) << endl;
	return 0;
}

四、不用加减乘除做加法

题目要求:
求 1+2+…+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)

示例1:
输入: n = 3
输出: 6

示范2:
输入: n = 9
输出: 45
限制:1 <= n <= 10000

1、两个数相加解析
1、分析在十进制下的常规情况
分析这道题之前,我们先把其简化一下,想象成我们比较常规的一个解法。我们做十进制的加法的时候比如得到5+17=22这个结果。

  1. 第一步,只做个位相加,不进位,此时结果是12。
  2. 第二步,做进位,5+7中有进位是10。
  3. 第三步,把上述两个结果相加12+10 = 22

2、类比到二进制中的情况
接下来,还是同样的方法,我们把这个方法运用到二进制加法用位运算来替代。比如5的二进制为00101加上17的二进制是10001。

  1. 第一步,不考虑进位对每一位相加

    因为在二进制运算中0+0=0,1+1=0;0+1=1,1+0=1.。有趣的是,我们发现这样一个相加的结果正好是异或的结果。
    在这里插入图片描述

  2. 第二步:考虑进位,我们知道对于0+0,0+1,1+0而言都不会产生进位,只有1+1才会产生进位。类比一下位运算,我们立即想到了与操作进位的过程就好像对这两个二进制数进行与操作,最后把与操作后的结果左移一位即可

在这里插入图片描述

  1. 第三步:相加的过程依然重复前面两步,直到不产生进位为止(num2为0)
    在这里插入图片描述
    代码实现如下:
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;
}

2、从1+2+3……+n解析

采用逻辑运算符中的短路效应,如下所示:

if(A && B) // 若 A 为 false ,则 B 的判断不会执行(即短路),直接判定 A && B 为 false

采用递归的方式求解。当n =1时终止递归。因为要开启n个递归函数,所以时间复杂度是O(n)。递归的深度也达到了n,系统使用O(n)大小的额外空间
在这里插入图片描述

int Sum2(int n)
{
	n > 1 && (n += Sum2(n - 1)) ;
	return n;
}
int main()
{
	cout << Sum2(3) << endl;
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值