【JavaScript】面试手撕数组篇

文章介绍

该文章主要是讲解我们在面试的时候碰到一些JS的手写题, 确实这种手写题还是比较恶心的。有些时候好不容易把题目写出来了,突然面试官冷不丁来一句有没有更优的解法,直接让我们僵在原地。为了解决兄弟们的这些困扰,这个专题于是就诞生啦。我们会将一些常见的不是最优解的答案作为对比,方便大家更好理解。

【JavaScript】面试手撕数组篇【JavaScript】面试手撕数组篇

数组常考题

这主要讲的是常考的数组常见题,还是很有必要记住下滴。

数组去重(难度:🌟)

先来一个简单的,对,就是数组去重。给定一个数组,去除掉数组中重复的数据。

有些年轻小伙子不讲武德,刚毕业,20来岁,上来就用两层For循环开开心心地写着,作为一个面试官,很多时候我想打断他, 但是又于心不忍,破坏他写代码时专注的气氛。最后,作为面试官我们来了一句

你觉得还有没有更好的解法呢?

【JavaScript】面试手撕数组篇【JavaScript】面试手撕数组篇

非最优解

function unique(arr){            
  for(let i=0; i<arr.length; i++){
    for(let j=i+1; j<arr.length; j++){
      if(arr[i] === arr[j]){       
        arr.splice(j,1);
        j--;
      }
    }
  }
  return arr;
}

const arr = [1,1,'true','true',true,true,15,15,false,false];
console.log(unique(arr)) // [ 1, 'true', true, 15, false ]

最优解

我将最优解分为了两类,一类是ES5,一类是ES6的。因为有些浏览器版本还不支持ES6,所以要做个兼容。

ES5

使用filter实现

function unique(arr) {
  return arr.filter(function(item, index, array) {
    return arr.indexOf(item) === index
  })
}

const arr = [1,1,'true','true',true,true,15,15,false,false];
console.log(unique(arr)) // [ 1, 'true', true, 15, false ]

ES6

使用Set数据结构实现

const unique = arr => [...new Set(arr)]

const arr = [1,1,'true','true',true,true,15,15,false,false];
console.log(unique(arr)) // [ 1, 'true', true, 15, false ]

数组扁平化(难度:🌟🌟)

数组扁平化就是把多维数组转化成一维数组。举个🌰:

['a',['b',['c']]]经过数组扁平化后变为['a','b','c']

有很多年轻人见到这题后,就开始了长久的静坐,咳咳。当然也不乏有些天资聪颖之辈,上来就花了10秒钟解决了这个问题, 他们说ES6中有flat方法。

let arr = ['a',['b',['c']]];
console.log(arr.flat(2)); // [ 'a', 'b', 'c' ]

是的 flat方法确实能将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

正在这些年轻人洋洋得意的时候,面试官突然来了一句

小伙子,不好意思,我没把问题表述清楚,我希望的是你能把这个flat方法实现一下。

接下来便是长久的静默……..

最优解

好像也没有什么非最优解,毕竟能做出来就很不错啦。

ES5

通过递归的方式将所有是数组的子元素进行遍历。

const flatten = (arr) => {
  let res = [];
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      res = res.concat(flatten(arr[i]));
    } else {
      res.push(arr[i]);
    }
  }
  return res;
};

let arr = ['a',['b',['c']]];
console.log(flatten(arr)); // [ 'a', 'b', 'c' ]

ES6

这个做法颇为巧妙,通过while(arr.some((item) => Array.isArray(item)))这段代码,如果数组里还有内嵌数组,那么这个条件一直为真,会一直执行下去,知道所有的元素都不是数组为止才会推出循环。

const flatten = (arr) => {
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
};

let arr = ["a", ["b", ["c"]]];
console.log(flatten(arr)); // [ 'a', 'b', 'c' ]

数组原型链常考题

主要讲的是数组原型链相关的考题,有些人可能会纳闷,数组和原型链之间有什么关系呢?我们日常使用的数组forEach,map等都是建立在原型链之上的。举个🌰,如我有一个数组const arr = [1,2,3]我想要调用arr.sum方法对arr数组的值进行求和,该如何做呢?我们知道数组没有sum函数,于是我们需要在数组的原型上定义这个函数,才能方便我们调用,具体代码如下。接下来我们就是采用这种方式去实现一些数组常用的方法。

Array.prototype.sum = function () {
  let sum = 0;
  for (let i = 0; i < this.length; i++) {
    sum += this[i];
  }
  return sum;
};

const arr = [1,2,3];
console.log(arr.sum()); // 6

forEach实现

首先,我们先了解forEach这个函数有哪些参数。我们先看一个例子, forEach的使用

const arr = [1,2,3];
arr.forEach((item,index,arr) => {
  console.log(item,index,arr);
})
/**
输入如下
1 0 [ 1, 2, 3 ]
2 1 [ 1, 2, 3 ]
3 2 [ 1, 2, 3 ]
**/

所以我们观察发现forEach传的是一个回调函数

function(currentValue, index, array) {}

