算法

212 阅读7分钟

刷题

JS 实现千位分隔符

  1. 正则实现
function numFormat(num){
  	var res=num.toString().replace(/\d+/, function(n){ // 先提取整数部分
  	    console.log(`n:${n}`)
        return n.replace(/(\d)(?=(\d{3})+$)/g,function(m){
            console.log(`m:${m}`)
            return m + ",";
        });
  })
  return res;
}
// 输出
n:1234567894532
m:1
m:4
m:7
m:4
  1. js自带函数toLocaleString()
1234567894532 .toLocaleString()

JS实现一个无限累加的add函数

add(1) //1
add(1)(2) //3
add(1)(2)(3) //6
function add(a) {
    function sum(b) { // 使用闭包
        a = a + b; // 累加
        return sum;
    }
    sum.toString = function() { // 重写toString()方法
        return a;
    }
     return sum; // 返回一个函数
}

使下面代码正常运行

const a = [1,2,3,4,5]
a.multiply()
console.log(a) // [1,2,3,4,5,1,4,9,16,25]
Array.prototype.multiply = function() {
	return this.concat(this.map(function(v){
		return v * v
    }))
}

1.toString()报错

在JS中.点操作符意味着调用Object的属性或者这是一个浮点数。当.跟在一个数字后面就意味着这个数字是一个浮点数。js的解释器把数字后的"."偷走了(作为前面数字的小数点)

1.toString() // Uncaught SyntaxError: Invalid or unexpected token
1..toString()
1 .toString()

发布订阅模式和观察者模式

发布订阅模式

EventEmitter 的核心就是事件触发与事件监听器功能的封装。

主要是用emit发事件,用on监听事件,还有removeListener销毁事件监听者

  • 发布订阅模式中,对于发布者Publisher和订阅者Subscriber没有特殊的约束
  • 松散耦合,灵活度高
  • 易理解,可类比于DOM事件中的dispatchEvent和addEventListener
  • 当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱。

image.png

class EventEmitter {
    constructor() {
        // 维护事件及监听者
        this.listeners = {}
    }
    /**
     * 注册事件监听者
     * @param {String} type 事件类型
     * @param {Function} cb 回调函数
     */
    on(type, cb) {
        if (!this.listeners[type]) {
            this.listeners[type] = []
        }
        this.listeners[type].push(cb)
    }
    /**
     * 发布事件
     * @param {String} type 事件类型
     * @param  {...any} args 参数列表,把emit传递的参数赋给回调函数
     */
    emit(type, ...args) {
        if (this.listeners[type]) {
            this.listeners[type].forEach(cb => {
                cb(...args)
            })
        }
    }
    /**
     * 移除某个事件的一个监听者
     * @param {String} type 事件类型
     * @param {Function} cb 回调函数
     */
    removeListener(type, cb) {
        if (this.listeners[type]) {
            const targetIndex = this.listeners[type].findIndex(item => item === cb)
            if (targetIndex !== -1) {
                this.listeners[type].splice(targetIndex, 1)
            }
            if (this.listeners[type].length === 0) {
                delete this.listeners[type]
            }
        }
    }
    /**
     * 移除某个事件的所有监听者
     * @param {String} type 事件类型
     */
    removeAllListeners(type) {
        if (this.listeners[type]) {
            delete this.listeners[type]
        }
    }
}

观察者模式

在观察者模式中,只有两个主体,分别是目标对象Subject,观察者Observer。

  • 观察者需Observer要实现update方法,供目标对象调用。update方法中可以执行自定义的业务代码。
  • Subject需要维护自身的观察者数组observerList,当自身发生变化时,通过调用自身的notify方法,依次通知每一个观察者执行update方法。

image.png

// 观察者
class Observer {
    /**
     * 构造器
     * @param {Function} cb 回调函数,收到目标对象通知时执行
     */
    constructor(cb){
        if (typeof cb === 'function') {
            this.cb = cb
        } else {
            throw new Error('Observer构造器必须传入函数类型!')
        }
    }
    /**
     * 被目标对象通知时执行
     */
    update() {
        this.cb()
    }
}

// 目标对象
class Subject {
    constructor() {
        // 维护观察者列表
        this.observerList = []
    }
    /**
     * 添加一个观察者
     * @param {Observer} observer Observer实例
     */
    addObserver(observer) {
        this.observerList.push(observer)
    }
    /**
     * 通知所有的观察者
     */
    notify() {
        this.observerList.forEach(observer => {
            observer.update()
        })
    }
}

实现 Storage

