coverPiccoverPic

最长连续子序列

前言

两周前的一场面试里,面试官出了一道算法题。

给定一个未排序的整数数组 nums,找出其中最长的连续子序列。

连续子序列指序列中相邻元素的值相差 1;值相同的元素也视为连续。返回该子序列本身。

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

示例:

输入:nums = [1, 2, 3, 4, 5, 1, 6, -1, 10, 11]

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

计数排序

我想到的就是排序后再取连续递增子序列。题目要 O(n)O(n),我想到的是桶排序,但一时想不出来,只好写了个计数排序勉强交差。还好面试过了。

typescript
  1. function longestConsecutiveByCounting(nums: number[]): number[] {
  2. if (nums.length === 0) return [];
  3. let min = nums[0];
  4. let max = nums[0];
  5. for (const x of nums) {
  6. if (x < min) min = x;
  7. if (x > max) max = x;
  8. }
  9. const count: number[] = [];
  10. for (const x of nums) {
  11. const k = x - min;
  12. count[k] = (count[k] ?? 0) + 1;
  13. }
  14. let bestStart = 0;
  15. let bestLen = 0;
  16. let i = 0;
  17. while (i < count.length) {
  18. if (!count[i]) {
  19. i++;
  20. continue;
  21. }
  22. let j = i;
  23. while (j + 1 < count.length && count[j + 1]) j++;
  24. const len = j - i + 1;
  25. if (len > bestLen) {
  26. bestLen = len;
  27. bestStart = i;
  28. }
  29. i = j + 1;
  30. }
  31. const result: number[] = [];
  32. for (let k = bestStart; k < bestStart + bestLen; k++) {
  33. for (let c = 0; c < (count[k] ?? 0); c++) {
  34. result.push(k + min);
  35. }
  36. }
  37. return result;
  38. }

桶排序

桶排序按值把元素分进桶里,下标取 x - min,所以下标越大值越大。遍历桶数组时,相邻几个桶里都有元素,就说明对应的值连续;取最长的一段,把桶里的元素依次取出来就是结果。

typescript
  1. function longestConsecutiveByBucket(nums: number[]): number[] {
  2. if (nums.length === 0) return [];
  3. let min = nums[0];
  4. let max = nums[0];
  5. for (const x of nums) {
  6. if (x < min) min = x;
  7. if (x > max) max = x;
  8. }
  9. const buckets: number[][] = [];
  10. for (const x of nums) {
  11. const k = x - min;
  12. (buckets[k] ??= []).push(x);
  13. }
  14. let bestStart = 0;
  15. let bestLen = 0;
  16. let i = 0;
  17. while (i < buckets.length) {
  18. if (!buckets[i]) {
  19. i++;
  20. continue;
  21. }
  22. let j = i;
  23. while (j + 1 < buckets.length && buckets[j + 1]) j++;
  24. const len = j - i + 1;
  25. if (len > bestLen) {
  26. bestLen = len;
  27. bestStart = i;
  28. }
  29. i = j + 1;
  30. }
  31. const result: number[] = [];
  32. for (let k = bestStart; k < bestStart + bestLen; k++) {
  33. for (const x of buckets[k] ?? []) result.push(x);
  34. }
  35. return result;
  36. }

哈希表

后面问了一下 AI 大人,AI 用 Map 记录每个值出现几次。遍历 Map 的所有 key,遇到 x - 1 不在 Map 里的,就以 x 为起点往后找连续的值,直到下一个值不在 Map 里。

typescript
  1. function longestConsecutiveByHash(nums: number[]): number[] {
  2. if (nums.length === 0) return [];
  3. const count = new Map<number, number>();
  4. for (const x of nums) {
  5. count.set(x, (count.get(x) ?? 0) + 1);
  6. }
  7. let bestStart = 0;
  8. let bestLen = 0;
  9. for (const x of count.keys()) {
  10. if (count.has(x - 1)) continue;
  11. let len = 1;
  12. while (count.has(x + len)) len++;
  13. if (len > bestLen || (len === bestLen && x < bestStart)) {
  14. bestLen = len;
  15. bestStart = x;
  16. }
  17. }
  18. const result: number[] = [];
  19. for (let v = bestStart; v < bestStart + bestLen; v++) {
  20. const c = count.get(v)!;
  21. for (let k = 0; k < c; k++) result.push(v);
  22. }
  23. return result;
  24. }

每个 key 只遍历一次,最多被扩展一次,所有 while 加起来不超过 nn 次。时间是 O(n)O(n) 期望,空间 O(n)O(n)。

计数排序和桶排序都要把值域扫一遍,时间是 O(n+R)O(n + R),R=max−min+1R = \text{max} - \text{min} + 1。RR 远大于 nn 的时候,这两个写法就不合适了。

后记

当时一开始想的是排序,排完再检查连续子串。我没想到还能用哈希表,哈希表不用排序,避免了排序引起的复杂度增加。

0 条评论未登录用户
Ctrl or + Enter 评论
© 2023-2025 LittleRangiferTarandus, All rights reserved.
🌸 Run