这些算法知识得知道~!

174 阅读9分钟

算法

字符串反转

function reverseString(str) {
  return str.split("").reverse().join("");
}

数组去重

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

防抖函数(Debounce)

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

function debounce(fn, delay) {
  let timer;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, delay);
  };
}

节流函数(Throttle)

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。

连续点击我,每一秒只会执行一次点击事件

function throttle(fn, delay) {
  let timer;
  let previous = 0;
  return function() {
    const context = this;
    const args = arguments;
    const now = new Date();
    if (now - previous > delay) {
      previous = now;
      fn.apply(context, args);
    } else {
      clearTimeout(timer);
      timer = setTimeout(() => {
        fn.apply(context, args);
      }, delay);
    }
  };
}

LRU 缓存算法(Least Recently Used)

LRU(Least Recently Used)缓存算法是一种常见的缓存淘汰策略,用于管理计算机系统内的缓存。简单来说,LRU算法会根据数据最近被访问的时间从缓存中清除最久未使用的数据,以腾出空间供新的数据使用

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.map = new Map();
  }

  get(key) {
    const val = this.map.get(key);
    if (typeof val === "undefined") return -1;

    this.map.delete(key);
    this.map.set(key, val);
    return val;
  }

  put(key, value) {
    if (this.map.has(key)) {
      this.map.delete(key);  
    }
    this.map.set(key, value);
    if (this.map.size > this.capacity) {
      const oldestKey = this.map.keys().next().value;
      this.map.delete(oldestKey);
    }
  }
}

快速排序

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

归并排序

归并排序是一种分治算法,它的核心思想是将待排序数组分成两个子数组,对每个子数组进行递归排序,然后将两个子数组合并成一个有序数组。它的时间复杂度是O(nlogn)。

具体实现步骤如下:

  1. 将待排序数组分成两个子数组,分别是左子数组和右子数组。

  2. 对左子数组和右子数组分别进行递归排序,直到子数组长度为1为止。

  3. 将左子数组和右子数组合并为一个有序数组。合并时,从左右两个子数组中取出一个元素进行比较,将较小的元素放入新数组中,直到其中一个子数组为空,然后将另一个子数组的剩余元素依次放入新数组中。

  4. 返回有序数组。

下面是JavaScript实现代码:

function mergeSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  
  const mid = Math.floor(arr.length / 2);
  const leftArr = arr.slice(0, mid);
  const rightArr = arr.slice(mid);
  
  const leftSorted = mergeSort(leftArr);
  const rightSorted = mergeSort(rightArr);
  
  return merge(leftSorted, rightSorted);
}

function merge(leftArr, rightArr) {
  const merged = [];
  let leftIndex = 0;
  let rightIndex = 0;
  
  while (leftIndex < leftArr.length && rightIndex < rightArr.length) {
    if (leftArr[leftIndex] <= rightArr[rightIndex]) {
      merged.push(leftArr[leftIndex]);
      leftIndex++;
    } else {
      merged.push(rightArr[rightIndex]);
      rightIndex++;
    }
  }
  
  return merged.concat(leftArr.slice(leftIndex), rightArr.slice(rightIndex));
}

堆排序

function heapSort(arr) {
  function buildHeap() {
    for (let i = Math.floor(arr.length / 2); i >= 0; i--) {
      heapify(i, arr.length);
    }
  }

  function heapify(start, end) {
    let leftChild = start * 2 + 1;
    let rightChild = start * 2 + 2;
    let largestIndex = start;
    if (leftChild < end && arr[leftChild] > arr[largestIndex]) {
      largestIndex = leftChild;
    }

    if (rightChild < end && arr[rightChild] > arr[largestIndex]) {
      largestIndex = rightChild;
    }

    if (largestIndex !== start) {
      [arr[start], arr[largestIndex]] = [arr[largestIndex], arr[start]];
      heapify(largestIndex, end);
    }
  }

  buildHeap();

  for (let i = arr.length - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]];
    heapify(0, i);
  }

  return arr;
}

二分查找

