《剑指Offer》笔记&题解&思路&技巧&优化——精心编写(2)

和你一起轻松愉快的刷题


一、前言

  • 为了方便阅读,完整笔记分为两篇文章,第(1)篇题目为1-38题,第(2)篇题目为39-75题。
  • 所有题目均来自 LeetCode《剑指 Offer(第 2 版)》。
  • 截止到编写文章时,所有题解代码均可通过LeetCode在线评测,即AC。
  • 笔记中一些题目给出了多种题解和思路,笔记中大多数题解都是较为完美的解法,时间复杂度和空间复杂度都较低的解法。
  • 由于作者水平有限,欢迎大家指教,共同交流学习。
  • 最后祝大家刷题愉快。

二、开始刷题

剑指 Offer 40. 最小的k个数

最小k个数       

        大根堆,优先队列(priority_queue)

        首先将前 k 个数插入大根堆中,随后从第 k+1 个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后大根堆里的 k 个数就是要求的最小的 k 个数。

        方法二:快排的思想

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        if(k <= 0)  return vector<int>();
        
        vector<int> ans(k, 0);
        priority_queue<int> pq;
        for(int i=0; i<k; ++i){
            pq.push(arr[i]);
        }
        for(int i=k; i<arr.size(); ++i){
            if(pq.top() > arr[i]){
                pq.pop();
                pq.push(arr[i]);
            }
        }
        for(int i=0; i<k; ++i){
            ans[i] = pq.top();
            pq.pop();
        }
        return ans;
    }
};

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

大堆小堆找中位数

        使用一个最大堆一个最小堆来管理海量数据。        

        将数据分为[小][大]两部分,[左边最大堆][右边最小堆]。

        维护两个平衡优先队列,保证左边数量>=右边数量 且 左边数量-右边数量<=1。

        当累计添加的数的数量为奇数时,向左边插入,此时中位数为左边的队头。当累计添加的数的数量为偶数时,向右边插入,两个优先队列中的数的数量相同,此时中位数为它们的队头的平均值。

class MedianFinder {
public:
    /** initialize your data structure here. */
    MedianFinder() {
         count = 0;
    }
    
    void addNum(int num) {
        ++count;
        if(count%2 == 1){ //奇数时
            right_small.push(num);
            left_big.push(right_small.top());
            right_small.pop();
        }else{ //偶数时
            left_big.push(num);
            right_small.push(left_big.top());
            left_big.pop();
        }
    }
    
    double findMedian() {
        if(count%2 == 1){
            return (left_big.top()) / 1.0;
        }
        return (left_big.top() + right_small.top()) / 2.0;
    }

private:
    int count;
    priority_queue<int, vector<int>, less<int>> left_big;
    priority_queue<int, vector<int>, greater<int>> right_small;
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder* obj = new MedianFinder();
 * obj->addNum(num);
 * double param_2 = obj->findMedian();
 */

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

动态规划

        常规DP做法,可以直接在原数组上进行修改,节约一点空间。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> dp(nums.size(), 0);
        dp[0] = nums[0];
        int maxSum = dp[0];
        for(int i=1; i<nums.size(); ++i){
            dp[i] = max(nums[i], nums[i] + dp[i-1]);
            maxSum = max(maxSum, dp[i]);
        }
        return maxSum;
    }
};
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int maxSum = nums[0];
        for(int i=1; i<nums.size(); ++i){
            nums[i] = max(nums[i], nums[i] + nums[i-1]);
            maxSum = max(maxSum, nums[i]);
        }
        return maxSum;
    }
};

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

思维 + 找规律

        分两种情况,例如:1234和2234,high为最高位,pow为最高位权重,在每种情况下都将数分段处理,即0-999,1000-1999,...,剩余部分

        case1:最高位是1,则最高位的1的次数为last+1(1000-1234),每阶段即0-999的1的个数1*countDigitOne(pow-1) ,除最高位剩余部分1的个数为countDigitOne(last)。

        case2:最高位不是1,则最高位的1的次数为pow(1000-1999) 每阶段除去最高位即0-999,1000-1999中1的次数为high*countDigitOne(pow-1) 剩余部分1的个数为countDigitOne(last) 发现两种情况差别仅在最高位的1的个数,因此单独计算最高位的1的次数,合并处理两种情况。

