1、猿辅导前端面试算法题

2021/6/19 22:57:03

本文主要是介绍1、猿辅导前端面试算法题,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1. 算法题

1、二叉树宽度

  1. 遍历二叉树,将非空的结点存到队列,其中一个元素表示为[结点,层次,结点编号]
  2. obj中记录当前的层次、当前层次的开始结点编号、结束结点编号[层次,开始编号,结束编号]
  3. 如果出队结点的层次与当前层次不同,则更新当前节点的层次。并根据obj中的开始、结束编号,记录上一层的宽度。与当前最大层次比较,取较大值
  4. 注意:每当遍历到下一层第一个节点时,才会记录上一层的宽度。因此最后需要单独计算最后一层的宽度。

注意: 答案在32位有符号整数的表示范围内。 

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
 //1.遍历二叉树,将非空的结点存到队列,其中一个元素表示为[结点,层次,结点编号]
 //2.obj中记录当前的层次、当前层次的开始结点编号、结束结点编号[层次,开始编号,结束编号]
 //3. 如果出队结点的层次与当前层次不同,则更新当前节点的层次。
 //   并根据obj中的开始、结束编号,记录上一层的宽度。与当前最大层次比较,取较大值
 //4. 注意:每当遍历到下一层第一个节点时,才会记录上一层的宽度。因此最后需要单独计算最后一层的宽度。
var widthOfBinaryTree = function(root) {
    let res = 0;
    if(!root) return res;
    let que = [[root, 0, 1]]; //[节点,层次,编号]
    let obj = { //当前的层次,开始编号,结束编号
        level : 0,
        start : 1,
        end : 1 
    }
    let can = Math.pow(2,32)-1;
    while(que.length){
        //1. 出队
        let [node, level, idx] = que.shift();
        //2. 判断层次是否变化
        if(obj.level === level){//层次未变,更新当前层的结束节点编号
            obj.end = idx;
        }else{// 层次变化,计算当前最大宽度。更新层次,开始结束编号
            res = Math.max(res, (obj.end-obj.start+1));
            obj.level = level;
            obj.start = idx; //当前层的第一个节点即为开始编号
            obj.end = idx; //在遍历中更新 结束编号
        }
        //3. 将非空左右孩子入队
        if(node.left) {que.push([node.left, level+1, (idx*2)%can])}
        if(node.right) {que.push([node.right, level+1, (idx*2+1)%can])}
    }
    //4. 计算最后一层的宽度
    res = Math.max(res, obj.end-obj.start+1);
    return res;
};

2、手撕快排

/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 * 将给定数组排序
 * @param arr int整型一维数组 待排序的数组
 * @return int整型一维数组
 */
function MySort( arr ) {
    Quicksort(arr,0,arr.length-1);
    return arr;
}
function Quicksort(arr, low, high){
		let l = low;
		let h = high;
		if(l < h){
			let p = arr[l];
			while(l<h){
				while(l<h && arr[h]>=p) h--;
				if(l<h) arr[l] = arr[h];
				while(l<h && arr[l]<=p) l++;
				if(l<h) arr[h] = arr[l];
			}
			arr[l] = p;
			Quicksort(arr,low,l-1);
			Quicksort(arr,l+1,high);
		}
}
module.exports = {
    MySort : MySort
};

3、手撕冒泡排序

function BubbleSort(arr){
		for(let i=arr.length-1;i>0;i--){
			let isChange = false; //未交换为false
			for(let j=0;j<i;j++){
				if(arr[j]>arr[j+1]){
					let temp = arr[j];
					arr[j] = arr[j+1];
					arr[j+1] = temp;
					isChange = true;
				}	
			}
			if(!isChange) break; //本趟未交换,则直接退出
		}
	}

4、岛屿的最大面积

/**
 * @param {number[][]} grid
 * @return {number}
 */
