代码随想录——贪心

分发饼干

455. 分发饼干

思路:贪心+双指针+排序
  1. 排序:对s和g都进行升序排列
  2. 双指针遍历
    • 使用两个指针 ij 分别遍历孩子数组 g 和饼干数组 s
    • 如果当前饼干 s[j] 可以满足当前孩子 g[i] 的胃口(即 s[j] >= g[i]),则满足一个孩子,ans 加 1,并移动到下一个孩子(i++)。
    • 无论当前饼干是否满足当前孩子,都要移动到下一个饼干(j++)。
代码
#include <stdlib.h>

// 比较函数,用于排序
int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

int findContentChildren(int* g, int gSize, int* s, int sSize) {
    // 对孩子的胃口和饼干的大小进行排序
    qsort(g, gSize, sizeof(int), cmp);
    qsort(s, sSize, sizeof(int), cmp);

    int i = 0, j = 0;
    int ans = 0;

    // 使用双指针遍历孩子和饼干数组
    while (i < gSize && j < sSize) {
        // 如果当前饼干可以满足当前孩子的胃口
        if (s[j] >= g[i]) {
            ans++;  // 满足一个孩子
            i++;    // 移动到下一个孩子
        }
        j++;  // 无论是否满足,都移动到下一个饼干
    }

    return ans;
}

摆动序列

376. 摆动序列

思路:贪心

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如图,我们可以将所有序列的关系化为这样的摆动图,取所有的峰值和低谷就可以得到摆动序列,计算峰值与低谷的数量即可。

代码
int wiggleMaxLength(int* nums, int numsSize) {
    if (numsSize < 2) {
        return numsSize; // 如果数组长度小于2,直接返回长度
    }

    int prevDiff = nums[1] - nums[0]; // 前一个差值
    int ans;
    if(prevDiff==0)ans=1; // 初始长度,根据前两个元素是否相等决定
    else ans=2;
    for (int i = 2; i < numsSize; i++) {
        int currDiff = nums[i] - nums[i - 1]; // 当前差值
        if ((prevDiff <= 0 && currDiff > 0) || (prevDiff >= 0 && currDiff < 0)) {
            ans++; // 如果差值符号发生变化,摆动序列长度加1
            prevDiff = currDiff; // 更新前一个差值
        }
    }

    return ans;
}

最大子数组和

53. 最大子数组和

思路一:贪心

遍历数组

  • 如果当前子数组的和 sum 大于0,说明当前子数组对后续的子数组和有贡献,因此继续累加当前元素。
  • 如果当前子数组的和 sum 小于或等于0,说明当前子数组对后续的子数组和没有贡献,因此重新开始计算子数组的和,从当前元素开始。
  • 在每一步更新最大子数组和 ans,确保 ans 始终保存当前找到的最大子数组和。
int maxSubArray(int* nums, int numsSize) {
    // 初始化结果变量为数组的第一个元素
    int ans = nums[0];
    // 初始化当前子数组的和为数组的第一个元素
    int sum = nums[0];
    
    // 从数组的第二个元素开始遍历
    for (int i = 1; i < numsSize; i++) {
        // 如果当前子数组的和大于0,继续累加当前元素
        if (sum > 0) {
            sum += nums[i];
        } 
        // 否则,重新开始计算子数组的和,从当前元素开始
        else {
            sum = nums[i];
        }
        // 更新最大子数组和
        ans = fmax(sum, ans);
    }
    
    // 返回最大子数组和
    return ans;
}
思路二:动态规划
  1. 动态规划:使用动态规划的思想,通过定义状态和状态转移方程来解决问题。
    • 状态定义dp[i] 表示以 nums[i] 结尾的最大子数组和。
    • 状态转移方程dp[i] = max(dp[i-1] + nums[i], nums[i])
      • 如果 dp[i-1] 是正数,那么加上 nums[i] 会使得子数组和更大。
      • 如果 dp[i-1] 是负数,那么从 nums[i] 重新开始计算子数组和会更优。
  2. 初始化
    • dp[0] 初始化为 nums[0],因为以第一个元素结尾的最大子数组和就是它本身。
    • ans 初始化为 dp[0],用于记录全局最大子数组和。
int maxSubArray(int* nums, int numsSize) {
    // 动态规划数组,dp[i] 表示以 nums[i] 结尾的最大子数组和
    int dp[numsSize];
    // 初始化 dp 数组为 0
    memset(dp, 0, sizeof(dp));
    // 初始化 dp[0] 为数组的第一个元素
    dp[0] = nums[0];
    // 初始化结果为 dp[0]
    int ans = dp[0];

    // 从数组的第二个元素开始遍历
    for (int i = 1; i < numsSize; i++) {
        // 状态转移方程:dp[i] 要么是前一个子数组和加上当前元素,要么是当前元素本身
        dp[i] = fmax(dp[i - 1] + nums[i], nums[i]);
        // 更新全局最大子数组和
        ans = fmax(ans, dp[i]);
    }

    // 返回全局最大子数组和
    return ans;
}

