LeetCode剑指offer(中等难度题)

剑指 Offer 49. 丑数

解题思路:首先我们利用的思路是丑数的递推性质:因为丑数只包含因子2,3,5,所以可以有一个规律就是:“丑数 = 某较小丑数 * 某因子”。假如我们要求的下个丑数为Xn+1,则Xn+1必定出现在Xa * 2,Xb * 3,Xc * 5这三个数中的某一个,也就是较小的一个

因此,我们可以使用动态规划的解题方法。首先我们可以设置指针a,b,c指向首个丑数(即1),利用上述的循环递推公式依次得到下一个丑数,并每轮将对应指针执行+1操作即可。
动态规划解析:

  • 状态定义:设动态规划列表dp,dp[i]代表第i+1个丑数
  • 每轮计算后需要更新索引a,b,c的值,使其始终满足方程条件。实现方法:分别独立判断dp[i]和dp[a] * 2,dp[b] * 3,dp[c] * 5的大小关系,若相等则将对应索引a,b,c➕1。不管有几个索引相等都将索引➕1
  • 初始状态:dp[0] = 1,即第一个丑数为1
  • 返回值:dp[n -1],即返回第n个丑数
  • 时间复杂度 O(N) : 其中N=n ,动态规划需遍历计算 dp 列表。
  • 空间复杂度 O(N): 长度为N 的 dp 列表使用 O(N) 的额外空间。
class Solution {
public:
    int nthUglyNumber(int n) {
        int a = 0, b = 0, c = 0;
        int dp[n];
        dp[0] = 1;
        for(int i = 1; i < n; i++) {
            int n2 = dp[a] * 2, n3 = dp[b] * 3, n5 = dp[c] * 5;
            dp[i] = min(min(n2, n3), n5);
            if(dp[i] == n2) a++;
            if(dp[i] == n3) b++;
            if(dp[i] == n5) c++;
        }
        return dp[n - 1];
    }
};

剑指 Offer 07. 重建二叉树

二叉树前序遍历的顺序为:

  • 先遍历根节点
  • 随后递归地遍历左子树
  • 最后递归地遍历右子树

二叉树中序遍历的顺序为:

  • 先递归地遍历左子树
  • 随后遍历根节点
  • 最后递归地遍历右子树

所以我们可以通过前序遍历先得到根节点,然后在中序遍历中以根节点为界线,我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,这样以来我们就知道了左子树的前序遍历和中序遍历结果,以及右子树的前序遍历和中序遍历结果,我们就可以递归地构造出左子树和右子树,再将这两颗子树接到根节点的左右
我们可以使用哈希表来帮助我们快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置。在构造二叉树的过程之前,我们可以对中序遍历的列表进行一遍扫描,就可以构造出这个哈希映射。在此后构造二叉树的过程中,我们就只需要O(1)的时间对根节点进行定位了。

  • 时间复杂度:O(n),其中 n是树中的节点个数。
  • 空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h是树的高度)的空间表示递归时栈空间。这里 h<n ,所以总空间复杂度为 O(n)。
class Solution {
private:
    unordered_map<int, int> index;

public:
    TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
        if (preorder_left > preorder_right) {
            return nullptr;
        }
        
        // 前序遍历中的第一个节点就是根节点
        int preorder_root = preorder_left;
        // 在中序遍历中定位根节点
        int inorder_root = index[preorder[preorder_root]];
        
        // 先把根节点建立出来
        TreeNode* root = new TreeNode(preorder[preorder_root]);
        // 得到左子树中的节点数目
        int size_left_subtree = inorder_root - inorder_left;
        // 递归地构造左子树,并连接到根节点
        // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
        root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
        // 递归地构造右子树,并连接到根节点
        // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
        root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();
        // 构造哈希映射,帮助我们快速定位根节点
        for (int i = 0; i < n; ++i) {
            index[inorder[i]] = i;
        }
        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
    }
};

岛屿数量

在这里插入图片描述

