1. 数据结构——树:
1.1 BFS搜索算法:
BFS概念 :
广搜 是一种图搜索算法,用于在图或树中系统地遍历或搜索节点。它从起始节点开始,逐层地向外扩展搜索,直到找到目标节点或遍历完整个图。
BFS算法的详细过程如下 :
- 从起始节点开始,将其标记为已访问,并将其放入一个队列中。
- 从队列中取出队首节点,并检查它的所有邻居节点。
- 对于每个邻居节点,如果该节点未被访问过,则将其标记为已访问,并将其放入队列的末尾。
- 重复步骤2和步骤3,直到队列为空。
- 如果目标节点被找到,搜索结束;否则,说明图中不存在目标节点。
BFS的特点 :
是按照距离从起始节点逐层扩展搜索,先遍历距离起始节点近的节点,再遍历距离起始节点稍远的节点。因此,BFS可以用于求解最短路径问题,例如在无权图中寻找两个节点之间的最短路径。
BFS的算法性能 :
BFS的时间复杂度为O(|V|+|E|),其中|V|是图中节点的数量,|E|是图中边的数量。
BFS通常需要定义额外的数据结构去存储遍历过的结点,这却决于具体的使用场景,通常来说,使用队列的空间复杂度为 O(n) | O(|E|)。
BFS的经典案例:
E1. 把二叉树打印成多行(JZ78)
牛客链接:把二叉树打印成多行牛客题霸牛客网 (nowcoder.com)
#include <deque>
class Solution {
public:
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int>> ans;
if (!pRoot) return ans; // 处理传入树根为空的情况
deque<TreeNode*> deque;
deque.push_back(pRoot); // 先把头结点压入队列
while (!deque.empty()){
int len = deque.size();
vector<int> tempList;
for (int i = 0; i < len;i++){
TreeNode* cur = deque.front();
deque.pop_front(); // 取到队列第一个值
if (cur->left) deque.push_back(cur->left);
if (cur->right) deque.push_back(cur->right);
tempList.push_back(cur->val);
}
ans.push_back(tempList);
}
return ans;
}
};
E2.按之字形顺序打印二叉树(JZ77)
牛客链接:按之字形顺序打印二叉树牛客题霸牛客网 (nowcoder.com)
#include <deque>
class Solution {
public:
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int>> ans;
if (!pRoot) return ans;
deque<TreeNode*> deque ;
int depth = 0;
deque.push_back(pRoot);
while (!deque.empty()) {
depth++;
int currSize = deque.size();
vector<int> res;
for (int i = 0;i < currSize;i++){
TreeNode* tmp = deque.front();
deque.pop_front();
if (tmp->left) deque.push_back(tmp->left);
if (tmp->right) deque.push_back(tmp->right);
// 利用深度 做不同分支逻辑
if (depth % 2) res.push_back(tmp->val);
else res.insert(res.begin(), tmp->val);
}
ans.push_back(res);
}
return ans;
}
};
1.2 DFS搜索算法:
在树的遍历过程中,我们按照一定规则依次访问树的每个节点,以获取树中的所有节点信息。树的遍历方式可以分为以下三种常见的方式:前序遍历、中序遍历和后序遍历。
前序遍历(Preorder Traversal) :
- 访问根节点。
- 对根节点的左子树进行前序遍历。
- 对根节点的右子树进行前序遍历。
中序遍历(Inorder Traversal) :
- 对根节点的左子树进行中序遍历。
- 访问根节点。
- 对根节点的右子树进行中序遍历。
后序遍历(Postorder Traversal) :
- 对根节点的左子树进行后序遍历。
- 对根节点的右子树进行后序遍历。
- 访问根节点。
此外,还有一种常见的树遍历方式是层序遍历(Level Order Traversal),它是按照从上到下、从左到右的顺序逐层遍历树的节点。层序遍历通常使用队列来实现。BFS此处思想不再过多赘述。
E1.重建二叉树(JZ7)
牛客链接:重建二叉树牛客题霸牛客网 (nowcoder.com)
#include <vector>
class Solution {
public:
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
int m = pre.size();
int n = vin.size();
if (!m || !n || m != n) return NULL;
TreeNode* root = new TreeNode(pre[0]); // 根节点
for (int i = 0; i < n;i++){
//找到中序遍历中 根节点 的位置i
if (pre[0] == vin[i]){
// 左子树的前序遍历
vector<int> left_pre (pre.begin() + 1, pre.begin() + i + 1);
// 左子树的中序遍历
vector<int> left_vin (vin.begin(), vin.begin() + i);
// 构建左子树
root->left = reConstructBinaryTree(left_pre, left_vin);
// 右子树的前序
vector<int> right_pre (pre.begin() + i + 1, pre.end());
// 右子树的中序
vector<int> right_vin (vin.begin() + i + 1, vin.end());
// 构建右子树
root->right = reConstructBinaryTree(right_pre, right_vin);
}
}
return root;
}
};
E2.树的子结构(JZ26)
牛客链接:树的子结构牛客题霸牛客网 (nowcoder.com)
class Solution {
public:
//递归判断结点值以及子节点是否相等
bool IsSame (TreeNode* root1, TreeNode* root2){
bool left = true, right = true;
if (!root1) return false;
if (root1->val != root2->val) return false;
if (root2->left) left = IsSame(root1->left, root2->left);
if (root2->right) right = IsSame(root1->right, root2->right);
return left && right;
}
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) {
if (!pRoot1 || !pRoot2) return false;
if (IsSame(pRoot1, pRoot2)) return true;
if (HasSubtree(pRoot1->left, pRoot2) || HasSubtree(pRoot1->right, pRoot2)) return true;
else return false;
}
};
E3. 二叉搜索树的后序遍历序列(JZ33)
牛客链接:二叉搜索树的后序遍历序列牛客题霸牛客网 (nowcoder.com)
#include <vector>
class Solution {
public:
bool VerifySquenceOfBST(vector<int> sequence, int l,int r){
if (l >= r) return true;
int root = sequence[r];
int splitPoint = l;
while (splitPoint < r && sequence[splitPoint] < root) splitPoint++; // 到右子树的第一个节点
// 检查右子树
for (int i = splitPoint; i < r;i++){
if (sequence[i] < root) return false;
}
// 分别检查 左子树 和 右子树
return VerifySquenceOfBST(sequence, l, splitPoint - 1) && VerifySquenceOfBST(sequence, splitPoint, r - 1);
}
bool VerifySquenceOfBST(vector<int> sequence) {
if (!sequence.size()) return false;
return VerifySquenceOfBST(sequence, 0, sequence.size()-1);
}
};
E4.二叉树中和为某一值的路径2(JZ34)
牛客链接:二叉树中和为某一值的路径(二)牛客题霸牛客网 (nowcoder.com)
#include <vector>
class Solution {
public:
vector<vector<int>> ans;
vector<int> tmp;
void FindPathDFS(TreeNode* cur, int expectNumber, int sum){
if (!cur) return;
sum += cur->val;
tmp.push_back(cur->val);
if (!cur->left && !cur->right && sum == expectNumber) ans.push_back(tmp);
FindPathDFS(cur->left, expectNumber, sum);
FindPathDFS(cur->right, expectNumber, sum);
tmp.pop_back();
}
vector<vector<int>> FindPath(TreeNode* root,int expectNumber) {
if (!root) return ans;
FindPathDFS(root, expectNumber, 0);
return ans;
}
};
总结 :求遍历路径的过程使用了深搜中的 剪枝 思想,切记在发现一条路走不通时,需要回溯到上层中继续进行另一条路的搜索。
E5.二叉树中和为某一值的路径3
牛客链接:二叉树中和为某一值的路径(三)牛客题霸牛客网 (nowcoder.com)
class Solution {
public:
int cnt = 0;
void FindPathDFS(TreeNode* root, int sum, int cur){
if (!root) return;
cur += root->val;
if (cur == sum) cnt++;
FindPathDFS(root->left, sum, cur);
FindPathDFS(root->right, sum, cur);
}
int FindPath(TreeNode* root, int sum) {
if (!root) return 0;
FindPathDFS(root, sum, 0);
FindPath(root->left, sum);
FindPath(root->right, sum);
return cnt;
}
};
总结:相比寻路2来说,去掉了 必须从头开始,从尾结束 的限制,意味着我们需要对每一个结点进行DFS寻路,此外,“剪枝” 操作可以通过函数形参的方式完美实现自动回溯。
E6.二叉搜索树与双向链表(JZ36)
牛客链接:二叉搜索树与双向链表牛客题霸牛客网 (nowcoder.com)
class Solution {
public:
TreeNode* preNode = NULL;
void MidTraversal(TreeNode* cur){
if (!cur) return;
MidTraversal(cur->left);
//双向链表是按顺序排列,故逻辑都写在中序遍历里
cur->left = preNode;
if (preNode) preNode->right = cur;
preNode = cur;
MidTraversal(cur->right);
}
TreeNode* Convert(TreeNode* pRootOfTree) {
if (!pRootOfTree) return pRootOfTree;
TreeNode* head = pRootOfTree;
while (head->left) head = head->left; // 双向链表的头
MidTraversal(pRootOfTree);
return head;
}
};
E7.在二叉树中找到两个节点的最近公共祖先(JZ86)
牛客链接:在二叉树中找到两个节点的最近公共祖先牛客题霸牛客网 (nowcoder.com)
核心思想:
以当前节点作为根节点,分别向 左,右子节点查找两点:
- 如果查到 哪边为空,则递归查找不为空的一边的子节点。
- 如果查到两边都不为空,则 公共父节点就是当前结点自己。
class Solution {
public:
TreeNode* findCommonParent(TreeNode* curr, int o1, int o2){
if (!curr || curr->val == o1 || curr->val == o2)
return curr; // 找到了返回当前节点
TreeNode* leftTar = findCommonParent(curr->left, o1, o2);
TreeNode* rightTar = findCommonParent(curr->right, o1, o2);
if (!leftTar) return rightTar;
if (!rightTar) return leftTar;
return curr;
}
int lowestCommonAncestor(TreeNode* root, int o1, int o2) {
return findCommonParent(root, o1, o2)->val;
}
};
1.3 树的镜像,对称与平衡
E1.二叉树的镜像(JZ27)
牛客链接:二叉树的镜像牛客题霸牛客网 (nowcoder.com)
class Solution {
public:
TreeNode* Mirror(TreeNode* pRoot) {
// write code here
if (!pRoot || (!pRoot->left && !pRoot->right)) return pRoot;
TreeNode* tmp = pRoot->left;
pRoot->left = Mirror(pRoot->right);
pRoot->right = Mirror(tmp);
return pRoot;
}
};
E2.判断是不是平衡二叉树(JZ79)
牛客链接:判断是不是平衡二叉树牛客题霸牛客网 (nowcoder.com)
性质:空树或者左右子树高度差绝对值不超过1。
class Solution {
public:
map<TreeNode*, int> node_dep_map ; // 存储树中每个结点的深度
int Depth(TreeNode* curr ){ // 构建 [结点 <--> 以该点为根的最大深度] 集合
if (!curr) return 0;
if (node_dep_map.find(curr) != node_dep_map.end()) return node_dep_map[curr]; // 没从子类读取到
int ldepth = Depth(curr->left);
int rdepth = Depth(curr->right);
return node_dep_map[curr] = max(ldepth, rdepth) + 1;
}
bool IsBalance(TreeNode* curr){ // 判断某节点是否是平衡节点
if (!curr) return true;
return abs(node_dep_map[curr->left] - node_dep_map[curr->right]) <= 1 &&
IsBalance(curr->left) && IsBalance(curr->right); // 满足自身平衡的同时保证子节点满足
}
bool IsBalanced_Solution(TreeNode* pRoot) {
Depth(pRoot);
return IsBalance(pRoot);
}
};
E3.对称的二叉树(JZ28)
牛客链接:对称的二叉树牛客题霸牛客网 (nowcoder.com)
class Solution {
public:
bool recursion(TreeNode* ltree, TreeNode* rtree){
if (!ltree && !rtree) return true;
if (!ltree || !rtree || ltree->val != rtree->val) return false;
return recursion(ltree->left, rtree->right) && recursion(ltree->right, rtree->left);
}
bool isSymmetrical(TreeNode* pRoot) {
return recursion(pRoot, pRoot);
}
};
2. 数据结构——栈与队列
2.1 栈和队列的构造
E1.用两个栈实现队列(JZ9)
牛客链接:用两个栈实现队列牛客题霸牛客网 (nowcoder.com)
class Solution
{
public:
void push(int node) {
stack1.push(node);
}
int pop() {
if (stack2.empty()){
while (!stack1.empty()) {
stack2.push(stack1.top());
stack1.pop();
}
}
int res = stack2.top();
stack2.pop();
return res;
}
private:
stack<int> stack1;
stack<int> stack2;
};
E2.包含min函数的栈(JZ30)
牛客链接:包含min函数的栈牛客题霸牛客网 (nowcoder.com)
#include <stack>
class Solution {
public:
stack<int> stack1; // 主栈
stack<int> stack2; // 最小栈
void push(int value) {
stack1.push(value);
if (stack2.empty() || value < stack2.top()) stack2.push(value); // 更新栈顶(最小值)
else stack2.push(stack2.top()); // 重复加入栈顶
}
void pop() {
stack1.pop();
stack2.pop();
}
int top() {
return stack1.top();
}
int min() {
return stack2.top();
}
};
E3.反转单词序列(JZ73)
牛客链接:翻转单词序列牛客题霸牛客网 (nowcoder.com)
#include <iterator>
class Solution {
public:
string ReverseSentence(string str) {
int n = str.size();
reverse(str.begin(), str.end());
for (int i = 0;i < n;i++){
int j = i;
while (j < n && str[j] != ' ') j++;
reverse(str.begin() + i, str.begin() + j);
i = j;
}
return str;
}
};
E4.栈的压入、弹出序列(JZ31)
牛客链接:栈的压入、弹出序列牛客题霸牛客网 (nowcoder.com)
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV) {
stack<int> stack;
int j = 0;
for (int i = 0;i < pushV.size();i++){
stack.push(pushV[i]);
while (!stack.empty() && stack.top() == popV[j]){
j++;
stack.pop();
}
}
if (stack.empty())
return true;
return false;
}
};
E5. 滑动窗口最大值(JZ59)
牛客链接:滑动窗口的最大值牛客题霸牛客网 (nowcoder.com)
暴力遍历:
class Solution {
public:
vector<int> maxInWindows(const vector<int>& num, unsigned int size) {
vector<int> ans;
if (num.size() < size || size == 0) return ans;
for (int i = 0;i <= num.size() - size;i++){
int max = -9999;
for (int j = i;j < i + size;j++){
if (max < num[j]){
max = num[j];
}
}
ans.push_back(max);
}
return ans;
}
};
滑动窗口算法*:
class Solution {
public:
vector<int> maxInWindows(const vector<int>& num, unsigned int size) {
vector<int> ans;
if (num.size() < size || size < 1) return ans;
int n = num.size();
deque<int> deque;
for (int i = 0;i < n;i++){
while (!deque.empty() && num[deque.back()] < num[i]) deque.pop_back();
deque.push_back(i); // 始终替换队头为最大值
if (deque.front() + size <= i) deque.pop_front(); // 判断当前最大值是否过期
if (i + 1 >= size) ans.push_back(deque.front()); // 窗口大小是否满足要求
}
return ans;
}
};
3. 搜索算法
3.1 二分搜索模板
E1. 一维二分搜索模板
迭代模板:
class Solution{
public:
int binarySearch(int target, vector<int> array){
int l = 0, r = array.size(), mid;
while (l <= r){
mid = (l + r) >> 1;
if (array[mid] > target) r = mid - 1;
else if (array[mid] < target) l = mid + 1;
else return mid;
}
return -1;
}
};
递归模板:
class Solution{
public:
int binarySearch(vector<int> array,int l,int r,int target){
if (l > r) return -1;
int mid = (l + r) >> 1;
if (array[mid] > target) return BinarySearch(array,l,mid - 1,target);
else if (array[mid] < target) return BinarySearch(array,mid + 1,r,target);
else return mid;
}
};
E2. 二维二分搜索模板
牛客链接:二维数组中的查找牛客题霸牛客网 (nowcoder.com)
class Solution {
public:
bool Find(int target, vector<vector<int> > array) {
if (array.size() == 0 || array[0].size() == 0) return false;
int n = array.size();
int m = array[0].size();
for (int i = 0,j = m - 1; i < n && j >= 0;){
int cur = array[i][j];
if (cur > target) j--;
else if (cur < target) i++;
else return true;
}
return false;
}
};
总结:真的只需要一层循环。(每次刷这道题都喜欢写两层!!!特别注意)
3.2 寻路 / 排列算法
E1. 排列字符串(JZ38)
思想核心:深搜 + 回溯(递归)
牛客链接:字符串的排列牛客题霸牛客网 (nowcoder.com)
class Solution {
public:
void DFS(set<string>& ans, string target, string& temp, vector<int>& flag){
if (temp.size() == target.size()){
ans.insert(temp);
return;
}
for (int i = 0;i < target.size();i++){
if (flag[i]) continue; // 被使用过直接跳过
flag[i] = 1; // 标记
temp.push_back(target[i]);
DFS(ans, target, temp, flag);
flag[i] = 0; // 回溯
temp.pop_back();
}
}
vector<string> Permutation(string str) {
sort(str.begin(), str.end());
set<string> ans;
string temp ;
vector<int> flag(str.size(), 0);
DFS(ans, str, temp, flag);
return vector<string>({ans.begin(), ans.end()});
}
};
总结:该题中所给元素存在重复,为了防止多次添加,可使用set进行去重。
E2. 矩阵中的路径(JZ12)
牛客链接:矩阵中的路径牛客题霸牛客网 (nowcoder.com)
#include <vector>
class Solution {
public:
bool Dfs(vector<vector<char>> matrix, string word,int n, int m, int i, int j, int k, vector<vector<bool>>& mark){
if (i < 0 || i >= n || j < 0 || j >= m || matrix[i][j] != word[k] || mark[i][j])
return false; // 寻路越界,路径走过,字符不等
if (k == word.size() - 1) return true; // 寻路成功
//寻路核心↓
mark[i][j] = true; // 标记
if ( Dfs(matrix, word, n, m, i, j - 1, k + 1, mark)
|| Dfs(matrix, word, n, m, i, j + 1, k + 1, mark)
|| Dfs(matrix, word, n, m, i - 1, j, k + 1, mark)
|| Dfs(matrix, word, n, m, i + 1, j, k + 1, mark)) // 上下左右任意方向能通
return true;
// 寻路失败进行回溯
mark[i][j] = false; // 回溯标记
return false;
}
bool hasPath(vector<vector<char>>& matrix, string word) {
int n = matrix.size();
if (n == 0) return false;
int m = matrix[0].size();
if (m == 0) return false;
vector<vector<bool>> mark(n, vector<bool>(m, false));
for (int i = 0; i < n;i++)
for (int j = 0;j < m;j++)
if (Dfs(matrix, word, n, m, i, j, 0, mark))
return true;
return false;
}
};