Algorithm(1)

64 阅读9分钟

1. 数据结构——树:

1.1 BFS搜索算法:

BFS概念

广搜 是一种图搜索算法,用于在图或树中系统地遍历或搜索节点。它从起始节点开始,逐层地向外扩展搜索,直到找到目标节点或遍历完整个图。

BFS算法的详细过程如下

  1. 从起始节点开始,将其标记为已访问,并将其放入一个队列中。
  2. 从队列中取出队首节点,并检查它的所有邻居节点。
  3. 对于每个邻居节点,如果该节点未被访问过,则将其标记为已访问,并将其放入队列的末尾。
  4. 重复步骤2和步骤3,直到队列为空。
  5. 如果目标节点被找到,搜索结束;否则,说明图中不存在目标节点。

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;                    
    }
};