系列综述:
💞目的:本系列是个人整理为了秋招算法
的,整理期间苛求每个知识点,平衡理解简易度与深入程度。
🥰来源:材料主要源于代码随想录
进行的,每个算法代码参考leetcode高赞回答和其他平台热门博客,其中也可能含有一些的个人思考。
🤭结语:如果有帮到你的地方,就点个赞和关注一下呗,谢谢🎈🎄🌷!!!
🌈数据结构基础知识总结篇
文章目录
- 一、贪心算法理论基础
- 定义
- 二、贪心算法基本题目
- 摆动序列
- 53. 最大子数组和
- 122. 买卖股票的最佳时机 II
- 55. 跳跃游戏
- 45. 跳跃游戏 II
- 1005. K 次取反后最大化的数组和
- 135. 分发糖果
- 406. 根据身高重建队列
- 452. 用最少数量的箭引爆气球
- 763. 划分字母区间
- 435. 无重叠区间
- 56. 合并区间
- 738. 单调递增的数字
- 参考博客
😊点此到文末惊喜↩︎
一、贪心算法理论基础
定义
- 贪心策略的选择
- 无后效性:当前做的选择不会影响未来做的选择,则该问题可通过所选择的贪心策略得到最优解
- 性价比高:贪心算法求得的较优解总能够满足是最优解的一个 1 − 1 e 1 - \frac {1} {e} 1−e1(63%) 近似
- 工程实践:通过对于
局部最优
举反例的方式,如果不能达到全局最优
,则换其他策略
- 一般步骤
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
- 贪心框架
void greedyAlgorithm(container<type> con){ // 健壮性检查 // 基本初始化 // 贪心策略的迭代 // 返回结果 return ; }
- 贪心策略的选择
- 图形化:将数据图形化,求特征
- 路径法:顺序遍历
动态规划对状态空间的遍历构成一张有向无环图,遍历就是该有向无环图的一个拓扑序。有向无环图中的节点对应问题中的「状态」,图中的边则对应状态之间的「转移」,转移的选取就是动态规划中的「决策」。
二、贪心算法基本题目
摆动序列
- 376. 摆动序列
- 贪心策略:将摆动进行图形化,记录峰值个数
- 本质:以返回值为目标,减少不必要的中间介质。本题不需要进行删除操作处理,只需要统计数组的峰值数量即可
- 尽量减少工作变量,如本题中,求峰值需要三个工作变量,但是只需要中间的两个差值。
int wiggleMaxLength(vector<int>& nums) { // 健壮性检查 if (nums.size() <= 1) return nums.size(); // 初始化 int curDiff = 0; // 当前一对差值 int preDiff = 0; // 前一对差值 int result = 1; // 记录峰值个数,序列默认序列最右边有一个峰值 for (int i = 0; i < nums.size() - 1; i++) { curDiff = nums[i + 1] - nums[i]; // 出现峰值 if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) { result++; preDiff = curDiff; } } return result; }
53. 最大子数组和
- 53. 最大子数组和
- 贪心策略:路径法,顺序遍历路径元素,求路径上的最大和,如果出现负数则重开路径
int wiggleMaxLength(vector<int>& nums) { // 健壮性检查 if (nums.size() <= 1) return nums.size(); // 初始化 int curDiff = 0; // 当前一对差值 int preDiff = 0; // 前一对差值 int result = 1; // 记录峰值个数,序列默认序列最右边有一个峰值 for (int i = 0; i < nums.size() - 1; i++) { curDiff = nums[i + 1] - nums[i]; // 出现峰值 if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) { result++; preDiff = curDiff; } } return result; }
122. 买卖股票的最佳时机 II
- 122. 买卖股票的最佳时机 II
- 图形化:向上的特征即为利润增加
- 贪心策略
- 空间开销小:利润>0就买入第二天卖出(贪心法应该单步考虑局部最优解)
- 时间开销小:只要明天比今天股价高,就买入并持有。(单次买入,只要涨就不卖出)
int maxProfit(vector<int>& prices) { int profit = 0; for(int i = 0; i + 1 < prices.size(); ++i) profit += max(prices[i + 1] - prices[i], 0); return profit; }
55. 跳跃游戏
- 55. 跳跃游戏
- 迭代范围也可以进行更改
- 贪心策略
- 每次在可选范围内,跳最大的
bool canJump(vector<int>& nums) { int cover = 0; if (nums.size() == 1) return true; // 每次更改迭代范围范围 for (int i = 0; i <= cover; i++) { cover = max(i + nums[i], cover); if (cover >= nums.size() - 1) return true; // 说明可以覆盖到终点了 } return false; }
45. 跳跃游戏 II
- 45. 跳跃游戏 II
int jump(vector<int>& nums) { int ans = 0; int end = 0; int maxPos = 0; for(int i = 0; i < nums.size() - 1; i++){ maxPos = max(nums[i] + i, maxPos); if(i == end){ end = maxPos;// 每次从结束位置开始,而不是起跳位置 ans++; } } return ans; }
1005. K 次取反后最大化的数组和
- 1005. K 次取反后最大化的数组和
- sort的使用:第三个参数为自定义的排序队则,在头文件#include
- accumulate的使用:第三个参数为累加的初值,在头文件include
static bool cmp(int a, int b) { return abs(a) > abs(b);// 绝对值的从大到小进行排序 } int largestSumAfterKNegations(vector<int>& A, int K) { // 将容器内的元素按照绝对值从大到小进行排序 sort(A.begin(), A.end(), cmp); // 在K>0的情况下,将负值按照绝对值从大到小依次取反 for (int i = 0; i < A.size(); i++) { if (A[i] < 0 && K > 0) { A[i] *= -1; K--; } } // 如果K为奇数,将最小的正数取反 if (K % 2 == 1) A[A.size() - 1] *= -1; // 求和 return accumulate(A.begin(),A.end(),0); // 第三个参数为累加的初值,在头文件include<numeric> }
135. 分发糖果
- 135. 分发糖果
双向遍历
进行贪心处理
int candy(vector<int>& ratings) { vector<int> candyVec(ratings.size(), 1); // 从前向后 for (int i = 1; i < ratings.size(); i++) { if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1; } // 从后向前 for (int i = ratings.size() - 2; i >= 0; i--) { if (ratings[i] > ratings[i + 1] ) { candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1); } } // 统计结果 int result = 0; for (int i = 0; i < candyVec.size(); i++) result += candyVec[i]; return result; }
406. 根据身高重建队列
- 406. 根据身高重建队列
两个维度遍历
进行贪心处理- 常用插入操作使用list进行处理
- 感觉可以局部最优推出整体最优,而且想不到反例。就可以使用贪心算法。
// 身高从大到小排(身高相同k小的站前面) static bool cmp(const vector<int>& a, const vector<int>& b) { if (a[0] == b[0]) // 相等的,数量小的在前 return a[1] < b[1]; return a[0] > b[0];// 其他情况身高高的的在前 } vector<vector<int>> reconstructQueue(vector<vector<int>>& people) { sort (people.begin(), people.end(), cmp); list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多 for (int i = 0; i < people.size(); i++) {// 按序插入 int position = people[i][1]; // 插入到下标为position的位置 std::list<vector<int>>::iterator it = que.begin(); while (position--) { // 寻找在插入位置 it++; } que.insert(it, people[i]); } return vector<vector<int>>(que.begin(), que.end()); }
452. 用最少数量的箭引爆气球
- 452. 用最少数量的箭引爆气球
- 贪心算法通常先进行
排序
和最值
处理
// 基本比较函数的使用:1.const &形参 2. 形参为比较的两个数据元素 3. 返回值为两个形参的比较 static bool cmp (const vector<int>& a, const vector<int>& b){ if(a[0] == b[0]) return a[1] < b[1]; return a[0] < b[0]; } int findMinArrowShots(vector<vector<int>>& points) { if(points.size() == 0) return 0; int count = 1;// 至少射一支箭 sort(points.begin(), points.end(), cmp); for(int i = 1; i < points.size(); ++i){ if(points[i-1][1] < points[i][0]){ count++; }else{// 记录最小下限 points[i][1] = min(points[i - 1][1], points[i][1]); } } return count; }
- 贪心算法通常先进行
763. 划分字母区间
- 763. 划分字母区间
- 如果只有字母,可以使用数组进行哈希映射。
vector<int> alphabet = {27,0}
vector<int> partitionLabels(string s) { // 统计每个字符的最远坐标 unordered_map<char, int> umap; for(int i = 0; i < s.size(); ++i){ umap[s[i]] = i; } // 初始化 vector<int> res; int left = 0; int right = 0; // 迭代 for(int i = 0; i < s.size(); ++i){ right = max(right, umap[s[i]]); // 找到字符出现的最远边界 if (i == right) { res.push_back(right - left + 1);// 结果记录 left = i + 1; } } return res; }
- 如果只有字母,可以使用数组进行哈希映射。
435. 无重叠区间
- 435. 无重叠区间
- 能读就不写:求数量的尽量不要改变改变原来数组,减少写操作。
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; }
56. 合并区间
- 56. 合并区间
- 排序参数lambda表达式的使用
- 遍历的容器尽量不用动,使用新的结果容器进行处理
vector<vector<int>> merge(vector<vector<int>>& intervals) { // 健壮性检查 if (intervals.size() == 0) return result; // 区间集合为空直接返回 vector<vector<int>> result; // 排序的参数使用了lambda表达式 sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b){ return a[0] < b[0]; } ); // 第一个区间就可以放进结果集里,后面如果重叠,在result上直接合并 result.push_back(intervals[0]); // 合并区间,只更新右边界就好, // 因为result.back()的左边界一定是最小值,因为我们按照左边界排序的 for (int i = 1; i < intervals.size(); i++) { if (result.back()[1] >= intervals[i][0]) { // 发现重叠区间 result.back()[1] = max(result.back()[1], intervals[i][1]); } else { result.push_back(intervals[i]); // 区间不重叠 } } return result; }
738. 单调递增的数字
- 738. 单调递增的数字
- 数字转换成字符串处理
int monotoneIncreasingDigits(int N) { string strNum = to_string(N); // flag用来标记赋值9从哪里开始 // 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行 int flag = strNum.size(); for (int i = strNum.size() - 1; i > 0; i--) { if (strNum[i - 1] > strNum[i] ) { flag = i; strNum[i - 1]--; } } for (int i = flag; i < strNum.size(); i++) { strNum[i] = '9'; } return stoi(strNum); }
🚩点此跳转到首行↩︎
参考博客
- 代码随想录
- letcode