【数据结构与算法】《剑指offer》学习笔记----第五章 优化时间和空间效率(含39-52题)

第五章 优化时间和空间效率

本章核心内容和思想,就是降低时间复杂度
(1)改用更加高效的算法。如39、40、42题
(2)用空间换时间。用数组实现简单哈希表在O(1)时间内知道任意字符出现的次数;创建一个缓存保存中间的计算结果避免重复计算;用递归思路求解问题的时候,如果有重复的子问题,也可以通过保存求解子问题的结果来避免重复计算。相关例题如10、49、50题
(3)嵌入式开发,不宜过度牺牲空间换时间

以下是具体内容

面试题39. 数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
 

限制:
1 <= 数组长度 <= 50000
class Solution {
public:
    int majorityElement(vector<int>& nums) {
        //方法1:排序后中间的元素一定是出现超过一半的数字
        // sort(nums.begin(),nums.end());
        // return nums[nums.size()/2];

        //方法2:哈希表
        unordered_map<int,int> mp;
        for(auto it : nums){
            mp[it]++;
            if(mp[it] > nums.size()/2){
                return it;
            }
        }
        return 0;

        //方法3:超过一半的数字比其他所有数字的总和次数多
        // int n=1;
        // int result = nums[0];
        // for(int i=1;i<nums.size();i++){
        //     if(n==0){
        //         result = nums[i];
        //         n=1;
        //     }else if(result == nums[i]){
        //         n++;
        //     }else{
        //         n--;
        //     }
        // }
        // return result;

    }
};

面试题40. 最小的k个数

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]


示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
 

限制:
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000

注释都在代码里了,每一行都充满了思考:

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        vector<int> res;//用于保存最终要返回的结果
        multiset<int,greater<int>> leastNumbers;//定义一个multiset(底层是红黑树),能在log(n)查找、删除、插入元素;元素按从大到小排列
        leastNumbers.clear();//清空内容,便于后续使用,保证用的是一个空的容器
        if(k<1 || arr.size()<k){//如果找前0个数据,毫无意义;如果待查数组长度比k小,根本达不到前k个数据,只能达到数组长度个数据,因此也毫无意义。这两种情况直接返回空数组。
            return {};
        }
        vector<int>::const_iterator iter = arr.begin();//定义遍历用的迭代器
        for(;iter!=arr.end();++iter){//遍历
            if(leastNumbers.size()<k){//没装够k个的时候,请放心大胆使劲装,不用管数值大小
                leastNumbers.insert(*iter);
            }else{//装够了k个时,要进行下面的判断,决定是否要装入新的数据
                auto iterGreatest = leastNumbers.begin();//仅仅是个取当前容器中最大值的迭代器
                if(*iter < *iterGreatest){//如果要插入的数据 小于 此时容器中的最大值,那肯定要取而代之了,成为前k小的数插入红黑树容器multiset中
                    leastNumbers.erase(iterGreatest);//辞旧
                    leastNumbers.insert(*iter);//迎新
                }
            }
        }
        //这里用一个for循环,将multiset中的元素,搬运到要返回的res数组中
        for(auto iter=leastNumbers.begin();iter!=leastNumbers.end();++iter){
            res.push_back(*iter);
        }

        return res;
    }
};

面试题41. 数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。


示例 1:
输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]


示例 2:
输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]
 

限制:
最多会对 addNum、findMedia进行 50000 次调用。

思路:

将左边部分构造成一个大顶堆,右边部分构造成一个小顶堆。

这样,来一个数据,只要总元素个数为偶数,都把数据放到小顶堆;只要元素个数为奇数,都把数据放到大顶堆。

为了解决元素总个数为偶数时,来的新元素num比小顶堆的最小元素还要小,这种情况下,这个元素num其实应该放到左边的大顶堆,然后从大顶堆中取出最大值放到小顶堆中;

同理,为了解决元素总个数为奇数时,来的新元素num比大顶堆的最大元素还要大的尴尬情况,先把这个num放入右边的小顶堆,然后从小顶堆取出最小元素放到大顶堆。

注意:小顶堆不“小”,小顶堆中的所有元素都应该大于大顶堆的所有元素。

