算法模板思维导图整理

这篇博客详细介绍了各种算法模板,包括二叉树的前序、中序、后序遍历,二分查找模板,动态规划中的并查集技巧,快速排序的实现,以及lowerbound和upperbound的使用。此外,还讲解了双指针在处理区间问题时的应用,如最长区间和定长区间。最后,讨论了单调队列和单调栈在求解某些问题时的策略,如最大矩形面积。
摘要由CSDN通过智能技术生成

力求用最短的代码实现算法模板——来自Dryad和carl

模板篇

二叉树dfs

  1. 前序遍历
class Solution {
public:
    vector<int> ans;
    void preorder(TreeNode* root){
        if(root == NULL) return;
        ans.push_back(root->val);
        preorder(root->left);
        preorder(root->right);
    }
    vector<int> preorderTraversal(TreeNode* root) {
        preorder(root);
        return ans;
    }
};
  1. 中序遍历
class Solution {
public:
    vector<int> ans;
    void inorder(TreeNode* root){
        if(root == NULL) return;
        inorder(root->left);
        ans.push_back(root->val);
        inorder(root->right);
    }
    vector<int> inorderTraversal(TreeNode* root) {
       inorder(root);
       return ans; 
    }
};

  1. 后序遍历
class Solution {
public:
    vector<int> ans;
    void postorder(TreeNode* root){
        if(root == NULL) return;
        postorder(root->left);
        postorder(root->right);
        ans.push_back(root->val);
    }
    vector<int> postorderTraversal(TreeNode* root) {
        postorder(root);
        return ans;
    }
};

二分查找模板

class Solution {
public: 
	int searchInsert(vector<int>& nums, int target)
	{
		int n = nums.size();
		int left = 0;
		int right = n; // 我们定义target在左闭右开的区间里,[left, right)
		while (left < right)  // 因为left == right的时候,在[left, right) 是无效的空间
		{
			int middle = left + ((right - left) >> 1);
			if (nums[middle] > target) {
				right = middle; // target在左区间,因为是左闭右开的区间,nums[middle]一定不是我们的目标值,所以right = middle,在[left, middle)中继续寻找目标值。
			} else if (nums[middle] < target) {
				left = middle + 1; // target在右区间,在 [middle+1, right)中
			} else { // nums[middle] == target
                return middle; // 数组中找到目标值的情况,直接返回下标
            }
		}
		return right;
	}
};

比如要求平方根问题:

class Solution {
public:
    int mySqrt(int x) {
        int bottom = 0, upper = x, ans = -1;
        while (bottom <= upper) {
            int mid = bottom + (upper - bottom) / 2;
            if ((long long) mid * mid <= x) {
                ans = mid;
                bottom = mid + 1;
            } else {
                upper = mid - 1;
            }
        }
        return ans;
    }
};

并查集

方便记忆:

int Find(int x) {

  // 查找根结点,也就是找目前这棵树上,最上面的“帮主”,通过递归来实现。

  int b = x;

  while (F[x] != x) {

    x = F[x];

  }

  // 路径压缩的实现

  // 将路径上的每个点指向根结点x,将帮主的直接查询权赋予给该点

  while (F[b] != x) {

    int p = F[b];

    F[b] = x;

    b = p;

  }

  return x;

}

合并的技巧

    int i = b;
    int j = m;
    int to = b;
    
    // 将两个子数组进行合并, 注意下面是一个很重要的模板
    // 这里的判断条,是只要两个子数组中还有元素
    while (i < m || j < e) {
      // 如果右子数组没有元素 或 左子数组开头的元素小于右子数组开头的元素
      // 那么取走左子数组开头的元素
      // 考点:a[i] <= a[j]这样可以保证合并排序是稳定的,不要写错!
      if (j >= e || i < m && a[i] <= a[j]) {
        t[to++] = a[i++];
      } else {
        // 否则就是取右子数组开头的元素
        t[to++] = a[j++];
      }
    }

快速排序

快速排序的特点:

  1. 根节点的处理,需要执行“三路切分”操作,将一个数组切分为三段
  2. 左右子区间是由切分动态生成的,并不像二叉树由指针固定。