这个回调函数有三个参数,分别如下

  • currentValue:必需,当前正在遍历的数组元素的值。
  • index:可选,当前正在遍历的数组元素的索引。
  • array:可选,指向正在被遍历的数组对象本身。

了解上述资料后,于是我们可以自己实现一个myForEach,代码如下

Array.prototype.myForEach = function (fn) {
  for (let i = 0; i < this.length; i++) {
    fn(this[i], i, this);
  }
};

const arr = [1, 2, 3];
arr.forEach((item, index, arr) => {
  console.log(item, index, arr);
});

map实现

同样,我们了解下map的方法有哪些参数,调研发现map的参数与forEach的参数前面一致,多了一个thisArg的参数

  • thisArg: 在执行回调函数时作为其上下文(即this值)的对象。如果提供了这个参数,回调函数内部的 this将指向它;如果不提供,则在严格模式下 this 会是 undefined,在非严格模式下 this 通常是全局对象(如浏览器环境中的window对象

我们也看一个🌰,帮助大家会一起map是如何使用的

const arr = [1, 2, 3];
// 这里为了能让大家看到所有的参数,于是将三个参数全部写了下来
const douleArr = arr.map((value,index,array) => value * 2);
console.log(douleArr);
/**
输入如下
[2, 4, 6]
**/

那么该如何实现呢,其实依靠forEach照葫芦画瓢即可,代码如下

Array.prototype.myMap = function (fn, thisArg) {
  const res = [];
  for (let i = 0; i < this.length; i++) {
    const mappedValue = fn.call(thisArg, this[i], i, this);
    res.push(mappedValue);
  }
  return res;
};

const arr = [1, 2, 3];
const douleArr = arr.myMap((value,index,array) => value * 2);
console.log(douleArr);

面试手撕数组排序

主要讲的是数组的排序篇,我们知道面试的时候,数组的排序是经常出现的题目。所以这块还是有必要进行一下讲解的。笔者观察了下前端这块的常用算法排序题,大概可以分为如下

  • 冒泡排序–> 稳定排序
  • 插入排序–> 稳定排序
  • 选择排序–> 不稳定排序
  • 快速排序–> 不稳定排序

所以笔者在该章节只会讲解这4大排序算法的实现,至于有些读者问如果面试题出了其他的排序算法呢?例如希尔排序,堆排序等,我个人认为如果一家公司给候选人出堆排序,那我觉得他可能就不太想让候选人通过,如果出希尔排序,那我建议你这次面试可以不用面了,因为95%以上是KPI面试。

【JavaScript】面试手撕数组篇【JavaScript】面试手撕数组篇

冒泡排序

冒泡排序工作原理:

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个。
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

时间复杂度:

代码如下:

function bubbleSort(arr) {
  const len = arr.length;
  for (let i = 0; i < len - 1; i++) {
    // 每轮循环最后i个元素已经是有序的,所以内层循环可以减去i
    for (let j = 0; j < len - 1 - i; j++) {
      // 如果前一个元素大于后一个元素,则交换它们的位置
      if (arr[j] > arr[j + 1]) {
        const temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
  return arr;
}

// 测试
const arr = [2, 3, 1, 4, 5];
console.log(bubbleSort(arr));
// 输出: [1, 2, 3, 4, 5]

插入排序

插入排序工作原理:

  1. 从第一个元素开始,该元素可以认为已经被排序。
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描。
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置。
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
  5. 将新元素插入到该位置后。
  6. 重复步骤2~5。

时间复杂度:

代码如下:

function insertionSort(arr) {
  const len = arr.length;
  for (let i = 1; i < len; i++) {
    const key = arr[i];
    let j = i - 1;
    // 将arr[i]元素移到已排序部分的正确位置
    while (j >= 0 && arr[j] > key) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = key;
  }
  return arr;
}

// 测试
const arr = [2, 3, 1, 4, 5];
console.log(insertionSort(arr));
// 输出: [1, 2, 3, 4, 5]

选择排序

选择排序工作原理:

  1. 在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置。
  2. 再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

时间复杂度:

代码如下:

function selectionSort(arr) {
  const n = arr.length;
  for (let i = 0; i < n; i++) {
    let minIndex = i;
    for (let j = i + 1; j < n; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    if (minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
  }
  return arr;
}

// 测试
const arr = [2, 3, 1, 4, 5];
console.log(selectionSort(arr));
// 输出: [1, 2, 3, 4, 5]

快速排序

快速排序工作原理:

  1. 从数列中挑出一个元素作为基准(pivot),通常选择第一个或最后一个元素。
  2. 重新排列数列,所有比基准小的元素摆放在基准前面,所有比基准大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序

时间复杂度:

代码如下:

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  const pivot = arr[0];
  const left = [];
  const right = [];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return [...quickSort(left), pivot, ...quickSort(right)];
}

完!

原文链接:https://juejin.cn/post/7337892215226318885 作者:鑫宝Code

(0)
上一篇 2024年2月22日 上午10:58
下一篇 2024年2月22日 上午11:08

相关推荐

发表回复

登录后才能评论