likes
comments
collection
share

【面试高频题】热门数据结构面试题合集(堆)

作者站长头像
站长
· 阅读数 13

本文正在参加「金石计划」

堆一直是面试数据结构中的重中之重,今天通过 555 道与堆相关的题目来进行学习。


373. 查找和最小的K对数字

给定两个以升序排列的整数数组 nums1nums2 , 以及一个整数 k 。

定义一对值 (u,v)(u,v)(u,v),其中第一个元素来自 nums1,第二个元素来自 nums2

请找到和最小的 k 个数对 (u1,v1), (u2,v2) ... (uk,vk)(u_1,v_1),  (u_2,v_2)  ...  (u_k,v_k)(u1,v1), (u2,v2) ... (uk,vk) 。 

示例 1:

输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3

输出: [1,2],[1,4],[1,6]

解释: 返回序列中的前 3 对数:
     [1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]

提示:

  • 1<=nums1.length,nums2.length<=1041 <= nums1.length, nums2.length <= 10^41<=nums1.length,nums2.length<=104
  • −109<=nums1[i],nums2[i]<=109-10^9 <= nums1[i], nums2[i] <= 10^9109<=nums1[i],nums2[i]<=109
  • nums1,nums2均为升序排列nums1, nums2 均为升序排列nums1,nums2均为升序排列
  • 1<=k<=10001 <= k <= 10001<=k<=1000
基本分析

这道题和 (题解) 786. 第 K 个最小的素数分数 几乎是一模一样,先做哪一道都是一样的,难度上没有区别 🤣

最常规的做法是使用「多路归并」,还不熟悉「多路归并」的同学,建议先学习前置🧀:多路归并入门,里面讲述了如何从「朴素优先队列」往「多路归并」进行转换。

多路归并

nums1nums1nums1 的长度为 nnnnums2nums2nums2 的长度为 mmm,所有的点对数量为 n∗mn * mnm

其中每个 nums1[i]nums1[i]nums1[i] 参与所组成的点序列为:

[(nums1[0], nums2[0]), (nums1[0], nums2[1]), ..., (nums1[0], nums2[m - 1])]\\ [(nums1[1], nums2[0]), (nums1[1], nums2[1]), ..., (nums1[1], nums2[m - 1])]\\ ...\\ [(nums1[n - 1], nums2[0]), (nums1[n - 1], nums2[1]), ..., (nums1[n - 1], nums2[m - 1])]\\

由于 nums1nums1nums1nums2nums2nums2 均已按升序排序,因此每个 nums1[i]nums1[i]nums1[i] 参与构成的点序列也为升序排序,这引导我们使用「多路归并」来进行求解。

具体的,起始我们将这 nnn 个序列的首位元素(点对)以二元组 (i,j)(i, j)(i,j) 放入优先队列(小根堆),其中 iii 为该点对中 nums1[i]nums1[i]nums1[i] 的下标,jjj 为该点对中 nums2[j]nums2[j]nums2[j] 的下标,这步操作的复杂度为 O(nlog⁡n)O(n\log{n})O(nlogn)。这里也可以得出一个小优化是:我们始终确保 nums1nums1nums1 为两数组中长度较少的那个,然后通过标识位来记录是否发生过交换,确保答案的点顺序的正确性。

每次从优先队列(堆)中取出堆顶元素(含义为当前未被加入到答案的所有点对中的最小值),加入答案,并将该点对所在序列的下一位(如果有)加入优先队列中。

举个 🌰,首次取出的二元组为 (0,0)(0, 0)(0,0),即点对 (nums1[0],nums2[0])(nums1[0], nums2[0])(nums1[0],nums2[0]),取完后将序列的下一位点对 (nums1[0],nums2[1])(nums1[0], nums2[1])(nums1[0],nums2[1]) 以二元组 (0,1)(0, 1)(0,1) 形式放入优先队列。

可通过「反证法」证明,每次这样的「取当前,放入下一位」的操作,可以确保当前未被加入答案的所有点对的最小值必然在优先队列(堆)中,即前 kkk 个出堆的元素必然是所有点对的前 kkk 小的值。

