前端面试算法题

1. 数组扁平化 .

数组是 JS 中使用频率仅次于对象的数据结构,官方提供了众多的 API。
今天我们来谈谈如何扁平化(flatten)数组。

扁平化就是将嵌套的数组变成一维数组的过程。

通常有几种方法可以实现扁平化:

第一个就是迭代递归法

var array = [[1,2,3],4,5,6,[[7]],[]]
var result = flatten(array)

console.log(result)

for…of 实现

function flatten(arr, result = []) {
    for (let item of arr) {
        if (Array.isArray(item))
            flatten(item, result)
        else
            result.push(item)
    }
    return result
}

迭代器实现:

众所周知,数组在 JS 中是一种可迭代结构,所以我们可以利用这一点修改它的迭代器实现扁平化:


Array.prototype[Symbol.iterator] = function() {
    let arr = [].concat(this)
    const getFirst = function(array) {
        let first = array[0]
        // 去掉为 [] 的元素
        while (Array.isArray(array[0]) && array.length === 0) {
           array.shift()
        }
        if (Array.isArray(first)) {
            // 即将是 []
            if (first.length === 1) array.shift()
            return getFirst(first)
        } else {
            array.shift()
            return first
        }
    }
    return {
        next: function() {
            let item = getFirst(arr)
            if (item) {
                return {
                    value: item,
                    done: false,
                }
            } else {
                return {
                    done: true,
                }
            }
        },
    }
}


生成器:
迭代器的升级版就是生成器(Generator),其实这种扁平化最适合用生成器来做了,因为我们的目的就是生成一个个的值,然后把它们组织成一维数组:

function* flat(arr) {
    for (let item of arr) {
        if (Array.isArray(item))
            yield* flat(item)
        else
            yield item
    }
}

function flatten(arr) {
    let result = []
    for (let val of flat(arr)) {
        result.push(val)
    }
    return result
}

reduce 三句实现法

function flatten(arr) {
  return arr.reduce((flat, toFlatten) => {
    return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
  }, []);
}

reduce 是函数式编程两大法宝之一,中文翻译为化简,用它来实现,简直是巧妙。

第二个曲线救国法:

function flatten(arr){
    let str = arr.toString()
    return str.split(',')
}

管你原来是几维,先来个二向箔:转成字符串,之后再复原成数组,不过这个方法有个缺点,就是原来的空数组转的空字符串也会被放入新生成的数组里去。所以如果不需要空串元素的话还需要对结果进行过滤操作。
除了直接调用它的 toString 方法之外,还可以用隐式转换间接调用:

function flatten(arr){
    return (arr + '').split(',')
}

写到最后:

Array.prototype.flatten 递归地将数组展按照指定的 depth 进行展平,depth 的默认值为 1。

// Flatten one level:
const array = [1, [2, [3]]];
array.flatten();
// → [1, 2, [3]]

// Flatten recursively until the array contains no more nested arrays:
array.flatten(Infinity);
// → [1, 2, 3]
同样的提议还包括 Array.prototype.flatMap,如同 Array.prototype.map 一样,可以在参数里面传递一个回调函数。

[2, 3, 4].flatMap((x) => [x, x * 2]);
// → [2, 4, 3, 6, 4, 8]

2. 实现斐波那契数列的计算

斐波那契数列又被称为黄金分割数列。
指的是这样的一个数列:1,1,2,3,5,8,13,21,34…,它有如下递推的方法
定义:F(1)=1,F(2)=1,F(n)=F(n-1)+F(n-2)(n>=2,n是正整数).
请使用js实现斐波那契函数:

第一种方法:

function fibonacci(n){

        if(n < 0) throw new Error('输入的数字不能小于0');

        if(n==1 || n==2){

            return 1;

        }else{

            return fibonacci1(n-1) + fibonacci1(n-2);

        }

    }

优点:比较简洁易懂;
缺点:当数字太大时,会变得特别慢,要计算之前的数值。

所以:使用普通的递归,会造成不必要的浪费,所以我们首先想到的应该是将每次产生的递归值保存下来,下次直接使用就行,代码如下:





function fibonacci(n){

        if(n < 0) throw new Error('输入的数字不能小于0');

        let arr = [0,1];//在外部函数中定义数组,内部函数给数组添加值

        function calc(n){

            if(n<2){

                return arr[n];

            }

            if(arr[n] != undefined){

                return arr[n];

            }

            let data = calc(n-1) + calc(n-2);//使用data将每次递归得到的值保存起来

            arr[n] = data;//将每次递归得到的值放到数组中保存

            return data;

        }

        return calc(n);

    }

