8个常用的js数组去重的方法与时间、空间复杂度分析

852 阅读7分钟

数组去重常见方法

数组去重是JavaScript中常见的操作,也是经典的前端面试题,在给出具体解决方法后,面试官可能还会要求你做一个优化分析,以下是一些常见的去重方法,以及对应的时间复杂度和空间复杂度分析:

1. 使用ES6的Set数据结构

function removeDuplicatesWithSet(arr) {  
    return [...new Set(arr)];  
}

时间复杂度: O(n),其中n是数组的长度。这是因为Set数据结构在插入元素时,平均情况下需要O(1)的时间复杂度。

空间复杂度: O(n)。因为我们需要一个新的Set来存储所有不重复的元素。在最坏的情况下,如果所有元素都是不重复的,那么Set的大小将与原始数组的大小相同。

2. 使用filter方法和indexOf方法

function removeDuplicatesWithFilter(arr) {  
    return arr.filter((value, index, self) => {  
        return self.indexOf(value) === index;  
    });  
}

时间复杂度: O(n^2)。这是因为filter方法中的回调函数对于数组中的每个元素都会被调用一次,而indexOf方法在最坏的情况下需要O(n)的时间复杂度来查找一个元素。因此,总体时间复杂度是O(n^2)。

空间复杂度: O(n)。我们需要一个新的数组来存储去重后的结果。在最坏的情况下,如果所有元素都是不重复的,那么新数组的大小将与原始数组的大小相同。

3. 使用两层循环

function removeDuplicatesWithLoop(arr) {  
    let newArr = [];  
    for(let i = 0; i < arr.length; i++) {  
        let isDuplicate = false;  
        for(let j = 0; j < newArr.length; j++) {  
            if(arr[i] === newArr[j]) {  
                isDuplicate = true;  
                break;  
            }  
        }  
        if(!isDuplicate) {  
            newArr.push(arr[i]);  
        }  
    }  
    return newArr;  
}

时间复杂度: O(n^2)。因为我们需要两层循环来比较每个元素。外层循环需要O(n)的时间复杂度,内层循环在最坏的情况下也需要O(n)的时间复杂度,所以总的时间复杂度是O(n^2)。

空间复杂度: O(n)。我们需要一个新的数组来存储去重后的结果。在最坏的情况下,如果所有元素都是不重复的,那么新数组的大小将与原始数组的大小相同。

4. 使用对象作为哈希表

function removeDuplicatesWithObject(arr) {  
    let obj = {};  
    let newArr = [];  
    for(let i = 0; i < arr.length; i++) {  
        if(!obj[arr[i]]) {  
            obj[arr[i]] = true;  
            newArr.push(arr[i]);  
        }  
    }  
    return newArr;  
}

时间复杂度: O(n)。因为我们只需要遍历数组一次,并且在哈希表中查找一个元素的时间复杂度是O(1)。

空间复杂度: O(n)。我们需要一个新的对象来作为哈希表,以及一个新的数组来存储去重后的结果。在最坏的情况下,如果所有元素都是不重复的,那么哈希表的大小和新数组的大小都将与原始数组的大小相同。

5. 使用Array.prototype.includes方法

function removeDuplicatesWithIncludes(arr) {  
    const newArr = [];  
    for (let i = 0; i < arr.length; i++) {  
        if (!newArr.includes(arr[i])) {  
            newArr.push(arr[i]);  
        }  
    }  
    return newArr;  
}

时间复杂度: O(n^2)。因为 includes 方法在最坏的情况下需要遍历整个 newArr 数组来检查元素是否存在,所以时间复杂度为 O(n)。由于这个操作在一个外层循环中执行,所以总的时间复杂度为 O(n^2)。

空间复杂度: O(n)。我们创建了一个新的数组 newArr 来存储去重后的元素,其大小在最坏的情况下与原始数组相同。

6. 使用Array.from和Map

function removeDuplicatesWithMap(arr) {  
    return Array.from(new Map(arr.map(item => [item, item])).values());  
}