解题思路:
首先 岛屿总是被水包围,并且每座岛屿只能由水平方向和(或)竖直方向上相邻的陆地连接形成。所以其实很简单就是一个深搜的问题,我们先经过遍历,当遇到第一个为1的元素时,我们就可以认为有一个岛屿了,然后我们进入深搜,搜索四周,注意返回条件,要么就是越界,因为是二维数组所以行和列都有可能越界,除此之外就是当我们遍历到第一个不为1的数字也就说明无法继续连接成岛屿了则直接返回,还需要知道当我们遍历过的地方我们需要给它赋值覆盖掉原本的1,这样是为了提高效率。最后返回岛屿数量就行。
代码如下:

class Solution {
public:
    void dfs(vector<vector<char>>& grid, int i, int j) {
        if (i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size() || grid[i][j] != '1') {
            return;
        }
        grid[i][j] = '2';
        dfs(grid, i + 1, j);
        dfs(grid, i - 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i, j - 1);
    }
    int numIslands(vector<vector<char>>& grid) {
        int isLands = 0;
        for (int i = 0; i < grid.size(); i++) {
            for (int j = 0; j < grid[0].size(); j++) {
                if (grid[i][j] == '1') {
                    dfs(grid, i, j);
                    isLands++;
                }
            }
        }
        return isLands;
    }
};

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

在这里插入图片描述
解题思路:
本题其实就是一个关于DFS的题目,机器人每次走一格就给计数➕1,所以我们只要搞清楚什么情况下机器人可以走,也就是边界问题以及行坐标和列坐标的数位之和不能大于k并且此格子机器人还没有进入过,既然有这个要求则还需要一个操作就是将每一个进入过的格子做一个标记,最后直接返回就行了
代码如下:

class Solution {
public:
int count = 0;
    int movingCount(int m, int n, int k) {
        vector<vector<int>> vis(m,vector<int>(n,0));
        dfs(vis,m,n,k,0,0);
        return count;
    }
    void dfs(vector<vector<int>> &vis ,int &m, int &n, int &k, int i, int j){
        if (i < 0 || j < 0 || i >= m || j >= n || !sumTarget(i,j,k) || vis[i][j] != 0) {
            return;
        }
        if (vis[i][j] == 0) {
            count++;
            vis[i][j] = 1;
            dfs(vis,m,n,k,i + 1,j);
            dfs(vis,m,n,k,i,j + 1);
        }
        
        
    }
    bool sumTarget(int i, int j, int k) {
        int temp = 0;
         while (i > 0) {
            temp += i % 10;
            i /= 10;
         }
         while(j > 0) {
            temp += j % 10;
            j /= 10;
         }
        if (temp > k) {
            return false;
        }
        return true;
    }
};

礼物的最大价值

在这里插入图片描述

解题思路:首先本题的解题思路还是动态规划,因为每次只能往右或者往下走所以我们来构建这样一个结构,我们的二维动态dp数组中,一定是一个矩形,所以这个矩形的上边肯定是由顶点一直往右得到的,所以我们先构造这个边,同理矩形左边的边也是由顶点一直往下得到的所以本题就很简单了,我们构造完矩形的这两个边就需要往矩形的内部拓展了,所以我们只需要考虑里边的每个点(元素)是由上边的点向下移动价值大还是由左边的点向右移动价值大,然后最后构造完这个二维矩形dp数组,返回右下角的元素即可。
代码如下:

class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int n = grid.size();
        int m = grid[0].size();
        int dp[n][m];
        dp[0][0] = grid[0][0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }
        for (int j = 1; j < m; j++) {
            dp[0][j] = dp[0][j - 1] + grid[0][j];
        }
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < m; j++) {
                dp[i][j] = max(dp[i][j - 1] + grid[i][j],dp[i - 1][j] + grid[i][j]);
            }
        
        }
        return dp[n - 1][m - 1];
    }
    
};

剑指 Offer 12. 矩阵中的路径

在这里插入图片描述

