博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
编程之美学习笔记——数字相关(一)
阅读量:5915 次
发布时间:2019-06-19

本文共 8327 字,大约阅读时间需要 27 分钟。

 

1.二进制数中1的个数

①有一个字节(8bit)变量,求其二进制表示中"1"的个数。

思路一:二进制数的最低位如果为1,那么它不可以被2整除,反之可以。我们可以先判断最后一位是否为1,然后右移一位,重复判断最后一位是否为1,直至该数为0;这个算法时间复杂度为O(N),其中N是这个二进制数的位数;

思路二:对于一个二进制数X,运算X&(X-1)可以将最后二进制表示中的最后一位1置为0,重复该操作直至该数为0;这个算法时间复杂度为O(M),其中M为二进制表示中1的个数;

代码:

 

int Count(int v){    int num = 0;    while(v)[        num += v & 0x01;        v >> 1;    ]    return num;}int Count(int v){    int num = 0;    while(v){        num++;        v = v & (v-1);    }    return num;}

②给定两个整数(二进制形式表示)A和B,问把A变为B需要改变多少位(bit)?

举例说明,为了减少复杂度,就使用八位二进制吧。设 A = 0010 1011, B = 0110 0101.

1. C = A & B = 0010 0001;
2. D = A | B = 0110 1111;
3. E = C ^ D = 0100 1110;
4. 结果E中有4个1,那么也就是说将A变成B,需要改变4位(bit)。
至于如何判断E的二进制表示中有几个1,可以采用快速移位与方法。
算法原理如下:
1. A & B,得到的结果C中的1的位表明了A和B中相同的位都是1的位,C中为1的位在D中必然也为1;
2. A | B, 得到的结果D中的1的位表明了A和B在该位至少有一个为1的位,包含了A 与 B 都是1的位数,D中为0的位在C中必然也为0;
经过前两步的位运算,C 中1的位表明了A 和 B在该位都是1,D中为0的位表明了A 和 B 在该位都是0 ,所以进行第三步。
3. C ^ D,E 中为1的位表明了A 和 B不同的位。

2.阶乘相关

 

①给定一个整数N,那么N! 末尾有多少个0呢?例如N=10,N!=3628800,N!末尾有2个0。

思路:如果N! = K * 10^M,其中K不能被10整除,那么N! 的末尾就有M个0,考虑对N! 进行质因数分解,N! = (2 ^ X) * (3 ^ Y) * (25 ^ Z)....,由于2 * 5 =10;所以M和X、Z一定相关,一对2和5相乘可以得到一个10,于是M=MIN(X,Z),不难看出X必然大于Z,所以M = Z;也就是要计算i(i = 0,1,2...N)的因式分解中5的指数,然后求和。

方法一:依次求每个数能贡献几个5

 

int ret = 0;for(int i = 1; i < N; i++){    int j = i;    while(j){        if(j%5 == 0)            ret++;        j/=5;    }}

方法二:

 

1到N的N个数中,可以被5整除的有N/5个,它们均贡献1个5;可以被25整除的有N/25个,它们在刚才的过程中贡献了一个5,然后又贡献第二个5;同理,能被125整除的贡献第三个5,依此类推...

 

int ret = 0;while(N){    ret += N/5;    N/=5;    }

②求N!的二进制表示中最低位1的位置

 

思路一:忽略掉除最低位1的其它所有1,如果这个1在第M位,那么我们发现这个数等于2 ^ (M-1),也就是说我们可以看原来的数中有多少个质因子2,这个数就是M-1。

 

int lastOne{    int ret = 0;    while(N){        N >> 1;        ret += N;     }    return N + 1;}

思路二:N!中含有的质因数2的个数,还等于N减去N的二进制表示中1的数目。

 

例如,假设N=11011,那么N!中含有质因数2的个数为N/2+N/4+N/8+...即,

1101 + 110 + 11 + 1 = {1000 + 100 + 1} + {100 + 10} + {10 + 1} + 1 = {1000 + 100 + 10 + 1} + {100 + 10 + 1} + 1 = 1111 + 111 + 1