买卖股票的最佳时机Ⅱ

122. 买卖股票的最佳时机 II

思路:贪心
  1. 贪心策略:通过捕捉所有正收益的交易来最大化利润。
    • 只要今天的价格比前一天高,就认为可以获利,并将差价累加到总利润中。
  2. 初始化
    • 将最大利润 ans 初始化为 0。
  3. 遍历价格数组
    • 从第二天开始遍历,计算当前价格与前一天的差价 tmp
    • 如果差价 tmp 大于 0,说明可以获利,将其累加到 ans 中。
代码
int maxProfit(int* prices, int pricesSize) {
    // 初始化最大利润为 0
    int ans = 0;

    // 从第二天开始遍历
    for (int i = 1; i < pricesSize; i++) {
        // 计算当前价格与前一天的差价
        int tmp = prices[i] - prices[i - 1];
        // 如果差价大于 0,说明可以获利,累加到结果中
        if (tmp > 0) {
            ans += tmp;
        }
    }

    // 返回最大利润
    return ans;
}

跳跃游戏

55. 跳跃游戏

思路:贪心
  1. 贪心算法:贪心策略,通过维护一个变量 sum 来记录当前能到达的最远位置。
    • 在遍历数组时,不断更新 sum,确保它始终表示从当前位置能跳到的最远位置。
  2. 初始化
    • sum 初始化为 0,表示初始位置。
  3. 遍历数组
    • 对于每个位置 i,检查是否能从之前的位置跳到当前位置(即 sum >= i)。
    • 如果能跳到当前位置,则更新 summax(sum, i + nums[i]),表示从当前位置能跳到的最远位置。
    • 如果 sum 已经超过或等于最后一个位置(numsSize - 1),则返回 true
代码
bool canJump(int* nums, int numsSize) {
    int sum = 0;
    for (int i = 0; i < numsSize; i++) {
        // 如果当前位置无法到达,直接返回 false
        if (sum < i) {
            return false;
        }
        // 更新能到达的最远位置
        sum = fmax(sum, i + nums[i]);
        // 如果最远位置已经超过或等于最后一个位置,返回 true
        if (sum >= numsSize - 1) {
            return true;
        }
    }
    return false;
}

跳跃游戏Ⅱ

45. 跳跃游戏 II

思路一:贪心
  • 使用贪心策略,每次在当前跳跃范围内选择能跳得最远的位置。
  • 维护两个变量:
    • sum:当前能够到达的最远位置。
    • cur:在当前跳跃范围内,能够到达的最远位置。
  • 当遍历到 sum 时,表示当前跳跃范围已经用尽,需要进行一次新的跳跃,并更新 sumcur
int jump(int* nums, int numsSize) {
    if (numsSize <= 1) return 0; // 如果数组长度为 0 或 1,不需要跳跃

    int sum = 0; // 当前能够到达的最远位置
    int cnt = 0; // 跳跃次数
    int cur = 0; // 在当前跳跃范围内,能够到达的最远位置

    for (int i = 0; i < numsSize - 1; i++) {
        if (sum >= numsSize - 1) return cnt; // 如果已经能够到达终点,直接返回跳跃次数

        // 更新当前能够到达的最远位置
        if (cur < nums[i] + i) {
            cur = nums[i] + i;
        }

        // 当遍历到 sum 时,表示当前跳跃范围已经用尽,需要进行一次新的跳跃
        if (sum == i) {
            sum = cur; // 更新 sum 为当前能够到达的最远位置
            cnt++;     // 增加跳跃次数
        }
    }

    return cnt; // 返回跳跃次数
}
思路二:动态规划
  1. 定义状态

    • dp[i] 表示从起点(位置 0)跳到位置 i 所需的最小跳跃次数。
  2. 状态转移方程

    • 对于每个位置 i,遍历所有可以跳到 i 的位置 j(即 j + nums[j] >= i),然后更新 dp[i]dp[j] + 1 的最小值。

    • 状态转移方程可以表示为:

      dp[i]=min⁡(dp[i],dp[j]+1)其中j+nums[j]≥i

  3. 初始化

    • dp[0] = 0,因为起点不需要跳跃。
    • 其他位置的 dp[i] 初始化为一个较大的值(如 INT_MAX),表示暂时无法到达。
  4. 最终结果

    • dp[numsSize - 1] 即为从起点跳到终点的最小跳跃次数。
