摆动序列
这道题就是要求给定的数组中的峰值数以及端点。
把数组抽象成折线图会好理解一些,那么根据题意,我们就需要判断这些相邻的数的差值,不妨设pre_diff = nums[i] - nums[i - 1],cur_diff = nums[i + 1] - nums[i]。
那么出现峰值的条件为
if((pre_diff < 0 && cur_diff > 0) || (pre_diff > 0 && cur_diff < 0))
那么就需要三个数来计算,但是数组假如就只有两个数呢?
我们在数组的第一个元素之前引入一个相同的数,所以在开始状态pre_diff = 0。那么就需要修改判断峰值条件了
if((pre_diff <= 0 && cur_diff > 0) || (pre_diff >= 0 && cur_diff < 0))
这样就包括了只有两个元素的情况。
还需要注意两个情况:
1、上下坡中有平坡
2、单调坡中有平坡
最大子数组和
这道题的暴力方法就是两个for循环,代码如下
int maxSubArray(int* nums, int numsSize) {
int result = INT_MIN;
for(int i = 0; i < numsSize; i++){
int count = 0;
for(int j = i; j < numsSize; j++){
count += nums[j];
result = result > count ? result : count;
}
}
return result;
}
暴力方法明显时间复杂度是O(n^2)。使用C语言的暴力在leetcode是c不了的,会超时。
那么就需要换一种想法:贪心。
这道题很全面的体现了贪心的贪。这道题的题意是求给定的数组中最大的子数组和。
而贪心贪的点就在于:要想子数组和最大,那么一定要保证子数组和大于0。(当然,若数组中的所有数都小于0,那么这就不成立了,对于这种情况,需要特别分析,后面会分析到)。
先上代码
int maxSubArray(int* nums, int numsSize) {
int count = 0;
int res = INT_MIN;
for(int i = 0; i < numsSize; i++){
count += nums[i];
res = res > count ? res : count;
if(count <= 0){
count = 0;
continue;
}
}
return res;
}
首先要明确的就是这道题目贪心的点,子数组和一定需要大于0,若小于0则会拖累整个子数组的和,需要舍弃。那么解释一下上面说到的,假如全数组都是小于0的呢?那么这样得到的子数组将会是一个元素,这个元素会是该数组中最大的值。比如[-1,-2,-3],最后返回的值会是-1。
有一个误区就是,遇到负数就舍弃,其实不能舍弃,假如遇到负数,但是该子数组和还是正数,那么对后面的元素还是有利的。
拓展:这只是求子数组和的最大值。那么假如要求子数组和最大的起始位置和结束位置呢?(也就是返回最大子数组)。其实只要改动上面的代码即可。这个最大子数组的起始位置就是:每次count小于0,之后的数组下标。结束位置是:result得到最大值的位置。只需要增加两个变量即可。
买股票的最大利润
拿到这题首先想到的是:选择一个最低的价格买入,然后选择一个高的卖出,一直反复……
但是这样的话,好像无法区分出,哪天才是最高的,怎么卖才利润最大。
要是改变一下思路,其实得到的利润可以分解,然后将分解的正利润相加,那么就可以得到最大利润了。那么如何分解呢?
假设第一天买入,第四天卖出。那么利润是prices[3] - prices[0]。(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0]) = prices[3] - prices[0](不管是正是负,都是利润)
下面我们模拟一下
我们分解了每一天得到的利润,那么为什么第一天没有利润呢?因为第一天只能买入,不能卖出。用这个例子分析一下分解的利润相加是否可以得到总的利润。prices[3] - prices[0] = 3。分解之后相加是:-6 + 4 + 5 = 3。
所以不用考虑最小价格买入,最大价格卖出的问题,直接分解为每天的利润,然后加上正利润就是最大的利润了。(可以抽象理解为:你买一次卖一次获得的利润,我可以买多次卖多次,只要保证每次都是赚的,也能获得和你一样的利润。)
代码奉上
int maxProfit(int* prices, int pricesSize) {
int result = 0;
for(int i = 1; i < pricesSize; i++){
if((prices[i] - prices[i - 1]) > 0){
result += prices[i] - prices[i - 1];
}
}
return result;
}
跳跃游戏
这道题也可以不用贪心。
题目要求是从第一个跳到最后一个,那么判断从最后一个到第一个也可以得到答案。
直接先放码
bool canJump(int* nums, int numsSize) {
int flag = numsSize - 1;
for(int i = numsSize - 2; i >= 0; i--){
if(i + nums[i] >= flag){
flag = i;
}
}
return flag == 0;
}
那为什么不能从前面判断呢。试试就逝世。上图,就知道原因了。
从前往后遍历,得到的是最从任意位置跳,最大可以跳到得位置。对于第一个位置开始,是充耳不闻。所以得从后往前遍历。
那么能否可贪?
当然可以。并且效率更高,后面会分析。每一个元素可以选择跳跃的距离,所以到底是选择跳多少步呢?难以判断。换个思路:计算最大可以跳到的位置
上图:
每次求最大的覆盖范围,只要在可以覆盖的地方跳跃,取最大覆盖的范围。这样就把题目转换为求最大覆盖范围是否能覆盖最后一个下标。
上码上码:
bool canJump(int* nums, int numsSize) {
if(numsSize == 1){
return true;
}
int cover = 0;
for(int i = 0; i <= cover; i++){
cover = (nums[i] + i) > cover ? (nums[i] + i) : cover;
if(cover >= numsSize - 1){
return true;
}
}
return false;
}
注意:1、i的结束范围是小于等于cover,cover表示覆盖的范围,也就是能跳跃的最大位置,只能在这些位置里面跳跃。
2、判断ture的条件必须在循环里面,不然会产生数组越界。(heap overflow)
跳跃游戏II
上一题是判断是否能到达最后一个下标。而这一题是求最小到达的跳跃次数。
那么问题来了,每一个可以有很多选择,那么是选择最大的步数跳跃吗?显然不是,例如数组:[2,3,1,1,4]。假如每次选择跳跃最大步数,那么达到最后一个下标需要3步,但是假如先跳一步,然后跳三步,这样就只需要两步了。
模拟:第一个位置可以跳到的最远位置,如果跳不到,那么下一次跳的位置应该是这一次覆盖的位置的最大位置。
#define max(a, b) (((a) > (b)) ? (a) : (b))
int jump(int* nums, int numsSize) {
if(numsSize == 1){
return 0;
}
int cur = 0, next = 0;
int result = 0;
for(int i = 0; i < numsSize; i++){
next = max(nums[i] + i, next);
if(i == cur){
result++;
cur = next;
if(cur >= numsSize - 1){
break;
}
}
}
return result;
}
k次取反后最大化的数组
这道题看起来很简单想到贪心的点,但是还是有一些细节需要注意。
这道题需要进行两次贪心,要想保证数组和最大,首先需要把数组中的负数变为正数。
先上代码
int cmp(const void* e1, const void* e2){
return *(int*)e1 - *(int*)e2;
}
int largestSumAfterKNegations(int* nums, int numsSize, int k) {
qsort(nums, numsSize, sizeof(int), cmp);
for(int i = 0; i < numsSize; i++){
if(nums[i] < 0 && k > 0){
nums[i] = -1 * nums[i];
k--;
}
if(k == 0){
break;
}
}
if(k % 2 == 1){
qsort(nums, numsSize, sizeof(int), cmp);
nums[0] = -1 * nums[0];
}
int result = 0;
for(int i = 0; i < numsSize; i++){
result += nums[i];
}
return result;
}
先将数组从小到大排序,然后从前往后遍历,若遇到小于0的数,这样先遇到的负数也是负得最多的,这样先相反这个数,那可以保证对数组最后的和最有利的。
还有一种情况,当数组中的负数全部逆转,那么数组中全是正数,然后k还有剩,这样先判断k的奇偶性,如果是奇数那么需要把最小的正数转为负数,这样才能保证对最后数组和的减小最小,若是偶数,就不要管了,因为取反偶数次,不会有正负改变。
但是找到最小的正数,需要再次排列数组。还有一种方法只需要排列一次
对绝对值从小到大排列
这样首先从后到前遍历,遇到的负数,一定是最小的负数,对它* -1即可。遍历完之后,k还有剩余,就只需要对第一个元素处理即可
上码
int cmp(const void* e1, const void* e2){
return abs(*(int*)e1) - abs(*(int*)e2);
}
int largestSumAfterKNegations(int* nums, int numsSize, int k) {
qsort(nums, numsSize, sizeof(int), cmp);
for(int i = numsSize - 1; i >= 0; i--){
if(nums[i] < 0 && k > 0){
nums[i] = -1 * nums[i];
k--;
}
}
if(k % 2 == 1){
nums[0] *= -1;
}
int result = 0;
for(int i = 0; i < numsSize; i++){
result += nums[i];
}
return result;
}
加油站
本题贪心的点在于找到油箱剩余的油小于0。那么起点就是下一个站点。
上图说明原因。
上码
int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize) {
int result = 0, total = 0, rest = 0;
for(int i = 0; i < gasSize; i++){
rest += gas[i] - cost[i];
total += gas[i] - cost[i];
if(rest < 0){
result = i + 1;
rest = 0;
}
}
if(total < 0){
return -1;
}
return result;
}
分发糖果
该题要保证每个小孩和左右比较,除两端孩子。定义一个candy数组,保存每个孩子的糖果数。一次遍历无法比较两边孩子。
上代码
#define max(a, b) (((a) > (b)) ? (a) : (b))
int candy(int* ratings, int ratingsSize) {
int* candy = (int*)malloc(sizeof(int) * ratingsSize);
for(int i = 0; i < ratingsSize; i++){
candy[i] = 1;
}
for(int i = 1; i < ratingsSize; i++){
if(ratings[i] > ratings[i - 1]){
candy[i] = candy[i - 1] + 1;
}
}
for(int i = ratingsSize - 2; i >=0 ; i--){
if(ratings[i] > ratings[i + 1]){
candy[i] = max(candy[i], candy[i + 1] + 1);
}
}
int result = 0;
for(int i = 0; i < ratingsSize; i++){
result += candy[i];
}
free(candy);
return result;
}
由于一次遍历无法比较左右孩子,所以进行两次,每个孩子的糖果应该为两次遍历最大的糖果数,这样就可以满足两边的大小。
柠檬水找零
顾客只给三种金额,5,10,20。柠檬水每杯5块,那么遇到5块不需找零,10块找零5块。这两种情况是固定的。遇到20的时候就有两种找零情况,一张10块,一张5块,或者3张5块。这里给一张10块和一张5块是局部最优,贪心的点也在这。
放码过来
bool lemonadeChange(int* bills, int billsSize) {
int five = 0, ten = 0, twenty = 0;
for(int i = 0; i < billsSize; i++){
if(bills[i] == 5){
five++;
}
else if(bills[i] == 10){
if(five > 0){
ten++;
five--;
}
else{
return false;
}
}
else{
if(ten > 0 && five > 0){
ten--;
five--;
}
else if(five >= 3){
five -= 3;
}
else{
return false;
}
}
}
return true;
}
根据身高重建队列
这题贪心的点并不好找,题意大体是,给定一个二维数组,一维数组是两个整数,前一个表示身高,后一个表示这个人前面有几个比它高或等于和它身高的人数。每个一维的整数是h,k。
思路是:先将这个给定的二维数组排序,根据身高从高到矮排,若身高相等,则根据前面的人数少到多排。然后根据k取插入排序。
原因:1、为什么要身高从高到矮排列?因为排列完之后是根据k的值来插入,矮的后插并不会影响前面高个子的k。2、当身高相同时,为什么要根据k的值从小到大排。插入时,k值小的先插,大的后插,这样k值大的一定插在k值小的后面,符合题意。
class Solution {
public:
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);
vector<vector<int>> que;
for (int i = 0; i < people.size(); i++) {
int position = people[i][1];
que.insert(que.begin() + position, people[i]);
}
return que;
}
};
用最少数量的箭射爆气球
题意分析:给你一个二维数组,这个数组的每一个一维表示一个气球的区间,(这个气球的直径等于这个区间的差)。假如有两个气球是有一部分重合的,那么一根箭就能射爆两个气球。题目要求最少的箭射爆全部的气球。
思路:这题目就是要求重合的区间。上图可能会更好理解。
上码
class Solution {
protected:
static bool cmp(const vector<int>& a, const vector<int>& b){
return a[0] < b[0]; //注意这里只能比较,不能相减,否则值溢出。
}
public:
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(), points.end(), cmp);
int res = 1;
for(int i = 1; i < points.size(); i++){
if(points[i][0] > points[i - 1][1]){
res++;
}
else{
points[i][1] = min(points[i][1], points[i - 1][1]);
}
}
return res;
}
};
res表示的箭的数量,这里起始值是1,表示至少需要一根箭。
下一个区间的左端点和上一个区间的右端点比较,假如小于或等于,表示这两个区间有重合的部分,然后更新这个区间的右端点,要更新为和上一个区间的右端点的最小值。
无重叠区间
这一题和上一题思路差不多。其实题意就是求重叠区间的个数。
思路:先排序,那么这里有两个选择,是按照左边界排列,还是右边界呢?其实这两种都可以,这里我是排列左边界。排列左边界时,假如两个区间的左边界相同,那么该怎么排呢,其实根本不用管右端点,我们直接在循环里面处理它就行了。而处理的过程就涉及到了一个贪心的点,怎样才能删除最少的区间呢?这里当我们判断到一个区间的左端点小于上一个区间的右端点,那么肯定这两个区间是重叠的,我们这里需要把当前区间的右端点更新为两个区间右端点的最小值,这样我们这个区间距离下一个区间就会跟远,自然删除的区间也就最小。而这样处理也就完美包含了当两个区间左端点相同时,右端点哪个大?该怎么排?
还有一种情况,画图说明吧。更好理解。
上码
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b){
return a[0] < b[0];
}
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
int res = 0;
for(int i = 1; i < intervals.size(); i++){
if(intervals[i][0] < intervals[i - 1][1]){
res++;
intervals[i][1] = min(intervals[i][1], intervals[i - 1][1]);
}
}
return res;
}
};
划分内容字母区间
题意:给定一个字符串,要求分成尽可能多的子串,每个子串的字母不在其他子串中出现。
思路:要尽可能多的子串,那么就从字符串第一个字符开始,找到这个字符最远出现的位置,这个位置到第一个字符就是一个子串。那么问题是如何找这个字符最远出现的位置呢?这里就可以使用哈希表,哈希表的下标是每个字母,存储的元素是该字母的最远位置。然后遍历整个字符串。
上代码:
int max(int a, int b){
return a > b ? a : b;
}
int* partitionLabels(char* s, int* returnSize) {
int hash_table[27] = {0};
for(int i = 0; i < strlen(s); i++){
hash_table[s[i] - 'a'] = i;
}
int left = 0, right = 0;
int* ret = malloc(sizeof(int) * strlen(s));
*returnSize = 0;
for(int i = 0; i < strlen(s); i++){
right = max(right, hash_table[s[i] - 'a']);
if(i == right){
ret[(*returnSize)++] = right - left + 1;
left = i + 1;
}
}
return ret;
}