class MedianFinder {
    priority_queue<int> bigHeap;    // 大顶堆,首元素是堆中最大元素
    priority_queue<int, vector<int>, greater<int>> smallHeap;   // 小顶堆,首元素最小

public:
    // Adds a number into the data structure.
    void addNum(int num)
    {
        if(((bigHeap.size()+smallHeap.size())&0x1) == 0){//总元素个数是偶数,插入最小堆
            if(bigHeap.size()>0 && num<bigHeap.top()){//最大堆不为空&&num<最大堆堆首,说明num应该出现在左半边,自然应该先插入最大堆
                bigHeap.push(num);//num先插入最大堆
                num = bigHeap.top();//然后取了最大堆堆首赋给num,后续将num插入最小堆
                bigHeap.pop();//弹出最大堆堆首
            }
            smallHeap.push(num);//如果最大堆为空或num>最大堆堆首
        }else{//总元素个数是奇数,插入最大堆
            if(smallHeap.size()>0 && num>smallHeap.top()){
                smallHeap.push(num);
                num = smallHeap.top();
                smallHeap.pop();
            }
            bigHeap.push(num);
        }   
    }

    // Returns the median of current data stream
    double findMedian()
    {
        int size = smallHeap.size()+bigHeap.size();
       // if(size==0) throw exception("No number are availabel");

        double median=0.0;

        if((size&0x1)==1){//元素个数为奇数时,此时如果再来数据应加入最大堆,说明此时最小堆元素数量多于最大堆
            median = smallHeap.top();//中位数就是最小堆的堆顶元素
        }else{//元素个数为偶数时,两个堆元素数量相同,中位数为两堆堆顶元素平均值
            median = (smallHeap.top()+bigHeap.top())*0.5;//此处绝对不能用>>1,否则得到的都是整数,而不是分数
        }
        return median;
    }
};

面试题42. 连续子数组的最大和

输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
 

提示:
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100

代码:

class Solution {
public:
    bool g_InvalidInput = false;
    int maxSubArray(vector<int>& nums) {
        if(nums.size()<=0){
            g_InvalidInput = true;
            return 0;
        }
        int nCurSum = 0;//当前和
        int nGreatestSum = 0x80000000;//当前的最大和
        for(int i=0;i<nums.size();++i){//遍历累加子数组和

            if(nCurSum<=0){//如果当前和<=0,说明前面加来加去白加了
                nCurSum = nums[i];//清空前面的结果
            }else{//如果当前和>0,说明前面加的结果还是很有效的,下面继续累加
                nCurSum += nums[i];
            }

            if(nCurSum>nGreatestSum){//如果加上这个nums[i]后,当前的和超过了当前最大和
                nGreatestSum = nCurSum;
            }
        }
        return nGreatestSum;
    }
};

面试题43. 1~n整数中1出现的次数

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

示例 1:
输入:n = 12
输出:5


示例 2:
输入:n = 13
输出:6
 

限制:
1 <= n < 2^31

思路:

统计某个位置上 1出现的次数。如34,1在十位上出现的次数是10次(10到19),1在个位上出现的次数是4次(1,11,21,31),因此34中1出现了14次。

对于整数n,将这个整数分为三部分:当前位数字cur,更高位数字high,更低位数字low,如:对于n=21034,当位数是十位时,cur=3,high=210,low=4。

我们从个位到最高位 依次计算每个位置出现1的次数:

在计算时,会出现三种情况:

1)当前位的数字等于0时,例如 n = 21034,在百位上的数字cur = 0,百位上是1的情况有:00100-00199,01100-01199,……,20100-20199。一共有21*100种情况,即high*100

2)当前位的数字等于1时,例如n = 21034,在千位上的数字cur = 1,千位上是1的情况有:01000-01999,11000-11999,21000-21034。一共有2*1000+(34+1)种情况,即high*1000+(low+1)

3)当前位的数字大于1时,例如n = 21034,在十位上的数字cur = 3,十位上是1的情况有:00010-00019,……,21010-21019。一共有(210+1)*10种情况,即(high+1)*10