#include <limits.h>

int jump(int* nums, int numsSize) {
    if (numsSize <= 1) return 0; // 如果数组长度为 0 或 1,不需要跳跃

    int dp[numsSize]; // 定义 dp 数组
    for (int i = 0; i < numsSize; i++) {
        dp[i] = INT_MAX; // 初始化 dp 数组为最大值
    }
    dp[0] = 0; // 起点不需要跳跃

    // 动态规划求解
    for (int i = 1; i < numsSize; i++) {
        for (int j = 0; j < i; j++) {
            if (j + nums[j] >= i) { // 如果可以从 j 跳到 i
                dp[i] = fmin( dp[i] ,dp[j] + 1); // 更新 dp[i]
            }
        }
    }

    return dp[numsSize - 1]; // 返回到达终点的最小跳跃次数
}

K次取反后最大化的数组和

1005. K 次取反后最大化的数组和

思路:贪心
  1. 核心思想
    • 为了使数组的和最大,应该优先对最小的负数取反(因为负数取反后会变成正数,增加总和)。
    • 如果所有负数都取反后,还有剩余的取反次数,应该对最小的正数反复取反(因为最小的正数取反后对总和的影响最小)。
  2. 步骤
    • 将数组排序,方便优先处理最小的负数。
    • 遍历数组,对负数取反,直到用完 k 次取反操作或没有负数为止。
    • 如果还有剩余的取反次数,对最小的正数反复取反。
    • 计算最终的总和。

给出两种代码,一种是if-else但是速度更快,击败100%,一种是优化后的代码,便于阅读。

代码一:
#include <stdlib.h> // 用于 qsort 函数
#include <math.h>   // 用于 fabs 和 fmin 函数

// 比较函数,用于排序
int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b; // 升序排序
}

int largestSumAfterKNegations(int* nums, int numsSize, int k) {
    // 对数组进行升序排序,方便优先处理最小的负数
    qsort(nums, numsSize, sizeof(int), cmp);

    int sum = 0; // 用于存储最终的和

    // 如果数组只有一个元素
    if (numsSize == 1) {
        if (k & 1) { // 如果 k 是奇数,取反一次
            return 0 - nums[0];
        }
        return nums[0]; // 如果 k 是偶数,直接返回原值
    }

    // 遍历数组
    for (int i = 0; i < numsSize; i++) {
        if (nums[i] < 0) { // 如果当前元素是负数
            if (k > 0) {   // 如果还有取反次数
                sum -= nums[i]; // 取反并累加到 sum
                k--;            // 减少取反次数
            } else {
                sum += nums[i]; // 如果没有取反次数,直接累加
            }
        } else { // 如果当前元素是非负数
            if (k % 2 == 0) { // 如果剩余的取反次数是偶数
                sum += nums[i]; // 直接累加
            } else { // 如果剩余的取反次数是奇数
                if (i == 0) { // 如果是第一个元素
                    sum -= nums[i]; // 取反并累加
                    k = 0;          // 取反次数用完
                } else if (nums[i] + nums[i - 1] > 0) { // 如果当前元素和前一个元素的和为正
                    sum += 2 * nums[i - 1]; // 将前一个元素取反两次(相当于恢复原值)
                    sum += nums[i];         // 累加当前元素
                    k = 0;                  // 取反次数用完
                } else { // 如果当前元素和前一个元素的和为负
                    sum -= nums[i]; // 取反并累加
                    k = 0;          // 取反次数用完
                }
            }
        }
    }

    // 如果遍历完数组后,还有剩余的取反次数(k 是奇数)
    if (k & 1) {
        int Minabs = fabs(nums[0]); // 初始化最小绝对值为第一个元素的绝对值
        for (int i = 1; i < numsSize; i++) {
            Minabs = fmin(fabs(nums[i]), Minabs); // 找到数组中的最小绝对值
        }
        sum -= 2 * Minabs; // 减去两倍的最小绝对值(相当于取反)
    }

    return sum; // 返回最终的和
}
代码二:(优化)
#include <stdlib.h>
#include <math.h>

// 比较函数,用于排序
int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

int largestSumAfterKNegations(int* nums, int numsSize, int k) {
    // 将数组排序,方便优先处理最小的负数
    qsort(nums, numsSize, sizeof(int), cmp);

    int sum = 0;
    int minAbs = INT_MAX; // 记录最小绝对值

    // 遍历数组,优先对负数取反
    for (int i = 0; i < numsSize; i++) {
        if (nums[i] < 0 && k > 0) {
            nums[i] = -nums[i]; // 取反
            k--;
        }
        sum += nums[i]; // 累加和
        minAbs = fmin(minAbs, nums[i]); // 更新最小绝对值
    }

    // 如果还有剩余的取反次数,对最小绝对值取反
    if (k % 2 == 1) {
        sum -= 2 * minAbs; // 减去两倍的最小绝对值(相当于取反)
    }

    return sum;
}