void qsort(vector<int> A, int b, int e) {
  // 像二叉树一样,如果空树/只有一个结点,那么不需要再递归了 
  // 如果给定的区间段为空,或者只有一个结点。 
  if (b >= e || b + 1 >= e) {
    return;
  }
  // 取数组中间的元素作为x
   int m = b + ((e - b) >> 1);
   int x = A[m];
  // 三路切分,这部分代码在例 3详细介绍!
  int l = b, i = b, r = e - 1;
  while (i <= r) {
    if (A[i] < x) {
      swap(A, l++, i++);
    } else if (A[i] == x) {
      i++;
    } else {
      swap(A, r--, i);
    }
  }
  // 像二叉树的前序遍历一样,分别遍历左子树与右子树。
  qsort(A, b, l);
  qsort(A, i, e);
}

lowerbound和upperbound

lowerbound用于查找有序数组中,最左边出现的元素(闭)

upperbound用于查找有序数组中,最右边出现的元素(开)

当给定数组 A[] = {1,2,2,2,3},运行 lowerBound(A, 2) 返回下标 1,而运行 upperBound(A,2) 却返回下标 4,但此时 A[4] = 3。

注意跟二分法的区别

  1. 用途不一样,边界问题是当排序数组有重复元素时,找到重复元素的上下边界。二分法是查询元素的一种方式,并不能准确的确定重复元素的具体位置,而且也往往没有重复元素。
  2. 算法不一样,边界问题返回的是left位置和right位置,对找左边界来说,当遇到A[m] < target的时候,l需要不停自增,当等于的时候,此时的l已经增到指定位置;对找右边界来说,当遇到A[m] <= target的时候,l需要自增,为什么呢,这是因为upperbound找到的都是" 更右边 "的位置,因此要把所有小于连同等于的元素都略过,才能遇到最右边的r,即右边界。对二分法来说,直接三个if判断,判断当前middle位置是否已经是target,如果不是,要么更新left,要么right,同时更新middle。
int lower_bound(vector<int> A, int target)
{
	int l = 0, r = A.size();
	while (l < r)
	{
		int m = l + ((r - l) >> 1);
		if (A[m] < target) {
			l  = m + 1;
		} else {
			r = m;
		}
	}
	return l;
}

int upper_bound(vector<int> A, int target)
{
	int l = 0, r = A.size();
	while (l < r)
	{
		int m = l + ((r - l) >> 1);
		if (A[m] <= target) {
			l  = m + 1;
		} else {
			r = m;
		}
	}
	return l;
}

双指针的区间问题

最长区间
  1. 两个指针,left指针和right指针,两个指针形成的区间为(left, right]
  2. left指针要等到条件不满足了,才向右移动
int maxLength(vector<int> A)
{
	int N = A.size();
	int left = -1;
	int ans = 0;
	for (int i = 0; i < N; i++)
	{
		// step1 直接将A[i]加到区间中,形成(left, i]
		// step2 将A[i]加入之后,惰性原则
		while (check((left, i]))/*TODO 检查区间状态是否满足条件*/) {
      ++left; // 如果不满足条件,移动左指针
      // TODO 修改区间的状态
    }
    // assert 此时(left, i]必然满足条件
    ans = max(ans, i - left);
  }
  return ans; // 返回最优解
}
定长区间

通常也被称为“滑动窗口算法”

