目录
1.二进制中1的个数(剑指offer 15 )
题目描述:
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
解题思路:
三种解题思路。
(1)用输入的数的每一位和1做与运算,统计结果为1的数量,即是结果。这里边难点是如何统计每一位的位运算结果?使用右移操作,把输入的数每次右移一位和1做与运算,然后依次统计每次循环的结果。这里有个问题,根据补充知识的介绍我们知道,当输出是负数的时候,右移操作需要在左边补上1而不是0(根据编译器决定),如果一直补0,很可能会造成死循环。所以这种做法错误。(2)常规解法:我们尽量比避免在位运算中进行右移操作。在不改变输入数据的情况下,用一个变量不断向左移和原数据进行与运算。(如用1(1)去判断最低位,用2(10)去判断倒数第2位,用4(100)去判断倒数第3位······以此类推)
优点:简单,好理解。
缺点:当输入数据的二进制位数越大时,进行比较的次数越多。(100位比较100次) 有没有比较少一点的方法?(3)惊喜解法:有几个1就比较几次的方法!!!以1100为例,我们先对其做一个减1的操作,得到:1011。然后再和原数据(1100)进行与运算,得到结果:1000 。这个结果实际上就是把原数据(1100)的最右边一个1变成了0。继续上边的操作,得到结果0000。结束,可以得到原数据中有2个1。一共比较2次。
注意 :把一个整数减去1之后再和原来的整数进行位与运算,得到的结果相当于把整数的二进制表示中最右边的1变成了0,很多二进制的位运算用到了这个结论!!!
代码:
常规解法
class Solution {
public:
int NumberOf1(int n) {
int count =0;
int flag = 1;//检测变量
while(flag)
{
if(flag&n) //检测位是1
count++;
flag = flag<<1; // 左移变量
}
return count;
}
};
惊喜解法
class Solution {
public:
int NumberOf1(int n) {
int count =0;
while(n)
{
count++;
n = (n-1)&n; // 整数减1再和整数做位与运算
}
return count;
}
};
总结扩充
注意 :把一个整数减去1之后再和原来的整数进行位与运算,得到的结果相当于把整数的二进制表示中最右边的1变成了0,很多二进制的位运算用到了这个结论!!!
相关题目:
1、用一条语句判断一个整数是不是2的整数次方?
答:一个整数是2的整数次方如4,8那么在二进制表示中有且只有一个1,我们只需要把这个整数减去1再和他自己做位于运算,这个整数的1就会变成0。
n&(n-1)==0(但是对最小的那个负数 10000000 应该不行)
2、输入两个整数m和n,计算需要改变m的二进制表示中的多少位才能得到n。比如10的二进制(1010),13的二进制(1101)。
答:分两步,我们首先先计算这两个数有几位是不同的,用异或运算即可。第二步统计异或运算结果中有几个1就可以知道两个数有几位是不同的。
2.数值的整数次方 (剑指offer 16 )
题目描述:
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
解题思路:
主要考察考虑问题要全面,对于这种实现库函数的题来说。
对于这道题,有2个情况要考虑
1.0的正数次方等于0,0的0次方和负数次方没有意义
2. 指数为负的情况,不是简单的底数累乘,还需要把结果求倒数,对于没有意义,也就是错误输入的,我们可以通过设置一个全局变量来提醒。下面的实现没有这样做,自己知道这种方法就行。
代码:
class Solution {
public:
double Power(double base, int exponent) {
if(base == 0.0 && exponent <=0){
return 0.0; // 0的非正数次方没有意义
}
bool is_negative = 0;
int absexp = exponent;
if(exponent<0){
is_negative = 1;
absexp =-absexp;
}
double res =1.0;
for(int i=1;i<=absexp;++i){
res *= base;
}
return (is_negative?(1.0/res):res);
}
};
3.丑数(剑指offer 49 )
题目描述:
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
解题思路:
思路1:
遍历每个数,假如这个数的因子只有2,3,5则 uglyfound++,uglyfound==我们要找的第n个数,则停止,返回。思路2:
丑数数组 arry 的下一个值,肯定是arry中已有的值 分别乘以2,3,5得到的所有情况中,满足大于arry中最大值的最小值,而且arry中肯定存在一个下标为x的丑数,它前面的丑数*2都小于已有最大的丑数,它后面的丑数都大于我们要加的新的丑数i 。 *3 *5 类似 有个 y z
那么新加的丑数i 就是 2*x,3*y,5*z中的最小值,并且选中了哪个 那个下标要++,而没选中的不动,并且要考虑到有2个及以上的下标是同一值,那就都++还有一个疑问 为啥 没选中的下标不动? 为什么它还会是下一次的候选人?
因为对于下一次选来说,在我之前的依旧不可能(因为最大的丑数变大了),而因为我没选上,在我之后的更不可能(因为在在我之后的*2 或者3,5) 比我更大。我都没轮到你怎么可能。
代码:
class Solution {
public:
int GetUglyNumber_Solution(int index) {
if(index<7) return index; // 1-6的丑数分别为1-6
vector<int> res(index);
int p2=0,p3=0,p5=0; //p2,p3,p5分别为三个队列的指针
res[0]=1; //第一个丑数为1
for(int i=1;i<index;++i){
res[i]=min(res[p2]*2,min(res[p3]*3,res[p5]*5));
//选出三个队列头最小的数
//这三个if有可能进入一个或者多个,进入多个是三个队列头最小的数有多个的情况
if(res[i]==res[p2]*2) p2++;
if(res[i]==res[p3]*3) p3++;
if(res[i]==res[p5]*5) p5++;
}
return res[index-1];
}
};
4.求1-k以内的所有素数
题目描述:
这里只是给出用筛法求素数的方法, 实际还有更好的方法,但爷不管了
解题思路:
因为1不是素数,所以从2开始,到k,遍历每个数i,假如i之前设置过不是素数了,跳过,假如i没有设置过,那去掉它的倍数,然后把i加进来。
代码:
vector<int> solve(int k){
bool not_prime[k+1]={0};
vecotr<int> res;
for(int i=2;i<=k;++i){
if(!not_prime[i]){ //假如是最小的素数
res.push_back(i);
for(int j=i*2;j<=k;j +=i)
not_prime[j]=true;
}
}
return res;
}
5.求1+2+3+...+n (剑指offer 64 )
题目描述:
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
解题思路:
本身没什么意义
求1+2…+n除了用公式n*(n+1)/2,无外乎循环和递归两种思路。但是循环不能使用,所以我们想办法用递归解决,但是递归也需要一个递归结束的条件,也需要判断但是没有条件,我们只能想到用短路特性来做。
短路特性
a&&b 如果a是假的那么不会执行b
代码:
// time: O(n)
class Solution {
public:
int Sum_Solution(int n) {
int sum = n;
sum && (sum += Sum_Solution(n-1)); // sum=0结束递归返回 1&&sum=1+0->
// 2&&sum=2+1->3&&sum=3+2+1
return sum;
}
};
// 还可以使用构造函数,虚函数,模板函数,函数指针
另一种解法,但是有些编译器,不支持数组的维度是个变量。
class Solution {
public:
int Sum_Solution(int n) {
bool a[n][n+1];
return sizeof(a)>>1;
}
};
6.不用加减乘除做加法(剑指offer 65 )
题目描述:
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
解题思路:
首先看十进制是如何做的: 5+7=12,三步走
第一步:相加各位的值,不算进位,得到2。
第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。
第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。同样我们可以用三步走的方式计算二进制值相加: 5-101,7-111
第一步:相加各位的值,不算进位,
得到010,二进制每位相加就相当于各位做异或操作,101^111。
第二步:计算进位值,得到1010,相当于各位做与操作得到101,再向左移一位得到1010,(101&111)<<1。
第三步重复上述两步, 各位相加 010^1010=1000,进位值为100=(010&1010)<<1。
继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果
代码:
代码1:
class Solution {
public:
int Add(int num1, int num2)
{
int sum, carry;
do
{
sum = num1 ^ num2; // 相加过程 位异或替代
carry = (num1 & num2)<<1; //进位过程 位与运算和左移替代
num1 = sum;
num2 = carry;
}
while(num2 !=0); // 当进位为0时 结束条件
return num1;
}
};
代码2:
class Solution {
public:
int Add(int num1, int num2)
{
return num2?Add(num1^num2,(num1&num2)<<1):num1;
//当进位 num2为0的时候,返回num1,否则继续计算
}
};
7.构建乘积数组(剑指offer 66)
题目描述:
给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。
解题思路:
暴力:使用两次for进行连乘可以完成目的,但是时间复杂度太高O(n^2)
O(N):
把B[i]=A[0]A[1]…*A[i-1]A[i+1]…*A[n-1]分成两部分,第一部分是A[0]A[1]…A[i-1]
第二部分是A[i+1]…*A[n-1],在每个部分里面把B[i]这一部分给计算好,第一部分是顺序的,第二部分是逆序的附图理解:
代码:
class Solution {
public:
vector<int> multiply(const vector<int>& A) {
int len = A.size();
vector<int> B(len);
int ret =1;
//第一部分,顺序
for(int i=0;i<len;++i){
B[i] = ret;
ret *= A[i];
}
//第二部分,逆序
ret = 1;
for (int i = len-1;i>=0;--i){
B[i] *= ret;
ret *= A[i];
}
return B;
}
};
8.数组中重复的数字(剑指offer 3)
题目描述:
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
解题思路:
1. 时间O(N) 空间 O(N)
遍历数组a[i],同时再搞个数组count, 如果count[a[i]]==1, return2. 时间O(N) 空间 O(1) 但是 可能会溢出,并且要修改原来的数组。
题目里写了数组里数字的范围保证在0 ~ n-1 之间,所以可以利用现有数组设置标志,当一个数字被访问过后,可以设置对应位上的数 + n,之后再遇到相同的数时,会发现对应位上的数已经大于等于n了,那么直接返回这个数即可。3. 时间O(N) 空间 O(1) 不会溢出,但是也有修改原来的数组
遍历数组,如果当前遍历到的值arr[i]=m 不等于i,那么就去看arr[m],如果 arr[m]=m,就说明找到了重复的数,如果arr【m】!=m,那么arr【i】和arr【m】 swap一下,这样m就到了它该去的地方,接下来重复这个比较和交换。
讲这个主要是为了另一题(自然数的排序):
假如给一个整型数组arr,其中有n个互不相等的1~n个自然数,在时间复杂度O(N),空间O(1),实现排序,并且不能通过直接赋值的方式。
这题就可以用这种方法!!!
代码:
class Solution {
public:
bool duplicate(int numbers[], int length, int* duplication) {
int *num=new int[length]();
for(int i=0;i<length;++i)
{
int t = numbers[i];
if(num[t] == 1){
*duplication = t;
return true;
}
++num[t];
}
return false;
}
};
代码2:
bool find_dup( int numbers[], int length, int* duplication) {
for ( int i= 0 ; i<length; i++) {
int index = numbers[i];
if (index >= length) {
index -= length;
}
if (numbers[index] >= length) {
*duplication = index;
return true;
}
numbers[index] = numbers[index] + length;
}
return false ;
}
方法3 部分代码
for(int i=0;i!=arr.size();++i){
while(arr[i] !=i){
// 找到了重复的数
if(arr[i]== arr[arr[i]])
return 结果
//否则交换
swap(arr[i],arr[arr[i]]);
}
}
9.数组中只出现一次的两个数字(剑指offer 56)
题目描述:
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
解题思路:
首先:位运算中异或的性质:两个相同数字异或=0,一个数和0异或还是它本身。
所以假如当只有一个数出现一次时,我们把数组中所有的数,依次异或运算,最后剩下的就是落单的数,因为成对出现的都抵消了。
依照这个思路,我们来看两个数(我们假设是AB)出现一次的数组。
我们首先还是先异或,剩下的数字肯定是A、B异或的结果,这个结果的二进制中的1,表现的是A和B的不同的位。
我们就取第一个1所在的位数,假设是第3位,接着把原数组分成两组,分组标准是第3位是否为1。如此,相同的数肯定在一个组,因为相同数字所有位都相同,而不同的数,肯定不在一组。然后把这两个组按照最开始的思路,依次异或,剩余的两个结果就是这两个只出现一次的数字。这里我用了n-(n&(n-1)) 的公式得到 0100这种形式
而书里的解法额。通用但是不够优美,但是可以看看
代码:
我的解法:
class Solution {
public:
void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
if(data.size()<2)
return;
int res=0;
for(int i=0;i!=data.size();++i){
res ^= data[i];
}
res = res-(res&(res-1)); // n&(n-1) 消去最右边的1
// res最后就是 000100 这种形式了
//括号必不可少
*num1=0;
*num2=0;
for(int i=0;i!=data.size();++i){
if(data[i]&res) *num1 ^= data[i];
else *num2 ^= data[i];
}
}
};
书里的解法:
class Solution {
public:
void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
if(data.size()<2) return;
int res = 0;
for(int i=0;i<data.size();++i){
res ^= data[i];
}
int index = findfirstbit1(res);
*num1= 0;
*num2=0;
for(int i=0;i<data.size();++i){
if(isbit1(data[i],index)){
*num1 ^= data[i];
}
else{
*num2 ^= data[i];
}
}
}
int findfirstbit1(int num){
int res=0;
while((num &1)==0 && res< 8*sizeof(int)){
num=num>>1;
++res;
}
return res;
}
bool isbit1(int num,int index){
num=num>>index;
return (num &1);
}
};