二分查找(Binary Search)是一种常见的查找算法,也称折半查找。二分查找针对的是一个有序的数组,算法每次都从数组的中间位置开始查找,如果中间位置的元素与要查找的元素相等,则查找成功;如果中间位置的元素比要查找的元素大,则在左半部分继续查找;如果中间位置的元素比要查找的元素小,则在右半部分继续查找。每次查找都缩小一半的查找范围,因此时间复杂度为O(logn)。但是,二分查找只适用于静态有序的数据,一旦数据发生变化,就需要重新构建有序的数组。

function binarySearch(arr, val) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] > val) {
      right = mid - 1;
    } else if (arr[mid] < val) {
      left = mid + 1;
    } else {
      return mid;
    }
  }

  return -1;
}

斐波那契数列

可以使用循环迭代和递归两种方式实现斐波那契数列。
  1. 循环迭代方法:
function fibonacci(n) {
  if(n == 0 || n == 1) {
    return n;
  }
  let a = 0, b = 1, c;
  for(let i = 2; i <= n; i++) {
    c = a + b;
    a = b;
    b = c;
  }
  return b;
}

function fibonacci(n) {
  if (n <= 1) return n;

  let pre = 0;
  let next = 1;

  for (let i = 2; i <= n; i++) {
    [pre, next] = [next, pre + next];
  }

  return next;
}

这里使用ab来分别存储前两个斐波那契数列的值,然后用循环来计算斐波那契数列的元素,最后返回第n个元素的值。

  1. 递归方法:
function fibonacci(n) {
  if(n == 0 || n == 1) {
    return n;
  }
  return fibonacci(n-1) + fibonacci(n-2);
}

这里使用递归的方式来实现斐波那契数列,当n为0或1时,直接返回n,否则递归计算前两个斐波那契数列的元素的和。需要注意的是,递归方法的时间复杂度为O(2^n),因此在计算大量数据时可能会极其缓慢或出现堆栈溢出的问题。

二叉树

二叉树是一种数据结构,它由节点组成,每个节点最多有两个子节点。在二叉树中,我们通常称左侧节点为“左子树”,右侧节点为“右子树”。二叉树的实现方式比较灵活,可以用数组或者链表来表示。

以下是基于链表的二叉树代码示例:

class Node{
  constructor(val){
    this.val = val; // 节点值
    this.leftChild = null; // 左子节点
    this.rightChild = null; // 右子节点
  }
}

class BinaryTree{
  constructor(){
    this.root = null;
  }

  insertNode(node, newNode){
    if (newNode.val < node.val){
      if (node.leftChild === null){
        node.leftChild = newNode;
      } else {
        this.insertNode(node.leftChild, newNode);
      }
    } else {
      if (node.rightChild === null){
        node.rightChild = newNode;
      } else {
        this.insertNode(node.rightChild, newNode);
      }
    }
  }

  insert(val){
    let newNode = new Node(val);

    if (this.root === null){
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }
}

链表

链表也是一种数据结构,它由节点组成,每个节点包含一个数据元素和一个指向下一个节点的指针。链表分为单向链表和双向链表两种,单向链表只有一个方向,即从前往后,而双向链表则可以从后往前遍历。

class Node{
  constructor(val){
    this.val = val; // 节点值
    this.next = null; // 下一个节点指针
  }
}

class LinkedList{
  constructor(){
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  append(val){ // 添加节点
    let newNode = new Node(val);

    if (this.head === null){
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }
    this.length++;
  }

  delete(val){ // 删除节点
    let currentNode = this.head;
    let previousNode = null;

    if (currentNode.val === val){
      this.head = currentNode.next;
    } else {
      while (currentNode.val !== val){
        previousNode = currentNode;
        currentNode = currentNode.next;
      }

      previousNode.next = currentNode.next;
    }
    this.length--;
  }
}

手写JS

实现柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

function curry(fn, args) {
    let length = fn.length;
    let args = args || [];
    return function(){
       newArgs = args.concat(Array.prototype.slice.call(arguments));
       if (newArgs.length < length) {
           return curry.call(this,fn,newArgs);
          }else{
           return fn.apply(this,newArgs);
        }
    }
}

function multiFn(a, b, c) {
    return a * b * c;
}
let multi = curry(multiFn);

multi(1)(2)(3);
multi(1,2,3);
multi(1)(2,3);

手写Promise实现

解决回调地域

