剑指offer(七)

61. 序列化二叉树

297. 二叉树的序列化与反序列化 - 力扣(LeetCode)

直接开抄。

二叉树的序列化最大的问题是如何唯一的表示一棵树

  • 单独的前中后序遍历都无法唯一表示一棵树。(前+ 后这种的才能确定一棵树)
  • 如果要唯一表示,可以采用层序遍历,然后遍历过程中将nullptr节点也记录下来,这里记做null
class Codec {
public:
    // Encodes a tree to a single string.
    string serialize(TreeNode* root) {
        if(root == nullptr) return "null";
        return to_string(root->val) + ',' + serialize(root->left) + ',' + serialize(root->right);
    }

    // Decodes your encoded data to tree.
    TreeNode* deserialize(string data) {
        queue<string> que;
        stringstream ss(data);
        string item;
        while(getline(ss,item,',')){
            que.push(item);
        }
        return deserializeHelper(que);
    }
private:
    TreeNode* deserializeHelper(queue<string>& que){
        string val = que.front();
        que.pop();
        if(val == "null") return nullptr;
        TreeNode* node = new TreeNode(stoi(val));
        node->left = deserializeHelper(que);
        node->right = deserializeHelper(que);
        return node;
    }
};

// Your Codec object will be instantiated and called as such:
// Codec ser, deser;
// TreeNode* ans = deser.deserialize(ser.serialize(root));

62. 二叉搜索树中第k小的元素

230. 二叉搜索树中第K小的元素 - 力扣(LeetCode)

由于是二叉搜索树,中序遍历的顺序就是从小到大的顺序。所以直接用迭代法遍历一次,每遍历一个数k减1,k减为0时就找到了第k小的元素。

class Solution {
public:
    int kthSmallest(TreeNode* root, int k) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur != nullptr || !st.empty()){
            if(cur != nullptr){
                st.push(cur);
                cur = cur->left;
            }else{
                cur = st.top();
                st.pop();
                k--;
                if(k == 0) return cur->val;
                cur = cur->right;
            }
        }
        return -1;
    }
};

63. 数据流中的中位数

数据流中的中位数_牛客题霸_牛客网 (nowcoder.com)

295. 数据流的中位数 - 力扣(LeetCode)

涉及到数据流的题目,牛客和力扣的表现形式好像都不太一致。但本质是一样的。

法一

本分老实人,说求中位数那就求,直接求。但是力扣上有规模特别大的数据,这样搞会超时。

class Solution {
public:
    void Insert(int num) {
        vec.push_back(num);
    }

    double GetMedian() { 
        sort(vec.begin(),vec.end());
        int len = vec.size();
        if(len % 2 == 0){
            return (double)(vec[len / 2] + vec[len / 2 - 1])/ 2;
        }else{
            return vec[len / 2]; 
        }
    }
private:
    vector<int> vec;
};

法二

需要进行优化。法一最大的时间消耗就是每次GetMedian都需要进行排序,数据量大的时候很浪费时间。

  • 如果能在插入数据时,使数组中的元素保持有序就不需要排序了。
  • 可以在插入的时候用二分搜索找到插入位置。

这样优化以后力扣能多过一点数据了,但还是不能全过去,21个用例,过了20个。

class Solution {
public:
    void Insert(int num) {
        int pos = getPos(num);
        vec.insert(vec.begin() + pos, num);
    }

    double GetMedian() { 
        int len = vec.size();
        if(len % 2 == 0){
            return (double)(vec[len / 2] + vec[len / 2 - 1])/ 2;
        }else{
            return vec[len / 2]; 
        }
    }
private:
    vector<int> vec;
    int getPos(int num){
        int left = 0, right = vec.size() - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if(num < vec[mid]) right = mid - 1;
            else if(num > vec[mid]) left = mid + 1;
            else{
                left = mid;
                break;
            }
        }
        return left;
    }
};

法三

