• 【数据结构与算法】《剑指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;
        }
    };
    
  • 相关阅读:
    【django 】后台在线查看文件,下载文件的配置
    【Python小随笔】Pandas读取每一行数据
    【Django】 admin创建普通用户
    【Django】 原生上传文件,后台接收文件
    Visual Studio Code 配置C、C++ 文件debug调试环境
    华为 matebook x pro 使用U盘启动winpe系统
    记微信第三方平台通过ticket 并解密 然后授权相关接口/获取令牌 获取component_access_token
    C#中字符串的修改
    HTML Table 合并单元格
    TortoiseGit小乌龟安装配置及使用
  • 原文地址:https://www.cnblogs.com/dindin1995/p/13059068.html
Copyright © 2020-2023  润新知