2021年面试复习题(下 - 算法部分)

463 阅读18分钟

拾几。手写JavaScript

5.1 数组扁平化

const arr = [1, [2, [3, [4, 5]]], 6];

方法一:使用flat()

const res1 = arr.flat(Infinity);

方法二:利用正则(但数据类型都会变为字符串)

const res2 = JSON.stringify(arr).replace(/\[|\]/g, '').split(',');

方法三:正则改良版本

const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']');

方法四:使用reduce

const flatten = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, [])
}
const res4 = flatten(arr);

方法五:函数递归

const res5 = [];
const fn = arr => {
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      fn(arr[i]);
    } else {
      res5.push(arr[i]);
    }
  }
}
fn(arr);

附加题 一:拉平至指定层级

// reduce + 递归
function flat(arr, num = 1) {
  return num > 0
    ? arr.reduce(
        (pre, cur) =>
          pre.concat(Array.isArray(cur) ? flat(cur, num - 1) : cur),
        []
      )
    : arr.slice();
}
var arr = [1, 2, [3, 4], 1, [[2], [3]], [[[1, 2], 3], 1], 2, [3, 5]];
flat(arr, Infinity);

5.1 数组去重

const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
// => [1, '1', 17, true, false, 'true', 'a', {}, {}]

方法一:利用Set

const res1 = Array.from(new Set(arr));

方法二:两层for循环+splice

const unique1 = arr => {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1);
        // 每删除一个树,j--保证j的值经过自加后不变。同时,len--,减少循环次数提升性能
        len--;
        j--;
      }
    }
  }
  return arr;
}

方法三:利用indexOf

当然也可以用include、filter,思路大同小异。

const unique2 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
  }
  return res;
}

方法四:利用include

const unique3 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!res.includes(arr[i])) res.push(arr[i]);
  }
  return res;
}

方法五:利用filter

const unique4 = arr => {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}

方法六:利用Map

const unique5 = arr => {
  const map = new Map();
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!map.has(arr[i])) {
      map.set(arr[i], true)
      res.push(arr[i]);
    }
  }
  return res;
}

5.1 类数组转化为数组

方法一:Array.from

Array.from(document.querySelectorAll('div'))

方法二:Array.prototype.slice.call()

Array.prototype.slice.call(document.querySelectorAll('div'))
// 或
Array.prototype.splice.call(arrayLike, 0);

方法三:扩展运算符

[...document.querySelectorAll('div')]

方法四:利用concat

Array.prototype.concat.apply([], document.querySelectorAll('div'));

5.1 扁平对象列表 ==> 树形结构Tree

自己的写法

function returnTreeList(list) { // 主函数 返回树状结构
  const treeList = [];
  // 根节点不唯一
  const superior = list.filter(a => a.superior === null);
  for (let i = 0; i < superior.length; i++) {
    treeList.push(recursiveTree(list, superior[i].id));
  }
  // deleteChildren(treeList);
  return treeList;
}

function recursiveTree(data, id) { // 递归 处理树状结构
  const node = data.find(a => a.id === id) || {};
  node.children = data.filter(a => a.superior === id).map(a => recursiveTree(data, a.id));
  return node;
}

function deleteChildren(node) { // 删除无子节点的children字段
  node.map(v => {
    if (v.children.length === 0) v.children = '';
    else deleteChildren(v.children);
  });
}

var list = [{id:1,superior:null},{id:2,superior:1}];
var treeList = returnTreeList(list);

面试了十几个高级前端,竟然连(扁平数据结构转Tree)都写不出来

var arr = [
    {id: 1, name: '部门1', pid: 0},
    {id: 2, name: '部门2', pid: 1},
    {id: 3, name: '部门3', pid: 1},
    {id: 4, name: '部门4', pid: 3},
    {id: 5, name: '部门5', pid: 4},
]

function arrayToTree(items) {
  const result = [];   // 存放结果集
  const itemMap = {};  // 
  for (const item of items) {
    const id = item.id;
    const pid = item.pid;

    if (!itemMap[id]) {
      itemMap[id] = {
        children: [],
      }
    }

    itemMap[id] = {
      ...item,
      children: itemMap[id]['children']
    }

    const treeItem =  itemMap[id];

    if (pid === 0) {
      result.push(treeItem);
    } else {
      if (!itemMap[pid]) {
        itemMap[pid] = {
          children: [],
        }
      }
      itemMap[pid].children.push(treeItem)
    }

  }
  return result;
}

arrayToTree(arr)

5.1 构造函数模式 + 原型模式

构造函数模式

function Person(name) {
    this.name = name;
    this.getName = getName;
}

function getName() {
    console.log(this.name);
}

var person1 = new Person('kevin');

构造函数模式与原型模式双剑合璧。

function Person(name) {
    this.name = name;
}

Person.prototype = {
    constructor: Person,
    getName: function () {
        console.log(this.name);
    }
};

var person1 = new Person();

5.1 继承

0.1 ES5 继承

// 创建一个父类
function Parent(){}
Parent.prototype.getName = function(){ return '沐华' }
// 子类
function Child(){}

// 方式一
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child // 重新指定 constructor
// 方式二
Child.prototype = Object.create(Parent.prototype,{
  constructor:{
    value: Child,
    writable: true, // 属性能不能修改
    enumerable: true, // 属性能不能枚举(可遍历性),比如在 for in/Object.keys/JSON.stringify
    configurable: true, // 属性能不能修改属性描述对象和能否删除
  }
})

console.log(new Child().getName) // 沐华

0.2 ES6 继承

// 创建一个父类
class Parent(){
  constructor(props){
    this.name = '沐华'
  }
}
// 创建一个继承自父类的子类
class Child extends Parent{
  // props是继承过来的属性, myAttr是自己的属性
  constructor(props, myAttr){
    // 调用父类的构造函数,相当于获得父类的this指向
    super(props)
  }
}
console.log(new Child().name) // 沐华