直接使用数组实现(动态规划)
  和方法2的思想类似,为了避免后续的重复计算,需要将计算过的值保存起来,我们可以直接使用数组进行保存。

function fibonacci(n){

        var a = [0,1,1];

        if(n < 0) throw new Error('输入的数字不能小于0');

        if(n >= 3){

            for(var i=3;i<=n;i++){

                a[i] = a[i-1]+a[i-2];

            }

        }

        return a[n];

    }

直接使用变量实现
  相校于使用数组的方式去存放,使用变量的方式就不会那么浪费内存了,因为总共只会有3个变量,但是也有缺点,它只能保存最后计算的值以及前两个值,以前的值会被替换掉。

function fibonacci(n){

        var pre = 0;//表示前一个值

        var cur = 1;//表示后一个值

        var data;//表示当前值

 

        if(n < 0) throw new Error('请输入大于0的值!');

        if(n == 0) return 0;

        if(n == 1) return 1;

        if(n > 2){

            for(var i=2;i<=n;i++){

                data = pre + cur;

                pre = cur;

                cur = data;

            }

        }

        return data;

    }

3. 回文判断是不是回文字符串

回文就是:根据中心左右对称的字符串;
如:php aaccaa FFGGFF等,简单的说就是正读和反读都一样。

原理:定义一个方法,我们把判断的字符串传进去。
第一先判断他是否是string格式,
是的话 我们把它塞进一个数组里(split())
然后倒叙排列(reverse())
最后拆分为字符串(join())
相反就直接return false;

function  test(abc){

// typeof  js中判断一个变量的类型

// split 把一个字符串 分割成字符串数组

// reverse  颠倒数组中的顺序

// join  把一个数组塞进一个字符串

if(typeof abc == 'string')

return abc.split('').reverse('').join('') == abc;

return false;

}

console.log(test('php')+' '+test('script'))

4. js实现随机选取10-100之间10个数字,存入数组

  function sortNumber(a,b){
   return a-b;//升序
   //return b-a;//降序
  }
  
  //js实现随机选取10–100之间的10个数字,存入一个数组,并排序
  var iArray =[];
  function getRandom(iStart,iEnd){
   var iChoice = iStart-iEnd+1;
   return Math.abs(Math.floor(Math.random()*iChoice))+iStart;
  }
  for(var i=0;i<10;i++){
   iArray.push(getRandom(10,100))
  }
  iArray.sort(sortNumber);
  console.log(iArray)

5. 输出今天日期

原生JS。
使用Date(方法)获取日期。

var myDate=new Date() ;

console.log(myDate);

console.log(Date());

alert(Date());


6. 连续子数组的最大和

解法 1:动态规划
定义状态数组dp[i]的含义:数组中元素下标为[0, i]的连续子数组最大和。

状态转移的过程如下:

初始情况:dp[0] = nums[0]
若 nums[i] > 0,那么 dp[i] = nums[i] + dp[i - 1]
若 nums[i] <= 0,那么 dp[i] = nums[i]

var maxSubArray = function(nums) {
    const dp = [];

    let res = (dp[0] = nums[0]);
    for (let i = 1; i < nums.length; ++i) {
        dp[i] = nums[i];
        if (dp[i - 1] > 0) {
            dp[i] += dp[i - 1];
        }
        res = Math.max(res, dp[i]);
    }
    return res;
};

o(n)&0(n)

解法 2:原地进行动态规划
在解法 1 中开辟了 dp 数组。其实在原数组上做修改,用nums[i]来表示dp[i]。所以解法 1 的代码可以优化为:

var maxSubArray = function(nums) {
    let res = nums[0];
    for (let i = 1; i < nums.length; ++i) {
        if (nums[i - 1] > 0) {
            nums[i] += nums[i - 1];
        }
        res = Math.max(res, nums[i]);
    }
    return res;
};
0(n)&0(1)

解法 3:贪心法
本题的贪心法的思路是:在循环中找到不断找到当前最优的和 sum。

注意:sum 是 nums[i] 和 sum + nums[i]中最大的值。
这种做法保证了 sum 是一直是针对连续数组算和。

var maxSubArray = function(nums) {
    let maxSum = (sum = nums[0]);
    for (let i = 1; i < nums.length; ++i) {
        sum = Math.max(nums[i], sum + nums[i]);
        maxSum = Math.max(maxSum, sum);
    }
    return maxSum;
};
0(n)&0(1)