解题思路
本题其实就是一个dfs的过程,首先就是先从顶点出发然后往四周扩散,但是我们要知道几个临界条件(不能超出范围,如果遇到了无法匹配的情况就退出)等等,然后如果匹配成功我们就返回true就行了。
代码如下:

class Solution {
public:
    bool exist(vector<vector<char>>& board, string word) {
        rows = board.size();
        cols = board[0].size();
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if(dfs(board,word,i,j,0)) {
                    return true;
                }
            }
        }
        return false;
    }
    private:
    int rows,cols;
    bool dfs(vector<vector<char>>& board, string word, int i, int j, int k) {
        if (i >= rows || i < 0 || j >= cols || j < 0 || board[i][j] != word[k]) {
            return false;
        }
        if (k == word.size() - 1) {
            return true;
        }
        board[i][j] = '\0';
        bool res = dfs(board,word,i+1,j,k+1) || dfs(board,word,i-1,j,k+1) || dfs(board,word,i,j+1,k+1) ||dfs(board,word,i,j-1,k+1);
        board[i][j] = word[k];
        return res;
    }
};

165. 比较版本号

请添加图片描述
我们使用两个指针i和j分别指向两个字符串的开头,然后向后遍历,当遇到小数点’.‘时停下来,并将每个小数点’.'分隔开的修订号解析成数字进行比较,越靠近前边,修订号的优先级越大。根据修订号大小关系,返回相应的数值。
具体过程如下:

  1. 定义两个指针 i和j,初始化i = 0,j = 0。
  2. 两个指针分别遍历两个字符串,将每个小数点’.'分隔开的修订号解析成数字,并进行大小比较:
    如果 num1 > num2,返回 1;
    如果 num1 < num2,返回 -1;
  3. i++,j++,两个指针都后移一步,进行下一轮的修订号解析比较。
  4. 如果遍历完两个字符串都没有返回相应结果,说明两个字符串相等,返回0。
    时间复杂度分析: 两个字符串各遍历一遍,因此时间复杂度为 O ( m a x ( n , m ) ) O(max(n,m)) O(max(n,m)) ,n和m分别是两个字符串的长度。

代码如下

class Solution {
public:
     int compareVersion(string version1, string version2) {
         int n = version1.length();
         int m = version2.length();
         int i = 0, j = 0;
         while(i < n || j < m) {
             long int num1 = 0, num2 = 0;
             while(i < n && version1[i] != '.') {
                 num1 = num1 * 10 + version1[i++] - '0';
             }
             while(j < m && version2[j] != '.') {
                 num2 = num2 * 10 + version2[j++] - '0';
             }
             if (num1 > num2) {
                 return 1;
             } else if (num1 < num2) {
                 return -1;
             }
             i++,j++;
         }
         return 0;
    }
};

54. 螺旋矩阵

请添加图片描述

解题思路:
这里的方法不需要记录已经走过的路径,所以执行用时和内存消耗都相对较小

  1. 首先设定上下左右边界
  2. 其次向右移动到最右,此时第一行因为已经使用过了,可以将其从图中删去,体现在代码中就是重新定义上边界
  3. 判断若重新定义后,上下边界交错,表明螺旋矩阵遍历结束,跳出循环,返回答案
  4. 若上下边界不交错,则遍历还未结束,接着向下向左向上移动,操作过程与第一,二步同理
  5. 不断循环以上步骤,直到某两条边界交错,跳出循环,返回答案
class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        vector<int> res;
        if (matrix.empty()) return res;
        int u  = 0;
        int d = matrix.size() - 1;
        int l = 0;
        int r = matrix[0].size() - 1;
        while (true) {
            for (int i = l; i <= r; i++) {
                res.push_back(matrix[u][i]);
            }
            if (++u > d) break;
            for (int i = u; i <= d; i++) {
                res.push_back(matrix[i][r]);

            }
            if (--r < l) break;
            for (int i = r; i >= l; i--) {
                res.push_back(matrix[d][i]);
            }
            if (--d < u) break;
            for (int i = d; i>= u; i--) {
                res.push_back(matrix[i][l]);
            }
            if (++l > r) break;
        }
        return res;
    }
};