class Solution {
public:
    string minWindow(string s, string t) {
        int M = s.size();
        int N = t.size();
        unordered_map<char, int> scount; // 该字典变量保存s[left, right]闭区间中各个元素出现的次数
        unordered_map<char, int> tcount; // 该字典变量保存t中元素出现次数。
        // left and right means [left, right] of s
        // count means the same chars of s[left, right] with t
        int left = 0, right = 0, count = 0; // count变量储存s[left, right]闭区间中已经覆盖了t中的多少个元素,如果cnt == t.size()说明该子数组覆盖了t。
        int minLen = INT_MAX;  // 该变量存储当前满足题目要求的最短子串长度,目的是用substr将res从s中取出来,其更新方法为right - left + 1。如果不是遇到子数组已经满足覆盖,开始收缩的时候,left是不会动的
        string res;
        // 给tcount字典初始化
        for (char c : t)
            ++tcount[c];
        // 开始准备滑动窗口
        while (right < M) {
            char c = s[right]; // 取出来,才能进行用字典的count方法查询是否存在
            scount[c] += 1;    // 滑动窗口嘛,所以记录数据就在改变,当考虑的时候,个数+1,后面又有减一的时候
            if (tcount.count(c) && scount[c] <= tcount[c]) {
                count += 1;
            }
            while (left <= right && count == N) {  // 如果count == t.size() 说明该子数组覆盖了t。
                // 如果覆盖了所有t的元素,说明这个区间是符合要求的一个区间。此时不必移动right指针。
                if (minLen > right - left + 1) {
                    minLen = right - left + 1;
                    res = s.substr(left, minLen);
                }
                char l = s[left];
                scount[l] -= 1;
                if (tcount.count(l) && scount[l] < tcount[l])
                    count -= 1;
                // 要移动left指针,直至s[left, right]子字符串不能覆盖t。
                ++left;
            }
            ++right;
        }
        return res;
    }
};

另一种滑动窗口的实现方法如下,单调队列

单调队列
  • 每次入队的时候,,为了保证队列的单调性,还要剔除尾部的元素。直到尾部的元素大于等于入队元素(因为是单调递减队列)。
  • 单调队列在出队时,需要给出一个value,如果value与队首相等,才能将这个数出队。
  • 性质:
      1. 队首元素就是最大值
      1. 入队和出队的组合,可以在O(1)时间得到某个区间上的最大值
class MyQueue {
public:
	deque<int> que;
	void pop(int value){
	        // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。因为很有可能该value已经不存在了,因为push的时候就有删除操作
        // 同时pop之前判断队列当前是否为空。
		if (!que.empty() && value == que.front()) {
			que.pop_front();
		}
	}
	// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
        // 这样就保持了队列里的数值是单调从大到小的了。
	void push(int value){
		while(!que.empty() && value > que.back()) {
			que.pop_back();
		}
		que.push_back(value);
	}
     // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
	int front() {
		return que.front();
	}
	
};

所谓的滑动窗口的最大值,其实并不是使用滑动窗口实现的!!!因为滑动窗口有伸张和收缩两个步骤,而本地的题眼,其实是最大二字。

单调栈

单调栈就是指栈中的元素必须是按照升序排列的栈,或者是降序排列的栈。
一般此类题目用于解决:数组中元素右边第一个比元素自身小的元素的位置。

单调栈的性质
  • 当单调递增栈中存放数组下标 i, j, k 时,其中 (i, k] 中的元素 > A[j];

  • 当单调递增栈中存放数组下标 i, j,并且当 A[k] 入栈,会把栈顶元素 A[j]“削”出栈时,其中 (j, k) 元素 > A[j]。
    用来求解最大矩形

单调栈模板

单调栈有别于单调队列,其往往在“最大矩形”一类题目中出现,

int largestRectangleArea(vector<int>& heights) {
        stack<int> st;
        heights.insert(heights.begin(), 0); // 数组头部加入元素0
        heights.push_back(0); // 数组尾部加入元素0
        st.push(0);
        int result = 0;
        // 第一个元素已经入栈,从下表1开始
        for (int i = 1; i < heights.size(); i++) {
            // 注意heights[i] 是和heights[st.top()] 比较 ,st.top()是下表
            if (heights[i] > heights[st.top()]) {
                st.push(i);
            } else if (heights[i] == heights[st.top()]) {
                st.pop(); // 这个可以加,可以不加,效果一样,思路不同
                st.push(i);
            } else {
                while (heights[i] < heights[st.top()]) { // 注意是while
                    int mid = st.top();
                    st.pop();
                    int left = st.top();
                    int right = i;
                    int w = right - left - 1;
                    int h = heights[mid];
                    result = max(result, w * h);
                }
                st.push(i);
            }
        }
        return result;
    }
普通栈模板

基本上,所有括号的问题,都要用到栈的操作,其实匹配问题都可以考虑用栈实现,模板如下:

class Solution {
public:
    bool isValid(string s) {
        stack<int> st;
        for (int i = 0; i < s.size(); i++) {
            if (s[i] == '(') st.push(')');
            else if (s[i] == '{') st.push('}');
            else if (s[i] == '[') st.push(']');
            // 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
            // 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
            else if (st.empty() || st.top() != s[i]) return false;
            else st.pop(); // st.top() 与 s[i]相等,栈弹出元素
        }
        // 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
        return st.empty();
    }
};

哦,对了,在补充一些面试考点

  1. C++中stack 是容器么?
    答:栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。所以STL中栈往往不被归类为容器,而被归类为容器适配器(container adapter)。
    另外可以拓展了解 “堆 ”和“ 栈 ”之间的区别。从数据结构角度来说,是两种不同的数据结构,前者由二叉树构建,后者先入后出;从计算机系统角度来说,栈区由编译器自动分配释放,存放函数的参数值,局部变量的值等;堆区一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。
    堆是由程序员自己申请并指明大小,在c中malloc函数 如p1 = (char *)malloc(10);
    栈由系统自动分配,如声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
  2. 我们使用的STL中stack是如何实现的?
    答:deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
  3. stack 提供迭代器来遍历stack空间么?
    答:栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。不像是set 或者map 提供迭代器iterator来遍历所有元素。

例题如:

  • 有效括号
  • 删除相邻相同元素(本质上还是类似括号问题,可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了)
最短区间
贪心算法

虽然说贪心算法是一种思想,不过还是有一些代码模板需要掌握。比如无重叠区间

按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的。
从左向右记录非交叉区间的个数,最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了
右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。局部最优可以推出全局最优。

区间,1,2,3,4,5,6都按照右边界排好序。

每次取非交叉区间的时候,都是可右边界最小的来做分割点(这样留给下一个区间的空间就越大),所以第一条分割线就是区间1结束的位置。

接下来就是找大于区间1结束位置的区间,是从区间4开始。那有同学问了为什么不从区间5开始?别忘已经是按照右边界排序的了。

区间4结束之后,在找到区间6,所以一共记录非交叉区间的个数是三个。

总共区间个数为6,减去非交叉区间的个数3。移除区间的最小数量就是3。

在这里插入图片描述

class Solution {
public:
    // 按照区间右边界排序
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[1] < b[1];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.size() == 0) return 0;
        sort(intervals.begin(), intervals.end(), cmp);
        int count = 1; // 记录非交叉区间的个数
        int end = intervals[0][1]; // 记录区间分割点
        for (int i = 1; i < intervals.size(); i++) {
            if (end <= intervals[i][0]) {
                end = intervals[i][1];
                count++;
            }
        }
        return intervals.size() - count;
    }
};
复杂度分析
  • 时间复杂度:O(nlogn) ,有一个快排
  • 空间复杂度:O(1)

回溯

一个核心,三个条件

  • 第 i 个人怎么选?
  • 什么样的状态是我们想要的?
  • 后面的人还有选项吗?如果后面所有的人都没有选项,就需要返回了。
  • 第 i 个人的宝石选项是什么样的?

pop的理解:为了避免弄丢东西,每个人都必须遵守规则:归还的箱子要与借出时一模一样,所以归还箱子的时候,需要把箱子里面的属于自己的东西拿出来。

void backtrace(int[] A,

               int i, /*第i个人*/

               Box s, /*箱子*/

               answer/*存放所有的答案*/) {

  final int N = A == null ? 0 : A.length;

  if (状态满足要求) {

    answer.add(s);

  }

 

  if ([i, ...., 后面)的人都没有任何选项了) {

    return;

  }

  for 宝石 in {第i个人当前所有宝石选项} {

    s.push(宝石);

    backtrace(A, i + 1, s, answer);

    s.pop();

  }

}

DFS和BFS

DFS 的模板 1

将 opt 设置为每次可以做出的选择

DFS 的模板 2

如果题目中需要求解最优解,那么,DFS 问题就退化为回溯问题,只不过满足约束的条件就变成:“从所有解中找到最优解”