class Solution {
public:
    int countDigitOne(int n) {
        if(n <= 0)   return 0;
        if(n < 10)  return 1;
        int high = n, pow = 1;
        while(high >= 10){
            high /= 10;
            pow *= 10;
        }
        int last = n - high*pow;
        int cnt = high==1? last+1: pow;
        return cnt + countDigitOne(last) + high*countDigitOne(pow-1);
    }
};

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

数学规律 + 迭代 + 求整 / 求余

class Solution {
public:
    int findNthDigit(int n) {
        int digit = 1;
        long long start = 1;
        long long count = 9;
        // 第1步
        while(n > count){
            n -= count;
            ++digit;
            start *= 10;
            count = 9 * start * digit; 
        }
        // 第2步
        int num = start + (n-1)/digit;
        // 第3步
        string s = to_string(num);
        return s[(n-1) % digit]-'0';
    }
};

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

动态规划

        经典青蛙跳台阶问题 f(n) = f(n-1) + f(n-2) 的变形。

        数字转字母,只能从两个途径产生,一个是自己单独成字母,一个是和前一个组合成两位数成字母。

        与青蛙跳台阶问题不同的是要注意边界条件:dp[1]既可能为1也可能为0,大于25就不是字母了,01, 02, ... 09 这种0开头的也不能转字母。

class Solution {
public:
    int translateNum(int num) {
        string str = to_string(num);
        int n = str.size();
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        for(int i=1; i<n; ++i){
            if(i == 1){ //特判dp[1] = ?
                if((str[0] == '1') || (str[0] == '2' && str[1] < '6')){
                    dp[1] = dp[0] + 1;
                }else{
                    dp[1] = dp[0];
                }
                continue;
            }
            string s = str.substr(i-1, 2);
            if(stoi(s) >= 10 && stoi(s) <= 25){
                dp[i] = dp[i-1] + dp[i-2];
            }else{
                dp[i] = dp[i-1];
            }
        }
        return dp[n-1];
    }
};

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

动态规划

        dp[i][j] = dp[i-1][j] + dp[i][j-1];

        可以使用两个长度为 n 的一位数组代替 m×n 的二维数组,交替地进行状态转移,减少空间复杂度。

class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector<vector<int>> dp(m, vector<int>(n));
        dp[0][0] = grid[0][0];
        // 处理第一行和第一列
        for(int i=1; i<m; ++i)  dp[i][0] = dp[i - 1][0] + grid[i][0];
        for(int j=1; j<n; ++j)  dp[0][j] = dp[0][j - 1] + grid[0][j];
        // dp
        for(int i=1; i<m; ++i){
            for(int j=1; j<n; ++j){
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i][j];
            }
        }

        return dp[m-1][n-1];
    }
};

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

动态规划 + 哈希表

        查找上一次元素出现的位置可以用哈希表或线性遍历

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n = s.size();
        if(n == 0)  return 0;
        vector<int> dp(n, 0);
        dp[0] = 1;
        unordered_map<char, int> hash;
        int maxLen = 1; 
        hash[s[0]] = 0;
        
        for(int i=1; i<n; ++i){
            if(hash.find(s[i]) == hash.end()){ //第一次出现,可以直接连接上dp[i-1]
                dp[i] = dp[i-1] + 1;               
            }else{
                if(hash[s[i]] < i-dp[i-1]){ //上一次出现不在dp[i-1]内,可以连接
                    dp[i] = dp[i-1] + 1;
                }else{ //上一次出现在dp[i-1]内,长度 = 当前位置-上一次出现的位置
                    dp[i] = i - hash[s[i]]; 
                }
            }
            hash[s[i]] = i; //加入到哈希表
            maxLen = max(maxLen, dp[i]); //更新最大长度
        }
        return maxLen;
    }
};

剑指 Offer 49. 丑数

最小堆

        要得到从小到大的第 n 个丑数,可以想到使用最小堆实现。

        初始时堆为空。首先将最小的丑数 1 加入堆。每次取出堆顶元素 x,则 x 是堆中最小的丑数,由于 2x,3x,5x 也是丑数,因此将 2x,3x,5x 加入堆。

        上述做法会导致堆中出现重复元素的情况。为了避免重复元素,可以使用哈希集合去重,避免相同元素多次加入堆。

        在排除重复元素的情况下,第 n 次从最小堆中取出的元素即为第 n 个丑数。

        时间复杂度:O(nlogn)

        空间复杂度:O(n)