使得该对象为单例,并对localStorage进行封装设置值setItem(key,value)和getItem(key) 单例模式: 保证一个类只有一个实例,并提供一个访问它的全局访问点

var instance = null;
class Storage {
    static getInstance() {
        if (!instance) {
            instance = new Storage();
        }
        return instance;
    }
    setItem = (key, value) => localStorage.setItem(key, value)
    getItem = key => localStorage.getItem(key)
}

js 一些方法的实现

Object

myIs

Object.myIs = function(x, y) {
    if (x === y) {
        // +0 != -0
        return x !== 0 || 1 / x === 1 / y
    }
    else {
        // NaN == NaN
        return x !== x && y !== y
    }
}

myCreate

Object.myCreate = function(proto, properties) {
    if (typeof proto != 'object' && typeof proto != 'function') {
        throw new Error('Object prototype may only be an Object: ' + proto)
    }
    function F () {}
    F.prototype = proto
    return new F()
}

call/apply/bind

call/apply

1. 实现: 改变 this 的指向、执行函数

function.call(thisArg, arg1, arg2, ...)

func.apply(thisArg, [argsArray])

Function.prototype.myCall = function (context, ...args) {
  context = context || window
  context.fn = this
  let result = context.fn(...args)
  delete context.fn
  return result
}
Function.prototype.myApply = function (context, args) {
  context = context || window
  context.fn = this
  let result
  if (args) {
    result = context.fn(...args)
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

2. 测试:

var value = 2
let obj = {
    value: 1
}
function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
}
console.log(bar.myCall(null))
console.log(bar.myCall(obj, 'kevin', 18))
console.log(bar.myApply(null))
console.log(bar.myApply(obj, ['kevin', 18]))

bind

1. 实现: 返回一个函数、可以传入参数、构造函数

Function.prototype.myBind = function(context, ...args) {
    if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
    }
    let self = this
    let fBound = function () {
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        let _this = this instanceof fBound ? this : context
        return self.apply(_this, [...args, ...arguments])
    }    
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例继承绑定函数的原型中的值
    fBound.prototype = Object.create(this.prototype)
    return fBound
}

2. 测试:

var value = 2 // var 声明的变量被挂到window上
let foo = {
    value: 1,
    bar: bar.myBind(null) // bind 调用 apply,apply 会兼容 this 执向 context||window
};
function bar() {
    console.log(this.value);
}
console.log('======')
foo.bar()

let foo2 = {
    value: 1
};
function bar2(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(this.friend);
    console.log(name);
    console.log(age);
}
bar2.prototype.friend = 'kevin';
let bindFoo = bar2.myBind(foo, 'daisy');
console.log('=====')
bindFoo(18)
console.log('=====')
let obj = new bindFoo('18')

数组排序

冒泡排序

依次比较、交换相邻的元素大小 =》稳定

  1. 比较相邻的元素,如果第一个比第二个大,就交换它们两个
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数
  3. 针对所有的元素重复以上的步骤,除了最后一个
  4. 重复步骤 1~3,直到排序完成
function bubbleSort(array) {
    for (let i = 0; i < array.length -1; i++) {
        for(let j = 1; j < array.length -1 - i; j++) {
            if (array[j] > array[j + 1]) {
                let temp = array[j + 1]
                array[j + 1] = array[i]
                array[j] = temp
            }
        }
    }
    return array
}

选择排序

选择排序: 每一次循环遍历寻找最小的数 =》不稳定

  1. 找到数组最小的元素,将它和数组红第一个元素交换位置
  2. 在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置
  3. 往复如此,直到将整个数组排序
function selectSort(array) {
    for (let i = 0; i < array.length; i++) {
        let tempIndex = i
        for (let j = i + 1; j < array.length; j ++) {
            if (array[j] < array[tempIndex]) {
                tempIndex = j
            }
        }
        if (tempIndex !== i) {
            let temp = array[tempIndex]
            array[tempIndex] = array[i]
            array[i] = temp
        }
    }
    return array
}

插入排序

在有序的数组中插入 =》稳定

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置
  6. 重复步骤 2~5
function insertSort(array) {
    for(let i = 1; i ++; i < array.length) {
        let current = array[i]
        let preIndex = i - 1
        while(preIndex >= 0 && array[preIndex] > current) {
            array[preIndex + 1] = array[preIndex]
            preIndex --
        }
        array[preIndex + 1] = current
    }
    return array
}

快速排序

快速排序: =》不稳定

  • 在数据集之中,选择一个元素作为"基准"(pivot)。
  • 所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。
  • 对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止
