《剑指offer》第四章 27-38

本文详细解析了《剑指offer》第四章的解题思路,包括通过画图、举例和分解方法来解决二叉树、矩阵、栈和链表等数据结构问题,如二叉树镜像、顺时针打印矩阵、包含min函数的栈等,并提供了具体实现策略。

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

第四章 解决面试题的思路

1.画图让抽象问题形象化

剑指 Offer 27. 二叉树的镜像

请完成一个函数,输入一个二叉树,该函数输出它的镜像。

例如输入:
     4
   /   \
  2     7
 / \   / \
1   3 6   9
镜像输出:
     4
   /   \
  7     2
 / \   / \
9   6 3   1

先序遍历这棵树的每个节点,如果遍历到的节点有子节点,就交换它的两个子节点。

   TreeNode* mirrorTree(TreeNode* root) {
        if(!root) return nullptr;
        root->left = mirrorTree(root->left);
        root->right = mirrorTree(root->right);
        swap(root->left,root->right);
        return root;
    }
剑指 Offer 28. 对称的二叉树

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
    1
   / \
  2   2
 / \ / \
3  4 4  3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
    1
   / \
  2   2
   \   \
   3    3
    bool isSymmetric(TreeNode* root) {
        if(!root) return true;//如果根节点为空,直接返回true
        return isMirror(root->left,root->right);
    }
    bool isMirror(TreeNode * root1,TreeNode *root2){
        if(!root1 && !root2) return true;//需要判空
        if(!root1 || !root2 || root1->val != root2->val) return false;//如果两个节点只有一个为空,一个不为空 或者值不相等,return false
        return isMirror(root1->left,root2->right) && isMirror(root1->right,root2->left);//镜像对称是树A的左儿子 等于B树的右儿子
    }
剑指 Offer 29. 顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

简单但是有些麻烦的题,需要把每一行每一列的遍历情况考虑到。

    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        if(matrix.empty()) return vector<int>{};
        int start = 0;
        int rows = matrix.size(),cols = matrix[0].size();
        vector<int> res;
        while(start*2 < rows && start*2 < cols){//循环继续条件,从左上角(0,0)开始,一圈一圈打印
            printMatrixInCircle(matrix, res, start);
            ++start;
        }
        return res;
    }
    void printMatrixInCircle(vector<vector<int>>& matrix, vector<int> &res, int start){//打印一圈
        int endX = matrix.size() - start -1;//打印这一圈的最大行数
        int endY = matrix[0].size() - start -1;//打印这一圈的最大列数
        for(int i = start; i <= endY; ++i){//第一步从左往右打印,必有
            res.emplace_back(matrix[start][i]);
        }
        if(endX > start){//第二步从上到下打印一列,存在第二步的前提是至少有两行,终止行号大于起始行号
            for(int i = start + 1; i <= endX; ++i){
                res.emplace_back(matrix[i][endY]);
            }
        }
        if(endX > start && endY > start){//第三步是从右到左打印一行,存在第三步的前提是终止列数大于起始列数,终止行数大于起始行数
            for(int i = endY-1; i >= start; --i){
                res.emplace_back(matrix[endX][i]);
            }

        }
        //第四步是从下到上打印一列:存在条件是至少有三行两列,即终止行号比起始行号至少大2
        if(endX > start+1 && endY > start){
            for(int i = endX-1; i > start; --i){
                res.emplace_back(matrix[i][start]);
            }
        }

    }

2.举例让抽象问题具体化

剑指 Offer 30. 包含min函数的栈

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.min();   --> 返回 -2.

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/bao-han-minhan-shu-de-zhan-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

我们每次插入原栈时,都向新栈插入一次原栈里所有值的最小值(新栈栈顶和待插入值中小的那一个);每次从原栈里取出数字时,同样取出新栈的栈顶。这样可以保证每次push和pop的时候两个栈内的元素都一样多,而且不管pop几次,辅助栈栈顶都是数据栈已有元素中的最小值。

class MinStack {
public:
    stack<int> sk,minSk;
    MinStack() {}
    
    void push(int x) {
        sk.push(x);
        if(minSk.empty() || x <= minSk.top() ){
            minSk.push(x);
        }else{
            minSk.push(minSk.top());
        }
    }
    
    void pop() {
        if(sk.empty()) return;
        sk.pop();
        minSk.pop();
    }
    
    int top() {
        return sk.top();
    }
    
    int min() {
        return minSk.top();
    }
};
剑指 Offer 31. 栈的压入、弹出序列

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

输入: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