={10000 - 1} + {1000 - 1} + {10 - 1} + {1 - 1}  = 11011-N中1的个数。

③给定整数n,判断它是否是2的方幂。

其实就是判断二进制表示中是否只有一个1,n > 0 && ((n & (n-1)) == 0)

3.寻找发帖水王

①论坛中某个人发帖数量超过总帖子的一半,如果有一个当前论坛上所有帖子的列表,帖子作者的ID也在其中,如何找到这个发帖数超过一半的人的ID?

思路一:将这些ID排序,如果有N个帖子,那么处于第N/2处的帖子的作者就是该水王,时间复杂度为O(N*logN)。

思路二:从这些帖子中找出发帖者不同的两个ID,删除这两个帖子,在剩下的帖子中,水王发过的帖子仍然占一半以上,重复此操作,那么剩下的一定是水王的ID;

 

Type findMaxId(Type * arr,int N){    Type candidate;    int nTimes,i;    for(i = 0; i < N; i++){        //两种情况下nTimes=0,即对于i=0和成对删除直至nTime=0        if(nTimes = 0){            candidate = arr[i];            nTimes = 1;        }        else{            if(candidate == arr[i])                nTimes++;            else                nTimes--;        }    }    return candidate;}

②有三个发帖很多的水王,每个人的发帖数超过总数的1/4,请找出这三个水王ID

 

思路:很明显,这次我们需要三个类似于candidate和三个类似于nTimes的变量,现在问题的关键是这三个nTimes为0和不为0该如何处理。

 

void findMaxId(Type * arr,int N,Type candidate[3]){  //candidate数组由main函数创建好传入    int nTimes[3],i,j;    bool flag;    for(i = 0; i < N; i++){        //flag用于标识candidate数组中的三个值有没有和arr[i]相同的        //如果有那么flag置为true,并跳出j循环,进入下一个i循环        //如果没有那么flag依旧为        flag = false;        for(j = 0; j < 3; j++){            if(arr[i] == candidate[j]){                nTimes[j]++;                flag = true;                break;            }        }        if(!flag){            //在flag为false时有三种情况,一种是nTimes中的3个值均为0,这时要重新设定candidate            //一种是nTimes中3个值均不为0,这时nTimes分别减1            //还有一种是个别nTimes值为0,这时要重新设置nTimes为0对应的candidate            if(nTimes[0] && nTimes[1] && nTimes[2]){                nTimes[0]--;                nTimes[1]--;                nTimes[2]--;            }            for(j = 0; j < 3; j++){                if(nTimes[j] != 0)                    continue;                else{                    candidate[j] = arr[i];                    nTimes[j] = 1;                    break;                }            }        }    }}

4.寻找N个数中最大的K个数

 

思路一:快速排序或堆排序,时间复杂度为Nlog(N),然后取出最大的K个数,总的时间复杂度为O(N*log(N))+O(K)=O(N*log(N));

思路二:我们只要找出最大的K个数,其余N-K个元素没必要进行排序,所以我们可以用交换排序或选择排序得到最大的K个元素,时间复杂度为O(N*K);

上面两种方法要根据log(N)和K的大小来选择;

思路三:快速排序可以把原数组分为两部分,一部分比所选的数要大,一部分要比所选的数要小,设这两部分分别为Sa和Sb,如果Sa的长度大于K,那么我们从Sa中找出最大的K个数;如果Sa的长度小于K,设为M,那么Sa中的数是我们要的,同时还要从Sb中找出K-M个数。这样我们就把原问题分成了两个子问题。这样递归解决的时间复杂度为O(N*logK)。

伪代码:

 

maxK(S,k):    if(k <= 0)        return []    if(S.length < k)        return S    [Sa,Sb] = partition(S)    return maxK(Sa,k).Append(maxK(Sb,k - Sa.length))partition(S):    p = S[1]    for i in [2:S.length]:        S[i] > p ? Sa.Append(S[i]) : Sb.Append(S[i])    Sa.length < Sb.length ? Sa.Append(p) : Sb.Append(p)    return [Sa,Sb]

用快速排序的思路找到第K大的元素:

 

 

int partition(int arr[],int begin,int end){    int x = arr[begin];    int i = begin,j = end + 1;    while(1){        //i指向的肯定是比x要小的,j指向的肯定是比x要大的        while(arr[++i] > x);        while(arr[--j] < x);        if(i >= j)            break;        int temp = arr[i];        arr[i] = arr[j];        arr[j] = temp;    }    //因为j指向的肯定比x要大,所以就把它和x交换了    a[l] = a[j];    a[j] = x;    return j;}int random_partition(int arr[],int begin,int end){    int i = begin + rand()%(end - begin + 1);    int temp = a[begin];    a[begin] = a[i];    a[i] = temp;    return partition(arr,begin,end);}int random_select(int arr[],int begin,int end,int k){    if(begin == end)    //递归结束        return arr[begin];    int index = random_partition(arr,begin,end);    int tempK = index - begin + 1;    if(tempK == k){ //找到第K大的元素        return arr[index];    }    elseif(temp > k){        return random_select(arr,begin,index - 1,k);    }else{        return random_select(arr,index + 1,end,k - tempK);    }}

思路四:上述几种方法的共同点都是对所有的数据需要访问多次,那么如果N特别特别大,达到百亿级别时该如何做呢,这个时候数据不可能完全放入内存,所以要尽量减少遍历的次数。此时我们可以维护一个大小为K的小根堆,根元素就是这K个元素中最小的一个,每次从N个元素中取出一个与它比较,如果比它小那么就舍弃,如果比它大就替换根元素,并调整小根堆,让根元素一直是最小的。调堆的过程时间复杂度为O(logK),总的时间复杂度为O(N*logK)。如果内存不能完全放下K个元素,那么我们可以先找到最大的K' 个元素,然后找次大的K' 个元素,我们需要遍历N个元素 K/K' 取上整次。

 

思路五:对于特殊情况的N个数会有特殊的解法,如果这N个整数都是正整数,并且在一定的取值范围内,那么我们找到其中最大的一个MAX,然后建立一个数组arr[MAX],下标为i的数组元素中存放的是元素i在N个数中出现的次数,我们扫描一次就可以得到该数组,这样我们就可以找到第K大的元素。

5.最大公约数

求两个整数的最大公约数。

思路一:设这两个数分别为X和Y,k = X/Y;b = X%Y;则X = k * Y + b;如果X和Y的最大公约数为Z,那么b必然可以被Z整除。f(X,Y) = f(Y,X%Y);当其中一个数为0时,另一个数就是最大公约数。

 

int gcd(int X,int Y){    if(Y == 0)        return X;    else        return gcd(Y,X%Y);}

思路二:思路一的弊端在于需要用到取模运算,对于大整数而言,这是非常严重的开销,是整个算法的瓶颈,所以我们要想别的办法避免取模运算。我们发现X和Y的最大公约数为Z,那么X-Y一定可以被Z整除,即f(X,Y) = f(X-Y,Y),如果X-Y小于Y,那么f(Y-X,Y);

 

 

int gcd(int X,int Y){    if(Y == 0)        return X;    else        return (X > Y) ? gcd(X-Y,Y) : gcd(Y-X,Y);}

思路三:思路二的弊端在于当X和Y的值相差非常大时,会多次迭代减法运算,造成很大的开销。我们观察:

 

如果X,Y均为偶数,那么2一定是它们的公约数,f(X,Y) = f(X >> 1, Y >> 1);

如果X或Y为偶数,那么2只能是其中一个的约数,设X为偶数,f(X,Y) = f(X >> 1, Y);

如果X与Y均为奇数,那么X-Y是偶数,就可以归结为上一种情况,f(X,Y) = f(X,X-Y);

 

int gcd(int X,int Y){    if(Y == 0)        return X;    else if(!(X & 0x1) && !(X & 0x1))        return  gcd(X >> 1, Y >> 1);    else if((X & 0x1) && (X & 0x1))        return (X > Y) ? gcd(X-Y,Y) : gcd(Y-X,Y);    else        return (X & 0x1) ? gcd(X >> 1,Y) : gcd(X,Y >> 1);}

 

6.寻找数组中的最大值和最小值、最大值和次大值

①求最大值和最小值

思路一:用两个变量MAX和MIN分别存放最大值和最小值,遍历数组,每个数分别与MAX和MIN比较,如果大于MAX或小于MIN就分别重置MAX或MIN,这样需要比较2N次;

思路二:用两个变量MAX和MIN分别存放最大值和最小值,成对取出数组中的数A和B,比较A和B,找到较小者(设为A),将A和MIN比较,将B和MAX比较,如果需要重置MAX或MIN则重置之,这样需要比较1.5N次;

思路三:分治法,将数组分为两部分,求出前一部分的最大和最小值,再求出后一部分的最大和最小值,然后综合起来求总体的最大值和最小值。这是个递归的过程。

 

void MaxAndMin(int A[],int begin,int end,int &Max,int &Min){    if(begin == end){        Max = Min = A[begin];        return;    }    if(begin + 1 == end){        if(A[begin] > A[end]){            Max = A[begin];            Min = A[end];        }else{            Min = A[begin];            Max = A[end];        }        return;    }    int mid = begin + (end - begin)/2;    int leftMax = 0;    int leftMin = 0;    MaxAndMin(A,begin,mid,leftMax,leftMin);    int rightMax = 0;    int rightMin = 0;    MaxAndMin(A,mid,end,rightMax,rightMin);    Max = (leftMax > rightMax) ? leftMax : rightMax;    Min = (leftMin < rightMin) ? leftMin : rightMin;}

我们来分析一下比较次数f(N),N为数组长度:

 

f(2) = 1

f(N) = 2 * f(N/2) + 2 = 2 * ( 2 * f(N/4) + 2) + 2 = 2^2 * f(N/4) + 2^2 + 2 = 2^3 * f(N/8) + 2^3 + 2^2 + 2 = 2^k * f(N/(2^k)) + 2^k + ... + 2^2 + 2 (k = logN - 1)= 1.5N-2

由此可见,分治法总的比较次数并未减少

②求最大值和次大值

利用分治法和上例方法类似,只不过有一个地方需要注意:

先求出左边的最大值leftmax和次大值leftsecond,再求出右边的最大值rightmax和次大值rightsecond,然后合并,如何合并呢?分情况考虑:

1 如果leftmax > rightmax,那么可以肯定leftmax是最大值,但次大值不一定是rightmax,但肯定不是rightsecond,只需将leftsecond与rightmax做一次比较即可。

2 如果rightmax > leftmax,那么可以肯定rightmax是最大值,但次大值不一定是leftmax,但肯定不是leftsecond,所以只需将leftmax与rightsecond做一次比较即可。

 

 

转载地址:http://kdgpx.baihongyu.com/

你可能感兴趣的文章
Google攻击最新调查结果曝光:密码系统受害
查看>>
Linux进程托管与守护进程设置
查看>>
自定义类和集合
查看>>
Android Mediaplayer解读
查看>>
Linux动态链接库的问题
查看>>
Processing的代码编写流程
查看>>
转[]面向对象基础(概念、特征、要素)
查看>>
GLSL学习笔记 [转]
查看>>
C#方法参数传递-输出参数out关键字
查看>>
IT项目各岗位职责 (转)
查看>>
fstab 介绍
查看>>
jquery.validate使用攻略
查看>>
如果将彩色图像和灰度图像一起放进 CNN 中去,会是什么结果?
查看>>
dubbo 学习笔记 -- provider端
查看>>
Mysql 不同版本 说明
查看>>
poj3090
查看>>
一些学习资源
查看>>
如何开启或关闭SELinux
查看>>
checkbox全选JQuery语句
查看>>
Android 开发学习资源
查看>>