/*
@ start 表示出发点
@ vis 记录每个点是否已经访问 
@ path 路径
@ answer 存放最优的答案
*/
void dfs(A, int start, vis, path, answer) {
  int N = A == null ? 0 : A.length;
  if (状态满足要求) { // 是更好的解吗?
    if (s better_than ans) {
        ans = s
    }
    return;
  }
  for next in {start点的后继点} {
    if !vis[next] {
      path.append(next);
      vis[next] = true;
      dfs(A, next, vis, path, answer);
      path.pop();
      vis[next] = false;
    }
  }
}
n皇后问题

这类模板解决如n皇后问题

class Solution {
private:
    vector<vector<string>> result;

    // n 为输入的棋盘大小
    // row 是当前递归到***的第几行了
    void backtracking(int n, int row, vector<string>& chessboard)
    {
        if (row == n)
        {
            result.push_back(chessboard);
            return;
        }
        for (int col = 0; col < n; col++)
        {
            if (isValid(row, col, chessboard, n)) // 验证合法就可以放
            {
                chessboard[row][col] = 'Q'; // 放置皇后
                backtracking(n, row + 1, chessboard);
                chessboard[row][col] = '.'; // 回溯,撤销皇后
            }
        }
    }

    bool isValid(int row, int col, vector<string>& chessboard, int n)
    {
        int count = 0;
        // 检查列
        for (int i = 0; i < row; i++)
        {
            if (chessboard[i][col] == 'Q')
            {
                return false;
            }
        }

        // 检查45度角是否有皇后
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--)
        {
            if (chessboard[i][j] == 'Q')
                return false;
        }

        // 检查135度角是否有皇后
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++)
        {
            if (chessboard[i][j] == 'Q')
                return false;
        }

        return true;
    }
    
public:
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        vector<string> chessboard(n, string(n, '.'));
        backtracking(n, 0, chessboard);
        return result;
    }
};
  1. 将visited换成了isValid,是因为要满足下一次dfs 的条件
  2. 将pop换成了清 ’ . ',这是因为要回溯,也就是刚刚考虑了放置的情况,放置之后我们进行了backtracking得到了一系列的结果,然而不放置的情况,又会如何如何,体现在for循环中,将别的地方放置Q。放置方法是独立同分布的,不能互相影响,所以要归位!
  3. 返回值,之前是为了找到最优解,有个better_than的判断,这儿的result是个private变量,一开始的chessboard全是空,然后用它作为dfs的桥梁,
  4. vector < string >,比如[".Q…"],可以看成是一个二维数组

DFS 记忆化搜索
将memory数组添加到backtracking函数参数里。

DFS 可以帮助我们找到最优解。不过在搜索的时候,若想知道一些关于“最近/最快/最少”之类问题的答案,往往采用 BFS 更加适合。

单词拆分

给定一个非空字符串s和一个包含非空单词的列表wordDict,判定s是否可以被空格拆分为一个或多个在字典中出现的单词。
使用memory数组保存每次计算的以startIndex起始的计算结果,如果 memory[startIndex]里已经被赋值了,直接用memory[startIndex]的结果。

class Solution {
private:
    bool backtracking (const string& s,
            const unordered_set<string>& wordSet,
            vector<int>& memory,
            int startIndex) {
        if (startIndex >= s.size()) {
            return true;
        }
        // 如果memory[startIndex]不是初始值了,直接使用memory[startIndex]的结果
        if (memory[startIndex] != -1) return memory[startIndex];
        for (int i = startIndex; i < s.size(); i++) {
            string word = s.substr(startIndex, i - startIndex + 1);
            if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, memory, i + 1)) {
                memory[startIndex] = 1; // 记录以startIndex开始的子串是可以被拆分的
                return true;
            }
        }
        memory[startIndex] = 0; // 记录以startIndex开始的子串是不可以被拆分的
        return false;
    }
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        vector<int> memory(s.size(), -1); // -1 表示初始化状态
        return backtracking(s, wordSet, memory, 0);
    }
};

复杂度分析

  • 时间复杂度:O(2^n),因为每一个单词都有两个状态,切割和不切割
  • 空间复杂度:O(n),算法递归系统调用栈的空间
三角形最小路径和

给定一个triangle,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。相邻的结点
指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。

输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)

思路一:由于列的选择有限制,所以我们采取自下而上的方式进行dp
class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {

        //很经典的dp题目
        int col = triangle.size();   //多少行

        vector<int> dp(col+1,0);    //根据多少行,我们来确定需要多大的辅助空间  这里将里面的元素都初始化为0

        for (int i = col - 1; i >= 0; i--)  //从最后一行开始向第一行走  即从下到上
        {
            for (int j = 0; j <triangle[i].size(); j++)    //从第一列向最后一列走, 从左到右
            {
                dp[j] = min(dp[j], dp[j + 1]) + triangle[i][j]; //先再选择最小的元素 然后再加上要计算的元素
            }
        }
        return dp[0];
    }
};
思路二:记忆化搜索
// 其实还是超时了
class Solution {
    
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        vector<vector<int>> memo(triangle.size(), vector<int>(triangle.size(), 0));
        return dfs(triangle, memo, 0, 0);
        
    }
private:
    int dfs(vector<vector<int>>& triangle, vector<vector<int>> memo, int i, int j)
    {
        if (i == triangle.size()) {
            return 0;
        }
        if (memo[i][j] != 0)
        {
            return memo[i][j];
        }
        return memo[i][j] = min(dfs(triangle, memo, i+1, j), dfs(triangle, memo, i+1, j+1)) + triangle[i][j];
    }
};

其实记忆化搜索,是剪枝的一种方式而已,减少了空间复杂度。我们也可以剪枝来减少时间复杂度,如组合总和问题中, i <= n - (k - path.size()) + 1。

BFS的模板

二叉树的层序遍历模板,需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public: 
	vector<vector<int>> levelOrder(TreeNode* root){
		queue<TreeNode*> que; // que存放的是一个节点
		if (root != nullptr) que.push(root);
		vector<vector<int>> result;// 返回的结果是一个二维数组,第一个维度代表一层
		while(!que.empty())
		{
			int size = que.size();
			vector<int> vec;
			// 这里一定要使用固定大小的size,不要使用que.size(),因为que.size()是不断变化的。
			for (int i = 0; i < size; i++)
			{
				TreeNode* node = que.front();
				// 先将队列中当前的节点弹出,但该节点的左右子节点会加入队列中去。上一轮while循环push_back进去的另一个节点,在下一次for循环也弹出,同时也加入该节点的左右子节点,于是,便实现了层序遍历。
				que.pop();
				// 由于que队列的首位元素已经赋值给了node,所以弹出并不影响取val值。其实这儿vec.push_back(que.front->val)也行吧。即将for循环改成如下形式
				vec.push_back(node->val);
				// 如果子节点存在的话
				if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);				
			}
			/*
			for (int i = 0; i < size; i++)
			{
				// TreeNode* node = que.front();
				// 先将队列中当前的节点弹出,但该节点的左右子节点会加入队列中去。上一轮while循环push_back进去的另一个节点,在下一次for循环也弹出,同时也加入该节点的左右子节点,于是,便实现了层序遍历。
				
				// 由于que队列的首位元素已经赋值给了node,所以弹出并不影响取val值。其实这儿vec.push_back(que.front->val)也行吧。
				vec.push_back(que.front()->val);
				// 如果子节点存在的话
				if (que.front()->left) que.push(que.front()->left);
                if (que.front()->right) que.push(que.front()->right);				
                que.pop();
			}
			*/
			result.push_back(vec);
		}
		return result;
	}
};
BFS(s) { // s表示出发点
  queue<TreeNode*> que;
  que.push(s), visited[s] = true // 标记s为已访问
  while (!que.empty()) {
    auto cur = que.front();
	que.pop();                           // 拿到当前结点 
    for next in getNext(u) { // 拿到u的后继next,方法比如说当前位置增加四个方向(如下例题),或者像是二叉树的方式直接->left或者->right。在当前层级的循环过程中,处理判断是否满足条件,将走过的位置visit置一,并同时将下一层级的元素push到队列中去,体现了BFS的思想。
      if (!visited[next]) { // 如果next还没有访问过 
        q.push(next);
        visited[next] = true;
      }
    }
  }
}
最短路径

给定一个矩阵,0表示可通行的路径,1表示墙,返回从左上角走到右下角的最短路径。如果不存在返回-1。