1.原型链继承

缺点:

  • 1.引用类型的属性被所有实例共享
  • 2.在创建 Child 的实例时,不能向Parent传参
function Parent () {
    this.name = 'kevin';
}

Parent.prototype.getName = function () {
    console.log(this.name);
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

console.log(child1.getName()) // kevin

2.借用构造函数(经典继承)

优点:

  • 1.避免了引用类型的属性被所有实例共享
  • 2.可以在 Child 中向 Parent 传参

缺点:

  • 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
function Parent () {
    this.names = ['kevin', 'daisy'];
}

function Child () {
    Parent.call(this);
}

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy"]

3.组合继承

原型链继承和经典继承双剑合璧。

优点:

  • 融合原型链继承和构造函数的优点,是JavaScript中最常用的继承模式。
function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {

    Parent.call(this, name);
    
    this.age = age;

}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');

child1.colors.push('black');

console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');

console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

5.1 call、apply、bind

基础数据

var obj = {
  a: 1,
  b: 2,
}

function method (v1, v2) {
  return this.a + this.b + v1 + v2;
}
  1. function.call(thisArg, arg1, arg2, ...)
Function.prototype.liccCall = function (ctx, ...args) {
  if (typeof this !== 'function') {
    throw new TypeError('Type Error');
  }
  // * 如果传 null || undefined 则指向 window || global
  ctx = ctx ? Object(ctx) : global;
  
  const fn = Symbol('fn');
  ctx[fn] = this;

  const res = ctx[fn](...args);
  delete ctx[fn];
  return res;
};

console.log('======= method.call():', method.call(1, 3, 4, 5));
console.log('======= method.liccCall():', method.liccCall(1, 3, 4, 5));
  1. function.apply(thisArg, [arg1, arg2, ...])
Function.prototype.liccApply = function (ctx, args) {
  if (typeof this !== 'function') {
    throw new TypeError('Type Error');
  }
  // * 如果传 null || undefined 则指向 window || global
  ctx = ctx ? Object(ctx) : global;
  
  const fn = Symbol('fn');
  ctx[fn] = this;

  // * args 可能为 undefined 的情况需要判空
  const res = args ? ctx[fn](...args) : ctx[fn]();
  delete ctx[fn];
  return res;
};

console.log('======= method.apply():', method.apply(1, [3, 4, 5]));
console.log('======= method.liccApply():', method.liccApply(1, [3, 4, 5]));
  1. function.bind(thisArg, arg1, arg2, ...)()
Function.prototype.liccBind = function (ctx, ...args) {
  if (typeof this !== 'function') {
    throw new Error("Type Error");
  }

  // 保存this的值
  var self = this;

  return function F() {
    // 考虑new的情况
    if (this instanceof F) {
      return new self(...args, ...arguments)
    }
    return self.apply(ctx, [...args, ...arguments])
  }
};

console.log('======= method.bind():', method.bind(obj, 3, 4, 5)());
console.log('======= method.liccBind():', method.liccBind(obj, 3, 4, 5)());

5.2 new

function newOperator(ctor, ...args) {
  if (typeof ctor !== 'function') {
    throw new TypeError('Type Error');
  }
  const obj = Object.create(ctor.prototype);
  const res = ctor.apply(obj, args);

  const isObject = typeof res === 'object' && res !== null;
  const isFunction = typeof res === 'function';
  return isObject || isFunction ? res : obj;
}

5.3 instanceof

const myInstanceof = (left, right) => {
  // 基本数据类型都返回false
  if (typeof left !== 'object' || left === null) return false;
  let proto = Object.getPrototypeOf(left);
  while (true) {
    if (proto === null) return false;
    if (proto === right.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

5.4 Object.assign

Object.defineProperty(Object, 'assign', {
  value: function(target, ...args) {
    if (target == null) {
      return new TypeError('Cannot convert undefined or null to object');
    }
    
    // 目标对象需要统一是引用数据类型,若不是会自动转换
    const to = Object(target);

    for (let i = 0; i < args.length; i++) {
      // 每一个源对象
      const nextSource = args[i];
      if (nextSource !== null) {
        // 使用for...in和hasOwnProperty双重判断,确保只拿到本身的属性、方法(不包含继承的)
        for (const nextKey in nextSource) {
          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
    }
    return to;
  },
  // 不可枚举
  enumerable: false,
  writable: true,
  configurable: true,
})

5.5 throttle(节流)

高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。

应用场景:监听滚动条加载数据、滚动页面图片懒加载等

const throttle = (fn, time) => {
  let flag = true;
  return function() {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, arguments);
      flag = true;
    }, time);
  }
}

大圣老师的写法

// 1. 节流
var throttle = function (fn, wait = 1000) {
  let prev = new Date();
  return function (...arguments) {
    // console.log('======= arguments:', arguments);
    let now = new Date();
    if (now - prev > wait) {
      fn.apply(this, arguments);
      prev = now;
    }
  }
}
// 用户滚动后的实际操作
let num = 1;
var handleAjax = function () {
  console.log('======= num++:', num++);
}
// 监听用户滚动事件
document.addEventListener('scroll', throttle(handleAjax), false);

5.6 debounce(防抖)

触发高频时间后n秒内函数只会执行一次,如果n秒内高频时间再次触发,则重新计算时间。

应用场景:输入框模糊查询、输入词语联想搜索、快速点击同一按钮等

const debounce = (fn, time) => {
  let timeout = null;
  return function() {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      fn.apply(this, arguments);
    }, time);
  }
};

自己的写法

// 2. 防抖
function debounce (fn, wait = 1000) {
  var timeout;
  return function (...arguments) {
    clearTimeout(timeout)
    timeout = setTimeout(fn, wait);
  }
}
// 用户滚动后的实际操作
let num = 1;
var handleAjax = function () {
  console.log('======= num++:', num++);
}
// 监听用户滚动事件
document.addEventListener('scroll', debounce(handleAjax), false);

5.7 函数柯里化

function curry(fn, args) {
    var length = fn.length;
    args = args || [];

    return function() {
        var _args = args.slice(0),
            arg, i;
        for (i = 0; i < arguments.length; i++) {
            arg = arguments[i];
            _args.push(arg);
        }

        if (_args.length < length) {
            return curry.call(this, fn, _args);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}

var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

5.8 数据双向绑定

let obj = {}
let input = document.getElementById('input')
let box = document.getElementById('box')
// 数据劫持
Object.defineProperty(obj, 'text', {
  configurable: true,
  enumerable: true,
  get() {
    // 获取数据就直接拿
    console.log('获取数据了')
  },
  set(newVal) {
    // 修改数据就重新赋值
    console.log('数据更新了')
    input.value = newVal
    box.innerHTML = newVal
  }
})
// 输入监听
input.addEventListener('keyup', function(e) {
  obj.text = e.target.value
})

5.8 watch

使用 Object.defineProperty 方法

(function(){
    var root = this;
    function watch(obj, name, func){
        var value = obj[name];

        Object.defineProperty(obj, name, {
            get: function() {
                return value;
            },
            set: function(newValue) {
                value = newValue;
                func(value)
            }
        });

        if (value) obj[name] = value
    }

    this.watch = watch;
})()

// 使用
var obj = {
    value: 1
}

watch(obj, "value", function(newvalue){
    document.getElementById('container').innerHTML = newvalue;
})

document.getElementById('button').addEventListener("click", function(){
    obj.value += 1
});

我们使用 proxy 再来写一下 watch 函数。使用效果如下:

(function() {
    var root = this;

    function watch(target, func) {

        var proxy = new Proxy(target, {
            get: function(target, prop) {
                return target[prop];
            },
            set: function(target, prop, value) {
                target[prop] = value;
                func(prop, value);
            }
        });

        return proxy;
    }

    this.watch = watch;
})()

var obj = {
    value: 1
}

var newObj = watch(obj, function(key, newvalue) {
    if (key == 'value') document.getElementById('container').innerHTML = newvalue;
})

document.getElementById('button').addEventListener("click", function() {
    newObj.value += 1
});

5.9 Promise

Promise() + then()

/**
 * 淳淳模拟手写
 * 1. 实现 resolve 和 reject
 *    1.1 状态不可变更
 *    1.2 throw
 * 2. 实现 then
 *    2.1 定时器情况
 *    2.2 链式调用
 *    2.3 微任务
 * 3. 其他方法
 *    3.1 all
 *    3.2 any
 *    3.3 race
 *    3.4 allSettled
*/
class LeePromise{
  // 静态属性
  static PENDING = 'pending';
  static FULFILLED = 'fulfilled';
  static REJECTED = 'rejected';

  // 构造方法
  constructor (executor) {
    // 初始化值
    this.initValue();
    // 初始化this指向
    this.initBind();
    // 捕获 executor 中的 throw 报错
    try {
      executor(this.resolve, this.reject);
    } catch (error) {
      // 捕捉到错误直接执行 reject
      this.reject(error);
    }
  }

  initValue() {
    // 最终的值
    this.PromiseResult = '';
    // 状态
    this.PromiseState = LeePromise.PENDING;
    // 保存成功的回调函数
    this.onFulfilledCallbacks = [];
    // 保存失败的回调函数
    this.onRejectedCallbacks = [];
  }

  initBind() {
    this.resolve = this.resolve.bind(this);
    this.reject = this.reject.bind(this);
  }

  resolve(value) {
    // state是不可变的
    if (this.PromiseState !== LeePromise.PENDING) return false;
    // 如果执行 resolve,状态变为 FULFILLED
    this.PromiseState = LeePromise.FULFILLED;
    // 终值为传进来的值
    this.PromiseResult = value;
    // 执行保存的成功回调
    while (this.onFulfilledCallbacks.length) {
      this.onFulfilledCallbacks.shift()(this.PromiseResult);
    }
    // console.log('value:', value);
  }

  reject(reason) {
    // state是不可变的
    if (this.PromiseState !== LeePromise.PENDING) return false;
    // 如果执行 reject,状态变为 REJECTED
    this.PromiseState = LeePromise.REJECTED;
    // 终值为传进来的reason
    this.PromiseResult = reason;
    // 执行保存的失败回调
    while (this.onRejectedCallbacks.length) {
      this.onRejectedCallbacks.shift()(this.PromiseResult);
    }
    // console.log('reason:', reason);
  }

  then(onFulfilled, onRejected) {
    // 接收两个回调 onFulfilled, onRejected

    // 参数校验,确保一定是函数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw(reason) };

    var thenPromise = new LeePromise((resolve, reject) => {
      var callbackPromise = cb => {
        // 因为整个 Promise 的 then 需要异步执行,暂时用 setTimeout 代替微任务队列
        setTimeout(() => {
          try {
            const x = cb(this.PromiseResult);
            // console.log('======= x:', x);
            if (x === LeePromise) {
              throw new Error('不能返回自身');
            }
            if (x instanceof LeePromise) {
              // 如果返回值是Promise
              // 如果返回值是promise对象,返回值为成功,新promise就是成功
              // 如果返回值是promise对象,返回值为失败,新promise就是失败
              // 谁知道返回的promise是失败成功?只有then知道
              x.then(resolve, reject);
            } else {
              // 非 Promise 直接成功
              resolve(x);
            }
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      switch (this.PromiseState) {
        case LeePromise.FULFILLED:
          /** 成功的 then 会走这里 */
          // onFulfilled(this.PromiseResult);
          callbackPromise(onFulfilled);
          break;
        case LeePromise.REJECTED:
          /** 失败的 then 会走这里 */
          // onRejected(this.PromiseResult);
          callbackPromise(onRejected);
          break;
        default:
          /** promise 中有定时器时会走这 */
          // this.onFulfilledCallbacks.push(onFulfilled.bind(this));
          this.onFulfilledCallbacks.push(callbackPromise.bind(this, onFulfilled));
          // this.onRejectedCallbacks.push(onRejected.bind(this));
          this.onRejectedCallbacks.push(callbackPromise.bind(this, onRejected));
          break;
      }
    });

    // 返回包装后的新 promise 对象
    return thenPromise;
  }
}

各种测试结果

/** 测试用例 */
// 1. 实现 resolve 和 reject
var test1 = new LeePromise((resolve, reject) => {
  // throw('这是报错');
  resolve('成功了');
  reject('失败了');
});
console.log('======= test1:', test1);

// 2. 实现 then
var test2 = new LeePromise((resolve, reject) => {
  // throw('这是报错');
  resolve('成功了');
  reject('失败了');
});
test2.then(
  1, '2' // 测试不是函数类型的错误
  // res => console.log('then:', res),
  // err => console.log('then:', err)
);
console.log('======= test2:', test2);

// 2.1 定时器情况
var test21 = new LeePromise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功了');
    reject('失败了');
  }, 1000);
});
test21.then(
  res => console.log('then:', res),
  err => console.log('then:', err)
);
console.log('======= test21:', test21);

// 2.2 链式调用
const test22 = new LeePromise((resolve, reject) => {
  resolve(100);
});
test22.then(
  res => {
    console.log('then:', res); // 100
    return 2 * res;
  },
  err => console.log(err)
).then(
  res => {
    console.log('then then:', res); // 200
    return new LeePromise((resolve, reject) => resolve(3 * res));
  },
  err => console.log(err)
).then(
  res => console.log('then then then:', res), // 600
  err => console.log(err)
);
console.log('======= test22:', test22);

// 2.3 微任务
console.log('sync:', 50);

1. all

全对再返回,一个错误就返回

  • 接收一个Promise数组,数组中如有非Promise项,则此项当做成功
  • 如果所有Promise都成功,则返回成功结果数组
  • 如果有一个Promise失败,则返回这个失败结果
    static all(promises) {
        const result = []
        let count = 0
        return new MyPromise((resolve, reject) => {
            const addData = (index, value) => {
                result[index] = value
                count++
                if (count === promises.length) resolve(result)
            }
            promises.forEach((promise, index) => {
                if (promise instanceof MyPromise) {
                    promise.then(res => {
                        addData(index, res)
                    }, err => reject(err))
                } else {
                    addData(index, promise)
                }
            })
        })
    }

2. any(any与all相反)

全错再返回,一个正确就返回

注:如果成功的那个没有返回值,一直不返回

  • 接收一个Promise数组,数组中如有非Promise项,则此项当做成功
  • 如果有一个Promise成功,则返回这个成功结果
  • 如果所有Promise都失败,则报错
    static any(promises) {
        return new Promise((resolve, reject) => {
            let count = 0
            promises.forEach((promise) => {
                promise.then(val => {
                    resolve(val)
                }, err => {
                    count++
                    if (count === promises.length) {
                        reject(new AggregateError('All promises were rejected'))
                    }
                })
            })
        })
    }

3. race

一个结束就返回,无论对错

  • 接收一个Promise数组,数组中如有非Promise项,则此项当做成功
  • 哪个Promise最快得到结果,就返回那个结果,无论成功失败
    static race(promises) {
        return new MyPromise((resolve, reject) => {
            promises.forEach(promise => {
                if (promise instanceof MyPromise) {
                    promise.then(res => {
                        resolve(res)
                    }, err => {
                        reject(err)
                    })
                } else {
                    resolve(promise)
                }
            })
        })
    }

4. allSettled

全部结束再返回

  • 接收一个Promise数组,数组中如有非Promise项,则此项当做成功
  • 把所有Promise的结果,集合成数组,返回
    static allSettled(promises) {
        return new Promise((resolve, reject) => {
            const res = []
            let count = 0
            const addData = (status, value, i) => {
                res[i] = {
                    status,
                    value
                }
                count++
                if (count === promises.length) {
                    resolve(res)
                }
            }
            promises.forEach((promise, i) => {
                if (promise instanceof MyPromise) {
                    promise.then(res => {
                        addData('fulfilled', res, i)
                    }, err => {
                        addData('rejected', err, i)
                    })
                } else {
                    addData('fulfilled', promise, i)
                }
            })
        })
    }

5.x AJAX

const getJSON = function(url) {
  return new Promise((resolve, reject) => {
    const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
    xhr.open('GET', url, false);
    xhr.setRequestHeader('Accept', 'application/json');
    xhr.onreadystatechange = function() {
      if (xhr.readyState !== 4) return;
      if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    }
    xhr.send();
  })
}

5.x 图片懒加载

可以给img标签统一自定义属性data-src='default.png',当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。

function lazyload() {
  const imgs = document.getElementsByTagName('img');
  const len = imgs.length;
  // 视口的高度
  const viewHeight = document.documentElement.clientHeight;
  // 滚动条高度
  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;
  for (let i = 0; i < len; i++) {
    const offsetHeight = imgs[i].offsetTop;
    if (offsetHeight < viewHeight + scrollHeight) {
      const src = imgs[i].dataset.src;
      imgs[i].src = src;
    }
  }
}

// 可以使用节流优化一下
window.addEventListener('scroll', lazyload);

5.x 滚动加载

原理就是监听页面滚动事件,分析clientHeight、scrollTop、scrollHeight三者的属性关系。

window.addEventListener('scroll', function() {
  const clientHeight = document.documentElement.clientHeight;
  const scrollTop = document.documentElement.scrollTop;
  const scrollHeight = document.documentElement.scrollHeight;
  if (clientHeight + scrollTop >= scrollHeight) {
    // 检测到滚动至页面底部,进行后续操作
    // ...
  }
}, false);

5.x 深拷贝

var obj = {
  number: 233,
  string: 'String',
  boolean: false,
  null: null,
  undefind: undefined,
  NaN: NaN,
  array: [{ name: '小明' }, { age: 18 }],
  function: function funcName () {
    console.log('this', this);
  },
  date: new Date(),
  reg: new RegExp(),
}

function deepClone (targetObj) {
  let temp;
  if (typeof targetObj === 'object') {
    if (targetObj === null) {
      temp = null;
    } else {
      if (targetObj instanceof Date) {
        temp = targetObj;
      } else if (targetObj instanceof RegExp) {
        temp = targetObj;
      } else if (targetObj instanceof Array) {
        temp = [];
        for (let i = 0, len = targetObj.length; i < len; i++) {
          temp.push(deepClone(targetObj[i]));
        }
      } else {
        temp = {};
        for (const j in targetObj) {
          temp[j] = deepClone(targetObj[j]);
        }
      }
    }
  } else {
    temp = targetObj;
  }
  return temp;
};

console.log(deepClone(obj));

5.x 获取URL参数

var url = 'https://juejin.cn/' +
  '?utm_source=gold_browser_extension' +
  '&' +
  'param={"adad":"wewq"}' +
  '&' +
  'url=https://www.baidu.com/?baiduparam=666' +
  '&' +
  'juejinparam=thisIsJJ' +
  '';

/**
 * 获取iframe的指定参数
 * @param variable 参数的key名
 * (传variable即获取当前String参数,不传即获取全部参数Object对象)
 */
function getIframeParams(variable = '') {
  // 最终得到的参数字符串
  // let paramStr = window.location.search.substring(1);
  let paramStr = url.slice(url.indexOf('?') + 1);
  paramStr = decodeURIComponent(paramStr);
  if (paramStr === '') {
    return {};
  }
  const paramsObj = new Object();
  // * 需要解析的参数(此处需要及时更新)
  const keys = ['utm_source', 'param', 'url', 'juejinparam', 'baiduparam']; // ,
  // 将所有参数按索引存储为一个对象
  const keysObj = new Object();
  // tslint:disable-next-line:prefer-for-of
  for (let i = 0; i < keys.length; i++) {
    const idx = paramStr.indexOf(keys[i]);
    if (idx >= 0) {
      keysObj[idx] = keys[i];
    }
  }
  // 遍历参数对象
  const entries = Object.entries(keysObj);
  for (let k = 0; k < entries.length; k++) {
    // 加上 = 算一个字符
    const start = entries[k][0] * 1 + entries[k][1].length + 1;
    // 去掉倒数第一个 & 算一个字符
    const end = k + 1 < entries.length ? (entries[k + 1][0] * 1 - 1) : 99999;
    const value = paramStr.slice(start, end);
    // 单独获取某个值
    if (variable !== '') {
      return value;
    }
    paramsObj[entries[k][1]] = value;
  }
  return paramsObj;
}

console.log(getIframeParams());

5.x 解析字符串

var a = { b: 123, c: '456', e: '789' }
var str = `a{a.b}aa{a.c}aa {a.d}aaaa`

function fn(str) {
    return str.replace(/{\w\.\w}/g, ($1) => {
        const { groups: { value } } = /{\w\.(?<value>\w)}/.exec($1);
        return a[value] || $1;
    })
}

fn(str) // "a123aa456aa {a.d}aaaa"
const fn1 = (str, obj) => {
	let res = '';
    // 标志位,标志前面是否有{
	let flag = false;
	let start;
	for (let i = 0; i < str.length; i++) {
		if (str[i] === '{') {
			flag = true;
			start = i + 1;
			continue;
		}
		if (!flag) res += str[i];
		else {
			if (str[i] === '}') {
				flag = false;
				res += match(str.slice(start, i), obj);
			}
		}
	}
	return res;
}
// 对象匹配操作
const match = (str, obj) => {
	const keys = str.split('.').slice(1);
	let index = 0;
	let o = obj;
	while (index < keys.length) {
		const key = keys[index];
		if (!o[key]) {
			return `{${str}}`;
		} else {
			o = o[key];
		}
		index++;
	}
	return o;
}

拾几。前端算法

4.0 排序算法

排序算法对比

4.0.1 关于时间复杂度:

  • 平方阶 (O(n2)) 排序
    • 各类简单排序:直接插入、直接选择和冒泡排序。
  • 线性对数阶 (O(nlog2n)) 排序
    • 快速排序、堆排序和归并排序;
  • O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。
    • 希尔排序
  • 线性阶 (O(n)) 排序
    • 基数排序,此外还有桶、箱排序。

4.0.2 关于稳定性:

  • 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
  • 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

4.0.3 名词解释:

  • n:数据规模
  • k:“桶”的个数
  • In-place:占用常数内存,不占用额外内存
  • Out-place:占用额外内存
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同

4.1 冒泡排序

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。

4.1.1 算法步骤

  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

4.1.2 什么时候最快

当输入的数据已经是正序时(都已经是正序了,我还要你冒泡排序有何用啊)。

4.1.3 什么时候最慢

当输入的数据是反序时(写一个 for 循环反序输出数据不就行了,干嘛要用你冒泡排序呢,我是闲的吗)。

4.1.4 动图演示

冒泡排序

4.1.5 JavaScript 代码实现

4.1.5.1 【最佳】增加判断标识,及时跳出循环

function bubbleSort3 (arr) {
  for (let i = 0; i < arr.length; i++) {
    let bool = false;
    for (let j = arr.length - 1; j > i; j--) {
      if (arr[j - 1] > arr[j]) {
        [arr[j - 1], arr[j]] = [arr[j], arr[j - 1]];
        bool = true;
      }
    }
    if (!bool) break;
  }
  // console.log('======= arr:', JSON.stringify(arr));
}
console.time('bubbleSort3 耗时');
bubbleSort3(arr);
console.timeEnd('bubbleSort3 耗时');

4.1.5.2 每趟排序结束记录最后交换的位置,下一趟只扫描到此即可

function bubbleSort4 (arr) {
  let i = arr.length - 1;
  while (i > 0) {
    let pos = 0;
    for (let j = 0; j < i.length; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        pos = j;
      }
    }
    i = pos;
  }
  // console.log('======= arr:', JSON.stringify(arr));
}
console.time('bubbleSort4 耗时');
bubbleSort4(arr);
console.timeEnd('bubbleSort4 耗时');

4.2 快速排序

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

4.2.1 算法步骤

  1. 从数列中挑出一个元素,称为 “基准”(pivot);

  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

4.2.2 动图演示

动图演示

4.2.3 JavaScript 代码实现

4.2.3.1 【量大,最优】普通遍历

function quickSort1 (arr, leftV, rightV) {
  let len = arr.length;
  let partitionIndex;
  // ! left 和 right 不能单纯的用 left || 0 去判断,因为会错失值就是 0 的情况
  let left = typeof leftV !== 'number' ? 0 : leftV;
  let right = typeof rightV !== 'number' ? len - 1 : rightV;
  if (left < right) {
    partitionIndex = partition1(arr, left, right);
    quickSort1(arr, left, partitionIndex - 1);
    quickSort1(arr, partitionIndex + 1, right);
  }
  // console.log('======= arr:', JSON.stringify(arr));
  return arr;
}
function partition1 (arr, left, right) {
  let pivot = left;
  let index = pivot + 1;
  for (let i = index; i <= right; i++) {
    if (arr[i] < arr[pivot]) {
      // swap(arr, i, index);
      [arr[i], arr[index]] = [arr[index], arr[i]];
      index++;
    }
  }
  // swap(arr, pivot, index - 1);
  [arr[pivot], arr[index - 1]] = [arr[index - 1], arr[pivot]];
  return index - 1;
}
function swap (arr, i, j) {
  var temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}
// 报错:RangeError: Maximum call stack size exceeded
console.time('quickSort1 耗时');
quickSort1(arr);
console.timeEnd('quickSort1 耗时');

4.2.3.1 【量小,最优】阮一峰的思路

参考链接

function quickSort2 (arr) {
  if (arr.length <= 1) return arr;
  let pivotIndex = Math.floor(arr.length / 2);
  let pivot = arr.splice(pivotIndex, 1)[0]; // ! 是 splice 不是 slice
  let left = [];
  let right = [];
  for (let i = 0; i < arr.length; i++) {
    arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i]);
  }
  const res = quickSort2(left).concat([pivot], quickSort2(right));
  // console.log('======= arr:', JSON.stringify(res));
  return res;
}
console.time('quickSort2 耗时');
quickSort2(arr);
console.timeEnd('quickSort2 耗时');