继续优化,感觉vector这个数据结构能换一换,每次在vector中间插入元素,从插入位置开始后面所有元素都要移动,这个是O(N)的。

  • 可以用multiset来代替vector,它是基于红黑树实现的,插入时间复杂度是O(logn)。

这个在两个平台都通过了。

class Solution {
public:
    Solution():mid(mst.end()){};
    void Insert(int num) {
        int n = mst.size();
        mst.insert(num);
        if(!n)   // 若multiset为空,设置mid为begin迭代器
            mid = mst.begin();
        else if(num < *mid)  // num小于中位数:若n为奇数,mid不变;若n是偶数,mid向前移动。
            mid = (n & 1) ? mid : prev(mid);
        else        // num大于等于中位数:若n为奇数,mid向后移动;若n是偶数,mid不变
            mid = (n & 1) ? next(mid) : mid;
        
    }

    double GetMedian() { 
        int n = mst.size();
        // 若n是奇数,next(mid,n %2 - 1) 返回mid本身,若n是偶数,则返回mid前一个元素
        return ((double)(*mid) + *next(mid,n %2 - 1)) / 2;
    }
private:
    multiset<int> mst;
    multiset<int>::iterator mid;
};

法四

经典的大小堆算法。

  • 数据结构
    • 用一个小顶堆存储右半部分,right。
    • 用一个大顶堆存储左半部分,left。
  • 数据个数关系:假设元素数量为N。
    • 如果N为偶数,left和right中元素数量相同。
    • 如果N为奇数,right中元素数量要比left中多一。(人为规定,可以反过来)
  • 插入时,假设left中有m个元素,right中有n个元素。
    • m = n:(N为偶数)需要向right中添加一个元素。实现:将新元素num添加到left,再将left堆顶元素插入到right。
      • 不能直接插入到right中,因为num大小未知,若其比较小,则其位置应该在left的中间。
      • 先往小的里面插,然后把小的里面的最大的插到大的里面去。如果一开始就插大的,后来发现很小那就直接错了。
    • m != n:(N为奇数)这时候需要向left里插入一个元素。实现方法:将新元素插入到right,再将right的堆顶元素插入到left。
  • 添加一个数字的时间复杂度是O(logn),查找中位数的时间复杂度是O(1)。
class Solution {
public:
    void Insert(int num) {
        if(left.size() != right.size()){  // 奇数
            right.push(num);
            left.push(right.top());
            right.pop();
        }else{      // 偶数
            left.push(num);
            right.push(left.top());
            left.pop();
        }
    }

    double GetMedian() { 
        return (right.size() != left.size()) ? right.top() : (double)(right.top() + left.top()) / 2;
    }
private:
    priority_queue<int,vector<int>, greater<int>> right;  // 小顶堆,数组右半边
    priority_queue<int,vector<int>,less<int>> left;   // 大顶堆,数组左半边
};

64. 滑动窗口的最大值

滑动窗口的最大值_牛客题霸_牛客网 (nowcoder.com)

239. 滑动窗口最大值 - 力扣(LeetCode)

