几种典型的算法类型

一、 单调栈

https://leetcode-cn.com/problems/next-greater-element-i/solution/dan-diao-zhan-jie-jue-next-greater-number-yi-lei-w/

在这里插入图片描述

1. 套路

  1. 从已知容器的后往前存栈
  2. 如果单调栈不为空,栈中元素小的出栈
  3. 先获取数,再把当前值入栈

2. 模板:

(1) 单数组版

vector<int> nextGreaterElement(vector<int>& nums) {
    vector<int> ans(nums.size()); // 存放答案的数组
    stack<int> s;
    for (int i = nums.size() - 1; i >= 0; i--) { // 倒着往栈里放
        while (!s.empty() && s.top() <= nums[i]) { // 判定个子高矮
            s.pop(); // 矮个起开,反正也被挡着了。。。
        }
        ans[i] = s.empty() ? -1 : s.top(); // 这个元素身后的第一个高个
        s.push(nums[i]); // 进队,接受之后的身高判定吧!
    }
    return ans;
}

作者:labuladong
链接:https://leetcode-cn.com/problems/next-greater-element-i/solution/dan-diao-zhan-jie-jue-next-greater-number-yi-lei-w/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

(2) 循环数组版

class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        int n = nums.size();
        vector<int> res(n, 0);
        stack<int> monoStack;
        // 1. 从已知容器的后往前存栈
        for (int i = n * 2 - 1; i >= 0; i--) {
            // 2. 如果单调栈不为空,栈中元素小的出栈
            while (!monoStack.empty() and nums[i % n] >= monoStack.top()) {
                monoStack.pop();
            }
            // 3. 先获取数,再把当前值入栈
            res[i % n] = monoStack.empty() ? -1 : monoStack.top();
            monoStack.push(nums[i % n]);
        }
        return res;
    }
};

3. leetcode例题:

496 下一个更大元素 I
503 下一个更大元素 II
739 每日温度

二、 并查集

https://leetcode-cn.com/problems/number-of-provinces/solution/python-duo-tu-xiang-jie-bing-cha-ji-by-m-vjdr/

在这里插入图片描述

1. 套路:

  1. 记录父节点
  2. 添加新节点
  3. 合并两个节点
  4. 判断两个节点是否连通
  5. 查找祖先(用状态压缩)

2. 模板:

class UnionFind{
public:
    // 5. 查找祖先(用状态压缩)
    int find(int x){
    	int root = x;
        while (father[root] != -1) {
        	root = father[root];
        }
        // 注意条件,容易错
       	while (root != x) {
        	int other_node = father[x];
        	father[x] = root;
        	x = other_node;
        }
        return root;
    }
    
    // 4. 判断两个节点是否连通
    bool is_connected(int x,int y){
        return find(x) == find(y);
    }
    
    // 3. 合并两个节点
    void merge(int x,int y){
    	int root_x = find(x);
    	int root_y = find(y);
    	if (root_x != root_y) {
    		father[find(x)] = find(y);
    	}
    }
    
    // 2. 添加新节点
    void add(int x){
        if (father.count(x) == 0) {
        	father[x] = -1;
        }
    }
    
private:
    // 1. 记录父节点
    unordered_map<int, int> father;
};

3. leetcode例题:

547 朋友圈
684 冗余连接
200 岛屿数量
1102 得分最高的路径(会员)
1135 最低成本联通所有城市(会员)
924 尽量减少恶意软件的传播
737 句子相似性II (会员)

三、 滑动窗口

1. 套路

  1. 先申请一个unordered_map保存,目标集合的元素need;
  2. 再申请一个unordered_map保存,滑动窗口的元素windows;
  3. 右指针滑动改变值,到一定条件,然后左指针滑动改变值;
  4. 注意左滑和右滑的逻辑一致;

2. 模板

void slidingWindow(string s, string t) {
    // 1. 先申请一个unordered_map保存,目标集合的元素need;
	unordered_map<char, int> need;
	// 2. 再申请一个unordered_map保存,滑动窗口的元素windows;
	unordered_map<char, int> windows;
	for (char c : t) need[c]++;
	int left = 0, right = 0;
	int valid = 0;
	while (right < s.size()) {
		// 注意:先取数再右移
		char c = s[right];
		right++;
		// 进行窗口内数据的一系列操作
		···
		// debug
		cout << left << ", " << right << endl;
		
		// 判断左侧窗口是否要收缩
		while (window needs shrink) {
			// 注意:一样,先取数,再右移
			char d = s[left];
			left++;
			// 进行窗口内数据的一系列操作
			··· 
		}
	}
}