动态规划 / 三指针

        首先一定要知道,后面的丑数一定由前面的丑数乘以2,或者乘以3,或者乘以5得来。

下一次寻找丑数时,则对这三个位置分别尝试使用一次乘2机会,乘3机会,乘5机会,看看哪个最小,最小的那个就是下一个丑数。最后,那个得到下一个丑数的指针位置加一,因为它对应的那次乘法使用完了。

        这里需要注意下去重的问题,如果某次寻找丑数,找到了下一个丑数10,则p2和p5都需要加一,因为5乘2等于10, 5乘2也等于10,这样可以确保10只被数一次。

        时间复杂度:O(n)

        空间复杂度:O(n)

class Solution {
public:
    int nthUglyNumber(int n) {
        vector<int> factors{2, 3, 5};
        unordered_set<long> uset;
        priority_queue<long, vector<long>, greater<long>> heap;
        heap.push(1);
        uset.insert(1);
        int ugly = 0;
        for(int i=0; i<n; ++i){
            long cur = heap.top();
            heap.pop();
            ugly = cur;
            for(int factor: factors){
                long next = cur * factor;
                if(!uset.count(next)){
                    uset.insert(next);
                    heap.push(next);
                }
            }
        }
        return ugly;
    }
};
class Solution {
public:
    int nthUglyNumber(int n) {
        if(n > 0 && n < 7)   return n;
        vector<int> res(n, 0);
        res[0] = 1;
        int p2 = 0, p3 = 0, p5 = 0;
        for(int i=1; i<n; ++i){
            int curUgly = min(min(res[p2]*2, res[p3]*3), res[p5]*5);
            if(curUgly == res[p2]*2)    ++p2;
            if(curUgly == res[p3]*3)    ++p3;
            if(curUgly == res[p5]*5)    ++p5;
            res[i] = curUgly;
        }
        return res[n-1];
    }
};

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

哈希表

        两次哈希操作,第一次从前到后记录字符出现的次数,第二次找最先出现一次的字符。

class Solution {
public:
    char firstUniqChar(string s) {
        unordered_map<char, int> hash;
        for(int i=0; i<s.size(); ++i){
            ++hash[s[i]];
        }
        for(int i=0; i<s.size(); ++i){
            if(hash[s[i]] == 1)
                return s[i];
        }
        return ' ';
    }
};

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

分治思想 / 归并排序

        「归并排序」与「逆序对」是息息相关的。归并排序体现了 “分而治之” 的算法思想,具体为:

        分: 不断将数组从中点位置划分开(即二分法),将整个数组的排序问题转化为子数组的排序问题;
        治: 划分到子数组长度为 1 时,开始向上合并两个排序数组,不断将 较短排序数组 合并为 较长排序数组,直至合并至原数组时完成排序;

        每当遇到 左子数组当前元素 > 右子数组当前元素 时,意味着 「左子数组当前元素 至 末尾元素」 与 「右子数组当前元素」 构成了若干 「逆序对」 。

class Solution {
public:
    int reversePairs(vector<int>& nums) {
        int n = nums.size();
        vector<int> tmp(n);
        return mergeSort(nums, tmp, 0, n-1);
    }

    int mergeSort(vector<int>& nums, vector<int>&tmp, int l, int r){
        // 终止条件
        if(l >= r)  return 0;
        // 递归 分
        int mid = l + (r-l)/2;
        int res = mergeSort(nums, tmp, l, mid) + mergeSort(nums, tmp, mid+1, r);
        // 合并 治
        int i = l, j = mid+1;
        for(int k=l; k<=r; ++k){
            tmp[k] = nums[k];
        }
        for(int k=l; k<=r; ++k){
            if(i == mid+1){ //左子数组已合并完
                nums[k] = tmp[j++];
            }else if(j == r+1 || tmp[i] <= tmp[j]){ //右子数组已合并完 或 左子元素<右子元素
                nums[k] = tmp[i++];
            }else{ // 右子元素<左子元素,产生了逆序对
                nums[k] = tmp[j++];
                res += mid-i+1;
            }
        }

        return res;
    }

};

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

相交链表

        假设链表 A 的头节点到相交点的距离是 a ,链表 B 的头节点到相交点的距离是 b ,相交点到链表终点的距离为 c 。我们使用两个指针,分别指向两个链表的头节点,并以相同的速度前进,若到达链表结尾,则移动到另一条链表的头节点继续前进。按照这种前进方法,两个指针会在 a + b + 次前进后同时到达相交节点。