4.3 插入排序

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

4.3.1 算法步骤

  1. 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

  2. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

4.3.2 动图演示

动图演示

4.3.3 JavaScript 代码实现

4.3.3.1 【最优】for + if

function insertionSort2 (arr) {
  let temp;
  for (let i = 1; i < arr.length; i++) { // ! i 从 1 开始
    temp = arr[i];
    // console.log('======= i:', i);
    let j = i - 1;
    // ! 如果把 let j = i - 1 放在 for 第一个参数,最后会多出个 NaN: 8
    // 因为 j 只是 for 内部块级作用域的变量,j + 1 === undefind + 1 === NaN
    // 所以 arr[j + 1] = arr[NaN] = 8
    for (j; j >= 0; j--) { // ! j >= 0 不是 j > 0
      // console.log('======= jjj:', j);
      if (arr[j] > temp) {
        arr[j + 1] = arr[j];
      } else {
        break;
      }
    }
    // console.log('======= j:', j);
    arr[j + 1] = temp;
  }
  // console.log('======= arr:', JSON.stringify(arr));
}
console.time('insertionSort2 耗时');
insertionSort2(arr);
console.timeEnd('insertionSort2 耗时');

4.3.3.2 普通遍历 + while

function insertionSort1 (arr) {
  let len = arr.length;
  let pi; // previous index
  let e; // current element
  for (let i = 1; i < len; i++) { // ! i 从 1 开始
    pi = i - 1;
    e = arr[i];
    // ! 前一个指针 > 0 && 前一个元素 > 当前元素
    while (pi >= 0 && arr[pi] > e) {
      // ! 后一个指针 <== 当前指针
      arr[pi + 1] = arr[pi];
      // ! 指针往前移一位
      pi--;
    }
    // ! 后一个指针 = 当前元素
    arr[pi + 1] = e;
  }
  // console.log('======= arr:', JSON.stringify(arr));
}
console.time('insertionSort1 耗时');
insertionSort1(arr);
console.timeEnd('insertionSort1 耗时');