class Solution {
public:
    int countDigitOne(int n) {
       int count = 0;
       long i = 1;//指向遍历的位数,如i=1即个位,i=10即十位,...,因为n可以有31位,所以10^31用long存储
        while(n/i !=0){          //n/i 控制遍历的次数,将所有的位数都遍历完毕
            long high = n/(10*i);//将当前位之前的所有高位都存在high中
            long cur = (n/i)%10;//将当前位记录在cur中,即每次都需要统计当前位上1出现的次数
            long low = n-(n/i)*i;//低位数字就是n减去当前数字在内的全部数
            if(cur == 0){
                count += high * i;
            } else if(cur == 1){
                count += high * i + low + 1;
            } else {
                count += high * i + i;
            }
            i = i * 10;//准备遍历下一位
       }
       return count;
    }
};

面试题44. 数字序列中某一位的数字

数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。

请写一个函数,求任意第n位对应的数字。

示例 1:
输入:n = 3
输出:3


示例 2:
输入:n = 11
输出:0
 

限制:
0 <= n < 2^31
class Solution {
public:
    int findNthDigit(int n) {

        if(n<=0){//如果是空数组,不鸟他
            return -1;
        }

        int digits = 1;//digits负责保存1,2,3,4,... 用来表示 位数
        while(true){
            long numbers = countOfIntegers(digits);//该函数得到digits位的数字总共有多少个,输入2,返回两位数的个数90,输入3返回3位数的个数900
            if(n < numbers * digits){//总共numbers个数字,其中每个数字有digits位,总欧诺个就是两者乘积个位数,看n有没有被含在内
                return digitAtN(n,digits);//如果n就在digits位的numbers个数字拆开的numbers*digits个位数以内,找到它
            }
            n -= digits * numbers;//如果n就在digits位的numbers个数字拆开的numbers*digits个位数以外,去掉这些累赘
            ++digits;//增大位数,继续找
        }
        return -1;
    }

    long countOfIntegers(int digits){//该函数得到digits位的数字总共有多少个,输入2,返回两位数的个数90,输入3返回3位数的个数900
        if(digits==1){
            return 10;
        }
        long count = (int)pow(10,digits-1);
        return 9*count;
    }

    int digitAtN(int n,int digits){//当单个数字有digits位的时候,第n位是哪个数字。如:对于所有的5位数,第32位是什么数字
        int numbers = beginNumber(digits) + n / digits;//第n位就位于数字numbers中
        int nFromRight = digits - n % digits;//有一说一,这里我还不会,实在难题体会为何这里要来这么一招
        for(int i=1;i<nFromRight;++i){//万分小心,如履薄冰
            numbers /=10;
        }
        return numbers%10;
    }

    int beginNumber(int digits){//digits位的数字,最开始的那个值。3-->1000;2-->100
        if(digits==1){
            return 0;
        }
        return (int) pow(10,digits-1);
    }
};

面试题45. 把数组排成最小的数

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:
输入: [10,2]
输出: "102"


示例 2:
输入: [3,30,34,5,9]
输出: "3033459"
 

提示:
0 < nums.length <= 100
说明:

输出结果可能非常大,所以你需要返回一个字符串而不是整数
拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0
class Solution {
public:
    string minNumber(vector<int>& nums) {
        if(nums.size()<=0) return "";//不鸟空数组
        vector<string> vs;//定义一个字符串数组
        for(auto iter = nums.begin();iter!=nums.end();++iter){//如数拷贝成字符串
            vs.push_back(to_string(*iter));
        }
        //玄妙又玄妙,深远又深远,这里运用了lamda表达式,让字符数组中的数字字符串顺序变成,前串+后串 < 后串+前串
        sort(vs.begin(),vs.end(),
             [](const string& s1,const string& s2){return s1+s2<s2+s1;});
        
        string temp="";//把字符数组中的字符串们如数拷贝到一个字符串中
        for(auto s:vs){
            temp += s;
        }
        return temp;
    }
};

面试题46. 把数字翻译成字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 1:
输入: 12258
输出: 5
解释: 122585种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi""mzi"
 

提示:
0 <= num < 2^31

无比详细的解释,都在注释里了:

class Solution {
public:
    int translateNum(int num) {
        if(num<0){//如果num<0,直接返回0,意思是没有任何办法把一个负数翻译成字符串
            return 0;
        }
        string number = to_string(num);//把int转换成字符串number
        
        int length = number.length();//字符串中的字符总个数
        vector<int> counts(length,0);//新建一个长度为字符总个数的数组,初始化为0。由于每个元素与number中的每个字符一一对应,可以用来记录遍历到每一个字符时对应的可能的翻译种类
        int count=0;//定义一个局部变量count用来记录从后往前扫描到当前字符时对应的可能的翻译种类
        for(int i=length-1;i>=0;--i){//从后往前扫描
            count = 0;//初始化为0
            if(i<length-1){//当不是最后一个元素的时候(大多数情况,因此放前)
                count = counts[i+1];//count初始化为和它后面的字符的统计总数一样;因为,偌大一个字符串,当当前字符定死了以后,只需要看后面所有字符所有的翻译种数,就表示当前字符单独翻译情况下的翻译种数
            }else{//当是最后一个元素的时候(仅一种情况,因此放后)
                count = 1;//初始化为1
            }
            if(i<length-1){//当不是最后一个元素的时候,才能观察当前字符+后面一个字符。
                int digit1 = number[i]-'0';//取得当前字符对应的数字
                int digit2 = number[i+1]-'0';//取得后一个字符对应的数字
                int convert = digit1*10+digit2;//拿到当前字符和后面字符组合成的数字(至少是个有效的两位数10-25)
                if(convert>=10 && convert<=25){//convert在10到25的时候,说明这两个数字字符可能组成一个合法的两位数数字,这个两位数数字仅代表一个字母
                    if(i<length-2){//如果当前字符距离尾部至少还有两个字符的距离,也就是说至少:当前字符-后字符-后字符-空尾。其他情况下:当前字符-后字符-后字符-后字符-...-空尾
                        count += counts[i+2];//此时让当前字符可能出现的翻译种数 累加上 从后两个字符开始到末尾可能的翻译种数;因为当前字符和他的后一字符已经组合成了一个字符,此时,具体有多少种可能性,只能看剩下的后面那些字符的翻译种数
                    }else{//如果当前字符距离尾部不到两个字符的距离,那不可能出现counts[i+2],因为i+2下标越界了,可能的情况是:当前字符-后字符-空尾、当前字符-空尾
                        count += 1;//count递增1
                    }
                }
            }
            counts[i] = count;//不管上头是不是最后一个元素,你们把数据处理好,我这里直接存入数组
        }
        count= counts[0];//前面的for循环保证counts数组中的数字被填满,这里的这句就是从counts数组中取出最开头的那个数字,表示从第0位开始到字符串末尾的排列方法总数
        return count;
    }
};

面试题47. 礼物的最大价值

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例 1:
输入: 
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12
解释: 路径 13521 可以拿到最多价值的礼物
 

提示:
0 < grid.length <= 200
0 < grid[0].length <= 200
class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int rows = grid.size();
        int cols = grid[0].size();
        vector<vector<int>> res(rows,vector<int>(cols,-1));//定义一个二维数组res,用于保存与grid对应的位置,走到那个位置,对应的礼物最大价值
        res[0][0] = grid[0][0];//初始值先拿过来
        for(int i=1;i<rows;++i){//先初始化第一列
            res[i][0] = res[i-1][0]+grid[i][0]; //第一列res的值,只能从上面的位置过来,而无法从左边的位置过来,只需要将当前的grid值与上一个res值加起来
        }
        for(int j=1;j<cols;++j){//再初始化第一行
            res[0][j] = res[0][j-1] + grid[0][j];//第一行res的值,只能从左边的位置过来,而无法从上面的位置过来,只需要将当前的grid值与左边的res值加起来
        }
        for(int i=1;i<rows;++i){//这样就能放心大胆开始计算其他的res二维数组中的值了
            for(int j=1;j<cols;++j){
                res[i][j] = max(res[i-1][j],res[i][j-1])+grid[i][j];//当前节点所取得的gift如何能达到最大,那肯定是当前的格子中的gift最大,并且从一个gift和最大的格子走过来,那样才会最大。既然当前格子的gift无法选择,所以只能尽量从两个可能过来的方向的格子中选一个累加gift和最大的格子,作为前一个跳板
            }
        }
        return res[rows-1][cols-1];//右下角的就是最大的
    }
};

面试题48. 最长不含重复字符的子字符串

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

示例 1:
输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。


示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。


示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
 

提示:
s.length <= 40000