var maxAreaOfIsland = function(grid) {
    //深度优先遍历。每次遍历完将其置为0
    let res = 0;
    for(let i=0;i<grid.length;i++){
        for(let j=0; j<grid[0].length; j++){
            if(grid[i][j] === 1){
               let count = dfs(grid, i, j);
               res = Math.max(count,res);
            }
        }
    }
    return res;
};

function dfs(grid, x, y){
    //1. 递归出口:下标越界 或 遇到水,即grid[x][y]===0,直接返回0
    if(x<0 || x>=grid.length || y<0 || y>=grid[0].length || grid[x][y]===0){
        return 0;
    }
    grid[x][y] = 0; //将访问过的陆地记位0,避免重复访问
    let count = 1; //记录访问过的陆地数目
    count+= dfs(grid, x+1, y);
    count+= dfs(grid, x-1, y);
    count+= dfs(grid, x, y+1); 
    count+= dfs(grid, x, y-1);
    return count;

}

5、二叉树层序遍历

function levelOrder( root ) {
    let res = [];
    let que = [];
    if(!root) return res;
    que.push(root);
    while(que.length>0){
        let len = que.length;
        let tem = [];
        for(let i=0; i<len; i++){
            let node = que.shift();
            tem.push(node.val);
            if(node.left) que.push(node.left);
            if(node.right) que.push(node.right);
        }
        res.push(tem);
    }
    return res;

}

6、最长公共子串(子串是连续的)

思路:

  1. 公共子串肯定在较短的字符串中,用str1存储较短的字符串;
  2. 先从最大长度为1,str1中开始查找;
  3. 只要str2中存在当前子串,则最大长度+1,继续向后查找
function LCS( str1 ,  str2 ) {
    let res = '';
    //1. 确定最短字符串
    if(str1.length > str2.length){
        [str1, str2] = [str2, str1];
    }
    //2.最短字符串长度,当前的最大长度
    let len = str1.length;
    let maxlen = 0; //表当前的最大长度-1的值
    //3.遍历最短字符串str1,寻找最长子串
    for(let i=0; i<len; i++){
        //4.获取当前终点为i,长度maxlen+1的子串
        let s = str1.slice(i-maxlen, i+1);
        //5. 为str2的子串,继续将最大长度+1
        if(str2.indexOf(s)!=-1){
            maxlen++;
            res = s;
        }
    }
    return res;
}

7、 找出连续的最长递增子序列

  1. count 为当前元素峰值,ans为最大峰值
  2. 初始化 count = 1
  3. 从 0 位置开始遍历,遍历时根据前后元素状态判断是否递增,递增则 count++,递减则 count=1
  4. 如果 count>ans,则更新 ans
  5. 直到循环结束

(1)找出连续的最长递增子序列的元素个数

var findLengthOfLCIS = function(nums) {
    //1. 数组长度<=1
    if(nums.length<=1){
        return nums.length;
    }
    let res = 0;
    let count = 1;
    //2. 遍历数组。下一个元素递增,则count+1;否则count=1,重新计数
    for(let i=0;i<nums.length-1;i++){
        if(nums[i]<nums[i+1]){
            count++;
        }else{
            count=1;
        }
        res = Math.max(count,res);
    }
    return res;
};

(2)找出连续的最长递增子序列:添加一个开始和结束下标

var findLengthOfLCIS = function(nums) {
    //1. 数组长度<=1
    if(nums.length<=1){
        return nums.length;
    }
    let res = 0;
	let resArr = []; //记录最长连续序列
    let count = 1;
	let start = 0; //当前连续序列的开始下标
	let end = 0; //当前连续序列的结束下标
    //2. 遍历数组。下一个元素递增,则count+1;否则count=1,重新计数
    for(let i=0;i<nums.length-1;i++){
        if(nums[i]<nums[i+1]){ //2.1 连续
            count++;
			end++; //连续序列的结束下标+1
        }else{//2.2 不连续。重置当前连续序列的开始和结束下标
			start = i;
			end = i;
            count=1;
        }
		if(count>res){ //3. 更新当前最长的连续序列
			res = count;
			resArr = nums.slice(start,end+1);
		}
    }
    return resArr;
};

 