  • 三种状态pending| fulfilled(resolved) | rejected
  • 当处于pending状态的时候,可以转移到fulfilled(resolved)或者rejected状态
  • 当处于fulfilled(resolved)状态或者rejected状态的时候,就不可变
class MyPromise {
    constructor(executor) {
        this.initValue()
        this.initBind()
        executor(this.resolve, this.reject)
    }
    initBind() {
        this.resolve = this.resolve.bind(this)
        this.reject = this.reject.bind(this)
    }

    initValue() {
        this.PromiseState = 'pending'
        this.PromiseResult = null
        this.onFulfilledCallbacks = [] // 保存成功回调  
        this.onRejectedCallbacks = [] // 保存失败回调
    }
    resolve(value) {
        if (this.PromiseState !== 'pending') return
        // 如果执行resolve,状态变为fulfilled
        this.PromiseState = 'fulfilled'
        // 终值为传进来的值
        this.PromiseResult = value
        // 执行保存的成功回调  
        while (this.onFulfilledCallbacks.length) { 
            this.onFulfilledCallbacks.shift()(this.PromiseResult)  
        }

    }
    reject(reason) {
        if (this.PromiseState !== 'pending') return
        // 如果执行reject,状态变为rejected
        this.PromiseState = 'rejected'
        // 终值为传进来的reason
        this.PromiseResult = reason
        // 执行保存的失败回调  
        while (this.onRejectedCallbacks.length) { 
            this.onRejectedCallbacks.shift()(this.PromiseResult)
        }
    }
     then(onFulfilled, onRejected) {
        // 接收两个回调 onFulfilled, onRejected
        // 参数校验,确保一定是函数
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }

        if (this.PromiseState === 'fulfilled') {
            // 如果当前为成功状态,执行第一个回调
            onFulfilled(this.PromiseResult)
        } else if (this.PromiseState === 'rejected') {
            // 如果当前为失败状态,执行第二哥回调
            onRejected(this.PromiseResult)
        } else if (this.PromiseState === 'pending') {
            // 如果状态为待定状态,暂时保存两个回调
            this.onFulfilledCallbacks.push(onFulfilled.bind(this))
            this.onRejectedCallbacks.push(onRejected.bind(this))
       }
    }
}

const test1 = new MyPromise((resolve, reject) => { resolve('success') reject('fail') }) 
console.log(test1) // MyPromise { PromiseState: 'fulfilled', PromiseResult: 'success' }

const test2 = new MyPromise((resolve, reject) => { 
setTimeout(
() => { 
    resolve('success') }, 1000) 
}).then(res => console.log(res), err => console.log(err))
function myPromise(constructor) {
    let self = this;
    self.status = "pending";
    self.value = undefined;
    self.reason = undefined;
    function resolve(value) {
      if (self.status === "pending") {
        self.value = value;
        self.status = "resolved";
      }
    }
    function reject(value) {
      if (self.status === "pending") {
        self.reason = reason;
        self.status = "rejected";
      }
    }
    try {
      constructor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }

  myPromise.prototype.then = function (onFullfilled, onRejected) {
    let self = this;
    switch (self.status) {
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:
    }
  };

实现一个继承 - 寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法

function Parent(name) {
    this.name = name;
}
Parent.prototype.sayName = function() {
    console.log('parent name:', this.name);
}
function Child(name, parentName) {
    Parent.call(this, parentName);  
    this.name = name;    
}
function create(proto) {
    function F(){}
    F.prototype = proto;
    return new F();
}
Child.prototype = create(Parent.prototype);
Child.prototype.sayName = function() {
    console.log('child name:', this.name);
}
Child.prototype.constructor = Child;

let parent = new Parent('father');
parent.sayName();  

let child = new Child('son', 'father');

实现bind()方法

bind()会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

Function.prototype.bind2 = function(content) {
    if(typeof this != "function") {
        throw Error("not a function")
    }
    let fn = this;
    let args = [...arguments].slice(1);
    
    let resFn = function() {
        return fn.apply(this instanceof resFn ? this : content,args.concat(...arguments) )
    }
    function tmp() {}
    tmp.prototype = this.prototype;
    resFn.prototype = new tmp();
    
    return resFn;
}

实现一个call或apply方法

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

Function.prototype.myCall = function (context) {
    let context = context || window;
    context.fn = this;

    let args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    let result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}
// 测试一下
let value = 2;

let obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call2(null); // 2

console.log(bar.call2(obj, 'wawa', 27));
// 1
// Object {
//    value: 1,
//    name: 'wawa',
//    age: 27
// }

apply 的实现跟 call 类似

Function.prototype.myApply = function (context, arr) {
    let context = Object(context) || window;
    context.fn = this;

    let result;
    if (!arr) {
        result = context.fn();
    }
    else {
        let args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

实现防抖

当一次事件发生后,事件处理器要等一定阈值的时间,如果这段时间过去后 再也没有 事件发生,就处理最后一次发生的事件。假设还差 0.01 秒就到达指定时间,这时又来了一个事件,那么之前的等待作废,需要重新再等待指定时间。

// 防抖动函数
function debounce(fn,wait=50,immediate) {
    let timer;
    return function() {
        if(immediate) {
            fn.apply(this,arguments)
        }
        if(timer) clearTimeout(timer)
        timer = setTimeout(()=> {
            fn.apply(this,arguments)
        },wait)
    }
}

// 结合实例
function go(){
    console.log("Success");
}
// 采用了防抖动
window.addEventListener('scroll',debounce(go,500));
// 没采用防抖动
window.addEventListener('scroll',go);

实现节流

可以理解为事件在一个管道中传输,加上这个节流阀以后,事件的流速就会减慢。实际上这个函数的作用就是如此,它可以将一个函数的调用频率限制在一定阈值内,例如 1s,那么 1s 内这个函数一定不会被调用两次

function throttle(fn, wait) {
	let prev = new Date();
	return function() { 
	  const args = arguments;
		const now = new Date();
		if (now - prev > wait) {
			fn.apply(this, args);
			prev = new Date();
		}
	}

手写一个JS深拷贝

(1)简单直接版

let newObj = JSON.parse( JSON.stringify( obj ) );

(2)常规版

function deepCopy(obj){
  //判断是否是简单数据类型,
  if(typeof obj == "object"){
      //复杂数据类型
      let result = obj.constructor == Array ? [] : {};
      for(let i in obj){
          result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
      }
  }else {
      //简单数据类型 直接 == 赋值
      let result = obj;
  }
  return result;
}

实现一个new操作符

1.先创建一个空对象用来存放实例

2.将构造函数的this指向空对象并执行函数体

3.将对象的__proto__属性指向构造函数的原型

4.返回新对象,如果构造函数返回值为引用类型,就返回这个引用类型,没有返回值或者返回值为基本类型就返回你的实例对象。

let obj = {}
Let result = Person.call(obj)
obj.__proto__ = Person.prototype
If (typeof(result) === ‘object’) {
	return result
} else {
	return p
}

简单实现

function myNew(fn, ...args) {
    const obj = {}
    obj.__proto__ = fn.prototype
    fn.apply(obj, args)
    return obj
}
let obj = myNew(A, 1, 2);
// equals to
let obj = new A(1, 2);

实现数组去重

(1)递归方式实现

let kkb = ["个人介绍", "工作记录", ["css", "html", [1, 2, 3]]];
let newArr = [];
function flat(arr) {
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      console.log("是数组");
      flat(arr[i]);
    } else {
      console.log("不是数组");
      newArr.push(arr[i]);
    }
  }
}
flat(kkb);
console.log("最后输出", newArr);

(2)Set去重

function flat(arr) {
    return [...new Set(arr)]
}

使用setTimeout实现setInterval

function mySetTimout(fn, delay) {
    let timer = null
    const interval = () => {
        fn()
        timer = setTimeout(interval, delay)
    }
    setTimeout(interval, delay)
    return {
        cancel: () => {
            clearTimeout(timer)
        }
    }
}

// 测试
const { cancel } = mySetTimout(() => console.log(888), 1000)
setTimeout(() => {
    cancel()
}, 4000)

实现函数结果累加

function fn1(x) {
    return x + 1;
}
function fn2(x) {
    return x + 2;
}
function fn3(x) {
    return x + 3;
}
function fn4(x) {
    return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);

实现方式

function compose(...fn) {
    if (fn.length === 0) return (num) => num
    if (fn.length === 1) return fn[0]
    return fn.reduce((pre, next) => {
        return (num) => {
            return next(pre(num))
        }
    })
}

实现发布订阅者模式

实现on ,emit , once, off 方法,类似eventBus实现

class EventEmitter {
    constructor() {
        this.cache = {}
    }

    on(name, fn) {
        const tasks = this.cache[name]
        if (tasks) {
            this.cache[name].push(fn)
        } else {
            this.cache[name] = [fn]
        }
    }

    off(name, fn) {
        const tasks = this.cache[name]
        if (task) {
            const index = tasks.findIndex(item => item === fn)
            if (index >= 0) {
                this.cache[name].splice(index, 1)
            }
        }
    }

    emit(name, ...args) {
        // 复制一份。防止回调里继续on,导致死循环
        const tasks = this.cache[name].slice()
        if (tasks) {
            for (let fn of tasks) {
                fn(...args)
            }
        }
    }

    once(name, cb) {
        function fn(...args) {
            cb(args)
            this.off(name, fn)
        }
        this.on(name, fn)
    }
}

实现dom树转化成树结构对象

<div>
    <span></span>
    <ul>
        <li></li>
        <li></li>
    </ul>
</div>

将上方的DOM转化为下面的树结构对象

{
    tag: 'DIV',
    children: [
        { tag: 'SPAN', children: [] },
        {
            tag: 'UL',
            children: [
                { tag: 'LI', children: [] },
                { tag: 'LI', children: [] }
            ]
        }
    ]
}

实现方式

function dom2tree(dom) {
    const obj = {}
    obj.tag = dom.tagName
    obj.children = []
    dom.childNodes.forEach(child => obj.children.push(dom2tree(child)))
    return obj
}

计算对象的层级数

function loopGetLevel(obj) {
    let res = 1;

    function computedLevel(obj, level) {
        let level = level ? level : 0;
        if (typeof obj === 'object') {
            for (let key in obj) {
                if (typeof obj[key] === 'object') {
                    computedLevel(obj[key], level + 1);
                } else {
                    res = level + 1 > res ? level + 1 : res;
                }
            }
        } else {
            res = level > res ? level : res;
        }
    }
    computedLevel(obj)

    return res
}

const obj = {
    a: { b: [1] },
    c: { d: { e: { f: 1 } } }
}

console.log(loopGetLevel(obj)) // 4

尾递归

尾递归是一种特殊的递归形式,在递归过程中,尽可能把所有的计算都放到最后一步完成,这样就可以优化递归的性能。

在传统的递归中,通常是先递归一些操作,然后再将递归的结果与剩余的计算结果进行处理。在尾递归中,函数的返回值就是递归函数的返回值,并且没有任何后续的计算。这种形式的递归可以使编译器对代码进行优化,消除递归调用的计算和内存开销。

对于尾递归的函数,只需要调用它一次,就可以完成整个递归,而不必反复地调用自身。这样,在递归层数很大的情况下,不会导致栈的爆炸,从而提高了函数的性能和效率。

下面是一个尾递归的例子,计算斐波那契数列:

function fib(n, a = 1, b = 1) {
  if (n <= 1) return a;
  return fib(n - 1, b, a + b);
}

**

在上面的代码中,尾递归的优化使得计算斐波那契数列的过程中只需要一个栈帧,而不需要创建多个栈帧,从而可以有效地减少内存的占用。

// 传统递归
function factorial(n) {
  if (n <= 1) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

// 尾递归
function factorialTail(n, acc = 1) {
  if (n <= 1) {
    return acc;
  } else {
    return factorialTail(n - 1, n * acc);
  }
}

console.log(factorial(5)); // 120
console.log(factorialTail(5)); // 120

**

上述示例中,我们分别使用了传统递归和尾递归的方式来计算5的阶乘。其中,传统递归的方式比较容易造成栈溢出,而尾递归则可以避免这个问题,并且执行效率更高。