215. 数组中的第K个最大元素

请添加图片描述

解题思路
运用到的是基于快速排序的选择排序,也叫快速选择。快速排序的原理就是可以一次把一个元素放到数组它对应的位置上,利用这个特性我们可以进行左右边界的更迭达到类似二分的效果,即一次甩掉一半的元素可以不必再纠结它们的顺序和大小。具体是否可以完成二分效果,取决于 base 元素的选择,如果 base 选择较差,那么会使算法退化到 O(N^2) 的时间复杂度;反之可以把问题规模每次都缩小一半。具体流程如下:

    //快速选择方法-迭代
class Solution {
public:
    void quickPartition(vector<int>& nums, int start, int end, int target) {
        // 随机取一个数作为基准
        srand(time(nullptr));
        int random = rand() % (end - start + 1) + start;
        int base = nums[random];
        // 将该数放到待快排区间开头第一个元素
        swap(nums[start], nums[random]);
        int index = start;
        // 从待快排区间的第二个元素开始,依次与base比较,如果大于等于base则将该元素
        // 交换到index + 1位置,index++,使得最终index前面的元素都比base大。
        for (int i = start + 1; i <= end; ++i) {
            if (nums[i] >= base) {
                swap(nums[index + 1], nums[i]);
                index++;
            }
        }
        // base存放在区间开头,现在需要把它交换到index位置,这就是它在整个有序数组中的位置。
        swap(nums[index], nums[start]);
	
        // 如果index小于target,需要在右边区间继续快排查找,否则到在边区间查找,
        // 如果等于已经找到目标值不需要递归,这里这么做优化了传统快排的复杂度。
        if (index < target) {
            quickPartition(nums, index + 1, end, target);
        }
        else if (index > target) {
            quickPartition(nums, start, index - 1, target);
        }
    }

    int findKthLargest(vector<int>& nums, int k) {
        // 方法1. 快速排序的分区思想,快排的思想是一次找出一个数的正确位置,
        // 并使得该数左边的元素都比它小,该数右边的元素都比它大,要找出第k
	// 大的元素,只需要在快排的时候采用降序排序,找到下标为k-1的元素即可。

        quickPartition(nums, 0, nums.size() - 1, k - 1);
        return nums[k - 1];
    }
};

3. 无重复字符的最长子串

请添加图片描述
解题思路
我们找出以每一个字符为第一个字符的无重复字符的子串,然后每一次通过比较替换最后直接返回就行了。如果我们依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的。这里的原因在于,假设我们选择字符串中的第k个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为rk。那么当我们选择第k+1个字符作为起始位置时,首先从k+1到rk的字符显然是不重复的,并且由于少了原本的第k个字符,我们可以尝试继续增加rk,直到右侧出现了重复字符位置。
其实就是定义一个滑动窗口:

  • 我们使用两个指针表示字符串中的某个子串(或窗口)的左右边界,其中左指针代表着上文中「枚举子串的起始位置」,而右指针即为上文中的 rk
  • 在每一步的操作中,我们会将左指针向右移动一格,表示 我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度;
  • 在枚举结束后,我们找到的最长的子串的长度即为答案。
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // 哈希集合,记录每个字符是否出现过
        unordered_set<char> occ;
        int n = s.size();
        // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
        int rk = -1, ans = 0;
        // 枚举左指针的位置,初始值隐性地表示为 -1
        for (int i = 0; i < n; ++i) {
            if (i != 0) {
                // 左指针向右移动一格,移除一个字符
                occ.erase(s[i - 1]);
            }
            while (rk + 1 < n && !occ.count(s[rk + 1])) {
                // 不断地移动右指针
                occ.insert(s[rk + 1]);
                ++rk;
            }
            // 第 i 到 rk 个字符是一个极长的无重复字符子串
            ans = max(ans, rk - i + 1);
        }
        return ans;
    }
};
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值