问题01 N个骰子的点数

剑指offer读书笔记:第六章,面试中的各项个能力02_#include

基于递归求骰子点数,时间效率不够高。

  • 先把骰子分成两堆,第一堆只有一个,第二堆有n-1个,
  • 单独的那一个可能出现1到6的点数,我们需要计算从1-6的每一种点数和剩下的n-1个骰子来计算点数和。
  • 还是把n-1个那部分分成两堆,上一轮的单独骰子点数和这一轮的单独骰子点数相加,然后再和剩下的n-2个骰子来计算点数和。
  • 不难发现这是一种递归的思路。定义一个长度为6n-n+1的数组,和为s的点数出现的次数保存到数组的第s-n个元素里。

基于循环求骰子点数,时间性能好。其实就是DP动态规划做法

  • 用两个数组来存储骰子点数的每一种出现的次数。
  • 在一次循环中,第一个数组中的第n个数字表示骰子和为n出现的次数。
  • 在下一次循环中我们加上一个新的骰子,此时和为n的骰子出现的次数应该等于上一次循环中骰子点数和为n-1、n-2、n-3、n-4、n-5与n-6的次数的综合,所以我们把另一个数组的第n个数字设为前一个数组对应的第n-1、n-2、n-3、n-4、n-5与n-6之和。

这道题其实很简单,但是书上的代码跟屎一样,太难看了。

代码如下:

#include <iostream>
#include <vector>
#include <map>
#include <unordered_map>
#include <set>
#include <unordered_set>
#include <queue>
#include <stack>
#include <string>
#include <climits>
#include <algorithm>
#include <sstream>
#include <functional>
#include <bitset>
#include <numeric>
#include <cmath>
#include <regex>
#include <iomanip>
#include <cstdlib>
#include <ctime>

using namespace std;

void dfs(vector<int>& sum, int count, int oneSum)
{
    if (count == 0)
        sum[oneSum] += 1;
    else
    {
        for (int i = 1; i <= 6; i++)
            dfs(sum, count - 1, oneSum + i);
    }
}

/*
    递归算法就不多说了,直接DFS深度优先遍历即可
*/
void calaProbabilityByDFS(int count)
{
    if (count < 1)
        return;
    int maxSum = count * 6;
    int minSum = count * 1;
    vector<int> sum(maxSum + 1, 0);

    dfs(sum, count, 0);

    int total = pow(6,count);
    for (int i = minSum; i <= maxSum; i++)
    {
        double ratio = (double)sum[i] / total;
        cout << "Sum: " << i << " , Count: " << sum[i] << " , " << "Ratio: " << ratio << endl;
    }
}

/*
    剑指Offer说是基于循环,其实就是基于DP动态规划
*/
void calaProbabilityByDP(int count)
{
    if (count < 1)
        return;
    int maxSum = count * 6;
    int minSum = count * 1;
    vector<vector<int>> dp(2,vector<int>(maxSum+1,0));

    /*
        flag表示上一次计算的和的情况
        1-flag为当前要计算的情况,二者来回交替计算
    */
    int flag = 0;
    for (int i = 1; i <= 6; i++)
        dp[flag][i] = 1;
    for (int k = 2; k <= count; k++)
    {
        for (int i = 0; i <= k * 6; i++)
            dp[1 - flag][i] = 0;
        for (int i = 1 * k; i <= 6 * k; i++)
        {
            for (int j = 1; j <= i && j <= 6; j++)
            {
                dp[1 - flag][i] += dp[flag][i - j];
            }
        }
        flag = 1 - flag;
    }

    int total = pow(6, count);
    for (int i = minSum; i <= maxSum; i++)
    {
        double ratio = (double)dp[flag][i] / total;
        cout << "Sum: " << i << " , Count: " << dp[flag][i] << " , " << "Ratio: " << ratio << endl;
    }

}

int main()
{
    const int time = 6;
    calaProbabilityByDFS(time);
    calaProbabilityByDP(time);
    return 0;
}

问题02 扑克牌顺子—>2018网易面试过我

剑指offer读书笔记:第六章,面试中的各项个能力02_构造函数_02

接下来我们分析怎样判断 5 个数字是不是连续的,最直观的方法是把数组排序。值得注意的是,由于 0 可以当成任意数字,我们可以用 0 去补满数组中的空缺。如果排序之后的数组不是连续的,即相邻的两个数字相隔若干个数字,但只要我们有足够的。可以补满这两个数字的空缺,这个数组实际上还是连续的。举个例子,数组排序之后为{0,1,3,4,5}在 1 和 3 之间空缺了一个 2,刚好我们有一个 0,也就是我们可以把它当成 2 去填补这个空缺。

