☰
数组筛选与处理实战:从基础API到底层机制
2026/9/26 6:04:48 网站建设 项目流程

标题看着简单,实际是面试题和日常开发里最容易被问穿的角落。数组的筛选与处理,往浅了说是循环加判断,往深了说牵扯到各语言的底层数据结构、内存布局、排序与去重的取舍、区间查询的算法设计,甚至多维数组的指针语义。这篇我不会只讲某一种语言的语法,而是把“数组筛选与处理”这件事拆开揉碎,覆盖 Python、JavaScript、C/C++、Java、Kotlin 等主流场景,结合我在实际项目中踩过的坑和用过的解法,整理成一份能直接参考的干活笔记。

1. 内容整体设计与思路拆解

1.1 “筛选”和“处理”到底在做什么

数组筛选,本质是在一个集合里按条件选出子集;数组处理,是把选出的结果进一步变换、排序、去重、转结构,或者反过来利用筛选结果影响原数组。两个动作经常连在一起用,但思考时必须分开:筛选解决“留下谁”,处理解决“留下之后怎么用”。

我举个生活中的例子。筛面粉是把大颗粒杂质挡在外面,留下细粉;之后加水揉面是处理。如果把“杂质”和“细粉”的判定条件写错,后面再怎么揉面都是白费。编程里也一样,筛选条件错了,后续所有统计、渲染、落库都是错的。所以做数组操作时,我习惯先问自己三个问题:条件是什么类型(等值、范围、正则、是否包含)?结果要新数组还是原数组原地改?后续还要不要再聚合或排序?

这三个问题定了,选 API 就是水到渠成的事。比如 JavaScript 里面filter出新数组,splice原地删;Python 里列表推导式生成新列表,sort原地排;C 语言里没人帮你,只能自己写循环加临时数组或者原地覆盖。语言不同,思路一致。

1.2 为什么很多方案最后都绕不开“条件表达”

你会发现一个规律:数组筛选的代码量,往往不取决于数组本身,而取决于条件怎么表达。[1,2,3,4,5].filter(x => x % 2 === 0)一行搞定取偶数,可一旦条件变成“找出连续三天成交量递增的股票”,这就不是单一元素筛选,而是对相邻元素关系、窗口跨度的综合判断。

实际项目里,筛选条件经常是复合的:多字段匹配、模糊搜索、区间判断、排除空值。把这些条件组合清楚,比背 API 重要得多。我建议把条件断言单独抽成函数或 lambda,方便单测和复用,这也是很多现代语言里predicate概念的本义。另一个关键点是“筛选后要不要保留原顺序”。数组本身有序,筛选一般也应保持原序,除非你明确要排序后再筛。

1.3 一次筛选涉及的典型技术面

从热搜词里能看到,这个话题实际覆盖了至少四层:基础 API(filter、slice、推导式)、算法(去重、排序、求区间最大值)、数据结构(树状数组、动态数组、指针数组)、以及跨语言对比(C++ 指针数组、Kotlin 增加元素、PHP 接口返回对象数组)。每一层都不是孤立的知识点,而是数组处理链路中的一个环节。后面每一章我会按语言和场景展开,并在最后做一份问题排查速查表。

2. 各语言核心实现与实操要点

2.1 Python:切片与条件筛选的黄金搭档

Python 的列表切片是我用得最多的筛选工具。基本形式arr[start:stop:step],左闭右开,索引从 0 开始。比如arr[1:5]取索引 1 到 4 的元素,arr[::2]取偶数位,arr[::-1]倒序。注意切片返回的是新列表,原列表不动,除非你直接赋值给切片位置,比如arr[2:5] = [0,0,0]这是原地替换。

条件筛选的场景,列表推导式比filter更直观:

# 选出大于 10 的元素并乘以 2 arr = [3, 12, 7, 18, 5] result = [x * 2 for x in arr if x > 10] # [24, 36]

文本匹配场景可以配合正则或者in操作:

words = ["apple", "banana", "cherry", "avocado"] # 筛选包含 "an" 的元素 result = [w for w in words if "an" in w] # ['banana']