Java 代码:

class Solution {
    boolean flag = true;
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        List<List<Integer>> ans = new ArrayList<>();
        int n = nums1.length, m = nums2.length;
        if (n > m && !(flag = false)) return kSmallestPairs(nums2, nums1, k);
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->(nums1[a[0]]+nums2[a[1]])-(nums1[b[0]]+nums2[b[1]]));
        for (int i = 0; i < Math.min(n, k); i++) q.add(new int[]{i, 0});
        while (ans.size() < k && !q.isEmpty()) {
            int[] poll = q.poll();
            int a = poll[0], b = poll[1];
            ans.add(new ArrayList<>(){{
                add(flag ? nums1[a] : nums2[b]);
                add(flag ? nums2[b] : nums1[a]);
            }});
            if (b + 1 < m) q.add(new int[]{a, b + 1});
        }
        return ans;
    }
}
  • 时间复杂度:令 MMMnnnmmmkkk 三者中的最小值,复杂度为 O(M+k)×log⁡M)O(M + k) \times \log{M})O(M+k)×logM)
  • 空间复杂度:O(M)O(M)O(M)
二分

我们还能够使用多次「二分」来做。

假设我们将所有「数对和」按照升序排序,两端的值分别为 l=nums1[0]+nums2[0]l = nums1[0] + nums2[0]l=nums1[0]+nums2[0]r=nums1[n−1]+nums2[m−1]r = nums1[n - 1] + nums2[m - 1]r=nums1[n1]+nums2[m1]

因此我们可以在值域 [l,r][l, r][l,r] 上进行二分,找到第一个满足「点对和小于等于 xxx 的,且数量超过 kkk 的值 xxx」。

之所以能够二分,是因为 xxx 所在的点对和数轴上具有二段性:

  • 点对和小于 xxx 的点对数量少于 kkk 个;
  • 点对和大于等于 xxx 的点对数量大于等于 kkk 个。

判定小于等于 xxx 的点对数量是否大于等于 kkk 个这一步可直接使用循环来做,由于二分是从中间值开始,这一步不会出现跑满两层循环的情况。

当二分出第 kkk 小的值为 xxx 后,由于存在不同点对的点对和值相等,我们需要先将所有点对和小于等于 xxx 的值加入答案,然后酌情把值等于 xxx 的点对加入答案,知道满足答案数量为 kkk

找值为 xxx 的所有点对这一步,可以通过枚举 nums1[i]nums1[i]nums1[i],然后在 nums2nums2nums2 上二分目标值 x−nums1[i]x - nums1[i]xnums1[i] 的左右端点来做。

最后,在所有处理过程中,我们都可以利用答案数组的大小与 kkk 的关系做剪枝。

Java 代码:

class Solution {
    int[] nums1, nums2;
    int n, m;
    public List<List<Integer>> kSmallestPairs(int[] n1, int[] n2, int k) {
        nums1 = n1; nums2 = n2;
        n = nums1.length; m = nums2.length;
        List<List<Integer>> ans = new ArrayList<>();
        int l = nums1[0] + nums2[0], r = nums1[n - 1] + nums2[m - 1];
        while (l < r) {
            int mid = (int)(0L + l + r >> 1);
            if (check(mid, k)) r = mid;
            else l = mid + 1;
        }
        int x = r;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (nums1[i] + nums2[j] < x) {
                    List<Integer> temp = new ArrayList<>();
                    temp.add(nums1[i]); temp.add(nums2[j]);
                    ans.add(temp);
                } else break;
            }
        }
        for (int i = 0; i < n && ans.size() < k; i++) {
            int a = nums1[i], b = x - a;
            int c = -1, d = -1;
            l = 0; r = m - 1;
            while (l < r) {
                int mid = (int)(0L + l + r >> 1);
                if (nums2[mid] >= b) r = mid;
                else l = mid + 1;
            }
            if (nums2[r] != b) continue;
            c = r;
            l = 0; r = m - 1;
            while (l < r) {
                int mid = (int)(0L + l + r + 1) >> 1;
                if (nums2[mid] <= b) l = mid;
                else r = mid - 1;
            }
            d = r;
            for (int p = c; p <= d && ans.size() < k; p++) {
                List<Integer> temp = new ArrayList<>();
                temp.add(a); temp.add(b);
                ans.add(temp);
            }
        }
        return ans;
    }
    boolean check(int x, int k) {
        int ans = 0;
        for (int i = 0; i < n && ans < k; i++) {
            for (int j = 0; j < m && ans < k; j++) {
                if (nums1[i] + nums2[j] <= x) ans++;
                else break;
            }
        }
        return ans >= k;
    }
}
  • 时间复杂度:假设点对和的值域大小范围为 MMM,第一次二分的复杂度为 O((n×m)×log⁡M)O((n \times m) \times \log{M})O((n×m)×logM);统计点对和值小于目标值 xxx 的复杂度为 O(n×m)O(n \times m)O(n×m);统计所有点对和等于目标值的复杂度为 O(max⁡(n×log⁡m,k))O(\max(n \times \log{m}, k))O(max(n×logm,k))(整个处理过程中利用了大小关系做了剪枝,大多循环都不会跑满,实际计算量会比理论分析的要低)
  • 空间复杂度:O(k)O(k)O(k)

264. 丑数 II

给你一个整数 n ,请你找出并返回第 n 个 丑数 。

丑数 就是只包含质因数 23 和 5 的正整数。

示例 1:

输入:n = 10

输出:12

解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。

提示:

  • 1<=n<=16901 <= n <= 16901<=n<=1690
基本思路

根据丑数的定义,我们有如下结论:

  • 111 是最小的丑数。
  • 对于任意一个丑数 xxx,其与任意的质因数(222333555)相乘,结果(2x2x2x3x3x3x5x5x5x)仍为丑数。
优先队列(小根堆)

有了基本的分析思路,一个简单的解法是使用优先队列:

  1. 起始先将最小丑数 111 放入队列
  2. 每次从队列取出最小值 xxx,然后将 xxx 所对应的丑数 2x2x2x3x3x3x5x5x5x 进行入队。
  3. 对步骤 2 循环多次,第 nnn 次出队的值即是答案。

为了防止同一丑数多次进队,我们需要使用数据结构 SetSetSet 来记录入过队列的丑数。

Java 代码:

class Solution {
    int[] nums = new int[]{2,3,5};
    public int nthUglyNumber(int n) {
        Set<Long> set = new HashSet<>();
        Queue<Long> pq = new PriorityQueue<>();
        set.add(1L);
        pq.add(1L);
        for (int i = 1; i <= n; i++) {
            long x = pq.poll();
            if (i == n) return (int)x;
            for (int num : nums) {
                long t = num * x;
                if (!set.contains(t)) {
                    set.add(t);
                    pq.add(t);
                }
            }
        }
        return -1;
    }
}
  • 时间复杂度:从优先队列中取最小值为 O(1)O(1)O(1),往优先队列中添加元素复杂度为 O(log⁡n)O(\log{n})O(logn)。整体复杂度为 O(nlog⁡n)O(n\log{n})O(nlogn)
  • 空间复杂度:O(n)O(n)O(n)
多路归并(多指针)

从解法一中不难发现,我们「往后产生的丑数」都是基于「已有丑数」而来(使用「已有丑数」乘上「质因数」222333555)。

因此,如果我们所有丑数的有序序列为 a1,a2,a3,...,ana1,a2,a3,...,ana1,a2,a3,...,an 的话,序列中的每一个数都必然能够被以下三个序列(中的至少一个)覆盖:

  • 由丑数 ×2\times 2×2 所得的有序序列:1×21 \times 21×22×22 \times 22×23×23 \times 23×24×24 \times 24×25×25 \times 25×26×26 \times 26×28×28 \times 28×2 ...
  • 由丑数 ×3 \times 3×3 所得的有序序列:1×31 \times 31×32×32 \times 32×33×33 \times 33×34×34 \times 34×35×35 \times 35×36×36 \times 36×38×38 \times 38×3 ...
  • 由丑数 ×5\times 5×5 所得的有序序列:1×51 \times 51×52×52 \times 52×53×53 \times 53×54×54 \times 54×55×55 \times 55×56×56 \times 56×58×58 \times 58×5 ...

