剑指offer相关代码整理5-6

第五部分 优化时间和空间效率

29 数组中出现次数超过一半的数字

这个题的关键在于,掌握的基本技巧之后,还要注意程序末尾有一个检验函数——也就是说存在那种可能性,最后存储的数字实际上并没有达到数量超过数组长度的一般,因此需要进一步检验一下。容易忘记。

int MoreThanHalfNum_Solution(vector<int> numbers) {
    if (numbers.size() == 0)
        return 0;
    int num = numbers[0], cnt = 0;
    int ind = 0;
    while (ind < numbers.size()) {
        if (cnt == 0) {
            num = numbers[ind];
            cnt++;
        }
        else {
            if (numbers[ind] == num) {
                cnt++;
            }
            else {
                cnt--;
            }
        }
        ind++;
    }
    //注意,还需要再次检验存储的num是否是符合要求的数字
    cnt = 0;
    for (int i = 0; i < numbers.size(); i++) {
        if (numbers[i] == num)
            cnt++;
    }
    if (cnt > (double)numbers.size() / 2)
        return num;
    else
        return 0;
}

另外还有一种基于partition函数的解法。

 

30 最小的k个数

我所用的最简单的方法就是投机取巧直接用set容器对数据重新进行存储。实际上,用multiset更加合理一些,因为数组中还是有可能有重复数据的。

vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
    vector<int> res;
    if (input.size() == 0 || k > input.size() || k <= 0)
        return res;
    set<int> store;
    for (int i = 0; i < input.size(); i++) {
        store.insert(input[i]);
    }
    int i = 0;
    for (auto it = store.begin(); i < k;it++, i++) {
        res.push_back(*it);
    }
    return res;
}

更合理的程序代码:维护一个k个元素的大根堆。注意是大根堆!用大根堆的原因是,用小根堆,那就还是得从堆尾来取元素,比较元素,以及删除元素。性能显然受损。而大根堆所有的比较读取删除都在堆头。这种方法在处理从海量数据中找k小数的问题中很有优势。其中用到的容器是multiset。

vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
    if(input.size()<k || k<=0)
        return vector<int>();

    priority_queue<int> pq;
    for(int i = 0; i <k; i++){
        pq.push(input[i]);
    }
    for(int i = k; i<input.size(); i++){
        if(pq.top()>input[i]){
            pq.pop();
            pq.push(input[i]);
        }
    }
    vector<int> res(k,0);
    for(int i = 0; i<k; i++){
        res[k - i - 1] = pq.top();
        pq.pop();
    }
    return res;
}

用partition处理最小k问题时间复杂度只有O(n),只是需要对数组进行修改。

两种算法各有优缺点,各自适用于不同的场合。因此应当在做题前了解清楚题目的要求——输入数据量有多大?能否一次性载入内存?是否允许交换输入数据中数字的顺序等等。

以上两个问题都可以用partition来解决。其实感觉用partition并不会更加直观。

 

31 连续子数组的最大和

难得一次性写对了。思路很清晰,但是如果钻牛角尖就容易怎么都写不对。关键步骤在于下面的,一旦当前临时sum<0,那么就别想其他的,把它丢了吧。因为无论当前的array[i]是大于0还是小于0,都是不管前面的小于0的sum,才能有可能获得和更大的值。

int FindGreatestSumOfSubArray(vector<int> array) {
    if (array.size() == 0)
        return 0;
    int res = 0x8fffffff;
    int sum = 0;
    for (int i = 0; i < array.size(); i++) {
        if (sum <= 0) {
            sum = array[i];
        }
        else {
            sum += array[i];
        }
        if (sum > res)
            res = sum;
    }
    return res;
}

32 整数中1出现的次数

实际上这道题目可以进行拓展——统计从1到n的数值中,0-9这10个数字出现的次数。

毫无疑问不能用暴力法。

现在约莫已经能独立写出这个代码了。思路是,非零的情况下,有分三种情况。下面的代码思路很清晰。其中,data/10指代当前位的前面所有位组合的数字,n%rec指代当前位的后面的所有位组成的数字。(源码书上的代码写得太复杂啦)

int cul1_9(int n, int i) {
    int data = n, rec = 1;
    int cnt = 0;
    while (data) {
        int tmp = data % 10;
        if (i < tmp) {
            cnt += (data / 10 + 1)*rec;
        }
        if (i == tmp) {
            cnt += data / 10 * rec + n%rec + 1;
        }
        if (i > tmp) {
            cnt += data / 10 * rec;
        }
        rec *= 10;
        data /= 10;
    }
    return cnt;
}