法一

  • 滑动窗口需要从左边删除数据,还需要从右边插入数据,所以用双端队列最为合适。
  • 还需要动态求最大值,可以想到“求下一个最大的值”的单调栈思想。
    • 对于窗口中的两个位置i和j。i< j,如果i对应的元素不大于j对应的元素。那么在窗口滑动的过程中,只要i还存在,nums[i]一定不是滑动窗口中最大值。(在队列中遍历时只需要保留一个最大值就好了)。这时候i这个位置是没必要保存的。
  • 实现原理:
    1. 使用deque存储有潜力成为最大值的元素索引
    2. 遍历nums,遍历过程中
      1. 先检查头部元素(存的是索引),若其与当前元素索引之差等于窗口大小说明该元素已经不再窗口里了,需要移除。——这就是在插入元素时,只把比当前元素小的元素pop出去的原因。如果插入元素比队列中元素小,那么一旦删除了队头元素,该元素就可能成为窗口中的最大值。
      2. 之后检查尾部元素,将队列中索引值对应的元素值小于当前元素的都pop出去,既然比当前元素小,只要当前元素在窗口内,这些元素永无出头之日。
      3. 将当前元素插入队列尾部。
    3. 继续遍历nums,知道遍历完整个数组。遍历过程中,不要忘记存储最大值,用作最后返回。
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        deque<int> deq;
        vector<int> ans;
        for(int i = 0;i<nums.size();i++){
            // 移除不在滑动窗口内的元素索引
            if(!deq.empty() && deq.front() == i - k) deq.pop_front();
            // 移除队列中小于当前元素nums[i]的索引
            // 队列中元素代表滑动窗口内元素,若是其小于nums[i]表示其肯定不可能是最大值了
            while(!deq.empty() && nums[deq.back()] < nums[i]) deq.pop_back();
            // 当前元素加入队列
            deq.push_back(i);
            // 窗口形成后,将最大值(队列头部)加入结果
            if(i >= k - 1){
                ans.push_back(nums[deq.front()]);
            }
        }
        return ans;
    }
};

法二

相当于从第k个元素开始不断求当前窗口内的最大值。

  • 很容易可以想到用优先队列可以动态维护一个最大值。但是,优先队列无法记录当前最大值在原数组中是第几个。
  • 优先队列中的元素用pair<元素,索引>,这样就可以在动态维护最大值的同时,记录当前最大值对应的索引元素,在该索引下的元素滑出窗口时将其出队。
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        priority_queue<pair<int,int>,vector<pair<int,int>>,less<pair<int,int>>> pq;  //大顶堆,存放{元素,索引}
        for(int i = 0;i<k;i++) pq.push({nums[i],i}); // 把前n个元素放入大顶堆
        vector<int> ans = {pq.top().first}; // 直接将第一个元素放入结果数组
        for(int i = k;i<nums.size();i++){
            pq.push({nums[i],i});
            while(pq.top().second < i - k + 1) 
								pq.pop();  // 将已经不在窗口内的最大值去除
            ans.push_back(pq.top().first);
        }
        return ans;
    }
};

65. 矩阵中的路径

矩阵中的路径_牛客题霸_牛客网 (nowcoder.com)

79. 单词搜索 - 力扣(LeetCode)

直接暴力回溯。但是牛客上给出的代码模板没有用vector,只有char*的字符串,一个比较好的方法就是拿到以前先将其转换成vector<vector<char>>,有STL为啥不用呢。

class Solution {
public:
    bool check(vector<vector<char>>& board,const string& word,int i ,int j ,int k){
        if(i >= board.size() || i < 0
            || j >= board[0].size() || j < 0
            || word[k] != board[i][j]) return false;  // 排除越界和字母不相等的情况
        if(k == word.size() - 1) return true;  // 找到单词了,直接返回true
        board[i][j] = '\0'; // 同一单元格的字母不允许重复使用,所以已经用过的要标记一下
        bool ans = check(board,word,i + 1,j,k + 1) || check(board,word,i,j + 1,k + 1)
                || check(board,word,i - 1,j, k + 1) || check(board,word,i,j - 1,k + 1);
        board[i][j] = word[k]; // 恢复现场,本轮遍历已经结果,要把原来的数组恢复到原来的样子否则会影响下一次判断
        return ans; 
    }
    bool exist(vector<vector<char>>& board, string word) {
        int h = board.size(), w = board[0].size();
        for(int i = 0;i<h;i++)
            for(int j = 0;j<w;j++)
                if(check(board,word,i,j,0)) return true;
        return false;
    }
};

66. 机器人的运动范围

机器人的运动范围_牛客题霸_牛客网 (nowcoder.com)

LCR 130. 衣橱整理 - 力扣(LeetCode)