实际工作中我常遇到一个坑:列表推导式里如果同时在if前写x if condition else y,容易把筛选和变换搞混。要记住for前面是最终要输出的表达式,for后面的if才是筛选条件。两者互不干扰,但结构上要分开看。

另外,Python 里筛选汉字相关的场景,比如热搜词提到的“创建数组元素为汉字”,本质上就是用列表存字符串,筛选时注意编码已经是 Unicode,直接比较就行,不用处理乱码问题。

2.2 JavaScript:filter、map、reduce 组合拳

JS 数组筛选最常用的是filter,它返回一个新数组,回调返回true的元素保留下来。

const arr = [1, 2, 3, 4, 5]; const evens = arr.filter(x => x % 2 === 0); // [2, 4]

map负责变换,reduce负责聚合。组合起来可以处理复杂业务。热搜词里的“js数组排序的几种方法”要提一下,排序本身也能作为筛选中“取前几名”的预处理手段:

// 取出成绩前 3 名的学生 const students = [{name: 'A', score: 88}, {name: 'B', score: 95}, {name: 'C', score: 70}]; const top3 = students.sort((a, b) => b.score - a.score).slice(0, 3);

注意sort是原地排序,默认按字符串 Unicode 码点排序,所以数字排序必须传比较函数。这个坑很多人踩过:[10, 9, 100].sort()得到[10, 100, 9],因为字符串比较时 "100" 排在 "9" 前面。处理“js数组删除指定元素”时,splice原地删,要传索引;如果不知道索引,可以先indexOf找到,或者用filter生成新数组。我建议业务代码尽量用filter生成新数组,避免原地修改带来的引用问题,在 React 等框架状态管理里尤其重要。另外,“es6+提取数组对象一部分”对应的是解构和展开符。最常见的是提取某个属性:arr.map(o => o.name),或者用解构排除某个字段:

const {password, ...safeUser} = user;

展开符...还可以做数组合并和浅拷贝,合并去重场景要到第 3 章细说。“数组方法”和“js数组的所有方法”如果刷一遍 MDN 文档,重点掌握slice、splice、concat、indexOf、includes、find、every、some,日常开发就够用了。最后提一个“电脑筛选状态下复制 重新应用后粘贴”对应的场景知识,来自 Excel 操作:Excel 筛选时如果直接复制粘贴,会把隐藏行也带进来,必须用快捷键定位可见单元格,或者重新应用筛选后再粘贴。Web 端表格筛选插件(比如 MSFlexGrid)也一样,筛选状态和复制粘贴之间要留意“可见行与数据行索引不一致”的问题。

2.3 C/C++:指针数组、多维数组与筛选的底层逻辑

C 语言没有内置 filter,所有筛选都得自己写循环。但正因为亲手写,反而更容易理解内存布局和指针语义。

先看基础版筛选:

#include <stdio.h> int main() { int arr[] = {3, 12, 7, 18, 5}; int result[5]; int count = 0; for (int i = 0; i < 5; i++) { if (arr[i] > 10) { result[count++] = arr[i]; } } // 前 count 个元素是筛选结果 }

这段代码很朴素,但有个隐患:result长度和原数组一样大,如果原数组有 10000 个元素,就浪费内存。工程上更常见的是先用一次遍历数出符合条件的个数,再动态分配内存:

int count = 0; for (int i = 0; i < n; i++) { if (arr[i] > 10) count++; } int *result = (int *)malloc(count * sizeof(int));

这也引出了动态数组的必要性。C++ 里std::vector的扩容机制是倍增策略:当容量不够时,通常扩展为原来的 1.5 倍或 2 倍,把旧数据拷贝到新内存再释放旧内存。理解了这一点,就知道为什么频繁往 vector 尾部 push_back 反而效率不低——摊还后每次插入是 O(1)。热搜词里的“c++ 数组扩充”本质上就是 vector 的扩容思想,自己实现时要注意内存拷贝和释放,避免内存泄漏。

“指针数组存放字符串”是另一个经典问题。指针数组的每个元素是一个指向字符串的指针,常用于处理一组不固定长度的字符串:

const char *names[] = {"Alice", "Bob", "Charlie"}; // names[1] 是指向 "Bob" 的指针

注意这里的字符串字面量存在只读区,试图修改names[0][0]会导致未定义行为。如果想修改,就要用二维字符数组:char names[][20],每一行固定长度。热搜词“二维数组”“二维字符数组”都在说这件事。二维数组在 C/C++ 里本质是一维数组的数组,内存是连续的。int a[3][4]在内存里是按行存储的 12 个 int,a[i][j]等价于*(*(a+i)+j)。这里有个常见的指针坑:int a[3][4]的数组名a类型是int (*)[4],不是int*。传给函数时,形参要写成int (*a)[4]或int a[][4],不能直接写int a[][],否则编译器不知道每一行有多宽。“c语言数组变量的类型转换”也跟这个有关。数组名在某些表达式里会退化为指向首元素的指针,但sizeof不会退化,&arr的类型是“指向整个数组的指针”。这些内容单独写能写几千字,但核心记住一条:数组和指针是两个不同的类型,只有在传参和特定表达式里才表现出等价性。另外,“c++ 多维数组 指针”还涉及指向多维数组的指针的加减运算,p+1跳过的是一整行而不是一个元素。我之前 debug 时浪费过不少时间,就是因为把int (*)[4]当成int*来算偏移。建议大家在 C++ 里优先用std::array或 vector 嵌套代替裸多维数组,除非对性能有极端要求。

“宏定义数组”在 C/C++ 里通常是定义大小或初始化值:

#define MAX_SIZE 100 int arr[MAX_SIZE];

宏是预处理阶段替换,不占变量存储,适合定义数组大小和常量。注意宏没有类型检查,C++ 里更推荐constexpr int MAX_SIZE = 100;。

“筛选法求素数”正好用到了布尔数组标记,这是数组筛选的经典算法。埃拉托斯特尼筛法的核心思路:

  1. 初始化一个布尔数组isPrime[0..n],全部为 true。
  2. 从 2 开始,如果isPrime[i]为 true,就把所有 i 的倍数(从 i*i 开始)标记为 false。
  3. 最后所有仍为 true 的下标就是素数。
void sieve(int n) { bool isPrime[n + 1]; memset(isPrime, true, sizeof(isPrime)); isPrime[0] = isPrime[1] = false; for (int i = 2; i * i <= n; i++) { if (isPrime[i]) { for (int j = i * i; j <= n; j += i) { isPrime[j] = false; } } } }

从i*i开始而不是i*2,是因为更小的倍数已经被更小的素数筛掉了。这算是一个小优化,但对理解“数组筛选的本质是标记状态”很有帮助。热搜词“树状数组模板”则属于区间处理的进阶结构。树状数组(Fenwick Tree)支持单点更新和前缀和查询,复杂度都是 O(log n)。它利用lowbit(i) = i & -i控制跳跃区间。模板通常很短:

int n; int bit[N]; void add(int idx, int delta) { while (idx <= n) { bit[idx] += delta; idx += idx & -idx; } } int query(int idx) { int sum = 0; while (idx > 0) { sum += bit[idx]; idx -= idx & -idx; } return sum; }

很多算法题里“数组求区间最大值的算法题”可以用线段树或 ST 表,但如果只求前缀和,树状数组是最小的实现成本。“三个数组最大的乘积”这类题则是把筛选和排序结合了,后面单独讲。

2.4 Java/Kotlin:集合 API 的场景化应用

Java 的数组和集合是两个体系。原始数组用Arrays工具类,集合用Stream。热搜词里“java 数组的所有方法”其实没有“所有方法”这种说法,数组本身没有方法,方法都在Arrays、System和集合类里。

Java 8 引入 Stream 后,筛选和处理变得很优雅:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); List<Integer> even = list.stream() .filter(x -> x % 2 == 0) .map(x -> x * 2) .collect(Collectors.toList());

注意toList()在 Java 16 才出现,之前用Collectors.toList()。“group()+数组java”这个热搜词其实是在说Collectors.groupingBy按字段分组:

Map<String, List<User>> grouped = users.stream() .collect(Collectors.groupingBy(User::getCity));

这算“处理”的高级形态——把数组转成了分组 map,后续按城市遍历就很方便。

Java 里数组转字符串用Arrays.toString(arr),多维数组用Arrays.deepToString(arr)。字符串转数组用split。对象数组去重用distinct(),注意对象要重写 equals/hashCode,否则去重无效。Kotlin 这边数组增加一项,最简单的是用plus:

val arr = intArrayOf(1, 2, 3) val newArr = arr + 4 // [1, 2, 3, 4]

注意 Kotlin 的+返回新数组,不修改原数组。如果是集合,更推荐toMutableList().add(),或者直接用listOf加+。“kotlin 给数组增加一项”本质上是在可变和不可变之间做选择,Kotlin 的设计哲学倾向不可变,所以优先返回新集合而不是原地改。

还有一个 Kotlin/C++ 混合场景:qt 窗体间引用数组const (&double [10]),如何接收赋值。这是在说 Qt 信号槽或函数参数里传 C++ 数组引用。形参写成const double (&arr)[10],这是“数组的引用”,可以避免数组退化为指针,函数内部能拿到完整的长度信息。实参可以直接传数组名。如果你写const double *arr,虽然也能用,但丢失了长度信息,必须额外传 size。这又是一个数组语义和指针语义纠缠的例子。

3. 典型场景实操:去重、排序、切片、区间问题

3.1 数组去重的多种方案对比

去重应该是数组处理里出现频率最高的需求之一。不同语言、不同场景要选不同方案。我在项目里归纳过一张对照表:

场景推荐方式复杂度注意点
JS 基础类型Array.from(new Set(arr))O(n)仅对 number/string/boolean 有效
JS 对象数组Map+ 唯一标识字段O(n)要自己指定去重键
JS 合并去重[...new Set([...a, ...b])]O(n)先合并再 Set
Python 基础类型list(dict.fromkeys(arr))O(n)Python 3.7+ 字典保序
Python 对象自定义 key 函数 + 循环O(n)用 set 记录已见 key
Java Streamstream().distinct()O(n)依赖 equals/hashCode
C 语言排序后相邻去重O(n log n)需要排序,原顺序丢失
SQL 层面DISTINCT/ GROUP BY—数据库去重,非数组讨论范围

JS 对象数组去重很典型:

const users = [ {id: 1, name: 'A'}, {id: 2, name: 'B'}, {id: 1, name: 'A'}, ]; const seen = new Map(); const unique = users.filter(u => { if (seen.has(u.id)) return false; seen.set(u.id, true); return true; }); // 按 id 去重

这里有个观点:不要迷信“一行代码去重”。Set 对数字和字符串确实快,但遇到对象数组,你必须先定义“重复”的标准是什么,否则去重结果和你预期不一致。我在评审代码时经常问一句话:“你是按哪个字段判重?”对方答不上来,说明需求本身就模糊,代码写得再花哨也没用。

Python 的场景,热搜词“python筛选一样的”应该就是在问这个。用字典保序去重是目前最稳妥的方案:

arr = [3, 1, 3, 2, 1] result = list(dict.fromkeys(arr)) # [3, 1, 2]

如果不在意顺序,用 set 就行,但多数业务场景要求保持原顺序,所以 dict 方案更实用。

3.2 排序与筛选的先后顺序怎么定

排序和筛选经常连着做,但顺序错了,结果可能天差地别。假设你要“取出前三大的数”,正确做法是排完序取前三个?还是先筛出大于某个阈值的再排序?没有标准答案,取决于你要“前三大的数”还是“大于阈值且排序的数”。

“三个数组最大的乘积”是 LeetCode 风格题。三个最大数的乘积并不一定是三个最大正数的乘积,因为可能有两个很大的负数乘一个正数,负负得正更大。所以要同时考虑最大的三个数和最小的两个数:

def maximumProduct(nums): nums.sort() return max(nums[-1] * nums[-2] * nums[-3], nums[0] * nums[1] * nums[-1])