3. leetcode例题

1208 尽可能使字符串相等 中等 滑动窗口
209 长度最小的子数组 中等 滑动窗口
1004. 最大连续1的个数 III 中等 滑动窗口
159 至多包含两个不同字符的最长子串(会员) 中等 会员 滑动窗口
1100 长度为 K 的无重复字符子串(会员) 中等 会员 滑动窗口

四、 前缀和&HASH

1. 套路

1.1 前缀和 — 复杂度(O(n^2))

  1. 申请一个vector保存叠加和,空间为原数组个数+1;
  2. 计算叠加和放入vector中;
  3. 逐步计算vector中的差值,如果等于目标值,count+1;

1.2 前缀和带HASH — 复杂度(O(n))

  1. 用一个值来保存叠加和;
  2. 用一个map来保存每个叠加和的出现次数,初始值0的value为1;
  3. 遍历原数组的值,并与叠加值求和,如果该值减去目标值的差值在map中出现过,则count加上map中对应差值的值;
  4. 最后把该叠加值存放在map中,值加1;
    注意:该解法,不管中间过程,只计算个数,所以可以一遍过去计算完成。

2. 模板

2.1 前缀和

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
    	// 1.  申请一个vector保存叠加和,空间为原数组个数+1;
        vector<int> preNums(nums.size() + 1);
        int count = 0;
        // 2.  计算叠加和放入vector中;
        for (int i = 0; i < nums.size(); i++) {
            preNums[i + 1] = preNums[i] + nums[i];
        }
        // 3.  逐步计算vector中的差值,如果等于目标值,count+1;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = i; j < nums.size(); j++) {
                if (preNums[j+1] - preNums[i] == k) {
                    count++;
                }
            }
        }
        return count;
    }
};

2.2 前缀和带HASH(推荐)

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
    //1. 用一个值来保存叠加和;
        int preNum = 0;
        int count = 0;
       	// 2. 用一个map来保存每个叠加和的出现次数,初始值0的value为1;
        unordered_map<int, int> preNumFraq;
        preNumFraq[0] = 1;
        // 3. 遍历原数组的值,并与叠加值求和,如果该值减去目标值的差值在map中出现过,则count加上map中对应差值的值;
        // 注意:for循环里的内容要根据具体改!!
        for (auto num : nums) {
            preNum += num;
            if (preNumFraq.count(preNum - k) != 0) {
                count += preNumFraq[preNum - k];
            }
            // 4. 最后把该叠加值存放在map中,值加1;
            // 注意:这里的preNum有些题要改!!
            preNumFraq[preNum]++;
        }
        return count;
    }
};

3. leetcode例题

560 和为K的子数组:O(n)复杂度解决 中等 前缀和&HASH
974 和可被 K 整除的子数组 中等 前缀和&HASH

四、 差分

1. 套路

1.1 套路1(适用小数据)
  1. 一般来说,这类题目要算差值;
  2. 可以先考虑平铺一个数组,作为中间变量,存值;
  3. 然后再逐个更新该数组,按照题目的要求;
1.2 套路2(适用大数据)
  1. 也可以用map(这里需要map的顺序)来存放内容;
  2. 然后逐个比对;

2. 模板

class Solution {
public:
    bool carPooling(vector<vector<int>>& trips, int capacity) {
        const int TOTAL = 1002;
        // 新建一个数组作为中间变量
        vector<int> pareVec(TOTAL);
        for (auto trip : trips) {
            pareVec[trip[1]+1] += trip[0];
            pareVec[trip[2]+1] -= trip[0];
        }
        // 然后对该数组逐个更新,要符合题意,最后得出结果
        for (int i = 1; i < TOTAL; i++) {
            pareVec[i] += pareVec[i - 1];
            if (pareVec[i] > capacity) {
                return false;
            }
        }
        return true;
    }
};
class Solution {
public:
    bool carPooling(vector<vector<int>>& trips, int capacity) {
        map<int, int> tripMap;
        for (auto trip : trips) {
            tripMap[trip[1]] += trip[0];
            tripMap[trip[2]] -= trip[0];
        }
        int res = 0;
        int capMax = 0;
        for (auto trip : tripMap) {
            res += trip.second;
            capMax = max(res, capMax);
            if (capMax > capacity) {
                return false;
            }
        }
        return true;
    }
};

3. leetcode例题