和书上的原题不同,这道题目比书上的原题更苛刻,书上字符串只包含a-z字母,这道题可能出现其他的字符。因此不可以像书上那样草草定义一个长度为26的position数组了事,这里使用map记录当前字符上次出现的位置i,所有思考全在注释里了:

class Solution {
public:
    int lengthOfLongestSubstring(string s){
        if (s.empty()){//不鸟空字符串
            return 0;
        }
        map<char, int> mp;  // 用来保存字符char最新一次出现的位置int
        int maxLength = 0;  // 最长不含重复字符的字符串长度
        int curLength = 0;  // 当前不含重复字符的字符串长度

        for(int i=0; i < s.size(); i++){//挨个遍历每个字符
            // 第 i 个字符之前没有出现过, f(i) = f(i-1)+1
            if(mp.find(s[i]) == mp.end()){
                curLength++;
            }
            // 第 i 个字符之前出现过,d表示第i个字符和它上次出现在字符串中的位置的距离i-map[s[i]]
            else if(i-mp[s[i]] <= curLength){ // d <= f(i-1)时,说明当前字符上一次出现的位置其实是位于最新连续字符串当中的,说明必然会造成连续字符串的破裂,从哪破裂呢,肯定是从当前字符上次出现的位置,后面的一个位置开始,直到并包括当前字符结束,所以长度为:f(i)=d
                maxLength = maxLength > curLength ? maxLength:curLength;//看是否需要更新最大长度,因为马上要截断了,会导致curLength减小,如果此时的长度比此时的最大长度还要大,那得需要更新最大长度;如果此时长度并没大过最大长度,则不用更新
                curLength = i - mp[s[i]];//当前长度肯定需要更新了,更新成d
            }
            else{ // d > f(i-1),此时位于i位置的当前字符并没有出现在已经形成的连续字符串内,因此丝毫不会影响已经形成的连续字符串,因此加上这个位于i位置的当前字符,只能说明连续字符串长度可以继续递增一个:f(i) = f(i-1)+1
                curLength++;
            }
            mp[s[i]] = i;//最后更新当前字符s[i]最新出现的位置,就是i
            maxLength = maxLength > curLength ? maxLength:curLength;//看是否需要更新最大值
        }
        return maxLength;
    }
};

面试题49. 丑数

我们把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。


说明:  
1 是丑数。
n 不超过1690

此题不太懂,后续再看:

class Solution {
public:
    int nthUglyNumber(int n) {
        if(n<=0)return 0;
        vector<int> UglyNum(n,0);
        UglyNum[0] = 1;
        int nextUglyIndex = 1;

        int p2=0,p3=0,p5=0;
        while(nextUglyIndex < n){
            int min = UglyNum[p2]*2 < UglyNum[p3]*3 ? UglyNum[p2]*2 : UglyNum[p3]*3;
            min = UglyNum[p5]*5 < min ? UglyNum[p5]*5 : min;
            UglyNum[nextUglyIndex] = min;
            while(UglyNum[p2]*2<=min){
                ++p2;
            }
            while(UglyNum[p3]*3<=min){
                ++p3;
            }
            while(UglyNum[p5]*5<=min){
                ++p5;
            }
            ++nextUglyIndex;
        }
        return UglyNum[nextUglyIndex - 1];
    }
};

面试题50. 第一个只出现一次的字符

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例:

s = "abaccdeff"
返回 "b"

s = "" 
返回 " "
 

限制:
0 <= s 的长度 <= 50000
class Solution {
public:
    char firstUniqChar(string s) {
        vector<unsigned int> hashTable(256,0);//创建一个数组,作为哈希表,索引范围是0-255共256个字符,因为ASCII码的个数就是256个,每个字符对应的ASCII码就是从0到255的数字,根据这个字符对应的ASCII码将字符存入数组的对应位置

        auto iter = s.begin();//从字符串s的开头遍历到结尾
        while(iter!=s.end()){
            hashTable[*(iter++)]++;//这一遍遍历是为了,将每个字符对应的位置计数值加1
        }

        iter = s.begin();//从字符串s的开头遍历到结尾
        while(iter!=s.end()){
            if(hashTable[*iter]==1){//这一遍遍历是为了,找到最靠近字符串s开头的计数值为1的那个字符
                return (char)(*iter);
            }
            iter++;
        }
        return ' ';//若没有次数为1,则输出空格' ' ,这一点很重要
    }
};