这题的启示是:数组处理里的“最大最小”往往要看符号,不能只看单一维度。筛选条件里含有负数的场景,大脑要自动画一条数轴,而不是只看排序结果。

日常开发里,“js数组排序的几种方法”也可以和筛选结合:

  • sort((a,b) => a - b)升序
  • sort((a,b) => b - a)降序
  • 先filter筛掉不符合条件的,再sort,最后slice取分页数据

分页取数的场景,slice((page-1)*size, page*size)必须放在排序之后,否则分页结果乱序。排序最好放在筛选之后,因为筛选会减少元素数量,排序开销更低;但如果筛选条件依赖“全局排序后的位置”(比如取排名前 10),那就必须全排完再筛再取。

3.3 Excel、数据库与编程数组的交叉场景

热搜词里出现了不少 Excel 和数据库相关的关键词,比如“excel多条件筛选”“excel 提取前两列匹配的数据成一个数组”“dbeaver怎么筛选重复项”。这些场景背后的逻辑其实和编程数组一致,只是操作载体不同。

Excel 多条件筛选可以用高级筛选,或者用公式生成数组。例如要提取 A、B 两列中匹配条件的行,形成新的数组,可以用FILTER函数(Excel 365):

=FILTER(A2:C100, (A2:A100="条件1") * (B2:B100="条件2"))

乘号表示 AND,加号表示 OR。这个公式的结果是一个动态数组,会在多个单元格溢出显示。老版本 Excel 则要按Ctrl+Shift+Enter输入数组公式。

我用 Excel 处理数据时最大的感悟是:Excel 的“筛选”和编程的“筛选”是同一套思维方式,只是一个是 GUI 操作,一个是代码逻辑。你在 Excel 里点“筛选”按钮,本质就是在列方向上施加条件,隐藏不满足的行;代码里filter就是遍历元素,跳过不满足条件的分支。理解了这个共性,你在 Excel 和代码之间切换会很顺手。

DBeaver 里筛选重复项,本质是 SQL 的GROUP BY加HAVING COUNT(*) > 1:

SELECT column_name, COUNT(*) FROM table_name GROUP BY column_name HAVING COUNT(*) > 1;

这比把所有数据拉下来到程序里再去重要高效得多,因为数据库索引和聚合帮我们做了大量工作。这个思路也提醒我们:数组筛选虽然方便,但如果数据量达到几万行以上,尽量把筛选逻辑下沉到数据库或服务端,不要在前端或脚本里全量拉取再过滤。

“msflexgrid实现筛选功能”是 VB6 时代的经典控件需求。思路是遍历网格行,按条件设置行的 Visible 属性。原理和 Excel 隐藏行一样,但可见行和真实行索引会错位,取数据时要用数据源的原始索引,不能用网格行号。这又是一个“筛选副作用”的例子。

3.4 二维数组与多列提取的实战

热搜词里“matlab数组+取出多列”“二维数组”都属于同一类场景。在 MATLAB 里取多列非常直观:

A = [1 2 3; 4 5 6; 7 8 9]; B = A(:, [1, 3]); % 取第 1 列和第 3 列

:表示所有行,[1,3]是列索引。换成 Python NumPy 是:

import numpy as np A = np.array([[1,2,3],[4,5,6],[7,8,9]]) B = A[:, [0, 2]] # 第 0 列和第 2 列

注意行列索引都从 0 开始,而 MATLAB 从 1 开始。跨语言最容易踩这种索引基准的坑。

“numpy三维数组相乘”是另一个典型问题。三维数组本质是多个二维矩阵叠加,乘的时候要确定是逐元素乘(*)还是矩阵乘(@)。逐元素乘要求形状一致或能广播,矩阵乘则要求最后两维满足矩阵乘法条件,前面维度做 batch 处理。实际使用中我建议尽量保持数据是三维以下的数组,超过三维就用np.einsum或明确说明维度含义,否则 debug 时你根本分不清谁乘谁。