4.4 选择排序

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

4.4.1 算法步骤

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置

  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

  3. 重复第二步,直到所有元素均排序完毕。

4.4.2 动图演示

动图演示

4.4.3 JavaScript 代码实现

function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
        minIndex = i;
        for (var j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {     // 寻找最小的数
                minIndex = j;                 // 将最小数的索引保存
            }
        }
        temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;
}

4.5 二分查找

构造数据前提条件(绝对正序排列)

let arr = [1, 3, 4, 6, 6, 9, 12]; // 正序
// for (let i = 0; i < 10; i++) { arr.push(Math.floor(Math.random() * 1000)); } // 乱序
// arr.sort((a, b) => a - b); // 绝对升序排列
// console.log('======= 原始数组:', arr);

三种变式

  • 第一个 ===key 的元素
  • 第一个 >=key 的元素
  • 最后一个 <=key 的元素
function binarySearch1 (arr, key) {
  let left = 0;
  let right = arr.length; // ! 是否需要 len - 1
  let mid;
  while (left <= right) {
    mid = (left + right) >> 1; // ! 等同于 Math.floor();
    if (arr[mid] >= key) {
      right = mid - 1;
    } else {
      left = mid + 1;
    }
  }
  /** 1. 第一个 ===key 的元素 → -1 */
  if (left <= arr.length && arr[left] === key) {
    return left;
  } else {
    return -1;
  }
  /** 2. 第一个 >=key 的元素 → 3 */
  // return left;
  /** 3. 最后一个 <=key 的元素 → 2 */
  // return right;
}
console.time('binarySearch1 耗时');
console.log(binarySearch1(arr, 5));
console.timeEnd('binarySearch1 耗时');