8、找出不连续的最长递增子序列(思路)

思路:

一共需要两个辅助数组和一个辅助变量:
dp数组:用来存储位置i对应的最长子序列的长度
end数组:有多个长度为i的子序列,存储最后一个元素最小的子序列,有更大的机会来扩展递增子序列的元素
len:用来记录当前找到的最长子序列的长度

举个例子
[3,2,5,8,6,7]
end数组:
i=0: 3	长度为1的子序列为:3
		end=[3]
i=1:2<3 长度为1的子序列为:2(用2替换3,因为这是是最容易使子序列扩展为长度为2的子序列的值)
		 end=[2]
i=2:5>2	所以长度为1的子序列可以扩展成一个长度为2的子序列
		长度为1的子序列为:2
		长度为2的子序列为:2 5
		end=[2 5]
i=3:8>5 所以长度为2的子序列可以扩展成一个长度为3的子序列
		长度为1的子序列为:2
		长度为2的子序列为:2 5
		长度为3的子序列为:2 5 8
		end=[2 5 8]
i=4:6>5&&6<8 所以长度为2的子序列可以扩展成一个长度为3的子序列
		长度为1的子序列为:2
		长度为2的子序列为:2 5
		长度为3的子序列为:2 5 6(用6替换8,因为只是最容易使子序列扩展为长度为4的子序列的值)
		end=[2 5 6]
i=5:7>6 所以长度为3的子序列可以扩展成一个长度为4的子序列
		长度为1的子序列为:2
		长度为2的子序列为:2 5
		长度为3的子序列为:2 5 6
		长度为4的子序列为:2 5 6 7	
		end=[2 5 6 7]
笔记:我们用end[i]来存储长度为i+1的子序列的最后一个元素的最小值,因为这是最有希望使子序列扩展的值
当a[x]>end[i]时,那么长度为i+1的子序列必定可以扩展成一个i+2的子序列,如果(old=end[i+1])>a[x],那么就令
end[i+1]=a[x],他的含义是只要a[y]>a[x]即使a[y]<old,那么长度为i+2的子序列也必然可以扩展为一个长度为i+3的子序列。
所有最终结束时end数组为[2,5,6,7]即end[i]表示长度为(i+1)的子序列的最后一个元素值。


遍历结束后也会生成    dp数组:
arr:[3,2,5,8,6,7]
dp :[1 1 2 3 3 4]
len=4
dp[i]表示到arr[i]为止最长的递增子序列,得到字典序最小的元素只需要倒过来遍历dp,依次输出最先遇到的长度为len,len-1,len-2....1的arr元素即可。
res用来存储字典序最小的最长子序列。
即:    len=4  dp[5]==4 ==>res[3]=arr[5]=7
	len=3  dp[4]==3 ==>res[3]=arr[4]=6
		 dp[3]==3  pass因为需要的是字典序最小的
    	len=2  dp[2]==2 ==>res[3]=arr[2]=5
	len=1  dp[1]==1 ==>res[3]=arr[1]=2
		 dp[0]==1  pass因为需要的是字典序最小的
	结果:res=[2,5,6,7]
笔记:设dp[x]==dp[y]&&x<y那么a[x]必定大于a[y],因为如果a[x]<a[y],那么dp[x]对应的子序列必定可以扩展为长度dp[x]+1的子序列即dp[y]=dp[x]+1>dp[x],矛盾,所以a[y]<a[x]

9、链表反转,分别用遍历与递归实现

(1)遍历

function ReverseList(pHead){
    if(pHead==null || pHead.next==null){
        return pHead;
    }
    let h = null;
    let p = null
    while(pHead){
        p = pHead.next;
        pHead.next = h;
        h = pHead;
        pHead = p;
    }
    return h;
}

(2) 递归

终止条件:链表为空,或者 链表只有一个节点时,直接返回。

if (head == null || head.next == null)
    return head;

思路:

1. 若链表为空,或者 链表只有一个节点时,直接返回