int cul0(int n) {
    int data = n, rec = 1;
    int cnt = 0;
    while (data) {
        int tmp = data % 10;
        if (0 < tmp) {
            cnt += data / 10*rec;
        }
        if (0 == tmp) {
            cnt += (data / 10 - 1) * rec + n%rec + 1;
        }
        rec *= 10;
        data /= 10;
    }
    return cnt;
}

33 把数组排成最小的数

这个问题的关键在于这里的比较函数。还是如此,有好的想法,就是使写代码事半功倍。这里用的比较就是看num1num2比较大还是num2num1比较大。其他的思路不仅麻烦还会出错。

bool compare(int num1, int num2) {
    int rec1 = 1, rec2 = 1, tmpnum1 = num1, tmpnum2 = num2;
    while (tmpnum1) {
        rec1 *= 10;
        tmpnum1 /= 10;
    }
    while (tmpnum2) {
        rec2 *= 10;
        tmpnum2 /= 10;
    }
    if (num1*rec2 + num2 < num2*rec1 + num1)
        return true;
    else
        return false;
}

string PrintMinNumber(vector<int> numbers) {
    sort(numbers.begin(), numbers.end(), compare);
    string res;
    for (auto it = numbers.begin(); it != numbers.end(); it++) {
        stringstream ss;
        ss << *it;
        string tmp;
        ss >> tmp;
        res += tmp;
    }
    return res;
}

 

34 丑数

    当然,这是用的空间换时间的方法。思路清晰,代码却也不是特别好写。

这题的关键在于如何遍历以及存储丑数。遍历的时候,关键是设置三个指针,指代2,3,5各个数字遍历到的位置;存储的时候,关键是要注意重复数字的情形(用set容器行不通,可以通过选出最小数之后,将这个最小数与所有的三个指针指向的数进行比较可得)

int minNum(int num1, int num2, int num3) {
    int tmp = min(num1, num2);
    return min(tmp, num3);
}
int GetUglyNumber_Solution(int index) {
    //需要考虑是否会出现重复数字的情形
    //代码的难点也在于此。
    vector<int> vec;
    vec.push_back(1);
    int ind2 = 0, ind3 = 0, ind5 = 0;
    for (int i = 1; i < index; i++) {
        int tmpnum2 = vec[ind2] * 2, tmpnum3 = vec[ind3] * 3, tmpnum5 = vec[ind5] * 5;
        int minnum = minNum(tmpnum2, tmpnum3, tmpnum5);
        vec.push_back(minnum);
        if (tmpnum2 == minnum)
            ind2++;
        if (tmpnum3 == minnum)
            ind3++;
        if (tmpnum5 == minnum)
            ind5++;
    }
    return vec[index - 1];
}

35 第一个只出现一次的字符

36 数组中的逆序对

思路是利用归并排序,计数的时候也要注意。

/*36 数组中的逆序对
进行归并排序计数
*/
long long cnt;
void merge(vector<int>& data, int begin, int end, int mid) {
    if (begin < end) {
        vector<int> tmpres;
        int ind1 = begin, ind2 = mid + 1;
        while (ind1 <= mid&&ind2 <= end) {
            if (data[ind1] <= data[ind2])
                tmpres.push_back(data[ind1++]);
            else {
                tmpres.push_back(data[ind2++]);
                cnt += mid - ind1 + 1;
            }
        }
        while (ind1 <= mid)
            tmpres.push_back(data[ind1++]);
        while (ind2 <= end)
            tmpres.push_back(data[ind2++]);
        for (int i = begin; i <= end;i++)
            data[i] = tmpres[i - begin];
    }
}

void mergesort(vector<int>& data, int begin, int end) {
    if (begin < end) {
        int mid = begin + (end - begin) / 2;
        mergesort(data, begin, mid);
        mergesort(data, mid + 1, end);
        merge(data, begin, end, mid);
    }
}

int InversePairs(vector<int> data) {
    if (data.size() == 0)
        return 0;
    cnt = 0;
    mergesort(data, 0, data.size() - 1);
    return cnt % 1000000007;
}

37 两个链表的第一个公共结点

 

第六部分 面试中的各项能力

一些注意点:当一个题目要求不明确的时候,注意交流与沟通。例如求树种的两个结点的最低公共祖先。(1)如果是排序的树——比较简单;(2)如果是有指向父结点指针的树——转化为求两个链表的第一个公共结点的解法;(3)如果只是一个普通的树——可以在遍历的时候用一个栈来保存从根结点到当前结点的路径,最终将它转化为求两个路径的最后一个公共结点。

