《剑指Offer(第二版)》03-10II解题思路

本文详细介绍了数组中重复数字的查找、二维数组中的查找、字符串空格替换、从尾到头打印链表、重建二叉树、用两个栈实现队列、斐波那契数列以及青蛙跳台阶问题的解决方案。通过具体的代码实现,阐述了如何利用数据结构和算法高效解决问题。

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

03.数组中的重复数字

找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

思路
其实思路很简单,数组查重直接调用HashSet即可,遍历数组一遍,如果set中不包含该元素,就插入,如果包含,即为重复的。


class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        set<int> s;
        int a = 0;
        for (int i : nums) {
            if (s.count(i) == 0){
                s.insert(i);
            }else{
                a = i;
                break;
            }
        }
        return a;
    }
};

04.二维数组中的查找

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路
本题要利用到排序的特性,对于一个给定的数,从矩阵中的每一个角开始尝试,探索规律,发现从右上角开始缩范围即可,例如矩阵
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]

target = 5
从右上角15开始查询,比5大,则根据矩阵特性,我们可以删除矩阵最后一列,因为这些数都必然比5大,删除后右上角变为11,还是比5大,再删除11所在的一列,同理删除7所在的一列,右上角元素变为4,比5小,根据矩阵特性,删除4所在的一行,因为这行元素必然都比5小,以此类推。
故解决方法是从右上角元素查起,比target大就删除这一列,比target小就删除这一行。逐渐缩小范围。

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        if (matrix.empty()||matrix[0].empty()){
            return false;
        }
        int rows = matrix.size(),columns = matrix[0].size();
        int r = 0,c = columns - 1;
        while (r < rows && c >= 0){
            int find = matrix[r][c];
            if (find == target){
                return true;
            }else if(find > target){
                c--;
            }else{
                r++;
            }
        }
        return false;
    }
};

05.替换空格

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

思路
如果不是原地修改,新开一个字符串就好了

class Solution {
public:
    string replaceSpace(string s) {
        string ans = "";
        for (int i = 0; i < s.size(); ++i) {
            if(s[i] == ' '){
                ans += "%20";
            }else{
                ans += s[i];
            }
        }
        return ans;
    }
};

如果是原地修改,那原字符数组肯定足够大,采用双指针方法即可,第一个指针p1指向原字符串结尾(’\0’),第二个指针p2指向替换后的字符串结尾的地方(通过计算得出),一个个复制过去,直到p1碰到空格,p1碰到空格后,将p1往前挪一位,p2前插入"%20",再将p2往前挪3位,以此类推直至p1和p2相遇即可。

06.从尾到头打印链表

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

思路
借助栈即可。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        stack<ListNode*> s;
        ListNode* p = head;
        while (p){
            s.push(p);
            p = p->next;
        }
        vector<int> ans;
        while(!s.empty()){
            ans.push_back(s.top()->val);
            s.pop();
        }
        return ans;
    }
};

07.重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如,给出

前序遍历 preorder = [3,9,20,15,7] 中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树:

思路
preorder[0]一定是根节点,而根据中序遍历找到这个根节点可以给出左子树和右子树的中序遍历,这样递归处理,需要借助另一个函数传入preorder和inorder的左右端点索引。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    
    private Map<Integer,Integer> map;

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        int n = preorder.length;
        //利用一个map储存根节点在inorder中的索引 方便算出左右子树的长度
        map = new HashMap<Integer, Integer>();
        for (int i = 0; i < n; i++) {
            map.put(inorder[i],i);
        }
        return myBuildTree(preorder,inorder,0,n - 1,0,n - 1);
    }
    private TreeNode myBuildTree(int[] preorder,int[] inorder,int preLeft,int preRight,int inLeft,int inRight){
        if(preLeft > preRight){
            return null;
        }
        int preRoot = preLeft;
        int inRoot = map.get(preorder[preRoot]);
        TreeNode root = new TreeNode(preorder[preRoot]);
        int leftSubTreeSize = inRoot - inLeft;
        root.left = myBuildTree(preorder,inorder,preLeft + 1,preLeft + leftSubTreeSize,inLeft,inRoot - 1);
        root.right = myBuildTree(preorder,inorder,preLeft +leftSubTreeSize + 1,preRight,inRoot + 1,inRight);
        return root;
    }
}

