《剑指offer》第五章 39-52

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

1. 时间效率

字符串多次拼接时,不要多次使用String的+来拼接字符串,这样会产生很多string临时实例。更好地是用append方法完成字符串拼接。s1.append(s2)把字符串s2拼接到s1后面。

查找:顺序查找需要O(n),排序数组需要O(logn),哈希表则是O(1)。

剑指 Offer 39. 数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。你可以假设数组是非空的,并且给定的数组总是存在多数元素。

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

初始版本:将原数组排序,由于所求数超过数组长度地一半,所以中位数必为所求数。sort复杂度O(n)。可以利用快速排序或者快速选择替代sort。

   int majorityElement(vector<int>& nums) {
        int n = nums.size();
        if(n == 1) return nums.front();
        sort(nums.begin(),nums.end());
        return nums[n/2];
    }

优化:根据数组特点找出O(n)的算法。遍历数组的时候保存前一个遍历地数字和频次。当我们遍历到当前数字的时候,如果当前数字和我们之前保存的数字相同,则频次+1;如果当前数字和之前保存的数字不同,则频次+1。如果频次为零,我们需要保存当前数组,并把频次设为1。

由于我们要找的数字出现的频次比其他所有数字出现的频次之和还要多,那么要找的数字肯定是最后一次把频次设为1时对应的数字。

     int majorityElement(vector<int>& nums) {
         if(nums.size() == 1) return nums.front();
         int res = nums[0],times = 1;//遍历数组的时候保存遍历地数字和频次
         for(int i = 1;i < nums.size();++i){
             if(nums[i] == res) ++times;//如果当前数字和我们之前保存的数字相同,则频次+1
             else{
                 --times;//如果当前数字和之前保存的数字不同,则频次-1。
                 if(!times){
                     res = nums[i];times = 1;//如果频次为零,我们需要保存当前数字,并把频次设为1
                 }
             }
         }
         return res;
     }

剑指 Offer 40. 最小的k个数

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

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

该算法适 合海量数据的输入。假设题目是要求从海量的数据中找出最小的k个数 字,由于内存的大小是有限的,有可能不能把这些海量的数据一次性全部载入内存。这个时候,我们可以从辅助存储空间(比如硬盘)中每次读入一个数字,因此它最适合的情形就是n很大并且k较小的问题。

    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        if(!k) return vector<int>{};
        if(arr.size() <= k) return arr;//如果数组内的元素个数小于k,直接全部输出
        priority_queue<int> pq;
        vector<int> res;
        for(int i = 0;i < k; ++i){//先将前k个放入大根堆中
            pq.push(arr[i]);
        }
        for(int i = k;i < arr.size();++i){//如果待插入的值比当前已有的最大值小,则用这个数替换当前最大值
            if(arr[i] < pq.top()){
                pq.pop();
                pq.push(arr[i]);
            }    
        }
        for(int i = 0;i < k; ++i){
            res.emplace_back(pq.top());
            pq.pop();
        }
        return res;
    }

剑指 Offer 41. 数据流中的中位数

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