又三种变式

  • 最后一个 ===key 的元素
  • 第一个 >key 的元素
  • 最后一个 <=key 的元素
function binarySearch2 (arr, key) {
  let left = 0;
  let right = arr.length;
  let mid;
  while (left <= right) {
    mid = (left + right) >> 1;
    if (arr[mid] > key) { // ! 唯一区别 >= 改成 >
      right = mid - 1;
    } else {
      left = mid + 1;
    }
  }
  /** 1. 最后一个 ===key 的元素 → -1 */
  if (right <= arr.length && arr[right] === key) { // ! 第二个区别 判断并返回 right
    return right;
  } else {
    return -1;
  }
  /** 2. 第一个 >key 的元素 → 3 */
  // return left;
  /** 3. 最后一个 <=key 的元素 → 2 */
  // return right;
}
console.time('binarySearch2 耗时');
console.log(binarySearch2(arr, 5));
console.timeEnd('binarySearch2 耗时');

4.6 获取树的最大深度

参考链接

原始数据

  var treeArr={
    name:'root',
    child:[
      {
        name: 'a',
        child: [
          {
            name: 'b',
            child: [
              {name: 'd'},
              {
                name: 'e',
                child:[
                  {name: 'h'},
                  {
                    name: 'i',
                    child: [
                      {name: 'j'}
                    ]
                  },
                ]
              },
            ],
          },
          {
            name: 'c',
            child: [
              {name: 'f'},
              {
                name: 'g',
                child:[
                {name: 'k'},
                {name: 'l'},
                {name: 'm'},
                {name: 'n'},
                ]
              },
            ]
          },
        ]
      }
    ]
  }