举个🌰,假设我们需要求得 [1,2,3,...,10,12][1, 2, 3, ... , 10, 12][1,2,3,...,10,12] 丑数序列 arrarrarr 的最后一位,那么该序列可以看作以下三个有序序列归并而来:

  • 1×2,2×2,3×2,...,10×2,12×21 \times 2, 2 \times 2, 3 \times 2, ... , 10 \times 2, 12 \times 21×2,2×2,3×2,...,10×2,12×2 ,将 222 提出,即 arr×2arr \times 2arr×2
  • 1×3,2×3,3×3,...,10×3,12×31 \times 3, 2 \times 3, 3 \times 3, ... , 10 \times 3, 12 \times 31×3,2×3,3×3,...,10×3,12×3 ,将 333 提出,即 arr×3arr \times 3arr×3
  • 1×5,2×5,3×5,...,10×5,12×51 \times 5, 2 \times 5, 3 \times 5, ... , 10 \times 5, 12 \times 51×5,2×5,3×5,...,10×5,12×5 ,将 555 提出,即 arr×5arr \times 5arr×5

因此我们可以使用三个指针来指向目标序列 arrarrarr 的某个下标(下标 000 作为哨兵不使用,起始都为 111),使用 arr[下标]×质因数arr[下标] \times 质因数arr[下标]×质因数 代表当前使用到三个有序序列中的哪一位,同时使用 idxidxidx 表示当前生成到 arrarrarr 哪一位丑数。

Java 代码:

class Solution {
    public int nthUglyNumber(int n) {
        // ans 用作存储已有丑数(从下标 1 开始存储,第一个丑数为 1)
        int[] ans = new int[n + 1];
        ans[1] = 1;
        // 由于三个有序序列都是由「已有丑数」*「质因数」而来
        // i2、i3 和 i5 分别代表三个有序序列当前使用到哪一位「已有丑数」下标(起始都指向 1)
        for (int i2 = 1, i3 = 1, i5 = 1, idx = 2; idx <= n; idx++) {
            // 由 ans[iX] * X 可得当前有序序列指向哪一位
            int a = ans[i2] * 2, b = ans[i3] * 3, c = ans[i5] * 5;
            // 将三个有序序列中的最小一位存入「已有丑数」序列,并将其下标后移
            int min = Math.min(a, Math.min(b, c));
            // 由于可能不同有序序列之间产生相同丑数,因此只要一样的丑数就跳过(不能使用 else if )
            if (min == a) i2++; 
            if (min == b) i3++;
            if (min == c) i5++;
            ans[idx] = min;
        }
        return ans[n];
    }
}
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(n)O(n)O(n)

313. 超级丑数

超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 primes 中。

给你一个整数 n 和一个整数数组 primes ,返回第 n 个 超级丑数 。

题目数据保证第 n 个 超级丑数 在 32-bit 带符号整数范围内。

示例 1:

输入:n = 12, primes = [2,7,13,19]

输出:32 

解释:给定长度为 4 的质数数组 primes = [2,7,13,19],前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32] 。

提示:

  • 1 <= n <= 10610^6106
  • 1 <= primes.length <= 100
  • 2 <= primes[i] <= 1000
  • 题目数据 保证 primes[i] 是一个质数
  • primes 中的所有值都 互不相同 ,且按 递增顺序 排列
基本分析

根据丑数的定义,我们有如下结论:

  • 111 是最小的丑数。
  • 对于任意一个丑数 xxx,其与任意给定的质因数 primes[i]primes[i]primes[i] 相乘,结果仍为丑数。
优先队列(堆)