代码如下:

#include <iostream>
#include <vector>
#include <map>
#include <unordered_map>
#include <set>
#include <unordered_set>
#include <queue>
#include <stack>
#include <string>
#include <climits>
#include <algorithm>
#include <sstream>
#include <functional>
#include <bitset>
#include <numeric>
#include <cmath>
#include <regex>
#include <iomanip>
#include <cstdlib>
#include <ctime>

using namespace std;


class Solution 
{
public:
    bool IsContinuous(vector<int> n) 
    {
        if (n.size() <= 0)
            return false;
        sort(n.begin(), n.end());
        int numOfZreo = 0;
        for (int i = 0; i < n.size() && n[i] == 0; i++)
            numOfZreo++;

        int sumOfGap = 0;
        int left = numOfZreo, right = left + 1;
        while (right < n.size())
        {
            if (n[left] == n[right])
                return false;
            else
            {
                sumOfGap += (n[right] - n[left] - 1);
                left = right;
                right += 1;
            }
        }
        if (numOfZreo >= sumOfGap)
            return true;
        else
            return false;
    }
};

问题03 圆圈中最后剩下的数字

剑指offer读书笔记:第六章,面试中的各项个能力02_#include_03

方法有两个,最笨的方法就是直接模拟环的删除,这个很简单,其实这个是存在公式的可以直接推导去做。

代码如下:

#include <iostream>
#include <vector>
#include <map>
#include <unordered_map>
#include <set>
#include <unordered_set>
#include <queue>
#include <stack>
#include <string>
#include <climits>
#include <algorithm>
#include <sstream>
#include <functional>
#include <bitset>
#include <numeric>
#include <cmath>
#include <regex>
#include <iomanip>
#include <cstdlib>
#include <ctime>

using namespace std;


class Solution 
{
public:
    int LastRemaining_SolutionByDP(unsigned int n, unsigned int m)
    {
        if (n<1 || m<1)
            return -1;
        int last = 0;
        for (int i = 2; i <= n; i++)
            last = (last + m) % i;
        return last;
    }

    int LastRemaining_Solution(int n, int m)
    {
        if (n < 1 || m < 1)
            return -1;
        vector<int> all;
        for (int i = 0; i < n; i++)
            all.push_back(i);

        int count = 1;
        int i = 0;
        while (all.size() > 1)
        {
            if (count == m)
            {
                all.erase(all.begin() + i);
                count = 1;
            }
            else
            {
                i = (i + 1) % all.size();
                count += 1;
            }
        }
        return all[0];
    }
};

问题04 求1+2+…+n

剑指offer读书笔记:第六章,面试中的各项个能力02_#include_04

这道题考察的就是思维发散能力,看到了下面几种解决方法

基于构造函数

剑指offer读书笔记:第六章,面试中的各项个能力02_构造函数_05

问题05 不用加减乘除做加法

剑指offer读书笔记:第六章,面试中的各项个能力02_构造函数_06

十进制加法分三步:(以5+17=22为例)
1. 只做各位相加不进位,此时相加结果为12(个位数5和7相加不进位是2,十位数0和1相加结果是1);
2. 做进位,5+7中有进位,进位的值是10;
3. 将前面两个结果相加,12+10=22

这三步同样适用于二进制位运算
1.不考虑进位对每一位相加。0加0、1加1结果都是0,0加1、1加0结果都是1。这和异或运算一样;
2.考虑进位,0加0、0加1、1加0都不产生进位,只有1加1向前产生一个进位。可看成是先做位与运算,然后向左移动一位;
3.相加过程重复前两步,直到不产生进位为止。

代码如下:

#include <iostream>
using namespace std;

class Solution 
{
public:
    int Add(int num1, int num2)
    {
        do
        {
            int sum = num1^num2;
            int carry = (num1&num2) << 1;
            num1 = sum;
            num2 = carry;
        } while (num2 != 0);
        return num1;
    }
};

问题06 不能被继承的类

剑指offer读书笔记:第六章,面试中的各项个能力02_#include_07

需要注意的是C++11已经有了final关键字来禁止继承。

参考这个连接《剑指offer》:[48]不能被继承的类-单例模式