例如,[2,3,4] 的中位数是 3。[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。

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

整个数据被中间一个或者两个数据分隔成两部分。左边的数据全部小于右边的数据。可以根据左边最大的数和右边最小的数得到中位数。所以将左边的数存入大根堆,右边的数存入小根堆。那么插入一个数的复杂度是O(logn),得到中位数复杂度是O(1)。

为了实现平均分配,当已有数据的总数目是偶数时,把新数据插入左边,也就是大根堆。当已有数据的总数目是奇数时,把新数据插入右边,即小根堆。插入过程中,如果新数据要插入左边,但新数据大于左边数据的最大值,那么把新数据插入右边,再把右边小根堆顶最小值删除,并插入左边。如果新数据要插入右边,但新数据小于右边数据的最小值,那么把新数据插入左边,再把左边大根堆顶最大值删除,并插入右边。

priority_queue<ListNode*,vector<ListNode*>,Comp>三个参数分别是<数据类型,保存数据的容器,元素比较方式>。

class MedianFinder {
private:
    priority_queue<int, vector<int>, less<int>> leftBig;//左边大根堆
    priority_queue<int, vector<int>, greater<int>> rightSmall;//右边小根堆
public:

    MedianFinder() {}
    
    void addNum(int num) {
        if(leftBig.size() == rightSmall.size()){//左右数量相等,则取出右边的一个对象push进入左边的大顶堆。
        //这里不是直接把num插入左边是为了防止新数据大于右边数据的最小值
           rightSmall.push(num);
           leftBig.push(rightSmall.top());
           rightSmall.pop();
        }else{//左边多一个,则取出左边的一个对象push进右边的小顶堆
            leftBig.push(num);
            rightSmall.push(leftBig.top());
            leftBig.pop();
        }
    }
    
    double findMedian() {
        double res = 0.0;
        if( leftBig.size() == rightSmall.size() ){//偶数
            res = ((double)leftBig.top() + (double)rightSmall.top() )/2.0;
        }else{
           res = leftBig.top();
        }
        return res;
    }
};

剑指 Offer 42. 连续子数组的最大和

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。

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

当以第i-1个数字结尾的子数组中所有数字的和小于0时,如果把这个负数与第i个数累加,得到的结果比第i个数字本身还要小,所以这种情况下以第i个数字结尾的子数组就是第i个数字本身。

如果以第i-1个数字结尾的子数组中所有数字的和大于0,与第i个数字累加就得到以第i个数字结尾的子数组中所有数字的和。

    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        int cur = nums[0],res = cur;
        for(int i = 1;i < n;++i){
            if(cur < 0) cur = nums[i];
            else cur += nums[i];
            res = max(res,cur);
        }
        return res;
    }

剑指 Offer 43. 1~n 整数中 1 出现的次数

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

输入:n = 12
输出:5

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FGu2EIiO-1622635133201)(images/image-20210531001231487.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3CqoMY3F-1622635133203)(images/image-20210531001245084.png)]

    int countDigitOne(int n) {
        if(n < 1) return 0;
        long long digit = 1;//表示个位
        int high = n/10;//高位
        int cur = n%10;//当前位
        int low = 0;//低位,因为当前位是个位,所以低位为0
        int res = 0;
        
        while(high || cur){
            if(!cur) res += high*digit;
            else if(cur == 1) res += high*digit+low+1;
            else res += (high+1)*digit;

            low += cur*digit;//改变当前位以及前后位的数
            cur = high%10;
            high /= 10;
            digit *= 10;
        }
        return res;
    }

剑指 Offer 44. 数字序列中某一位的数字

数字以0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。请写一个函数,求任意第n位对应的数字。0 <= n < 231

输入:n = 11
输出:0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wR0Wl5xf-1622635133208)(images/image-20210531084745560.png)]

对于第 n 位对应的数字,我们令这个数字对应的数为 target,然后分三步进行。

  • 首先找到这个数字对应的数是几位数,用 digits 表示;

  • 然后确定这个对应的数的数值 target;

  • 最后确定返回值是 target 中的哪个数字。

    举个例子:

比如输入的 n 是 365:

经过第一步计算我们可以得到第 365 个数字表示的数是三位数,n = 365 - 9 - 90 = 176,digtis = 3。这时 n = 176 表示目标数字是三位数中的第 176 个数字。

我们设目标数字所在的数为 number,计算得到 number = 100 + 176/3 = 158,idx 是目标数字在 number 中的索引,如果 idx = 0,表示目标数字是 number 中的最后一个数字。

根据步骤2,我们可以计算得到 idx = n % digits = 176 % 3 = 2,说明目标数字应该是 number = 158 中的第二个数字,即输出为 5。

