1966 words
10 minutes
Page views--Visits--
秋招算法高频精讲:双指针与有序数组求交集深度复盘(含极端数据与工程追问)

秋招算法高频精讲:双指针与有序数组求交集深度复盘(含极端数据与工程追问)#

Author#

KardeniaPoyu · Blog


📌 真实面试背景#

值得买科技 等互联网与游戏大厂的算法与工程岗技术面试中,面试官经常以看似简单的基础题作为引子,层层设套考察应聘者的边界处理能力、复杂度敏感度、极端规模优化意识(MNM \ll N以及海量数据工程落地思维(大数据外排序)

面试真题还原
给定两个单调递增的有序数组 AA(长度 MM)和 BB(长度 NN),数组中可能存在重复元素,要求找出两数组中数值相同的公共元素集合(下标可以不同)。

本文将从最朴素的双指针算法出发,剖析 4 种演进方案,并深入探讨 C++ STL 内部实现与海量数据外存流式处理。


目录#


一、 算法一:标准双指针扫描法(Baseline)#

1. 核心数学原理#

由于数组 AABB 均已升序排列,数组元素具有严格的单调性: i1<i2    A[i1]A[i2]\forall i_1 < i_2 \implies A[i_1] \le A[i_2]

利用此性质,我们可以维护两个指针 iijj,分别从 A[0]A[0]B[0]B[0] 出发:

  • A[i]==B[j]A[i] == B[j]:找到一个公共值,记录该值,随后 i++,j++i++, j++
  • A[i]<B[j]A[i] < B[j]:说明当前 A[i]A[i] 小于 B[j]B[j]BB 中后续所有元素,在 BB 中绝无匹配可能,必须右移 iii++i++)。
  • A[i]>B[j]A[i] > B[j]:同理,B[j]B[j] 小于 A[i]A[i]AA 中后续元素,必须右移 jjj++j++)。
  • 循环终止条件:iMi \ge MjNj \ge N
数组 A: [ 1, 3, 4, 7, 9, 11 ]
↑ i
数组 B: [ 2, 3, 5, 7, 8, 9, 12 ]
↑ j
Step 1: A[0]=1 < B[0]=2 --> i++
Step 2: A[1]=3 == B[0]=3 --> Match 3! i++, j++
Step 3: A[2]=4 < B[1]=5 --> i++
Step 4: A[3]=7 > B[1]=5 --> j++
Step 5: A[3]=7 == B[2]=7 --> Match 7! i++, j++
...

2. 复杂度分析#

  • 时间复杂度O(M+N)\mathcal{O}(M + N)。每个指针至多移动各自数组长度的次数。
  • 空间复杂度O(1)\mathcal{O}(1)(仅需常数级别的指针变量,不计输出数组)。

