Flutter & 前端通用算法题:双指针 / 哈希 / 贪心 10 讲

1 阅读7分钟

大家好,我是14年菜鸟前端,在面试和日常刷题中总结了10 道超高频算法题,涵盖排序、LIS、双指针、哈希、前缀和、贪心等核心考点,每道题都附带清晰思路 + 极简最优代码 + JS/Dart 双语言实现,非常适合面试突击和博客学习。

全文结构统一:题目描述 → 思路分析 → 核心数据结构 / 算法 → 代码实现(JS + Dart) → 复杂度分析


1. 俄罗斯套娃信封问题(困难・面试必考)

题目描述

给你一个二维整数数组 envelopes,其中 envelopes[i] = [wi, hi],表示第 i 个信封的宽度和高度。当一个信封的宽度和高度都比另一个大时,才能嵌套。求最多能套多少层。

思路

  1. 排序:宽度升序,宽度相同时高度降序(避免同宽重复嵌套)
  2. 提取高度数组,求最长严格递增子序列 LIS
  3. LIS 使用二分优化到 O(n log n)

JS 实现

javascript

运行

var maxEnvelopes = function(envelopes) {
  envelopes.sort((a, b) => {
    if (a[0] !== b[0]) return a[0] - b[0]
    return b[1] - a[1]
  })

  const heights = envelopes.map(e => e[1])
  const tails = []

  for (const h of heights) {
    let l = 0, r = tails.length
    while (l < r) {
      const mid = (l + r) >> 1
      if (tails[mid] >= h) r = mid
      else l = mid + 1
    }
    l === tails.length ? tails.push(h) : (tails[l] = h)
  }

  return tails.length
}

Dart 实现

dart

int maxEnvelopes(List<List<int>> envelopes) {
  envelopes.sort((a, b) {
    if (a[0] != b[0]) return a[0] - b[0];
    return b[1] - a[1];
  });

  final heights = envelopes.map((e) => e[1]).toList();
  final tails = <int>[];

  for (int h in heights) {
    int l = 0, r = tails.length;
    while (l < r) {
      int mid = (l + r) ~/ 2;
      if (tails[mid] >= h) {
        r = mid;
      } else {
        l = mid + 1;
      }
    }
    l == tails.length ? tails.add(h) : tails[l] = h;
  }

  return tails.length;
}

复杂度

  • 时间:O(n log n)
  • 空间:O(n)

2. 最长连续递增序列(简单・双指针)

题目描述

给定一个未排序数组,找出最长连续递增子序列的长度。

思路

  • 双指针维护一个递增窗口
  • 遇到下降就重置左指针

JS

javascript

运行

var findLengthOfLCIS = function(nums) {
  if (!nums.length) return 0
  let maxLen = 1, left = 0
  for (let right = 1; right < nums.length; right++) {
    if (nums[right] <= nums[right - 1]) left = right
    maxLen = Math.max(maxLen, right - left + 1)
  }
  return maxLen
}

Dart

dart

int findLengthOfLCIS(List<int> nums) {
  if (nums.isEmpty) return 0;
  int maxLen = 1, left = 0;
  for (int right = 1; right < nums.length; right++) {
    if (nums[right] <= nums[right - 1]) left = right;
    maxLen = max(maxLen, right - left + 1);
  }
  return maxLen;
}

复杂度

O(n) / O(1)


3. 最长连续序列(困难・哈希经典)

题目描述

给定未排序数组,找出数字连续最长序列的长度(要求 O (n))。

思路

  • 用 Set 去重 + O (1) 查询
  • 只从序列起点(num-1 不存在)开始统计

JS

javascript

运行

var longestConsecutive = function(nums) {
  const set = new Set(nums)
  let maxLen = 0
  for (const num of set) {
    if (!set.has(num - 1)) {
      let cur = num, len = 1
      while (set.has(cur + 1)) {
        cur++
        len++
      }
      maxLen = Math.max(maxLen, len)
    }
  }
  return maxLen
}

Dart

dart

int longestConsecutive(List<int> nums) {
  final set = nums.toSet();
  int maxLen = 0;
  for (int num in set) {
    if (!set.contains(num - 1)) {
      int cur = num, len = 1;
      while (set.contains(cur + 1)) {
        cur++;
        len++;
      }
      maxLen = max(maxLen, len);
    }
  }
  return maxLen;
}

复杂度

O(n) / O(n)


4. 盛最多水的容器(困难・面试高频)

题目描述

数组代表高度,选两条线,使它们与 x 轴容纳最多水。

