基本算法

66 阅读8分钟

1、冒泡排序

从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需要比较到 length - 1 的位置。

 function bubbleSort(list) {
    let len = list.length;
    for (let i = 0; i < len; i++) {
      for (let j = 0; j < len - i - 1; j++) {
        if (list[j] > list[j + 1]) {
          let tmp = list[j + 1];
          list[j + 1] = list[j];
          list[j] = tmp;
        }
      }
    }
  }

2、快速排序

找到中间位置midValue,遍历数组,小于midValue放在left,否则放在right继续递归,最后concat拼接返回,使用splice会修改原数组,使用slice不会修改原数组(推荐)一层遍历+二分的时间复杂度是O(nlogn)

function quickSort2(arr) {
    let len = arr.length;
    if (len === 0) return arr;
    let midIndex = Math.floor(len / 2);
    let midValue = arr.slice(midIndex, midIndex + 1)[0];
    let left = [];
    let right = [];
    for (let i = 0; i < len; i++) {
      if (arr[i] !== midValue) {
        if (arr[i] < midValue) {
          left.push(arr[i]);
        } else {
          right.push(arr[i]);
        }
      }
    }
    return quickSort2(left).concat([midValue], quickSort2(right));
}

3、选择排序

其主要原理是不断地选择剩余元素中的最小值(或最大值)并将其放置到已排序部分的末尾

function selectSort(arr) {
    // 缓存数组长度
    const len = arr.length;
    // 定义 minIndex,缓存当前区间最小值的索引,注意是索引
    let minIndex;
    // i 是当前排序区间的起点
    for (let i = 0; i < len - 1; i++) {
      // 初始化 minIndex 为当前区间第一个元素
      minIndex = i;
      // i、j分别定义当前区间的上下界,i是左边界,j是右边界
      for (let j = i; j < len; j++) {
        // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
        if (arr[j] < arr[minIndex]) {
          minIndex = j;
        }
      }
      // 如果 minIndex 对应元素不是目前的头部元素,则交换两者
      if (minIndex !== i) {
        [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
      }
    }
    return arr;
}

4、插入排序

工作原理类似于人类按顺序整理一手扑克牌。每次取一张牌并将其插入到已经排序好的牌中的适当位置

function insertSort(arr) {
    for (let i = 1; i < arr.length; i++) {
      let j = i;
      let target = arr[i];
      while (j > 0 && arr[j - 1] > target) {
        arr[j] = arr[j - 1];
        j--;
      }
      arr[j] = target;
    }
    return arr;
}

5、归并排序

interview.poetries.top/algorithm/a…

6、深拷贝

  • 简便方式
  const newObj = JSON.parse(JSON.stringify(oldObj));
  • json.stringify局限性

    • 无法解决循环引用的问题
    const a = {val:2};
    a.target = a;
    拷贝 a 会出现系统栈溢出,因为出现了无限递归的情况。
    
    • 无法拷贝一些特殊的对象,诸如 RegExp, Date, Set, Map
    • 无法拷贝函数
  • 完整实现方式

const getType = obj => Object.prototype.toString.call(obj);

const isObject = (target) => (typeof target === 'object' 
|| typeof target === 'function') && target !== null;

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=().+(?=)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

const deepClone = (target, map = new WeakMap()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 处理不能遍历的对象
    return handleNotTraverse(target, type);
  }else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.constructor;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.set(target, true);

  if(type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key, map), deepClone(item, map));
    })
  }
  
  if(type === setTag) {
    //处理Set
    target.forEach(item => {
      cloneTarget.add(deepClone(item, map));
    })
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
}

7、call、apply、bind

call实现
Function.prototype.myCall = function(context = window , ...args){
    if(typeof context !== 'object'){
      context = new Object(context)
    }
    const fnKey =  Symbol();
    context[fnKey] = this;
    let result =  context[fnKey](...args);
    delete context[fnKey];
    return result
}
apply实现
Function.prototype.myApply = function (context = window, args) {
    if (typeof context !== "object") {
      context = new Object(context);
    }
    let fnkey = Symbol();
    context[fnkey] = this;
    let result = context[fnkey](...args);
    delete context[fnkey];
    return result;
};
bind实现
Function.prototype.myBind = function (context = window, ...args) {
    if (typeof context !== "object") {
      context = new Object(context);
    }
    let self = this;
    let result = function (innerArgs) {
      return self.apply(
        this instanceof result ? this : context,
        args.concat(innerArgs)
      );
    };
    // 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法:保证原函数的原型对象上的属性不丢失
    // 实现继承的方式: 使用Object.create
    result.prototype = Object.create(this.prototype);
    return result;
};

8、设计模式相关

