刷题(4)-位运算和数组-题目(1)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/speargod/article/details/97187151

目录

 

1.二进制中1的个数(剑指offer 15 ) 

2.数值的整数次方  (剑指offer 16 ) 

3.丑数(剑指offer 49 ) 

4.求1-k以内的所有素数 

5.求1+2+3+...+n (剑指offer 64 ) 

6.不用加减乘除做加法(剑指offer 65 ) 

7.构建乘积数组(剑指offer 66) 

8.数组中重复的数字(剑指offer 3) 

9.数组中只出现一次的两个数字(剑指offer 56) 


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(但是对负数应该不行)
2、输入两个整数m和n,计算需要改变m的二进制表示中的多少位才能得到n。比如10的二进制(1010),13的二进制(1101)。
:分两步,我们首先先计算这两个数有几位是不同的,用异或运算即可。第二步统计异或运算结果中有几个1就可以知道两个数有几位是不同的。
 

2.数值的整数次方  (剑指offer 16 ) 

题目描述:

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

解题思路:

主要考察考虑问题要全面,对于这种实现库函数的题来说

对于这道题,有2个情况要考虑
1.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]这一部分给计算好,第一部分是顺序的,第二部分是逆序的

附图理解:

!aGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3NDY2MTIx,size_16,color_FFFFFF,t_70)

代码:

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, return 

2. 时间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);
	}
};

猜你喜欢

转载自blog.csdn.net/speargod/article/details/97187151