leetcode hot100:二、解题思路大全:堆(数组中的第k个最大元素、前k个高频元素、数据流的中位数)、贪心(买卖股票的最佳时机、跳跃游戏、跳跃游戏Ⅱ、划分字母区间)

因为某大厂的算法没有撕出来,怒而整理该贴。部分题目有AC代码。

数组中的第k个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

思路

感觉和堆也没多大关系,当然,可以用堆。但是我选择快排(

前k个高频元素

面试有考到这个场景题。

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

提示:

  • 1 <= nums.length <= 10^5
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

思路

使用 Counter 计算每个数字的频率,然后创建大小为k的小顶堆。如果堆未满,直接添加元素。如果当前元素的频率大于堆顶元素的频率,则弹出堆顶元素,并将当前元素加入堆中。最后返回堆中的元素,因为可能有相同频次的元素。

数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

例如 arr = [2,3,4] 的中位数是 3 。
例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder 类:

MedianFinder() 初始化 MedianFinder 对象。

void addNum(int num) 将数据流中的整数 num 添加到数据结构中。

double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

提示:

  • -10^5 <= num <= 10^5
  • 在调用 findMedian 之前,数据结构中至少有一个元素
  • 最多 5 * 10^4 次调用 addNum 和 findMedian

思路

为什么不能直接快排?因为快排不能保证插入有序。而我们维护小顶堆可以保证插入有序。

建立一个 小顶堆 A 和 大顶堆 B ,各保存列表的一半元素,且规定:

A 保存 较大 的一半,长度为M/2 ( N 为偶数)或 (M+1)/2 ( M 为奇数)。
B 保存 较小 的一半,长度为N/2 ( N 为偶数)或 (N+1)/2 ( N 为奇数)。
随后,中位数可仅根据 A,B 的堆顶元素计算得到
中位数为 A的堆顶元素(M≠N)或者 (A的堆顶元素+B的堆顶元素)/ 2(M==N)

时间复杂度 O(logN) :

  • 查找中位数 O(1) : 获取堆顶元素使用 O(1) 时间。
  • 添加数字 O(logN) : 堆的插入和弹出操作使用 O(logN) 时间。

空间复杂度 O(N) : 其中 N 为数据流中的元素数量,小顶堆 A 和大顶堆 B 最多同时保存 N 个元素。

贪心

买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

提示:

  • 1 <= prices.length <= 10^5
  • 0 <= prices[i] <= 10^4

思路

一看就知道用dp,dp[i][0]代表第i天持有股票的最大利润,dp[i][1]代表第i天不持有股票的最大利润,最后根据贪心思想可知,答案返回dp[length-1][1]

跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

提示:

  • 1 <= nums.length <= 10^4
  • 0 <= nums[i] <= 10^5

思路

从左到右遍历数组,用一个变量记录当前能到达的最远位置。在遍历过程中,每次都尝试更新这个最远位置。如果在遍历结束前,最远位置已经超过或等于数组的最后一个下标,那就说明可以到达最后一个下标。时间复杂度为O(N)

也可以用动态规划,但是很麻烦,因为状态转移很麻烦,而且时间复杂度为O(n)

  • 核心思想:从数组的最后一个位置开始向前推导,判断每个位置是否能到达最后一个下标,利用前面位置的结果来计算当前位置的结果。
  • 具体过程:
    - 创建一个长度为数组 nums 长度的列表 dp
    - 初始值都为 False ,dp[i] 表示从位置 i 出发能否到达最后一个下标。
    - 将 dp 数组的最后一个元素设为 True ,因为最后一个位置本身就在最后一个下标处,肯定能到达。
    - 从倒数第二个位置开始向前遍历数组,对于每个位置 i :
    - 遍历从 1 到 nums[i] 的所有可能跳跃距离 j ,如果 i + j 不超过数组长度,并且 dp[i + j] 为 True ,说明从位置 i 跳跃 j 步后能到达最后一个下标,那么 dp[i] 为 True ,并跳出当前循环(因为只要有一种跳跃方式能到达就可以了)。
    - 最后返回 dp[0] ,即从起始位置是否能到达最后一个下标。

跳跃游戏Ⅱ

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。

每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。

思路

最开始肯定想到的是dp,但是这个时间复杂度为也为O(n^2)

dp[i] = 从索引i跳到最后一个位置的最小跳跃次数
倒数开始遍历dp,dp[length-1] = 0
对每个i
    for j in range(i-nums[i], i):
        dp[i] = dp[j]+1
返回dp[0]

所以我们用贪心 + 层次遍历,模拟跳跃过程,按跳跃次数分层,每次记录当前层能到达的最远范围。

  • 核心思想:
    • 维护当前层的跳跃范围:记录当前层(第 step 次跳跃)能到达的最远左边界 left 和右边界 right。
    • 扩展下一层范围:遍历当前层 [left, right] 内的所有位置,计算下一层能到达的最远右边界 next_right。
    • 更新跳跃次数:每扩展一层,跳跃次数 step 加 1,直到 right 到达或超过终点。

划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 “ababcc” 能够被分为 [“abab”, “cc”],但类似 [“aba”, “bcc”] 或 [“ab”, “ab”, “cc”] 的划分是非法的。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

返回一个表示每个字符串片段的长度的列表。

示例 1:
输入:s = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”、“defegde”、“hijhklij” 。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 这样的划分是错误的,因为划分的片段数较少。

提示:

1 <= s.length <= 500
s 仅由小写英文字母组成

思路

记录每个字符最后出现的位置:首先遍历字符串,用哈希表记录每个字符最后一次出现的索引。

  • 贪心扩展当前片段边界:从左到右遍历字符串,维护当前片段的起始位置 start 和结束位置 end。
  • 对于每个字符 s[i],更新 end 为当前字符最后出现的位置与 end 的较大值。
  • 当遍历到 i == end 时,说明当前片段可以结束(所有字符的最后出现位置都在 end 内),记录片段长度 end - start + 1,并将 start 移动到下一片段的起始位置 i + 1。

你可能感兴趣的:(LeetCode,leetcode,算法,数据结构,堆,贪心,笔试,python)