面试杂谈:数组去重和时间复杂度

Posted 恪愚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试杂谈:数组去重和时间复杂度相关的知识,希望对你有一定的参考价值。

在有了基础做保障之后的面试确实是提升自己扩展视野的绝好手段。
面试官的问题可能会让你眼前一亮或者你知道但是压根没有联系到一起过。而这些可能是对于工作非常有帮助的。

面试官:“请实现一个数组去重?”

我:“有什么特别的要求吗?”

面试官:“没有。”

我:

let newArr = [...new Set(arr)];

面试官:“…能换一种方式吗?用原生的、es5的方式实现?”

我:

function unique(arr) 
    if (!Array.isArray(arr)) 
        console.log('type error!')
        return
    
    var array = [];
    for (var i = 0; i < arr.length; i++) 
        if (array .indexOf(arr[i]) === -1) 
            array .push(arr[i])
        
    
    return array;

console.log(unique(arr))

“然后还有其他方法…”

面试官:“好的先停一下,你能说下indexOf的复杂度吗?你知道不?”

我:(思索2s)
“嗯,是的。”
“它是指查找与未排序数组中的值匹配的第一个索引,而这件事最有效的方法是按顺序遍历列表。也就是O(n)”。
“关于这一点,MDN上也有说明。”

MDN提示:开始搜索的索引。如果索引大于或等于数组的长度,则返回-1,这意味着将不搜索数组。如果提供的索引值为负数,则将其视为距数组末尾的偏移量。注意:如果提供的索引为负,则仍然从前到后搜索数组。如果计算的索引小于0,则将搜索整个数组。

面试官:“那就是了,你上面这段代码的时间复杂度…”

我:(打断)“哦我明白了,这段代码在indexOf外又套了一层循环,时间复杂度和循环套循环的方式其实是一样的,都是O(n^2),非常低。我换一种方式”
(思索3s)
“循环套循环肯定是不行了,indexOf复杂度又和前一种是一样的,那用sort?不行,sort虽然内部也做了优化,但是他的复杂度即使在数据量小于10个的情况下最低仍然是O(n),而且仍需借助for循环,,,这样一来,嗯,可以模仿字符串原型上已存在的不能再次存在的去重方式,利用对象的属性不能相同的特点进行去重!”

function unique(arr) 
    if (!Array.isArray(arr)) 
        console.log('type error!')
        return
    
    let arrry= [];
    let obj = ;
    for (let i = 0; i < arr.length; i++) 
        if (!obj[arr[i]]) 
            arrry.push(arr[i])
            obj[arr[i]] = 1
        
    
    return arrry;

console.log(unique(arr))

“这也是一种用空间换时间的做法。”

面试官:(露出笑容)“嗯,好的。那你能简单描述下sort吗?”

我:“嗯”
sort内部对排序做了一些优化,主要集中在对排序方法的选择上 —— 如果要排序的元素个数是 n 的时候,那么就会有以下几种情况:

  • 当 n<=10 时,采用插入排序;
  • 当 n>10 时,采用三路快速排序;
  • 10<n <=1000,采用中位数作为哨兵元素;
  • n>1000,每隔 200~215 个元素挑出一个元素,放到一个新数组中,然后对它排序,找到中间位置的数,以此作为中位数。

为什么元素个数少的时候要采用插入排序?

虽然插入排序理论上是平均时间复杂度为 O(n^2) 的算法,快速排序是一个平均 O(nlogn) 级别的算法。但这只是理论上平均的时间复杂度估算,但是它们也有最好的时间复杂度情况,而插入排序在最好的情况下时间复杂度是 O(n)

在实际情况中两者的算法复杂度前面都会有一个系数,当 n 足够小的时候,快速排序 nlogn 的优势会越来越小。倘若插入排序的 n 足够小,那么就会超过快排。而事实上正是如此,插入排序经过优化以后,对于小数据集的排序会有非常优越的性能,很多时候甚至会超过快排。因此,对于很小的数据量,应用插入排序是一个非常不错的选择。

为什么要花这么大的力气选择哨兵元素?

因为快速排序的性能瓶颈在于递归的深度,最坏的情况是每次的哨兵都是最小元素或者最大元素,那么进行 partition(一边是小于哨兵的元素,另一边是大于哨兵的元素)时,就会有一边是空的。如果这么排下去,递归的层数就达到了 n , 而每一层的复杂度是 O(n),因此快排这时候会退化成 O(n^2)级别。

这种情况是要尽力避免的,那么如何来避免?就是让哨兵元素尽可能地处于数组的中间位置,让最大或者最小的情况尽可能少。

面试官:“嗯,不错的。最后再问一个问题:你有读过indexOf的源码吗?”

我:“读过。阅读源码也是我学习一个知识点的必不可少的一步。”(吹起来了)

indexOf是如何实现的

虽然是吹的,但在曾经阅读相关书籍的时候我还真看过webkit对于它的实现:

EncodedJSValue JSC_HOST_CALL arrayProtoFuncIndexOf(ExecState* exec)

    // 15.4.4.14
    JSObject* thisObj = exec->hostThisValue().toThis(exec, StrictMode).toObject(exec);
    unsigned length = thisObj->get(exec, exec->propertyNames().length).toUInt32(exec);
    if (exec->hadException())
        return JSValue::encode(jsUndefined());

    unsigned index = argumentClampedIndexFromStartOrEnd(exec, 1, length);
    JSValue searchElement = exec->argument(0);
    for (; index < length; ++index) 
        JSValue e = getProperty(exec, thisObj, index);
        if (exec->hadException())
            return JSValue::encode(jsUndefined());
        if (!e)
            continue;
        if (JSValue::strictEqual(exec, searchElement, e))
            return JSValue::encode(jsNumber(index));
    

    return JSValue::encode(jsNumber(-1));

可以看到,使用的c++的流操作,那么这样的话,lastIndexOf

以上是关于面试杂谈:数组去重和时间复杂度的主要内容,如果未能解决你的问题,请参考以下文章

华为上机真题 2023数组去重和排序 (华为机考真题)

getValues去重和数组去重

7数组去重和排序

去重和数组排序

华为机试真题 C++ 实现数组去重和排序

华为机试真题 C++ 实现数组去重和排序