热搜词里有“python数组切片”,这个在第 2.1 节已经讲了基础。再补充一个实际经验:切片返回视图还是副本,在 NumPy 里有严格区别。普通 Python 列表切片返回新列表;NumPy 数组切片返回视图,切片操作会共享底层数据,修改视图会影响原数组。如果不想影响原数组,必须显式.copy()。这个差异常被忽视,结果就是查 bug 发现“我没动原数组,它怎么变了”。记住:NumPy 切片是引用,Python 列表切片是复制。

3.5 动态数组的扩容与性能取舍

热搜词“动态数组”“c++ 数组扩充”“数组初始化规则”都指向内存管理与生命周期。动态数组的核心问题不是“怎么加元素”,而是“加到什么时候需要扩容”。

C++std::vector的扩容策略不同实现有差异,常见的是 2 倍或 1.5 倍。倍增能保证摊还插入 O(1),但代价是瞬间占用两份内存。假设 vector 里面有 1 亿个 8 字节元素,容量到 1 亿时再插入会临时申请 2 亿元素的内存,内存峰值 1.6GB,机器不够直接 OOM。这时可以选择先用reserve预估大小,或者自定义deque结构避免“连续内存 + 频繁扩容”的问题。

Kotlin 的plus返回新数组,每次加一个元素都是 O(n) 拷贝。如果循环里频繁加元素,性能会非常差,应该先转MutableList,最后再转回数组。JS 的push是原地操作,均摊 O(1),没有这个问题;Python 列表同理。所以跨语言性能对比不能只看语法简洁度,要看底层实现。

数组初始化规则也值得多说一句。C 语言里未初始化的局部数组是随机值,全局数组是 0。int arr[10] = {0}只能把第一个元素显式设为 0,其余元素因为“部分初始化,剩余补 0”的规则也都变成了 0,这是合法的写法,但容易引起误解。C++ 里int arr[10] = {}全部初始化为 0,std::array<int, 10> arr{}同理。Java 里int[] arr = new int[10]默认全部是 0,boolean 默认 false,String 默认 null。搞清楚这些默认值,才不会在筛选条件里出现“把未初始化的垃圾值筛进来”的 bug。

4. 常见问题与排查技巧实录

4.1 筛选后数据丢失或错位的原因排查

我在真实项目里经常遇到筛选结果和预期不一致,总结下来最常见是这么几种:

  1. 排序函数没传比较函数(JS 经典坑)。
  2. 筛选条件多字段时用错了与或逻辑。
  3. 对象引用去重时比较的是地址而不是值。
  4. 切片边界写错(Python 是左闭右开,arr[1:5]不包含索引 5 的元素)。

排查套路:先把筛选结果打印出来,和原始数据手动比对几条,确认是条件问题还是数据问题。如果筛选结果数量是对的,但内容不对,大概率是条件表达式写错了;如果数量都不对,可能是边界或数据结构问题。

4.2 跨语言数组操作对照速查表

操作PythonJavaScriptJavaC/C++
筛选[x for x in arr if cond]arr.filter(x => cond)stream().filter()手写循环
切片arr[1:4]arr.slice(1,4)Arrays.copyOfRange()循环拷贝
排序sorted(arr)返回新,arr.sort()原地arr.sort((a,b)=>a-b)原地Arrays.sort(arr)std::sort
去重list(dict.fromkeys(arr))[...new Set(arr)]stream().distinct()排序+相邻去重
反转arr[::-1]arr.reverse()Collections.reverse()std::reverse
转字符串','.join(map(str, arr))arr.join(',')Arrays.toString(arr)手写循环或 stringstream
删除元素列表推导式重新赋值splice(idx,1)List.remove(idx)手写移动元素

这张表帮我对齐思维,不会因为切换语言而犯低级错误。

4.3 真实项目中的三个排查案例

第一个案例:一次用 JS 写表格筛选,发现筛选后的行和表格原来的行位置错位。原因是表格组件内部维护了“筛选项勾选状态”的数组,去重时用了new Set(items),但 items 里是对象,Set 按引用去重,两个内容相同但引用不同的对象被认为不同,导致筛选结果重复。解决方案:把去重键改成item.id字符串,先map再Set,最后再映射回对象。