1094 拼车 中等 差分
122 买卖股票的最佳时机 II 简单 差分
1109 航班预订统计 中等 差分
253 会议室 II (会员) 中等 会员 差分

五、 排序

1. 套路

1.1 拓扑排序(BFS)

  1. 识别该类题,看题中元素是否有关联关系;
  2. 核心思路是以队列为载体,每次把入度为0的元素放入;
  3. 需要几个数据结构,分别是unordered_map,元素是int和set,存放元素以及后续结点;
  4. 还需要一个vector存放元素的入度数;
  5. 然后就是队列来存放入度为0的元素(注意是元素,而不是入度数);
  6. 每次从队列取值,进行相应操作,从unordered_map,并结合vector找到后续相应元素(入度为0),继续放入队列中,循环从队列取值,直到队列中没值;

2. 模板

2.1 拓扑排序(BFS)

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        unordered_map<int, set<int>> adjTable;
        vector<int> inDegree(numCourses, 0);
        queue<int> workQueue;
        vector<int> res;
        if (numCourses <= 0) {
            return res;
        }
        for (auto p : prerequisites) {
            if (adjTable.count(p[1]) == 0) {
                set<int> newSet;
                newSet.insert(p[0]);
                adjTable[p[1]] = newSet;
            } else {
                adjTable[p[1]].insert(p[0]);
            }
            inDegree[p[0]]++;
        }
        for (int i = 0; i < inDegree.size(); i++) {
            if (inDegree[i] == 0) {
                workQueue.push(i);
            }
        }
        int count = 0;
        while (!workQueue.empty()) {
            int value = workQueue.front();
            workQueue.pop();
            res.push_back(value);
            count++;
            for (auto successor : adjTable[value]) {
                inDegree[successor]--;
                if (inDegree[successor] == 0) {
                    workQueue.push(successor);
                }
            }
        }
        if (count != numCourses) {
            vector<int> res1;
            return res1;
        }
        return res;
    }
};

3. leetcode例题

210 课程表 II 中等 拓扑排序
444 序列重建 中等 会员 拓扑排序
269 火星词典 困难 会员 拓扑排序
179 最大数 中等 排序+字符串处理(熟练使用c++排序函数)
1305 两棵二叉搜索树中的所有元素 中等 排序+二叉搜索树
1353 最多可以参加的会议数目 中等 排序+贪心
853 车队 中等 排序+遍历
1333 餐厅过滤器 中等 排序

六、 字符串

1. 套路

2. 模板

3. leetcode例题

5 最长回文子串 中等 字符串
93 复原IP地址 中等 字符串
227 基本计算器 II 中等 字符串

七、 DFS

1. 套路

核心思想是基于栈,一直往深度遍历,然后逐步返回得到解

2. 模板

result = []
def backtrack(路径,选择列表):
	if 满足结束条件:
		result.add(路径)
		return;
	for 选择 in 选择列表:
		做选择
		backtrack(路径,选择列表)
		撤销选择

3. leetcode例题

八、 BFS

1. 套路

  1. 核心思想基于队列,一般也是用队列求解;
  2. 取出一个值,把符合条件的值放入队列尾部,直到队列为空;

2. 模板

int BFS(Node start, Node target) {
	Queue<Node> q; // 核心数据结构
	Set<Node> visited; // 避免走回头路
	
	q.offer(start); // 将起点加入队列
	visited.add(start);
	int step = 0; // 记录扩散的步数
	while (q not empty) {
		int sz = q.size();
		/* 将当前队列中的所有节点向四周扩散 */
		for (int i = 0; i < sz; i++) {
			Node cur = q.poll();
			/* 划重点: 这里判断是否到达终点 */
			if (cur is target) {
				return step;
			}
			/* 将cur的相邻节点加入队列 */
			for (Node x : cur.adj()) {
				if (x not in visited) {
					q.offer(x);
					visited.add(x);
				}
			}
		}
		/* 划重点:在这里更新步数 */
		step++;
	}
}

3. leetcode例题

九、 贪心

1. 套路

  1. 主要是优先选择,按固定的规律取值(会用到优先队列取最小值);
  2. 重点思想是把一些值平铺开;

2. 模板

3. leetcode例题

  • 1353 最多可以参加的会议数目
    在这里插入图片描述
