Hot 100(31~40)
Hot 100(31~40)
31.颜色分类
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库的sort函数的情况下解决这个问题。
这是一道考察排序的算法题。最简单是统计每个元素个数,然后重写整个数组,但题目要求原地排序。题目中只有三个元素,只要把两个元素排好序之后,另外一个自然是顺序排列。
单指针
具体做法:用变量ptr
保存0的范围,第一次扫描数组,将遇到的0也就是nums[i]
与nums[ptr]
进行交换。第二次扫描,将遇到的1也就是nums[i]
与nums[ptr]
交换,循环结束之后数组顺序排列。
void sortColors(vector<int>& nums)
{
int n = nums.size(), ptr = 0;
for (int i = 0; i < n; i ++)
{
if (nums[i] == 0)
{
swap(nums[i], nums[ptr]);
ptr ++;
}
}
for (int i = ptr; i < n; i ++)
{
if (nums[i] == 1)
{
swap(nums[i], nums[ptr]);
ptr ++;
}
}
}
时间复杂度$O(n)$,空间复杂度$O(1)$
双指针
上述做法需要两次扫描,如果用两个指针则可以单次扫描完成排列。具体做法:前后两个指针,当nums[i]
遇到0时,和指针p0
交换,遇到2时和指针p2
交换。当 i 大于 p2时,说明已经交换完毕。
void sortColors(vector<int>& nums)
{
int p0 = 0, p2 = nums.size() - 1;
for (int i = 0; i <= p2; i ++)
{
while (i <= p2 && nums[i] == 2)
{
swap(nums[i], nums[p2]);
p2 --;
}
if (nums[i] == 0)
{
swap(nums[i], nums[p0]);
p0 ++;
}
}
}
时间复杂度$O(n)$,空间复杂度$O(1)$
33.子集
给你一个整数数组
nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
这种暴搜的题目要想到dfs,需要记录保存的路径path。
vector<vector<int>> ans;
vector<int> path;
void dfs(int u, vector<int>& nums)
{
if (u == nums.size())
{
ans.push_back(path);
return;
}
path.push_back(nums[u]);
dfs(u + 1, nums);
path.pop_back();
dfs(u + 1, nums);
}
vector<vector<int>> subsets(vector<int>& nums)
{
dfs(0, nums);
return ans;
}
34.单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
这题就是剑指Offer上的矩阵中的路径,主要做法是用回溯。
回溯
bool exist(vector<vector<char>>& board, string word)
{
for(int i = 0; i < board.size(); i++)
for(int j = 0; j < board[i].size(); j++)
if(dfs(board,word,0,i,j)) return true;
return false;
}
int dx[4] = {-1,0,1,0}, dy[4] = {0,1,0,-1}; //方向数组
bool dfs(vector<vector<char>>& board, string& word,int u,int x,int y)
{
if(board[x][y] != word[u]) return false;
if(u == word.size() - 1) return true;
char t = board[x][y];
board[x][y] = '.';
for(int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
//出界或者走到已经搜索过的位置
if(a < 0 || a >= board.size() || b < 0 || b >= board[0].size() || board[a][b] == '.') continue;
if(dfs(board,word,u+1,a,b)) return true;
}
board[x][y] = t;
return false;
}
37.二叉树的中序遍历
给定一个二叉树的根节点
root
,返回 它的 中序 遍历 。
中序遍历是指按照左子树——根节点——右子树的顺序遍历二叉树。
递归
void inorder(TreeNode* root, vector<int>& res)
{
if (!root) return; // 如果为空,结束递归
inorder(root->left, res);
res.push_back(root->val);
inorder(root->right, res);
}
vector<int> inorderTraversal(TreeNode* root)
{
vector<int> res;
inorder(root, res);
return res;
}
时间复杂度$O(n)$,空间复杂度$O(n)$
迭代
迭代的方法和递归类似,但是需要用辅助栈来存储向下遍历的根节点。
vector<int> inorderTraversal(TreeNode* root)
{
vector<int> res;
stack<TreeNode*> stk;
while (root != nullptr || !stk.empty())
{
while (root != nullptr)
{
stk.push(root);
root = root->left;
} // 退出循环时说明已经达到左子树的空节点
root = stk.top(); // 取出上一个节点
stk.pop();
res.push_back(root->val);
root = root->right; // 循环右子节点的子树
}
return res;
}
时间复杂度$O(n)$,空间复杂度$O(n)$
38.不同的二叉搜索树
这题看似考察二叉树,其实考察排列组合,问有多少种排列方式。
动态规划
遍历每个节点,把每个节点当作根节点,左边的数当作左子树,右边的数当作右子树,然后用相同的方法递归构造二叉树。该节点能够构造的数量等于左子树能够构造的数量乘上右子树能够构造的数量,也就是取笛卡尔积。
$$
G(n)=\sum_{i=1}^nG(i−1)⋅G(n−i)
$$ {n个节点构建的二叉树数量}
int numTrees(int n)
{
vector<int> G(n + 1, 0);
G(0) = 1;
G(1) = 1; // 边界条件,没有节点和1个节点只有一种情况
for (int i = 2; i <= n; i ++)
for (int j = 1; j <= i; j ++)
G[i] += G(j - 1) * G(i - j);
return G[n];
}
时间复杂度$O(n^2)$,空间复杂度$O(n)$,存储G数组
39.验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
循环判断每个节点左子节点是否小于根节点,右子节点是否大于根节点。可以使用递归和中序迭代来完成。和37题差不多。
递归
设计一个递归函数helper,里面三个参数(根节点,下界,上界)。递归调用时,每个左子节点的上界为根节点的值,每个右子节点的下界为根节点的值。root调用该函数时上界下界为无穷大或无穷小。
bool helper(TreeNode* root, long long lower, long long upper)
{
if (root == nullptr) return true;
if (root->val <= lower || root->val >= upper) return false;
return helper(root->left, lower, root->val) && helper(root->right, root->val, upper);
}
bool isValidBST(TreeNode* root)
{
return helper(root, LONG_MIN, LONG_MAX);
}
时间复杂度$O(n)$,空间复杂度$O(n)$,最坏情况退化成单链表需要递归n层
中序迭代
bool isValidBST(TreeNode* root)
{
stack<TreeNode*> stk;
long long inorder = (long long)INT_MIN - 1;
while (!stk.empty() || root != nullptr)
{
while (root != nullptr)
{
stk.push_back(root);
root->val;
}
root = stk.top();
stk.pop();
if (root->val <= inorder) return false;
inorder = root->val;
root = root->right;
}
return true;
}
40.对称二叉树
给你一个二叉树的根节点
root
, 检查它是否轴对称。
左子树的左子节点和右子树的右子节点对应。左子树的右子节点和右子树的左子节点对应。
递归
bool check(TreeNode* l, TreeNode* r)
{
if (!l && !r) return true;
if (!l || !r) return false;
return l->val == r->val && check(l->left, r->right) && check(l->right, r->left);
}
bool isSymmetric(TreeNode* root)
{
return check(root, root);
}
时间复杂度$O(n)$,空间复杂度$O(n)$
迭代
引入一个队列,这是把递归程序改写成迭代程序的常用方法。初始化时我们把根节点入队两次。每次提取两个结点并比较它们的值(队列中每两个连续的结点应该是相等的,而且它们的子树互为镜像),然后将两个结点的左右子结点按相反的顺序插入队列中。当队列为空时,或者我们检测到树不对称(即从队列中取出两个不相等的连续结点)时,该算法结束。
bool check(TreeNode* l, TreeNode* r)
{
queue<TreeNode*> q;
q.push_back(l), q.push_back(r);
while (!q.empty())
{
l = q.front(), q.pop();
r = q.front(), q.pop();
if (!l && !r) continue;
if ((!l || !r) || (l->val != r->val)) return fasle;
q.push(l->left);
q.push(r->right);
q.push(l->right);
q.push(r->left);
}
return true;
}
bool isSymmetric(TreeNode* root)
{
return check(root, root);
}