阅读 721

常见的手写面试题: 方法实现、算法

实现简易版方法

call
  • 参数: 一个或多个参数;第一个为对象,后面若干为数据。
  • 执行方式: 立刻执行。
  • 实现原理: 在对象上添加一个属性,属性的值为当前对象,这样可以改变当前对象的指向了。

举例子

function showName() {
    console.log(name);
}
let person = {
    name: 'mochixuan',
    age: 20
}
person.temp = showName;
person.temp(); //调用
复制代码

实现

Function.prototype.call1 = function () {
    // 数组解构,获取参数
    let [thisArgs,...args] = [...arguments];
    thisArgs = thisArgs || window;
    // 使用Symbol可以防止原有属性被替换。
    const tempArg = Symbol('call1');
    thisArgs[tempArg] = this;
    let result;
    if (args == undefined) {
        result = thisArgs[tempArg]()
    } else {
        result = thisArgs[tempArg](...args)
    }
    delete thisArgs[tempArg]
    return result;
}
复制代码
apply
  • 参数: 参数一个或多个参数,第一个为对象,后面为数组的数据。
  • 执行方式: 立刻执行。
  • 实现原理: 在对象上添加一个属性,属性的值为当前对象,这样可以改变当前对象的指向了。
Function.prototype.apply1 = function (thisArgs,args) {
    thisArgs = thisArgs || window;
    const tempArg = Symbol('apply1');
    thisArgs[tempArg] = this;
    let result;
    if (args == undefined) {
        result = thisArgs[tempArg]()
    } else {
        result = thisArgs[tempArg](...args)
    }
    delete thisArgs[tempArg]
    return result;
}
复制代码
bind
  • 参数: 参数一个或多个参数,第一个为对象,后面为的数据。
  • 执行方式: 返回函数,函数被调用后执行。
  • 实现原理: 生成一个函数,函数内部使用call改变this指向。
  • 注意事项: 生成的bind函数,如果使用new进行实例化,this还是指向原来的对象。
Function.prototype.bind1 = function (thisArg) {
    if (typeof this !== 'function') return new TypeError(this+'must be function')
    const arg1 = Array.prototype.slice.call(arguments,1);
    const self = this;
    const bound = function (args) {
        const arg2 = Array.prototype.slice.call(arguments);
        const allArgs = arg1.concat(arg2);
        if (this instanceof bound) {
            if (self.prototype) {
                function TempFC() {}
                TempFC.prototype = self.prototype;
                bound.prototype = new TempFC();
            }
            return self.apply(this,allArgs);
        } else {
            return self.apply(thisArg,allArgs);
        }
    }
    return bound;
}
复制代码
深拷贝
  • 实现原理: 递归。
  • 注意事项: 1. 反正循环引用造成死循环。2. Date、正则表达式、函数要进行特殊处理这里就不考虑了。
  • 建议项目开发使用:lodash里的方法。

死循环例子

let a = {x: 1,y: 2};
a.z = a;
复制代码
// Map: Map对象保存键值对,类似于数据结构字典;与传统上的对象只能用字符串当键不同,Map对象可以使用任意值当键
// WeakMap: 对象保存键值对,与Map不同的是其键必须是对象,因为键是弱引用,在键对象消失后自动释放内存.
function deepCopy(data,map = new WeakMap()) {
    if (!data || typeof data !== 'object') return data; // null也是object

    const result = Array.isArray(data) ? [] : {};

    // 解决死循环问题
    if (map.has(data)) {
        return map.get(data);
    } else {
        map.set(data,result);
    }

    for (let key in data) {
        if (key != null && data.hasOwnProperty(key)) { //原型链上的可枚举属性,去除原型链上的数据
            if (typeof data === 'object') {
                result[key] = deepCopy(data[key],map);
            } else {
                result[key] = data[key];
            }
        }
    }

    return result;
}
复制代码
浅比较: PureComponent里用到了
  • 实现原理: 对象则比较第一层数据,基本数据类型直接比较,对象比较引用。