2. 翻转当前head节点后面的节点,即递归翻转head.next

3. 再将head节点连接到 翻转后的head.next的尾部

function ReverseList(pHead){
    if(pHead==null || pHead.next==null){
        return pHead;
    }
    // reverse指向翻转后的第一个节点
    let reverse = ReverseList(pHead.next); 
    // pHead.next指向翻转后的最后一个节点
    pHead.next.next = pHead;
    pHead.next = null;
    return reverse;
}

10、链表求和 (只能使用递归,不准提前反转链表,也不借用其他的数据结构如栈)

var addTwoNumbers = function(l1, l2) {
    // 存储相加结果的链表
    let head = new ListNode(0);
    getRes(head, l1, l2, 0)
    return head.next;
};
/*
head: 结果链表;l1、l2链表相加;carry:进位
* */
function getRes(head, l1, l2, carry){
    //1. 递归退出条件
    if(l1==null && l2==null && carry===0){
        return;
    }
    //2. 计算当前节点相加的和
    let sum = (l1!=null ? l1.val:0)+(l2!=null ? l2.val:0)+carry;
    head.next = new ListNode(sum%10);
    carry = Math.floor(sum/10);
    //3. 递归计算下一节点的值
    getRes(head.next, l1!=null ? l1.next:null, l2!=null?l2.next:null,carry);
}

11、是否是回文链表

(1)使用栈。时间复杂度O(n),空间复杂度O(n)

var isPalindrome = function(head) {
    //1. 先将节点压入栈中
    let sta = [];
    while(head){
        sta.push(head.val);
        head = head.next;
    }
    //2. 分别从栈头和栈尾开始比较
    let start = 0, end = sta.length-1;
    while(start<end){
        if(sta[start]!== sta[end]){
            return false;
        }
        start++;
        end--;
    }
    return true;
};

(2)使用快慢指针。时间复杂度O(n), 空间复杂度O(1)

    public boolean isPalindrome(ListNode head) {
        if(head == null || head.next == null) {
            return true;
        }
        ListNode slow = head, fast = head;
        ListNode pre = head, prepre = null;

      //1. 寻找中点,同时反转前半部分的链表
        while(fast != null && fast.next != null) {
            pre = slow;
            slow = slow.next;
            fast = fast.next.next;

            pre.next = prepre; //头插法反转链表
            prepre = pre;
        }
        
        //2. 元素个数为奇数时,使slow指向后半部分的第一个节点
        if(fast != null) { 
            slow = slow.next;
        }
        //3.判断回文子串。pre指向前半部分第一个节点,slow指向后半部分的第一个节点
        while(pre != null && slow != null) { 
            if(pre.val != slow.val) {
                return false;
            }
            pre = pre.next;
            slow = slow.next;
        }
        return true;
    }

12、找数组中重复数字

(1) 使用set。 空间复杂度O(n)

var findRepeatNumber = function(nums) {
    let set = new Set();
    for(let i=0;i<nums.length;i++){
        if(set.has(nums[i])){return nums[i];}
        set.add(nums[i]);
    }
};

(2)原地交换 。空间复杂度O(1)

Picture0.png

var findRepeatNumber = function(nums) {
    let i = 0;
    while(i<nums.length){
        //1. 下标i 与其存储的值相等
        if(nums[i] === i){
            i++;
            continue;
        }
        //2. 出现重复的值。下标nums[i] 和 i处存储的元素相等(nums[i]和i不相等)
        if(nums[nums[i]] === nums[i]){ 
            return nums[i];
        }
        //3. 未出现重复值,将当前元素交换到与其值对应的下标处
        let temp = nums[i];
        nums[i] = nums[temp]; 
        nums[temp] = temp; // 相当于 nums[nums[i]] = nums[i]. 即nums[a] = a,将元素存到对应下标的位置
    }
    return -1;
};

13、根据前序、中序,重建二叉树