如果下一个弹出的数字刚好是栈顶数字,那么 直接弹出。如果下一个弹出的数字不在栈顶,我们把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止。如果所有的数字都压入栈了仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列。

    bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
        if(pushed.empty() && popped.empty()) return true;//如果两个都为空,返回true
        if(pushed.empty() || popped.empty() || pushed.size() != popped.size()) return false;//如果只有一个为空,或者数组内元素个数不同,则肯定不是弹出序列。
        stack<int> sk;
        int i = 0;
        for(int j = 0;j < popped.size();++j){
            while(sk.empty() || sk.top() != popped[j]){//如果下一个弹出的数字不在栈顶,我们把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止
                if(i == pushed.size()) return false;//如果所有的数字都压入栈了仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列
                sk.push(pushed[i++]);
                
            }
            sk.pop();//如果下一个弹出的数字刚好是栈顶数字,那么 直接弹出。
        }
        return true;
    }
剑指 Offer 32 - I. 从上到下打印二叉树

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

    3
   / \
  9  20
    /  \
   15   7
返回:[3,9,20,15,7]

层次遍历法。

    vector<int> levelOrder(TreeNode* root) {
        vector<int> res;
        if(!root) return res;
        queue<TreeNode*> qe;
        qe.push(root);
        while(!qe.empty()){
            TreeNode* node = qe.front();
            qe.pop();
            res.emplace_back(node->val);
            if(node->left) qe.push(node->left);
            if(node->right) qe.push(node->right);
        }
        return res;
    }
剑指 Offer 32 - II. 从上到下打印二叉树 II
返回其层次遍历结果:
[
  [3],
  [9,20],
  [15,7]
]
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> res;
        if(!root) return res;
        queue<TreeNode*> qe;
        qe.push(root);
        while(!qe.empty()){
            vector<int> temp;
            int n = qe.size();
            for(int i = 0;i < n; ++i){
                TreeNode * node = qe.front();
                qe.pop();
                temp.emplace_back(node->val);
                if(node->left) qe.push(node->left);
                if(node->right) qe.push(node->right);
            }
            res.emplace_back(temp);
        }
        return res;
    }
剑指 Offer 32 - III. 从上到下打印二叉树 III

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

    3
   / \
  9  20
    /  \
   15   7
   返回其层次遍历结果:
[[3],[20,9],[15,7]]
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> res;
        if(!root) return res;
        queue<TreeNode*> qe;
        qe.push(root);
        int layer = -1;
        while(!qe.empty()){
            vector<int> temp;
            int n = qe.size();
            ++layer;
            for(int i = 0;i < n; ++i){
                TreeNode * node = qe.front();
                qe.pop();
                temp.emplace_back(node->val);
                if(node->left) qe.push(node->left);
                if(node->right) qe.push(node->right);
            }
            if(layer&1 == 1) reverse(temp.begin(),temp.end());//如果是奇数行,在层次遍历的基础上reverse一下
            res.emplace_back(temp);
        }
        return res;
    }
剑指 Offer 33. 二叉搜索树的后序遍历序列

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

     5
    / \
   2   6
  / \
 1   3
输入: [1,3,2,6,5]
输出: true

这道题突破口在利用true和false两个实例模拟判断过程。

    bool verifyPostorder(vector<int>& postorder) {
        if(postorder.empty() || postorder.size() == 1) return true;
        return verifyBST(postorder, 0, postorder.size()-1);
    }
    bool verifyBST(vector<int>& postorder, int start,int end){
        if(start >= end) return true;//如果树只有一个节点,或者无节点,直接返回true
        int root = postorder[end];//最后一个元素为根节点
        int pos = start;
        while(postorder[pos] < root) ++pos;//凡是小于根节点的全部为左子树
        int leftEnd = pos-1;//左子树结束位置
        while(postorder[pos] > root) ++pos;//右子树的值都应该比根节点大
        return pos==end && verifyBST(postorder,start, leftEnd) && verifyBST(postorder, leftEnd+1, end-1);//return 右子树的值都比根节点大 && 左子树为BST && 右子树为BST
    }
剑指 Offer 34. 二叉树中和为某一值的路径

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

给定如下二叉树,以及目标和 target = 22,
             5
            / \
           4   8
          /   / \
         11  13  4
        /  \    / \
       7    2  5   1
返回:[[5,4,11,2],[5,8,4,5]]