设计模式分类 image.png

  • 单例模式:核心就是保证全局只有一个对象可以访问, 例如vuex和redux的基础设计就是单例模式, 全局只有一个store, 全局缓存也算单例模式, 确保实例只创建一次.全局类型的dom弹层的实现就是单例模式

  • 工厂模式:通过传入不同的参数达到实例化不同的对象这一目的。比如: 有一天你去餐厅想吃宫保鸡丁,然后餐厅给你实例化了宫保鸡丁。 例如封装im消息类型的时候采用的就是该模式

  • 抽象工厂模式:就是定制了实例的结构,比如宫保鸡丁和西红柿炒鸡蛋都可以吃那么他们有共同的结构吃,又比如饭店有做菜的功能,其他饭店也可以做菜,那么做菜这个方法可以作为抽象类来约束实例。抽象类(对有相同方法(结构)的实例进行约束),

工厂模式主要关注的是当前产品的创建,
    优势:良好的封装,符合开放封闭原则
    劣势: 增加系统复杂性
抽象工厂模式关注当前的抽象类,以及当前的产品。
    劣势:(违反了开放封闭原则,添加系统复杂性)
    优势:我们不需要知道产品的具体细节,只要看当前抽象类的方法(结构)进行编程就可以了
  • 状态模式:一个对象有状态变化,每一个状态变化都会触发一个逻辑,我们不能总是if...else 来写,所以我们就把状态和当前对象分离开来,比如最常见都红绿灯。红灯状态下是(停下),黄灯状态是(警告),绿灯状态是(通行)。那么我们就可以把这3个状态和方法都抽离出来。提高代码复用,符合开放封闭原则

  • 适配器模式: 适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。在 Vue 中,我们其实经常使用到适配器模式。比如父组件传递给子组件一个时间戳属性,组件内部需要将时间戳转为正常的日期显示,一般会使用 computed 来做转换这件事情,这个过程就使用到了适配器模式, 还有就是对于图表和数据展示的时候, 前端图表数据需要中转函数来处理

  • 装饰模式: 不需要改变已有的接口,作用是给对象添加功能。就像我们经常需要给手机戴个保护套防摔一样,不改变手机自身,给手机添加了保护套提供防摔功能。在 React 中,装饰模式其实随处可见

import { connect } from 'react-redux'
class MyComponent extends React.Component {
    // ...
}
export default connect(mapStateToProps)(MyComponent)
  • 代理模式:- 代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有很多代理的场景。比如你需要买一件国外的产品,这时候你可以通过代购来购买产品。实际用法如事件代理

  • 发布-订阅模式(观察者模式):通过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知。在现实生活中,也有很多类似场景,比如我需要在购物网站上购买一个产品,但是发现该产品目前处于缺货状态,这时候我可以点击有货通知的按钮,让网站在产品有货的时候通过短信通知我。 实际工作中: 1、点击一个按钮触发了点击事件就是使用了该模式 2、在 Vue 中,如何实现响应式也是使用了该模式。对于需要实现响应式的对象来说,在 get 的时候会进行依赖收集,当改变了对象的属性时,就会触发派发更新。

9、输出问题

1this问题
let obj1 = {
    a: 1,
    foo: () => {
        console.log(this.a)
    }
}
// log1
console.log(obj1.foo())
const obj2 = obj1.foo
// log2
console.log(obj2())

2、事件循环问题
  const p1 = () => (new Promise((resolve, reject) => {
      console.log(1);
      let p2 = new Promise((resolve, reject) => {
          console.log(2);
          const timeOut1 = setTimeout(() => {
              console.log(3);
              resolve(4);
          }, 0)
          resolve(5);
      });
      resolve(6);
      p2.then((arg) => {
          console.log(arg);
      });
  }));

  const timeOut2 = setTimeout(() => {
      console.log(8);
      const p3 = new Promise(reject => {
          reject(9);
      }).then(res => {
          console.log(res)
      })
  }, 0)
  
  
  p1().then((arg) => {
      console.log(arg);
  });

  console.log(10);
3、批处理和函数更新的问题
import { useState } from 'react'

const Test = () => {
        
    const [count1, setCount1] = useState(1)
  
    const [count2, setCount2] = useState(100)
  
    console.log('重新渲染了!')

    const onOK = () => {
        setCount1((state) => state + 1)
        console.log(count1)
        setCount1((state) => state + 1)
        console.log(count1)
        setTimeout(() => {
          setCount1(count1 + 1)
          console.log(count1)
          setCount2(count2 + 100)
          console.log(count2)
        }, 1000)
    
    }

    return (
        <>
          <div onClick={onOK}>按钮</div>
          <p>{ count1 }</p>
          <p>{ count2 }</p>
        </> 
    )
}

export default Test