加油站

134. 加油站

思路:贪心
  1. 问题理解
  • 每个加油站的油量差为 gas[i] - cost[i],表示从第 i 个加油站出发,到达下一个加油站后剩余的油量。
  • 如果总油量差(所有加油站的油量差之和)小于 0,说明无论如何都无法完成一圈,直接返回 -1
  • 如果总油量差大于等于 0,说明存在一个起点,可以从该起点出发完成一圈。
  1. 贪心算法
  • 从第一个加油站开始,依次计算每个加油站的油量差,并累加到 cursum 中。
  • 如果在某个加油站,cursum 达到最小值,说明从该加油站出发,油量最“紧张”。因此,下一个加油站(即 ans + 1)可能是最佳起点。
  • 遍历完成后,检查 cursum 是否小于 0。如果是,则无法完成一圈,返回 -1;否则返回最佳起点索引。
  1. 关键点
  • 最小油量差的位置minsum 的位置决定了最佳起点。因为从 minsum 的下一个位置出发,油量差会逐渐增加,更容易完成一圈。
  • 总油量差的判断:如果总油量差小于 0,说明无论如何都无法完成一圈。
代码
int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize) {
    // 初始化当前油量差和最小油量差
    int cursum = gas[0] - cost[0];
    int minsum = gas[0] - cost[0];
    // 初始化起点索引
    int ans = 0;

    // 遍历所有加油站
    for (int i = 1; i < gasSize; i++) {
        // 更新当前油量差
        cursum = cursum + gas[i] - cost[i];
        // 如果当前油量差小于最小油量差,更新最小油量差和起点索引
        if (cursum < minsum) {
            ans = i;
            minsum = cursum;
        }
    }

    // 如果总油量差小于0,说明无法完成一圈
    if (cursum < 0) return -1;

    // 返回起点索引,因为起点索引是minsum对应的下一个位置
    return (ans + 1) % gasSize;
}

分发糖果

135. 分发糖果

思路:贪心
  1. 第一次遍历(从左到右)
    • 确保每个孩子比左边评分低的孩子多一个糖果。
    • 这样保证了从左到右的规则。
  2. 第二次遍历(从右到左)
    • 确保每个孩子比右边评分低的孩子多一个糖果。
    • 同时,如果当前孩子的糖果数已经比右边孩子的糖果数加1多,则不需要更新。
    • 这样保证了从右到左的规则。
  3. 最终结果
    • 将每个孩子的糖果数相加,得到总的糖果数。
代码
#include <stdio.h>
#include <stdlib.h>

int candy(int* ratings, int ratingsSize) {
    // 创建一个数组 nums,用于存储每个孩子分到的糖果数
    int nums[ratingsSize];
    
    // 初始化每个孩子至少分到一个糖果
    for (int i = 0; i < ratingsSize; i++) {
        nums[i] = 1;
    }

    // 第一次遍历:从左到右
    // 如果当前孩子的评分比左边孩子高,则当前孩子的糖果数比左边孩子多一个
    for (int i = 1; i < ratingsSize; i++) {
        if (ratings[i] > ratings[i - 1]) {
            nums[i] = nums[i - 1] + 1;
        }
    }

    // 第二次遍历:从右到左
    // 如果当前孩子的评分比右边孩子高,则当前孩子的糖果数比右边孩子多一个
    // 同时,如果当前孩子的糖果数已经比右边孩子的糖果数加1多,则不需要更新
    for (int i = ratingsSize - 2; i >= 0; i--) {
        if (ratings[i] > ratings[i + 1]) {
            if (nums[i] < nums[i + 1] + 1) {
                nums[i] = nums[i + 1] + 1;
            }
        }
    }

    // 计算总的糖果数
    int ans = 0;
    for (int i = 0; i < ratingsSize; i++) {
        ans += nums[i];
    }

    return ans;
}

柠檬水找零

860. 柠檬水找零

