一道经典面试题,七种实现思路,带你理解时间复杂度的演进
为什么需要数组去重?
日常开发中,我们经常遇到这样的场景:
- 前端处理后端返回的数据,需要去掉重复的选项
- 用户操作产生的一系列 ID,需要过滤重复项
- 数据统计前需要清洗重复值
数组去重虽然基础,但不同的实现方式体现了对 JavaScript 语言理解的深度。
代码是写给人看的,只是顺便能在机器上运行
实际开发中,一段代码往往会被多个人维护。过了一周、一个月,你或者同事可能早就忘了当初为什么这么写。
好注释的价值:
- 有利于协作 ------ 开发者和使用者不是一个人
- 防止遗忘 ------ 注释是代码的一部分
- 提高可读性 ------ 看注释就能理解意图
代码层面的原则:
- 一个函数只做一个功能
- 复杂逻辑要封装
- 函数要有健壮性,主动检验参数
每次第一步:参数校验
JavaScript 是动态弱类型语言,调用者可能传进来任何东西:数组、字符串、数字、甚至是 null。
参数校验是最容易被忽略的"基础功":
javascript
javascript
// 错误示范:没有校验,直接使用
function unique(arr) {
return [...new Set(arr)]; // 如果 arr 不是数组,直接报错
}
基础版本:双重循环
最直观的思路:维护一个新数组,遍历原数组,判断每个元素是否已经存在于新数组中。
javascript
ini
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
let res = [arr[0]];
for (let i = 1; i < arr.length; i++) {
let flag = true;
for (let j = 0; j < res.length; j++) {
if (arr[i] === res[j]) {
flag = false;
break;
}
}
if (flag) {
res.push(arr[i]);
}
}
return res;
}
console.log(unique([1, 2, 3, 2, 5])); // [1, 2, 3, 5]
时间复杂度 :O(n²),两层循环嵌套。
缺点:数据量大时性能较差。
改进一:利用 indexOf
用 indexOf 替代内层循环,代码更简洁,但时间复杂度仍是 O(n²)。
javascript
ini
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
const res = [];
for (let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1) {
res.push(arr[i]);
}
}
return res;
}
改进二:filter + indexOf
利用数组的 filter 方法,代码更函数式。核心逻辑:当前元素第一次出现的位置等于当前位置才保留。
javascript
javascript
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}
注意 :indexOf 会返回元素第一次出现的位置,如果当前索引不是第一次出现,说明是重复项,被过滤掉。
改进三:排序后去重
先排序,然后比较相邻元素是否相同。
javascript
ini
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
arr = arr.sort();
let res = [arr[0]];
for (let i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i - 1]) {
res.push(arr[i]);
}
}
return res;
}
console.log(unique([1, 2, 5, 3, 2])); // [1, 2, 3, 5]
时间复杂度 :O(n log n),主要来自排序。
注意:排序会改变元素顺序,如果不需要保持原顺序,这是不错的选择。
改进四:空间换时间 ------ 对象字面量
利用对象属性的唯一性,将元素作为对象的 key,实现 O(n) 时间复杂度。
javascript
ini
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
const res = [];
const obj = {};
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
res.push(arr[i]);
obj[arr[i]] = 1;
} else {
obj[arr[i]]++;
}
}
return res;
}
时间复杂度 :O(n)
空间复杂度:O(n),用额外的对象存储已出现的元素。
这种方式是典型的"用空间换时间"策略。
改进五:Set ------ 最优雅的解法
ES6 新增的 Set 数据结构,天然保证元素不重复。一行代码解决问题。
javascript
javascript
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error');
return [];
}
return [...new Set(arr)];
}
console.log(unique([1, 2, 5, 3, 2])); // [1, 2, 5, 3]
原理 :Set 类似于数组,但成员的值都是唯一的。展开运算符 ... 将 Set 转回数组。
时间复杂度 :O(n)
适用性:现代浏览器和 Node.js 环境均可使用。
方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 是否保持顺序 | 代码复杂度 |
|---|---|---|---|---|
| 双重循环 | O(n²) | O(n) | ✅ | 高 |
| indexOf | O(n²) | O(n) | ✅ | 中 |
| filter+indexOf | O(n²) | O(n) | ✅ | 低 |
| 排序去重 | O(n log n) | O(n) | ❌ | 中 |
| 对象标记 | O(n) | O(n) | ✅ | 中 |
| Set | O(n) | O(n) | ✅ | 最低 |
开发建议
- 日常开发 :首选
Set方式,简洁且性能好 - 需要兼容旧浏览器:用对象标记法
- 不在意顺序:排序去重也是好选择
- 务必做参数校验:判断是否为数组,增强代码健壮性
javascript
javascript
// 健壮性示例
if (!Array.isArray(arr)) {
throw new TypeError('Expected an array');
}
总结
从双重循环到 Set,体现了编程思想的演进:
- 学会用语言提供的 API(indexOf、filter)
- 理解不同数据结构的特性(对象、Set)
- 懂得权衡时间与空间
- 关注代码的可读性与健壮性
一道简单的面试题,其实考察了很多底层能力。你学会了吗?