注意这道题n的范围0 <= n < 231,int能表示的范围是int的[ − 231 , 231 − 1 ],这道题很有可能超出int的范围,所以在编写程序过程中如果某个变量涉及到*操作时,尽量注意是否溢出。

    int findNthDigit(int n) {
        //计算该数字由几位数组成,由1位:digit = 1;2位:digit = 2...
        long long base = 9, digit = 1;
        while(n - base*digit > 0){//1~9有9个数,10~99有90个数,100~999有900个数,1000~9999有9000个数
            n -= base*digit;
            base *= 10;
            ++digit;
        }

        //计算真实代表的数字是多少
        int idx = n%digit;//由于上面的计算,n现在代表digit位数中的第n个数字
        if(!idx) idx = digit;
        long long number = 1;
        for(int i = 1;i < digit; ++i){
            number *= 10;
        }
        number += (idx == digit)? n/digit-1 : n/digit;

        //从真实的数字中找到我们想要的那个数字
        for(int i = idx; i < digit; ++i){
            number /= 10;
        }
        return number%10;
    }

剑指 Offer 45. 把数组排成最小的数

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

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

首先解决大数问题,需要把数字转换成字符串,然后进行排序,可以使用内置的sort,也可以自己写快速排序。

  1. sort中的比较函数compare要声明为静态成员函数全局函数,不能作为普通成员函数,否则会报错。 因为:非静态成员函数是依赖于具体对象的,而std::sort这类函数是全局的,因此无法再sort中调用非静态成员函数。**静态成员函数或者全局函数是不依赖于具体对象的, 可以独立访问,无须创建任何对象实例就可以访问。**同时静态成员函数不可以调用类的非静态成员。
  2. sort函数是比较是绝对大小,所以只能<或者>,不能>=或<=简单来讲函数如果比较两个相同的值,那么比较函数应该返回false,否则再运算过程中会产生冲突报错
class Solution {
public:
    string minNumber(vector<int>& nums) {
        vector<string> str;
        string res;
        for(auto &num:nums){
            str.push_back(to_string(num));
        }
        sort(str.begin(),str.end(),compare);
        for(auto & s:str){
            res.append(s);
        }
        return res;
    }
private:
    static bool compare(const string &a,const string &b){
        return a+b < b+a;//如比较30和3,显然303比330小,所以30应该在前边。比较3和34,显然334<343,所以3应该在前边,3<34
    }
};
  1. 自己写快速排序
class Solution{
public:
    string minNumber(vector<int> & nums){
        vector<string> strs;
        for(auto &num:nums){
            strs.push_back(to_string(num));
        }
        quickSort(strs, 0, strs.size()-1);//对字符串进行排序
        string res;
        for(auto & s:strs){
            res.append(s);
        }
        return res;
    }
    void quickSort(vector<string> & strs, int l, int r){
        if(l >= r) return;
        int i = l, j =r;
        while(i < j){
            while(strs[j] + strs[l] >= strs[l] + strs[j] && i < j) --j;//如果右边的数都大于基准strs[l],则继续
            while(strs[i] + strs[l] <= strs[l] + strs[i] && i < j) ++i;//如果左边的数都大于基准strs[l],则继续
            swap(strs[i],strs[j]);
        }
        swap(strs[i], strs[l]);
        quickSort(strs, l, i-1);
        quickSort(strs, i+1, r);
    }
};

剑指 Offer 46. 把数字翻译成字符串

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

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

按照一位数翻译还是按照两位数翻译,取决于前一个数字的大小。

如果前一个数为1,那么当前数无论是任何都可以;如果前一个数是2,当前数必须小于等于5,才有可能翻译成两位数。

