剑指OFFER(6)

来源:互联网 发布:八爪鱼导出到数据库 编辑:程序博客网 时间:2024/06/05 11:47

面试题38:数字在排序数组中出现的次数

显然这是要用二分搜索。不同的是,数字会出现多个。
包含重复数值的二分搜索,重点在于,a[mid]==k的情况:

    int first(int* a,int n,int k){        if(a[0]>k||a[n-1]<k)            return -1;        int left=0,right=n-1;        while(left+1<right){            int mid=left+(right-left)/2;            if(a[mid]<k){                left=mid;            }else                right=mid;        }        if(a[left]==k)            return left;        if(a[right]==k)            return right;        return -1;    }    int last(int* a,int n,int k){        if(a[0]>k||a[n-1]<k)            return -1;        int left=0,right=n-1;        while(left+1<right){            int mid=left+(right-left)/2;            if(a[mid]<=k){                left=mid;            }else                right=mid;        }        if(a[right]==k)            return right;        if(a[left]==k)            return left;        return -1;    }    int GetNumberOfK(vector<int> data ,int k) {        int n=data.size();        if(n<=0)            return 0;        int* a=&data[0];        int fir=first(a,n,k);        if(fir==-1)            return 0;        else            return last(a,n,k)-first(a,n,k)+1;    }

其基本思路是,用left和right对区间进行囊括,首先直接否定a[0]>k和a[n-1]

面试题39:二叉树的深度

不多说,递归即可。

    int TreeDepth(TreeNode* pRoot){        if(pRoot==nullptr)            return 0;        return 1+max(TreeDepth(pRoot->left),TreeDepth(pRoot->right));    }

扩展:判断平衡二叉树

平衡二叉树的条件:某根节点,它的左右子树都是平衡树,且左右子树深度相差不大于1.

    //如果是平衡树,则返回深度;否则返回-1;    int depth(TreeNode* p){        if(p==nullptr)            return 0;        int dep1=depth(p->left),dep2=depth(p->right);        if(dep1!=-1 && dep2!=-1 &&abs(dep1-dep2)<=1){            return max(dep1,dep2)+1;        }else{            return -1;        }    }    bool IsBalanced_Solution(TreeNode* pRoot) {        return depth(pRoot)!=-1;    }