class Solution {
public:
    int maxEvents(vector<vector<int>>& events) {
        int maxDay = 0;
        // 构建一个【开始天】 和 【结束天】的映射
        unordered_map<int, vector<int>> day2days;
        for (vector<int>& event : events)
        {
            if (maxDay < event[1])
            {
                maxDay = event[1];
            }
            day2days[event[0]].push_back(event[1]);
        }
        // 记录参见会议的次数
        int res = 0;
        // 小顶堆队列
        priority_queue<int, vector<int>, greater<int>> q;
        for (int i = 1; i <= maxDay; ++i)
        {
            // 增加新的结束时间
            if (day2days.find(i) != day2days.end())
            {
                for (int day : day2days[i])
                {
                    q.push(day);
                }
            }
            // 删除队列里结束时间小于i的会议:因为它们已经结束了,无法再选择
            while (!q.empty() && q.top() < i)
            {
                q.pop();
            }
            // 直接取最小结束时间会议,次数+1
            if (!q.empty())
            {
                q.pop();
                ++res;
            }
        }
        return res;
    }
};
class Solution:
    def maxEvents(self, events: List[List[int]]) -> int:
        day_to_days = dict()
        max_day = 0
        for start, end in events:
            day_to_days.setdefault(start, []).append(end)
            max_day = max(max_day, end)
        q = list()
        res = 0
        for d in range(1, max_day+1):
            if day_to_days.get(d):
                for i in day_to_days[d]:
                    heapq.heappush(q, i)
            while len(q) > 0 and q[0] < d:
                heapq.heappop(q)
            if len(q) > 0:
                heapq.heappop(q)
                res += 1
        return res

在这里插入图片描述

class Solution {
public:
    int scheduleCourse(vector<vector<int>>& courses) {
        sort(courses.begin(), courses.end(), [](const auto& c0, const auto& c1) {
            return c0[1] < c1[1];
        });

        priority_queue<int> q;
        // 优先队列中所有课程的总时间
        int total = 0;

        for (const auto& course: courses) {
            int ti = course[0], di = course[1];
            if (total + ti <= di) {
                total += ti;
                q.push(ti);
            }
            else if (!q.empty() && q.top() > ti) {
                total -= q.top() - ti;
                q.pop();
                q.push(ti);
            }
        }

        return q.size();
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/course-schedule-iii/solution/ke-cheng-biao-iii-by-leetcode-solution-yoyz/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

452 用最少数量的箭引爆气球 中等 贪心
在这里插入图片描述

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if (points.empty()) {
            return 0;
        }
        sort(points.begin(), points.end(), [](const vector<int> &p1, const vector<int> &p2) {
            return p1[1] < p2[1];
        });
        int pos = points[0][1];
        int res = 1;
        for (auto point : points) {
            if (point[0] > pos) {
                pos = point[1];
                res++;
            }
        }
        return res;
    }
};

376 摆动序列 中等 贪心
在这里插入图片描述
贪心题解:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() == 1) {
            return 1;
        }
        int preDiff = nums[1] - nums[0];
        int res = preDiff != 0 ? 2 : 1;
        for (int i = 2; i < nums.size(); i++) {
            int currDiff = nums[i] - nums[i - 1];
            if ((preDiff >= 0 && currDiff < 0) || (preDiff <= 0 && currDiff > 0)) {
                res++;
                preDiff = currDiff;
            }
        }
        return res;
    }
};

类比:

6066. 统计区间中的整数数目
给你区间的 空 集,请你设计并实现满足要求的数据结构:

新增:添加一个区间到这个区间集合中。
统计:计算出现在 至少一个 区间中的整数个数。
实现 CountIntervals 类:

CountIntervals() 使用区间的空集初始化对象
void add(int left, int right) 添加区间 [left, right] 到区间集合之中。
int count() 返回出现在 至少一个 区间中的整数个数。
注意:区间 [left, right] 表示满足 left <= x <= right 的所有整数 x 。

 

示例 1:

输入
["CountIntervals", "add", "add", "count", "add", "count"]
[[], [2, 3], [7, 10], [], [5, 8], []]
输出
[null, null, null, 6, null, 8]

解释
CountIntervals countIntervals = new CountIntervals(); // 用一个区间空集初始化对象
countIntervals.add(2, 3);  // 将 [2, 3] 添加到区间集合中
countIntervals.add(7, 10); // 将 [7, 10] 添加到区间集合中
countIntervals.count();    // 返回 6
                           // 整数 2 和 3 出现在区间 [2, 3] 中
                           // 整数 7、8、9、10 出现在区间 [7, 10] 中