38 数字在排序数组中出现的次数

39 二叉树的深度

判断平衡二叉树——其实也简单的,只是要注意记录树高度。

    另外,如果不用递归的方法,实际上就是要用非递归的后序遍历法来解题。。感觉又忘记后序遍历非递归算法了。

bool funcIBS(TreeNode* pRoot, int& height) {
    if (pRoot == NULL) {
        height = 0;
        return true;
    }
    int height1 = 0, height2 = 0;
    bool b1 = funcIBS(pRoot->left, height1);
    bool b2 = funcIBS(pRoot->right, height2);
    if (b1&&b2&&abs(height1 - height2) <= 1) {
        height = max(height1, height2) + 1;
        return true;
    }
    return false;
}

bool IsBalanced_Solution(TreeNode* pRoot) {
    int height = 0;
    return funcIBS(pRoot, height);
}

40 数组中只出现一次的数字

知道方法之后会很简单

/*40 数组中只出现一次的数字
*/
void FindNumsAppearOnce(vector<int> data, int* num1, int *num2) {
    if (data.size() < 2)
        return;
    int rec = 0;
    for (int i = 0; i < data.size(); i++) {
        rec = rec^data[i];
    }
    if (rec == 0)
        return;
    int num = 1;
    while ((rec&num) == 0) {
        num = num << 1;
    }
    *num1 = 0, *num2 = 0;
    for (int i = 0; i < data.size(); i++) {
        if (num&data[i]) {
            *num1 = *num1^data[i];
        }
        else
            *num2 = *num2^data[i];
    }
}

41 和为s的两个数字VS和为s的连续正数序列

1) 和为s的两个数字

这里,实际上关乎了一种寻找两数和的思想。这种思想可以推广至求三数和甚至四数和。方法也简单,也就是设置两个指针,一前一后,依据两数和与sum之间的关系进行变动,最终得到结果。另外,这里找到的是乘积最小的那个。

vector<int> FindNumbersWithSum(vector<int> array, int sum) {
    int begin = 0; int end = array.size() - 1;
    vector<int> res;
    while (begin < end) {
        if (array[begin] + array[end] == sum) {
            res.push_back(array[begin]);
            res.push_back(array[end]);
            break;
        }
        else if (array[begin] + array[end] < sum) {
            begin++;
        }
        else
            end--;
    }
    return res;
}

2) 和为s的连续正整数

/*41 和为S的连续正数序列
*/
vector<vector<int> > FindContinuousSequence(int sum) {
    vector<vector<int>> res;
    if (sum <= 0)
        return res;
    int cntsum = 1;
    int begin = 1;
    int end = 1;
    //如果是,end <= sum / 2 + 1, 那么会死循环了。。最害怕这样的条件判断。
    while (end <= (sum + 1) / 2) {
        while (cntsum < sum&&end <= sum/2) {
            end++;
            cntsum += end;
        }
        while (cntsum > sum&&begin <= end) {
            cntsum -= begin;
            begin++;
        }
        if (cntsum == sum) {
            vector<int> tmp;
            for (int i = begin; i <= end; i++) {
                tmp.push_back(i);
            }
            if (tmp.size() > 1)
                res.push_back(tmp);
            end++;
            cntsum += end;
        }
    }
    return res;
}

42 翻转单词顺序VS左旋转字符串

 

抽象建模相关。抽象建模相关的题目实际上会更麻烦一点儿。

43 n个骰子的点数

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

第一种方法:朴素的模拟过程法

/*43 n个骰子的点数
*/
void funcCPBB(const int n, int ind, int sum, vector<int>& res) {
    if (ind > n) {
        res[sum- n]++;
        return;
    }
    for (int i = 1; i <= 6;i++) {
        sum += i;
        funcCPBB(n, ind + 1, sum, res);
        sum -= i;
    }
}
void culProbability(int n) {
    vector<int> res(6*n-n+1,0);
    int sum = 0;
    int cnt = 1;
    for (int i = 0; i < n; i++)
        cnt *= 6;
    funcCPBB(n, 1, sum, res);
    for (int i = 0; i < 6 * n - n + 1; i++) {
        cout << "The Prob of " << i + n << " is " << (double)res[i] / cnt << endl;
    }
}

上一种方法效率较低。