思路:贪心+模拟
  • 使用两个变量 fivenumstennums 分别记录当前拥有的 5和5和10 的数量。
  • 遍历顾客支付的钞票:
    • 如果是 $5,直接收下,fivenums++
    • 如果是 10,需要找零10,需要找零5,fivenums--,同时 tennums++
    • 如果是 20,优先找零20,优先找零10 + 5(因为5(因为10 更有用),如果没有 10,则找零10,则找零5 + 5+5+5。
  • 如果无法找零,返回 false;否则遍历结束后返回 true
代码
bool lemonadeChange(int* bills, int billsSize) {
    int fivenums = 0; // 记录当前拥有的 $5 的数量
    int tennums = 0;  // 记录当前拥有的 $10 的数量

    // 遍历每个顾客支付的钞票
    for (int i = 0; i < billsSize; i++) {
        if (bills[i] == 5) {
            // 如果是 $5,直接收下,不需要找零
            fivenums++;
        } else if (bills[i] == 10) {
            // 如果是 $10,需要找零 $5
            if (fivenums > 0) {
                fivenums--; // 给顾客 $5
                tennums++;  // 收下 $10
            } else {
                // 如果没有 $5,无法找零,返回 false
                return false;
            }
        } else {
            // 如果是 $20,优先找零 $10 + $5
            if (tennums > 0 && fivenums > 0) {
                tennums--; // 给顾客 $10
                fivenums--; // 给顾客 $5
            } else if (fivenums >= 3) {
                // 如果没有 $10,尝试找零 $5 + $5 + $5
                fivenums -= 3; // 给顾客 3 张 $5
            } else {
                // 如果无法找零,返回 false
                return false;
            }
        }
    }

    // 如果所有顾客都能成功找零,返回 true
    return true;
}

根据身高重建队列

406. 根据身高重建队列

思路:排序+插入
  1. 排序:首先按照身高 h 从高到低排序,如果身高相同,则按照 k 从小到大排序。这样做的目的是为了确保在插入时,高个子的人先被处理,这样后续插入的矮个子不会影响已经插入的高个子的 k 值。
  2. 插入:遍历排序后的数组,按照每个人的 k 值将其插入到结果队列的相应位置。由于高个子已经排好序,插入矮个子时不会影响高个子的 k 值。
代码一:身高降序,k值升序
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

// 定义结构体表示每个人的身高和 k 值
typedef struct {
    int h;
    int k;
} person;

// 比较函数,用于排序
int cmp(const void* a, const void* b) {
    person *p1 = (person*)a;
    person *p2 = (person*)b;
    // 按身高从高到低排序,如果身高相同,则按 k 从小到大排序
    if (p1->h == p2->h) {
        return p1->k - p2->k;
    }
    return p2->h - p1->h;
}

int** reconstructQueue(int** people, int peopleSize, int* peopleColSize, int* returnSize, int** returnColumnSizes) {
    // 设置返回数组的大小
    *returnSize = peopleSize;
    // 分配结果数组的内存
    int **res = malloc(sizeof(int*) * peopleSize);
    // 分配列大小数组的内存
    *returnColumnSizes = malloc(sizeof(int) * peopleSize);
    // 将列大小数组的值全部设置为 2
    for (int i = 0; i < peopleSize; i++) {
        (*returnColumnSizes)[i] = 2;
    }

    // 将输入转换为 person 结构体数组
    person p[peopleSize];
    for (int i = 0; i < peopleSize; i++) {
        p[i].h = people[i][0];
        p[i].k = people[i][1];
    }

    // 对 person 数组进行排序
    qsort(p, peopleSize, sizeof(person), cmp);

    // 插入排序
    for (int i = 0; i < peopleSize; i++) {
        int pos = p[i].k; // 需要插入的位置
        // 将 pos 之后的元素向后移动
        for (int j = i; j > pos; j--) {
            res[j] = res[j - 1];
        }
        // 插入当前元素
        res[pos] = malloc(sizeof(int) * 2);
        res[pos][0] = p[i].h;
        res[pos][1] = p[i].k;
    }

    return res;
}

还有一种思路是将身高由小到大排序。

每次处理当前队列中身高最小的,再根据k值,在其前面留下k个空位,这样可以使后面处理身高比其大的人时,满足刚好有k个大于等于他的人在前面。

代码二:身高升序,k值升序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 比较函数,用于排序
int cmp(const void* _a, const void* _b) {
    int *a = *(int**)_a, *b = *(int**)_b;
    // 如果身高相同,则按 k 值从大到小排序;否则按身高从小到大排序
    return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0];
}