思路

  • 左右双指针
  • 每次移动较短边(贪心)

JS

javascript

运行

var maxArea = function(height) {
  let l = 0, r = height.length - 1, max = 0
  while (l < r) {
    const h = Math.min(height[l], height[r])
    max = Math.max(max, h * (r - l))
    height[l] < height[r] ? l++ : r--
  }
  return max
}

Dart

dart

int maxArea(List<int> height) {
  int l = 0, r = height.length - 1, max = 0;
  while (l < r) {
    int h = min(height[l], height[r]);
    max = max(max, h * (r - l));
    height[l] < height[r] ? l++ : r--;
  }
  return max;
}

复杂度

O(n) / O(1)


5. 寻找两个正序数组的中位数(困难・双指针)

题目描述

两个正序数组,找出整体中位数,要求尽量最优。

思路

双指针遍历,只走到中位数位置即可,无需合并全部数组。

JS

javascript

运行

var findMedianSortedArrays = function(nums1, nums2) {
  const len1 = nums1.length, len2 = nums2.length
  const total = len1 + len2
  let i = 0, j = 0, pre = 0, cur = 0

  for (let k = 0; k <= total ~/ 2; k++) {
    pre = cur
    if (i < len1 && (j >= len2 || nums1[i] < nums2[j])) {
      cur = nums1[i++]
    } else {
      cur = nums2[j++]
    }
  }

  return total % 2 === 1 ? cur : (pre + cur) / 2
}

Dart

dart

double findMedianSortedArrays(List<int> nums1, List<int> nums2) {
  int len1 = nums1.length, len2 = nums2.length;
  int total = len1 + len2;
  int i = 0, j = 0, pre = 0, cur = 0;

  for (int k = 0; k <= total ~/ 2; k++) {
    pre = cur;
    if (i < len1 && (j >= len2 || nums1[i] < nums2[j])) {
      cur = nums1[i++];
    } else {
      cur = nums2[j++];
    }
  }

  return total.isOdd ? cur.toDouble() : (pre + cur) / 2.0;
}

复杂度

O(m+n) / O(1)


6. 删除有序数组中的重复项(简单・快慢指针)

题目描述

原地删除重复元素,返回新长度。

思路

  • 慢指针:有效区末尾
  • 快指针:遍历

JS

javascript

运行

var removeDuplicates = function(nums) {
  if (!nums.length) return 0
  let slow = 0
  for (let fast = 1; fast < nums.length; fast++) {
    if (nums[fast] !== nums[slow]) {
      nums[++slow] = nums[fast]
    }
  }
  return slow + 1
}

Dart

dart

int removeDuplicates(List<int> nums) {
  if (nums.isEmpty) return 0;
  int slow = 0;
  for (int fast = 1; fast < nums.length; fast++) {
    if (nums[fast] != nums[slow]) {
      nums[++slow] = nums[fast];
    }
  }
  return slow + 1;
}

复杂度

O(n) / O(1)


7. 和为 K 的子数组(中等・前缀和 + 哈希)

题目描述

统计数组中和为 K 的连续子数组数量。

思路

  • 前缀和 preSum
  • 哈希表存 preSum 出现次数
  • 子数组和 = preSum [j] - preSum [i]

JS

javascript

运行

var subarraySum = function(nums, k) {
  const map = new Map([[0, 1]])
  let preSum = 0, count = 0
  for (const n of nums) {
    preSum += n
    count += map.get(preSum - k) || 0
    map.set(preSum, (map.get(preSum) || 0) + 1)
  }
  return count
}

Dart

dart

int subarraySum(List<int> nums, int k) {
  final map = <int, int>{0: 1};
  int preSum = 0, count = 0;
  for (int n in nums) {
    preSum += n;
    count += map[preSum - k] ?? 0;
    map[preSum] = (map[preSum] ?? 0) + 1;
  }
  return count;
}

复杂度

O(n) / O(n)


8. nSum 系列(简单・哈希 + 双指针)

这里放最常考的 2Sum + 3Sum

2Sum JS

javascript

运行

var twoSum = function(nums, target) {
  const map = new Map()
  for (let i = 0; i < nums.length; i++) {
    const t = target - nums[i]
    if (map.has(t)) return [map.get(t), i]
    map.set(nums[i], i)
  }
}

2Sum Dart

dart

List<int> twoSum(List<int> nums, int target) {
  final map = <int, int>{};
  for (int i = 0; i < nums.length; i++) {
    int t = target - nums[i];
    if (map.containsKey(t)) return [map[t]!, i];
    map[nums[i]] = i;
  }
  return [];
}