算法实现

  var deepArr = [];				//定义存放每条路径深度的数组
  getDeep(treeArr,0,deepArr);	//调用函数,三个参数分别为,结点,计数器,以及存放深度的数组

  function getDeep(data,i,deepArr){
  	//获取当前结点的子数组,并且打印当前结点的值
  	console.log(data.name)
    var treeRoot = data.child
    //如果当前结点没有子数组了(没有子结点)就跳出当前递归,并且使计数器+1,并把计数器i的值存入深度数组中
    if(!treeRoot){
      i++
      deepArr.push(i);
      return
    }
    //如果当前结点有子数组,就要使计数器+1
    i++
    //通过for循环来找出每一条路径,对遍历到的结点使用递归
    for(let j=0;j<treeRoot.length;j++){
      getDeep(treeRoot[j],i,deepArr)		//递归时传入的就是当前结点的第j个子结点,当这第j个子结点中的所有子孙结点全部遍历完成之后,再去遍历第j+1个结点的所有子孙结点
    }
  }
  //最后的到的这个深度数组,就可以通过对每一项进行比较从而得出最大值即最大深度
  deepArr.sort((a,b) => b - a)
  console.log(deepArr[0])

4.7 深度优先遍历

优缺点

  • 占内存少但速度较慢
  • 能找出所有解决方案
  • 优先搜索一棵子树,然后是另一棵,所以和广搜对比,有着内存需要相对较少的优点
  • 要多次遍历,搜索所有可能路径,标识做了之后还要取消。
  • 在深度很大的情况下效率不高