int** reconstructQueue(int** people, int peopleSize, int* peopleColSize, int* returnSize, int** returnColumnSizes) {
    // 对 people 数组进行排序
    qsort(people, peopleSize, sizeof(int*), cmp);

    // 分配结果数组的内存
    int** ans = malloc(sizeof(int*) * peopleSize);
    // 设置返回数组的大小
    *returnSize = peopleSize;
    // 分配列大小数组的内存
    *returnColumnSizes = malloc(sizeof(int) * peopleSize);
    // 初始化列大小数组为 0
    memset(*returnColumnSizes, 0, sizeof(int) * peopleSize);

    // 遍历排序后的 people 数组
    for (int i = 0; i < peopleSize; ++i) {
        // 计算当前元素需要插入的位置
        int spaces = people[i][1] + 1;
        // 在结果数组中找到合适的位置
        for (int j = 0; j < peopleSize; ++j) {
            // 如果当前位置未被占用
            if ((*returnColumnSizes)[j] == 0) {
                spaces--;
                // 如果 spaces 减到 0,说明找到了插入位置
                if (!spaces) {
                    // 标记当前位置已被占用
                    (*returnColumnSizes)[j] = 2;
                    // 分配内存并插入当前元素
                    ans[j] = malloc(sizeof(int) * 2);
                    ans[j][0] = people[i][0], ans[j][1] = people[i][1];
                    break;
                }
            }
        }
    }

    // 返回结果数组
    return ans;
}

用最少数量的箭引爆气球

452. 用最少数量的箭引爆气球

思路:排序+贪心

贪心策略

  • 按气球的结束位置(或者起始位置)排序。
  • 从第一个气球开始,用一支箭射爆所有与当前气球区间重叠的气球。
  • 如果遇到一个气球的起始位置 > 当前箭的射击区间的结束位置,说明需要新增一支箭
代码一:按结束位置排序
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>

// 比较函数,用于按气球区间的结束位置由小到大排序
int cmp(const void* a, const void* b) {
    int* interval1 = *(int**)a;
    int* interval2 = *(int**)b;
    if(interval1[1] <interval2[1])return -1;
    else return 1;
}

int findMinArrowShots(int** points, int pointsSize, int* pointsColSize) {
    if (pointsSize == 0) return 0;

    // 按气球区间的结束位置排序
    qsort(points, pointsSize, sizeof(int*), cmp);

    // 初始化箭的数量
    int cnt = 1;
    // 当前箭的射击区间
    int end = points[0][1];

    // 遍历所有气球区间
    for (int i = 1; i < pointsSize; i++) {
        // 如果当前气球的起始位置 > 当前箭的射击区间的结束位置
        if (points[i][0] > end) {
            // 需要新增一支箭
            cnt++;
            // 更新当前箭的射击区间
            end = points[i][1];
        }
    }

    return cnt;
}
代码二:按结束位置排序
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>

// 比较函数,用于按气球区间的起始位置由小到大排序
int cmp(const void* a, const void* b) {
    int* interval1 = *(int**)a;
    int* interval2 = *(int**)b;
    if(interval1[0] <interval2[0])return -1;
    else return 1;
}

int findMinArrowShots(int** points, int pointsSize, int* pointsColSize) {
    if (pointsSize == 0) return 0;

    // 按气球区间的结束位置排序
    qsort(points, pointsSize, sizeof(int*), cmp);

    // 初始化箭的数量
    int cnt = 1;
    // 当前箭的射击区间
    int end = points[0][1];

    // 遍历所有气球区间
    for (int i = 1; i < pointsSize; i++) {
        // 如果当前气球的起始位置 > 当前箭的射击区间的结束位置
        if (points[i][0] > end) {
            // 需要新增一支箭
            cnt++;
            // 更新当前箭的射击区间
            end = points[i][1];
        }
        else{
            end=fmin(end,points[i][1]);//更新(缩小)射击区间
        }
    }

    return cnt;
}

无重叠区间

435. 无重叠区间

思路:贪心

刚读完题感觉和上一题(452. 用最少数量的箭引爆气球)很像,在上一题的代码基础上稍作修改就通过了。

  1. 问题描述
    • 给定一组区间,找到需要移除的最小区间数量,使得剩余的区间互不重叠。
  2. 贪心策略
    • 按区间的起始位置排序。
    • 遍历区间,如果当前区间与前一个区间重叠,则移除结束位置较大的区间(保留结束位置较小的区间,以减少与后续区间重叠的可能性)。
  3. 关键点
    • 按起始位置排序后,遍历时只需比较当前区间与前一个区间的结束位置。
    • 如果当前区间与前一个区间不重叠,则保留当前区间。
    • 如果重叠,则移除结束位置较大的区间,并更新结束位置为较小的值。
代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>

// 比较函数,用于按区间的起始位置从小到大排序
int cmp(const void* a, const void* b) {
    int* interval1 = *(int**)a;
    int* interval2 = *(int**)b;
    // 按起始位置从小到大排序
    if (interval1[0] < interval2[0]) return -1;
    else return 1;
}