countIntervals.add(5, 8);  // 将 [5, 8] 添加到区间集合中
countIntervals.count();    // 返回 8
                           // 整数 2 和 3 出现在区间 [2, 3] 中
                           // 整数 5 和 6 出现在区间 [5, 8] 中
                           // 整数 7 和 8 出现在区间 [5, 8] 和区间 [7, 10] 中
                           // 整数 9 和 10 出现在区间 [7, 10] 中
 

提示:

1 <= left <= right <= 109
最多调用  add 和 count 方法 总计 105 次
调用 count 方法至少一次
通过次数2,536提交次数9,939
from sortedcontainers import SortedDict as SD
class CountIntervals:

    def __init__(self):
        self.mp = SD()
        self.cnt = 0

    def add(self, left: int, right: int) -> None:
        idx = self.mp.bisect_left(left)
        n = len(self.mp)
        while idx < n and self.mp.values()[idx] <= right:
            r, l = self.mp.items()[idx]
            left, right = min(left, l), max(r, right)
            self.mp.popitem(idx)
            n -= 1
            self.cnt -= r - l + 1
        self.mp[right] = left
        self.cnt += right - left + 1

    def count(self) -> int:
        return self.cnt


# Your CountIntervals object will be instantiated and called as such:
# obj = CountIntervals()
# obj.add(left,right)
# param_2 = obj.count()

知识点:

  1. SortedDict bisect_left 方法

十、 字典树

1. 套路

2. 模板

首先是手撕前缀树,一个前缀树包括当前的值word以及子节点数组children,这个数组的长度是26。然后就可以利用c++的结构体struct进行构造

struct TrieNode{
        string word;
        TrieNode* children[26];

//初始化
        TrieNode(){
            word = "";

//利用memset方法,把children所有元素填充0
            memset(children, 0, sizeof(children));
        }
    };

第二,实现分割字符串的方法,这里返回一个字符串数组,每一个元素是一个单词。

vector<string> Split(string& str){
        vector<string> res;

        //转化成为stringsteam对象
        stringstream ss(str);

        string curr;

        //使用std::getline方法按照空格分割stringstream对象ss,并把结果存储到curr当中
        while(std::getline(ss, curr, ' ')){
            if(curr.size() > 0){
                res.push_back(curr);
            }
        }
        return res;
    }

第三,通过dictionary数组构造前缀树

 TrieNode* root = new TrieNode();    
        for(int i = 0; i < dictionary.size(); i++){
            TrieNode* curr = root;
            for(char letter: dictionary[i]){
                if(curr -> children[letter - 'a'] == NULL){
                    curr -> children[letter - 'a'] = new TrieNode();
                }//不断往下插入字母
                curr = curr -> children[letter - 'a'];
            }
            //直到最后插入一个前缀的所有字母之后,在最后一个节点存储前缀单词名称
            curr -> word = dictionary[i];
        }

注意是在每一个前缀存储完成之后的节点存储前缀的名称,因为最后的替换操作要求的是最短的满足条件的前缀,因此只要是word 不为空,即说明对当前单词的替换过程要终止了

第四步,替换单词

//首先利用之前实现的split方法,分割字符串
vector<string> words = Split(sentence);
        string res;

        for(int i = 0; i < words.size(); i++){
            TrieNode* curr = root;
            for(char letter: words[i]){
                //如果没有对应的子节点了,就跳出对words[i]的循环,不对它进行替换
                if(curr -> children[letter - 'a'] == NULL){
                    break;
                }
                else{
                    curr = curr -> children[letter - 'a'];

//如果当前节点的前缀不为空了,意味着到达了最短的前缀,此时完成替换过程,跳出当前的循环
                    if(!curr -> word.empty()){
                        break;
                    }
                }
            }
            //除了刚开始,其他的时候在插入字符串之前,都要先插入“ ”
            if(!res.empty()) {
                res += " ";
            }
            //如果当前的前缀是空,则说明是没有合适替换的情况,此时不进行替换,直接插入原单词,反之,如果为空,则进行最短前缀的替换
            res += (curr -> word.empty())? words[i]: curr -> word;
        }

作者:Algorithms_Sustecher
链接:https://leetcode-cn.com/problems/replace-words/solution/c-chao-guo-96-qian-zhui-shu-de-qing-xi-j-3ld7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

3. leetcode例题

820 单词的压缩编码 中等 字典树
648 单词替换 中等 字典树
208 实现 Trie (前缀树) 中等 字典树

*、 几套题目

406, 358, 316,635

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas_Cai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值