/**
 * 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) {
        ListNode* L1 = headA, *L2 = headB;
        while(L1 != L2){
            L1 = L1? L1->next: headB;
            L2 = L2? L2->next: headA;
        }
        return L1;
    }
};

剑指 Offer 53 - I. 在排序数组中查找数字 I

STL中equal_range() 函数

        函数equal_range()返回first和last之间等于val的元素区间,返回值是一对迭代器。 
        此函数假定first和last区间内的元素可以使用<操作符或者指定的comp执行比较操作。
        equal_range()可以被认为是lower_bound()和upper_bound()的结合,pair中的第一个迭代器由lower_bound返回,第二个则由upper_bound返回。

实现lower_bound()和upper_bound()函数

        二分法

class Solution {
public:
    int search(vector<int>& nums, int target) {
        auto pos = equal_range(nums.begin(), nums.end(), target);
        return pos.second - pos.first;
    }
};

 剑指 Offer 53 - II. 0~n-1中缺失的数字

哈希集合

        unordered_set

数学公式

        S = \frac{n(n-1)}{2}

位运算

        a ^ b ^ a = b

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        unordered_set<int> uset;
        int n = nums.size() + 1;
        for(int num: nums){
            uset.insert(num);
        }
        for(int i=0; i<n; ++i){
            if(!uset.count(i)){
                return i;
            }
        }
        return -1;
    }
};
class Solution {
public:
    int missingNumber(vector<int>& nums) {
        unordered_set<int> uset;
        int n = nums.size() + 1;
        int total = n*(n-1) / 2;
        int sum = 0;
        for(int num: nums){
            sum += num;
        }
        return total - sum;
    }
};
class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int n = nums.size() + 1;
        int ans = 0;
        for(int i=0; i<n; ++i){
            ans ^= i;
        }
        for(int num: nums){
            ans ^= num;
        }
        return ans;
    }
};

剑指 Offer 54. 二叉搜索树的第k大节点 

中序遍历

        二叉搜索树的中序遍历为递增序列,所以二叉搜索树的中序遍历倒序为递减序列。

        求 “二叉搜索树第 k 大的节点” 可转化为求 “此树的中序遍历倒序的第 k 个节点”。

        中序遍历的倒序为:“右、根、左”

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int kthLargest(TreeNode* root, int k) {
        if(root == nullptr)   return 0;
        stack<TreeNode*> st;
        
        while(!st.empty() || root != nullptr){
            while(root){               
                st.push(root);
                root = root->right;
            }
            root = st.top();
            st.pop();
            if(--k == 0)    return root->val;
            root = root->left;
        }
        return 0;
    }
};


剑指 Offer 55 - I. 二叉树的深度

二叉树深度

        递归 / 深度优先搜索

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int maxDepth(TreeNode* root) {
        if(root){
            return 1 + max(maxDepth(root->left), maxDepth(root->right));
        }else{
            return 0;
        }
    }
};

剑指 Offer 55 - II. 平衡二叉树

树的深度

        解法类似于求树的最大深度,但有两个不同的地方:一是我们需要先处理子树的深度再进行比较,二是如果我们在处理子树时发现其已经不平衡了,则可以返回一个-1,使得所有其长辈节点可以避免多余的判断(本题的判断比较简单,做差后取绝对值即可;但如果此处是一个开销较大的比较过程,则避免重复判断可以节省大量的计算时间)。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isBalanced(TreeNode* root) {
        return helper(root) != -1;
    }

    int helper(TreeNode* root){
        if(!root)   return 0;
        int left = helper(root->left), right = helper(root->right);
        if(left == -1 || right == -1 || abs(left-right) == -1){
            return -1;
        }
        return 1 + max(left, right);
    }

};

剑指 Offer 56 - I. 数组中数字出现的次数

位运算 ^

        a ^ b ^ a = b
        1.全部异或    2.从右向左找非 0 位    3.根据 2 找到的位置 0/1 分两组    4.分别找各组只出现一次的元素。
class Solution {
public:
    vector<int> singleNumbers(vector<int>& nums) {
        int tmp = 0;
        for(int num: nums){
            tmp ^= num;
        }
        // 找到从右向左的首个非0位
        int index = 1;
        while((tmp & index) == 0){
            index <<= 1;
        }
        // 分组异或
        int first = 0, second = 0;
        for(int num: nums){
            if(num & index){
                first ^= num;
            }else{
                second ^= num;
            }
        }
        return vector<int> {first, second};
    }
};

剑指 Offer 56 - II. 数组中数字出现的次数 II

哈希表

        大家都会

位运算 + 遍历统计

        使用与运算,可获取二进制数字 num 的最右一位 n1,配合右移操作,可获取 num 所有位的值(即 n1​ ~ n32),将 counts 各元素对 3 求余,则结果为 “只出现一次的数字” 的各二进制位。

        利用左移操作和或运算,可将 counts 数组中各二进位的值恢复到数字 res 上( i∈[0,31] )。

        实际上,只需要修改求余数值 m ,即可实现解决 除了一个数字以外,其余数字都出现 m 次 的通用问题。

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        unordered_map<int, int> hash;
        for(int num: nums){
            ++hash[num];
        }
        for(int num: nums){
            if(hash[num] == 1){
                return num;
            }
        }
        return -1;
    }
};
class Solution {
public:
    int singleNumber(vector<int>& nums) {
        vector<int> count(32, 0);
        for(int num: nums){
            for(int i=0; i<32; ++i){
                count[i] += num & 1;
                num >>= 1;
            }
        }
        int res = 0, m = 3;
        for(int i=0; i<32; ++i){
            res <<= 1;
            res |= count[31-i] % m;
        }
        
        return res;
    }
};

剑指 Offer 57. 和为s的两个数字

双指针 / 滑动窗口

        哈希表一次遍历,时间空间复杂度均为 O(n)。

        由于是已排序数组,想到用双指针,降低空间复杂度为 O(1)。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int l=0, r=nums.size()-1;
        while(l < r){
            int sum = nums[l] + nums[r];
            if(sum == target){
                return vector<int>{nums[l], nums[r]};
            }else if(sum > target){
                --r;
            }else{
                ++l;
            }
        }
        return vector<int>();
    }
};

剑指 Offer 57 - II. 和为s的连续正数序列

暴力枚举

        枚举每个正整数为起点,判断以它为起点的序列和 sum 是否等于 target 即可,由于题目要求序列长度至少大于 2,所以枚举的上界为 target / 2。

双指针/滑动窗口 + 数学

        由 x 累加到 y 的求和公式得:

\frac{(x+y) \times (y-x+1)}{2} = target

class Solution {
public:
    vector<vector<int>> findContinuousSequence(int target) {
        vector<vector<int>> res;
        vector<int> tmp;
        int sum = 0, limit = target / 2;
        for(int i=1; i<=limit; ++i){
            for(int j=i; ; ++j){
                sum += j;
                if(sum > target){
                    sum = 0;
                    break;
                }else if(sum == target){
                    tmp.clear();
                    for(int k=i; k<=j; ++k){
                        tmp.emplace_back(k);
                    }
                    res.emplace_back(tmp);
                    sum = 0;
                    break;
                }
            }
        }
        return res;
    }
};
class Solution {
public:
    vector<vector<int>> findContinuousSequence(int target) {
        vector<vector<int>> res;
        int l = 1, r = 2;
        while(l < r){
            int sum = (l + r) * (r - l + 1) / 2; //求和
            vector<int> tmp;
            if(sum == target){
                for(int i=l; i<=r; ++i){
                    tmp.emplace_back(i);
                }
                res.emplace_back(tmp);
                ++l; //即使当前满足,依然要前进,这有点tcp滑动窗口的意思吧
            }else if(sum < target){
                ++r;
            }else{
                ++l;
            }
        }
        
        return std::move(res); //借助C++11的move函数,总体时间会更短
    }
};

剑指 Offer 58 - I. 翻转单词顺序

倒叙拆分

class Solution {
public:
    string reverseWords(string s) {
        int l = 0, r= s.size()-1;
        // 去除首位空格
        while(l<=r && s[l]==' ')    ++l;
        while(l<=r && s[r]==' ')    --r;

        string res, tmp;
        int cnt = 0; //记录单词中间空格个数
        for(int i=r; i>=l; --i){
            if(s[i] != ' '){
                cnt = 0;
                tmp = s[i] + tmp;
            }else if(s[i] == ' '){
                if(++cnt == 1){
                    res = res + tmp + " ";
                    tmp = "";
                }
            }
        }
        if(tmp.size() != 0)
            res += tmp;

        return res;
    }
};

剑指 Offer 58 - II. 左旋转字符串

substr()函数

拆分再拼接

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        int len = s.size();
        // if(n > len) n %= len; //n可能大于len
        s += s;
        return s.substr(n, len);
    }
};
class Solution {
public:
    string reverseLeftWords(string s, int n) {
        int len = s.size();
        string str, ans;
        for(int i=0; i<n; ++i){
            str += s[i];
        }
        for(int i=n; i<len; ++i){
            ans += s[i];
        }
        ans += str;
        
        return ans;
    }
};

剑指 Offer 59 - I. 滑动窗口的最大值

优先队列 / 最大堆

        对于「最大值」,一种非常合适的数据结构,优先队列(大根堆)。为了方便判断堆顶元素与滑动窗口的位置关系,可以在优先队列中存储二元组 (num, index)。

        时间复杂度:O(nlogn)

        空间复杂度:O(n)

双端队列

       利用双端队列(单调队列)进行操作:每当向右移动时,把窗口左端的值从队列左端剔除,把队列右边小于窗口右端的值全部剔除。这样双端队列的最左端永远是当前窗口内的最大值。另外,这道题也是单调栈的一种延申:该双端队列利用从左到右递减来维持大小关系。

        双端队列的作用是保证每次L边界右移时从队列头弹出的都是当前窗口的最大值,队列中存储元素下标即可。

        时间复杂度:O(n)

        空间复杂度:O(k)

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> ans;
        priority_queue<pair<int, int>> heap;
        for(int i=0; i<k; ++i){
            heap.emplace(nums[i], i);
        }
        ans.emplace_back(heap.top().first);
        for(int i=k; i<n; ++i){
            heap.emplace(nums[i], i);
            while(heap.top().second <= i-k){
                heap.pop();
            }
            ans.emplace_back(heap.top().first);
        }
        return ans;
    }
};
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> ans;
        deque<int> dq;
        for(int i=0; i<n; ++i){
            // 如果首元素超过滑动窗口,弹出
            while(!dq.empty() && dq.front() <= i-k){
                dq.pop_front();
            }
            // 构造单调递减队列
            while(!dq.empty() && nums[i] > nums[dq.back()]){
                dq.pop_back();
            }
            dq.push_back(i);
            // 在滑动窗口内每次都要添加最大值
            if(i >= k-1){
                ans.push_back(nums[dq.front()]);
            }
        }

        return ans;
    }
};

剑指 Offer 60. n个骰子的点数

动态规划

        给定 n 个骰子,可得:每个骰子摇到 1 至 6 的概率相等,都为 1/6 。将每个骰子的点数看作独立情况,共有 6^n种「点数组合」。
        n 个骰子「点数和」的范围为 [n,6n] ,数量为 6n−n+1 = 5n+1 种。

        二维dp数组,dp[i][j]:i表示几个骰子,j表示数字之和,dp[i][j]表示概率。
        递推公式为:n个骰子某个数字之和 sum 的概率为n-1个骰子中(sum - 1~6)数字之和的概率之和除以6。
        注意不能超范围,比如两个骰子就不可能有数字之和为1,如果数字之和取2,第二个骰子也取不到3点。

class Solution {
public:
    vector<double> dicesProbability(int n) {
        vector<vector<double>> dp(n+1, vector<double>(6*n + 1, 0));
        // 1个骰子全部数字的概率都是1/6
        for(int i=1; i<=6; ++i){
            dp[1][i] = 1.0 / 6;
        }
        for(int i=2; i<=n; ++i){ //骰子个数
            for(int j=i; j<=6*i; ++j){ //当前骰子个数,能取到的数字和的概率
                for(int k=1; k<=6; ++k){ //第n个骰子取的值
                    if(j-k >= i-1){ //除去第n个骰子,数字和必须大于骰子个数
                        dp[i][j] += dp[i-1][j-k] / 6;
                    }else{
                        break;
                    }
                }
            }
        }
        vector<double> res(5*n+1, 0);
        for(int i=0; i<=5*n; ++i){
            res[i] = dp[n][n+i];
        }
        return res;
    }
};

剑指 Offer 62. 圆圈中最后剩下的数字

队列(超时)

        先把所有元素入队,然后取出对队,判断个数是否达到m,不到加入队尾,到了重新计数。

数学推导(最优解)

class Solution {
public:
    int lastRemaining(int n, int m) {
        queue<int> q;
        for(int i=0; i<n; ++i){
            q.push(i);
        }
        int cnt = 0;
        while (q.size() > 1) {
            int temp = q.front();
            q.pop();
            cnt++;
            if (cnt == m) {
                cnt = 0;
            } else {
                q.push(temp);
            }
        }

        return q.front();
    }
};
class Solution {
public:
    int lastRemaining(int n, int m) {
        int ans = 0;
        // 最后一轮剩下2个人,所以从2开始反推
        for(int i=2; i<=n; ++i){
            ans = (ans + m) % i;
        }
        return ans;
    }
};

121. 买卖股票的最佳时机

买卖股票

        遍历一遍数组,在每一个位置 i 时,记录 i 位置之前所有价格中的最低价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益即可。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int buy = INT_MAX, sell = 0;
        for(int price: prices){
            buy = min(buy, price);
            sell = max(sell, price - buy);
        }
        return sell;
    }
};

剑指 Offer 64. 求1+2+…+n

求和公式

        (n * (n + 1)) / 2

递归

        利用逻辑与的短路特性实现递归终止。

class Solution {
public:
    int sumNums(int n) {
        char arr[n][n+1];
        return sizeof(arr) >> 1;
    }
};
class Solution {
public:
    int sumNums(int n) {
        int sum = n;
        n > 0 && (sum += sumNums(n - 1));
        return sum;
    }
};

剑指 Offer 65. 不用加减乘除做加法

位运算

        &操作获取进位,^操作相当于是不计算进位的求和运算,将&操作的结果<<1,重复操作直到没有进位。

class Solution {
public:
    int add(int a, int b) {
        while( b != 0){
            unsigned int carry = (unsigned int)(a & b) << 1;
            a = a ^ b;
            b = carry;
        }
        return a;
    }
};


剑指 Offer 66. 构建乘积数组

暴力(超时)

        O(n^2)

左右乘积

        O(n),妙

class Solution {
public:
    vector<int> constructArr(vector<int>& a) {
        vector<int> ans;
        for(int i=0; i<a.size(); ++i){
            int tmp = 1;
            for(int j=0; j<a.size(); ++j){
                if(i != j)  tmp *= a[j];
            }
            ans.push_back(tmp);
        }
        return ans;
    }
};
class Solution {
public:
    vector<int> constructArr(vector<int>& a) {
        int n = a.size();
        vector<int> ans(n, 0);
        int tmp = 1;
        for(int i=0; i<n; ++i){
            ans[i] = tmp;
            tmp *= a[i];
        }
        tmp = 1;
        for(int i=n-1; i>=0; --i){
            ans[i] *= tmp;
            tmp *= a[i];
        }
        
        return ans;
    }
};

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

递归

        根据二叉搜索树性质递归。

        函数体内不需要判空节点,以为根据二叉搜索树的性质去寻找,一定不会走到空节点。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root->val > p->val && root->val > q->val){
            return lowestCommonAncestor(root->left, p, q);
        }else if(root->val < p->val && root->val < q->val){
            return lowestCommonAncestor(root->right, p, q);
        }else{
            return root;
        }
    }
};

剑指 Offer 68 - II. 二叉树的最近公共祖先

递归

        若 root 是 p,q 的最近公共祖先 ,则只可能为以下情况之一:

        (1)p 和 q 在 root 的子树中,且分列 root 的异侧(即分别在左、右子树中);

        (2)p = root ,且 q 在 root 的左或右子树中;

        (2)q = root ,且 p 在 root 的左或右子树中;

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(!root || root==p || root==q) return root;
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        if(left && right){
            return root;
        }else if(left){
            return left;
        }else{
            return right;
        }
    }
};

面试题13. 机器人的运动范围

DFS

        经典迷宫问题

class Solution {
public:
    int getSum(int row, int col){
        int sum = 0;
        while(row != 0){
            sum += row % 10;
            row /= 10;
        }
        while(col != 0){
            sum += col % 10;
            col /= 10;
        }
        return sum;
    }

    void movingCountCore(int m, int n, int k, vector<vector<bool>> &visit, int row, int col, int &cnt){
        if(row<0 || col<0 || row>m-1 || col>n-1 || visit[row][col]==true )
            return ;
        if(getSum(row, col) > k){
            visit[row][col] = false;
            return ;
        }
        visit[row][col] = true;
        ++cnt;

        movingCountCore(m, n, k ,visit, row+1, col, cnt);
        movingCountCore(m, n, k ,visit, row-1, col, cnt);
        movingCountCore(m, n, k ,visit, row, col+1, cnt);
        movingCountCore(m, n, k ,visit, row, col-1, cnt);
    }
    
    int movingCount(int m, int n, int k) {
        vector<vector<bool>> visit(m, vector<bool>(n, false));
        int cnt = 0;
        movingCountCore(m, n, k, visit, 0, 0, cnt);
        return cnt;
    }

    
};

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

排序

        此题求拼接起来的最小数字,本质上是一个排序问题。设数组 nums 中任意两数字的字符串为 x 和 y ,则规定排序判断规则 为:

        若拼接字符串 x+y > y+x ,则 x “大于” y ;反之,若 x+y < y+x ,则 x “小于” y ;

        x “小于” y 代表:排序完成后,数组中 x 应在 y 左边;“大于” 则反之。

class Solution {
public:
    string minNumber(vector<int>& nums) {
        vector<string> strs;
        for(int num: nums){
            strs.push_back(to_string(num));
        }
        sort(strs.begin(), strs.end(), [](string& x, string& y){return x+y < y+x;});
        string res;
        for(string str: strs){
            res.append(str);
        }
        return res;
    }
};

面试题59 - II. 队列的最大值

维护一个单调的双端队列

        插入操作虽然看起来有循环,做一个插入操作时最多可能会有 n 次出队操作。但要注意,由于每个数字只会出队一次,因此对于所有的 n 个数字的插入过程,对应的所有出队操作也不会大于 n 次。因此将出队的时间均摊到每个插入操作上,时间复杂度为 O(1)。

class MaxQueue {
public:
    queue<int> q;
    deque<int> dq;

    MaxQueue() {

    }
    
    int max_value() {
        if(dq.empty())  return -1;
        return dq.front();
    }
    
    void push_back(int value) {
        while(!dq.empty() && dq.back() < value){
            dq.pop_back();
        }
        dq.push_back(value);
        q.push(value);
    }
    
    int pop_front() {
        if(q.empty())   return -1;
        int ans = q.front();
        if(ans == dq.front()){
            dq.pop_front();
        }
        q.pop();
        return ans;
    }
};

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue* obj = new MaxQueue();
 * int param_1 = obj->max_value();
 * obj->push_back(value);
 * int param_3 = obj->pop_front();
 */