var buildTree = function(preorder, inorder) {
    // 根据前序,可确定根节点。由根节点,将前序和中序序列分为左右两部分,递归创建二叉树
    let n = preorder.length;
    return myTree(preorder,0,n-1,inorder,0,n-1);
};
// 参数:前序序列,当前的开始结束下标;中序序列,当前的开始结束下标
function myTree(preorder,l1,h1,inorder,l2,h2){
    if(l1>h1){
        return null;
    }
    //1. 确定根节点
    let rootVal = preorder[l1];
    let root = new TreeNode(rootVal);
    //2. 寻找根节点在中序中的位置
    let mid = 0;
    for(let i=l2; i<=h2; i++){
        if(inorder[i] === rootVal){
            mid = i;
            break;
        }
    }
    //3. 递归的创建根节点的左右孩子
    let leftNum = mid-l2; //左节点数目
    let rightNum = h2-mid; //右结点数目
    //3.1 左孩子
    root.left = myTree(preorder,l1+1,l1+leftNum,inorder,l2,mid-1);
    //3.2 右孩子
    root.right = myTree(preorder,h1-rightNum+1,h1,inorder,mid+1,h2);
    return root;
}

14、二叉树的右视图

力扣:https://leetcode-cn.com/problems/binary-tree-right-side-view/

思路:层次遍历,记录每层的最后一个元素。时间和空间复杂度均为O(n)

var rightSideView = function(root) {
    if(!root){
        return [];
    }
    let res = [];
    let que = [root];

    while(que.length){
        let len = que.length;
        for(let i=0; i<len; i++){
            let node = que.shift();
            if(node.left){que.push(node.left);}
            if(node.right){que.push(node.right);}
            if(i==len-1) res.push(node.val);
        }
    }
    return res;
};

15、二分查找

var search = function(nums, target) {
    if(nums.length < 1) return -1; 
    let l = 0, h=nums.length-1;
    while(l<=h){ //要写<=
        let mid = Math.floor((l+h)/2);
        if(nums[mid]===target){
            return mid;
        }else if(nums[mid] < target){
            l = mid+1;
        }else{
            h = mid-1;
        }
    }
    return -1;
};

16、求二叉树的深度,不使用递归

(1) 递归

var maxDepth = function(root) {
    if(!root) return 0;
    return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
};

(2) 非递归。使用层次遍历,每遍历一层,计数器+1。

var maxDepth = function(root) {
    if(!root) return 0;
    let que = [root];
    let res = 0;
    while(que.length){
        let len = que.length;
        for(let i=0; i<len; i++){
            let node = que.shift();
            if(node.left) {que.push(node.left)}
            if(node.right) {que.push(node.right)}
        }
        res++;
    }
    return res;
};

17、两个栈实现队列

var s1 = [];
var s2 = [];
//1. 实现入队num
function push(num){
    s1.push(num);
}
//2. 实现出队
function pop(){
    if(!s2.length){
        while(s1.length){
            s2.push(s1.pop());
        }
    }
    return s2.pop();
}

18、全排列

const permute = (nums) => {
  const res = []; //结果集
  const used = {}; //访问标记数组

  function dfs(path) {
    if (path.length == nums.length) { // 个数选够了
      res.push(path.slice()); // 拷贝一份path,加入解集res
      return;                 // 结束当前递归分支
    }
    for (const num of nums) { // for枚举出每个可选的选项
      // if (path.includes(num)) continue; // 别这么写!查找的时间是O(n),增加时间复杂度
      if (used[num]) continue; // 使用过的,跳过

      path.push(num);         // 选择当前的数,加入path
      used[num] = true;       // 记录一下 使用了
      dfs(path);              // 基于选了当前的数,递归
      
      path.pop();             // 上一句的递归结束,回溯,将最后选的数pop出来
      used[num] = false;      // 撤销这个记录
    }
  }

  dfs([]); // 递归的入口,空path传进去
  return res;
};

19、排序时间复杂度

除了堆排序、二路归并排序的时间复杂度为O(nlog2n),