数据结构如下,查找西宁市的数据

var tree = {
    name: '中国',
    children: [
        {
            name: '北京',
            children: [
                {
                    name: '朝阳群众',
                    children: [
                        {
                            name: '西宁市',
                            code: '0521',
                        }
                    ]
                },
                {
                    name: '海淀区',
                },
                {
                    name: '昌平区',
                },
            ],
        },
        {
            name: '浙江省',
            children: [
                {
                    name: '杭州市',
                    code: '0571',
                },
                {
                    name: '嘉兴市',
                },
                {
                    name: '绍兴市',
                },
                {
                    name: '宁波市',
                },
            ],
        },
    ],
};
var node = dfs/bfs(tree, '西宁市');
console.log(node); // 输出: { name: '西宁市', code: '0521' }

递归写法

function dfs(tree, name){
    if(tree.name === name){
        return tree;
    }
    if(!tree.name && !tree.children){
        return null
    }
    for(let i = 0; i < tree.children.length; i++){
        var result = dfs(tree.children[i], name)
        if(result){
            return result;
        }
    }   
    return null
}
 /*
* 深度遍历
* 思路:遍历第一层,得到每一个item,获取其中的name,再对item下一级的children进行递归
* */
function dfs(arr) {
    let result = [];
    for(let key in arr) {
        let item = arr[key];
        result.push(item.name);
        if(item.children) result.push(...dfs(item.children))
    }
    return result;
}