第二种方法:运用技巧,通过模拟再次放上一个骰子的过程——(还是不太容易的)

用一个数组下标表示当前可以模拟出的数字,数组中存放的是当前这个数字将会出现的次数。——模拟过程:用两个数组来存储骰子点数的每一个总数出现的次数。在下一次循环中,第一个数组中的第n个数字表示骰子和为n出现的次数。在下一次循环中,加上一个新的骰子,此时和为n的骰子出现的次数,应该等于上一次循环中骰子点数和为n-1,n-2,n-3,n-4,n-5,n-6的次数的总和。重复该过程,则可以模拟出最终结果。

这个方法还是很好用的。其实写起来,需要的是细心。

另外,实际上不要默认骰子的点数为6,更好~

void culProbability2(int n) {
    vector<int> res = { 1,1,1,1,1,1 };
    int ind = 2; //表示开始将第二个骰子加进去
    while (ind <= n) {
        for (int i = 0; i < 6; i++)
            res.push_back(0); //为了下面代码的方便。
        vector<int> tmpvec(ind*6,0);
        for (int i = ind; i <= ind * 6; i++) {
            int sum = 0;
            for (int j = i - 2; j >= i - 7 && j >= 0; j--) {
                sum += res[j];
            }
            tmpvec[i - 1] = sum;
        }
        res = tmpvec;
        ind++;
    }
    int cnt = 1;
    for (int i = 0; i < n; i++)
        cnt *= 6;
    for (int i = n; i <= 6 * n; i++) {
        cout << "The Prob of " << i << " is " << (double)res[i - 1] / cnt << endl;
    }
}

44 扑克牌的顺子

方法非常白,也没有什么巧妙的技巧。

 

45 圆圈中最后剩下的数字

经典的解法时间复杂度为O(mn)。而用创新的解法则需要去推算公式,这个过程并不容易。

小朋友的编号为0到n-1,不停地删除第m个人。定义一个关于m和n的函数f(n,m),表示每次从n个人中每次删除第m个人后最后剩下的那个序号。我们的目的是从中寻找递归关系。

第一步:从当前0->n-1的圈中删除第m个数,删除后的序列变为0->k-1,k+1->n-1。

第二步:现在是要从删除一个元素之后的序列中继续删除,不再能够依然执行先前的函数f(n,m)。因此重新定义一个函数g(n-1,m),表示从当前这个少了一个数的序列中每次删除第m个数最后剩下一个人的操作。

第三步:为了找出f(n,m)与g(n,m)之间的关系,先观察一个新的映射

k+1 – 0

          k+2 – 1

          ……

          n-1 – n-k-2

0  -- n-k-1

1  – n-k

……

k-1 – n-2

可以从中找到映射规律为p(x)=(x-k-1)%n。逆映射p-1(x)=(x+k+1)%n。因此,g(n-1,m)=p-1(f(n-1,m))。

第四步:于是有:f(n,m)=g(n-1,m)= p-1(f(n-1,m))=(f(n-1,m)+k+1)%n。

k = (m-1)%n,因此有f(n,m)=(f(n-1,m)+m)%n。

另外的关键点在于当最终只有一个人时,f(1,m)=0,因为就是将编号为0的人删去了。现在需要知道的是这个编号为0的人的原始编号是多少?也就是求f(n,m)的值?

这就是需要寻找的递归关系,根据这个递归关系可以得到O(n)时间复杂度求解该问题的比较好的方法。

int LastRemaining_Solution(int n, int m)
{
    if (n <= 0 || m <= 0)
        return -1;
    int last = 0;
    //i代表的是当前序列中一共有多少个元素
    for (int i = 2; i <= n; i++) {
        last = (last + m) % i;
    }
    return last;
}

发散思维能力。我的发散思维能力很差,简单来说就是不够聪明。

46 求1+2+…+n

题目描述:求1+。。。+n,要求不能用乘除,以及for while if else switch case等关键字和条件判断语句。

解法一、利用构造函数——关键在于,利用创建对象数组时需要所有的这些对象调用构造函数。注意,由于设置了两个静态的变量,这两个静态变量在每一次重新运算时都应该要进行重置。

class DataAdd1 {
public:
    DataAdd1() {
        N++;
        sum += N;
    }
    static int result() {
        return sum;
    }
    static void reset() { //需要格外注意这一个步骤
        N = 0;
        sum = 0;
    }
private:
    static int N;
    static int sum;
};
int DataAdd1::N = 0;
int DataAdd1::sum = 0;
int funcSum1(int n) {
    DataAdd1::reset();
    DataAdd1* dataArray = new DataAdd1[n];
    return DataAdd1::result();
}
int main() {
    int res = funcSum1(100);
    return 0;
}