其余都为O(n^2),如:插入、冒泡、选择、快速。

20、螺旋矩阵

function spiralOrder( matrix ) {
    let res = [];
    if(matrix.length ==0 ){
        return res;
    }
    let top=0, bottom=matrix.length-1, left=0, right=matrix[0].length-1;
    while(top < Math.floor((matrix.length+1)/2) && left < Math.floor((matrix[0].length+1)/2))
    { //1. 上面:左-》右
        for(let i=left; i<=right; i++){
            res.push(matrix[top][i]);
        }
        //2. 右面:上-》下
        for(let i=top+1; i<=bottom; i++){
            res.push(matrix[i][right]);
        }
        //3. 下面:右-》左
        for(let i=right-1; i>=left && top!=bottom; i--){
            res.push(matrix[bottom][i]);
        }
        //4. 左面:下-》上
        for(let i=bottom-1; i>=top+1 && left!=right; i--){
            res.push(matrix[i][left]);
        }
        ++top;
        ++left;
        --bottom;
        --right;
    }
    return res;
}

 

用链表实现一个队列

合并区间

合并两个有序链表,不能使用循环

反转一个字符串中的单词顺序,中间的空格如果有多个则合并为一个(filter

最长的有效括号(难)

滑动窗口最大值(难)

实现sqrt函数,结果保留两位小数

给一个有序整数数组A[0...n],和一个整数target.返回A中与target的差的绝对值最小的元素的下标,若存在多个则全都要输出.

给一个数组,找出数组中任意三个数的最大乘积


ES6一般常用考点是 Promiselet/const()=>async/await

栈和队列的区别,在操作系统的应用?

vue中v-for的key值是做什么的,仅仅使用数组下标作为key值有什么缺点

 0.1+0.2 !== 0.3问题,为什么,怎么解决

1、二维数组回行打印

3、给你一个二叉树,从上往下看,然后左往右顺序输出你能看到节点,同一个竖直方向上上面的节点把下面的节点遮挡住了

4、链表反转,分别用遍历与递归实现

 

6、是否是回文链表

7、错位的全排列(第一位不能是1,第二位不能是2)

2. 其它

1. 手写Promise.all()

全部成功才会成功;只有一个失败,就失败。

核心思路:

  1. Promise.myAll()返回的肯定是一个promise对象,所以可以直接return new Promise((resolve, reject) => {});
  2. 然后遍历传入的Promise数组,用Promise.resolve()将参数"包一层",使其变成一个promise对象;
  3. 如果当前的Promsie[i]成功,则将其成功结果存入结果数组res,计数器+1(计数器记录当前成功的Promise数量);
  4. 通过比较计数器count和Promise总数,判断当前所有的Promise是否全部成功;
  5. 若全部成功,则resolve;遇到一个不成功,则reject;
  • 一些细节:
  1. 官方规定Promise.all()接受的参数是一个可遍历的参数,所以未必一定是一个数组,所以用Array.from()转化一下
  2. 使用for…of进行遍历,因为凡是可遍历的变量应该都是部署了iterator方法,所以用for…of遍历最安全
// 一、手写promise.all方法。 参数promiseArr: Promise数组
	Promise.myAll = function (promiseArr) {
		//1. 用于计数,表示已成功的Promise对象个数。若等于len,则成功,就resolve
		let count = 0;
		//2. Promise数组的长度
		let len = promiseArr.length;
		let res = []; //存放成功结果
		//3. Promise.myAll()返回的是一个promise对象,故直接return Promise
		return new Promise((resolve, reject) => {
			//4. 遍历Promise数组
			for (let i in promiseArr) {//i为索引
				Promise.resolve(promiseArr[i]).then( //使其变成一个promise对象
					//5. 如果 promiseArr[i] 成功
					result => {
						res[i] = result;
						//6. Promise数组中全部成功,返回所有元素的计算结果
						if (++count === len) {
							resolve(res);
						}
					}).catch(err => { //7. 如果 promiseArr[i] 失败,则直接reject
						reject(err);
					})
			}
		})
	}
	//二、测试
	var promise1 = Promise.resolve(1);
	var promise2 = new Promise((resolve, reject) => {
		resolve('hello');
	});
	var promise3 = 42;

	// 打印结果:[1, "hello", 42]
	Promise.myAll([promise1, promise2, promise3]).then(result => { console.log(result) })

2、手写Promise.race()

const p = Promise.race([p1, p2, p3]);

只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。

核心思路:
谁先决议那么就返回谁,所以将all的计数器和逻辑判断全部去除掉就可以了。

// 一、手写promise.race方法。 参数promiseArr: Promise数组
	Promise.myRace = function(promiseArr){
		return new Promise((resolve, reject) => {
			//1. 遍历promiseArr数组
			for(let i in promiseArr){
				Promise.resolve(promiseArr[i]).then( // 使其变成一个promise对象
					result => { //2. 成功
						resolve(result);
					}).catch( err => { //3. 失败
						reject(err);
					})
			}
		})
	}

	//二、测试
	var promise1 = new Promise(function(resolve, reject) {
    	setTimeout(resolve, 500, 'one'); // 500ms后,resolve('one')
	});

	var promise2 = new Promise(function(resolve, reject) {
    	setTimeout(resolve, 100, 'two'); // 100ms后,resolve('two')
	});

	// 打印结果:two
	Promise.myRace([promise1, promise2]).then(result => { console.log(result) })

 3、用非递归的方式实现数组扁平化:some+展开运算符

function flatten(arr) {
		while(arr.some( item => Array.isArray(item))){ //数组arr中有一项是数组
			arr = [].concat(...arr);
		}
		return arr;
	}

4、用递归的方式实现数组扁平化

function flatten(arr) {
		let res = [];
		for (let i = 0; i < arr.length; i++) {
			if (Array.isArray(arr[i])) { //1.是数组,递归
				res = res.concat(flatten(arr[i]));
			} else { //2. 不是数组,直接存入结果集
				res.push(arr[i]);
			}
		}
		return res;
	}

5、⭐解析url中的字符串a=1&b=2&b=3&c=1....,转化为对象

问题:解析URL:"a=1&b=2&b=3&c=4&d=&d=&e=%E4%BD%A0%E5%A5%BD&b=8"

    解析结果:

    a: "1"

    b: ["2", "3", "8"]

    c: "4"

    d: ""

    e: "你好"

解决:用decodeURIComponent方法进行解析

function getUrlObject(url) { //url:"a=1&b=2&b=3&c=1...."
		let res = {}; //存放解析url后对应的对象
		let items = url.length ? url.split("&") : []; //按&将字符串拆分为数组

		let item=null, name=null, value=null;
		for(let i=0; i<items.length; i++){ //遍历字符数组
			item = items[i].split("="); //将每一项拆分。例:a=1拆分为[a,1]
			if(item[0].length){//键不为空
				name = decodeURIComponent(item[0]); //键。解析item[0]
				value = decodeURIComponent(item[1]); //值。解析item[1]
				if(res[name]){ //该键出现多次,将该键对应的值合并为数组
					res[name] = [...res[name]].concat(value);
				}else{ //该键第一次出现
					res[name] = value;
				}
				
			}
		}
		return res;
	}
	
	console.log(getUrlObject("a=1&b=2&b=3&c=4&d=&d=&e=%E4%BD%A0%E5%A5%BD&b=8"));
	/*
	解析结果:
	a: "1"
	b: ["2", "3", "8"]
	c: "4"
	d: ""
	e: "你好"
	*/

6、用js手动实现一个new操作符

首先要知道 new 操作符都做了什么事,即构造函数的内部原理:

  1. 创建一个新对象;
  2. 链接到原型(将构造函数的 prototype 赋值给新对象的 __proto__);
  3. 绑定this(构造函数中的this指向新对象并且调用构造函数);
  4. 返回新对象;

这样我们就可以手动实现一个 new 方法了:

//一、手动实现new
function myNew(){
	//1. 创建一个新对象
	let obj = {};
	//2. 获取构造函数。即arguments的第一项
	let Con = [].shift.call(arguments);
	//3.将新对象链接到原型链
	obj.__proto__ = Con.prototype;
	//4.绑定构造函数的this,使this执行obj对象。并传参,为obj的属性赋值
	let res = Con.apply(obj,arguments);
	//5. 确保最后得到的是一个对象
	return typeof res==="object" ? res : obj;
}

 测试:

//二、测试
function Person (name,age){
    this.name = name;
    this.age = age;
    this.say = function () {
        console.log("I am " + this.name)
    }
}
 
//通过new创建构造实例
let person1 = new Person("Curry",18);
console.log(person1.name);      //"Curry"
console.log(person1.age);       //18
person1.say();      //"I am Curry'
 
//通过myNew()方法创造实例
let person2 = myNew (Person,"Curry",18);
console.log(person2.name);      //"Curry"
console.log(person2.age);       //18
person2.say();      //"I am Curry'

详细解释:

我们实现的 myNew() 方法需要传入的参数是:构造函数 + 属性

1、首先我们创建一个新对象;

2、然后通过 arguments 类数组我们可以知道参数中包含了构造函数以及我们调用myNew时传入的其他参数;

3、然后得到 构造函数和其他的参数,由于arguments是类数组,没有直接的方法可以供其使用,我们可以有以下两种方法获取构造函数:

  • Array.from(arguments).shift();  转换成数组 使用数组的方法 shift 将第一项弹出
  • [].shift().call(arguments) ;  通过 call() 让arguments能够借用shift()方法

4、绑定this的时候需要注意:

  • 给构造函数传入属性,注意构造函数的this属性
  • 参数传进 Con 对 obj 的属性赋值,this要指向 obj 对象
  • 在 Con 内部手动指定函数执行时的this ,使用call、apply实现

5、最后我们需要返回一个对象
 

7、用setTimeout实现一个setInterval

(1)setTimeout和setInterval的区别:

  • setInterval:每隔1s打印一次,执行多次。
const timer1 = setInterval(() => {
    console.log(1)
}, 1000)
  •  setTimeout:2s之后打印,只执行依次。
const timer1 = setTimeout(() => {
    console.log(2)
}, 2000)

(2)用setTimeout实现一个setInterval(用递归)

	//1. 用setTimeout实现一个setInterval
	function mysetInterval() {
		let timer1 = setTimeout(() => {
			console.log(2);
			mysetInterval(); //通过递归,重复调用
		}, 1000)
	}

	//调用。每隔2s,打印一次
	mysetInterval(); 

8、手写原生call

  1. 先判断this是不是函数
  2. 将函数设为对象的属性;
  3. 指定this到函数,并传入给定参数执行函数;
  4. 执行之后删除这个函数;
//一、手写原生call方法
	Function.prototype.mycall = function (obj, ...args) {
		// 1. 判断this是否函数,否返回typeerror
		if (typeof this !== 'function') {
			throw new TypeError('不是函数');
		}
		//2. 绑定this。(obj.fn为要执行的函数,例person)
		obj.fn = this;

		//3. 执行函数,获取结果。
		const res = obj.fn(...args);

		//4. 获取到结果后,删除函数
		delete obj.fn;
		//5. 返回结果
		return res;
	}

	// 二、测试
	function person() {
		console.log(this.name); //打印this的name属性值,即mycall绑定的this
		if (arguments.length < 1) return;
		for (var i = 0; i < arguments.length; i++) {
			console.log(arguments[i])//打印参数
		}
	}

	var p = { name: "小明" };
	//通过mycall方法,执行person方法,绑定其this为p,并传参
	person.mycall(p, '我', '爱', '中国'); 

9、手写原生apply

10、手写原生bind

 

手写原型链继承、以及其它的继承方式

10、手写防抖节流实现

 

 

 

 



这篇关于1、猿辅导前端面试算法题的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程