当用前序 遍历的方式访问到某一结点时,我们把该结点添加到路径上,并累加 该结点的值。如果该结点为叶结点并且路径中结点值的和刚好等于输 入的整数,则当前的路径符合要求,我们把它打印出来。如果当前结 点不是叶结点,则继续访问它的子结点。当前结点访问结束后,递归 函数将自动回到它的父结点。因此我们在函数退出之前要在路径上删 除当前结点并减去当前结点的值,以确保返回父结点时路径刚好是从 根结点到父结点的路径。

    vector<vector<int>> pathSum(TreeNode* root, int target) {
        vector<vector<int>> res;
        if(!root) return res;
        vector<int> path;
        findPath(root, target, path, res);
        return res;
    }
    void findPath(TreeNode* root, int target, vector<int> &path, vector<vector<int>> &res){
        path.emplace_back(root->val);
        if(root->val == target && !root->left && !root->right){//如果是节点值等于路径和,且是叶子节点,则存储这条路径
            res.emplace_back(path);
        }
        //如果当前节点小于目标值,则继续去子树寻找累加和
        if(root->left) findPath(root->left, target - root->val, path, res);
        if(root->right) findPath(root->right, target - root->val, path, res);
        path.pop_back();//在返回父节点之前,在路径上删除当前节点
    }

3.分解让复杂问题简单化

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

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

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

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

方法一:第一步仍然是复制原始链表上的每个结点N创建N’,然后把这些创建出来的结点用p->Next链接起来。同时我们把<N,N’>的配对信息放到一个哈希表中。第二步还是设置复制链表上每个结点的random。如果在原始链表中结 点N的random指向结点S,那么在复制链表中,对应的N’应该指向S’。相当于用空间换时间。对于有n个结点的链表我们需要 一个大小为O(n)的哈希表,也就是说我们以O(n)的空间消耗把时间复杂度由O(n2)降低到O(n)。

class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
//利用hash表建立原节点和克隆节点的对应关系
Node* copyRandomList(Node* head) {
        if(!head) return head;
        unordered_map<Node*,Node*> hash;//hash[原节点] = 克隆后链表节点
        Node * cur = head;
        while(cur){//利用hash表key表示原节点,val对应克隆后的节点,这里建立了与原节点val相同的节点
            hash[cur] = new Node(cur->val);
            cur = cur->next;
        }
        cur = head;
        while(cur){//建立克隆节点之间的next和random链接关系
            hash[cur]->next = hash[cur->next];//cur的克隆节点的next = cur->next的克隆节点
            hash[cur]->random = hash[cur->random];//cur的克隆节点的random = cur->random的克隆节点
            cur = cur->next;
        }
        return hash[head];//返回头结点的克隆节点
    }

方法二:不用辅助空间的情况下实现O(n) 的时间效率。

第一步仍然是根据原始链表的每个结点N创建对应的N’。这一次,我们把N’链接在N的后面。

image-20210527000104815

第二步设置复制出来的结点的random。假设原始链表上的N的random指向结点S,那么其对应复制出来的N’是N的Next指向的结点,同样S’也是S的Next指向的结点。

第三步把这个长链表拆分成两个链表:把奇数位置的结点用Next链接起来就是原始链表,把偶数位置的结点用Next链接起 来就是复制出来的链表。

   //这道题的核心是 克隆节点的next,random也是对应的克隆节点
    Node* copyRandomList(Node* head){
        cloneNodes(head);//根据原始链表的每个结点N创建对应的N’,把N’链接在N的后面   
        connectRandom(head);//设置复制出来的结点的random
        return reconnect(head);//长链表拆分成两个链表
    }
    void cloneNodes(Node* &head){
        Node* cur = head;
        while(cur){
            Node* cloned = new Node(cur->val);//把克隆节点连接在原节点后面
            cloned->next = cur->next;
            cloned->random = nullptr;
            cur->next = cloned;
            cur = cloned->next;
        }
    }
    void connectRandom(Node* &head){
        Node * cur = head;
        while(cur){
            Node* curCloned = cur->next;//当前节点的克隆节点是 当前节点的下一个
            if(cur->random){//如果当前节点的random存在,那么当前节点的克隆节点 的random是当前节点random的下一个克隆节点
                curCloned->random = cur->random->next;
            }
            cur = curCloned->next;
        }
    }
   Node* reconnect(Node* &head){//把奇数位置的结点用Next链接起来就是原始链表,把偶数位置的结点用Next链接起 来就是复制出来的链表。
        Node* cur = head;
        Node* cloneHead = nullptr;
        Node* curCloned = nullptr;
        if(cur){//将原始链表指针cur迭代到第二个节点处,将复制链表的指针curCloned迭代到第一个节点的克隆节点处
            cloneHead = curCloned = cur->next;
            cur->next = curCloned->next;
            cur = cur->next;
        }
        while(cur){
            curCloned->next = cur->next;
            curCloned = curCloned->next;
            cur->next = curCloned->next;
            cur = cur->next;
        }
        return cloneHead;
    }
