数据结构-学习
作者:互联网
数据结构
- 本文档资料参考leetcode和拉勾教育里的<300分钟搞定数据结构与算法>课程, 欢迎大家学习
- 如涉及侵权, 请及时联系
常用数据结构
- 数组\字符串 Array & String
- 链表 Linked-list
- 栈 Stack
- 队列 Queue
- 双端队列 Deque
- 树 Tree
数组\字符串
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j2SfuU4S-1614745156437)(dataStructureAndCpp.assets/image-20201130104101212.png)]
交换字符串首尾, 两个指针
-
优点
构建一个数组非常简单
能让我们在O(1)的时间里根据数组的下标(index)查询某个元素
-
缺点
构建时必须分配一段连续的空间
查询某个元素是否存在时需要遍历整个数组,耗费O(n)的时间, 其中,n是元素的个数
删除和添加某个元素时, 同样需要耗费O(n)的时间
-
例题:leetcode242 有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1: 输入: s = "anagram", t = "nagaram" 输出: true 示例 2: 输入: s = "rat", t = "car" 输出: false
#include <string> #include <iostream> #include <vector> #include <algorithm> using namespace std; class Solution { public: bool isAnagram(string s, string t) { if (s.length() != t.length()) { return false; } sort(s.begin(), s.end()); sort(t.begin(), t.end()); return s == t; } bool isAnagram2(string s, string t) { if (s.size() != t.size()) return false; //首先进行最基本的退出判断,那么之后的代码全是在s.size==t.size的基础上 vector<int> table(26, 0); //初始化一个长度为26,全部为0的vec, 用于记录对应的位置的字母出现的计数 for (auto ch : s) { table[ch - 'a']++; } for (auto ch : t) { table[ch - 'a']--; if (table[ch - 'a'] < 0) { return false; } } return true; } }; int main() { string s = "hello"; string t = "helo"; bool rets; Solution sol; rets = sol.isAnagram2(s, t); cout << rets << endl; return 0; }
-
总结, 异位词
- 长度肯定一样
- 含有的字母一样
- 分析转化问题的本质, 进而用相应的数据结构表示
-
链表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k4DhqZgk-1614745156441)(dataStructureAndCpp.assets/image-20201130104512922.png)]
-
优点
灵活地分配内存空间
能在O(1)时间内删除或添加元素 -
缺点
不能通过下标进行查询
查询元素需要O(n)时间 -
解题技巧
利用快慢指针(有时需要用到三个指针)
构建一个虚假的链表头 -
例如
两个排序链表, 进行整合排序
将链表的奇偶数按原定顺序分离, 生成前半部分为奇数,后半部分为偶数的链表 -
如何训练解题技巧
在纸上或白板上画出节点之间的相互关系
画出修改的方法 -
例题 leetcode25. K个一组翻转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例: 给你这个链表:1->2->3->4->5 当 k = 2 时,应当返回: 2->1->4->3->5 当 k = 3 时,应当返回: 3->2->1->4->5 说明: 你的算法只能使用常数的额外空间。 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
#include <iostream> #include <vector> #include <tuple> using namespace std; //定义一个结构体链表节点 struct ListNode { double value = NULL; ListNode *next; }; class Solution { public: // 翻转一个子链表,并且返回新的头与尾 pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) { ListNode* prev = tail->next; ListNode* p = head; while (prev != tail) { ListNode* nex = p->next; p->next = prev; prev = p; p = nex; } return{ tail, head }; } ListNode* reverseKGroup(ListNode* head, int k) { ListNode* hair = new ListNode; //定义一个假的头指向真正的head hair->next = head; ListNode* pre = hair; //定义指向链表的节点 pre while (head) { ListNode* tail = pre; // 查看剩余部分长度是否大于等于 k for (int i = 0; i < k; ++i) { tail = tail->next; if (!tail) { return hair->next; } } ListNode* nex = tail->next; // c11写法 //pair<ListNode*, ListNode*> result = myReverse(head, tail); //head = result.first; //tail = result.second; // 这里是 C++17 的写法 tie(head, tail) = myReverse(head, tail); //传进去处理的是指针 // 把子链表重新接回原链表 pre->next = head; tail->next = nex; pre = tail; head = tail->next; } return hair->next; } }; int main() { vector<int> vec = { 1, 2, 3, 4, 5 ,6,7,8}; //初始化链表并赋值 ListNode *head; head = new ListNode; ListNode *tmp = head; for (auto v : vec) { if (tmp->value == NULL) { head->value = v; head->next = nullptr; } else { ListNode *ptr = new ListNode; ptr->value = v; ptr->next = nullptr; tmp->next = ptr; tmp = ptr; } } //执行 ListNode *ptr = head; Solution sol; ptr = sol.reverseKGroup(head, 3); //遍历链表 while (ptr != nullptr) { cout << ptr->value << " "; ptr = ptr->next; } cout << "\n"; return 0; }
栈
-
特点
后进先出
-
算法基本思想
可以用一个单链表来实现
只关心上一次操作
处理完上一次操作后,能在O(1)时间内查找到更前一次的操作
-
例题 leetcode 20 有效的括号
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。输入: "()" 输出: true 输入: "()[]{}" 输出: true 输入: "(]" 输出: false
#include <iostream> #include <stack> #include <string> #include <map> #include <unordered_map> using namespace std; class SolutionLeetcode { public: bool isValid(string s) { int n = s.size(); if (n % 2 == 1) { return false; } unordered_map<char, char> pairs = { { ')', '(' }, { ']', '[' }, { '}', '{' } }; stack<char> stk; for (char ch : s) { if (pairs.count(ch)) { if (stk.empty() || stk.top() != pairs[ch]) { return false; } stk.pop(); } else { stk.push(ch); } } return stk.empty(); } }; class Solution { public: bool isValid(string s) { stack<char> stacks; for (auto si : s) { if (stacks.empty()) { if ((si == ')') || (si == ']') || (si == ']')) return false; stacks.push(si); } else { char cur_s = stacks.top(); if ((cur_s == ')') || (cur_s == ']') || (cur_s == ']')) return false; if (cur_s == '(' && si == ')') { stacks.pop(); continue; } else if (cur_s == '[' && si == ']') { stacks.pop(); continue; } else if (cur_s == '{' && si == '}') { stacks.pop(); continue; } else { stacks.push(si); } } } if (stacks.empty()) return true; else return false; } }; int main() { string str = "[{[())()]}]"; bool ret; SolutionLeetcode sol; ret = sol.isValid(str); cout << "ret: " << ret << endl; return 0; }
-
例题 leetcode 739 每日温度
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
#include <iostream> #include <vector> #include <stack> using namespace std; class SolutionLeetcode { public: vector<int> dailyTemperatures(vector<int>& T) { int n = T.size(); vector<int> ans(n); stack<int> s; for (int i = 0; i < n; ++i) { while (!s.empty() && T[i] > T[s.top()]) { int previousIndex = s.top(); ans[previousIndex] = i - previousIndex; s.pop(); } s.push(i); } return ans; } }; class Solution { public: vector<int> dailyTemperatures(vector<int>& T) { vector<int> rets(T.size(),0); stack<int> stk; for (int i = 0; i < T.size(); i++) { if (stk.empty()) { stk.push(i); } else { int top_i = stk.top(); while (T[top_i] < T[i]) { rets[top_i] = i - top_i; stk.pop(); if (stk.empty()) break; top_i = stk.top(); } stk.push(i); } } return rets; } }; int main() { vector<int> T = { 73, 74, 75, 71, 69, 72, 76, 73 }; vector<int> rets; Solution sol; rets = sol.dailyTemperatures(T); for (auto ret : rets) { cout << ret << " "; } cout << endl; return 0; }
队列
-
特点
先进先出(按一定的顺序)
-
常用场景
广度优先搜索
双端队列
-
基本实现
可以利用一个双链表实现
队列的头尾两端能在O(1)的时间内进行数据的查看,添加和删除
-
常用的场景
实现一个长度动态变化的窗口或者连续区间
-
例题
239 滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7#include <iostream> #include <vector> #include <queue> using namespace std; class Solution { public: vector<int> maxSlidingWindow(vector<int>& nums, int k) { deque<pair<int,int>> dq; vector<int> rets; //初始化dq for (int i = 0; i < k; i++){ if (dq.empty()) dq.push_back({ i, nums[i] }); else{ while (!dq.empty() && dq.back().second < nums[i]) { dq.pop_back(); } dq.push_back({ i, nums[i] }); } } rets.push_back(dq.front().second); for (int i = k; i < nums.size(); i++){ if (dq.front().first <= i-3 ) dq.pop_front(); while (!dq.empty() && dq.back().second < nums[i]) { dq.pop_back(); } dq.push_back({ i, nums[i] }); rets.push_back(dq.front().second); } return rets; } }; int main(){ vector<int> nums = { 1, 3,-1, -3, 5, 3, 6, 7 }; int k = 3; vector<int> rets; Solution sol; rets = sol.maxSlidingWindow(nums, k); for (auto ret : rets){ cout << ret << " "; } cout << endl; return 0; }
-
解题思路
- 处理前 k 个元素,初始化双向队列。
-
遍历整个数组。在每一步 :
清理双向队列 :-
只保留当前滑动窗口中有的元素的索引。
-
移除比当前元素小的所有元素,它们不可能是最大的。
-
将当前元素添加到双向队列中。
-
将 deque[0] 添加到输出中。
-
返回输出数组。
-
做到一步一步分析, 清晰的分析每一步会造成的影响和该有的操作
不必着急要把所有的步骤都一次性想清楚
-
树
-
树的共性
结构直观
通过树的问题来考察 <递归算法>
-
常见
普通二叉树
平衡二叉树
完全二查树
二叉搜索树: 左 < 根 < 右
四叉树
多叉树
特殊树: 红黑树
-
遍历
前序遍历 preorder
中序遍历
后序遍历
-
例题leetcode230 二叉搜索中第K小的元素
给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/
1 4
2
输出: 1示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/
3 6
/
2 4
/
1
输出: 3#include <iostream> #include <stack> #include <vector> #include "binarytree.h" using namespace std; vector<int> ret; void inorder(BinTreeNode<int> *subTree) { if (subTree != NULL) { inorder(subTree->leftChild); ret.push_back(subTree->data); inorder(subTree->rightChild); } } int main() { vector<int> vec = { 5, 3, 1, 4, 2,6,8,10,7,9 }; int k = 8; BinTreeNode<int> *rets = NULL; BinaryTree<int> btree; rets = btree.CreateBinTree(vec); //递归遍历:中序遍历 inorder(rets); cout << "kthSmallest : " << ret[k-1] << endl; return 0; }
堆
- 堆是具有以下性质的完全二叉树
- 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆
- 或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BNHa1fCN-1614745156444)(dataStructureAndCpp.assets/image-20201214102213947.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ahIHqEGV-1614745156450)(dataStructureAndCpp.assets/image-20201214102436974.png)]
- 同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子,该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
- 大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
- 小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
高级数据结构
优先队列
- 与普通队列的区别
- 保证每次取出的元素是队列中优先级最高的
- 优先级别可自定义(例如数值最小,最大)
- 最常用的场景
- 从杂乱无章的数据中按照一定的顺序(或者优先级)筛选数据(例如找出前k大的数)
- 本质
- 二又堆的结构,堆在英文里叫 Binary Heap
- 利用一个数组结构来实现完全二叉树
- 特性
- 数组里的第一个元素 array[0]拥有最高的优先级
- 给定一个下标i,那么对于元素 array[i]而言
- 父节点对应的元素下标是(i-1)/2
- 左侧子节点对应的元素下标是2 x i + 1
- *右侧子节点对应的元素下标是2 x i + 2
- 数组中每个元素的优先级都必须要高于它两侧子节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6e4ZNqBV-1614745156454)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/image-20201222104319874.png)]
- 其基本操作为以下两个
- 向上筛选( sift up/ bubble up)
- 向下筛选( sift down/ bubble down)
另一个最重要的时间复杂度:优先队列的初始化
-
例题 leetcode347.前K个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:输入: nums = [1], k = 1
输出: [1]#include <iostream> #include <vector> #include <unordered_map> #include <queue> #include <typeinfo> using namespace std; class Solution { public: static bool cmp(pair<int, int>& m, pair<int, int>& n) { return m.second > n.second; } vector<int> topKFrequent(vector<int>& nums, int k) { unordered_map<int, int> occurrences; for (auto& v : nums) { occurrences[v]++; } // pair 的第一个元素代表数组的值,第二个元素代表了该值出现的次数 priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> q(cmp); for (auto& v : occurrences) { int num = v.first; int count = v.second; if (q.size() == k) { if (q.top().second < count) { q.pop(); q.emplace(num, count); } } else { q.emplace(num, count); } } vector<int> ret; while (!q.empty()) { ret.emplace_back(q.top().first); q.pop(); } return ret; } }; int main() { vector<int> nums = { 1, 1, 1, 2, 2, 3 ,3,3}; int k = 2; vector<int> rets; Solution sol; rets = sol.topKFrequent(nums, k); for (auto ret : rets) { cout << ret << " "; } cout << endl; return 0; }
图
-
最基本知识点如下
阶、度
树、森林、环
有向图、无向图、完全有向图、完全无向图
连通图、连通分量
图的存储和表达方式:邻接矩阵、邻接链表 -
围绕图的算法也是各式各样
图的遍历:深度优先、广度优先
环的检测:有向图、无向图
拓扑排序
最短路径算法: Dijkstra、 Bellman-Ford、 Floyd Warshall
连通性相关算法: Kosaraju、 Tarjan、求解孤岛的数量、判断是否为树
图的着色、旅行商问题等 -
必需熟练掌握的知识点
图的存储和表达方式:邻接矩阵、邻接链表
图的遍历深度优先、广度优先
部图的检测( Bipartite)、树的检测、环的检测:有向图、无向图拓扑排序
联合-查找算法( Union-Find)
最短路径: Dijkstra、 Bellman-Ford -
例题 leetcode785.判断二分图
给定一个无向图graph,当这个图为二分图时返回true。
如果我们能将一个图的节点集合分割成两个独立的子集A和B,并使图中的每一条边的两个节点一个来自A集合,一个来自B集合,我们就将这个图称为二分图。
graph将会以邻接表方式给出,graph[i]表示图中与节点i相连的所有节点。每个节点都是一个在0到graph.length-1之间的整数。这图中没有自环和平行边: graph[i] 中不存在i,并且graph[i]中没有重复的值。
示例 1: 输入: [[1,3], [0,2], [1,3], [0,2]] 输出: true 解释: 无向图如下: 0----1 | | | | 3----2 我们可以将节点分成两组: {0, 2} 和 {1, 3}。 示例 2: 输入: [[1,2,3], [0,2], [0,1,3], [0,2]] 输出: false 解释: 无向图如下: 0----1 | \ | | \ | 3----2 我们不能将节点分割成两个独立的子集。
转化成图着色问题:分别使用深度优先搜索DFS和广度优先搜索BFS
#include <iostream> #include <vector> using namespace std; class Solution { private: static const int UNCOLORED = 0; //代表未着色 static const int RED = 1; //代表着色红色 static const int GREEN = 2; //代表着色绿色 vector<int> color; //存放节点颜色 bool valid; public: void dfs(int node, int c, const vector<vector<int>>& graph) { color[node] = c; //给节点赋予颜色 int cNei = (c == RED ? GREEN : RED); for (int neighbor : graph[node]) { if (color[neighbor] == UNCOLORED) { dfs(neighbor, cNei, graph); if (!valid) { return; } } else if (color[neighbor] != cNei) { valid = false; return; } } } bool isBipartite(vector<vector<int>>& graph) { int n = graph.size(); valid = true; color.assign(n, UNCOLORED); //初始化所有节点颜色为0 for (int i = 0; i < n && valid; ++i) { if (color[i] == UNCOLORED) { dfs(i, RED, graph); } } return valid; } }; int main() { //邻接表的方式存放图, 分别代表和{1,3}代表和0节点有边相连的点, {0,2}代表和1节点... vector<vector<int>> graph = { { 1, 3 }, { 0, 2 }, { 1, 3 }, { 0, 2 } }; bool ret; Solution sol; ret = sol.isBipartite(graph); cout << ret << endl; return 0; }
前缀树
-
也称字典树
这种数据结构被广泛地运用在字典查找当中 -
什么是字典查找?
例如:给定一系列构成字典的字符串,要求在字典当中找出所有以
ABC"开头的字符串
方法一:暴力搜索法
时间复杂度:O(M*N)
方法二:前缀树
时间复杂度:O(M)
-
经典应用
搜索框输入搜索文字,会罗列以搜索词开头的相关搜索汉字拼音输入法-联想输入法
-
重要性质
每个节点至少包含两个基本属性
children:数组或者集合,罗列出每个分支当中包含的所有字符
isend:布尔值,表示该节点是否为某字符串的结尾
根节点是空的
除了根节点,其他所有节点都可能是单词的结尾,叶子节点一定都是单词的结尾 -
最基本的操作
-
创建
- 方法
遍历一遍输入的字符串,对每个字符串的字符进行遍历
从前缀树的根节点开始,将每个字符加入到节点的 children字符集当中
如果字符集已经包含了这个字符,跳过
如果当前字符是字符串的最后一个,把当前节点的 isend标记为真
- 方法
-
搜索
-
方法
从前缀树的根节点出发,逐个匹配输入的前缀字符
如果遇到了,继续往下一层搜索
如果没遇到,立即返回
-
-
-
例题 leetcode212.单词搜索
给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sijq4JAG-1614745156456)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/search1.jpg)]
输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"] 输出:["eat","oath"]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8QkVzYqS-1614745156458)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/search2.jpg)]
输入:board = [["a","b"],["c","d"]], words = ["abcb"] 输出:[]
#include <iostream> #include <vector> #include <string> using namespace std; class TrieNode{ public: string word = ""; vector<TrieNode*> nodes; TrieNode() :nodes(26, 0){} }; class Solution { int rows, cols; vector<string> res; public: vector<string> findWords(vector<vector<char>>& board, vector<string>& words) { rows = board.size(); cols = rows ? board[0].size() : 0; if (rows == 0 || cols == 0) return res; //建立字典树的模板 TrieNode* root = new TrieNode(); for (string word : words){ TrieNode *cur = root; for (int i = 0; i<word.size(); ++i){ int idx = word[i] - 'a'; if (cur->nodes[idx] == 0) cur->nodes[idx] = new TrieNode(); cur = cur->nodes[idx]; } cur->word = word; } //DFS模板 for (int i = 0; i<rows; ++i){ for (int j = 0; j<cols; ++j){ dfs(board, root, i, j); } } return res; } void dfs(vector<vector<char>>& board, TrieNode* root, int x, int y){ char c = board[x][y]; //递归边界 if (c == '.' || root->nodes[c - 'a'] == 0) return; root = root->nodes[c - 'a']; if (root->word != ""){ res.push_back(root->word); root->word = ""; } board[x][y] = '.'; if (x>0) dfs(board, root, x - 1, y); if (y>0) dfs(board, root, x, y - 1); if (x + 1<rows) dfs(board, root, x + 1, y); if (y + 1<cols) dfs(board, root, x, y + 1); board[x][y] = c; } }; int main() { vector<vector<char>> board; board = { { 'o', 'a', 'a', 'n' }, { 'e', 't', 'a', 'e' }, { 'i', 'h', 'k', 'r' }, { 'i', 'f', 'l', 'v' } }; vector<string> words = { "oath", "pea", "eat", "rain" }; return 0; }
线段树
- 先从一个例题出发
假设我们有一个数组 array[0…n-1], 里面有n个元素,现在我们要经常对这个数组做两件事
1.更新数组元素的数值
2.求数组任意一段区间里元素的总和(或者平均值)- 方法一:遍历一遍数组
时间复杂度:O(n) - 方法二:线段树
时间复杂度: O(logn)
- 方法一:遍历一遍数组
- 什么是线段树
种按照二叉树的形式存储数据的结构,每个节点
保存的都是数组里某一段的总和 - 例如
数组是[1,3,5,7,9,11]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fd81mVty-1614745156459)(dataStructureAndCpp.assets/image-20201201091550134.png)]
-
例题 leetcode315.计算右侧小于当前元素的个数
给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
输入:nums = [5,2,6,1] 输出:[2,1,1,0] 解释: 5 的右侧有 2 个更小的元素 (2 和 1) 2 的右侧仅有 1 个更小的元素 (1) 6 的右侧有 1 个更小的元素 (1) 1 的右侧有 0 个更小的元素
#include <iostream> #include <vector> #include <algorithm> #include <iterator> using namespace std; class Solution { private: vector<int> c; vector<int> a; void Init(int length) { c.resize(length, 0); } int LowBit(int x) { return x & (-x); } void Update(int pos) { while (pos < c.size()) { c[pos] += 1; pos += LowBit(pos); } } int Query(int pos) { int ret = 0; while (pos > 0) { ret += c[pos]; pos -= LowBit(pos); } return ret; } void Discretization(vector<int>& nums) { a.assign(nums.begin(), nums.end()); sort(a.begin(), a.end()); a.erase(unique(a.begin(), a.end()), a.end()); } int getId(int x) { return lower_bound(a.begin(), a.end(), x) - a.begin() + 1; } public: vector<int> countSmaller(vector<int>& nums) { vector<int> resultList; Discretization(nums); Init(nums.size() + 5); for (int i = (int)nums.size() - 1; i >= 0; --i) { int id = getId(nums[i]); resultList.push_back(Query(id - 1)); Update(id); } reverse(resultList.begin(), resultList.end()); return resultList; } }; int main() { vector<int> nums = { 5, 2, 6, 1 }; vector<int> rets; Solution sol; rets = sol.countSmaller(nums); for (auto ret : rets) cout << ret << endl; return 0; }
树状数组
也被称为 Binary Indexed Tree
-
先从一个例题出发
假设我们有一个数组array[0…n-1],里面有n个元素,现在我们要经常对这个数组做两件事- 更新数组元素的数值
- 求数组前k个元素的总和(或者平均值)
-
方法一: 线段树
时间复杂度 O(logn)
-
方法一: 树状数组
时间复杂度 O(logn)
-
重要的基本特征
利用数组来表示多又树的结构,和优先队列有些类似
优先队列是用数组来表示完全二又树,而树状数组是多叉树
树状数组的第一个元素是空节点
如果节点 tree[y]是 tree[x]的父节点,那么需要满足y=x-(x&(-x)) -
例题leetcode 308.二维区域和检索-可变
给你一个 2D 矩阵 matrix,请计算出从左上角(row1, col1)
到右下角(row2, col2)
组成的矩形中所有元素的和。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cNde47T3-1614745156461)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/308_range_sum_query_2d_mutable.png)]
上述粉色矩形框内的,该矩形由左上角 (row1, col1) = (2, 1) 和右下角 (row2, col2) = (4, 3) 确定。其中,所包括的元素总和 sum = 8。
给定 matrix = [ [3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5] ] sumRegion(2, 1, 4, 3) -> 8 update(3, 2, 2) sumRegion(2, 1, 4, 3) -> 10
#include <iostream> #include <vector> using namespace std; class NumMatrix { private: vector<vector<int>> clone; vector<vector<int>> tree; int lowbit(int x) { return x & (-x); } int getSum(int row, int col) { int ans = 0; for (int i = row; i > 0; i -= lowbit(i)) for (int j = col; j > 0; j -= lowbit(j)) ans += tree[i][j]; return ans; } public: NumMatrix(vector<vector<int>>& matrix) { if (matrix.empty() || matrix[0].empty()) return; int n = matrix.size(), m = matrix[0].size(); clone.assign(n, vector<int>(m)); tree.assign(n + 1, vector<int>(m + 1)); for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) update(i, j, matrix[i][j]); } void update(int row, int col, int val) { int delta = val - clone[row][col]; clone[row][col] = val; for (int i = row + 1; i < tree.size(); i += lowbit(i)) for (int j = col + 1; j < tree[0].size(); j += lowbit(j)) tree[i][j] += delta; } int sumRegion(int row1, int col1, int row2, int col2) { return getSum(row2 + 1, col2 + 1) - getSum(row1, col2 + 1) - getSum(row2 + 1, col1) + getSum(row1, col1); } }; /** * Your NumMatrix object will be instantiated and called as such: * NumMatrix* obj = new NumMatrix(matrix); * obj->update(row,col,val); * int param_2 = obj->sumRegion(row1,col1,row2,col2); */ int main() { vector<vector<int>> matrix = { { 3, 0, 1, 4, 2 }, { 5, 6, 3, 2, 1 }, { 1, 2, 0, 1, 5 }, { 4, 1, 0, 1, 7 }, { 1, 0, 3, 0, 5 } }; int row1 = 2, col1=1, row2=4, col2=3; int rets; NumMatrix sol(matrix); rets = sol.sumRegion(row1, col1, row2, col2); cout << rets << endl; }
本节课总结
优先队列:常见面试考点,实现过程比较繁琐。在解決面试中的问题时,实行“拿来主义"即可
图:被广泛运用的数据结构,如大数据问题都得运用图论
在社交网络里,每个人可以用图的顶点表示,人与人直接的关系可以用图的边表
在地图上,要求解从起始点到目的地,如何行驶会更快捷,需要运用图论里的最短路径算法
前缀树;出现在面试的难题中,要求能熟练地书写它的实现以及运用
线段树和树状数组:应用场合比较明确
如果要求在一幅图片当中修改像素的颜色,求解任意矩形区间的灰度平均值,则需要采用二维的线段树
排序算法
-
基本的排序算法【简单直接助你迅速写出没有bug的代码
- 冒泡排序/ Bubble Sort
- 插入排序/ Insertion Sort
-
常考的排序算法【解决绝大部分涉及排序问题的关键
-
归并排序/ Merge Sort
-
快速排序/ Quick Sort
-
拓扑排序/ Topological Sort
-
其他排序算法【掌握好它的解题思想能开阔解题思路
-
堆排序/ Heap Sort
-
桶排序/ Bucket Sort
-
排序代码
#include <cstdlib> #include <ctime> #include <iostream> #include <string.h> using namespace std; void swap(int *nums, int i, int j) { int tmp = 0; tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } void insertSort(int *nums, int numsLen) /*插入排序*/ { for (int i = 1, j, current; i < numsLen; i++) { current = nums[i]; for (j = i - 1; j >= 0 && nums[j]>current; j--) { nums[j + 1] = nums[j]; } nums[j + 1] = current; } } void merge(int nums[], int left, int mid, int right) { int n = right - left + 1; int *tmp = new int[n]; int i = 0; int ll = left; int rr = mid + 1; while (ll <= mid && rr <= right) { tmp[i++] = nums[ll] <= nums[rr] ? nums[ll++] : nums[rr++]; } while (ll <= mid) { tmp[i++] = nums[ll++]; } while (rr <= right) { tmp[i++] = nums[rr++]; } for (int j = 0; j < n; ++j) { nums[left + j] = tmp[j]; } delete[] tmp;//删掉堆区的内存 } void mergeSort(int nums[], int left, int right) /*递归理解,归并排序*/ { if (left >= right) return; int mid = left + (right - left) / 2; mergeSort(nums, left, mid); mergeSort(nums, mid + 1, right); merge(nums, left, mid, right); } int partition(int nums[], int left, int right) { int randnum = rand() % (right - left + 1) + left; swap(nums, randnum, right); int i, j; for (i = left, j = left; j < right; j++) { if (nums[j] <= nums[right]) { swap(nums, i++, j); } } swap(nums, i, j); return i; } void quickSort(int nums[], int left, int right) /*快速排序*/ { if (left >= right) return; int p = partition(nums, left, right); quickSort(nums, left, p - 1); quickSort(nums, p + 1, right); } void bubbleSort(int *nums, int numsLen, int reverse) /*冒泡排序 *reverse=1正序,从小到大排序, 0逆序,从大到小排序 */ { //控制比较轮数 bool haschange = true; for (int i = 1; i < numsLen && haschange; i++) { //每轮比较多少 haschange = false; for (int j = 0; j < numsLen - i; j++) { if (reverse == 1) //正序 { if (nums[j] > nums[j + 1]) { swap(nums, j, j + 1); haschange = true; } } else //逆序 { if (nums[j] < nums[j + 1]) { swap(nums, j, j + 1); haschange = true; } } } } } int main() { int nums[] = { 100, 21, 27, 31,201, 19, 50, 32, 16, 25 }; int numsLen = sizeof(nums) / sizeof(nums[0]); cout << "排序前:" << endl; for (int i = 0; i < numsLen; i++) { cout << nums[i] << " "; } cout << endl; //bubbleSort(nums, numsLen, 1); //insertSort(nums, numsLen); //mergeSort(nums, 0, numsLen - 1); quickSort(nums, 0, numsLen - 1); cout << "排序后:" << endl; for (int i = 0; i < numsLen; i++) { cout << nums[i] << " "; } cout << endl; return 0; }
冒泡排序
原理图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rcEbIrvw-1614745156462)(dataStructureAndCpp.assets/dc2429f5191b7271678adaeb8d08535f.gif)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xjiwvYmz-1614745156463)(dataStructureAndCpp.assets/image-20201212121353353.png)]
两两比较元素是关键, 怎么知道是否排好序了, 定义一个haschange的变量, 如果该轮比较没有过元素交换, 则证明已经排好序了.
时间复杂度: (n-1) + (n-2) + … + 1 = n*(n-1)/2 --> O(n^2)
空间复杂度:O(1)
插入排序
-
与冒泡排序对比
在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的;
在插入排序中,经过每一轮的排序处理后,数组前端的数都是排好序的 -
插入排序的算法思想
不断地将尚未排好序的数插入到已经排好序的部分。 -
复杂度
空间复杂度: O(1)
时间复杂度: 1 + 2 + 3 + … + (n-1) = n*(n-1)/2 --> O(n^2)
-
leetcode147
归并排序
-
分治的思想
归并排序的核心思想是分治,把一个复杂问题拆分成若干个子问题来求解。 -
归并排序的算法思想
把数组从中间划分成两个子数组
直递归地把子数组划分成更小的子数组,直到子数组里面只有一个元素; -
复杂度
时间复杂度:T(n) = 2*T(n/2) + O(n) -> O(nlogn) 需要进行logn次切分,每一层的复杂度都是O(n)
空间复杂度:O(n)
快速排序
-
快速排序的算法思想
快速排序也采用了分治的思想
把原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子数组;在分成较小和较大的两个子数组过程中,如何选定一个基准值尤为关键。
-
复杂度
最优时间复杂度:T(n) = 2*T(n/2) + O(n) -> O(nlogn)
最复杂的情况时间复杂度: 每次选择了最大或最小值作为基准值
空间复杂度: O(logn)
-
leetcode215
拓扑排序
- 应用场合
拓扑排序就是要将图论里的顶点按照相连的性质进行排序 - 前提
必须是有向图
图里没有环 - 例题
有一个学生想要修完5门课程的学分,这5门课程分别用1、2、 5来表示,现在已知学习这些课程有如下的要求:
课程2和4依赖于课程1
课程3依赖于课程2和4
课程4依赖于课程1和2
课程5依赖于课程3和4
那么
这个学生应该按照怎样的顺序来学习这5门课程呢?
-
复杂度
时间复杂度:O(n)
-
需要的额外知识
深度优先搜索
-
一开始对各个顶点统计前驱(入度), 依次将入度为0的加入排序队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N1Qrqjrj-1614745156465)(dataStructureAndCpp.assets/image-20201214101039363.png)]
堆排序
参考:https://www.cnblogs.com/chengxiao/p/6129630.html
-
基本思路:
- a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
- b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
- c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
-
复杂度
时间复杂度:O(nlogn)
桶排序
-
基本思想
划分多个范围相同的区间,每个子区间自排序,最后合并
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VWAz8sUV-1614745156466)(dataStructureAndCpp.assets/image-20201214103846176.png)]
-
复杂度
时间复杂度: O(n + C)
空间复杂度:O(n + m)
递归和回溯
- 递归的基本性质:函数调用本身
把大规模的问题不断地变小,再进行推导的过程 - 回溯:利用递归的性质
从问题的起始点出发,不断尝试
返回一步甚至多步再做选择,直到抵达终点的过程
递归
-
递归算法是一种调用自身函数的算法
特点:可以使一个看似复杂的问题变得简洁和易于理解
经典案例:汉诺塔(又称河内塔) -
算法思想
要懂得如何将一个问题的规模变小
再利用从小规模问题中得出的结果
结合当前的值或者情况,得出最终的结果 -
通俗理解-自顶向下
把要实现的递归函数,看成已经实现好的
直接利用解决一些子问题
思考:如何根据子问题的解以及当前面对的情况得出答案 -
例题 leetcode 91 解码方法
一条包含字母
A-Z
的消息通过以下方式进行了编码:'A' -> 1 'B' -> 2 ... 'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
题目数据保证答案肯定是一个 32 位的整数。
输入:s = "12" 输出:2 解释:它可以解码为 "AB"(1 2)或者 "L"(12)。 输入:s = "226" 输出:3 解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。 输入:s = "0" 输出:0 输入:s = "1" 输出:1 输入:s = "2" 输出:1
#include <iostream> #include <string> using namespace std; //动态规划 int numDecodings(string s) { if (s[0] == '0') return 0; int pre = 1, curr = 1;//dp[-1] = dp[0] = 1 for (int i = 1; i < s.size(); i++) { int tmp = curr; if (s[i] == '0') if (s[i - 1] == '1' || s[i - 1] == '2') curr = pre; else return 0; else if (s[i - 1] == '1' || (s[i - 1] == '2' && s[i] >= '1' && s[i] <= '6')) curr = curr + pre; pre = tmp; } return curr; } int main() { string s = "226"; int rets; rets = numDecodings(s); cout << rets << endl; return 0; }
-
递归写法结构总结
- 判断当前情况是否非法,如果非法就立即返回,
也称为完整性检查( Sanity Check) - 判断是否满足结束递归的条件
- 将问题的规模缩小,递归调用
- 利用在小规模问题中的答案,结合当前的数据进行整合,得出最终的答案
- 判断当前情况是否非法,如果非法就立即返回,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PuHu9272-1614745156468)(dataStructureAndCpp.assets/image-20201214113027078.png)]
-
例题
LeetCode247 .中心对称数ii
中心对称数是指一个数字在旋转了180度之后看起来依旧相同的
数字(或者上下颠倒地看).
找到所有长度为n的中心对称数。示例
输入:n=2
输出:[“11”,“69”,"88"“96”]#include <iostream> #include <stdlib.h> #include <string> #include <stdexcept> #include <vector> using namespace std; class Solution { public: vector<string> helper(int n) { if (n == 1) { return vector<string>{"0", "1", "8"}; } if (n == 2) { return vector<string>{"11", "69", "88", "96", "00"}; } vector<string> subs = helper(n - 2); vector<string> ans; for (auto sub : subs) { ans.push_back("0" + sub + "0"); ans.push_back("1" + sub + "1"); ans.push_back("6" + sub + "9"); ans.push_back("9" + sub + "6"); ans.push_back("8" + sub + "8"); } return ans; } vector<string> findStrobogrammatic(int n) { if (n == 1) { return vector<string>{"0", "1", "8"}; } if (n == 2) { return vector<string>{"11", "69", "88", "96"}; } vector<string> subs = helper(n - 2); vector<string> ans; for (auto sub : subs) { ans.push_back("1" + sub + "1"); ans.push_back("6" + sub + "9"); ans.push_back("9" + sub + "6"); ans.push_back("8" + sub + "8"); } return ans; } }; int main() { int n = 3; vector<string> res; vector<string> res_help; Solution sol; try { res = sol.findStrobogrammatic(n); res_help = sol.helper(n); } catch (exception & e) { cout << "catch (runtime_error): " << e.what() << endl; } for (int i = 0; i < res.size();i++) cout << res[i] << " "; cout << endl; for (int i = 0; i < res_help. size(); i++) cout << res_help[i] << " "; cout << endl; return 0; }
-
复杂度计算
时间复杂度
-
迭代法
void hano(char A,char B, char C, int n) { if (n>0) #1个时间 { hano(A,C,B,n-1); #T(n-1)的时间 move(A,C); #1个时间 hano(B,A,C,n-1); #T(n-1)的时间 } }
T(n) = 1 + 2T(n-1) + 1 = 2T(n-1) + O(1) ->O(n) = 2^n
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zHD19mbS-1614745156470)(dataStructureAndCpp.assets/image-20201216093717047.png)]
-
公式法
当递归函数的时间执行函数满足如下的关系式时,可以利用公式法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n5VeYiOg-1614745156473)(dataStructureAndCpp.assets/image-20201216093925761.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w36qi64n-1614745156475)(dataStructureAndCpp.assets/image-20201216094021425.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-60PJDEjj-1614745156478)(dataStructureAndCpp.assets/image-20201216094128598.png)]
int recursiveFn(int n) { if (n==0) { return 0; } return recursiveFn(n/4) + recursiveFn(n/4) }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-017eyFZ1-1614745156479)(dataStructureAndCpp.assets/image-20201216094519378.png)]
-
回溯
-
回溯算法是一种试探算法,与暴力搜索最大的区别:
在回溯算法中,是一步步向前试探,对每一步探测的情况评估,再決定是否继续,可避免走弯路 -
回溯算法的精华
出现非法的情况时,可退到之前的情景,可返回一步或多步
再去尝试别的路径和办法 -
想要采用回溯算法,就必须保证:每次都有多种尝试的可能
-
回溯解题写法
1.首先判断当前情况是否非法,如果非法就立即返回
2.看看当前情况是否已经满足条件?如果是,就将当前结果保存起来并返回
3.在当前情况下,遍历所有可能出现的情况,并进行递归
4.递归完毕后,立即回溯,回溯的方法就是取消前一步进行的尝试[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MLg0b4v1-1614745156482)(dataStructureAndCpp.assets/image-20201216094901533.png)]
-
例题 leetcode39
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]#include <iostream> #include <vector> using namespace std; class Solution{ public: vector<vector<int>> backtracking(vector<int> candidates, int target, int start, vector<int> solution, vector<vector<int>> results) /*回溯函数:实际的函数体*/ { if (target < 0) return results; if (target == 0) { results.push_back(solution); return results; } for (int i = start; i < candidates.size(); i++) { solution.push_back(candidates[i]); results = backtracking(candidates, target - candidates[i], i, solution, results); solution.pop_back(); } return results; } vector<vector<int>> combinationSum(vector<int> candidates, int target) /*融合函数:入口函数(提供起始执行位置)和出口函数*/ { vector<vector<int>> results; results = backtracking(candidates, target, 0, {}, results); return results; } }; int main() { vector<int> candidates = { 2, 3, 6, 7 }; int target = 8; vector<vector<int>> rets; Solution sol; rets = sol.combinationSum(candidates, target); cout <<"rets_size():" << rets.size() << endl; for (auto ret : rets) { for (auto r : ret) { cout << r << " "; } cout << endl; } return 0; }
-
例题 leetcode52 N皇后
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
(每行一个皇后, 正上方和左右斜上方不能有皇后)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oPNQSBv0-1614745156484)(dataStructureAndCpp.assets/8-queens.png)]
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回 n 皇后不同的解决方案的数量。
示例:
输入: 4
输出: 2
解释: 4 皇后问题存在如下两个不同的解法。
[
[".Q…", // 解法 1
“…Q”,
“Q…”,
“…Q.”],["…Q.", // 解法 2
“Q…”,
“…Q”,
“.Q…”]
]#include <iostream> #include <vector> #include <cmath> using namespace std; class Solution{ public: int count; bool check(int row, int col, int columns[]) { for (int r = 0; r < row; r++) { if (columns[r] == col || row - r == abs(columns[r] - col)) { return false; } } return true; } void backtracking(int n, int row, int columns[]) { //是否所有n行里都摆放好了皇后 if (row == n) { count++; //找到了新的摆放方法 return; } //尝试将皇后放置在当前行中的每一列 for (int col = 0; col < n; col++) { columns[row] = col; //检查是否合法,如果合法就继续到下一行 if (check(row, col, columns)) { backtracking(n, row + 1, columns); } //如果不合法,就不要把皇后放在这列中(回溯) columns[row] = -1; } } int totalNQueens(int n) { count = 0; backtracking(n, 0, new int[n]); return count; } }; int main() { int n = 4; int rets = 0; Solution sol; rets = sol.totalNQueens(n); cout << rets << endl; return 0; }
-
复杂度
时间复杂度: O(n!)
深度与广度优先搜索
深度优先搜索DFS
- DFS解决什么问题
- DFS解決的是连通性的问题,即给定两一个起始点(或某种起始状态)和一个终点(或某种最终状态),
- 判断是否有一条路径能从起点连接到终点。
- 很多情况下,连通的路径有很多条,只需要找出一条即可,DFS只关心路径存在与否,不在乎其长短。
- 算法的思想
- 从起点出发,选择一个可选方向不断向前,直到无法继续为止
- 然后尝试另外一种方向,直到最后走到终点
- 如何对这个图进行深度优先的遍历呢?
- 1.深度优先遍历必须依赖**栈( Stack)**这个数据结构
- 2.栈的特点是后进先出(LIFO)
- DFS的递归实现
-
利用递归去实现DFS可以让代码看上去很简洁
-
递归的时候需要将当前程序中的变量以及状态压入到系统的栈里面
-
压入和弹出栈都需要较多的时间,如果需要压入很深的栈,会造成运行效率低下
-
- DFS的非递归实现
-
栈的数据结构也支持压入和弹出操作
-
完全可以利用栈来提高运行效率
-
- DFS复杂度分析
由于DFS是图论里的算法,分析利用DFS解题的复杂度时,应当借用图论的思想,图有两种表示方式- 邻接表(图里有V个顶点,E条边)
访问所有顶点的时间为O(V),查找所有顶点的邻居的时间为O(日,所以总的时间复杂度是O(V+E) - 邻接矩阵(图里有V个顶点,E条边)
查找每个顶点的邻居需要0的时间,所以查找整个矩阵的时候需要O(V^2)的时间
- 邻接表(图里有V个顶点,E条边)
- 利用DFS在迷宫里找一条路径
由于迷宫是用矩阵表示,所以假设它是一个M行N列邻接矩阵- 时间复杂度为O(MxN)
因为一共有MxN个顶点,所以时间复杂度就是O(MxN) - 空间复杂度为O(MxN)
DFS需要堆栈来辅助,在最坏情况下所有顶点都被压入堆栈,所以它的 空间复杂度是O(V),即O(MxN)
- 时间复杂度为O(MxN)
- 如何利用DFS寻找最短路径?
- 暴力解题法
找出所有路径,然后比较它们的长短,找出最短的那个
如果硬要使用DFS去找最短的路径,我们必须尝试所有的可能
DFS解決的只是连通性问题,不是用来求解最短路径问题的 - 优化解题思路
边寻找目的地,一边记录它和起始点的距离(也就是步数)
当发现从某个方向过来所需要的步数更少,则更新到这个点的步数
如果发现步数更多,则不再继续尝试
- 暴力解题法
广度优先搜索BFS
-
广度优先搜索一般用来解决最短路径的问题
-
广度优先的搜索是从起始点出发,一层一层地进行
-
每层当中的点距离起始点的步数都是相同的
-
双端BFS
-
同时从起始点和终点开始进行的广度优先的搜索称为双端BFS
-
双端BFS可以大大地提高搜索的效率
-
例如,想判断社交应用程序中两个人之间需要经过多少朋友介绍才能互相认识
-
-
如何对这个图进行广度优先的遍历呢?
- 1.广度优先遍历需要借用的数据结构是队列( Queue)
- 2.队列特点是先进先出(FIFO)
-
如何运用广度优先搜索在迷宮中寻找最短的路径?
-
复杂度分析
- 邻接表(图里有V个顶点,E条边)
- 每个顶点都需要被访问一次,因此时间复杂度是O(V),在访问每个顶点 的时候,与它相连的顶点(也就是每条边)也都要被访问一次,所以加起来就是O(E),因此整体时间复杂度就是O(V+E)。
- 邻接矩阵(图里有V个顶点,E条边)
- 由于有V个顶点,每次都要检查每个顶点与其他顶点是否有联系,因此时间复杂度是O(V^2)
- 邻接表(图里有V个顶点,E条边)
-
利用BFS在迷宫里找一条路径
由于迷宫是用矩阵表示,所以假设它是一个M行N列邻接矩阵- 时间复杂度为O(MxN)
- 因为一共有MxN个顶点,所以时间复杂度就是O(MxN)
- 空间复杂度为O(MxN)
- BFS需要借助一个队列,所有顶点都要进入队列一次,从队列弹出 一次
在最坏的情况下,空间复杂度是O(V),即O(MxN)
- BFS需要借助一个队列,所有顶点都要进入队列一次,从队列弹出 一次
- 时间复杂度为O(MxN)
-
从A走到B最多允许打通3堵墙,求最短路径的步数
-
暴力解题法
首先枚举出所有拆墙的方法,假设一共有K堵墙在当前的迷宫里,现在最多允许拆3堵墙,意味着可以选择 不拆、只拆一堵墙、两堵墙或三堵墙,那么一共有这么多种组合方式:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hNk297HX-1614745156486)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/image-20210101104447791.png)]
在这么多种情况下分别进行BFS,整体的时间复杂度就是O(n2xKw),从中找到最短的那条路径, 很显然,从中找到最短的那条路径是非常没有效率的做法
-
如何将BFS的数量减少?
在不允许打通墙的情況下,只有一个人进行BFS搜索,时间复杂度是n^2
允许打通一堵墙的情況下,分身为两个人进行BFS搜索,时间复杂度是2n^2
允许打通两堵墙的情况下,分身为三个人进行BFS搜索,时间复杂度是3n^2
允许打通三堵墙的情況下,分身为四个人进行BFS搜索,时间复杂度是4n^2-
关键问题
如果第一个人又遇到了一堵墙,那么他是否需要再次分身呢?不能
第一个人怎么告诉第二个人可以去访问这个点呢?把这个点放入到队列中就好了
如何让4个人在独立的平面里搜索呢?利用一个三维矩阵记录每个层面里的点即可
-
-
动态规划
将问题逐渐增大或将问题逐渐缩小, 寻找解决方案
-
动态规划的定义
一种数学优化的方法,同时也是编程的方法。 -
重要属性
-
最优子结构 Optimal Substructure
- 状态转移方程f(n)
-
重叠子问题 Overlapping Sub- problems
-
例题 leetcode300.最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1: 输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。 示例 2: 输入:nums = [0,1,0,3,2,3] 输出:4 示例 3: 输入:nums = [7,7,7,7,7,7,7] 输出:1
#include <iostream> #include <vector> #include <algorithm> #include <Windows.h> //max using namespace std; #define MAX(a,b) a>b?a:b class Solution { public: public: int lengthOfLIS(vector<int>& nums) { /* *官方: 动态规划 *问题规模逐渐扩大, 每一步仅考虑序列开头到该位置的最大长度, 只记录最大长度 */ int n = (int)nums.size(); if (n == 0) return 0; vector<int> dp(n, 0); for (int i = 0; i < n; ++i) { dp[i] = 1; for (int j = 0; j < i; ++j) { if (nums[j] < nums[i]) { dp[i] = max(dp[i], dp[j] + 1); //求位置i处的最长子序列, 方法是将0 ~ i-1 位置处的元素与之一一比较 } } } return *max_element(dp.begin(), dp.end()); } int lengthOfLIS_baoli(vector<int>& nums) { /* 暴力法 遍历nums, 分别找到以nums[i]元素开头的最长子序列 注意需要保存上一步的结果 */ int maxlen=1; for (int i = 0; i < nums.size(); i++) { vector<vector<int>> tmpvec = {}; vector<int> tempi = { nums[i] }; tmpvec.push_back(tempi); for (int j = i + 1; j < nums.size(); j++) { vector<vector<int>> tmpvec2(tmpvec); //每一步操作之前, 先将上一步的结果保存下来 if (nums[j] > nums[i]) { for (int k = 0; k < tmpvec.size();k++) //遍历vec { if (tmpvec[k].at(tmpvec[k].size() - 1) < nums[j]) //只要当前元素大于vec里子元素的最后一个数值, 就加入 { tmpvec[k].push_back(nums[j]); maxlen = MAX(maxlen, tmpvec[k].size()); } } tmpvec.insert(tmpvec.end(), tmpvec2.begin(), tmpvec2.end()); //将vec2合并到vec } } } return maxlen; } }; int main() { vector<int> nums = { 0, 1, 0, 3, 2, 3 }; int rets; Solution sol; rets = sol.lengthOfLIS(nums); cout << rets << endl; return 0; }
线性规划
-
各个子问题的规模以线性的方式分布
-
子问题的最佳状态或结果可以存储在一维线性的数据结构中,例如:一位数组,哈希表等
-
通常我们会用dp[i]表示第i个位置的结果,或者从0开始到第i个位置为止的最佳状态或结果
-
基本形式
-
当前所求的值仅仅依赖于有限个先前计算好的值,即dp[i]仅仅依赖于有限个dp[j],其中j<i
-
例题1:求解斐波那契数列时,dp[i]=dp[i-1]+dip[i-2]。
-
当前值只依赖于前面两个计算好的值。
-
-
例题: leetcode70爬楼梯,就是一道求解斐波那契数列的题目
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
**注意:**给定 n 是一个正整数。
示例 1: 输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶 示例 2: 输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶 输入:4 输出: 5 解释: 1.1+1 +2 2.2 +2 ****** 3.1+1+1 +1 4.1+2 +1 5.2+1 +1
- 问题可以转为, 假设你以及爬到 n-1阶梯和n-2阶梯, 从n-1和n-2阶梯分别到达n阶梯所有的方法, 就是到达n的方法
- 状态转移方程为: f(n) = f(n-1) +f(n-2)
#include <iostream> using namespace std; class Solution { public: int climbStairs(int n) { /* *滚动数组思想: 把空间复杂度优化成 O(1) *将已经计算的结果记录下来, 避免重复计算 */ int p = 0, q = 0, r = 1; for (int i = 1; i <= n; ++i) { p = q; q = r; r = p + q; } return r; } int climbStairs_digui(int n) { /*递归方法: 时间复杂度高*/ if (n == 1) return 1; if (n == 2) return 2; return climbStairs(n - 1) + climbStairs(n - 2); } }; int main() { int n = 44; int rets; Solution sol; rets = sol.climbStairs(n); cout << rets << endl; return 0; }
-
例题: Leetcode198 打家劫舍: 给定一个数组,不能选择相邻的数,求如何才能使总数最大。
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1: 输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。 示例 2: 输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
- 与爬楼梯类似, 不同的是
- 状态转移方程: f(n) = max(f(n-1), f(n-2)+nums[n])
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Solution { public: int rob(vector<int>& nums) { /* 初始化一个vector用于记录只用n家时, 最大的解法 */ if (nums.empty()) { return 0; } int size = nums.size(); if (size == 1) { return nums[0]; } vector<int> dp = vector<int>(size, 0); dp[0] = nums[0]; dp[1] = max(nums[0], nums[1]); for (int i = 2; i < size; i++) { dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); } return dp[size - 1]; } int rob_my(vector<int>& nums) { /* *利用滑动数组 状态转移方程: f(n) = max(f(n-1), f(n-2)+nums[n]) */ if (nums.size() == 0) return 0; if (nums.size() == 1) return nums[0]; int p=0, q=0, r=nums[0]; for (int i = 1; i < nums.size(); i++) { p = q; q = r; r = max(q, p + nums[i]); } return r; } }; int main() { vector<int> nums = { 2,7,9,3,1 }; int rets; Solution sol; rets = sol.rob(nums); cout << rets << endl; return 0; }
-
例题:leetcode62 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3a8aMoCh-1614745156489)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/robot_maze.png)]
示例 1: 输入:m = 3, n = 7 输出:28 示例 2: 输入:m = 3, n = 2 输出:3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向右 -> 向下 2. 向右 -> 向下 -> 向右 3. 向下 -> 向右 -> 向右 示例 3: 输入:m = 7, n = 3 输出:28 示例 4: 输入:m = 3, n = 3 输出:6
#include <iostream> #include <vector> using namespace std; class Solution { public: int uniquePaths(int m, int n) { /* 状态转移方程: f(m,n) = f(m-1,n) + f(m,n-1) 递归方法, 时间复杂度高 return uniquePaths(m - 1, n) + uniquePaths(m, n - 1); 优化: 用一个m*n的矩阵, 保存每个位置的方法 */ if (m == 1 || n == 1) return 1; vector<vector<int>> vec2d(m, vector<int>(n, 1)); for (int i = 1; i < m; i++){ for (int j = 1; j < n; j++){ vec2d[i][j] = vec2d[i][j - 1] + vec2d[i - 1][j]; } } return vec2d[m - 1][n - 1]; } }; int main() { int rets; int m = 3, n =7; Solution sol; rets = sol.uniquePaths(m, n); cout << rets << endl; return 0; }
-
基本形式2
-
当前所求的值仅仅依赖于有限个先前计算好的值,即dp[i]仅仅依赖于有限个dp[j],其中j<i
-
当前所求的值仅仅依赖于所有先前计算和的值,即dp[i]是各个dp이j]的某种组合,其中j由0遍历到i-1
-
例题:在求解最长上升子序列时,dpli]=max(dp[j]+1)+1,0<=j<i
-
当前值依赖于前面所有计算好的值。
-
区间规划
-
各个子问题的规模由不同区间来定义
-
子问题的最佳状态或结果可以存储在二维数组中。
-
这类问题的时间复杂度一般为多项式时间,即对于一个大小为n的问题,时间复杂度不会超过n的多项式倍数。
-
例题 leetcode516 最长回文子序列
给定一个字符串
s
,找到其中最长的回文子序列,并返回该序列的长度。可以假设s
的最大长度为1000
。示例 1: 输入: "bbbab" 输出: 4 一个可能的最长回文子序列为 "bbbb"。 示例 2: 输入: "cbbd" 输出: 2 一个可能的最长回文子序列为 "bb"。
-
解题思路
状态 f[i][j] 表示 s 的第 i 个字符到第 j 个字符组成的子串中,最长的回文序列长度是多少。 转移方程 如果 s 的第 i 个字符和第 j 个字符相同的话 f[i][j] = f[i + 1][j - 1] + 2 如果 s 的第 i 个字符和第 j 个字符不同的话 f[i][j] = max(f[i + 1][j], f[i][j - 1]) 然后注意遍历顺序,i 从最后一个字符开始往前遍历,j 从 i + 1 开始往后遍历,这样可以保证每个子问题都已经算好了。 初始化 f[i][i] = 1 单个字符的最长回文序列是 1 结果 f[0][n - 1]
#include <iostream> #include <string> #include <vector> #include <algorithm> using namespace std; class Solution { public: int longestPalindromeSubseq(string s) { /*动态规划-区间规划写法 f[i][j] 表示 s 的第 i 个字符到第 j 个字符组成的子串中,最长的回文序列长度是多少。 转移方程 如果 s 的第 i 个字符和第 j 个字符相同的话 f[i][j] = f[i + 1][j - 1] + 2 如果 s 的第 i 个字符和第 j 个字符不同的话 f[i][j] = max(f[i + 1][j], f[i][j - 1]) */ int n = s.size(); vector<vector<int>> f(n, vector<int>(n)); for (int i = n - 1; i >= 0; i--){ f[i][i] = 1; for (int j = i + 1; j < n; j++){ if (s[i] == s[j]){ f[i][j] = f[i + 1][j - 1] + 2; } else{ f[i][j] = max(f[i + 1][j], f[i][j - 1]); //将递归倒过来写, 将计算结果保存下来, 避免重复计算 } } } return f[0][n - 1]; } int fij(string s, int i, int j){ if (i == j) return 1; if (i + 1 == j) { if (s[i] == s[j]){ return 2; } else { return 1; } } if (s[i] == s[j]) { return fij(s, i + 1, j - 1) + 2; } else { return max(fij(s, i, j - 1), fij(s, i + 1, j)); } } int longestPalindromeSubseq_digui(string s) { /*递归写法*/ int n = s.size(); return fij(s, 0, n); } }; int main() { string s = "bbbab"; int rets; Solution sol; rets = sol.longestPalindromeSubseq_digui(s); cout << rets << endl; return 0; }
-
约束规划
- 在普通的线性规划和区间规划里,一般题目有两种需求
- 统计
- 最优解
- 例题:0-1背包问题
给定n个物品,每个物品有各自的价值νi和重量wi,在限定的最大重量W内,
我们如何选择,才能使被帯走的物品的价值总和最大?
二分搜索算法和贪婪
- 二分搜索算法
-
看似简单,写对很难
-
变形很多
-
在面试中常用来考察code能力
-
- 贪婪算法
- 是一种比较直观的算法
- 难以证明它的正确性
二分搜索
-
定义
- 二分搜索也称折半搜索,是一种在有序数组中査找某一特定元素的搜索算法
-
运用前提
-
数组必须是排好序的
-
输入并不一定是数组,也可能是给定一个区间的起始和终止的位置
-
优点
- 分搜索也称对数搜索,其时间复杂度为O(logn),是一种非常高效的搜索
-
缺点
-
要求待查找的数组或区间是排好序的
-
若要求对数组进行动态地删除和插入操作并完成査找,平均复杂度会变为O(n)
-
采取自平衡的二又查找树
-
可在O( nlogn)的时间内用给定的数据构建出一棵二叉查找树
-
可在O(logn)的时间内对数据进行搜索
-
可在O(logn)的时间内完成删除和插入的操作
-
-
当:输入的数组或区间是有序的,且不会常变动,要求从中找出一个满足条件的元素-> 采用二分搜索
-
解题思路
-
解题模板
-
递归
-
优点是简洁
-
缺点是执行消耗大
1.二分搜索函数的定义中, 不仅要指定数组nums和目标查找数 target, 还要指定査找区间的起点Low和终点位置high. 2.为避免无限循环,开始时要判断一下 如果起点位置大于终点位置,表明这是一个非法区间; 或者说,已尝试了所有的搜索区间还是没找到结果。 返回-1. 3.接下来,取正中间那个数的下标 middle. 4.判断一下正中间的那个数是不是要找的目标数 target. 如果是,就返回下标 middle. 5.如果发现目标数在左边, 那么就递归地从左半边进行二分搜索。 6.否则从右半边递归地进行二分搜索。
**三个关键点** 1.计算 middle下标时, 不能简单地用(low+high)/2 这样可能会导致溢出。 2.取左半边和右半边的区间时, 左半边是[low, middle-1], 右半边是[ middle+1,high],这是两个闭区间。 我们确定了 middle点不是我们要找的, 因此没有必要再把它加入到左、右半边了。 3.对于一个长度为奇数的数组,例如:{1,2,3,4,5} 按照low+(high-Low)/2来计算的话, middle就是正中间的那个位, 对于ー个长度为偶数的数组,例如:{1,2,3,4} middle就是正中间靠左边的一个位置。
- 时间复杂度 : O(logn)
-
-
非递归
1.在 while循环中,判断一下搜索的区间是否有效。 2.计算正中间数的下标。 3.判断一下正中间的那个数是不是要找的目标数 target. 如果是,就返回下标 middle. 4.如果发现目标数在左边, 调整搜索区间的终点为 middle 5.否则,调整搜索区间的终点为 middle+1. 6.如果超出了搜索区间,表明无法找到目标数,返回-1
-
-
二分搜索的核心
- 确定搜索的范围和区间
- 取中间的数判断是否满足条件
- 如果不满足条件,判定应该往哪个半边继续进行搜索
-
找确定的边界
- 边界分为上边界与下边界,有时也称为左边界和右边界
- 确定的边界,指边界的数值等于要找的目标数
-
例题 leetcode34 在排序数组中査找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:
你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
示例 1: 输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4] 示例 2: 输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1] 示例 3: 输入:nums = [], target = 0 输出:[-1,-1]
-
分析
**两个成为8的下边界的条件** -该数必须是8 -该数的左边一个数必须不是8 --8的左边有数,那么该数必须小于8 --8的左边没有数,即8是数组的第一个数 **两个成为8的上边界的条件** -该数必须是8 -该数的右边一个数必须不是8 --8的右边有数,那么该数必须大于8 --8的右边没有数,即8是数组的最后一个数
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Solution { public: int searchLowerBound(vector<int>& nums, int target, int low, int high){ /*寻找下边界*/ if (low > high) return -1; int middle = low + (high - low) / 2; //判断是否是下边界时, //先看看 middle 的数是否为 target, //并判断该数是否已为数组的第一个数, //或者,它左边的一个数是不是已经比它小, //如果都满足,即为下边界。 if (nums[middle] == target && (middle == 0 || nums[middle - 1] < target)){ return middle; } if (target <= nums[middle]){ return searchLowerBound(nums, target, low, middle - 1); } else{ return searchLowerBound(nums, target, middle + 1, high); } } int searchUpperBound(vector<int>& nums, int target, int low, int high){ if (low > high) return -1; int middle = low + (high - low) / 2; 判断是否是上边界时, //先看看 middle 的数是否为 target, //并判断该数是否已为数组的最后一个数, //或者,它右边的数是不是比它大,如果都满足,即为上边界。 if (nums[middle] == target && (middle == nums.size() - 1 || nums[middle + 1] > target)){ return middle; } if (target < nums[middle]){ return searchUpperBound(nums, target, low, middle - 1); } else { return searchUpperBound(nums, target, middle+1, high); } } vector<int> searchRange(vector<int>& nums, int target){ /*递归, 二分搜索*/ int low = 0; int high = nums.size(); vector<int> ret(2, -1); if (high == 0) return ret; ret[0] = searchLowerBound(nums, target, low, high-1); ret[1] = searchUpperBound(nums, target, low, high-1); return ret; } }; int main() { vector<int> rets; vector<int> nums = { 2,2 }; int target = 3; Solution sol; rets = sol.searchRange(nums, target); for (auto ret : rets) cout << ret << endl; return 0; }
-
-
例题 leetcode 33.搜索旋转过的排序数组
升序排列的整数数组 nums 在预先未知的某个点上进行了旋转(例如, [0,1,2,4,5,6,7] 经旋转后可能变为 [4,5,6,7,0,1,2] )。
请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
示例 1: 输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4 示例 2: 输入:nums = [4,5,6,7,0,1,2], target = 3 输出:-1 示例 3: 输入:nums = [1], target = 0 输出:-1
#include<iostream> #include <vector> using namespace std; class Solution { public: int searchLowerBound(vector<int>& nums, int target, int low, int high){ if (low > high) return -1; int middle = low + (high - low) / 2; if (nums[middle] == target) return middle; if (nums[low] <= nums[middle]){ /*左侧是排好序的*/ if (nums[low] <= target && nums[middle] >= target){ return searchLowerBound(nums, target, low, middle - 1); } else { return searchLowerBound(nums, target, middle+1, high); } } else { /*右侧是排好序的*/ if (nums[middle] <= target && nums[high] >= target){ return searchLowerBound(nums, target, middle + 1, high); } else { return searchLowerBound(nums, target, low, middle-1); } } } int search(vector<int>& nums, int target) { /*二分搜索*/ int low = 0; int high = nums.size(); if (high == 0) return -1; return searchLowerBound(nums, target, low, high-1); } }; int main() { int rets; vector<int> nums = {3,1}; int target = 1; Solution sol; rets = sol.search(nums, target); cout << rets << endl; return 0; }
贪婪
-
定义
- 贪婪是一种在每一步选中都采取在当前状态下最好或最优的选择,从而希望导致结果是最好或最优的算法
-
优点
- 对于ー些问题,贪婪算法非常的直观有效
-
缺点
- 往往,它得到的结果并不是正确的
- 贪婪算法容易过早地做出決定,从而没有办法达到最优解
-
贪婪的弊端
-
总是做出在当前看来是最好的选择
-
不从整体的角度去考虑,仅对局部的最优解感兴趣
-
-
什么问题适用贪婪算法
-
只有当那些局部最优策略能产生全局最优策略的时候
-
例题 leetcode253 会议室
给定一个会议时间安排的数组,每个会议时间都会包括开始和结束的时间 [[s1,e1],[s2,e2],…] (si < ei),为避免会议冲突,同时要考虑充分利用会议室资源,请你计算至少需要多少间会议室,才能满足这些会议安排。
示例 1:
输入: [[0, 30],[5, 10],[15, 20]]
输出: 2
示例 2:输入: [[7,10],[2,4]]
输出: 1#include <iostream> #include <vector> #include <queue> #include <functional> //std::greater /std::less using namespace std; class Solution { public: int minMeetingRooms(vector<vector<int>>& intervals) { //按开始时间从小到大排序 sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>&b){ return a[0] < b[0]; }); //用优先队列构建一个堆 priority_queue<int, vector<int>, greater<int>> heap; //小顶堆 for (int i = 0; i < intervals.size(); i++) { if (!heap.empty() && heap.top() <= intervals[i][0]) { heap.pop(); } heap.push(intervals[i][1]); } return heap.size(); } }; int main() { vector<vector<int>> intervals = { { 0, 30 }, {5, 10 },{15, 20}}; int rets; Solution sol; rets = sol.minMeetingRooms(intervals); cout << rets << endl; return 0; }
高频真题
leetcode3.无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:
输入: s = ""
输出: 0
-
暴力法:子序列和子串的区别
-
子序列不需要相互挨着
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a5H624Ym-1614745156490)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/image-20210107104023622.png)]
-
子串要相互挨着
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KU8FWBOl-1614745156491)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/image-20210107104056876.png)]
-
-
线性法
- 快慢指针
- 哈希set
-
优化线性方法
- 哈希表记录位置
#include <iostream>
#include <unordered_set>
#include <algorithm>
using namespace std;
//滑动窗口法, 找到以每个字符开头的最大无重复子串
class Solution {
public:
int lengthOfLongestSubstring(string s) {
// 哈希集合,记录每个字符是否出现过
unordered_set<char> occ;
int n = s.size();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
// 枚举左指针的位置,初始值隐性地表示为 -1
for (int i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.erase(s[i - 1]);
}
while (rk + 1 < n && !occ.count(s[rk + 1])) {
// 不断地移动右指针
occ.insert(s[rk + 1]);
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = max(ans, rk - i + 1);
}
return ans;
}
};
int main()
{
int rets;
string s = "abcabcdbb";
Solution sol;
rets = sol.lengthOfLongestSubstring(s);
cout << rets << endl;
return 0;
}
leetcode4.寻找两个有序数组的中位数
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。
进阶:你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗?
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
示例 3:
输入:nums1 = [0,0], nums2 = [0,0]
输出:0.00000
示例 4:
输入:nums1 = [], nums2 = [1]
输出:1.00000
示例 5:
输入:nums1 = [2], nums2 = []
输出:2.00000
- 暴力法
- 切分法
- 拓展
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//二分查找
class Solution {
public:
int getKthElement(const vector<int>& nums1, const vector<int>& nums2, int k) {
/* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
* 这里的 "/" 表示整除
* nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
* nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
* 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
* 这样 pivot 本身最大也只能是第 k-1 小的元素
* 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
* 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
* 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
*/
int m = nums1.size();
int n = nums2.size();
int index1 = 0, index2 = 0;
while (true) {
// 边界情况
if (index1 == m) {
return nums2[index2 + k - 1];
}
if (index2 == n) {
return nums1[index1 + k - 1];
}
if (k == 1) {
return min(nums1[index1], nums2[index2]);
}
// 正常情况
int newIndex1 = min(index1 + k / 2 - 1, m - 1);
int newIndex2 = min(index2 + k / 2 - 1, n - 1);
int pivot1 = nums1[newIndex1];
int pivot2 = nums2[newIndex2];
if (pivot1 <= pivot2) {
k -= newIndex1 - index1 + 1;
index1 = newIndex1 + 1;
}
else {
k -= newIndex2 - index2 + 1;
index2 = newIndex2 + 1;
}
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int totalLength = nums1.size() + nums2.size();
if (totalLength % 2 == 1) {
return getKthElement(nums1, nums2, (totalLength + 1) / 2);
}
else {
return (getKthElement(nums1, nums2, totalLength / 2) + getKthElement(nums1, nums2, totalLength / 2 + 1)) / 2.0;
}
}
};
int main()
{
double rets;
vector<int> nums1 = { 1, 3 };
vector<int> nums2 = { 2 };
Solution sol;
rets = sol.findMedianSortedArrays(nums1, nums2);
cout << rets << endl;
return 0;
}
leetcode23.合并K个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例 2:
输入:lists = []
输出:[]
示例 3:
输入:lists = [[]]
输出:[]
- 暴力法
- 最小堆
- 分治法
//1顺序合并: 以合并2个序列为前提
class Solution {
public:
ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
if ((!a) || (!b)) return a ? a : b;
ListNode head, *tail = &head, *aPtr = a, *bPtr = b;
while (aPtr && bPtr) {
if (aPtr->val < bPtr->val) {
tail->next = aPtr; aPtr = aPtr->next;
} else {
tail->next = bPtr; bPtr = bPtr->next;
}
tail = tail->next;
}
tail->next = (aPtr ? aPtr : bPtr);
return head.next;
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode *ans = nullptr;
for (size_t i = 0; i < lists.size(); ++i) {
ans = mergeTwoLists(ans, lists[i]);
}
return ans;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/merge-k-sorted-lists/solution/he-bing-kge-pai-xu-lian-biao-by-leetcode-solutio-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
//2.分治法
class Solution {
public:
ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
if ((!a) || (!b)) return a ? a : b;
ListNode head, *tail = &head, *aPtr = a, *bPtr = b;
while (aPtr && bPtr) {
if (aPtr->val < bPtr->val) {
tail->next = aPtr; aPtr = aPtr->next;
} else {
tail->next = bPtr; bPtr = bPtr->next;
}
tail = tail->next;
}
tail->next = (aPtr ? aPtr : bPtr);
return head.next;
}
ListNode* merge(vector <ListNode*> &lists, int l, int r) {
if (l == r) return lists[l];
if (l > r) return nullptr;
int mid = (l + r) >> 1;
return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
return merge(lists, 0, lists.size() - 1);
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/merge-k-sorted-lists/solution/he-bing-kge-pai-xu-lian-biao-by-leetcode-solutio-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
//3.优先队列: 指针必须要指向一个地方, 初始化的时候必须为其开辟一个地方去指向 new xxx
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
using namespace std;
/**
* Definition for singly-linked list.
*/
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
struct Status {
int val;
ListNode *ptr;
bool operator < (const Status &rhs) const {
return val > rhs.val;
}
};
priority_queue <Status> q;
ListNode* mergeKLists(vector<ListNode*>& lists) {
for (auto node : lists) {
if (node) q.push({ node->val, node });
}
ListNode head, *tail = &head;
while (!q.empty()) {
auto f = q.top(); q.pop();
tail->next = f.ptr;
tail = tail->next;
if (f.ptr->next) q.push({ f.ptr->next->val, f.ptr->next });
}
return head.next;
}
};
int main()
{
vector<ListNode*> lists;
ListNode* val11 = new ListNode(0);
ListNode* val12 = new ListNode(0);
ListNode* val13 = new ListNode(0);
val11->val = 1;
val12->val = 4;
val13->val = 5;
val11->next = val12;
val12->next = val13;
lists.push_back(val11);
vector<int> lis2 = { 3, 4 };
ListNode* val21 = new ListNode(0);
val21->val = 1;
ListNode* cur = val21;
for (auto lis : lis2)
{
ListNode* tmp = new ListNode(0);
tmp->val = lis;
cur->next = tmp;
cur = tmp;
}
lists.push_back(val21);
vector<int> lis3 = { 6 };
ListNode* val31 = new ListNode(0);
val31->val = 2;
ListNode* cur3 = val31;
for (auto lis : lis3)
{
ListNode* tmp = new ListNode(0);
tmp->val = lis;
cur3->next = tmp;
cur3 = tmp;
}
lists.push_back(val31);
ListNode* rets = new ListNode(0);
Solution sol;
rets = sol.mergeKLists(lists);
while (rets){
cout << rets->val << endl;
rets = rets->next;
}
return 0;
}
leetcode56.合并区间
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入: intervals = [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.size() == 0) {
return{};
}
sort(intervals.begin(), intervals.end()); //首先将区间按左端点进行排序
vector<vector<int>> merged;
for (int i = 0; i < intervals.size(); ++i) {
int L = intervals[i][0], R = intervals[i][1];
if (!merged.size() || merged.back()[1] < L) {
merged.push_back({ L, R });
}
else {
merged.back()[1] = max(merged.back()[1], R);
}
}
return merged;
}
};
int main()
{
vector<vector<int>> intervals = { { 1, 3 }, { 2, 6 }, { 8, 10 }, { 15, 18 } };
vector<vector<int>> rets;
Solution sol;
rets = sol.merge(intervals);
for (auto ret : rets){
for (auto r : ret){
cout << r << " , ";
}
cout << endl;
}
return 0;
}
leetcode435.无重叠区间
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
- 暴力法
- 贪婪法
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.size() < 2) return 0;
//首先排序
sort(intervals.begin(), intervals.end());
int ret=0;
int L=0, R=0;
for (int i = 0; i < intervals.size(); i++){
if (i == 0){
L = intervals[i][0];
R = intervals[i][1];
}
else{
if (intervals[i][0] < R){
ret = ret + 1;
R = min(R, intervals[i][1]);
}
else
{
L = intervals[i][0];
R = intervals[i][1];
}
}
}
return ret;
}
};
int main()
{
vector<vector<int>> intervals = { {1,2}, { 1,2 }, {1,2 }, { 1,2 } };
int rets;
Solution sol;
rets = sol.eraseOverlapIntervals(intervals);
cout << rets << endl;
return 0;
}
leetcode269. 火星字典
- 懂得进行问题分解, 以及指针和引用在函数中的应用
- 题目描述
现有一种使用字母的全新语言,这门语言的字母顺序与英语顺序不同。
假设,您并不知道其中字母之间的先后顺序。但是,会收到词典中获得一个 不为空的 单词列表。因为是从词典中获得的,所以该单词列表内的单词已经 按这门新语言的字母顺序进行了排序。
您需要根据这个输入的列表,还原出此语言中已知的字母顺序。
示例 1:
输入:
[
"wrt",
"wrf",
"er",
"ett",
"rftt"
]
输出: "wertf"
示例 2:
输入:
[
"z",
"x"
]
输出: "zx"
示例 3:
输入:
[
"z",
"x",
"z"
]
输出: ""
解释: 此顺序是非法的,因此返回 ""。
提示:
你可以默认输入的全部都是小写字母
若给定的顺序是不合法的,则返回空字符串即可
若存在多种可能的合法字母顺序,请返回其中任意一种顺序即可
-
有向图
wrt
wrf
-
拓扑排序
-
解法:
逐位地比较两个相邻的字符串
第一个字母出现的顺序越早,排位越靠前
第一个字母相同时,比较第二字母,以此类推
注意:两个字符串某个相同的位置出现了不同,就立即能得出它们的顺序,无需继续比较字符串剩余字母。
//两大步骤,第一步是根据输入构建一个有向图;第二步是对这个有向图进行拓扑排序。
#include<iostream>
#include<vector>
#include<string>
#include<map>
#include<list>
#include<algorithm>
#include<set>
#include<stack>
using namespace std;
class Solution {
public:
// 将当前节点 u 加入到 visited 集合以及 loop 集合中。
bool topologicalSort(map<char, vector<char>>& adjList, char u, set<char>& visited, set<char>& loop, stack<char>&stk){
visited.insert(u);
loop.insert(u);
if (adjList.count(u)){
for (int i = 0; i < adjList.at(u).size(); i++){
char v = adjList.at(u)[i];
if (loop.count(v)) return false;
if (!visited.count(v)){
if (!topologicalSort(adjList, v, visited, loop, stk)){
return false;
}
}
}
}
loop.erase(u);
stk.push(u);
return true;
}
string alienOrder(vector<string>& words) {
// 基本情况处理,比如输入为空,或者输入的字符串只有一个
if (words.size() < 1) return "";
else if (words.size() == 1) return words[0];
// 构建有向图:定义一个邻接链表 adjList,也可以用邻接矩阵
map<char, vector<char>> adjList;
for (int i = 0; i < words.size()-1; i++){
string w1 = words[i], w2 = words[i + 1];
int n1 = w1.size(), n2 = w2.size();
bool found = false;
for (int j = 0; j < max(w1.size(), w2.size()); j++){
char c1 = j < n1 ? w1[j] : NULL;
char c2 = j < n2 ? w2[j] : NULL;
if (c1 != NULL && !adjList.count(c1)){
adjList[c1] = vector<char>();
}
if (c2 != NULL && !adjList.count(c2)){
adjList[c2] = vector<char>();
}
if (c1 != NULL && c2 != NULL && c1 != c2 && !found){
adjList.at(c1).push_back(c2);
found = true;
}
}
}
set<char> visited;
set<char> loop;
stack<char> stk;
for (auto items : adjList){
auto key = items.first;
if (!visited.count(key)){
if (!topologicalSort(adjList, key, visited, loop, stk)){
return "";
}
}
}
string sb="";
while (!stk.empty()){
sb = sb + stk.top();
stk.pop();
}
return sb;
}
};
int main()
{
string rets;
vector<string> words = { "wrt", "wrf", "er", "ett", "rftt" }; //
Solution sol;
rets = sol.alienOrder(words);
cout << rets << endl;
return 0;
}
leetcode772.基本计算器
实现一个基本的计算器来计算简单的表达式字符串。
表达式字符串可以包含左括号 (
和右括号)
,加号 +
和减号 -
,非负 整数和空格 。
表达式字符串只包含非负整数, +
, -
, *
, /
操作符,左括号 (
,右括号 )
和空格。整数除法需要向下截断。
你可以假定给定的字符串总是有效的。所有的中间结果的范围为 [-2147483648, 2147483647]。
"1 + 1" = 2
" 6-4 / 2 " = 4
"2*(5+5*2)/3+(6/2+8)" = 21
"(2+6* 3+5- (3*14/7+2)*5)+3"=-12
注:不要 使用内置库函数 eval。
#include<iostream>
#include<string>
#include<queue>
#include<ctype.h>
#include<stack>
using namespace std;
class Solution{
public:
int calculate(string s){
/*假设只有+和数字的情况*/
queue<char> q;
for (auto c : s){
if (c != ' '){ //允许空格出现
q.push(c);
}
}
q.push('+');
return calculate(q); //拓展允许小括号出现
}
int calculate(queue<char>& q){ //&q 使用q的引用
char sign = '+'; //拓展, 允许'-'出现, sign用于表示数字的正负
//定义两个变量, num用来表示当前数字
//sum用来记录最后的和
int num = 0, sum = 0;
//拓展, 有乘除号 , 定义一个新变量stack, 用来记录要被处理的数
stack<int> stk;
//遍历队列, 从队列中一个一个取出字符
while (!q.empty()){
char c = q.front(); //取出第一个字符
q.pop(); //取出后删除第一个字符
//如果当前字符是数字, 那么久更新num变量
//如果遇到了加号,就把当前num加到sum, num清零
if (isdigit(c)){
num = 10 * num + c - '0';
}
else if (c == '('){ //遇到左括号, 递归处理, 直到遇到右括号
num = calculate(q);
}
else{
//遇到了符号
if (sign == '+') {
stk.push(num); //遇到加号, 将当前数压入堆栈
}
else if (sign == '-'){
stk.push(-num); //遇到减号, 将当前相反数压入堆栈
}
else if (sign == '*'){
int tmp = stk.top();
stk.pop();
stk.push(tmp * num); //乘号
}
else if (sign == '/'){
int tmp = stk.top();
stk.pop();
stk.push(tmp / num); //乘号
}
num = 0;
sign = c; //更新sign
//遇到右括号
if (c == ')'){
break;
}
}
}
//将堆栈里的数字加起来
while (!stk.empty()){
sum += stk.top();
stk.pop();
}
return sum;
}
};
int main()
{
string s = "100*(15 - 5*2 )";
int rets;
Solution sol;
rets = sol.calculate(s);
cout << rets << endl;
return 0;
}
leetcode10. 正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
- ‘.’ 匹配任意单个字符
- ‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
示例 1:
输入:s = "aa" p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:s = "aa" p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
输入:s = "ab" p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入:s = "aab" p = "c*a*b"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入:s = "mississippi" p = "mis*is*p*."
输出:false
-
递归写法
#include <iostream> #include <vector> using namespace std; class Solution { public: bool isMatch(string s, int i, string p, int j) { int m = s.size(); int n = p.size(); //看看pattern和字符串是否都扫描完毕 //首先考虑的递归结束条件 if (j == n) return i == m; //next char is not '*', 必须满足当前字符并递归到下一层 if (j == n - 1 || p[j + 1] != '*'){ return (i < m) && (p[j] == '.' || s[i] == p[j]) && isMatch(s, i + 1, p, j + 1); } //next char is '*', 如果有连续的s[i]出现并且都等于p[j], 一直尝试下去 if (j < n - 1 && p[j + 1] == '*'){ while ((i < m) && (p[j] == '.' || s[i] == p[j])){ if (isMatch(s, i, p, j + 2)){ return true; } i++; } } //接着继续下去 return isMatch(s, i, p, j + 2); } bool isMatch(string s, string p) { if (s.size() == 0 || p.size() == 0) return false; return isMatch(s, 0, p, 0); } }; int main() { string s = "aa"; string p = ".*"; bool rets; Solution sol; rets = sol.isMatch(s, p); cout << rets << endl; return 0; }
-
递归写法 二: 倒着考虑
leetcode44.通配符匹配
给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。
‘?’ 可以匹配任何单个字符。
‘*’ 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。
说明:
s
可能为空,且只包含从a-z
的小写字母。p
可能为空,且只包含从a-z
的小写字母,以及字符?
和*
。
示例 1:
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:
s = "aa"
p = "*"
输出: true
解释: '*' 可以匹配任意字符串。
示例 3:
输入:
s = "cb"
p = "?a"
输出: false
解释: '?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。
示例 4:
输入:
s = "adceb"
p = "*a*b"
输出: true
解释: 第一个 '*' 可以匹配空字符串, 第二个 '*' 可以匹配字符串 "dce".
示例 5:
输入:
s = "acdcb"
p = "a*c?b"
输出: false
leetcode84.柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例:
输入: [2,1,5,6,2,3]
输出: 10
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NgPDUkjN-1614745156494)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/image-20210111113415359.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ijJjgIT-1614745156495)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/image-20210111113432564.png)]
- 暴力法
- 堆栈思想
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size(), maxarea = 0;
stack<int> stk;
for (int i = 0; i <= n; i++){
while (!stk.empty() && (i == n || heights[i] < heights[stk.top()])){
int height = heights[stk.top()];
int width = stk.empty() ? i : i - stk.top();
maxarea = max(maxarea, width*height);
stk.pop();
}
stk.push(i);
}
return maxarea;
}
};
int main()
{
vector <int> heights = { 2, 1, 5, 6, 5, 3 };
int rets;
Solution sol;
rets = sol.largestRectangleArea(heights);
cout << rets << endl;
return 0;
}
leetcode28.实现strStr()
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1:
输入: haystack = "hello", needle = "ll"
输出: 2
示例 2:
输入: haystack = "aaaaa", needle = "bba"
输出: -1
-
暴力法
#include <iostream> #include <string> using namespace std; class Solution { public: int strStr(string haystack, string needle) { /*暴力法*/ for (int i = 0;; i++){ for (int j = 0;; j++){ if (j == needle.size()) return i; if (i + j == haystack.size()) return -1; if (needle[j] != haystack[i + j]) break; } } } }; int main() { string haystack = "hllello"; string needle = "ll"; int rets; Solution sol; rets = sol.strStr(haystack, needle); cout << rets << endl; return 0; }
-
KMP: 跳跃式比较
- 重要数据结构:最长的公共前缀和后缀( Longest Prefix and Suffix,简称LPS)
- 它是一个数组
- 记录了字符串从头开始到某个位置结束的一段字串中,公共前缀和后缀的最大长度
- 公共前缀和后缀,即字符串的前缀等于后缀,并且,前缀和后缀不能是同一段字符串
#include <iostream> #include <string>
- 重要数据结构:最长的公共前缀和后缀( Longest Prefix and Suffix,简称LPS)
#include
using namespace std;
class Solution {
public:
vector getLPS(string str){
//初始化一个lps容器保存最终结果
vector lps(str.size(),0);
// lps 的第一个值一定是 0,
//即长度为 1 的字符串的最长公共前缀后缀的长度为 0,直接从第二个位置遍历。
//并且,初始化当前最长的 lps 长度为 0,用 len 变量记录下
int i = 1, len = 0;
//指针i遍历整个输入字符串
while (i < str.size()){
//若 i 指针能延续前缀和后缀,则更新 lps 值为 len+1
if (str[i] == str[len]) { lps[i++] = ++len; }
//否则,判断 len 是否大于 0,尝试第二长的前缀和后缀,是否能继续延续下去
else if (len>0) { len = lps[len - 1]; }
else { i++; }
}
return lps;
}
int strStr(string haystack, string needle) {
/KMP法/
int m = haystack.size();
int n = needle.size();
if (n == 0) return 0;
vector lps= getLPS(needle);
int i = 0, j = 0;
while (i < m){
if (haystack[i] == needle[j]){
i++;
j++;
}
else if (j == n){
return i - n;
}
else if (j>0){
j = lps[j - 1];
}
else{
i++;
}
}
return -1;
}
};
int main()
{
string haystack = “hello”;
string needle = “ll”;
int rets;
Solution sol;
rets = sol.strStr(haystack, needle);
cout << rets << endl;
return 0;
}
## leetcode336.回文对
给定一组 **互不相同** 的单词, 找出所有**不同** 的索引对`(i, j)`,使得列表中的两个单词, `words[i] + words[j]` ,可拼接成回文串。
示例 1:
输入:[“abcd”,“dcba”,“lls”,“s”,“sssll”]
输出:[[0,1],[1,0],[3,2],[2,4]]
解释:可拼接成的回文串为 [“dcbaabcd”,“abcddcba”,“slls”,“llssssll”]
示例 2:
输入:[“bat”,“tab”,“cat”]
输出:[[0,1],[1,0]]
解释:可拼接成的回文串为 [“battab”,“tabbat”]
- 解题思路
- 回文:正读和反读都一样的字符串
- 检查字符串是否回文
- 翻转给定字符串后,对比原字符串,
需 要O(n)的空间复杂度
- 定义指针主指向字符串的头,定义指针j指向字符串的尾,
从两头开始检查,发现不相等表明不是回文,
直检查到两个指针相遇为止
```c++
//判断回文对
bool isPalindrome(string word){
int i = 0;
int j = word.size() - 1;
while (i < j){
if (word[i++] != word[j--]){
return false;
}
}
return true;
}
```
- 暴力法
```c++
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <unordered_map>
using namespace std;
class Solution {
public:
//判断回文对
bool isPalindrome(string word){
int i = 0;
int j = word.size() - 1;
while (i < j){
if (word[i++] != word[j--]){
return false;
}
}
return true;
}
vector<vector<int>> palindromePairs(vector<string>& words) {
/*暴力法*/
vector<vector<int>> rets;
int m = words.size();
for (int i = 0; i < m; i++){
for (int j = i + 1; j < m; j++){
if (isPalindrome(words[i] + words[j])){
vector<int> temp = { i, j };
rets.push_back(temp);
}
if (isPalindrome(words[j] + words[i])){
vector<int> temp = { j, i };
rets.push_back(temp);
}
}
}
return rets;
}
};
int main()
{
vector<string> words = { "bat", "tab", "cat" };
vector<vector<int>> rets;
Solution sol;
rets = sol.palindromePairs(words);
for (auto ret : rets){
cout << ret[0] << " , " << ret[1] << endl;
}
return 0;
}
- 利用树结构 ******
leetcode340.至多包含K个不同字符的最长子串
给定一个字符串 *s* ,找出 至多 包含 k 个不同字符的最长子串 *T*。
示例 1:
输入: s = "eceba", k = 2
输出: 3
解释: 则 T 为 "ece",所以长度为 3。
示例 2:
输入: s = "aa", k = 1
输出: 2
解释: 则 T 为 "aa",所以长度为 2。
-
快慢指针
#include <iostream> #include <string> #include <map> #include <algorithm> using namespace std; class Solution { public: int lengthOfLongestSubstringKDistinct(string s, int k) { /*快慢指针,滑动窗口*/ map<char, int> mp; int maxlen = 0; for (int i = 0, j = 0; j < s.size(); j++){ char cj = s[j]; //step1. count the char mp[cj] = mp.count(cj) + 1; //step2.clean up the map if condition doesn't match while (mp.size() > k){ char ci = s[i]; mp[ci] = mp.count(ci) - 1; if (mp[ci] == 0){ mp.erase(ci); } i++; } //step3. condition matched, now update the result maxlen = max(maxlen, j - i + 1); } return maxlen; } }; int main() { int rets; string s = "ecaba"; int k = 2; Solution sol; rets = sol.lengthOfLongestSubstringKDistinct(s, k); cout << rets << endl; return 0; }
leetcode407.接雨水
给你一个 m x n
的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
示例:
给出如下 3x6 的高度图:
[
[1,4,3,1,3,2],
[3,2,1,3,2,4],
[2,3,3,2,3,1]
]
返回 4 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dpiqrjAn-1614745156496)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/image-20210112145844597.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UfxYrr31-1614745156498)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/image-20210112145901498.png)]
- 农村包围城市的方法/ BFS
leetcode417.太平洋大西洋水流问题
冲刺
- 不光是数量,质量上要严格把关
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LAPtGqi5-1614745156499)(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.assets/image-20210112151603057.png)]
-
白板代码
- 字迹要清楚
- 间距把握好
- 注意和面试官沟通
-
回顾做过的题目
-
整理思路
-
写到github上, 进行反复查阅
https://github.com/jeantimex/javascript-problems-and-solutions
-
-
简历
- 要有针对性
/
int m = haystack.size();
int n = needle.size();
if (n == 0) return 0;
vector lps= getLPS(needle);
int i = 0, j = 0;
while (i < m){
if (haystack[i] == needle[j]){
i++;
j++;
}
else if (j == n){
return i - n;
}
else if (j>0){
j = lps[j - 1];
}
else{
i++;
}
}
return -1;
}
};
int main()
{
string haystack = “hello”;
string needle = “ll”;
int rets;
Solution sol;
rets = sol.strStr(haystack, needle);
cout << rets << endl;
return 0;
} - 要有针对性
leetcode336.回文对
给定一组 互不相同 的单词, 找出所有不同 的索引对(i, j)
,使得列表中的两个单词, words[i] + words[j]
,可拼接成回文串。
示例 1:
输入:["abcd","dcba","lls","s","sssll"]
输出:[[0,1],[1,0],[3,2],[2,4]]
解释:可拼接成的回文串为 ["dcbaabcd","abcddcba","slls","llssssll"]
示例 2:
输入:["bat","tab","cat"]
输出:[[0,1],[1,0]]
解释:可拼接成的回文串为 ["battab","tabbat"]
-
解题思路
-
回文:正读和反读都一样的字符串
-
检查字符串是否回文
-
翻转给定字符串后,对比原字符串,
需 要O(n)的空间复杂度 -
定义指针主指向字符串的头,定义指针j指向字符串的尾,
从两头开始检查,发现不相等表明不是回文,
直检查到两个指针相遇为止
//判断回文对 bool isPalindrome(string word){ int i = 0; int j = word.size() - 1; while (i < j){ if (word[i++] != word[j--]){ return false; } } return true; }
-
-
-
暴力法
#include <iostream> #include <string> #include <vector> #include <map> #include <unordered_map> using namespace std; class Solution { public: //判断回文对 bool isPalindrome(string word){ int i = 0; int j = word.size() - 1; while (i < j){ if (word[i++] != word[j--]){ return false; } } return true; } vector<vector<int>> palindromePairs(vector<string>& words) { /*暴力法*/ vector<vector<int>> rets; int m = words.size(); for (int i = 0; i < m; i++){ for (int j = i + 1; j < m; j++){ if (isPalindrome(words[i] + words[j])){ vector<int> temp = { i, j }; rets.push_back(temp); } if (isPalindrome(words[j] + words[i])){ vector<int> temp = { j, i }; rets.push_back(temp); } } } return rets; } }; int main() { vector<string> words = { "bat", "tab", "cat" }; vector<vector<int>> rets; Solution sol; rets = sol.palindromePairs(words); for (auto ret : rets){ cout << ret[0] << " , " << ret[1] << endl; } return 0; }
-
利用树结构 ******
leetcode340.至多包含K个不同字符的最长子串
给定一个字符串 *s* ,找出 至多 包含 k 个不同字符的最长子串 *T*。
示例 1:
输入: s = "eceba", k = 2
输出: 3
解释: 则 T 为 "ece",所以长度为 3。
示例 2:
输入: s = "aa", k = 1
输出: 2
解释: 则 T 为 "aa",所以长度为 2。
-
快慢指针
#include <iostream> #include <string> #include <map> #include <algorithm> using namespace std; class Solution { public: int lengthOfLongestSubstringKDistinct(string s, int k) { /*快慢指针,滑动窗口*/ map<char, int> mp; int maxlen = 0; for (int i = 0, j = 0; j < s.size(); j++){ char cj = s[j]; //step1. count the char mp[cj] = mp.count(cj) + 1; //step2.clean up the map if condition doesn't match while (mp.size() > k){ char ci = s[i]; mp[ci] = mp.count(ci) - 1; if (mp[ci] == 0){ mp.erase(ci); } i++; } //step3. condition matched, now update the result maxlen = max(maxlen, j - i + 1); } return maxlen; } }; int main() { int rets; string s = "ecaba"; int k = 2; Solution sol; rets = sol.lengthOfLongestSubstringKDistinct(s, k); cout << rets << endl; return 0; }
leetcode407.接雨水
给你一个 m x n
的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
示例:
给出如下 3x6 的高度图:
[
[1,4,3,1,3,2],
[3,2,1,3,2,4],
[2,3,3,2,3,1]
]
返回 4 。
[外链图片转存中…(img-dpiqrjAn-1614745156496)]
[外链图片转存中…(img-UfxYrr31-1614745156498)]
- 农村包围城市的方法/ BFS
leetcode417.太平洋大西洋水流问题
冲刺
- 不光是数量,质量上要严格把关
[外链图片转存中…(img-LAPtGqi5-1614745156499)]
-
白板代码
- 字迹要清楚
- 间距把握好
- 注意和面试官沟通
-
回顾做过的题目
-
整理思路
-
写到github上, 进行反复查阅
https://github.com/jeantimex/javascript-problems-and-solutions
-
-
简历
- 要有针对性
- 写好工作履历
标签:rets,include,return,nums,int,学习,vector,数据结构 来源: https://blog.csdn.net/jonado13/article/details/114306901