第二个案例:写 Python 数据处理脚本时,从 CSV 读入的年龄列有空格,比如" 23 ",筛选条件age > 18报错。原因是最初没做类型转换,字符串和数字比较在 Python 3 里会直接抛异常。解决:读取后先strip()再int()。这个坑太常见了,凡是外部输入的数据源,先做数据清洗再做筛选,否则一步错步步错。

第三个案例:C++ 里动态数组扩容后忘记释放旧缓冲区,导致内存泄漏。排查时用了 valgrind,定位到realloc失败,因为自己写的扩容逻辑里,申请新内存前先free了旧指针,然后memcpy时崩溃。正确的做法是先申请新内存,再拷贝,再释放旧内存。

第四个案例:Excel 筛选状态下复制粘贴多算了数据。这个我在第 2.2 节提过,Windows 下 Excel 筛选后必须先用Alt+;定位可见单元格再复制,不然隐藏行也会被复制。如果本意是想筛选后重新计算再粘贴,最好先“重新应用”筛选条件,再复制可见区域。

4.4 面试与算法题里的实战提醒

热搜词里“数组求区间最大值的算法题”和“树状数组模板”结合起来,基本就是面试题库的常客。区间最大值有几种解法:

  • 静态数组多次查询:ST 表,预处理 O(n log n),查询 O(1)。
  • 动态更新加查询:线段树,更新和查询都是 O(log n)。
  • 只求前缀和/前缀最值:树状数组最简单。

我给初学者一个建议:不要背模板,要把 lowbit 和线段树每个节点的含义先讲清楚,能讲明白“为什么 query 循环要减去 lowbit”才算真会。我当时理解树状数组,就是画一棵节点图,把i += lowbit(i)和i -= lowbit(i)的路径走一遍,自然而然就记住了。

另一个高频题是“数组去重”,有时候会限制“不使用额外空间”,那就只能在原地做:先排序,再用双指针覆盖重复元素。这个解法空间 O(1),时间 O(n log n)。我面试时比较看重候选人能不能从 O(n) 空间过渡到 O(1) 空间,这反映的是对“数据规模与资源约束”的敏感度。

4.5 实际问题速查表

最后整理一个帮助定位问题的速查表:

问题症状可能原因解决方向
筛选数量比预想少条件多字段用了and而应该是or检查条件组合
筛选数量比预想多没排除空值或重复值先清洗再筛选
筛选结果顺序乱了排序函数没传比较器,或默认按字符串排序检查 sort 参数
对象去重无效比较的是引用而不是字段值指定去重键
Python 切片结果不对把 stop 当成包含项记住左闭右开
NumPy 切片改了原数组切片返回视图不是副本显式调用 copy()
C 数组长度丢失数组退化为指针传长度或使用 std::array
Java 去重无效没重写 equals/hashCode重写或自定义 key
Kotlin plus 性能差每次循环调用 plus 都建新数组使用 MutableList
Excel 复制多数据筛选状态未生效定位可见单元格再复制

5. 我个人在实际操作中的一点体会

数组筛选与处理是我日常工作里打交道最多的操作之一,从最早用 C 语言手写冒泡和筛选,到后来 Python 一行列表推导式搞定,再到用 Spark 处理千万级数据,这个过程最大的心得是:语言和框架会变,但“条件—变换—聚合”这条主线永远不变。不管你在哪个生态,先想清楚“筛选条件是什么、结果的形态是什么”,再去查对应 API,通常都不会走太远。

踩过的坑多了,我现在写数组操作时反而会很保守:能用不可变方法就不原地改,能显式写类型就不靠隐式推导,能加断言就加断言。筛选看似简单,却是代码里最容易埋雷的地方,条件写错一个符号,上游数据全乱。最后再分享一个小技巧:在写任何筛选逻辑之前,先写下三行注释——输入是什么、筛选条件是什么、输出是什么。这三行注释写清楚,代码基本不会出错,这也是我评审代码时让团队所有人养成的好习惯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询