面试题40:数组中只出现一次的数组

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
答案很巧妙地使用了异或来求解。异或满足交换和结合律,就如同加法和乘法一样,这意味着一列数进行异或,不论这些数排列如何,结果是一样的。
基本公式:
0^x=x x^x=0
假如:一列数仅仅有一个数出现了一次,其他数都出现两次,求这个数。那么所有的数都进行异或(选择0做基底,类似于累加或累乘),最后的值一定是那个数。
例如:a^b^c^b^a=a^a^b^b^c=0^0^c=c
这里做了升级版,因为数组中有两个这样的数。所以最后的异或结果,是这两个数的异或。所以要找到某个条件,对这两个数进行区分。
因为这两个数是不同的,一定会有一位不同。那么找到这个位,把原数组按照这个位进行分组,那么一定可以把这两个数分入不同的组内。

    void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {        int x=0;        for(int i=0;i<data.size();++i)            x^=data[i];        int b=1;        while((b&x)==0)//位运算优先级仅仅高于逻辑/条件/赋值/逗号 所以一般都要加括号            b<<=1;        int x1=0;        for(int i=0;i<data.size();++i)            if(data[i]&b)              x1^=data[i];        *num1=x1;        *num2=x^x1;    }    //右边的>单目的>四则》移位》比较》位运算》逻辑条件》赋值

首先用一个x对所有数进行异或,最后x=num1^num2;从右向左找到x第一个为1的位,这表示该位上,num1和num2是不同的。然后用x0对所有该位上是1的数进行异或,那么x0最终一定是num1或num2中的某个数。
tip: 常用位操作
取位操作 x&(1<

面试题41:和为s的两个数字 VS 和为s的连续正数序列

题1:输入一个递增排序的数组和一个数字S,在数组中查找两个数,使他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
如果采用两层遍历的方法,需要O(N^2)的时间,显然不是最好的。
例如 2 4 5 6 7 8 9 15 20中选取两数字使得和为15.
这里选用了一个策略:
使用left和right表征这两个数。
首先选取最小的2,与最大的20.两者和大于15
此时如果想要减少它们的和,要么使得left左移动,要么使right左移。显然,只能让right左移,此时两数字为2+15>15:所以还需要让right左移。
现在两数:2+9<15.为了让这两个数的和加大,要么使得left右移,要么使right右移。但是right就是左移过来的,所以只能让left右移。
所以整个过程是:
if A[left]+A[right]

    vector<int> FindNumbersWithSum(vector<int> array,int sum) {        vector<int> v;        if(array.empty())            return v;        int left=0,right=array.size()-1;        while(left<=right){            if(array[left]+array[right]==sum){                v.push_back(array[left]);                v.push_back(array[right]);                return v;            }            if(array[left]+array[right]<sum)                left++;            else                right--;        }        return v;    }

题2:从[1,2,3,4…]中找出所有和为S的连续正数序列。
规则和上题不同:初始化left和right为[1,2];如果以left和right做边界的区间,数总和小于S的话,就让right右移;反之让left右移。
上题中,初始时left和right分别在数组的首尾端
本题中,初始时left和right分别是最前端的两个数,如果和不够,就扩右边界;如果超出,则缩左边界。
注意本题是找到所有的子区间。

    vector<vector<int> > FindContinuousSequence(int sum) {        vector<vector<int> > v;        if(sum<3)            return v;        //初始的情况:只有[1 2]两个数,和为3        int min=1,max=2,s=3;        while(min<max){//最后的情况,肯定是序列长度越来越短            if(s<sum){                max+=1;                s+=max;            }else if(s>sum){                s-=min;                min+=1;            }else{                vector<int> v1;                for(int i=min;i<=max;++i)                    v1.push_back(i);                v.push_back(v1);                //重点:当找到一段序列后,如何寻找下一段?                //答案:右边界右移。这样才会纳入新数据进入区间。                max++;                s+=max;            }        }        return v;    }

面试题42:翻转单词顺序 VS 左旋转字符串

题目1:翻转句子中的单词,但单词间顺序不变。单词之间用空格划分。
思路:每个单词分别反转;然后反转整个句子。
要点:切分每个单词。如果依照空格来划分单词的话,注意最后的单词,结尾是没有空格的。
利用STL可以比较容易实现。注意string类的函数,对下标的兼容性很好。

    string ReverseSentence(string str) {        if(str.empty())            return str;        int n=str.length();        int i=0;        while(1){            int j=str.find(" ",i);            if(j==-1){                reverse(str.begin()+i,str.end());                break;               }else{                reverse(str.begin()+i,str.begin()+j);                i=j+1;            }        }        reverse(str.begin(),str.end());        return str;    }

题目2:汇编语言中有一种移位指令叫做循环左移(ROL),现在有一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。
思路:前后两段分别反转;然后整个序列反转。(注意这个顺序不要反!)

    string LeftRotateString(string str, int n) {        if(str.empty()||n>str.length())            return str;        reverse(str.begin(),str.begin()+n);        reverse(str.begin()+n,str.end());        reverse(str.begin(),str.end());        return str;    }

面试题43:n个骰子的点数

求n个骰子扔在地上,点数总和为K的情况有多少种。每个骰子视为不同的。
非常不错的动态规划题。
先尝试用递归的思想,推导出状态转移方程:
F(n,K)=F(n-1,K-1)+F(n-2,K-2)+…F(n-6,K-6)
动态规划一般有若干个步骤n,推导的是F(n)和F(n-1)的关系。在第n步时,选择分支是有限的,选择分支直接影响目标结果。
假设当前有i个骰子,需要凑总和j。那么比较F(i)和F(i-1),所做的唯一改变,就是加入了第i个骰子。第i个骰子的点数可以是1到6,假设点数是3,那么只需i-1个骰子,去凑j-3就可以了。
那么:
F(i,j)=F(i-1,j-1)+F(i-1,j-2)…+F(i-1,j-6)
从i到i-1的状态转移,右边有若干个子项,分别是i步的选择分支。
程序实现的时候,一般是定义二维数组。一般来说,数组的第一行和第一列都要单独空出来(不一定都是0!)以便于后续迭代。

int func(int n,int k){    //注意:这类问题的数目往往会很大    //申请二维vector;注意书写形式,以及规模。vector自动初始化为0.    //如果是数组,要手动初始化。    vector<vector<long> > F(n+1,vector<long>(k+1));    //数组第0行:表示用0个骰子,分别凑[0 .. k]的情况    F(0,0)=1; //这里赋值F(0,0);别的都是0不用动。    //数组的第0列,表示用[0..n]个骰子,分别凑0的情况    //这里不用动。    for(int i=1;i<=n;++i){        for(int j=1;j<=k;++j){            //注意下面的写法:没有写h<=6,为什么?            for(int h=1;j-h>=0;++h)                F(i,j)+=F(i-1,j-h);        }    }}

总共的时间复杂度:O(n*k*6)=O(n*k)
想想有什么优化方法?
- 关于空间:每次只使用当前列和上一列,可以用两个一维数组迭代赋值即可。但会花费数组拷贝的时间。
- 关于时间:其实就是比较F(j)和F(j-1)的关系。每次求和6个数,每次的操作很像队列:进入一个数,弹出一个数。那么使用队列和变量sum,就可以在常数时间内求和了。不过本来就是6个数,就是常数,所以意义不大。
另外:数组迭代时,考虑边界条件,即下标的有效性。

扩展:凑钱问题
凑钱问题是经典的动态规划,这里不多表述。面额: A[1 2 5 10 20],去凑K。那么第i步,就是选取A[0..i]来凑m元。那么F(i)和F(i-1)的唯一区别,就是加入了A[i]面额的钞票。所以:

F(i,m)=F(i-1,m-A[i]*0)+F(i-1,m-A[i]*1)+F(i-1,m-A[i]*2)+...F(i-1,m-A[i]*x)until: m-A[i]*x<=0

右边的选择分支,表明A[i]面值使用了0次,1次,2次…注意此时选择分支是无固定总数的。这样的时间复杂度通常是O(n*k*x),而x=K/A[i],A[i]为常值,故最终为O(n*k^2)
这时候,看看有无优化方法。注意到:

F(i,m-A[i])=F(i-1,m-A[i])+F(i-1,m-A[i]*2)+...F(i,m)=F(i-1,m-A[i]*0)+F(i,m-A[i])

这样,就可以优化到O(n*k)了。
当然,这样写起来会大大加重代码量。因为上述的所有递归公式,都是在下标有效的基础上设计的。
例如:F(i,m)=F(i-1,m-A[i]*0)+F(i,m-A[i])前提是m-A[i]>=0,否则只能依次相加。
具体实现暂略,因为没有找到数据验证。

面试题44:扑克牌的顺子

五张点数连续的牌,就是顺子。大小王可以视作任意点数来凑顺子。
输入:五张牌的点数,其中王为0.
这种题目,就属于“思路理清,豁然开朗”的类型。
除了大小王之外的牌依次排序,看看中间有多少空隙,大小王分别可以填一个空隙,两个连在一起可以填两个空隙。所以一开始想,可能要分情况处理。
但是最简单的思路是:
大小王可以做任意牌来填空隙,比较空隙和大小王的数目即可。
关于空隙:A[i]-A[i-1]-1
例如[1 3]的空隙是1,[1 4]的空隙是2
特别注意 :如果两牌相等,则一定不是顺子。

    bool IsContinuous( vector<int> numbers ) {        if(numbers.size()!=5)            return false;        sort(numbers.begin(),numbers.end());        int cnt=0;        for(int i=0;i<numbers.size();++i)            if(numbers[i]>0){                cnt=i;                break;            }        int s=0;        for(int i=cnt;i<numbers.size()-1;++i){            if(numbers[i+1]==numbers[i])                return false;            s+=numbers[i+1]-numbers[i]-1;        }        if(cnt>=s)            return true;        else            return false;    }

面试题45:圆圈中最后剩下的数字(约瑟夫环)

经典的动态规划。重点是在N个人中的幸存者,和N-1个人中的幸存者,是什么关系?
我们看看规模缩减时,情况发生的变化。
N个人的问题:[0 1 2 3 .. N-1]中循环报数,求幸存者。
第k个人死了,游戏继续,此时问题就成了[ k+1 k+2 ..N-1 0 1 2 .. k-1] 这些人继续做游戏。
但是,N-1个人的问题,应该是[0 1 2 3 ..N-2]中循环报数求幸存,所以为了保证子问题的一致性,需要对[ k+1 k+2 ..N-1 0 1 2 .. k-1] ->[0 1 2 3 4 5 6 ..N-2]做映射。不过递归计算是和调用顺序相反的,需要求逆映射。
例如:
N=6
[0 1 2 3 4 5]
k=4
[4 5 0 1 2]->[0 1 2 3 4]
那么[0 1 2 3 4]->[4 5 0 1 2]
实际上是做后者到前者的映射,显然是(x+k)%n
所以说,如果知道F(n-1)中幸存者是x,那么在F(n)中,他的编号应该是(x+k)%n。

    int F(int n, int m)    {        if(n==1)            return 0;        return((F(n-1,m)+m)%n);    }

面试题46:求1+2+3…n

不许用乘除,循环(for,while),条件(if switch ?:)
不许用循环,只能用递归。但递归需要条件判断终止。所以要点是不用条件语句,写出判断来。
关键字:短路求值。

//递归形式1: //if(A) sum=a;//else  sum=b;     int Sum_Solution1(int n) {        int sum;        if(n==1)            sum=1;        else            sum=n+Sum_Solution1(n-1);        return sum;    }//递归形式2:   //sum=a;//if(!A) sum=b;     int Sum_Solution2(int n) {        int sum=1;        if(n!=1)            sum=n+Sum_Solution2(n-1);        return sum;    }//递归形式3:注意运算优先级//sum=a;//A||(sum=b)    int Sum_Solution3(int n) {        int sum=1;        n==1||(sum=n+Sum_Solution3(n-1));        return sum;    }//递归形式4:注意运算优先级//sum=a;//!A&&(sum=b)    int Sum_Solution(int n) {        int sum=1;        n!=1&&(sum=n+Sum_Solution(n-1));        return sum;    }

利用构造函数与静态成员:定义静态成员n和sum,每次构造对象,使得n++;sum+=n;最后使用数组的形式构造多个对象。

class A{    public:        static int n,sum;        A(){            n++;            sum+=n;        }    };int A::n=0;int A::sum=0;int main(){    int t=10;    A* p=new A[t];    delete[] p;    cout<<A::sum;    system("pause");}

在这里说一下静态成员:
1. 静态成员可以用逗号,来实现一个语句多个定义。
2. 静态成员本质上是全局变量,全局变量不归属类所有
3. 静态成员需要额外的声明以及初始化。注意,它需要声明,而且是在全局范围内声明。
4. 通常静态成员的声明在类的cpp文件内,它相当于全局范围(因为没有在任何函数内)。
5. 静态成员不归类所有,即使未创建对象,依然存在(从它在全局范围内的声明和初始化开始);即使对象全部析构,它也依然不会消失(完全等效于全局变量)。

利用虚函数求解:实际上用多态来代替条件语句

class A;A* Array[2];class A{    public:    virtual int Sum(int n){return 0;}};class B:public A{    virtual int Sum(int n){        return Array[!!n]->Sum(n-1)+n;    }};int main(){    int n=10;    A a;    B b;    Array[0]=&a;    Array[1]=&b;    return Array[1]->Sum(n);}

纯C环境下,利用函数指针,和上面一个道理。

typedef int (*fun)(int);int f(int n){return 0;}int ff(int n){    static fun f[2]={f,ff};    return n+f[!!n](n-1);}

还有拿模板类做的,感觉都很花哨。

面试题47:不用加减乘除做加法

其实就是拿位运算来模拟加减进位的问题。
比较笨的方法:就像用字符串实现加法一样,从右向左,一位一位,记录相加之后的值以及进位的情况;最后遍历完之后,处理最高的进位。
对于两个位bit1 ,bit2 来说:
相加之后的结果:bit1^bit2 (只有一个是0另一个是1相加才是1)
相加之后的进位:bit1&bit2(只有两个都是1才会进位)

让人惊叹的方法:
比如我们计算十进制加法:
136
298
从右向左,依次是:
6+8结果4进位1
3+9+1结果3进位1
1+2+1结果4进位0
如果我们把结果和进位分开呢?
单单计算结果:324
单单计算进位:011
注意进位是要乘10的,进位表示的真实数值是110
两者再相加:324+110=434
这种做法,是每次两数字相加时,把进位所代表的数,暗暗记在另一笔账上,最后再把账合起来。总之,是拆分后分别相加的做法。

136+298=100+30+6+200+90+8        =(100+200)+(30+90)+(6+8)        =(300)+(20+100)+(4+10)        =300+20+4+100+10        =324+110

用这种做法求出:136+298=324+110的优点在于,第二个数的末端变成了0;继续递归使用它,直至第二个数全0为止。
利用这种思路,不难写出二进制的加法:

    int Add(int num1, int num2){        unsigned int res,carry;        while(num2!=0){            res=num1^num2;            carry=num1&num2;            num1=res;            num2=carry<<1;        }        return num1;    }

扩展:不用额外空间交换两个数:

a=a+b;b=a-b;a=a-b;
a=a^b;b=a^b;a=a^b;

面试题48:不能被继承的类

容易想到,定义构造函数为私有,可以防止继承。因为即使是继承,也是站在“第三者”的角度对基类的public方法和成员进行调用。
如果定义构造为私有,只能通过类中其他的public方法,返回new生成的指针:

class A{    public:        static A* make(){return new A();}        static void del(A* p){delete p;}    private:        A(){}        ~A(){}    };

注意:因为无法预先创建A的实例,所以当然函数需要是static。
局限性:仅能在堆上创建实例。
新解法:虚继承
虚继承是在多重继承的场合下诞生的,其特点是,派生类的祖先们,对派生类是透明的,可以消除重复继承的问题。
简单理解,虚继承中,派生在创建对象时,会直接调用祖先的构造函数;
一般继承中,派生类调用其父辈,其父辈再分别调用它自己的父辈,来一层层调用来完成。

非虚继承     虚继承A  A         A|  |         /\B  C        B  C \/          \/ D           D 
template <typename T> class A{    friend T;    private:        A();        ~A();};class B:virtual public A<B>{    public:        B();        ~B();};

这样B可以自由创建对象,因为它是A的友元,可以调用A的构造;B不可以有派生,因为B的派生也会是虚继承,它会直接调用A的构造,致使失败。