剑指 Offer 36. 二叉搜索树与双向链表

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

image-20210527152301135

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

image-20210527152334176

中序遍历算法搜索二叉树后得到是一串递增的序列,所以中序遍历的时候改变其左右指针就能得到递增的双向链表。对于每一层递归来说,根节点的左指针指向其左子树的最大值,即左子树中最右下的结点;右指针指向其右子树的最小值,即右子树中最左下的结点。所以每一层都可以考虑递归。

按照中序遍历的顺序,当我们遍历转换到根结点时,它的左子树已经转换成一个排序的链表了,并且处在链表中的最后一个结点是当前值最大的结点。可以直接把根节点和当前值最大的节点相链接,所以递归函数中传递一个变量来保存当前链表的最大值。

    Node* treeToDoublyList(Node* root) {
        if(!root) return root;//如果root为空,不用处理
        Node* listLast = nullptr;//保存转换后链表的尾结点
        convertNode(root,listLast);
        Node* listHead = listLast;
        while(listHead && listHead->left){//双向链表向左迭代,找到链表的头结点
            listHead = listHead->left;
        }
        listHead->left = listLast;//最后将链表头尾节点相连
        listLast->right = listHead;
        return listHead;
    }
    //中序遍历
    void convertNode(Node* root, Node * & listLast){
        if(!root) return;
        if(root->left) convertNode(root->left, listLast);
        root->left = listLast;//根节点的左指针指向 左子树的最大值 也就是左子树链表的尾部
        if(listLast) listLast->right = root;//左子树链表的尾部的右指针 指向根节点
        listLast = root;//将链表最大值迭代为根节点
        if(root->right) convertNode(root->right, listLast);//然后递归右子树,也就是在右子树中寻找最小值
    }
剑指 Offer 37. 序列化二叉树

请实现两个函数,分别用来序列化和反序列化二叉树。

你可以将以下二叉树:
    1
   / \
  2   3
     / \
    4   5
序列化为 "[1,2,3,null,null,4,5]"

采用前序遍历,从根节点开始。二叉树的序列化也是从根节点开始,那么相应的反序列化如果读出数值就可以重建;每遍历到一个数就+一个,如果为空,就+“NULL,”;反序列化时利用队列,用相同的顺序重建二叉树。

class Codec {
public:
    // Encodes a tree to a single string.
    string str = "";
    string serialize(TreeNode* root) {
        serializeTree(root);
        return str;
    }
    void serializeTree(TreeNode * root){//前序遍历,直接序列化
        if(!root) str += "NULL,";
        else{
            str += to_string(root->val) + ',';
            serializeTree(root->left);
            serializeTree(root->right);      
        }
    }

    // Decodes your encoded data to tree.
    queue<string> qe;
    TreeNode* deserialize(string data) {
        int i = 0, j = 0;//双指针,分别指向逗号分隔字符串的首尾
        while(i < data.size()){
            while(data[i] != ',' && i < data.size()) ++i;//i指向该字符串后的逗号
            string temp = data.substr(j,i-j);//j表示被分隔字符串的首部
            qe.push(temp);
            ++i;//跳过下一个逗号
            j = i;//下一个逗号起始的下一个字符串的第一位
        }
        return deserializeTree();
    }
    TreeNode* deserializeTree(){
        auto t = qe.front();
        qe.pop();
        if(t == "NULL") return nullptr;//前序遍历第一个入队的是根节点,根节点都为空的话,树不存在
        TreeNode* root = new TreeNode(stoi(t));
        root->left = deserializeTree();//左儿子是下一次递归取出来的节点
        root->right = deserializeTree();
        return root;
    }
};
剑指 Offer 38. 字符串的排列

输入一个字符串,打印出该字符串中字符的所有排列。 你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]

与之前排列组合需要push path不同,解法选择将pos的元素和后面的每一个元素进行交换,也能达到全部排列的效果。存在重复元素时则选择用set进行统计,如果出现则跳过当前pos。

    vector<string> permutation(string s) {
        vector<string> res;
        if(s.empty()) return res;
        traceBack(s, res, 0);
        return res;
    }

    void traceBack(string s,vector<string> &res, int pos){
        if(pos == s.size()-1){
            res.emplace_back(s);
            return;
        }
        set<char> st;
        for(int i = pos; i < s.size(); ++i){
            if(st.find(s[i]) != st.end() ) continue;//c存在重复元素,剪枝
            st.insert(s[i]);
            swap(s[i],s[pos]);
            traceBack(s, res, pos+1);
            swap(s[i],s[pos]);
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值