// 该方法会忽略掉那些从原型链上继承到的属性
const hasOwnProperty = Object.prototype.hasOwnProperty 
function is(x, y) {
    // === 严格判断适用于对象和原始类型。但是有个例外,就是NaN和正负0。
    if (x === y) {
        //这个是个例外,为了针对0的不同,譬如 -0 === 0 : true
        // (1 / x) === (1 / y)这个就比较有意思,可以区分正负0, 1 / 0 : Infinity, 1 / -0 : -Infinity
        return x !== 0 || y !== 0 || 1 / x === 1 / y
    } else {
        // 这个就是针对上面的NaN的情况: parseInt('abc')  // NaN
        return x !== x && y !== y
    }
}


function shallowEqual(objA, objB) {

    // Object.is()
    if (is(objA, objB)) {
        return true;
    }

    // null 也是对象
    //下面这个就是,如果objA和objB其中有个不是对象或者有一个是null, 那就认为不相等。
    //不是对象,或者是null.我们可以根据上面的排除来猜想是哪些情况:
    //有个不是对象类型或者有个是null,那么我们就直接返回,认为他不同。其主要目的是为了确保两个都是对象,并且不是null。
    if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
        return false
    }

    var keysA = Object.keys(objA);
    var keysB = Object.keys(objB);

    if (keysA.length !== keysB.length) {
        return false;
    }

    //这里只比较了对象A和B第一层是否相等
    for (var i = 0; i < keysA.length; i++) {
        if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
            return false;
        }
    }

    return true;
}
复制代码

常见算法

冒泡排序
  • 原理: 比较相邻的元素。如果第一个比第二个大,就交换他们两个.
  • 时间复杂度: O(n^2)
function bubbleSort(items) {
    for (let i = 0 ; i < items.length - 1; i++) {
        for (let j = 0 ; j < items.length - 1 - i ; j++) {
            if (items[j] > items[j+1]) {
                let temp = items[j];
                items[j] = items[j+1];
                items[j+1] = temp;
            }
        }
    }
    return items;
}
复制代码
选择排序
  • 规则: 在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止
  • 时间复杂度O(n^2)
  • 交换次数比冒泡排序少,性能上要好于冒泡排序
function selectSort(items) {
    for (let i = 0 ; i < items.length ; i++) {
        let tempIndex = i;
        for (let j = i+1 ; j < items.length ; j++) {
            if (items[j] < items[tempIndex]) {
                tempIndex = j;
            }
        }
        if (i != tempIndex) {
            let temp = items[i];
            items[i] = items[tempIndex];
            items[tempIndex] = temp;
        }
    }
    return items;
}
复制代码
快速排序
  • 原理: 二分法,递归实现,每次得到一个正确的位置。
  • 时间复杂度: O(nlogn).
function quick(items,low,high) {
    if (low == null && high == null) {
        low = 0;
        high = items.length - 1;
    }
    if (low >= high) return items;
    let middle = getMiddle(items,low,high);
    if (middle > low) quick(items,low,middle-1);
    if (middle < high) quick(items,middle+1,high)
    return items;
}

function getMiddle(items,start,end) {
    let temp = items[start];
    while (start < end) {
        while (start < end && items[end] >= temp) --end;
        items[start] = items[end];
        while (start < end && items[start] <= temp)  ++start;
        items[end] = items[start];
    }
    items[start] = temp;
    return start;
}
复制代码
二叉树查找: 最大值、最小值、固定值
  • 原理: 遍历
  • 时间复杂度
function queryMinMaxFix(node,fix,result) {

    if (result == null) result = {};

    if (node != null) {
        if (result.min == null) result.min = node.value;
        if (result.max == null) result.max = node.value;
        if (result.fix == null) result.fix = [];

        if (node.value < result.min) result.min = node.value;
        if (node.value > result.max) result.max = node.value;
        if (node.value == fix) result.fix.push(node);

        if (node.left != null) queryMinMaxFix(node.left, fix, result);
        if (node.right != null) queryMinMaxFix(node.right, fix, result);
    }

    return result;
}
复制代码
二叉树遍历
  • 原理: 递归