方法1:设置构造函数和析构函数为私有+新增两方法创建和销毁对象。

  1. 把构造函数和析构函数设置为私有函数,这样可以防止子类调用构造函数和析构函数,这样也就实现了防止继承;
  2. 新增两个方法来创建对象和销毁对象是为了不影响自己创建和销毁对象,不能为了限制别人把自己也坑了,这样就不划算了。所以我们采用了静态方法创建和销毁对象。
    缺点:会影响类对象的创建,并且只能创建堆上的对象,不能创建栈上的对象
    代码如下:
#include <iostream>
using namespace std;
class SealClass
{
public:
    static SealClass *GetInstance()//静态的全局访问接口
    {
        if(NULL==m_pInstance)
            m_pInstace = new SealClass();
        return m_pInstace;
    }
    static void DeleteInstance(SealClass *pInstance)
    {
        delete pInstance;
    }
    int GetNum()
    {
        return number;
    }
private:
    static SealClass *m_pInstance; //静态的私有实例化指针
    SealClass(){  }
    ~SealClass()   //其实析构函数可以不用写到这里;
    {
        cout<<"private: ~SealClass!"<<endl;
    }
};
class Driver:public SealClass
{
public:
    Driver(){   }
};
int main()
{
    //SealClass ss(11);//ERROR,不能访问私有的构造函数;
    //SealClass *s=new SealClass(11);//ERROR,不能访问私有的构造函数;
    SealClass *ss=SealClass::GetInstance(11); //OK,只能是堆上的对象;
    int result=ss->GetNum();
    cout<<"number: "<<result<<endl;
    SealClass::DeleteInstance(ss);
    //创建子类:
    //Driver d; //ERROR,不能访问构造和析构函数;
    //Driver *d=new Driver;//ERROR,不能访问构造和析构函数;
    system("pause");
    return 0;
}

方案二:思想:使用虚基类 和友元类处理(推荐,使用起来比较常规,堆和栈的对象都可以得到)。
方法:
- 虚基类+虚基类构造函数和析构函数私有化+不能派生的SealClass变成基类友员类
- 另外设置一个基类Base,并把基类Base的构造函数和析构函数设置为私有
- 把要设置SealClass类变成基类的友元类
- SealClass类虚拟继承Base。

原因:
- 把基类Base的构造函数和析构函数设置为私有化,主要目的也是为了防止让其他类从基类base中派生,当然此时该类也不能定义对象;
- 设置SealClass类为基类Base的友元类,主要目的是根据友员的性质,该类可以自由访问基类的私有的构造函数和析构函数;
- 基类的友员不能被派生类继承,此时之后创建的Driver就不能使用父类的友元了。

为什么是虚函数?
主要原因:如果是虚继承的话,假设类Driver要从SealClass继承,由于是虚继承,该派生类Driver会直接调用虚基类Base的构造函数,而不会通过基类SealClass,此时就会报错。

一定要设置为虚继承:如果不是虚继承的话,类Driver的对象会调用父类的构造函数,而且父类SealClass可以调用Base的构造函数和析构函数,则此时是可以有类从SealClass继承的。

虚继承还有一个优点:就是解决多继承的二义性问题,如果B和C都继承A,D继承B和C,如果D要用A的一个变量,就会出现二义性,是DBA还是DCA呢:
如果是虚继承,调用的时候就会越过BC,直接调用A的数据,解决了这种二义性的问题。

缺点:不易扩展,主要是因为编译器对虚基类和友元类friend类型的支持和要求不一样。

要参考这个连接虚基类调用顺序 + 不能继承的类

代码如下:

#include <iostream>
using namespace std;
class Base
{
public:
    friend class SealClass;
private:
    Base()
    {
        cout<<"private: Base!"<<endl;
    }
    ~Base()
    {
        cout<<"private: ~Base!"<<endl;
    }
};
class SealClass:virtual public Base
{
public:
    SealClass()
    {
        cout<<"SealClass!"<<endl;
    }
    ~SealClass()
    {
        cout<<"~SealClass!"<<endl;
    }
};
//class Driver:public SealClass
//{
//public:
//  Driver()
//  {
//      cout<<"Driver!"<<endl;
//  }
//  ~Driver()
//  {
//      cout<<"~Driver!"<<endl;
//  }
//};
int main()
{
    //Driver d;//ERROR,不能访问私有的构造和析构:
    //Driver *d=new Driver;//ERROR,不能访问私有的构造和析构:
    SealClass s; //OK;
    SealClass *ss=new SealClass; //OK
    delete ss;
    system("pause");
    return 0;
}