3Sum JS

javascript

运行

var threeSum = function(nums) {
  nums.sort((a, b) => a - b)
  const res = []
  for (let i = 0; i < nums.length - 2; i++) {
    if (i > 0 && nums[i] === nums[i - 1]) continue
    let l = i + 1, r = nums.length - 1
    while (l < r) {
      const sum = nums[i] + nums[l] + nums[r]
      if (sum === 0) {
        res.push([nums[i], nums[l], nums[r]])
        while (l < r && nums[l] === nums[l + 1]) l++
        while (l < r && nums[r] === nums[r - 1]) r--
        l++; r--
      } else sum < 0 ? l++ : r--
    }
  }
  return res
}

3Sum Dart

dart

List<List<int>> threeSum(List<int> nums) {
  nums.sort();
  final res = <List<int>>[];
  for (int i = 0; i < nums.length - 2; i++) {
    if (i > 0 && nums[i] == nums[i - 1]) continue;
    int l = i + 1, r = nums.length - 1;
    while (l < r) {
      int sum = nums[i] + nums[l] + nums[r];
      if (sum == 0) {
        res.add([nums[i], nums[l], nums[r]]);
        while (l < r && nums[l] == nums[l + 1]) l++;
        while (l < r && nums[r] == nums[r - 1]) r--;
        l++; r--;
      } else {
        sum < 0 ? l++ : r--;
      }
    }
  }
  return res;
}

9. 接雨水(困难・面试压轴)

题目描述

给定高度数组,计算能接多少雨水。

思路

预处理左右最大高度数组,每个位置存水量:min(leftMax[i], rightMax[i]) - height[i]

JS

javascript

运行

var trap = function(height) {
  const n = height.length
  if (n <= 2) return 0
  const leftMax = new Array(n)
  const rightMax = new Array(n)
  leftMax[0] = height[0]
  rightMax[n - 1] = height[n - 1]

  for (let i = 1; i < n; i++) leftMax[i] = Math.max(leftMax[i - 1], height[i])
  for (let i = n - 2; i >= 0; i--) rightMax[i] = Math.max(rightMax[i + 1], height[i])

  let water = 0
  for (let i = 0; i < n; i++) {
    water += Math.min(leftMax[i], rightMax[i]) - height[i]
  }
  return water
}

Dart

dart

int trap(List<int> height) {
  int n = height.length;
  if (n <= 2) return 0;
  final leftMax = List.filled(n, 0);
  final rightMax = List.filled(n, 0);
  leftMax[0] = height[0];
  rightMax[n - 1] = height[n - 1];

  for (int i = 1; i < n; i++) {
    leftMax[i] = max(leftMax[i - 1], height[i]);
  }
  for (int i = n - 2; i >= 0; i--) {
    rightMax[i] = max(rightMax[i + 1], height[i]);
  }

  int water = 0;
  for (int i = 0; i < n; i++) {
    water += min(leftMax[i], rightMax[i]) - height[i];
  }
  return water;
}

复杂度

O(n) / O(n)


10. 跳跃游戏系列(中等・贪心)

包含 I(能否跳到终点)+ II(最小步数)

跳跃游戏 I JS

javascript

运行

var canJump = function(nums) {
  let maxReach = 0
  for (let i = 0; i < nums.length; i++) {
    if (i > maxReach) return false
    maxReach = Math.max(maxReach, i + nums[i])
    if (maxReach >= nums.length - 1) return true
  }
  return true
}

跳跃游戏 II JS

javascript

运行

var jump = function(nums) {
  let step = 0, end = 0, maxReach = 0
  for (let i = 0; i < nums.length - 1; i++) {
    maxReach = Math.max(maxReach, i + nums[i])
    if (i === end) {
      end = maxReach
      step++
    }
  }
  return step
}

Dart 双实现

dart

bool canJump(List<int> nums) {
  int maxReach = 0;
  for (int i = 0; i < nums.length; i++) {
    if (i > maxReach) return false;
    maxReach = max(maxReach, i + nums[i]);
    if (maxReach >= nums.length - 1) return true;
  }
  return true;
}

int jump(List<int> nums) {
  int step = 0, end = 0, maxReach = 0;
  for (int i = 0; i < nums.length - 1; i++) {
    maxReach = max(maxReach, i + nums[i]);
    if (i == end) {
      end = maxReach;
      step++;
    }
  }
  return step;
}