function traversal(node,tempOrderTraversal) {
    if (node != null) {
        // tempOrderTraversal.push(node.value) 前序遍历
        if (node.left != null) {
            preOrderTraversal(node.left,tempOrderTraversal)
        }
        // tempOrderTraversal.push(node.value) 中序遍历
        if (node.right != null) {
            preOrderTraversal(node.right,tempOrderTraversal)
        }
        // tempOrderTraversal.push(node.value) 后序遍历
    }
}
复制代码

不能使用递归时,则使用栈就是JS的数组push、pop

// 非递归遍历
var kthSmallest = function(root, k) {
    const tempArr = [];
    let result;
    tempArr.push(root);
    while (tempArr.length > 0) {
        result = tempArr.pop();
        if (result.value == k) break;
        if (result.left != null) tempArr.push(result.left);
        if (result.right != null) tempArr.push(result.right);
    }
    return result;
};

复制代码
二叉树的最大深度
  • 最大深度是从根节点到最远叶节点的最长路径上的节点数
  • 原理: 遍历
var maxDepth = function(root) {
    let nodeArrs = [];
    if (root != null) nodeArrs.push(root);
    
    let result = 0;
    while(nodeArrs.length > 0) {
      result++;  
      let temp = [];  
      for(let i = 0 ; i < nodeArrs.length ; i++) {
          if (nodeArrs[i].left != null) temp.push(nodeArrs[i].left);
          if (nodeArrs[i].right != null) temp.push(nodeArrs[i].right);
      } 
      nodeArrs = temp;  
   }     
   return result;
};
复制代码
给予链表中的任一节点,把它删除掉
  • 编写一个函数来删除单链表中的节点(尾部除外),只允许访问该节点
var deleteNode = function(node) {
    if (node.next != null) {
        node.val = node.next.val;
        node.next = node.next.next
    }
};
复制代码
链表倒叙
  • 原理: 借用临时数据。
  • 时间复杂度: n
function reserveLink(head) {
    let result = null;
    let temp = null;
    while (head) {
        temp = head.next;
        head.next = result;
        result = head;
        head = temp;
    }
    return result;
}
复制代码
如何判断一个单链表有环
  • 原理:使用两个临时指针,一个指针每次加一.next,一个每次加二.next.next,当它们相等时就是有环,一般会在一次循环后相遇如果有环的化。
  • 时间复杂度: n
  • 常见解法: Set存储每个Node节点,has存在时则闭环,不存在则不必环
function hasRing(root) {
    if(root == null) return false;
    let oneNode = root;
    let twoNode = root;
    while(twoNode) {
        if (twoNode.value === oneNode.value) {
            return true;
        }
        oneNode = oneNode.next;
        if (twoNode.next != null) {
            twoNode = twoNode.next.next;
        }
    }
    return false;
}
复制代码
给定一个有序数组,找出两个数相加为一个目标数
  • 原理: 用两个指针。一般手写出现比较多的是三个数相加等于一个数
  • 时间复杂度: n
let data = [1,3,5,9,11];
let target = 8;
output: 3,5 或者是下标
复制代码
function(data,target) {
    let i = 0;
    let j = data.length - 1;
    for( ; i < j ;) {
        if (data[i] + data[j] === target) {
            break;
        } else if (data[i] + data[j] > target) {
            j--;
        } else {
            j++;
        }
    }
    return [i,j];
}
复制代码
找出一个无序数组中出现超过一半次数的数字
  • 原理: 超过一半有个特点,假设超过一般的那个数为X,则X>n/2,可以抵消法,同性相加,异性相抵。
function (data) {
    let x = data[0];
    let total = 0;
    for(let i = 0 ; i < data.length ; i++) {
        if (x === data[i]) {
            total = total + 1;
        } else {
            total = total - 1;
        }
        
        if (total === 0 && i < data.length - 1) {
            x = data[i+1];
        }
    }
    return x;
}
复制代码

后续慢慢再加

关注下面的标签,发现更多相似文章
评论