function binarySearch(array, val, low, heigh) {
    if (low > heigh) {
        return -1
    }
    let m = Math.floor((low + heigh) / 2)
    if (array[m] == val) {
        return m
    }
    else if (array[m] < val) {
        low = m -1
        return binarySearch(array, val, low, heigh)
    }
    else {
        heigh = m + 1
        return binarySearch(array, val, low, heigh)
    }    
}

数组去重

参考文档

indexOf 底层使用的是 === 进行判断,所以使用 indexOf 查找不到 NaN 元素

Set可以去重NaN类型, Set内部认为尽管 NaN === NaN 为 false,但是这两个元素是重复的

获取 object 的 keys 通过 for ... in 或者 object.keys

时间复杂度:O(1)没有循环、O(n)一层for循环、O(n*n)双重for循环

双重 for 循环

对象和 NaN 不去重

function distinct1(arr) {
    for (let i = 0, len = arr.length; i < len; i++) {
        for (let j = i + 1; j < len; j ++) {
            if (arr[i] == arr[j]) {
                arr.splice(j, 1)
                // splice 会改变数组长度,所以要将数组长度 len 和下标 j 减一
                len --
                j --
            }
        }
    }
    return arr
}

Array.filter + indexOf

对象不去重 NaN 会被忽略掉

function distinct2(arr) {
    return arr.filter((item, index) => {
        return arr.indexOf(item) == index
    })
}

Array.sort + 一遍冒泡排序

对象和 NaN 不去重 数字 1 也不去重

function distinct3(arr) {
    let res = []
    let sortedArray = arr.sort()
    for (let i = 0, len = sortedArray.length; i < len; i++) {
        // // 如果是第一个元素或者相邻的元素不相同
        if (!i || seen != sortedArray[i]) {
            res.push(sortedArray[i])
        }
        seen = sortedArray[i]
    }
    return res
}

Object 键值对

全部去重

function distinct4(arr) {
    var obj = {}
    return arr.filter((item, index, array) => {
        // 123 -> 'number123' 与 '123' -> 'string123'
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}

ES6 set

对象不去重 NaN 去重

function distinct5(arr) {
    return Array.from(new Set(arr))
}

数组出现次数最多的数

参考文档

以空间换时间

定义数组count,对arr中元素出现的次数进行计数,count中最大元素对应的下标,即出现次数最多的元素值

function SearchMuchArray(arr) {
    let count = [0,0,0,0,0,0,0,0,0,0]
    for(let i = 0, len = arr.length; i < len; i++) {
        count[arr[i]]++
    }
    let maxVal = 0
    let maxCount = 0
    for(let i = 0, len = count.length; i < len; i++) {
        if(count[i] > maxCount) {
            maxVal = i
            maxCount = count[i]
        }
    }
    return {
        maxVal,
        maxCount
    }
}

使用Map

每个Entry的key存放数组中的数字,value存放该数字出现的次数。最大的value对应的key即出现次数最多的那个数

function SearchMuchMap(arr) {
    let myMap = new Map()
    for(let i = 0, len = arr.length; i < len; i++) {
        let val = arr[i]
        if (myMap.has(val)) {
            myMap.set(val, myMap.get(val) + 1)
        }
        else {
            myMap.set(val, 1)
        }
    }
    let maxVal = 0
    let maxCount = 0
    for(let item of myMap) {
        if(item[1] > maxCount) {
            maxVal = item[0]
            maxCount = item[1]
        }
    }
    return {
        maxVal,
        maxCount
    }
}

查找

二分查找

function binarySearch(array, val) {
    let low = 0
    let heigh = array.length
    while (low <= heigh) {
        let m = Math.floor((heigh + low) / 2)
        if (array[m] == val) {
            return m
        }
        else if (array[m] < val) {
            low = m -1
        }
        else {
            heigh = m + 1
        }
    }
    return -1
}

二叉树

参考

获取从根节点到所有叶子节点的路径

// 树的节点个数
function getNodeNum(root) {
    if (!root)
        return 0
    return getNodeNum(root.left) + getNodeNum(root.right) + 1
}
// 树的叶子节点个数
function getLeftNodeNum(root) {
    if (!root.left && !root.right)
        return 1
    return getNodeNum(root.left) + getNodeNum(root.right)
}
function getPathSum(root){
    if (!root)
        return 0
    let value = 0
    let list = []
    list.push(root)
    while(list.length > 0) {
        let node = list.pop()
        value += getNodeNum(node) * node.value
        if(node.left)
            list.push(node.left)
        if(node.right)
            list.push(node.right)
    }
    return value
}