时间复杂度: O(n)。map 方法遍历数组一次,Map 对象在插入时具有接近常数时间复杂度的特性,而 Array.from 转换 Map 对象为数组也是一个线性操作。

空间复杂度: O(n)。我们需要一个新的 Map 对象来存储键值对,并且最后还需要一个新的数组来存储去重后的值。在最坏的情况下,如果所有元素都是不重复的,Map 的大小和最终数组的大小都将与原始数组相同。

7. 使用reduce方法

function removeDuplicatesWithReduce(arr) {  
    return arr.reduce((accumulator, currentValue) => {  
        if (!accumulator.includes(currentValue)) {  
            accumulator.push(currentValue);  
        }  
        return accumulator;  
    }, []);  
}

时间复杂度: O(n^2)。和 includes 方法类似,reduce 中的 includes 检查也需要在最坏情况下遍历整个累积器数组,导致整体时间复杂度为 O(n^2)。

空间复杂度: O(n)。reduce 方法创建了一个新的累积器数组来存储去重后的元素。

8. 先排序后去重

function removeDuplicatesWithSort(arr) {  
    arr.sort((a, b) => a - b);  
    let newArr = [arr[0]];  
    for (let i = 1; i < arr.length; i++) {  
        if (arr[i] !== newArr[newArr.length - 1]) {  
            newArr.push(arr[i]);  
        }  
    }  
    return newArr;  
}

时间复杂度: O(n log n) + O(n)。排序的时间复杂度通常为 O(n log n),去重过程的时间复杂度为 O(n)。因此,总的时间复杂度为排序和去重之和。

空间复杂度: O(log n) + O(n)。排序操作可能需要额外的空间(取决于具体的排序算法实现),通常为 O(log n)。去重过程需要一个新的数组来存储结果,其空间复杂度为 O(n)。

注意:排序后去重的方法改变了原始数组的顺序,这可能不适用于所有情况。

实际应用中需要考虑的因素

在选择数组去重方法时,应根据具体的应用场景和需求来权衡时间复杂度和空间复杂度。在大多数情况下,使用 Set 数据结构是一个高效且简洁的选择,因为它提供了接近常数时间复杂度的去重操作。然而,如果数据规模较小或对性能要求不那么严格,其他方法可能也是可行的。可根据以下这些方面考虑:

  1. 数据规模

    • 对于小型数组或数据集,各种去重方法的性能差异可能并不明显。在这种情况下,您可以选择最直观、最容易理解的方法。
    • 对于大型数组或数据集,性能变得至关重要。在这种情况下,应该优先考虑使用具有更低时间复杂度的方法,如使用 Set 数据结构。
  2. 数据特性

    • 如果数据包含复杂的数据结构(如对象或嵌套数组),那么可能需要自定义的去重逻辑,而不是简单地比较值。
    • 如果数据是有序的或可以预先排序,那么可以使用更高效的去重算法,如双指针法。
  3. 性能要求

    • 如果您的应用对性能有严格的要求,例如需要实时处理大量数据,那么应该选择具有更低时间复杂度和更高效率的去重方法。
    • 对于性能要求不那么严格的应用,可以选择更直观、易于维护的方法。
  4. 编程语言和工具

    • 不同的编程语言和工具提供了不同的数据结构和算法支持。例如,JavaScript 中的 Set 数据结构非常适合去重操作。
    • 熟悉您所使用的编程语言和工具库,以便利用它们提供的优化功能。
  5. 空间复杂度

    • 除了时间复杂度外,还需要考虑空间复杂度。某些方法可能需要额外的存储空间来存储去重后的结果或中间状态。如果您的应用受到内存限制,那么应该选择具有更低空间复杂度的方法。
  6. 代码可读性和可维护性

    • 在选择去重方法时,除了性能外,还应考虑代码的可读性和可维护性。选择一种易于理解和维护的方法有助于减少未来的维护成本。
  7. 测试和基准测试

    • 对于关键的去重操作,建议进行基准测试以比较不同方法的性能。这可以帮助您在实际应用中做出更明智的选择。