int eraseOverlapIntervals(int** intervals, int intervalsSize, int* intervalsColSize) {
    // 如果没有区间,直接返回 0
    if (intervalsSize == 0) return 0;

    // 按区间的起始位置排序
    qsort(intervals, intervalsSize, sizeof(int*), cmp);

    // 初始化计数器,记录保留的区间数量
    int cnt = 1;
    // 当前保留区间的结束位置
    int end = intervals[0][1];

    // 遍历所有区间
    for (int i = 1; i < intervalsSize; i++) {
        // 如果当前区间的起始位置 >= 当前保留区间的结束位置
        if (intervals[i][0] >= end) {
            // 保留当前区间
            cnt++;
            // 更新当前保留区间的结束位置
            end = intervals[i][1];
        } else {
            // 如果重叠,保留结束位置较小的区间
            end = fmin(end, intervals[i][1]);
        }
    }

    // 需要移除的区间数量 = 总区间数量 - 保留的区间数量
    return intervalsSize - cnt;
}

划分字母区间

763. 划分字母区间

思路:贪心
  1. 问题描述
    • 给定一个字符串 s,要求将字符串划分为若干片段,使得每个字母最多出现在一个片段中。
    • 返回每个片段的长度。
  2. 关键点
    • 每个字母的最后出现位置决定了片段的边界。
    • 遍历字符串时,动态更新当前片段的结束位置。
  3. 算法步骤
    • 首先记录每个字母的最后出现位置。
    • 遍历字符串,更新当前片段的结束位置。
    • 当遍历到当前片段的结束位置时,记录片段长度,并开始下一个片段。
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int* partitionLabels(char* s, int* returnSize) {
    // 分配结果数组的内存
    int* ans = malloc(sizeof(int) * 500);
    // 初始化返回的片段数量为 0
    *returnSize = 0;

    // 字符串的长度
    int n = strlen(s);
    // 记录每个字母的最后出现位置
    int last[26] = {0};

    // 遍历字符串,记录每个字母的最后出现位置
    for (int i = 0; i < n; i++) {
        last[s[i] - 'a'] = i;
    }

    // 初始化当前片段的起始位置和结束位置
    int start = 0, end = 0;
    // 遍历字符串
    for (int i = 0; i < n; i++) {
        // 更新当前片段的结束位置
        end = fmax(end, last[s[i] - 'a']);
        // 如果遍历到当前片段的结束位置
        if (i == end) {
            // 记录当前片段的长度
            ans[(*returnSize)++] = end - start + 1;
            // 更新下一个片段的起始位置
            start = end + 1;
        }
    }

    // 返回结果数组
    return ans;
}

合并区间

56. 合并区间

思路:贪心
  • 按区间的起始位置排序。
  • 遍历区间,如果当前区间与前一个区间重叠,则合并它们;否则,将前一个区间加入结果列表。

个人感觉比较简单~~

代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>

// 比较函数,用于按区间的起始位置从小到大排序
int cmp(const void* a, const void* b) {
    int* intervala = *(int**)a;
    int* intervalb = *(int**)b;
    // 按起始位置从小到大排序
    if (intervala[0] - intervalb[0] < 0) return -1;
    return 1;
}

int** merge(int** intervals, int intervalsSize, int* intervalsColSize, int* returnSize, int** returnColumnSizes) {
    // 按区间的起始位置排序
    qsort(intervals, intervalsSize, sizeof(int*), cmp);

    // 分配结果数组的内存
    int** res = malloc(sizeof(int*) * intervalsSize);
    // 初始化返回的区间数量为 0
    *returnSize = 0;

    // 初始化当前区间的起始和结束位置
    int start = intervals[0][0];
    int end = intervals[0][1];

    // 遍历所有区间
    for (int i = 1; i < intervalsSize; i++) {
        // 如果当前区间与前一个区间重叠
        if (intervals[i][0] <= end) {
            // 合并区间,更新结束位置
            end = fmax(end, intervals[i][1]);
        } else {
            // 如果不重叠,将前一个区间加入结果列表
            res[*returnSize] = malloc(sizeof(int) * 2);
            res[*returnSize][0] = start;
            res[*returnSize][1] = end;
            (*returnSize)++;
            // 更新当前区间的起始和结束位置
            start = intervals[i][0];
            end = intervals[i][1];
        }
    }

    // 将最后一个区间加入结果列表
    res[*returnSize] = malloc(sizeof(int) * 2);
    res[*returnSize][0] = start;
    res[*returnSize][1] = end;
    (*returnSize)++;

    // 分配列大小数组的内存
    *returnColumnSizes = malloc(sizeof(int) * (*returnSize));
    // 设置每个区间的列大小为 2
    for (int i = 0; i < *returnSize; i++) {
        (*returnColumnSizes)[i] = 2;
    }

    // 返回结果数组
    return res;
}