7. 给定非空字符串,判断是否由它一个子字符串重复构成

这个用正则的方式,比较简单。
w+ : w 表示 匹配任何字类字符,包括下划线。与"[A-Za-z0-9_]"等效。 + 表示:一次或多次匹配前面的字符或子表达式。
例如:"zo+"与"zo"和"zoo"匹配,但与"z"不匹配。+ 等效于 {1,}。
\1+ 表示:重复问上面捕获组里的内容一次或多次

function repeatedSubstringPattern (str) {
  return /^(\w+)\1+$/.test(str)
}
const result4 = repeatedSubstringPattern('abcabc')
// true

8. 树形结构的数据与一维数组数据的相互转换

数组转树形结构:

var data = [

  {"id":2,"name":"第一级1","pid":0},

  {"id":3,"name":"第二级1","pid":2},

  {"id":5,"name":"第三级1","pid":4},

  {"id":100,"name":"第三级2","pid":3},

  {"id":6,"name":"第三级2","pid":3},

  {"id":601,"name":"第三级2","pid":6},

  {"id":602,"name":"第三级2","pid":6},

  {"id":603,"name":"第三级2","pid":6}

];

数组转树形结构数据
(原理即为通过设置id为key值,再通过pid去找这个key是否一样,一样则为这数据的子级数据)

function arrayToJson(treeArray){

    var r = [];

    var tmpMap ={};

    for (var i=0, l=treeArray.length; i

9. 随机生成一定范围内元素组成的length的数组

使用场景;在 js 生成验证码或者随机选中一个选项时很有用。

//生成从minNum到maxNum的随机数
function randomNum(minNum,maxNum){ 
    switch(arguments.length){ 
        case 1: 
            return parseInt(Math.random()*minNum+1,10); 
        break; 
        case 2: 
            return parseInt(Math.random()*(maxNum-minNum+1)+minNum,10); 
        break; 
            default: 
                return 0; 
            break; 
    } 
} 

10. add(1)(2)

我们有一个需求,用js 实现一个无限极累加的函数.
形如 add(1) //=> 1;
add(1)(2) //=> 2;
add(1)(2)(3) //=> 6;
add(1)(2)(3)(4) //=> 10; 以此类推.
补充一点知识:
函数柯里化(curry)是函数式编程里面的概念。curry的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

简单点来说就是:每次调用函数时,它只接受一部分参数,并返回一个函数,直到传递所有参数为止

const add = (x, y) => x + y;
add(1, 2);

改成每次只接受一个参数的函数

const add = x => y => x + y;
add(1)(2);

柯里化主要有3个作用: 参数复用、提前返回和 延迟执行。

主要思路:要判断当前传入函数的参数个数 (args.length) 是否大于等于原函数所需参数个数 (fn.length) 。
如果是,则执行当前函数;如果是小于,则返回一个函数。

const curry = (fn, ...args) => 
    // 函数的参数个数可以直接通过函数数的.length属性来访问
    args.length >= fn.length // 这个判断很关键!!!
    // 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
    ? fn(...args)
    /**
     * 传入的参数小于原始函数fn的参数个数时
     * 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
    */
    : (..._args) => curry(fn, ...args, ..._args);

function add1(x, y, z) {
    return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));

Ramda

Ramda 中的函数所有都支持柯里化。也就是说,所有的多参数函数,默认都可以使用单参数函数。

还是举上面的例子
const addThreeNumbers = (x, y, z) => x + y + z;
const curriedAddaddThreeNumbers = R.curry(addThreeNumbers);
const f = curriedAddaddThreeNumbers(1, 2);
console.log(f(3));

大名鼎鼎的 lodash 中也提供了 柯里化 函数 ,那么它和Ramda有什么区别呢

lodash是一个很强大的工具函数库,比如 节流,防抖,深拷贝等等,只要引入 lodash ,我们就可以直接使用。
Ramda 是一个函数式编程风格的函数库。

我们来简单的解释一下:
参数复用:拿上面 f这个函数举例,只要传入一个参数 z,执行,计算结果就是 1 + 2 + z 的结果,1 和 2 这两个参数就直接可以复用了。
提前返回 和 延迟执行 也很好理解,因为每次调用函数时,它只接受一部分参数,并返回一个函数(提前返回),直到(延迟执行)传递所有参数为止。

你可能感兴趣的:(vue.js,前端,面试,算法)