接上文,为力扣刷题个人笔记
二叉树
二叉树的深度优先遍历用
递归(内部是用stack栈实现的,也称为隐式递归)。
用显式的stack栈实现。
广度优先遍历用栈
用queue队列实现。
用stack实现可以,但是很绕。
543.二叉树的直径(简单)(二刷)
给你一颗二叉树的节点,返回该树的直径。
二叉树的直径是指树中任意两个节点之间最长路径的长度。这条路径可能经过也可能不经过根节点root。
两节点之间的路径长度由它们之间边数表示。
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
思路:来自力扣官方
深度优先搜索
首先我们知道一条路径的长度为该路径经过的节点数减一,所以求直径(即求路径长度的最大值)等效于求路径经过节点数的最大值减一。
而任意一条路径均可以被看作由某个节点为起点,从其左儿子和右儿子向下遍历的路径拼接得到。
如图我们可以知道路径 [9, 4, 2, 5, 7, 8] 可以被看作以 222 为起点,从其左儿子向下遍历的路径 [2, 4, 9] 和从其右儿子向下遍历的路径 [2, 5, 7, 8] 拼接得到。
假设我们知道对于该节点的左儿子向下遍历经过最多的节点数 L (即以左儿子为根的子树的深度) 和其右儿子向下遍历经过最多的节点数 R (即以右儿子为根的子树的深度),那么以该节点为起点的路径经过节点数的最大值即为 L+R+1。我们记节点 node为起点的路径经过节点数的最大值为 dnode那么二叉树的直径就是所有节点 dnode的最大值减一。
即:经过某个节点的最大路径长度,为其左子树的深度L+右子树的深度R+1。
某树的直径就是该树中最大路径长度-1。
算法流程:构造递归函数,
class Solution {
public:
int deepOfBinaryTree(TreeNode* root){
//深度优先遍历,构造递归函数
//递归终止条件
if(!root) return 0;
//递归函数
return max(deepOfBinaryTree(root->left),deepOfBinaryTree(root->right))+1;
}
int diameterOfBinaryTree(TreeNode* root) {
//递归判断经过每个节点的最大路径长度
//递归终止条件
if(!root) return 0;
//直径等于经过某节点的左右子树深度和的最大值
int currentDiameter = deepOfBinaryTree(root->left)+deepOfBinaryTree(root->right);
int leftDiameter = diameterOfBinaryTree(root->left);
int rightDiameter = diameterOfBinaryTree(root->right);
return max(currentDiameter,max(leftDiameter,rightDiameter));
}
};
通过了验证正确,但是效率低下,原因在于用了两个递归,力扣官方给的解答将两个递归整合为一个递归函数。
时间:24ms,5.9%
空间:19.71MB,13.43%
class Solution {
int ans;
int depth(TreeNode* rt){
if (rt == NULL) {
return 0; // 访问到空节点了,返回0
}
int L = depth(rt->left); // 左儿子为根的子树的深度
int R = depth(rt->right); // 右儿子为根的子树的深度
ans = max(ans, L + R + 1); // 计算d_node即L+R+1 并更新ans
return max(L, R) + 1; // 返回该节点为根的子树的深度
}
public:
int diameterOfBinaryTree(TreeNode* root) {
ans = 1;
depth(root);
return ans - 1;
}
};
时间复杂度:O(N),12ms,47.33%
空间复杂度:O(N),19.55MB,32.82%
102.二叉树的层序遍历(中等)(二刷)
给你二叉树的根节点root,返回其节点值的层序遍历。(即逐层地,从左到右访问所有节点)。
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
思路:二叉树的广度优先遍历,利用队列实现
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int> > ans;
if(!root) return ans;
queue<TreeNode* > Q; //队列存储每一层的节点
Q.push(root);
while(!Q.empty()){
int sz = Q.size();
vector<int> tempAns; //存储每一层的ans
while(sz > 0){
TreeNode* node = Q.front();//node指向Q队列的头
tempAns.push_back(node->val);
if(node->left) Q.push(node->left);
if(node->right) Q.push(node->right);
Q.pop();
sz -=1;
}
ans.push_back(tempAns);
}
return ans;
}
};
时间复杂度:O(N),12ms,13.74%
空间复杂度:O(M),13.44MB,13.35%。M为最大层的节点数
108.将有序数组转换为平衡二叉搜索树(简单)(二刷)
平衡:任意节点的左子树和右子树的高度差不大于1
二叉搜索树:任意节点的值大于左节点,小于右节点
给你一个整数数组nums,其中元素已经按升序排列,请你将其转换为一棵高度平衡二叉搜索树。高度平衡二叉树是一棵满足【每个节点的左右两个子树的高度差的绝对值不超过1】的二叉树
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
思路:递归,将数组中间元素作为根节点。数组左边元素插入到左子树,右边元素插入到右子树。
class Solution {
public:
TreeNode* arrayTOBSTHelper(vector<int>& nums, int start, int end){
//该函数用于递归辅助函数
//终止条件
if(end < start) return nullptr;
//递归函数
int mid = (start + end)/2;
TreeNode* root = new TreeNode(nums[mid]);
root->left = arrayTOBSTHelper(nums,start,mid-1);
root->right = arrayTOBSTHelper(nums,mid+1,end);
return root;
}
TreeNode* sortedArrayToBST(vector<int>& nums) {
return arrayTOBSTHelper(nums,0,static_cast<int>(nums.size())-1); //static_cast类型转换函数,将后面的转为int
}
};
时间复杂度:O(N),12ms,74.14%
空间复杂度:O(N),20.61MB,38.47%
//写在一个函数体里面
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
/*思路:递归实现,找到数组的中点,中点之前为左子树所需数组,中点之后为右子树所需数组*/
int N = nums.size();
if(N==0) return nullptr;
int mid = N/2;
TreeNode* root = new TreeNode(nums[mid]);
vector<int>nums_left(nums.begin(),nums.begin()+mid);
vector<int>nums_right(nums.begin()+mid+1,nums.end());
root->left = sortedArrayToBST(nums_left);
root->right = sortedArrayToBST(nums_right);
return root;
}
};
98.验证二叉搜索树(中等)(二刷)
给你一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树的定义如下:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
输入:root = [2,1,3]
输出:true
思路:
要解决这道题首先我们要了解二叉搜索树有什么性质可以给我们利用,由题目给出的信息我们可以知道:如果该二叉树的左子树不为空,则左子树上所有节点的值均小于它的根节点的值; 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;它的左右子树也为二叉搜索树。
这启示我们设计一个递归函数 helper(root, lower, upper) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (l,r)的范围内(注意是开区间)。如果 root 节点的值 val 不在 (l,r)的范围内说明不满足条件直接返回,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。
那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界 upper 改为 root.val,即调用 helper(root.left, lower, root.val),因为左子树里所有节点的值均小于它的根节点的值。同理递归调用右子树时,我们需要把下界 lower 改为 root.val,即调用 helper(root.right, root.val, upper)。
函数递归调用的入口为 helper(root, -inf, +inf), inf 表示一个无穷大的值。
class Solution {
public:
long long pre = (long long) INT_MIN-1;
bool isValidBST(TreeNode* root) {
/*思路:中序遍历为升序遍历,则为二叉搜索树,深度优先遍历,递归实现
巧妙通过pre记录中序遍历的上一个值,与当前值比较是否为升序*/
if(!root) return true;
bool L = isValidBST(root->left);
bool temp = (pre < root->val);
pre = root->val;
bool R = isValidBST(root->right);
return temp && L && R;
}
};
思路二:中序遍历,(左根右)得到的是一个升序的数组,则说明是二叉搜索树。
class Solution {
public:
bool isValidBST(TreeNode* root) {
stack<TreeNode*> stack; //建立栈来存储所有左节点
long long inorder = (long long)INT_MIN - 1; //inorder初始化为最小的int
while (!stack.empty() || root != nullptr) {
while (root != nullptr) { //如果root为非空指针,则入栈并向左遍历
stack.push(root);
root = root -> left;
}
root = stack.top(); //root重定位到栈头节点
stack.pop();
// 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
if (root -> val <= inorder) {//如果root值小于等于前一个inorder值,则说明不是升序序列
return false;
}
inorder = root -> val; //进入下一次迭代
root = root -> right;
}
return true;
}
};
时间复杂度: 12ms,63.42%
空间复杂度:21.21MB,9.16%
230.二叉搜索树中第K小的元素(中等)(二刷)
给定一个二叉搜索树的根节点root,和一个整数k,请你设计一个算法查找其中第k个最小元素(从1开始计数)
输入:root = [3,1,4,null,2], k = 1
输出:1
思路:二叉搜索树的中序遍历,只输出第k个值即可
//使用深度优先遍历
class Solution {
public:
void kthSmallest_helper(TreeNode* root,vector<int> &temp){
//该函数用于返回指定二叉树的中序遍历结果
if(!root) return;
kthSmallest_helper(root->left,temp);
temp.push_back(root->val);
kthSmallest_helper(root->right,temp);
return;
}
int kthSmallest(TreeNode* root, int k) {
/*二叉搜索树的中序遍历即为有序序列,找出其中第k个即可*/
if(!root) return 0;
vector<int> temp;
kthSmallest_helper(root,temp);
return temp[k-1];
}
};
时间复杂度:O(N)
空间复杂度:O(N)
思路二:如果你需要频繁地查找第k小的值,你将如何优化算法?
记录下每个节点左子树的节点个数。k如果大于左子树的节点个数则一定出现在该节点的右子树中。
199.二叉树的右视图(中等)(二刷)
给定一个二叉树的根节点root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
输入: [1,2,3,null,5,null,4]
输出: [1,3,4]
思路:等价于二叉树的层序遍历(用队列),只返回每一层的最右侧节点的值。
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
//特判
vector<int> ans;
if(root == nullptr) return ans;
//建立队列辅助实现广度优先遍历
queue<TreeNode* > Q;
Q.push(root);
int sz =1;
while(!Q.empty()){
ans.push_back(Q.back()->val);
sz = Q.size();
while(sz != 0){
root = Q.front();
if(root->left != nullptr) Q.push(root->left);
if(root->right != nullptr) Q.push(root->right);
Q.pop();
sz--;
}
}
return ans;
}
};
时间复杂度:O(N),0ms,100.00%
空间复杂度:O(M),11.84MB,24.62%。M为最大层的节点数。
114.二叉树展开为链表(中等)(二刷)(有助于理解递归的过程)
给你二叉树的根节点root,请你将它展开为一个单链表
- 展开后的单链表应该同样使用TreeNode,其中right子指针指向链表中下一个结点,而左子指针始终为null
- 展开后的单链表应该与二叉树先序遍历顺序相同
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
思路:使用一个 prev 指针来跟踪先前处理的节点。我们首先处理右子树,然后处理左子树,最后处理根节点。这样,当我们到达根节点时,prev 指针将指向先序遍历中的下一个节点。我们将当前节点的右子节点设置为 prev,并将左子节点设置为 nullptr。然后,我们更新 prev 为当前节点。
class Solution {
public:
TreeNode* prev = nullptr;
void flatten(TreeNode* root) {
/*递归实现,因为需按先序遍历结果返回,
所以,递归的时候按照右左根的顺序传递下去,这样归回来的时候,就是根左右,
并用一个指针记录传递下去时候的上一个节点*/
if(!root) return;
flatten(root->right);
flatten(root->left);
root->right = prev;
root->left = nullptr;
prev = root;
return;
}
};
105.从前序与中序遍历序列构造二叉树(中等)(二刷)
给定两个整数数组preorder和inorder,其中preorder是二叉树的先序遍历,inorder是同一棵树的中序遍历,请构造二叉树并返回其根节点。
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
思路:递归实现。中序序列中先找到根节点,根节点左边的一定在左子树,根节点右边的一定在右子树。
只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,对上述形式中的所有左右括号进行定位。
这样以来,我们就知道了左子树的前序遍历和中序遍历结果,以及右子树的前序遍历和中序遍历结果,我们就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。
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);
}
};
时间复杂度:O(1),12ms,90.27%
空间复杂度:O(N),25.44MB,31.59%
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
/*经典问题,根据二叉树的任意两个遍历序列,即可构造二叉树
递归实现,将其转化为递归子问题,可以找到递归先序子序列和中序子序列*/
int sz = preorder.size();
if(sz==0) return nullptr;
TreeNode* root = new TreeNode(preorder[0]);
vector<int> new_preorder_left, new_preorder_right;
vector<int> new_inorder_left, new_inorder_right;
int i=0;
for(i;i<sz;i++){
if(preorder[0]==inorder[i]) break;
new_preorder_left.push_back(preorder[i+1]);
new_inorder_left.push_back(inorder[i]);
}
i=i+1;
for(i;i<sz;i++){
new_inorder_right.push_back(inorder[i]);
new_preorder_right.push_back(preorder[i]);
}
root->left = buildTree(new_preorder_left,new_inorder_left);
root->right = buildTree(new_preorder_right,new_inorder_right);
return root;
}
};
437.路径总和(中等)(二刷,没弄懂为什么必须双递归)
给定一个二叉树的根节点root,和一个整数targetSum,求该二叉树里节点值之和等于targetSum的路径数目。路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
思路:递归实现,遍历二叉树。假设路径起始节点为当前节点,需要遍历当前节点向下的所有可能的路径。回溯的思想。对左子树和右子树也需要进行同样的操作。
难点在于不一定包含根节点,就是不知道路径的起始位置和终止位置。
深度优先遍历,双递归
穷举所有的可能,对于我们访问的每一个节点node,都认为其为路径的起始点。
class Solution {
public:
int rootSum(TreeNode* root, long targetSum) {
//该函数用于求解以当前节点为起始节点的路径
if (!root) {
return 0;
}
int ret = 0;
if (root->val == targetSum) {
ret++;
}
ret += rootSum(root->left, targetSum - root->val);
ret += rootSum(root->right, targetSum - root->val);
return ret;
}
int pathSum(TreeNode* root, int targetSum) {
if (!root) {
return 0;
}
int ret = rootSum(root, targetSum); //求解当前节点为起始节点的路径条数
//递归调用左右子树
ret += pathSum(root->left, targetSum);
ret += pathSum(root->right, targetSum);
return ret;
}
};
时间复杂度:O(N*N)
空间复杂度:O(N),递归需要在栈上开辟空间。
236.二叉树的最近公共祖先(中等)(二刷,再看)
给定一个二叉树,找到该树中两个指定节点的最近公共祖先。
公共祖先的定义:对于有根数T的两个节点p,q。最近公共祖先表示为一个节点x,满足x是p、q的祖先且x的深度尽可能大(一个节点也可以是它自己的祖先)。
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
分析思路:深度优先遍历,判断同时包含两个节点的最小子树,返回最小子树的根节点。
同上题思路,双重递归,深度优先遍历
class Solution {
public:
bool nodeInTree(TreeNode* root,TreeNode*p){
//该函数用于判断p是否存在于树root中
//终止条件
if(root == nullptr) return false;
if(root == p) return true;
return nodeInTree(root->left,p) || nodeInTree(root->right,p);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
//找出同时包含p,q节点的最小子树
//终止条件
if(root == nullptr) return root;
if(nodeInTree(root->left,p) && nodeInTree(root->left,q)){
//如果p,q都在左子树上
return lowestCommonAncestor(root->left,p,q);
}
else if(nodeInTree(root->right,p) && nodeInTree(root->right,q)){
//如果p,q都在右子树上
return lowestCommonAncestor(root->right,p,q);
}
return root;
}
};
时间复杂度:O(N*N),暴力搜索。
空间复杂度:O(N),递归内部实现用栈。
思路二:如果root等于p或q则root就是最近祖先。同理,先序遍历。如果left为空,right不为空,则说明p,q都不在left中,则直接返回right。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == nullptr || root == p || root == q) return root;
TreeNode *left = lowestCommonAncestor(root->left, p, q);
TreeNode *right = lowestCommonAncestor(root->right, p, q);
if(left == nullptr) return right;
if(right == nullptr) return left;
return root; //如果left和right都不为空,则说明在左右两个子树上
}
};
时间复杂度:O(N)
空间复杂度:O(N)
图论(多写)
即二维数组,递归遍历的思想
200.岛屿数量(中等)(二刷)
给你一个由‘1’(陆地)和‘0’(水)组成的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
输入:grid = [
[“1”,“1”,“1”,“1”,“0”],
[“1”,“1”,“0”,“1”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“0”,“0”,“0”]
]
输出:1
思路:看力扣官网题解,写得很详细网格的DFS遍历,深度优先搜索。
class Solution {
public:
void DFS_Islands(vector<vector<char>>& grid,int i, int j){
//将函数用于将grid[i][j]所在的岛屿值全部搜索一遍,并置为2
if(!inLands(grid,i,j)) return ; //当i,j不在网格中
if(grid[i][j]!='1') return ; //递归终止条件
grid[i][j] = '2';
DFS_Islands(grid,i,j+1);
DFS_Islands(grid,i,j-1);
DFS_Islands(grid,i+1,j);
DFS_Islands(grid,i-1,j);
return ;
}
bool inLands(vector<vector<char>>& grid,int i, int j){
//该函数用于判断grid[i][j]是否在grid中
return i>=0 && j>=0 && i<grid.size() && j<grid[0].size();
}
int numIslands(vector<vector<char>>& grid) {
/*遍历网格,深度优先搜索,将搜索过的节点值置为2,
深度优先搜索的次数即为岛屿的数量,递归实现*/
int number = 0;
int M=grid.size(), N = grid[0].size();
for(int i=0;i<M;i++){
for(int j=0; j<N; j++){
if(grid[i][j]=='1') {
number++;
DFS_Islands(grid,i,j);
}
}
}
return number;
}
};
时间复杂度:O(r*c),20ms,99.83%
空间复杂度:O(N),11.94MB,64.18%。递归实现,内部有栈
例题1,695.最大岛屿面积(中等)
给定一个包含了一些0和1的非空二维数组grid,一个岛屿是一组相邻的1(代表陆地),这里的【相邻】要求两个1必须在水平或者竖直方向上相邻。你可以假设grid的四个边缘都被0(代表海洋)包围着。
找到给定的二维数组中最大的岛屿面积。如果没有岛屿,则返回面积为0。
输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出:6
解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。
class Solution {
public:
int DFS_Area(vector<vector<int> >&grid, int r, int c ){
//该函数用于网格的深度优先遍历,并计算岛屿面积
//终止条件
if (!inArea(grid,r,c)){
return 0;
}
//如果当前节点不是未访问的岛屿,直接返回
if(grid[r][c]!=1) return 0;
grid[r][c] = 2; //将当前节点标记为已访问
//访问上下左右四个相邻节点
return 1
+DFS_Area(grid,r-1,c)
+DFS_Area(grid,r+1,c)
+DFS_Area(grid,r,c-1)
+DFS_Area(grid,r,c+1);
}
bool inArea(vector<vector<int> > &grid,int r, int c){
//该函数用于判断当前节点是否位于网格内
return r>=0 && c >=0 && r<grid.size() && c<grid[0].size();
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
int res = 0;
for(int r=0;r<grid.size();r++){
for(int c=0;c<grid[0].size();c++){
if(grid[r][c]==1){//对于岛屿进行访问
int a = DFS_Area(grid,r,c);
res = max(res,a);
}
}
}
return res;
}
};
时间复杂度:O(r*c),12ms,94.97%
空间复杂度:O(N),22.42MB,67.04%。递归实现
例题2,827 填海造陆问题(难)
在二维地图上,0代表海洋,1代表陆地,我们最多只能将一格0(海洋)变成1(陆地)。进行填海之后,地图上最大的岛屿面积是多少?
例题3,463 岛屿的周长(简单)
给定一个包含 0 和 1 的二维网格地图,其中 1 表示陆地,0 表示海洋。网格中的格子水平和垂直方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(一个或多个表示陆地的格子相连组成岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。计算这个岛屿的周长。
输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
输出:16
解释:它的周长是上面图片中的 16 个黄色的边
当我们的 dfs 函数因为「坐标 (r, c) 超出网格范围」返回的时候,实际上就经过了一条黄色的边;而当函数因为「当前格子是海洋格子」返回的时候,实际上就经过了一条蓝色的边。这样,我们就把岛屿的周长跟 DFS 遍历联系起来了,我们的题解代码也呼之欲出:
class Solution {
public:
int DFS_island(vector<vector<int>>& grid,int r,int c){
//该函数用于DFS遍历网格,返回周长
//终止条件
if(!inGrid(grid,r,c)) return 1; //超出网格边界
if(grid[r][c] == 0) return 1; //当前节点是海洋
if(grid[r][c] == 2) return 0; //当前节点已访问
//访问相邻节点
grid[r][c] = 2; //标记为已访问
return DFS_island(grid,r-1,c)+DFS_island(grid,r+1,c)+DFS_island(grid,r,c-1)+DFS_island(grid,r,c+1);
}
bool inGrid(vector<vector<int>>& grid, int r,int c){
//该函数用于判断当前节点是否在网格内
return r>=0 && c>=0 && r<grid.size() && c<grid[0].size();
}
int islandPerimeter(vector<vector<int>>& grid) {
for(int r=0; r<grid.size();r++){
for(int c=0; c<grid[0].size();c++){
if(grid[r][c] == 1) return DFS_island(grid,r,c);
}
}
return 0;
}
};
时间复杂度:O(r*c),112ms,23.4%
空间复杂度:O(N),92.35MB,24.18%
994.腐败的橘子(中等)(二刷)(典型的多源广度优先搜索,配合上一题理解多源深度优先搜索)
在给定的M*N网格grid中,每个单元格可以有以下三个值之一:
- 值0代表空单元格
- 值1代表新鲜橘子
- 值2代表腐烂的橘子
- 每分钟,腐烂的橘子周围4个方向上相邻的新鲜橘子都会腐烂。
- 返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回-1。
输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
输出:4
思路:广度优先搜索的过程。
力扣官方题解:多源广度优先搜索
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
int row=grid.size();
int col=grid[0].size();
int res=0;
vector<int>dx={-1,0,0,1};//辅助定位即将被腐烂的橘子的横坐标
vector<int>dy={0,1,-1,0};//辅助定位即将被腐烂的橘子的纵坐标,对应构成腐烂橘子的四个污染方向
queue<pair<int,int>>rot ; //pair存储一对关联的数据
for(int i=0;i<row;++i) //将腐烂橘子一一压入队列
for(int j=0;j<col;++j)
if(grid[i][j]==2)
rot.push({i,j}); //将腐烂橘子的坐标压入队列
while(!rot.empty())
{
int vol=rot.size();//标记队列内腐烂橘子个数
for(int i=0;i<vol;++i)
{
pair<int,int> fir=rot.front();//取出首个腐烂橘子
rot.pop();
for(int j=0;j<4;++j)//进行四个方向污染
{
int x=fir.first+dx[j],y=fir.second+dy[j];
if(x>=0&&x<row&&y>=0&&y<col&&grid[x][y]==1)//判断是否存在新鲜橘子
{
grid[x][y]=2;
rot.push({x,y});
}
}
}
if(!rot.empty())//如果为空表示一开始就没有腐烂橘子,返回0分钟
res++;//每次取出队列所有橘子时间加1,同时压入被污染的新一批橘子
}
for(int i=0;i<row;++i)//检查是否还有新鲜橘子
for(int j=0;j<col;++j)
if(grid[i][j]==1)
return -1;
return res;
}
};
时间复杂度:4ms,91.51%
空间复杂度:12.74MB,45.45%
自写:
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
//该函数利用多源BFS返回腐烂橘子所需天数
//遍历网格找出第0天,所有的烂橘子,作为第一天入队列的开始
int m = grid.size();
int n = grid[0].size();
int flush = 0;
queue<pair<int, int>> rot; //队列存储烂橘子的坐标
for(int r=0; r<m;r++){
for(int c=0; c<n; c++){
//遍历网格
if(grid[r][c]==2) rot.push({r,c}); //将烂橘子坐标存入队列
if(grid[r][c]==1) flush++; //记录好橘子的个数
}
}
//BFS污染周围橘子
int day = 0;
while(!rot.empty() && flush >0){
day++;
int vol = rot.size(); //每层烂橘子的个数,即循环的次数
for (int i=0; i<vol;i++){
//循环vol次,将该层橘子依次取出
pair<int,int> fir = rot.front();
rot.pop();
//将fir指向的烂橘子向四个方向污染
int r=fir.first;
int c=fir.second;
if((r-1)>=0 && grid[r-1][c] == 1) {
grid[r-1][c] = 2;
rot.push({r-1,c});
flush--;
}
if((r+1)<m && grid[r+1][c] == 1) {
grid[r+1][c] = 2;
rot.push({r+1,c});
flush--;
}
if((c-1)>=0 && grid[r][c-1] == 1) {
grid[r][c-1] = 2;
rot.push({r,c-1});
flush--;
}
if((c+1)<n && grid[r][c+1] == 1) {
grid[r][c+1] = 2;
rot.push({r,c+1});
flush--;
}
}
}
if(flush == 0) return day;
else return -1;
}
};
时间复杂度:O(MN),8ms,64.00%
空间复杂度:O(MN),12.8MB,37.24%
207.课程表(中等)(二刷)(还得练)
你这个学期必须选修numCourses门课程,记为0到numCourses-1。
在选修某些课程之前需要一些先修课程。先修课程按数组prerequisite给出,其中prerequisite【i】=【ai,bi】,表示如果要学习课程ai则必须先学习课程bi。
例如,先修课程对【0,1】表示:想要学习课程0,你需要先完成课程1。
请你判断是否可能完成所有课程的学习?如果可以,返回true;否则,返回false。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
思路:拓扑排序问题用广度优先遍历实现
对于图G中的任意一条有向边(u,v),u在排列中都出现在v的前面。
那么称该排列是图G的【拓扑排序】。
总结:拓扑排序问题
- 根据依赖关系,构建邻接表、入度数组。
- 选取入度为 0 的数据,根据邻接表,减小依赖它的数据的入度。
- 找出入度变为 0 的数据,重复第 2 步。
- 直至所有数据的入度为 0,得到排序,如果还有数据的入度不为 0,说明图中存在环。
拓扑排序的图解,依层将入度为0的节点从图中删除。如果能删除所有节点,则存在拓扑排序。如果不能,则不存在拓扑排序,一定存在回路。
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
//该函数用来判断是否存在拓扑排序
vector<int> edges(numCourses); //用以记录每个节点的入度
unordered_map<int,vector<int> > map; //哈希表用来存储邻接链表
//遍历prerequisite,构造邻接表和入度数组
for(int i=0;i<prerequisites.size();i++){
map[prerequisites[i][1]].push_back(prerequisites[i][0]);
edges[prerequisites[i][0]]++;
}
//BFS遍历,将入度为0的节点作为第一层,再将第一层全部出队列,并同时压入第二层
queue<int> node;
for(int i=0; i<edges.size();i++){
if(edges[i]==0) node.push(i); //将入度为0的节点放入队列
}
int cnt=0; //用以记录已经处理的节点数
while(!node.empty()){
int ptr = node.front();
node.pop();
cnt++;
//更新邻接表和入度数组,从链表中删除ptr节点,将ptr指向节点的邻接节点入度为0的节点全部入队列
for(int i=0;i<map[ptr].size();i++){
if(edges[map[ptr][i]]>0){
edges[map[ptr][i]]--;
if(edges[map[ptr][i]]==0){
node.push(map[ptr][i]);//将入度减为0的节点入队列
}
}
}
}
//如果最后还有节点没有入过队列,则说明不存在拓扑排序
if(cnt == numCourses) return true;
else return false;
}
};
时间复杂度:O(M+N),20ms,66.55%
空间复杂度:O(M+N),13.62MB,27.41%
208.实现Trie(前缀树) (中等)(看题解很详细)
前缀树是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用场景,例如自动补完和拼写检查。
实现Trie类:
- Trie()初始化前缀树对象
- void insert(string word)向前缀树中插入字符串word。
- boolean search(String word)如果字符串word在前缀树中,返回true(即,在检索之前已经插入);否则,返回false。
- boolean starsWith(String prefix)如果之前已经插入的字符串word的前缀之一为prefix,返回true;否则,返回false。
示例:
输入
[“Trie”, “insert”, “search”, “search”, “startsWith”, “insert”, “search”]
[[], [“apple”], [“apple”], [“app”], [“app”], [“app”], [“app”]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert(“apple”);
trie.search(“apple”); // 返回 True
trie.search(“app”); // 返回 False
trie.startsWith(“app”); // 返回 True
trie.insert(“app”);
trie.search(“app”); // 返回 True
class Trie {
private:
//结点值
bool isEnd;
Trie* next[26];
public:
Trie() {
//构造函数,设置哑结点
isEnd = false;
memset(next, 0, sizeof(next));
}
void insert(string word) {
//该函数向前缀树中插入单词
Trie* node = this;
for (char c : word) {
//遍历需要插入的单词,如果当前前缀树没有该单词则依次创建对应子树
if (node->next[c-'a'] == NULL) {
//如果没有指向下一个节点值的前缀树,则创建一个
node->next[c-'a'] = new Trie();
}
//移动到该单词的下一个字母节点
node = node->next[c-'a'];
}
//遍历完,之后一个结点标记为单词的结束
node->isEnd = true;
}
bool search(string word) {
//该函数用于查找前缀树中是否有该单词
Trie* node = this;
for (char c : word) {
node = node->next[c - 'a'];
if (node == NULL) {
return false;
}
}
return node->isEnd;
}
bool startsWith(string prefix) {
//该函数用于判断该前缀是否出现在前缀树中
Trie* node = this;
for (char c : prefix) {
node = node->next[c-'a'];
if (node == NULL) {
return false;
}
}
return true;
}
};
回溯
回溯算法尝试分步解决问题。
回溯其实可以说是我们熟悉的DFS,本质上一种暴力枚举算法。
回溯算法的唯一优化方法是剪枝。
回溯算法的模板(个人总结可能存在错误)
back(index){
终止条件;
for(元素集合){ //指向当前处理的位置
操作处理;
递归;
back(index+1);
回溯;//恢复现场
}
}
回溯遍历的两层变量要区分开,回溯的遍历脚标控制容易混淆
46.全排列(中等)
给定一个不含重复数字的数组nums,返回所有可能的全排列。你可以按任意顺序返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
将该问题看作是有n个排列成一行的空格,我们需要从左往右依次填入题目给定的n个数,每个数只能使用一次。
我们定义递归函数backtrack(first,output)表示从左往右填到第first个位置,当前排列为output。那么整个递归函数分为两个情况:
- (终止条件) 如果first=n,说明我们已经填完了n个位置,找到了一个可行的解,我们将output放入答案数组中,递归结束
- 如果first<n,我们要考虑这第first个位置我们要填哪个数。所填的数为未使用过的数。为了节省空间,不产生visit[]数组的开销。我们将nums数组first标记的左边部分记为output,右边部分则为待填的可选数字。在每次迭代前动态维护数组执行交换操作,迭代完成之后再恢复撤销交换操作即可。
自写:
class Solution {
public:
void permuteHelper(vector<vector<int> >& res, vector<int>& nums,int index,int lenth){
//该函数用于辅助
//终止条件
if(index == lenth){
res.push_back(nums);
return ;
}
//递归回溯
for(int i=index; i<lenth;++i){//index指向当前处理的位置
//操作
swap(nums[i],nums[index]);
//递归
permuteHelper(res,nums,index+1,lenth);
//回溯,撤销操作
swap(nums[i],nums[index]);
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int> > res;
permuteHelper(res,nums,0,(int)nums.size());
return res;
}
};
时间复杂度:O(NxN!),4ms,67.55%
空间复杂度:O(NxN!),7.58MB,61.37%
78.子集(中等)
给你一个整数数组nums,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集。你可以按任意顺序返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
思路:从数组中,选择数字进行输出
- ptr指向当前选择的数字。
class Solution {
public:
void subsetsHelper(vector<vector<int>>& res,vector<int>& nums,vector<int>& temp,int ptr,int lenth){
//该函数用于辅助完成输出子集
res.push_back(temp);
//终止条件
if(ptr == lenth) return;
//递归回溯
for(int i=ptr;i<lenth;++i){
//操作,放入一个数值
temp.push_back(nums[i]);
//递归,选择下一个数字
subsetsHelper(res,nums,temp,i+1,lenth);
//回溯,恢复现场
temp.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int> > res;
vector<int> temp;
int ptr=0;
subsetsHelper(res,nums,temp,ptr,(int)nums.size());
return res;
}
};
回溯的时间复杂度很高,所以能用空间换时间就换。
时间复杂度:0ms,100.00%
空间复杂度:7.22MB,26.73%
17.电话号码的字母组合(中等)
给定一个仅包含数字2-9的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。
给出数字到字母的映射如下(与电话按键相同)。注意1不对应任何字母
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
思路:建立数字2-9,每个数字和每个字符集的映射。遍历数字,从该数字对应的字符集中选择一个字母,遍历完成后撤销选择操作,递归遍历下一个字符。
类似全排列,向固定长度的string中填入字母。
class Solution {
public:
void letterCombinationsHelper(vector<string>& res, string ¤t, const string &digits, int index, unordered_map<char, string>& map) {
//该函数用于辅助返回电话号码的字母组合,index指向处理的数字位置
//终止条件
if(index == digits.size()) {
if (!current.empty()) {
res.push_back(current);
}
return;
}
//操作-递归-回溯
for(int j = 0; j < map[digits[index]].size(); j++) {
//for循环确定当前处理的数字为digits[index],从中遍历选择一个字母进行操作
//操作
current.push_back(map[digits[index]][j]);
//递归
letterCombinationsHelper(res, current, digits, index + 1, map);
//回溯
current.pop_back();
}
}
vector<string> letterCombinations(string digits) {
//该函数用于返回电话号码的字母组合
unordered_map<char, string> map = {
{'2', "abc"},
{'3', "def"},
{'4', "ghi"},
{'5', "jkl"},
{'6', "mno"},
{'7', "pqrs"},
{'8', "tuv"},
{'9', "wxyz"}
};
vector<string> res;
string current;
letterCombinationsHelper(res, current, digits, 0, map);
return res;
}
};
39.组合总和(中等)
给你一个无重复元素的整数数组candidates和一个目标整数target,找出candidates中可以使数字和为目标数target的所有不同组合,并以列表形式返回。你可以按任意顺序返回这些组合。
candidates中的同一个数字可以无限制重复被选取。如果至少一个数字的被选数量不同,则两种组合是不同的。对于给定的输入,保证和为target的不同组合数少于150个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
思路:遍历candidates,搜索target-candidates是否在candidates中,如果为正继续搜索。
- 终止条件:target == 0,输出路径上的数值,即为需要求解的答案。target<0,该路径行不通,终止。
- 主体:
-
- 操作:target依次减candidates[i]数值,将candidates[i]存储到current当中
-
- 递归:
-
- 回溯:撤销操作,回到过去。
难点在于如何不产生重复的数组输出。
class Solution {
public:
void combinationSumHelper(vector<vector<int> > &res,vector<int>& current,vector<int>& candidates,int target,int lenth,int index){
//该函数用于辅助操作,index用来标记本层中candidates已经使用过的数字
//终止条件
if(target < 0) return;
if(target == 0) {//输出
res.push_back(current);
return;
}
//主体
for(int i=index;i<lenth;++i){
//操作
current.push_back(candidates[i]);
//递归
combinationSumHelper(res,current,candidates,target-candidates[i],lenth,i);//参数i非常关键
//回溯
current.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
//该函数用于返回组合总和,输入为candidates,和目标target,输出为列表
vector<vector<int> > res;
vector<int> current;
combinationSumHelper(res,current,candidates,target,candidates.size(),0);
return res;
}
};
时间:0ms,100.00%
空间:10.49MB,64.63%
22.括号生成(中等)
数字n代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。
示例 1:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
示例 2:
输入:n = 1
输出:[“()”]
思路:采用递归回溯的思路,首先画出回溯树。
- 每一层选择生成左括号或者右括号。并且要始终保持左括号的数量大于等于右括号的数量,如若不然,则进行剪枝
- 终止条件:生成左括号的数量和右括号的数量达到n。
class Solution {
public:
void generateParenthesisHelper(vector<string>& res,string& current,int n,int leftN,int rightN){
//该函数用于辅助完成括号的生成
/*res ---传入传出参数,输出结果
* current ---传入传出参数,保存当前已经生成的字符串
* n ---传入参数,生成的括号对数
* leftN ---传入参数,当前已经生成的左括号数
* rightN ---传入参数,当前已经生成的右括号数
*/
//终止条件
if(leftN == n && rightN==n) {
res.push_back(current);
return;
}
if(leftN < rightN || leftN > n || rightN > n) return; //剪枝,为不需要的情况
//函数主体
//插入左括号
current+="("; //操作
generateParenthesisHelper(res,current,n,leftN+1,rightN); //递归
current.pop_back(); //回溯
//插入右括号
current+=")"; //操作
generateParenthesisHelper(res,current,n,leftN,rightN+1); //递归
current.pop_back(); //回溯
}
vector<string> generateParenthesis(int n) {
//该函数用于括号的生成,输入参数n为生成的括号数目
vector<string> res;
string current;
generateParenthesisHelper(res,current,n,0,0);
return res;
}
};
79.单词搜索(中等)
给定一个mxn二维字符网格board和一个字符串单词word。如果word存在于网格中,返回true;否则,返回false。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例1:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
思路:网格的遍历,根据word的单词,在board网格中查找首字母。
- 如果查找不到首字母,说明不存在,返回false
- 如果查到首字母,在网格中相邻节点进行搜索。
-
- 已经查找过的进行标记
-
- 递归调用后续的字母。
-
- 回溯,取消标记
- 终止条件:1、下一个单词在相邻网格内搜索不到,返回,回溯。2、搜索超过网格边界,返回,回溯 3、当前网格的字母不是需要搜索的字母
class Solution {
public:
bool existHelper(vector<vector<char>>& board, const string& word, int index, int r, int c) {
//该函数用于辅助单词搜索
/*board --- 传入的网格
*word --- 搜索的单词
*index --- 当前判断的单词的第几个字母
*r,c --- 当前指向网格中节点的索引
*/
//终止条件
if (index == word.size()) return true; //word搜索完成
if (r < 0 || r >= board.size() || c < 0 || c >= board[0].size()) return false; //超过网格的边界
if (board[r][c] != word[index]) return false; //当前节点不等于当前搜索的word字母
char temp = board[r][c];
board[r][c] = '*'; // 标记网格为已经查找到的字母
bool found =
existHelper(board, word, index+1, r-1, c) ||
existHelper(board, word, index+1, r+1, c) ||
existHelper(board, word, index+1, r, c-1) ||
existHelper(board, word, index+1, r, c+1); //向相邻网格搜索
board[r][c] = temp; // 回溯,恢复当前网格的字母
return found;
}
bool exist(vector<vector<char>>& board, string word) {
//该函数用于判断单词是否在网格内能搜索到,board为网格,word为搜索的单词
int m = board.size();
int n = board[0].size();
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
//遍历网格,找到首字母,用existHelper函数判断剩余字母
if (board[i][j] == word[0] && existHelper(board, word, 0, i, j)) return true;
}
}
return false;
}
};
时间:420ms,70.76%
空间:7.97MB,41.86%
131.分割回文串(中等)(没看懂)
给你一个字符串s,请你将s分割成一些子串,使每个子串都是回文串。返回s所有可能的分割方案。
回文串是正着读和反着读都一样的字符串。
示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a”
输出:[[“a”]]
思路:遍历字符串s,将字符串前n个回文串分割出来,作为结果之一输出。递归调用后续剩余的字符串s’。直到s被分割完成停止。如果遇到分割之后的子串不是回文串,则需要回溯,并将子串作为结果之一输出。
自写:(编译器找不到removeFromEnd函数,不知道为什么)。也可以像官方思路一样,不必进行字符串的添加和删除操作。直接用指针指示当前的搜索区间即可。
class Solution {
public:
bool isPartition(const string& s) {
//该函数用于判断s是否是回文串
int n = s.size();
for (int i = 0; i < (n / 2); ++i) {
if (s[i] != s[n - 1 - i]) return false;
}
return true;
}
void partitionHelper(vector<vector<string>>& res, const string& s, int start, vector<string>& ans) {
//该函数用于辅助分割回文串
/*
* res --- 存储最后的结果
* s --- 需要分割的字符串
* start --- 分割的开始
* ans --- 暂存分割完的子串
*/
//终止条件
if (start == s.size()) {
//当s分割完成
res.push_back(ans);
return;
}
for (int end = start + 1; end <= s.size(); ++end) {
string s_son = s.substr(start, end - start); //分割的子串
if (isPartition(s_son)) {
//如果子串是回文串,则分割
ans.push_back(s_son); //操作,存储子串
partitionHelper(res, s, end, ans); //递归
ans.pop_back(); // 回溯
}
}
}
vector<vector<string>> partition(string s) {
//该函数用于分割回文串
vector<vector<string>> res;
vector<string> ans;
partitionHelper(res, s, 0, ans);
return res;
}
};
时间:104ms,88.28%
空间:47.33MB,94.75%
二分查找
35.搜索插入位置(简单)
给定一个排序数组和一个目标值,在数值中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为O(logn)的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
思路:将问题转化为在一个有序数组中找第一个大于等于target的下标。常用的二分查找—使用left、right、mid三个指针进行移动。
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
//该函数用于查找插入的位置
/* nums ---有序数组 target --- 需插入的元素*/
int n = nums.size();
int left = 0, right = n - 1, ans = n;
while (left <= right) {
int mid = ((right - left) >> 1) + left; //右移运算符,是一种快速除以2的处理方式,比用除法算法效率高
if (target <= nums[mid]) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
}
};
时间复杂度:O(logN),12ms,28.94%
空间复杂度:O(1),9.8MB,5.36%
74.搜索二维矩阵
给你一个满足下述两条属性的mXn的整数矩阵;
- 每行中的整数从左到右按非严格递增顺序排列
- 每行的第一个整数大于前一行的最后一个整数。
给你一个整数target,如果target在矩阵中,返回true;否则,返回false。
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true
思路:二分查找
- 如果查找成功,则返回true
- 如果前一个小于target,后一个大于target。返回false。
由于二维数组是有序的,所以当前的个数和脚标有对应的转化关系存在。
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
//通过二分查找,搜索二维矩阵matrix中是否存在target
int m = matrix.size();
int n = matrix[0].size();
int start = 0, end = n*m-1;
while(start <= end){
int mid = (end-start)/2 + start;
int midvalue = matrix[mid/n][mid%n];
if(midvalue > target) end = mid-1; //区间左移
else if (midvalue < target) start = mid+1; //区间右移
else return true;
}
return false;
}
};
时间复杂度:O(logN),0ms,100.00%
空间复杂度:O(1),9.42MB,23.74%
34.在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排序的整数数组nums,和一个目标值target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值target,返回[-1,-1]。
你必须设计并实现时间复杂度为O(logN)的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
思路:二分查找,同理
- 当查找到target后,向target前和后搜索,第一个不等于target的脚标。
- 如果没有找到target,说明不存在返回[-1,-1]。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
//同理二分查找
int n = nums.size();
int left = 0, right = n-1;
vector<int> res;
while(left<=right){
int mid = left + (right-left)/2;
if (nums[mid] < target) left = mid+1;
else if (nums[mid] > target) right = mid-1;
else {
//当查找到target的情况
int r=mid,c=mid;
while( r>0 && nums[r] == nums[r-1]){
r--;
}
res.push_back(r);
while(c < n-1 && nums[c] == nums[c+1]){
c++;
}
res.push_back(c);
return res;
}
}
return {-1,-1};
}
};
时间复杂度:O(logN),4ms,94.01%
空间复杂度:O(1),13.53MB,6.81%
33.搜索旋转排序数组(中等)
整数数组nums按升序排列,数组中的值互不相同。
在传递给函数之前,nums在预先未知的某个下标k(0<=k<nums.length)上进行了旋转,使数组变为[nums[k]],nums[k+1],…,nums[n-1],nums[0],nums[1],…,nums[k-1]](下标从0开始计数)。例如,【0,1,2,4,5,6,7】在下标3处经旋转后可能变为【4,5,6,7,0,1,2】。
给你旋转后的数组nums和一个整数target,如果nums中存在这个目标值target,则返回它的下标,否则返回-1。你必须设计一个时间复杂度为O(logN)的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
思路:nums为有序数组经部分旋转后的结果。
- 分为两个部分,其一,找出旋转的位置。然后分别在前后两部分进行查找。
- 同理,设置三个指针,left,mid,right。
- 如果mid的值大于left,说明left到mid的值都为升序数组。
-
- 如果target落在此区间,则二分查找
-
- 如果target不落在此区间,则在mid到right的区间内
- 如果mid的值小于left,说明mid到right的值都为升序数组,
-
- 如果target落在此区间,二分查找
-
- 如果target不落在此区间,则在left到mid的区间内
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = (int)nums.size();
if (!n) {
return -1;
}
if (n == 1) {
return nums[0] == target ? 0 : -1;
}
int l = 0, r = n - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target) return mid;
if (nums[0] <= nums[mid]) {
if (nums[0] <= target && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[n - 1]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return -1;
}
};
153.寻找旋转排序数组中的最小值(中等)
已知一个长度为n的数组,预先按照升序排列,经由1到n次旋转后,得到输入数组。例如,原数组nums=【0,1,2,4,5,6,7】在变化后可能得到:
- 若旋转4次,则可以得到【4,5,6,7,0,1,2】
- 若旋转7次,则可以得到【0,1,2,4,5,6,7】
给你一个元素值互不相同的数组nums,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的最小元素。
时间复杂度为O(logN)的算法
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
思路:与上题同理,最小的数字一定出现在旋转数组中。
最小值一定出现在数组的左侧。用折半查找,更好理解。虽然本质上都是二分查找。
class Solution {
public:
int findMin(vector<int>& nums) {
//折半查找
int left = 0, right = nums.size()-1;
while(left < right){
int mid = left + (right-left)/2;
if(nums[mid] < nums[right]){
right = mid;
}
else {
left = mid+1;
}
}
return nums[left];
}
};
时间复杂度:O(logN),0ms,100.00%
空间复杂度:O(1),9.92MB,45.96%
栈
20.有效的括号(简单)
给定一个只包括‘(’,‘)’,‘{’,‘}’,‘【’,‘】’的字符串s,判断字符串是否有效。
有效字符串需满足:
1、左括号必须用相同类型的右括号闭合。
2、左括号必须以正确的顺序闭合。
3、每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = “()”
输出:true
示例 2:
输入:s = “()[]{}”
输出:true
示例 3:
输入:s = “(]”
输出:false
思路:栈的基本应用
- 输入一个左括号,则入栈
- 输入一个右括号
-
- 如果栈不如空,则出栈,并比较与出栈元素能够构成一对括号。如能继续下一个元素,如不能则返回false
-
- 如果栈为空,则返回false
- 直到所有元素访问完成,如果栈为空则返回true,否则返回false。
class Solution {
public:
bool isValid(string s) {
//该函数用于判断是否是有效的括号,为栈的基本应用
stack<char> stack;
for(char test:s){
if(stack.empty() && (test == ')' || test == '}' || test == ']') ) return false;
if(test == '(' || test == '{' || test == '[') stack.push(test); //为左括号,则入栈
if(test == ')' && !stack.empty()) {
char temp = stack.top();
stack.pop();
if(temp != '(') return false;
}
if(test == '}'&& !stack.empty()){
char temp = stack.top();
stack.pop();
if(temp != '{') return false;
}
if(test == ']'&& !stack.empty()){
char temp = stack.top();
stack.pop();
if(temp != '[') return false;
}
}
if(!stack.empty() ) return false;
return true;
}
};
时间复杂度:O(N),0ms,100.00%
空间复杂度:O(N),6.44MB,18.10%
155.最小栈(中等)
设计一个支持push,pop,top操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack类
- MinStack()初始化堆栈对象。
- void push(int val)将元素val推入堆栈
- void pop()删除堆栈顶部的元素
- int top()获取堆栈顶部的元素
- int getMin()获取堆栈中的最小元素。
示例 1:
输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
思路:问题的难点在于在常数时间内检索到最小元素。因此,可以想到使用一个辅助栈,来存储当前元素入栈时,栈内已有的最小值。辅助栈与栈构成了一对元素。同时入栈和同时出栈。辅助栈存储的是主栈对应元素下的最小值。
class MinStack {
private:
stack<int> main_stack;
stack<int> min_stack; //辅助栈
public:
MinStack() {
//该函数用于初始化最小栈
min_stack.push(INT_MAX);
}
void push(int val) {
//该函数用于将元素val推入堆栈中
main_stack.push(val);
min_stack.push(min(val,min_stack.top()));
}
void pop() {
//该函数用于删除堆栈顶部的元素
main_stack.pop();
min_stack.pop();
}
int top() {
//该函数用于返回堆栈顶部元素
return (int)main_stack.top();
}
int getMin() {
//该函数用于返回堆栈的最小值
return (int)min_stack.top();
}
};
394.字符串解码(中等)
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为:K[encoded_string],表示其中方括号内部的 encoded_string正好重复k次。注意k保证为正整数。你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数k,例如不会出现像3a或2[4]的输入。
示例 1:
输入:s = “3[a]2[bc]”
输出:“aaabcbc”
示例 2:
输入:s = “3[a2[c]]”
输出:“accaccacc”
示例 3:
输入:s = “2[abc]3[cd]ef”
输出:“abcabccdcdcdef”
示例 4:
输入:s = “abc3[cd]xyz”
输出:“abccdcdcdxyz”
思路:
数字k作为需要输出的次数,即循环输出的循环次数。左括号作为输入字符串开始的标记,右括号作为输入字符串结束的标记。用栈保存输入的字符串。堆栈保存的数据为string,数字、子母串、括号作为三个独立的string存储。本质上就是将string进行切片
如果当前的字符为数位,解析出一个数字(连续的多个数位)并进栈
如果当前的字符为字母或者左括号,直接进栈
如果当前的字符为右括号,开始出栈,一直到左括号出栈,出栈序列反转后拼接成一个字符串,此时取出栈顶的数字(此时栈顶一定是数字),就是这个字符串应该出现的次数,我们根据这个次数和字符串构造出新的字符串并进栈
class Solution {
public:
string getDigits(string &s, size_t &ptr) {
//该函数用于解析出数字
/*s --- 为需解析的字符串
*ptr--- 为指向的字符串下标
*返回值为解析出的数字,类型为字符串
*/
string ret = "";
while (isdigit(s[ptr])) {
ret.push_back(s[ptr++]);
}
return ret;
}
string getString(vector <string> &v) {
//该函数用于解析出字母串
string ret;
for (const auto &s: v) {
ret += s;
}
return ret;
}
string decodeString(string s) {
//该函数用于字符串解码
vector <string> stk;
size_t ptr = 0;
while (ptr < s.size()) {
char cur = s[ptr];
if (isdigit(cur)) {
// 获取一个数字并进栈
string digits = getDigits(s, ptr);
stk.push_back(digits);
} else if (isalpha(cur) || cur == '[') {
// 获取一个字母并进栈
stk.push_back(string(1, s[ptr++]));
} else {
++ptr;
vector <string> sub;
while (stk.back() != "[") {
sub.push_back(stk.back());
stk.pop_back();
}
reverse(sub.begin(), sub.end());
// 左括号出栈
stk.pop_back();
// 此时栈顶为当前 sub 对应的字符串应该出现的次数
int repTime = stoi(stk.back());
stk.pop_back();
string t, o = getString(sub);
// 构造字符串
while (repTime--) t += o;
// 将构造好的字符串入栈
stk.push_back(t);
}
}
return getString(stk);
}
};
时间复杂度:O(N),0ms,100.00%
空间复杂度:O(N),6.63MB,32.84%
739.每日温度(中等)(没看懂)
给定一个整数数组temperatures,表示每天的温度,返回一个数组answer,其中answer[i]是指对于第i天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用0来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
示例 2:
输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]
示例 3:
输入: temperatures = [30,60,90]
输出: [1,1,0]
思路:利用单调栈的思想。看图解。维护一个存储数组下标的单调栈。当当前栈非空,并且当前指向元素大于栈顶元素时,当前指向元素就是栈顶元素之后的第一个较大元素。两个下标差就是需要得到的结果。
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> res(n, 0); //结果初始化为0
stack<int> st;
for (int i = 0; i < temperatures.size(); ++i) {
while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
//当栈非空,并且当前第i天的温度大于栈顶元素的温度
auto t = st.top(); st.pop();
res[t] = i - t;
}
st.push(i);
}
return res;
}
};
时间复杂度:O(N),160ms,63.61%
空间复杂度:O(N),98.68MB,19.74%
堆(不会呀)
215.数组中的第k个最大元素(中等)
给定整数数组nums和整数k,请返回数组中第k个最大的元素。
请注意,你需要找的是数组排序后的第k个最大的元素,而不是第k个不同的元素。
你必须设计并实现时间复杂度为O(N)的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
思路:直接将nums排序后输出。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
int n=nums.size();
sort(nums.begin(),nums.end());
return nums[n-k];
}
};
时间复杂度:O(NlogN),92ms,79.6%
空间复杂度:O(1),53.01MB,54.46%
时间复杂度不符合要求。
思路二:快速排序。
快速排序的核心包括“哨兵划分”和“递归”
- 哨兵划分:以数组某个元素(一般选取首元素)为基准数,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。
- 递归:对左子数组和右子数组递归执行哨兵划分,直至子数组长度为1时,终止递归,即可完成对整个数组的排序。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
return quickSelect(nums, k);
}
private:
int quickSelect(vector<int>& nums, int k) {
// 随机选择基准数
int pivot = nums[rand() % nums.size()];
// 将大于、小于、等于 pivot 的元素划分至 big, small, equal 中
vector<int> big, equal, small;
for (int num : nums) {
if (num > pivot)
big.push_back(num);
else if (num < pivot)
small.push_back(num);
else
equal.push_back(num);
}
// 第 k 大元素在 big 中,递归划分
if (k <= big.size())
return quickSelect(big, k);
// 第 k 大元素在 small 中,递归划分
if (nums.size() - small.size() < k)
return quickSelect(small, k - nums.size() + small.size());
// 第 k 大元素在 equal 中,直接返回 pivot
return pivot;
}
};
时间复杂度:O(N),80ms,89.15%
空间复杂度:O(logN),74.45MB,5.01%
347.前k个高频元素(中等)(不会)
给你一个整数数组nums和一个整数k,请你返回其中出现频率前k高的元素。你可以按任意顺序返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
思路:
- 先利用哈希表,建立的键值对为 元素-频率。对哈希表的元素按照频率的高低进行排序。
- 维护一个元素数目为k的最小堆
- 每次都将新的元素与堆顶元素(堆中频率最小的元素)进行比较
- 如果新的元素的频率比堆顶端的元素大,则弹出堆顶端的元素,将新的元素添加到堆中
- 最终,堆中的k个元素即为前k个高频元素
class Solution {
public:
// 小顶堆
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
// 要统计元素出现频率
unordered_map<int, int> map; // map<num[i], 对应出现次数>
for (int i = 0; i < nums.size(); i++) {
map[nums[i]]++;
}
// 对频率排序
// 定义一个小顶堆,大小为k
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
// 用固定大小为k的小顶堆,扫描所有频率的数值
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
pri_que.push(*it);
if (pri_que.size() > k) {// 如果堆的大小大于k,则队列弹出,保证堆的大小一直为k
pri_que.pop();
}
}
// 找出前k个高频元素,因为小顶堆先弹出的是最小的,所以倒序输出到数组
vector<int> ans(k);
for (int i = k - 1; i >= 0; i--) {
ans[i] = pri_que.top().first;
pri_que.pop();
}
return ans;
}
};
时间复杂度:
空间复杂度:
贪心算法
121.买卖股票的最佳时机(简单)
给定一个数组prices,它的第i个元素prices[i]表示一支给定股票第i填的价格。
你只能选择某一天买入这只股票,并选择在未来的某个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
思路:贪心算法,遍历数组prices,更新前i天的最低成本,同步更新第i天的最大利润。返回出现的最大利润即可。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int cost = INT_MAX;
int profit = 0;
for(int price : prices){
cost = min(cost,price);
profit = max(profit,price-cost);
}
return profit;
}
};
时间复杂度:O(N),88ms‘,93.78%
空间复杂度:O(1),89.39MB,42.66%
55.跳跃游戏(中等)
给你一个非负整数数组nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标,如果可以,返回true,否则返回false。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
思路:遍历数组nums,记录前i个数所能到达的最远下标。如果最远下标超过了数组nums的大小则能到达,否则不能到达。
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxLenth = 0;
int n = nums.size();
for(int i=0; i<n; ++i){
if(maxLenth>=i){
maxLenth = max(maxLenth,i+nums[i]);
}
if(maxLenth >= n-1) return true;
if(i==maxLenth && nums[i]==0) return false;
}
return false;
}
};
时间复杂度:O(N),56ms,45.49%
空间复杂度:O(1),46.71MB,10.45%
45.跳跃游戏二(中等)
给定一个长度为n的0索引整数数组nums。初始位置为nums[0]。
每个元素nums[i]表示从索引i向前跳转的最大长度。换句话说,如果你在nums[i]处,你可以跳转到任意nums[i+j]处:
- 0<=j<=nums[i]
- i+j<n
返回到达nums[n-1]的最小跳跃次数。生成的测试用例可以到达nums[n-1]。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
思路:要求最小的跳跃次数,即贪心的思想。选择单次跳跃得最远的位置。
class Solution {
public:
int jump(vector<int>& nums)
{
int ans = 0;
int end = 0;
int maxPos = 0;
for (int i = 0; i < nums.size() - 1; i++)
{
maxPos = max(nums[i] + i, maxPos);
if (i == end)
{
//当前一跳所能到达的最远距离,则进行下一跳
end = maxPos;
ans++;
}
}
return ans;
}
};
时间复杂度:O(N),4ms,99.70%
空间复杂度:O(1),16.04MB,56.80%
763.划分字母区间(中等)
给你一个字符串s,我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是s。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”、“defegde”、“hijhklij” 。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = “eccbbbbdec”
输出:[10]
思路:如果有前后两个相同的字母,则需要将它们划分到一个片段中。因此,需要遍历字符串,得到每个字母最后一次出现的下标位置。
class Solution {
public:
vector<int> partitionLabels(string s) {
//
int start = 0;
int end = 0;
int n = s.size();
int last[26];
//遍历一遍,记录每个字母最后出现的位置
for(int i=0; i<n; ++i){
last[s[i]-'a'] = i;
}
vector<int> ans;
//划分片段
for(int i=0; i<n;i++){
end = max(end,last[s[i]-'a']);
if(i==end){
ans.push_back(end-start+1);
start = end+1;
}
}
return ans;
}
};
时间复杂度:O(N),4ms,69.62%
空间复杂度:O(1),6.56MB,57.19%
动态规划
动态规划在寻找很多重叠子问题的情况很有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并存储,从简单的问题直到整个问题都被解决。
如果一个问题满足一下两点,那么它就能用动态规划解决
- 问题的答案依赖于问题的规模,也就是问题的所有答案构成了一个数列。
- 大规模问题的答案可以由小规模问题的答案递推得到。
应用动态规划——将动态规划拆分为三个子目标
- 建立状态转移方程
这一步是最难的。已经知道f(1)~f(n-1)的值,想办法利用它们求得f(n)。 - 缓存并复用以往结果
- 按顺序从小往大算
大小指的是问题的规模,
动态规划的四个解题步骤:
- 定义子问题
- 写出子问题的递推关系
- 确定DP数组的计算顺序
- 空间优化(可选)
70.爬楼梯(简单)
假设你正在爬楼梯,需要n阶你才能到达楼顶。
每次你可以爬1或2个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
. 1 阶 + 1 阶
. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1 阶 + 1 阶 + 1 阶
. 1 阶 + 2 阶
. 2 阶 + 1 阶
思路:在每一层,你要么爬一个或者两个台阶,不管怎么爬,只要最终的总台阶数目=N,就是到达楼顶。记录有多少种路径。
列出动态规划的转移方程:
f(x) = f(x-1)+f(x-2)
第x级台阶的方案数是爬到第x-1级台阶的方案数和第x-2级台阶的方案数的和。
边界条件:
f(0)=1,f(1)=1。求出对应的f(n)。
class Solution {
public:
int climbStairs(int n) {
int p=0,q=0,r=1;
for(int i=1; i<=n; ++i){
p=q;
q=r;
r=p+q;
}
return r;
}
};
时间复杂度:O(N),0ms,100.00%
空间复杂度:O(1),5.98MB,53.62%
分析思路:利用动态规划的标准三步法:子问题,转移方程,DP数组求解
class Solution {
public:
int climbStairs(int n) {
if(n==0) return 0;
if(n==1) return 1;
if(n==2) return 2;
vector<int> DP(n+1,0);
DP[0] = 0;
DP[1] = 1;
DP[2] = 2;
for(int i=3;i<=n;i++){
DP[i] = DP[i-1]+DP[i-2];
}
return DP[n];
}
};
时间复杂度:O(N),0ms,100.00%
空间复杂度:O(N),6.23MB,20.70%
118.杨辉三角(简单)
给定一个非负整数numRows,生成杨辉三角的前numRows行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
示例 1:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入: numRows = 1
输出: [[1]]
思路:直接按照杨辉三角的结构进行构造
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> ret(numRows);
for (int i = 0; i < numRows; ++i) {
ret[i].resize(i + 1); //每行重铸大小
ret[i][0] = ret[i][i] = 1; //每行首尾为1
for (int j = 1; j < i; ++j) {
//该行的第j个等于上一行的第j个加上上一行的第j-1个元素
ret[i][j] = ret[i - 1][j] + ret[i - 1][j - 1];
}
}
return ret;
}
};
时间复杂度:O(N),0ms,100.00%。N为元素的个数
空间复杂度:O(N),6.71MB,50.58%
分析思路:按照三要素求解
198.打家劫舍(中等)(一题详解动态规划)
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏着一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统就会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
思路:选择最大的一个数字进行选择,选择之后相邻两个数字就不可以选择。再在剩下可选的数字中选择次大的数字。(局部最优不一定是全局最优,不行)。
定义子问题:
子问题的递推关系:
确定DP数组(用来存储子问题的解)的计算顺序:
空间优化:(优化DP数组)
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n==1) return nums[0];
vector<int> DP(n+1,0);
DP[0] = 0;
DP[1] = nums[0];
for(int i=2;i<=n;i++){
DP[i] = max(DP[i-1],DP[i-2]+nums[i-1]);
}
return DP[n];
}
};
时间复杂度:O(N),0ms,100.00%
空间复杂度:O(N),7.76MB,33.47%
279.完全平方数(中等)
给你一个整数n,返回和为n的完全平方数的最少数量。
完全平方数是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9和16都是完全平方数,而3和11不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
思路:子问题,递推关系,DP数组。
子问题:f[i]表示最少需要多少个数的平方来表示整数i。我们需要枚举这些数,同时注意到这些数zhi,假设当前枚举到j,那么我们还需要取若干数的平方,构成i-j^2,此时,我们发现该子问题和原问题类似,只是规模变小了。这符合了动态规划的要求,于是我们可以写出状态转移方程。
递推关系:
class Solution {
public:
int numSquares(int n) {
vector<int> f(n+1);
for(int i=1;i<=n;++i){
int minn = INT_MAX;
//解决子问题
for(int j=1;j*j<=i;j++){
minn = min(minn,f[i-j*j]);
}
f[i] = minn+1;
}
return f[n];
}
};
时间复杂度:O(N*根号N),68ms,89.71%
空间复杂度:O(N),9.10MB,24.46%