09.用两个栈实现队列

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead
,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

思路
维护两个栈,第一个栈支持插入操作,第二个栈支持删除操作。
根据栈先进后出的特性,我们每次往第一个栈里插入元素后,第一个栈的底部元素是最后插入的元素,第一个栈的顶部元素是下一个待删除的元素。为了维护队列先进先出的特性,我们引入第二个栈,用第二个栈维护待删除的元素,在执行删除操作的时候我们首先看下第二个栈是否为空。如果为空,我们将第一个栈里的元素一个个弹出插入到第二个栈里,这样第二个栈里元素的顺序就是待删除的元素的顺序,要执行删除操作的时候我们直接弹出第二个栈的元素返回即可。
成员变量
维护两个栈 stack1 和 stack2,其中 stack1 支持插入操作,stack2 支持删除操作
构造方法
初始化 stack1 和 stack2 为空
插入元素
插入元素对应方法 appendTail
stack1 直接插入元素
删除元素
删除元素对应方法 deleteHead
如果 stack2 为空,则将 stack1 里的所有元素弹出插入到 stack2 里
如果 stack2 仍为空,则返回 -1,否则从 stack2 弹出一个元素并返回

class CQueue {
    stack<int> s1,s2;
public:
    CQueue() {
        while(!s1.empty()){
            s1.pop();
        }
        while(!s2.empty()){
            s2.pop();
        }
    }
    void appendTail(int value) {
        s1.push(value);
    }

    int deleteHead() {
        if(s1.empty()&&s2.empty()){
            return -1;
        }
        if(s1.empty()){
            int a = s2.top();
            s2.pop();
            return a;
        } 
        else{
            if(!s2.empty()){
                int a = s2.top();
                s2.pop();
                return a; 
            }else{
                while (!s1.empty()){
                    s2.push(s1.top());
                    s1.pop();
                }
                int a = s2.top();
                s2.pop();
                return a;
            }
        }
    }
};


/**
 * Your CQueue object will be instantiated and called as such:
 * CQueue* obj = new CQueue();
 * obj->appendTail(value);
 * int param_2 = obj->deleteHead();
 */

10-I.斐波那契数列

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中 N > 1. 斐波那契数列由 0 和
1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

思路
先考虑N为0或1的情况,然后N大于等于2时,维护两个变量a和b从F(0)和F(1)开始,维护一个新的变量c=a+b,然后用循环让这三个数滚动赋值,注意计算过程中取模即可。这样的方法应该是时间复杂度和空间复杂度都很低的做法。

class Solution {
public:
    int fib(int n) {
        if( n == 0 || n == 1) {
            return n;
        }
        int a = 0, b = 1,count = 0;
        int c = 0;
        while (count < n - 1){
            c = (a + b)%1000000007;
            a = b;
            b = c;
            count++;
        }
        return c;
    }
};

10-II. 青蛙跳台阶问题

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+71000000007),如计算初始结果为:1000000008,请返回 1

思路
动态规划求出状态转移方程,对于n级台阶,考虑其跳法f(n),如果上一级在n-1阶则只能迈一个台阶,方法数为f(n-1),如果上一级在n-2阶,则只能迈两个台阶,没有别的方式,则f(n)=f(n-1)+f(n-2),发现就是斐波那契数列,直接用上题思路即可。

class Solution {
public:
    int numWays(int n) {
    if( n == 0 || n == 1) {
        return 1;
    }
    int a = 1, b = 1,count = 0;
    int c = 0;
    while (count < n - 1){
         c = (a + b)%1000000007;
         a = b;
         b = c;
         count++;
    }
     return c;
    }
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值