现在的位置: 首页 > 综合 > 正文

编程之美学习笔记——数字相关(一)

2018年02月18日 ⁄ 综合 ⁄ 共 6467字 ⁄ 字号 评论关闭

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做一次比较即可。

【上篇】
【下篇】

抱歉!评论已关闭.