class Solution {
public:
	int shortestPathBinaryMatrix(vector<vector<int>>& A)
	{
		const int R = A.size();
		const int C = A[0].size();

		int dir[][2] = {
		{0, 1}, {0, -1}, {1, 0}, {-1, 0},
		{-1, -1}, {-1, 1}, {1, -1}, {1,1},
		};
		// 首先处理特殊情况,为空,或者只有一个格子
		if (R <= 1 || C <= 1) { 
			return min(R, C);
		}
		// 首先处理起始点,如果起点和终点都不可以走
		if (A[0][0] == 1 || A[R - 1][C - 1] == 1) {
			return -1;
		}
		queue<pair<int, int>> Q;
		Q.push({0, 0});
		A[0][0] = 1;
		int ans = 0;
		
		while (!Q.empty())
		{
			ans++;
			// 注意这里类似二叉树层序遍历的做法,取出qSize,可以一层一层的遍历
			int QSize = Q.size();
			while (Qsize--) {
				auto cur = Q.front();
				Q.pop();
				// 如果已经走到了目的地
				if (cur.first == R - 1 && cur.second == C - 1) {
					return ans;
				}
				// BFS搜索,在当前点处,扩展8个方向上的位移,完成两件事,一是将走到的位置置1,也就是visit的作用,二是将新位置push到Q当中,类似层序遍历
				for (int d = 0; d < 8; d++)
				{
					int nr = cur.first + dir[d][0];
					int nc = cur.second + dir[d][1];
					if (!(nr < 0 || nc < 0 || nr >= R || nc >= C)) {
						if (A[nr][nc] != 1)
						{
							A[nr][nc] = 1;
							Q.push({nr, nc});
						}
					}
				}
			}
		}
	return -1;
	}
}

动态规划

KMP算法

void kmp(int * next, const string& s)
{
	next[0] = -1;
	int j = -1;
	for (int i = 1; i < s.size(); i++)
	{
		while(j >= 0 && s[i] != s[j+1]) {
			j = next[j];
		}
		if (s[i] == s[j+1]) {
			j++;
		}
		next[i] = j;
	}
}

Dijkstra算法

该算法用于查询最短路径
通过该算法解决了从某个源点到其余各顶点的最短路径问题。
https://blog.csdn.net/fatfairyyy/article/details/107378839

Floyd算法

该算法用于查询最短路径
相比于Dijkstra算法,Floyd算法关注的是多元最短路径问题,即一个是从某一个顶点出发求出其达到其他顶点的最短路径,一个是从每个顶点出发到达其他顶点的最短路径。即如果面临需要求所有顶点至所有所有顶点的最短路径问题,Floyd算法应该是不错的选择。

void ShortestPath_Floyd(MGraph *G) {
	for (int i = 0; i < G->numVer)
}

关键路径算法

该算法用于查找最短路径
从源点到汇点具有最大长度的路径叫做关键路径,在关键路径上的活动叫做关键活动
求最晚发生时间数组的值,即为求最早发生时间的逆过程

void CriticalPath(GraphAdjList *G){
	TopologicalSort(G); // 调用上述拓扑排序代码
	for (int i = 0; i < G->numVertexes; i++)
	{
		ltv[i] = etv[G->numVertexes - 1];
	}
	while (!s2.empty()) {
		int gettop = s2.top();
		s2.pop();
		EdgeNode *e;
		for (e = G->adjList[].FirstEdge; e != nullptr; e = e->next) {
			// 求各顶点事件的最晚发生时间
			int k = e->adjvex;
			if(ltv[k] - e -> weight < ltv[gettop]){
				ltv[gettop] = ltv[k] - e -> weight;
			} // 因最晚发生时间是从后向前找(栈后进先出),因此此处为“相减”与“小值”。即求etv的逆运算
		}
	}
for(int j=0;j<G->numVertexes;j++)/*遍历顶点表上各顶点*/{
  for(EdgeNode *e = G->adjList[j].FirstEdge;e != NULL ; e = e->next){
   int k = e->adjvex;
   int ete = etv[j];/*活动最早开始时间*/
   int lte = ltv[k] - e->weight;/*最迟开始时间,减去e->weight是因为k的
   下标指向的是边表一号结点,而非顶点表结点*/
   if(ete == lte){/*二者相等即在关键路径上*/ 
    cout<<"<v"<<G->adjList[j].data<<",v" <<G->adjList[k].data<<"> length:"<<e->weight<<endl;
   }
  }
 }
}