有了基本的分析思路,一个简单的解法是使用优先队列:

  1. 起始先将最小丑数 111 放入队列
  2. 每次从队列取出最小值 xxx,然后将 xxx 所对应的丑数 x∗primes[i]x * primes[i]xprimes[i] 进行入队。
  3. 对步骤 222 循环多次,第 nnn 次出队的值即是答案。

为了防止同一丑数多次进队,我们可以使用数据结构 SetSetSet 来记录入过队列的丑数,但该做法常数较大,容易被卡。

利用题目规定的答案为 intintint 范围,以及丑数性质,我们可以直接在入队的时候做控制。

代码:

class Solution {
    public int nthSuperUglyNumber(int n, int[] primes) {
        PriorityQueue<Integer> q = new PriorityQueue<>();
        q.add(1);
        while (n-- > 0) {
            int x = q.poll();
            if (n == 0) return x;
            for (int k : primes) {
                if (k <= Integer.MAX_VALUE / x) q.add(k * x);
                if (x % k == 0) break;
            }
        }
        return -1; // never
    }
}
  • 时间复杂度:令 primesprimesprimes 长度为 mmm,需要从优先队列(堆)中弹出 nnn 个元素,每次弹出最多需要放入 mmm 个元素,堆中最多有 n∗mn * mnm 个元素。复杂度为 O(n∗mlog⁡(n∗m))O(n * m \log{(n * m)})O(nmlog(nm))
  • 空间复杂度:O(n∗m)O(n * m)O(nm)
多路归并

从解法一中不难发现,我们「往后产生的丑数」都是基于「已有丑数」而来(使用「已有丑数」乘上「给定质因数」primes[i]primes[i]primes[i])。

因此,如果我们所有丑数的有序序列为 a1,a2,a3,...,ana1,a2,a3,...,ana1,a2,a3,...,an 的话,序列中的每一个数都必然能够被以下三个序列(中的至少一个)覆盖(这里假设 primes=[2,3,5]primes = [2,3,5]primes=[2,3,5]):

  • 由丑数 * 222 所得的有序序列:1∗21 * 2122∗22 * 2223∗23 * 2324∗24 * 2425∗25 * 2526∗26 * 2628∗28 * 282 ...
  • 由丑数 * 333 所得的有序序列:1∗31 * 3132∗32 * 3233∗33 * 3334∗34 * 3435∗35 * 3536∗36 * 3638∗38 * 383 ...
  • 由丑数 * 555 所得的有序序列:1∗51 * 5152∗52 * 5253∗53 * 5354∗54 * 5455∗55 * 5556∗56 * 5658∗58 * 585 ...

我们令这些有序序列为 arrarrarr,最终的丑数序列为 ansansans

如果 primesprimesprimes 的长度为 mmm 的话,我们可以使用 mmm 个指针来指向这 mmm 个有序序列 arrarrarr 的当前下标。

显然,我们需要每次取 mmm 个指针中值最小的一个,然后让指针后移(即将当前序列的下一个值放入堆中),不断重复这个过程,直到我们找到第 nnn 个丑数。

当然,实现上,我们并不需要构造出这 mmm 个有序序列。

我们可以构造一个存储三元组的小根堆,三元组信息为 (val,i,idx)(val, i, idx)(val,i,idx)

  • valvalval :为当前列表指针指向具体值;
  • iii :代表这是由 primes[i]primes[i]primes[i] 构造出来的有序序列;
  • idxidxidx:代表丑数下标,存在关系 val=ans[idx]∗primes[i]val = ans[idx] * primes[i]val=ans[idx]primes[i]

起始时,我们将所有的 (primes[i],i,0)(primes[i], i, 0)(primes[i],i,0) 加入优先队列(堆)中,每次从堆中取出最小元素,那么下一个该放入的元素为 (ans[idx+1]∗primes[i],i,idx+1)(ans[idx + 1] * primes[i], i, idx + 1)(ans[idx+1]primes[i],i,idx+1)

另外,由于我们每个 arrarrarr 的指针移动和 ansansans 的构造,都是单调递增,因此我们可以通过与当前最后一位构造的 ans[x]ans[x]ans[x] 进行比较来实现去重,而无须引用常数较大的 Set 结构。