二、 算法二:极不平衡规模优化——二分查找法(MNM \ll N#

1. 痛点:为什么双指针不是万能的?#

假设 M=10M = 10(短数组),而 N=108N = 10^8(超长数组)。

  • 双指针法:需要逐步推进 jj 指针遍历长数组,运算次数约 10810^8 次。
  • 二分查找优化:遍历短数组 AA 中的每一个元素 A[i]A[i],在长数组 BB 中执行二分查找(Binary Search / std::lower_bound)。
    • 单次二分查找耗时 O(logN)=log2(108)27\mathcal{O}(\log N) = \log_2(10^8) \approx 27 次比较。
    • 1010 个元素总共仅需 10×27=27010 \times 27 = 270 次比较!
    • 加速比超过 370,000 倍!

2. 复杂度#

  • 时间复杂度O(MlogN)\mathcal{O}(M \log N)(当 MlogN<M+NM \log N < M + N,即 MNM \ll N 时显著占优)。
  • 空间复杂度O(1)\mathcal{O}(1)

在二分查找的基础上,如果短数组 AA 中的元素也是递增且分布相对聚集的,我们可以进一步利用上一轮在 BB 中找到的位置作为下界起点,并结合倍增跳跃(Galloping Search)

  1. 寻找查找上界:检查 B[offset+1],B[offset+2],B[offset+4],,B[offset+2k]B[\text{offset} + 1], B[\text{offset} + 2], B[\text{offset} + 4], \dots, B[\text{offset} + 2^k]
  2. 确定区间后,仅在 [2k1,2k][2^{k-1}, 2^k] 这个极小区间内执行二分查找。
  3. 将时间复杂度进一步平摊至 O(MlogNM)\mathcal{O}\left(M \log \frac{N}{M}\right)。著名的 Python / Java 默认排序算法 Timsort 在归并阶段即大量采用了此策略。

四、 算法四:海量数据与外排序流式处理(Memory Constraint)#

1. 极端场景:数组数据无法一次性放入内存#

面试官追问:如果文件 AA 和文件 BB 各有 100GB100\text{GB} 且均已排序存放在磁盘上,而当前服务器物理内存仅有 2GB2\text{GB},如何高效求交集?

2. 流式归并方案(Block-based Streaming Merge)#

由于两个文件本身已经全局有序,我们完全不需要将全量数据加载到内存

  1. 缓冲区开辟:在内存中为文件 AA 开辟输入缓冲区 BufA(如 64MB),为文件 BB 开辟输入缓冲区 BufB(如 64MB),并开辟一个输出缓冲区 BufOut(如 64MB)。
  2. 块级加载:从文件 A,BA, B 各自读取第一块数据至缓冲区。
  3. 双指针流式比对
    • 指针在 BufABufB 内部按算法一的规则比对。
    • BufA 的游标耗尽时,触发磁盘 I/O 顺序读取 AA 的下一块(fread / read)。
    • BufB 的游标耗尽时,读取 BB 的下一块。
    • 匹配到的元素写入 BufOut,满 64MB 批量刷盘(fwrite)。
  4. 性能优势
    • 内存占用严格恒定为 O(1)\mathcal{O}(1)(即 3×64MB=192MB3 \times 64\text{MB} = 192\text{MB})。
    • 磁盘 I/O 全部为最高效的顺序流式读取(Sequential I/O),充分利用 OS Page Cache 与磁盘预读机制。

五、 重复元素处理策略与 STL 源码对比#

1. 去重(Unique Elements Only) vs 保留多重频次(Multi-set Intersection)#

面试中必须主动向面试官确认对于重复元素的定义:

  • LeetCode 349(去重集合交集):两数组若都有多个 3,结果集仅保留一个 3
    • 处理:匹配后,利用 while (i + 1 < M && A[i] == A[i + 1]) i++; 跳过所有重复项。
  • LeetCode 350(多重集合交集):若 AA 有 3 个 3BB 有 2 个 3,结果集保留 min(3,2)=2\min(3, 2) = 23
    • 处理:每次匹配一个元素,两个指针同时向后自增 1(i++,j++i++, j++)。

2. C++ STL std::set_intersection 源码核心剖析#

<algorithm> 头文件中,STL 官方实现即采用了标准双指针设计:

template <class InputIterator1, class InputIterator2, class OutputIterator>
OutputIterator set_intersection(InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, InputIterator2 last2,
OutputIterator result) {
while (first1 != last1 && first2 != last2) {
if (*first1 < *first2) {
++first1;
} else if (*first2 < *first1) {
++first2;
} else {
*result = *first1;
++result;
++first1;
++first2;
}
}
return result;
}

六、 完整 C++ 工业级工程实现#

#include <iostream>
#include <vector>
#include <algorithm>
class ArrayIntersectionSolution {
public:
// 方案一:标准双指针求交集 (保留重复出现的多重频次)
static std::vector<int> intersectTwoPointers(const std::vector<int>& nums1, const std::vector<int>& nums2) {
std::vector<int> result;
size_t i = 0, j = 0;
const size_t m = nums1.size(), n = nums2.size();
while (i < m && j < n) {
if (nums1[i] == nums2[j]) {
result.push_back(nums1[i]);
++i;
++j;
} else if (nums1[i] < nums2[j]) {
++i;
} else {
++j;
}
}
return result;
}
// 方案二:极不平衡数据 (nums1 极小, nums2 极大) -> 二分查找优化
static std::vector<int> intersectBinarySearch(const std::vector<int>& small, const std::vector<int>& large) {
std::vector<int> result;
auto it_start = large.begin();
for (int val : small) {
// 在大数组剩余区间中二分查找大于等于 val 的第一个位置
auto it = std::lower_bound(it_start, large.end(), val);
if (it != large.end() && *it == val) {
result.push_back(val);
it_start = it + 1; // 避免重复匹配同一位置
} else {
it_start = it; // 缩小下一轮二分查找的起点区间
}
if (it_start == large.end()) break;
}
return result;
}
};
int main() {
std::vector<int> A = {1, 3, 4, 7, 9, 11};
std::vector<int> B = {2, 3, 5, 7, 8, 9, 12};
auto res = ArrayIntersectionSolution::intersectTwoPointers(A, B);
std::cout << "Intersection: ";
for (int num : res) {
std::cout << num << " ";
}
std::cout << std::endl; // 输出: 3 7 9
return 0;
}

🎯 总结与面试应答速记卡#

┌──────────────────────────────────────────────┐
│ 递增有序数组寻找相同数值 (求交集) │
└──────────────────────┬───────────────────────┘
┌───────────────────────┴───────────────────────┐
▼ ▼
【常规规模 M ≈ N】 【极端不均衡 M ≪ N】
双指针同向扫描 遍历短数组 A + 在长数组 B 中二分
时间: O(M + N) 时间: O(M log N)
空间: O(1) 空间: O(1)
│ │
└───────────────────────┬───────────────────────┘
【超内存限制 / 磁盘海量数据】
分块流式归并 (Block Streaming Merge)
内存严格 O(1),全顺序磁盘 I/O
秋招算法高频精讲:双指针与有序数组求交集深度复盘(含极端数据与工程追问)
https://blog.apoyu.com/posts/0081/
Author
Kuchina
Published at
2026-08-30

Share Article

Generate a share poster or copy the link to share this article.

Continue reading

Related reading

Based on shared tags and categories

Take another route

A consistent pick from other articles

Comments

Loading comments...