Kruskal算法

该算法用于代价最小的图连接方式

给定一个图的点集,边集和权重,返回构建最小生成树的代价。
输入:N = 2, conn = [[1, 2, 37], [2, 1, 17], [1, 2, 68]]

输出:17

解释:图中只有两个点 [1, 2],当然是选择最小连接 [2, 1, 17]

利用并查集 + 贪心算法,可以生成一个图的最小生成树,这种方法也被称为 Kruskal 算法。并查集可以用来将两个点进行 Union,不过在并查集的 Union 代码中,并没有权重这一项,那我们该怎么办呢?

在 Union 的时候,就直接根据边的权重来排序,然后再处理,这不就是经典的 Kruskal 算法。

这里我们可以讲一下最小生成树的思路:

首先初始化并查集

将边集按照权重排序

利用边集将不同的两点进行 Union

将不同的集合进行 Union 时需要加上新加入的边的代价(即边的权重)。


/* Kruskal算法求无向图的最小生成树 */
int Kruskal(int n, int m, vector<edge>& E)
{
	vector<int> father(n);			// 并查集数组
	int ans = 0;					// 所求边权之和
	int NumEdge = 0;				// 记录最小生成树边数
	for (int i = 0; i < n; i++)		// 初始化并查集
		father[i] = i;
	sort(E.begin(), E.end(), cmp);	// 所有边按边权从小到大,贪心算法体现
	for (int i = 0; i < m; ++i)		// 枚举所有边
	{
		int faU = findFather(father, E[i].u);	// 查询端点u所在集合的根节点
		int faV = findFather(father, E[i].v);	// 查询端点v所在集合的根节点
		if (faU != faV)							// 如果不在一个集合中
		{										// 合并集合 (相当于把测试边加入到最小生成树)
			father[faU] = faV;
			ans += E[i].cost;
			NumEdge++;							// 当前生成树边数+1
			if (NumEdge == n - 1)				// 边数等于顶点数减一,算法结束。
				break;
		}
	}
	if (NumEdge != n - 1)						// 无法连通时返回-1
		return -1;
	else 
		return ans;								// 返回最小生成树边权之和
}
  1. 采用的数据结构:边集数组
const int maxn = 5005;
struct Edge{
	int ew, ev, d;
} e[maxn];
  1. 采用的查找方式:并查集
    并查集作用:查找某个元素的“祖先”,在图问题中可以理解为,查找某个结点和另一个结点是否联通。
    查找方式:递归的方式
int findf(int x){
	while(x != f[x]) x = f[x] = f[f[x]];
	return x;
}
  1. 初始化,用sort()的比较函数cmp等先略过不谈
    1). cnt统计已加入生成树的边数,任意一棵生成树,一定满足e = n - 1,故当有n-1条边加入生成树时,最小生成树已经建成了。
    2). ev,ew是用来查找边的两个结点的祖先
    3). ans是全局变量,统计边的权值,在最小生成树问题中,ans即对应于构建最小生成树需要的最小权值。
bool Kruscal(){
	int cnt = 0, ev, ew;
	sort(e+1, e+m+1, cmp);
	for (int i = 1; i <= m; i++)
	{
		ev = findf(e[i].ev); // 这儿的findf不明白可以先跳过,按下不表,一会儿解释
		ew = findf(e[i].ew);
		if (ev == ew) continue;
		f[ew] = ev;
		ans += e[i].d;
		if (++cnt == n - 1) return true;
	}
	return false;
}

Prim算法

最小生成树

哈夫曼树

工具篇

我们是在学习算法,不是在练习打字!
vscode模板插件——User snippets

  1. 打开网站
  2. 打开vscode
    在这里插入图片描述
  3. 填充
    注意删除scope,或者指定scope
    在这里插入图片描述
  4. 重启生效,输入prefix即可
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值