代码:

class Solution {
    public int nthSuperUglyNumber(int n, int[] primes) {
        int m = primes.length;
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[0]-b[0]); 
        for (int i = 0; i < m; i++) {
            q.add(new int[]{primes[i], i, 0});
        }
        int[] ans = new int[n];
        ans[0] = 1;
        for (int j = 1; j < n; ) {
            int[] poll = q.poll();
            int val = poll[0], i = poll[1], idx = poll[2];
            if (val != ans[j - 1]) ans[j++] = val;
            q.add(new int[]{ans[idx + 1] * primes[i], i, idx + 1});
        }
        return ans[n - 1];
    }
}
  • 时间复杂度:需要构造长度为 nnn 的答案,每次构造需要往堆中取出和放入元素,堆中有 mmm 个元素,起始时,需要对 primesprimesprimes 进行遍历,复杂度为 O(m)O(m)O(m)。整体复杂度为 O(max⁡(m,nlog⁡m))O(\max(m, n\log{m}))O(max(m,nlogm))
  • 空间复杂度:存储 nnn 个答案,堆中有 mmm 个元素,复杂度为 O(n+m)O(n + m)O(n+m)

451. 根据字符出现频率排序

给定一个字符串,请将字符串里的字符按照出现的频率降序排列。

示例 1:

输入:
"tree"

输出:
"eert"

解释:
'e'出现两次,'r''t'都只出现一次。
因此'e'必须出现在'r''t'之前。此外,"eetr"也是一个有效的答案。

提示:

  • 1<=s.length<=5×1051 <= s.length <= 5 \times 10^51<=s.length<=5×105
  • s 由大小写英文字母和数字组成
数据结构 + 模拟

这是一道考察数据结构运用的模拟题。

具体做法如下:

  1. 先使用「哈希表」对词频进行统计;
  2. 遍历统计好词频的哈希表,将每个键值对以 {字符,词频} 的形式存储到「优先队列(堆)」中。并规定「优先队列(堆)」排序逻辑为:
    • 如果 词频 不同,则按照 词频 倒序;
    • 如果 词频 相同,则根据 字符字典序 升序(由于本题采用 Special Judge 机制,这个排序策略随意调整也可以。但通常为了确保排序逻辑满足「全序关系」,这个地方可以写正写反,但理论上不能不写,否则不能确保每次排序结果相同);
  3. 从「优先队列(堆)」依次弹出,构造答案。

代码:

class Solution {
    public String frequencySort(String s) {
        char[] cs = s.toCharArray();
        Map<Character, Integer> map = new HashMap<>();
        for (char c : cs) map.put(c, map.getOrDefault(c, 0) + 1);
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->{
            return a[1] != b[1] ? b[1] - a[1] : a[0] - b[0];
        });
        for (char c : map.keySet()) q.add(new int[]{c, map.get(c)});
        StringBuilder sb = new StringBuilder();
        while (!q.isEmpty()) {
            int[] poll = q.poll();
            int c = poll[0], k = poll[1];
            while (k-- > 0) sb.append((char)(c));
        }
        return sb.toString();
    }
}
  • 时间复杂度:令字符集的大小为 CCC。使用「哈希表」统计词频的复杂度为 O(n)O(n)O(n);最坏情况下字符集中的所有字符都有出现,最多有 CCC 个节点要添加到「优先队列(堆)」中,复杂度为 O(Clog⁡C)O(C\log{C})O(ClogC);构造答案需要从「优先队列(堆)」中取出元素并拼接,复杂度为 O(n)O(n)O(n)。整体复杂度为 O(max⁡(n,Clog⁡C))O(\max(n, C\log{C}))O(max(n,ClogC))
  • 空间复杂度:O(n)O(n)O(n)
数组实现 + 模拟

基本思路不变,将上述过程所用到的数据结构使用数组替代。

具体的,利用 ASCII 字符集共 128128128 位,预先建立一个大小为 128128128 的数组,利用「桶排序」的思路替代「哈希表」和「优先队列(堆)」的作用。

代码:

class Solution {   
    public String frequencySort(String s) {
        int[][] cnts = new int[128][2];
        char[] cs = s.toCharArray();
        for (int i = 0; i < 128; i++) cnts[i][0] = i;
        for (char c : cs) cnts[c][1]++;
        Arrays.sort(cnts, (a, b)->{
            return a[1] != b[1] ? b[1] - a[1] : a[0] - b[0];
        });
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 128; i++) {
            char c = (char)cnts[i][0];
            int k = cnts[i][1];
            while (k-- > 0) sb.append(c);
        }
        return sb.toString();
    }
}
  • 时间复杂度:令字符集的大小为 CCC,复杂度为 O(max⁡(n,Clog⁡C))O(\max(n, C\log{C}))O(max(n,ClogC))
  • 空间复杂度:O(n+C+log⁡C)O(n + C + \log{C})O(n+C+logC)

871. 最低加油次数

汽车从起点出发驶向目的地,该目的地位于出发位置东面 target 英里处。

沿途有加油站,每个 station[i] 代表一个加油站,它位于出发位置东面 station[i][0] 英里处,并且有 station[i][1] 升汽油。

假设汽车油箱的容量是无限的,其中最初有 startFuel 升燃料。它每行驶 111 英里就会用掉 111 升汽油。

当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。

为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 −1-11

注意:如果汽车到达加油站时剩余燃料为 000,它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 000,仍然认为它已经到达目的地。

示例 1:

输入:target = 1, startFuel = 1, stations = []

输出:0

解释:我们可以在不加油的情况下到达目的地。

提示:

  • 1<=target,startFuel,stations[i][1]<=1091 <= target, startFuel, stations[i][1] <= 10^91<=target,startFuel,stations[i][1]<=109
  • 0<=stations.length<=5000 <= stations.length <= 5000<=stations.length<=500
  • 0<stations[0][0]<stations[1][0]<...<stations[stations.length−1][0]<target0 < stations[0][0] < stations[1][0] < ... < stations[stations.length-1][0] < target0<stations[0][0]<stations[1][0]<...<stations[stations.length1][0]<target
贪心 + 优先队列(堆)

为了方便,我们记 targett,记 startFuelstart,记 stationsss

我们可以模拟行进过程,使用 n 代表加油站总个数,idx 代表经过的加油站下标,使用 remain 代表当前有多少油(起始有 remain = start),loc 代表走了多远,ans 代表答案(至少需要的加油次数)。

只要 loc < t,代表还没到达(经过)目标位置,我们可以继续模拟行进过程,每次将 remain 累加到 loc 中,含义为使用完剩余的油量,可以去到的最远距离,同时将所在位置 ss[idx][0] <= loc 的加油站数量加入优先队列(大根堆,根据油量排倒序)中,再次检查是否满足 loc < t(下次循环),此时由于清空了剩余油量 remain,我们尝试从优先队列(大根堆)中取出过往油量最大的加油站并进行加油(同时对加油次数 ans 进行加一操作),使用新的剩余油量 remain 重复上述过程,直到满足 loc >= t 或无油可加。

容易证明该做法的正确性:同样是消耗一次加油次数,始终选择油量最大的加油站进行加油,可以确保不存在更优的结果。

代码:

class Solution {
    public int minRefuelStops(int t, int start, int[][] ss) {
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int n = ss.length, idx = 0;
        int remain = start, loc = 0, ans = 0;
        while (loc < t) {
            if (remain == 0) {
                if (!q.isEmpty() && ++ans >= 0) remain += q.poll();
                else return -1;
            }
            loc += remain; remain = 0;
            while (idx < n && ss[idx][0] <= loc) q.add(ss[idx++][1]);
        }
        return ans;
    }
}
  • 时间复杂度:O(nlog⁡n)O(n\log{n})O(nlogn)
  • 空间复杂度:O(n)O(n)O(n)

总结

优先队列(堆)一直都是笔试/面试中的重点,而且考察的地方往往不在于手写堆,而是考察如何将原问题转换为能够使用优先队列(堆)来求解的问题。