力扣上的题目不一样,但就是换了个皮,内核一样,还是直接暴力。

class Solution {
public:
    int getDigit(int x){
        int sum =  0;
        while(x){
            sum += x % 10 ;
            x /= 10;
        }
        return sum;
    }
    int dfs(int i,int j,int m,int n,int cnt,vector<vector<bool>>& visited){
        if(i < 0 || i >= m || j < 0 || j >= n
        || visited[i][j]   // has visited
        || getDigit(i) + getDigit(j) > cnt)  // don't need calc 
            return false;
        visited[i][j] = true;
        return 1 +  dfs(i + 1,j,m,n,cnt,visited) + dfs(i ,j + 1,m ,n,cnt,visited) + 
                    dfs(i- 1,j,m,n,cnt,visited) + dfs(i ,j - 1,m,n,cnt,visited);
    }
    int wardrobeFinishing(int m, int n, int cnt) {
        vector<vector<bool>> visited(m,vector<bool>(n,false));
        return dfs(0,0,m,n,cnt,visited);
    }
};

67. 剪绳子

剪绳子_牛客题霸_牛客网 (nowcoder.com)

343. 整数拆分 - 力扣(LeetCode)

法一

使用dp。

  • 定义dp[i]表示拆分i后得到的最大乘积。
  • 递推公式:dp[i] = max(dp[i],dp[j]*dp[i - j])
    • 表面上看只是拆分为了两部分,但是dp[i]代表的是i拆分后得到的最大乘积而不是i本身。
    • 其代表的不是一个数而是一个最大乘积积,是一个或多个数的乘积。例如,dp[8]可以拆分为dp[1]dp[7]。而dp[7]并不等于7,其等于dp[3]*dp[4]
  • 初始化。初始化过程中要首先2和3需要特殊判定。因为这两个数拆分后的乘积是比自己小的。所以其需要单独return,而在dp数组中dp[2]dp[3]都应该以2和3的身份出现
    • 因为题目要求至少拆分为两项,在递推公式中已经保证了将整数至少拆分为两项。
    • 所以,这里2和3这里不需要拆分,直接以本身的形式出现保证最后取得的乘积最大。
class Solution {
public:
    int integerBreak(int n) {
        if(n == 2) return 1;
        if(n == 3) return 2;
        vector<int> dp(n + 1);
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 3;
        for(int i = 3;i<=n;i++){
            for(int j = 1;j <= i / 2;j++){  // 拆分后的所有结果是对称的只要取一半就行了
                dp[i] = max(dp[i],dp[j] * dp[i - j]);
            }
        }
        return dp[n];
    }
};

法二

法一的写法很直观,但对于2和3需要有个特判,不太统一显得不是特别优雅。

此外,对于dp[2]dp[3]表示的也不是dp[i]拆分后的最大乘积了,与最初dp[i]的定义略有不符,所以有点不太美观。

其实,上面这些不统一的本质在于2和3拆分后的乘积比本身要小。所以,我们递推公式写成这样dp[i] = max(dp[i],max(j* (i - j),j * dp[i - j]));

  • 其中j * (i - j)是单纯将i拆分为两个整数来乘,j * dp[i - j]则是拆分为两个以及两个以上是数来成。而j本身又考虑了所有可能的情况,所以已经覆盖了所有可能的情况。
  • 注意j * dp[i - j]不能写成dp[j]* dp[i - j]。因为改完后貌似将j又进一步进行了拆分考虑的情况更完全了,但其在面对j > dp[j]情况时得到的结果是比j * dp[i - j]小的就会忽略掉真正的最大乘积。
class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n + 1);
        dp[2] = 1;
        for(int i = 3;i<=n;i++){
            for(int j = 1;j<= i / 2;j++){
                dp[i] = max(dp[i],max(j* (i - j),j * dp[i - j]));
            }
        }
        return dp[n];
    }
};
  • 15
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

记与思

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值