<think>我们需要从数组中删除那些出现超过两次的元素。也就是说,如果某个元素出现的次数大于2,那么我们就删除多余的部分,使得该元素最多只出现两次。
思路:
1. 我们可以遍历数组,同时记录每个元素出现的次数。
2. 在遍历过程中,我们需要判断当前元素已经出现的次数,如果小于2,则保留,并且计数加一;如果已经出现两次或以上,则跳过(即删除)。
但是,由于数组的删除操作(特别是从中间删除)会导致索引变化,通常我们会考虑使用新数组来存储结果,或者从后往前遍历进行删除。
另一种思路是使用两个指针,一个用于遍历,一个用于指向新数组的当前位置,同时利用一个对象(或Map)来记录每个元素出现的次数。
具体步骤:
方法一:使用额外空间(新数组)和哈希表计数
- 创建一个空数组(结果数组)和一个空对象(用于计数)。
- 遍历原数组的每个元素:
如果该元素在对象中的计数不存在或小于2,则将其推入结果数组,并将计数加1(如果不存在则初始化为1)。
如果计数已经达到2,则跳过(即不推入结果数组)。
- 返回结果数组。
方法二:原地修改数组(使用两个指针)
- 使用一个对象记录每个元素出现的次数。
- 使用两个指针:i(遍历指针)和j(指向下一个要保留元素的位置)。
- 遍历数组,对于每个元素:
如果该元素的计数小于2,则将其复制到j位置,同时j向后移动,并将计数加1。
否则,跳过该元素(即只保留最多两次出现的元素)。
- 最后,将数组长度设置为j(即删除多余元素)。
注意:方法二需要修改原数组,并且由于是原地修改,我们需要确保在遍历过程中不会影响计数。
我们选择方法二来实现,因为题目要求删除元素,通常意味着修改原数组。但需要注意的是,在JavaScript中,数组是引用类型,我们可以修改原数组。
步骤详细说明(方法二):
1. 初始化一个空对象countMap用于记录每个元素出现的次数。
2. 初始化j=0(指向下一个保留元素的位置)。
3. 遍历数组,i从0到arr.length-1:
令当前元素为arr[i]。
如果countMap[arr[i]]为undefined(即未出现过),则将其设为1,并将arr[j]设置为arr[i],然后j++。
如果countMap[arr[i]]已经存在且小于2,则将其加1,并将arr[j]设置为arr[i],然后j++。
如果countMap[arr[i]]已经大于等于2,则跳过(即不做任何操作,i继续向后遍历)。
4. 遍历结束后,将数组的长度设置为j(即arr.length = j),这样就删除了多余的元素。
但是,注意:我们是在遍历的同时修改数组,并且将后面的元素向前移动(覆盖)。由于我们是从左向右遍历,并且j始终不会超过i,所以不会覆盖掉未处理的元素。
然而,这种方法会改变原数组中元素的相对顺序(保留的元素顺序不变,只是删除了多余的元素,并且后面的元素会覆盖前面的位置,但因为我们只保留前两次出现的,所以实际上顺序不会乱,因为我们是按顺序遍历的,每个元素第一次出现和第二次出现都会被保留,之后出现的都会被跳过)。
另一种方法:使用filter方法(更简洁,但需要额外空间?)
- 我们可以使用filter方法,同时利用一个计数对象,但是注意filter会遍历所有元素,并且返回一个新数组,不会改变原数组。如果要求修改原数组,我们可以将原数组的内容替换掉。
使用filter的方法:
let countMap = {};
let newArr = arr.filter(item => {
countMap[item] = (countMap[item] || 0) + 1;
return countMap[item] <= 2;
});
然后,如果我们想修改原数组,可以这样:
arr.length = 0;
arr.push(...newArr);
但是,这种方法会创建新数组,并且改变原数组的引用(但这里我们通过清空并重新填充的方式修改了原数组)。
根据要求,我们可以选择一种方法。这里我们提供两种方法供选择。
考虑到性能,如果数组很大,方法二(原地修改)可能更节省空间,但filter方法更简洁。
我们分别实现:
方法1:使用filter(非原地,但最后替换原数组)
步骤:
1. 创建一个空对象countMap。
2. 使用filter方法,对每个元素,更新计数,如果计数<=2则保留。
3. 将原数组清空,并将filter后的数组元素全部加入。
方法2:使用双指针(原地修改)
我们分别写一下代码:
方法1代码:
function removeMoreThanTwo(arr) {
let countMap = {};
let filtered = arr.filter(item => {
countMap[item] = (countMap[item] || 0) + 1;
return countMap[item] <= 2;
});
// 清空原数组并加入过滤后的元素
arr.length = 0;
filtered.forEach(item => arr.push(item));
// 或者直接 arr.push(...filtered);
return arr; // 返回原数组,此时已修改
}
注意:上面的filter方法中,计数是按顺序进行的,所以会保留前两次出现的元素,而删除之后出现的。但是,如果同一个元素分散在数组的不同位置,我们希望保留前两次出现的,那么这种方法就是正确的。
方法2代码(双指针):
function removeMoreThanTwo(arr) {
let countMap = {};
let j = 0;
for (let i = 0; i < arr.length; i++) {
let item = arr[i];
// 如果还没有记录,初始化为0
if (countMap[item] === undefined) {
countMap[item] = 0;
}
if (countMap[item] < 2) {
// 保留该元素
arr[j] = arr[i];
j++;
countMap[item]++;
}
// 如果已经达到2次,则跳过,j不变
}
// 修改数组长度
arr.length = j;
return arr;
}
两种方法都可以,我们选择双指针方法,因为它是原地修改,且只需要遍历一次,不需要额外空间(除了countMap)。
但是,注意:双指针方法会改变数组元素的顺序吗?
不会,它只是将后面保留的元素覆盖到前面,而跳过的元素会被后面的保留元素覆盖,但每个元素被保留时,其相对顺序没有改变(因为遍历顺序是从前往后)。
测试:
输入:[1, 2, 3, 1, 2, 3, 1, 2, 3]
期望输出:[1, 2, 3, 1, 2, 3] (前两次出现的1,2,3都被保留,第三个1,2,3被删除)
在双指针方法中:
遍历到第一个1:countMap[1]=0 -> 保留,arr[0]=1, j=1, countMap[1]=1
遍历到2:保留,arr[1]=2, j=2, countMap[2]=1
遍历到3:保留,arr[2]=3, j=3, countMap[3]=1
遍历到1:countMap[1]=1 -> 保留,arr[3]=1, j=4, countMap[1]=2
遍历到2:保留,arr[4]=2, j=5, countMap[2]=2
遍历到3:保留,arr[5]=3, j=6, countMap[3]=2
遍历到1:countMap[1]=2 -> 跳过
遍历到2:countMap[2]=2 -> 跳过
遍历到3:countMap[3]=2 -> 跳过
最后,arr.length=6,数组为[1,2,3,1,2,3]
所以,符合预期。
但是,注意:如果数组中有不同的数据类型,比如数字和字符串,或者对象,那么使用对象作为计数Map可能会出现问题,因为对象的键会被转换为字符串。例如,数字1和字符串'1'会被视为同一个键。所以我们需要考虑数组元素的类型。
如果数组中包含不同类型的相同值,比如1和'1',我们希望它们被当作不同的元素,那么使用对象作为Map就不合适了。我们可以使用Map数据结构,它可以保存键的类型。
改进:使用Map来计数,这样键可以是任意类型。
修改双指针方法:
function removeMoreThanTwo(arr) {
let countMap = new Map(); // 使用Map
let j = 0;
for (let i = 0; i < arr.length; i++) {
let item = arr[i];
// 获取当前计数,如果没有则为0
let count = countMap.get(item) || 0;
if (count < 2) {
arr[j] = arr[i];
j++;
countMap.set(item, count + 1);
}
}
arr.length = j;
return arr;
}
这样,不同的类型(比如1和'1')会被视为不同的键。
测试:数组[1, '1', 1, 2, 2, 1]
期望:保留前两个1(第一个1和第三个1?注意:第二个是'1',所以第一个1出现两次,第三个1是第三次出现,所以第三个1被删除?)
但是,注意遍历顺序:
第一个元素:1 -> countMap(1)为0 -> 保留,j=1,countMap.set(1,1)
第二个元素:'1' -> 是一个不同的键,countMap.get('1')为0 -> 保留,j=2,countMap.set('1',1)
第三个元素:1 -> 此时countMap.get(1)为1 -> 保留,j=3,countMap.set(1,2)
第四个元素:2 -> 保留,j=4,countMap.set(2,1)
第五个元素:2 -> 保留,j=5,countMap.set(2,2)
第六个元素:1 -> countMap.get(1)为2 -> 跳过
结果:[1, '1', 1, 2, 2] 符合预期(每个值最多两次,1出现了两次,'1'一次,2两次)。
因此,我们使用Map来计数,以支持不同类型的元素。
最终代码:</think>在 JavaScript 中,要从数组中删除出现超过两次的元素,可以使用以下两种方法。核心思路是**使用哈希表记录元素出现次数,通过过滤或双指针原地修改数组**:
### 方法一:使用 `filter()` 和 Map(推荐)
```javascript
function removeExcessDuplicates(arr) {
const countMap = new Map();
return arr.filter(item => {
const count = countMap.get(item) || 0;
countMap.set(item, count + 1);
return count < 2; // 只保留出现次数小于2的元素
});
}
// 示例
const arr = [1, 2, 3, 2, 1, 4, 1, 5];
console.log(removeExcessDuplicates(arr)); // [1, 2, 3, 2, 1, 4, 5]
```
**原理**:
1. 创建 `Map` 记录每个元素出现次数[^1]
2. 遍历数组时更新计数
3. 只保留计数小于2的元素(即最多保留两次出现)
---
### 方法二:双指针原地修改(节省空间)
```javascript
function removeExcessDuplicatesInPlace(arr) {
const countMap = new Map();
let j = 0; // 慢指针(保留元素的位置)
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
const count = countMap.get(item) || 0;
if (count < 2) {
arr[j] = item; // 保留有效元素
j++;
countMap.set(item, count + 1);
}
}
arr.length = j; // 截断多余元素
return arr;
}
```
### 关键点说明
1. **使用 `Map` 而非普通对象**
确保正确处理特殊值(如 `NaN`、对象引用等):
```javascript
// 普通对象无法区分字符串和数字
const obj = {};
obj[1] = "num";
obj["1"] = "string"; // 会覆盖前值
// Map 能区分类型
const map = new Map();
map.set(1, "num");
map.set("1", "string"); // 两个独立键
```
2. **时间复杂度**
两种方法均为 $O(n)$ 时间复杂度,仅需一次遍历[^2]
3. **稳定性**
保留元素的**原始顺序**,只删除第三次及后续出现的重复项
---
### 使用场景示例
```javascript
// 清理用户输入(如重复提交)
const userInputs = ["save", "save", "undo", "save", "redo"];
console.log(removeExcessDuplicates(userInputs));
// ["save", "save", "undo", "redo"]
// 处理传感器数据(允许短暂波动)
const sensorData = [23, 23, 23, 24, 24, 23, 25];
console.log(removeExcessDuplicates(sensorData));
// [23, 23, 24, 24, 25]
```