单调递增的数字

738. 单调递增的数字

思路:贪心
  1. 问题描述
    • 给定一个整数 n,找到小于或等于 n 的最大单调递增数字。
    • 单调递增数字是指从左到右每一位数字都不大于下一位数字。
  2. 关键点
    • 从右到左遍历数字,找到第一个不满足单调递增的位置。
    • 将该位置减 1,并将其后的所有位置设置为 9。
  3. 算法步骤
    • 将数字转换为数组形式,方便逐位操作。
    • 从右到左遍历数组,找到第一个不满足单调递增的位置。
    • 将该位置减 1,并将其后的所有位置设置为 9。
    • 将数组转换回整数形式。

注:

  • 如果遇到332这样的需要将第一个3减一,后面的数都变为9。因此当遇到不满足单调递增的位置时,需要回去找到第一个等于该数的位置(相当于找到332中第一个3的位置),将其减一,后面置为9.

  • 下面将数字转换为数组的函数都是倒序的,例如12345对应的数组为54321

代码
#include <stdio.h>
#include <stdlib.h>

// 将整数转换为数组形式
void int2vec(int x, int* res, int* returnSize) {
    int cnt = 0;
    // 从低位到高位依次提取数字
    while (x) {
        res[cnt++] = x % 10;
        x /= 10;
    }
    // 返回数组的长度
    *returnSize = cnt;
}

// 将数组形式的数字转换为整数
int vec2int(int* num, int numSize) {
    int ans = 0;
    // 从高位到低位依次计算整数
    for (int i = numSize - 1; i >= 0; i--) {
        ans = ans * 10 + num[i];
    }
    return ans;
}

int monotoneIncreasingDigits(int n) {
    // 如果 n 是一位数,直接返回
    if (n < 10) return n;

    // 分配数组内存,用于存储数字的每一位
    int* num = malloc(sizeof(int) * 10);
    int len = 0;
    // 将 n 转换为数组形式
    int2vec(n, num, &len);

    // 从右到左遍历数组
    for (int i = len - 1; i >= 1; i--) {
        // 如果当前位大于前一位
        if (num[i] > num[i - 1]) {
            // 找到连续相等的高位
            int j = i + 1;
            while (j < len && num[j] == num[i]) j++;
            // 将高位减 1
            num[j - 1]--;
            // 将低位全部设置为 9
            for (int k = 0; k < j - 1; k++) num[k] = 9;
        }
    }

    // 将数组转换回整数形式
    return vec2int(num, len);
}

监控二叉树

968. 监控二叉树

思路:二叉树遍历
  1. 贪心算法

    • 从叶子节点向上遍历,尽量在父节点安装摄像头,以覆盖更多的节点。
    • 使用后序遍历(左右根)来遍历二叉树。
  2. 状态定义

    • 每个节点有三种状态:
      • 0:未被监控。
      • 1:被监控,但没有安装摄像头。
      • 2:安装了摄像头。
  3. 状态转移

    • 如果左孩子或右孩子中有一个未被监控(状态 0),则当前节点必须安装摄像头(状态 2)。
    • 如果左孩子或右孩子中有一个安装了摄像头(状态 2),则当前节点被监控(状态 1)。
    • 否则,当前节点未被监控(状态 0)。
  4. 边界条件

    • 如果根节点未被监控,则需要在其上安装摄像头。(易漏)
代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

// 全局变量,记录摄像头的数量
int result = 0;
//0:无覆盖
//1:有覆盖
//2:有摄像头
// 后序遍历函数,返回节点的状态
int traversal(struct TreeNode* root) {
    // 空节点默认是被监控的(状态 1)
    if (root == NULL) return 1;

    // 递归遍历左子树和右子树
    int left = traversal(root->left);
    int right = traversal(root->right);

    // 如果左孩子或右孩子中有一个未被监控,当前节点必须安装摄像头
    if (left == 0 || right == 0) {
        result++;
        return 2;
    }

    // 如果左孩子或右孩子中有一个安装了摄像头,当前节点被监控
    if (left == 2 || right == 2) {
        return 1;
    }

    // 否则,当前节点未被监控
    return 0;
}

// 主函数,返回需要安装的摄像头数量
int minCameraCover(struct TreeNode* root) {
    // 初始化摄像头数量
    result = 0;

    // 如果根节点未被监控,需要安装摄像头
    if (traversal(root) == 0) {
        result++;
    }

    return result;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值