面试题61. 扑克牌中的顺子

思维

        根据题意,此 5 张牌是顺子的 充分条件如下:

        除大小王外,所有牌无重复 ;
        设此 5 张牌中最大的牌为 max ,最小的牌为 min (大小王除外),则需满足:max − min < 5

class Solution {
public:
    bool isStraight(vector<int>& nums) {
        unordered_set<int> u_set;
        int nmax = 0, nmin = 14;
        for(int num: nums){
            if(num == 0)    continue;
            nmax = max(nmax, num);
            nmin = min(nmin, num);
            if(u_set.find(num) != u_set.end()) return false;
            u_set.insert(num);
        }
        return nmax-nmin < 5;
    }
};

面试题67. 把字符串转换成整数

模拟

        atoi() / stoi()

class Solution {
public:
    int strToInt(string str) {
        int n = str.size();
        if(n == 0)  return 0;
        int flag = 1, signal = 0;
        int i = 0;
        while(i<n && str[i]==' '){ //去掉前面空格
            ++i;
        }
        if(i == n)    return 0; //不能全为空格
        while(i<n && (str[i]=='+' || str[i]=='-')){ //判断数字的+-号,且只能有一个
            if(str[i] == '-')   flag = -1;
            ++i;
            ++signal;
            if(signal > 1)  return 0;
        }

        long long res = 0;
        for(i; i<n; ++i){
            if(str[i]<'0' || str[i]>'9')    break;
            res = res*10 + str[i] - '0';
            if(res > INT_MAX && flag == 1)  return INT_MAX; //正数溢出
            if(res-1 > INT_MAX && flag == -1)  return INT_MIN; //负数溢出
        }

        return flag * res;
    }
};

觉得文章还不错,可以点个小赞赞,支持一下哈!

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值