解法二、利用函数指针

关键思想是递归方法的运用。这个方法相比较于用虚函数,会更加直观。稍微注意下!!n表达技巧。也就是实现——当n不为0时,调用函数funcSum2_Solu,而当n等于0时,调用函数funcSum2_terminate终结函数调用。

/*第二种方法——利用函数指针
关键思路在于递归方法的运用
*/
unsigned int funcSum2_terminate(unsigned int n) {
    return 0;
}
unsigned int funcSum2_Solu(unsigned int n) {
    return n + (!!n ? funcSum2_Solu(n - 1) : funcSum2_terminate(n - 1));
}
int main() {
    int res = funcSum2_Solu(100);
    return 0;
}

解法三、利用虚函数

利用虚函数的实现方法的思想类似于以上利用函数指针的思想,但是实现起来更加复杂一些,毕竟涉及到了虚函数,多态。

/*第三种方法——利用虚函数
*/
class A;
A* Array[2];
class A {
public:
    virtual unsigned int funcSum(unsigned int n) {
        return n + Array[!!n]->funcSum(n - 1);
    }
};
class B :public A {
public:
    virtual unsigned int funcSum(unsigned int n) {
        return 0;
    }
};
unsigned int funcSum3(unsigned int n) {
    A classa;
    B classb;
    Array[0] = &classb;
    Array[1] = &classa;
    return Array[1]->funcSum(n);
}
int main() {
    int res = funcSum3(100);
    return 0;
}

解法四、利用模板类型

这里涉及到了template元编程。真的还是挺难理解的。

/*第四种方法——template元编程
*/
template<unsigned int n>
struct FuncSum4 {
    enum { N = FuncSum4<n - 1>::N + n };
};
template<>
struct FuncSum4<1> {
    enum { N = 1 };
};
int main() {
    int res = FuncSum4<100>::N;
    return 0;
}


47 不用加减乘除做加法

老方法。这方法还是比较强大的,也不需要去考虑符号问题。

int Add(int num1, int num2)
{
    if (num2 == 0)
        return num1;
    int newNum = num1 ^ num2;
    int anoNum = (num1&&num2) << 1;
    return Add(newNum, anoNum);
}

拓展:交换两个变量a和b的值

a=a^b

b=a^b

a=a^b

48 不能被继承的类

第一种方法:常规解法,把构造函数设为私有函数。

由于构造函数和析构函数都是私有函数,如何才能得到类的实例呢?——通过定义共有的静态函数来创建和释放类的实例。

注意,这里必须使用静态方法,因为如果是普通的类成员函数,没有对象的情况下是不能调用的。唯有静态方法可以通过类名来进行调用。于是可行。

/*48 不能被继承的类
*/
/*第一种方法,把构造函数设为私有函数
*/
class SealedClass1 {
public:
    static SealedClass1* getInstance() {
        return new SealedClass1();
    }
    static void deleteInstance(SealedClass1* pInstance) {
        delete pInstance;
    }
private:
    SealedClass1(){}
    ~SealedClass1(){}
};

第二种方法:利用虚拟继承。

这方法说实话真的很复杂,复杂在原理上。主要原因是自己对模板,以及虚拟继承,友元这一套东西还是不够熟练。

主要优点是,该类型使用起来与普通的类型没有区别,可以在栈上,也可以在堆上创建实例。用到的主要原理有:模板实现方法的一般化,友元函数的不可继承性——正好利用之来实现不能继承,以及虚继承的原理。

注意一下:虚继承之后的类,其子类继承上一层基类的方式是跳过子类继承而来的类而直接调用基类的构造函数。从A类直接虚拟派生和间接派生的类中,其构造函数的初始化列表中都要列出对虚基类A构造函数的调用,这种机制保证了不管有多少层继承,虚基类的构造函数必须且只能被调用一次。若在初始化列表中没有显式调用虚基类的构造函数,则将调用虚基类的默认构造函数,若虚基类没有定义默认构造函数,则编译出错。

template<typename T>
class MakeSealed {
public:
    friend T;
private:
    MakeSealed() {}
    ~MakeSealed(){}
};
class SealedClass2: virtual public MakeSealed<SealedClass2>{
};

 

posted on 2017-07-18 16:28  sjqiu  阅读(223)  评论(0)    收藏  举报

导航