非递归写法

根据先进后出原则,我们从右即左,按深度方式遍历(找到终止循环break)

function dfs(tree, name){
    var stack = [], result = {};
    stack.push(tree)
    while(stack.length != 0){
        var item = stack.pop();
        if(item.name == name){
            result = item;
            break
        }
        let children = item.children;
        if(children){
            for(let i = children.length - 1; i >= 0; i--){
                stack.push(children[i])
            }
        }
    }    return result
}

4.8 广度优先遍历

优势

  • 占内存多速度较快,在距离和深度成正比的情况下能较快地求出最优解。
  • 对于解决最短或最少问题特别有效,而且寻找深度小
  • 每个结点只访问一遍,结点总是以最短路径被访问,所以第二次路径确定不会比第一次短
  • 内存耗费量大(需要开大量的数组单元用来存储状态)

递归写法

/*
*   思路:递归方式
* 	因为是按层进行递归,所以遇到children并不能马上进行,应该先定义数组收集好下层的children,当前层遍历完后再递归下一层
* 	例如:在遍历[a, a2]时,收集数组的下层[b,c,d,b2,c2,d2],依次类推
* */
function bfs(arr) {
    let result= [];
    let cacheLevel = []; // 记录下层待遍历的数组
    for(let key in arr) {
        let item = arr[key];
        result.push(item.name)
        if(item.children) cacheLevel.push(...item.children);
    }
    if(cacheLevel.length > 0) result.push(...bfs(cacheLevel));
    return result;
}

优化:队列思维实现

/*
* 队列思维实现:队列方式
* 思路:创建一个队列,不断读取第一个队列,发现下层有要遍历的则添加进队列中,然后每遍历完自身就去掉,知道队列中把所有的都遍历完。
* */
function queueBfs(arr) {
    let result = [];
    let queue = arr;
    while(queue.length > 0) {
        let item  = queue[0];
        result.push(item.name);
        if(item.children) queue.push(...item.children);
        queue.shift();
    }
    return result
}

非递归写法

根据先进先出原则,我们从上到下,按层级一层一层往下遍历 (找到终止循环break)

// 非递归算法 广度优先
function bfs(tree,name){
    var queue = [],result = {};;
    queue.unshift(tree);
    while(queue.length!=0){
        var item = queue.shift();
        if(item.name == name){
            result = item;
            break
        }
        var children=item.children;
        if(children){
            for(let i = 0; i < children.length; i++){
                queue.push(children[i])
            }
        }
    }    return result
}

4.9 构造二叉树

function Node (data, left, right) {
    this.data = data;
    this.left = left;
    this.right = right;
    this.show = function () {
        return this.data;
    };
}

function BST () {
    this.root = null;
    this.insert = function (data) {
        var node = new Node(data, null, null);
        if (this.root === null) {
            this.root = node;
        } else {
            var current = this.root;
            var parent;
            while (true) {
                parent = current;
                if (data < current.data) {
                    current = current.left;
                    if (current === null) {
                        parent.left = node;
                        break;
                    }
                } else {
                    current = current.right;
                    if(current === null) {
                        parent.right = node;
                        break;
                    }
                }
            }
        }
    };
    // 中序遍历
    this.inOrder = function (node) {
        if (node !== null) {
            this.inOrder(node.left);
            console.log(node.show());
            this.inOrder(node.right);
        }
    };
    // 先序遍历
    this.preOrder = function (node) {
        if (node !== null) {
            console.log(node.show());
            this.preOrder(node.left);
            this.preOrder(node.right);
        }
    };
    // 后序遍历
    this.afterOrder = function (node) {
        if (node !== null) {
            this.afterOrder(node.left);
            this.afterOrder(node.right);
            console.log(node.show());
        }
    };

    this.getMin = function () {
        var current = this.root;
        while (current.left !== null) {
            current = current.left;
        }
        return current.data;
    };


    this.getMax = function () {
        var current = this.root;
        while (current.right !== null) {
            current = current.right;
        }
        return current.data;
    };

    this.find = function (data) {
        var current = this.root;
        while (current !== null) {
            if (data < current.data) {
                current = current.left;
            } else if (data > current.data) {
                current = current.right;
            } else {
                return current;
            }
        }
        return null;
    };

    

    this.remove = function (data) {
        this.root = this._removeNode(this.root, data); //将根节点转换
    };

    this._getSmallest = function (node) {
        while(node.left!=null){
            node=node.left;
        }
        return node;
    };


    this._removeNode = function (node, data) {
        if (node === null) {
            return null;
        }
        if (data === node.data) {
            // 如果没有子节点
            if (node.right === null && node.left === null) {
                return null;
            }
            // 如果没有左子节点
            if (node.left === null) {
                return node.right;//直接指向其右节点
            }
            // 如果没有右子节点
            if (node.right === null) {
                return node.left;
            }
            // 如果有两个节点
            if (node.right !== null && node.left !== null) {
                var tempNode = this._getSmallest(node.right);   // 找到最小的右节点
                node.data = tempNode.data;
                node.right = this._removeNode(node.right, tempNode.data);    // 依次寻找
                return node;
            }
        } else if (data < node.data){
            node.left = this._removeNode(node.left, data);
            return node;
        } else {
            node.right = this._removeNode(node.right, data);
            return node;
        }
    };
}

二叉树  数据结构的使用方法如下:

复制代码
var bst = new BST ();
bst.insert(40);
bst.insert(20);
bst.insert(70);
bst.insert(60);
bst.insert(75);
bst.insert(71);
bst.insert(73);

bst.inOrder(bst.root);
bst.remove(70);
console.log("----------------");
bst.inOrder(bst.root);
console.log("----------------");
console.log(bst.find(73))