除此之外,只能一个数一个数的翻译,那么翻译方式唯一等于前一个数字的翻译方式。

    int translateNum(int num) {
        string nums = to_string(num);
        int n = nums.size();
        if(n <= 1) return 1;
        int pre1 = 1, pre2 = 1,cur = 0;
        for(int i = 1;i < n; ++i){
            //如果前一个数为1,那么当前数无论是任何都可以;如果前一个数是2,当前数必须小于等于5,才有可能翻译成两位数
            if(nums[i-1] == '1' || (nums[i-1] == '2' && nums[i] <= '5') ){
                cur = pre1 + pre2;
            }else{
                cur = pre1;
            }
            pre2 = pre1;
            pre1 = cur;
        }
        return cur;
    }

剑指 Offer 47. 礼物的最大价值

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

输入: [
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
/*    int maxValue(vector<vector<int>>& grid) {
        if(grid.empty()) return 0;
        int m = grid.size(),n = grid[0].size();
        vector<vector<int>> gift(m+1,vector<int>(n+1,0));
        for(int i = 1; i <= m; ++i){
            for(int j = 1; j <= n; ++j){
                gift[i][j] = max(gift[i-1][j], gift[i][j-1]) + grid[i-1][j-1];
            }
        }
        return gift[m][n];
    }
    */
    /*当前位置的礼物数,只与左边和上面的礼物数有关
    优化:利用一维数组代替上面位置礼物,利用left记录每次左边数量,从左往右遍历*/
    int maxValue(vector<vector<int>>& grid) {
        if(grid.empty()) return 0;
        int m = grid.size(),n = grid[0].size();
        vector<int> top(n,0);
        int left = 0,cur = 0;
        for(int i = 0; i < m; ++i){
            for(int j = 0; j < n; ++j){
                cur = max(left,top[j]) + grid[i][j];
                top[j] = cur;
                left = cur;
            }
            left = 0;//新的一行开始后,最左边礼物数为0
        }
        return cur;
    }

剑指 Offer 48. 最长不含重复字符的子字符串

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

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

双指针:每次遍历固定左指针,移动右指针,将元素存入哈希表,如果右指针所指元素在哈希表种存在,则停止右指针,更新字串长度。下次一循环中,把字符串头部,也就是left所指元素删掉。

    int lengthOfLongestSubstring(string s) {
        if(s.size() <= 1) return s.size();//注意空格无法存入set中
        unordered_set<char> hash;
        int left = 0, right = left, res = 0;
        for(int left = 0; left < s.size(); ++left){
            if(left > 0) hash.erase(s[left-1]);//左指针向右移动一格,移除一个字符
            while(right < s.size() && !hash.count(s[right])){//如果不重复,不断移动右指针
                hash.insert(s[right]);
                ++right;
            }
            int len = hash.size();
            res = max(res,len);
        }
        return res;
    }

2.时间效率和空间效率的平衡

在软件开发的过程中我们允 许以牺牲一定的空间为代码来优化时间性能,以尽可能地缩短软件的响应时间。这就是我们通常所说的“以空间换时间”。

但对于一些嵌入式的开发就要慎重。

剑指 Offer 49. 丑数

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

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

基础版本:说如果一个数 能被2整除,我们把它连续除以2;如果能被3整除,就连续除以3;如 果能被5整除,就除以连续5。如果最后我们得到的是1,那么这个数就是丑数,否则不是。但每个整数都需要计算。即使一个数字不是丑数,我们还是需要对它做求余数和除法操作。

优化:根据丑数定义,丑数是另一个丑数乘以2、3、5的结果,所以可以创建一个数组,里面存储排序好的丑数,后面的每一个数字都是前面某个数乘2、3、5得到的。假设当前存在3个数组nums2,nums3,nums5分别代表从1开始分别乘以2,3,5的序列,则最终的丑数序列实际上就是这三个有序序列对的合并结果,计算丑数序列相当于合并三个有序序列。合并有序序列时,需要给每个序列都各自维护一个指针,然后比较指针指向的元素的值,将最小的元素放入有序丑数数组中,并将相应的指针向后移动一个元素。

    int nthUglyNumber(int n) {
        vector<int> dp(n); 
        dp[0] = 1;
        int p2 = 0, p3 = 0, p5 = 0;//2,3,5的倍数的序列指针
        for(int i = 1; i < n; ++i){
            dp[i] = min( min(dp[p2]*2, dp[p3]*3), dp[p5]*5);//从头开始,找到丑数序列中第i个数
            if(dp[i] == dp[p2]*2) ++p2;//找到当前丑数序列中第i个数对应的是三个倍数序列中的哪一个序列中的元素
            if(dp[i] == dp[p3]*3) ++p3;
            if(dp[i] == dp[p5]*5) ++p5;
        }
        return dp[n-1];
    }

剑指 Offer 50. 第一个只出现一次的字符

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

s = "abaccdeff"
返回 "b"
s = "" 
返回 " "
    char firstUniqChar(string s) {
        char res = ' ';
        if(s.empty()) return res;
        unordered_map<char,int> hash;
        for(auto &ss:s){
            hash[ss]++;
        }
        for(auto &h:hash){
            if(h.second == 1) res = h.first;
        }
        return res;
    }

剑指 Offer 51. 数组中的逆序对

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

输入: [7,5,6,4]
输出: 5

利用归并排序的思想,本质是分而治之,每当调整两个数组中对应的两个数的顺序进行归并时都会进行一次数的比较,由于总是两个排序好的数组进行归并,所以每次归并的时候的逆序对都可以找出来,以此类推,直到归并所有的数。

    int reversePairs(vector<int>& nums) {
        vector<int> temp(nums.size());
        return mergeSort(0, nums.size()-1, nums, temp);
    }
    int mergeSort(int l, int r, vector<int> &nums, vector<int> &temp){
        if(l >= r) return 0;//终止条件
        int m = (l + r)/2;//递归划分
        int res = 0;
        res = mergeSort(l, m, nums, temp) + mergeSort(m + 1, r, nums, temp);
        //合并
        int i = l, j = m + 1;//i是左边一半数组的指针,j是右边一半数组的指针
        for(int k = l; k <= r; ++k){
            temp[k] = nums[k];//把l~r范围内的数拷贝进临时数组中
        }
        for(int k = l; k <= r; ++k){
            //i == m+1时,左边的数组遍历完了,只剩下右边的数组没有遍历完了,右边数组是已经排好序的状态,所以直接一次放进nums就行
            if(i == m+1) nums[k] = temp[j++];
            //j == r+1时,右边的数组遍历完了,只剩下左边的数组没有遍历完了,左边数组是已经排好序的状态,所以直接一次放进nums中
            else if(j == r+1 || temp[i] <= temp[j]) nums[k] = temp[i++];
            //另一种情况是左边数组中的当前元素要比右边数组中的当前元素小,那它就比右边元素剩下的当前元素小,所以不会在后序遍历中构成逆序对,所以直接放
            else{
                nums[k] = temp[j++];
                //前面已经包含了所有可能的情况,在else中就表示出现了一个逆序对,即左边数组的当前元素比右边数组中的大,所以左边数组中剩下的所有元素都比右边数组当前元素大
                res += m-i+1;//m=i+1等于当前左边数组中剩下的元素个数,因为剩下所有元素都比右边数组当前元素大,所以左边剩下多少个元素,当前就构成多少个逆序
            }
        }
        return res;
    }

剑指 Offer 52. 两个链表的第一个公共节点

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

如下面的两个链表**:**

img

在节点 c1 开始相交。

    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(!headA || !headB) return nullptr;
        ListNode * A = headA, * B = headB;
        while(A != B){
            if( !A ) A = headB;
            else A = A->next;
            if( !B ) B = headA;
            else B = B->next;
        }
        return A;
    }

猜你喜欢

转载自blog.csdn.net/ClaireSy/article/details/117481300