面试题51. 数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:
输入: [7,5,6,4]
输出: 5
 

限制:
0 <= 数组长度 <= 50000

知识点1:归并排序中略动的手脚

这题可以用归并排序来做,只在归并排序基础上加一句代码。针对添加的这句神来之笔,我的理解是这样的:

左侧数组是[left,mid],右侧数组是[mid+1,right],如果发现左侧下标为i的元素比右侧下标为j的元素大,说明,[i,mid]的每个数据都比右侧下标为j的元素大,[i,mid]中共有mid-i+1个元素,所以,此处会形成mid-i+1个逆序对。

为啥是mid-i后还要+1呢,是因为,要把当前的下标为i的这个元素算上,因为它本身就比下标为j的这个元素要大,两者也能构成逆序对

知识点2:C++ STL中copy函数的使用

如果要把一个序列(sequence)拷贝到一个容器(container)中去,通常用std::copy算法,代码如下:

std::copy(start, end, std::back_inserter(container));
即:
std::copy(start, end, container.begin());

这里,start和end是输入序列的迭代器(iterator),container是一个容器,该容器的接口包含函数push_back,复制的元素都被追加到container的末尾了。

完成代码如下

class Solution {
public:
    int global_count = 0; //全局变量
    
    int reversePairs(vector<int>& nums) {
        vector<int> copyarr(nums.size(), 0);
        merge_sort(nums, copyarr, 0, nums.size()-1);
        return global_count;
    }

    void merge_sort(vector<int>& nums, vector<int>& copyarr, int left, int right) {
        if (left >= right) return;
        int mid = (left+right) / 2;
        merge_sort(nums, copyarr, left, mid);
        merge_sort(nums, copyarr, mid+1, right);
        int i = left, j = mid+1, k = left;
        while (i <= mid && j <= right) {
            if (nums[j] < nums[i]) {
                copyarr[k++] = nums[j++];
                global_count += (mid-i+1);// 关键点,也是归并排序添加的唯一一行代码。
            } else {
                copyarr[k++] = nums[i++];
            }
        }
        if (i <= mid) copy(nums.begin()+i, nums.begin()+mid+1, copyarr.begin()+k);//把nums数组的[i,mid+1)拷贝到从k开始的位置
        if (j <= right) copy(nums.begin()+j, nums.begin()+right+1, copyarr.begin()+k);//把nums数组的[j,right+1)拷贝到从k开始的位置
        copy(copyarr.begin()+left, copyarr.begin()+right+1, nums.begin()+left);//最后,把copyarr所有元素拷贝到nums从left开始的位置
    }
};

面试题52. 两个链表的第一个公共节点

输入两个链表,找出它们的第一个公共节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(headA==NULL || headB == NULL)return NULL;

        unsigned int lenA = GetListLength(headA);
        unsigned int lenB = GetListLength(headB);

        //默认情况是A比B长,或者A与B一样长
        ListNode * longList = headA;
        ListNode * shortList = headB;
        int gap = lenA-lenB;
        //如果A比B短,那么调整一下
        if(lenA<lenB){
            longList = headB;
            shortList = headA;
            gap = lenB-lenA;
        }
        //先让长链表走上gap。 如果gap是零,说明A与B等长,那就不执行这个for循环
        for(int i=0;i<gap;++i){
            longList = longList->next;
        }
        //让两个链表一起往下走。 如果gap是零,也不要紧,俩人一同起步边走边比较,如果走到两个链表的最后还不同,那么此时的longList和shortList就是NULL,随便返回一个即可
        while(longList!=NULL && shortList!=NULL && longList!=shortList){
            longList = longList->next;
            shortList = shortList->next;
        }
        ListNode * pFirstCommonNode = longList;//or shortList;得到第一个公共节点
       
        return pFirstCommonNode;
    }

    int GetListLength(ListNode* pHead){
        int len = 0;
        ListNode * pNode = pHead;
        while(pNode!=NULL){
            ++len;
            pNode = pNode->next;
        }
        return len;
    }
};
原文地址:https://www.cnblogs.com/dindin1995/p/13059068.html