剑指 Offer 31-40

该篇博客详细介绍了《剑指 Offer》书中第 31 至 40 题的解题思路,涵盖了二叉树的层次遍历、后序遍历序列判断、复杂链表复制、二叉搜索树与双向链表转化等问题,以及相关算法和数据结构的使用,如单调栈、哈希表和摩尔投票法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

剑指 Offer 31. 栈的压入、弹出序列

难度中等62

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

 

示例 1:

输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1

示例 2:

输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。
class Solution {
public:
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
        stack<int> s;
        //做模拟???
        int left=0;
        for(int i=0;i<pushed.size();i++){
            s.push(pushed[i]);
            while(!s.empty()&&s.top()==popped[left]){
                s.pop();left++;
            }
        }
        if(left!=pushed.size()) return false;
        return true;
        
    }
};

剑指 Offer 32 - I. 从上到下打印二叉树

难度中等33

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

 

例如:
给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回:

[3,9,20,15,7]
vector<int> levelOrder(TreeNode* root) {
        vector<int> ans;
        queue<TreeNode*> q;
        if(root==NULL) return ans;
        q.push(root);
        while(!q.empty()){
            TreeNode* now=q.front();q.pop();
            ans.push_back(now->val);
            if(now->left!=NULL) q.push(now->left);
            if(now->right!=NULL) q.push(now->right);
        }
        return ans;
    }

剑指 Offer 32 - II. 从上到下打印二叉树 II

难度简单41

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。

 

例如:
给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其层次遍历结果:

[
  [3],
  [9,20],
  [15,7]
]
/**
 * 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:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        queue<TreeNode*> q;
        if(root==NULL) return ans;
        q.push(root);
        vector<int> tmp;
        while(!q.empty()){
            int size=q.size();tmp.clear();
            while(size--){
                TreeNode* now=q.front();q.pop();
                tmp.push_back(now->val);
                if(now->left!=NULL) q.push(now->left);
                if(now->right!=NULL) q.push(now->right);
            }
            ans.push_back(tmp);
        }
        return ans;
    }
};

剑指 Offer 32 - III. 从上到下打印二叉树 III

难度中等38

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

 

例如:
给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其层次遍历结果:

[
  [3],
  [20,9],
  [15,7]
]

也可以用双端队列,

奇偶层的打印顺序不一样是相反的,可以利用层数偶数与否调用reverse来解决,但是海量数据时这个效率很低,不推荐。
因为奇数层的打印顺序是从左到右,偶数层的打印顺序是从右到左,可以利用STL容器deque中
push_back(),push_front(),front(),back(),pop(),popfront()来实现

 vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        queue<TreeNode*> q;
        if(root==NULL) return ans;
        q.push(root);
        vector<int> tmp;
        int cnt=0;
        
        while(!q.empty()){
            int size=q.size();tmp.clear();
            while(size--){
                TreeNode* now=q.front();q.pop();
                // ans[cnt].push_back(now->val);
                tmp.push_back(now->val);
                if(now->left!=NULL) q.push(now->left);
                if(now->right!=NULL) q.push(now->right);
            }
            cnt++;
            if(cnt%2==0) reverse(tmp.begin(),tmp.end());
            ans.push_back(tmp);
        }
        return ans;
    }

剑指 Offer 33. 二叉搜索树的后序遍历序列

难度中等93

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

 

参考以下这颗二叉搜索树:

     5
    / \
   2   6
  / \
 1   3

示例 1:

输入: [1,6,3,2,5]
输出: false

示例 2:

输入: [1,3,2,6,5]
输出: true
class Solution {
public:
    bool verify(vector<int> postorder,int l,int r){
        if(l>=r) return true;
        int root=postorder[r];
        int i=l;//左子树
        while(i<r&&postorder[i]<postorder[r]) i++;
        int tmp=i;
        //l 到 i-1为左子树
        while(i<r){
            if(postorder[i]<root) return false;
            i++;
        }
        return verify(postorder,l,tmp-1)&&verify(postorder,tmp,r-1);
    }
    bool verifyPostorder(vector<int>& postorder) {
        return verify(postorder,0,postorder.size()-1);

    }
};

单调栈解决:

class Solution {
    public boolean verifyPostorder(int[] postorder) {
        Stack<Integer> stack = new Stack<>();
        int root = Integer.MAX_VALUE;
        for(int i = postorder.length - 1; i >= 0; i--) {
            if(postorder[i] > root) return false;
            while(!stack.isEmpty() && stack.peek() > postorder[i])
            	root = stack.pop();
            stack.add(postorder[i]);
        }
        return true;
    }
}

剑指 Offer 34. 二叉树中和为某一值的路径

难度中等69

输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。

 

示例:
给定如下二叉树,以及目标和 sum = 22

              5
             / \
            4   8
           /   / \
          11  13  4
         /  \    / \
        7    2  5   1

返回:

[
   [5,4,11,2],
   [5,8,4,5]
]

    void get(vector<int> temp,int nowsum,int sum,vector<vector<int>> &ans,TreeNode* root){
        if(root==NULL) return;
        temp.push_back(root->val);
        nowsum+=root->val;
        if(root->left==NULL&&root->right==NULL&&nowsum==sum){
            ans.push_back(temp);
            return;
        }
        get(temp,nowsum,sum,ans,root->left);
        get(temp,nowsum,sum,ans,root->right);
        temp.pop_back();
        
    }
    vector<vector<int>> pathSum(TreeNode* root, int sum) {
        // 从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
        vector<vector<int>> ans;
        vector<int> temp;
        get(temp,0,sum,ans,root);
        return ans;
    }

剑指 Offer 35. 复杂链表的复制

难度中等80

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null

 

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例 3:

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

示例 4:

输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。
看不懂先跳过

剑指 Offer 36. 二叉搜索树与双向链表

难度中等86

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

 

为了让您更好地理解问题,以下面的二叉搜索树为例:

 

 

我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。

特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。

先跳过、

剑指 Offer 38. 字符串的排列

难度中等79

输入一个字符串,打印出该字符串中字符的所有排列。

 

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

 

示例:

输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
//回溯、交换、剪枝的做法
class Solution {
public:
    vector<string> ans;
    void dfs(int x,string s){
        if(x==s.length()-1){
            ans.push_back(s);return;
        }
        unordered_map<char,int> mp;
        for(int i=x;i<=s.length()-1;i++){
            if(mp.count(s[i])) continue;
            mp[s[i]]=1;
            swap(s[i],s[x]);
            dfs(x+1,s);
            swap(s[i],s[x]);
        }
    }
    vector<string> permutation(string s) {
        dfs(0,s);
        return ans;
    }
};

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

难度简单49

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

 

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

 

示例 1:

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

本题常见解法如下:

哈希表统计法: 遍历数组 nums ,用 HashMap 统计各数字的数量,最终超过数组长度一半的数字则为众数。此方法时间和空间复杂度均为 O(N)O(N)O(N) 。
数组排序法: 将数组 nums 排序,由于众数的数量超过数组长度一半,因此 数组中点的元素 一定为众数。此方法时间复杂度 O(Nlog2N)O(N log_2 N)O(Nlog2​N)。
摩尔投票法: 核心理念为 “正负抵消” ;时间和空间复杂度分别为 O(N)O(N)O(N) 和 O(1)O(1)O(1) ;是本题的最佳解法。

作者:jyd
链接:https://leetcode-cn.com/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/solution/mian-shi-ti-39-shu-zu-zhong-chu-xian-ci-shu-chao-3/

//主要是因为出现了超过一半.
    int majorityElement(vector<int>& nums) {
        int count = 1, cur = nums[0];
        for(int i = 1; i < nums.size(); i++) {
            if(cur != nums[i]) {
                --count;
                if(count < 0) {
                    cur = nums[i];
                    count = 1;
                }
            }
            else ++count;
        }
        return cur;

    }

剑指 Offer 40. 最小的k个数

难度简单119

输入整数数组 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

我们用一个大根堆实时维护数组的前 k小值。首先将前 k个数插入大根堆中,随后从第 k+1个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后将大根堆里的数存入数组返回即可。在下面的代码中,由于 C++ 语言中的堆(即优先队列)为大根堆,我们可以这么做。而 Python 语言中的对为小根堆,因此我们要对数组中所有的数取其相反数,才能使用小根堆维护前 k小值。

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        vector<int>vec(k, 0);
        if (k == 0) return vec; // 排除 0 的情况
        priority_queue<int>Q;
        for (int i = 0; i < k; ++i) Q.push(arr[i]);
        for (int i = k; i < (int)arr.size(); ++i) {
            if (Q.top() > arr[i]) {
                Q.pop();
                Q.push(arr[i]);
            }
        }
        for (int i = 0; i < k; ++i) {
            vec[i] = Q.top();
            Q.pop();
        }
        return vec;
    }
};

 

class Solution {
public:
    //快排  时间复杂度O(N)

    int partition(int left,int right,vector<int> &nums){
        int tmp=left;
        int pivot=nums[left];
        //从left+1开始
        for(int i=left+1;i<=right;i++){
            if(nums[i]<pivot){
                tmp++;//在前面...
                swap(nums[i],nums[tmp]);
            }
        }
        //最后呢pivot的位置就是left
        swap(nums[tmp],nums[left]);
        return tmp;
    }
    void quicksort(vector<int> &nums,int left,int right,int k) {
        int j=partition(left,right,nums);
        if(j==k) return;
        else if(k<j)
            quicksort(nums,left,j-1,k);
        else
            quicksort(nums,j+1,right,k);
    }
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        vector<int> res(k);
        if (k == 0) {
            return res;
        } else if (arr.size() <= k) {
            return arr;
        }
        quicksort(arr, 0, arr.size()- 1, k);
        // 数组的前 k 个数此时就是最小的 k 个数,将其存入结果
        for (int i = 0; i < k; i++)
            res[i] = arr[i];
        
        return res;
    }

};

在面试中,另一个常常问的问题就是这两种方法有何优劣。看起来分治法的快速选择算法的时间、空间复杂度都优于使用堆的方法,但是要注意到快速选择算法的几点局限性:
第一,算法需要修改原数组,如果原数组不能修改的话,还需要拷贝一份数组,空间复杂度就上去了。
第二,算法需要保存所有的数据。如果把数据看成输入流的话,使用堆的方法是来一个处理一个,不需要保存数据,只需要保存 k 个元素的最大堆。而快速选择的方法需要先保存下来所有的数据,再运行算法。当数据量非常大的时候,甚至内存都放不下的时候,就麻烦了。所以当数据量大的时候还是用基于堆的方法比较好。

作者:nettee
链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/tu-jie-top-k-wen-ti-de-liang-chong-jie-fa-you-lie-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

class Solution {
public:
//     left_index = parent_index * 2 + 1;
// right_index = parent_index * 2 + 2;

// parent_index = (left_index - 1) / 2;
// parent_index = (right_index - 1) / 2;

//本题用到的是最大堆啊!!!!!!!!!!!!!!!!!!!!!
// 对于比较大的值,做下沉操作,得到我们的最小堆(从非叶子结点开始)

void downAdjust(vector<int>& nums,int index){
    int j=index*2+1;
    int parent=nums[index];
    while(j<nums.size()){
        //较小的上浮
        if(j+1<nums.size()&&nums[j+1]>nums[j]) j=j+1;
        if(parent>=nums[j]) break;
        // cout<<nums[index]<<" "<<nums[j];
        nums[index]=nums[j];
        index=j;
        j=2*index+1;
    }
    nums[index]=parent;
    //也可以避免重复交换,也就是在最后确认parent的坐标后再写nums[index]=parent;
}
void buildHeap(vector<int> &nums){
    for(int i= nums.size()/ 2;i>=0;i--) downAdjust(nums,i);
}

vector<int> getLeastNumbers(vector<int>& arr, int k) {
        if(k == 0) return vector<int>();
        vector<int> max_heap_vec(arr.begin(), arr.begin()+k);
        buildHeap(max_heap_vec);
        for(int i=0;i<k;i++) cout<<" "<<max_heap_vec[i];
        for(int i = k; i<arr.size(); ++i){
            // 出现比堆顶元素小的值, 置换堆顶元素, 并调整堆
            if(arr[i] < max_heap_vec[0]){
                max_heap_vec[0]=arr[i];
                downAdjust(max_heap_vec,0);
            }
        }
        return max_heap_vec;
}

};
/*
class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        if(k == 0) return vector<int>();
        vector<int> max_heap_vec(arr.begin(), arr.begin()+k);
        buildMaxHeap(max_heap_vec);
        for(int i = k; i<arr.size(); ++i){
            // 出现比堆顶元素小的值, 置换堆顶元素, 并调整堆
            if(arr[i] < max_heap_vec[0]){
                emplacePeek(max_heap_vec, arr[i]);
            }
        }
        return max_heap_vec;
    }
private:
    void buildMaxHeap(vector<int>& v){
        // 所有非叶子节点从后往前依次下沉
        for(int i = (v.size()-2) / 2; i>=0; --i){
            downAdjust(v, i);
        }
    }

    void downAdjust(vector<int>& v, int index){
        int parent = v[index];
        // 左孩子节点索引
        int child_index = 2*index + 1;
        while(child_index < v.size()){
            // 判断是否存在右孩子, 并选出较大的节点
            if(child_index + 1 < v.size() && v[child_index+1] > v[child_index]){
                child_index += 1;
            }
            // 判断父节点和子节点的大小关系
            if(parent >= v[child_index]) break;
            // 较大节点上浮
            v[index] = v[child_index];
            index = child_index;
            child_index = 2*index + 1;
        }
        // 避免重复交换的无用工作
        v[index] = parent;
    }

    void emplacePeek(vector<int>& v, int val){
        v[0] = val;
        downAdjust(v, 0);
    }
};
*/

应该面试常问的问题吧:十亿个杂乱无章的数中找出前1000个最小的数

我当时先给出的回答是分块处理,例如一亿数据为一组,找出一组中的前1000个最小的数,然后再在10*1000个数据中找出前1000个最小的数。
面试官问怎么找出一亿数据中的Top1000?我先回答说排序,用快排。
面试官说复杂度有些高呀,可以优化么?我想了一下,只需要找出前1000个数,并不要求有序,排序肯定是不必要的,但总不能遍历1000次数组找每次的最小值吧。虽然不合理,但是避免冷场,我还是说出来了。
面试官说,也算是一种方法,还可以优化么。我首先想到的是,设置1000大小的窗口,当每次来一个数的时候,判断它和窗口的上边界的大小再去插入,但是有一个问题就是这个数据如何插入。因为输出结果并不要求有序,所以需要一种数据结构可以每次插入之后自动调整出最大值
呐,这不就是最大堆的性质么,然后我就哇啦哇啦的说了一堆,下面再介绍,也不知道面试官是否满意它也没说话。

作者:zuo-10
链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/c-dui-pai-xu-jie-jue-topkwen-ti-pei-tu-by-zuo-10/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值