zoukankan      html  css  js  c++  java
  • 编程之美学习笔记——数字相关(一)

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














  • 相关阅读:
    经典排序之 计数排序
    经典算法 总结
    经典排序之 二路归并排序
    经典排序之 堆排序
    经典排序之 插入排序
    经典排序之 冒泡排序
    经典排序之 选择排序
    经典排序之 快速排序
    两个队列实现一个栈
    Java Web系列:JDBC 基础
  • 原文地址:https://www.cnblogs.com/dyllove98/p/3144859.html
Copyright © 2011-2022 走看看