前端算法总结

排序

冒泡排序

冒泡排序的基本思想是,对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,
最终达到完全有序。

代码实现:


function bubbleSort(arr) {
    if (!Array.isArray(arr) || arr.length <= 1) return;
    let lastIndex = arr.length - 1;
    while (lastIndex > 0) { // 当最后一个交换的元素为第一个时,说明后面全部排序完毕
        let flag = true, k = lastIndex;
        for (let j = 0; j < k; j++) {
            if (arr[j] > arr[j + 1]) {
                flag = false;
              	lastIndex = j; // 设置最后一次交换元素的位置
                [arr[j], arr[j+1]] = [arr[j+1], arr[j]];
            }
        }
      	if (flag) break;
    }
}

冒泡排序有两种优化方式。

一种是外层循环的优化,我们可以记录当前循环中是否发生了交换,如果没有发生交换,则说明该序列已经为有序序列了。
因此我们不需要再执行之后的外层循环,此时可以直接结束。

一种是内层循环的优化,我们可以记录当前循环中最后一次元素交换的位置,该位置以后的序列都是已排好的序列,因此下
一轮循环中无需再去比较。

优化后的冒泡排序,当排序序列为已排序序列时,为最好的时间复杂度为 O(n)。

冒泡排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,是稳定排序。

详细资料可以参考:
《图解排序算法(一)》
《常见排序算法 - 鸡尾酒排序 》
《前端笔试&面试爬坑系列—算法》
《前端面试之道》

选择排序

选择排序的基本思想为每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止。

在算法实现时,每一趟确定最小元素的时候会通过不断地比较交换来使得首位置为当前最小,交换是个比较耗时的操作。其实
我们很容易发现,在还未完全确定当前最小元素之前,这些交换都是无意义的。我们可以通过设置一个变量 min,每一次比较
仅存储较小元素的数组下标,当轮循环结束之后,那这个变量存储的就是当前最小元素的下标,此时再执行交换操作即可。

代码实现:

function selectSort(array) {

  let length = array.length;

  // 如果不是数组或者数组长度小于等于1,直接返回,不需要排序 
  if (!Array.isArray(array) || length <= 1) return;

  for (let i = 0; i < length - 1; i++) {

    let minIndex = i; // 设置当前循环最小元素索引

    for (let j = i + 1; j < length; j++) {

      // 如果当前元素比最小元素索引,则更新最小元素索引
      if (array[minIndex] > array[j]) {
        minIndex = j;
      }
    }

    // 交换最小元素到当前位置
    // [array[i], array[minIndex]] = [array[minIndex], array[i]];
    swap(array, i, minIndex);
  }

  return array;
}

// 交换数组中两个元素的位置
function swap(array, left, right) {
  var temp = array[left];
  array[left] = array[right];
  array[right] = temp;
}

选择排序不管初始序列是否有序,时间复杂度都为 O(n²)。

选择排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,不是稳定排序。

详细资料可以参考:
《图解排序算法(一)》

插入排序

直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。

插入排序核心–扑克牌思想: 就想着自己在打扑克牌,接起来一张,放哪里无所谓,再接起来一张,比第一张小,放左边,
继续接,可能是中间数,就插在中间…依次

代码实现:

function insertSort(array) {

  let length = array.length;

  // 如果不是数组或者数组长度小于等于1,直接返回,不需要排序 
  if (!Array.isArray(array) || length <= 1) return;

  // 循环从 1 开始,0 位置为默认的已排序的序列
  for (let i = 1; i < length; i++) {
    let temp = array[i]; // 保存当前需要排序的元素
    let j = i;

    // 在当前已排序序列中比较,如果比需要排序的元素大,就依次往后移动位置
    while (j -1 >= 0 && array[j - 1] > temp) {
      array[j] = array[j - 1];
      j--;
    }

    // 将找到的位置插入元素
    array[j] = temp;
  }

  return array;
}

当排序序列为已排序序列时,为最好的时间复杂度 O(n)。

插入排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,是稳定排序。

详细资料可以参考:
《图解排序算法(一)》

希尔排序

希尔排序的基本思想是把数组按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的元
素越来越多,当增量减至1时,整个数组恰被分成一组,算法便终止。

代码实现:

function hillSort(array) {

  let length = array.length;

  // 如果不是数组或者数组长度小于等于1,直接返回,不需要排序 
  if (!Array.isArray(array) || length <= 1) return;


  // 第一层确定增量的大小,每次增量的大小减半
  for (let gap = parseInt(length >> 1); gap >= 1; gap = parseInt(gap >> 1)) {

    // 对每个分组使用插入排序,相当于将插入排序的1换成了 n
    for (let i = gap; i < length; i++) {
      let temp = array[i];
      let j = i;

      while (j - gap >= 0 && array[j - gap] > temp) {
        array[j] = array[j - gap];
        j -= gap;
      }
      array[j] = temp;
    }
  }

  return array;
}

希尔排序是利用了插入排序对于已排序序列排序效果最好的特点,在一开始序列为无序序列时,将序列分为多个小的分组进行
基数排序,由于排序基数小,每次基数排序的效果较好,然后在逐步增大增量,将分组的大小增大,由于每一次都是基于上一
次排序后的结果,所以每一次都可以看做是一个基本排序的序列,所以能够最大化插入排序的优点。

简单来说就是,由于开始时每组只有很少整数,所以排序很快。之后每组含有的整数越来越多,但是由于这些数也越来越有序,
所以排序速度也很快。

希尔排序的时间复杂度根据选择的增量序列不同而不同,但总的来说时间复杂度是小于 O(n^2) 的。

插入排序是一个稳定排序,但是在希尔排序中,由于相同的元素可能在不同的分组中,所以可能会造成相同元素位置的变化,
所以希尔排序是一个不稳定的排序。

希尔排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(n^s) ,空间复杂度为 O(1) ,不是稳定排序。

详细资料可以参考:
《图解排序算法(二)之希尔排序》
《数据结构基础 希尔排序 之 算法复杂度浅析》

归并排序

归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略。递归的将数组两两分开直到只包含一个元素,然后
将数组排序合并,最终合并为排序好的数组。

代码实现:

function mergeSort(array) {

  let length = array.length;

  // 如果不是数组或者数组长度小于等于0,直接返回,不需要排序 
  if (!Array.isArray(array) || length === 0) return;

  if (length === 1) {
    return array;
  }

  let mid = parseInt(length >> 1), // 找到中间索引值
    left = array.slice(0, mid), // 截取左半部分
    right = array.slice(mid, length); // 截取右半部分

  return merge(mergeSort(left), mergeSort(right)); // 递归分解后,进行排序合并
}


function merge(leftArray, rightArray) {

  let result = [],
    leftLength = leftArray.length,
    rightLength = rightArray.length,
    il = 0,
    ir = 0;

  // 左右两个数组的元素依次比较,将较小的元素加入结果数组中,直到其中一个数组的元素全部加入完则停止
  while (il < leftLength && ir < rightLength) {
    if (leftArray[il] < rightArray[ir]) {
      result.push(leftArray[il++]);
    } else {
      result.push(rightArray[ir++]);
    }
  }

  // 如果是左边数组还有剩余,则把剩余的元素全部加入到结果数组中。
  while (il < leftLength) {
    result.push(leftArray[il++]);
  }

  // 如果是右边数组还有剩余,则把剩余的元素全部加入到结果数组中。
  while (ir < rightLength) {
    result.push(rightArray[ir++]);
  }

  return result;
}

归并排序将整个排序序列看成一个二叉树进行分解,首先将树分解到每一个子节点,树的每一层都是一个归并排序的过程,每
一层归并的时间复杂度为 O(n),因为整个树的高度为 lgn,所以归并排序的时间复杂度不管在什么情况下都为O(nlogn)。

归并排序的空间复杂度取决于递归的深度和用于归并时的临时数组,所以递归的深度为 logn,临时数组的大小为 n,所以归
并排序的空间复杂度为 O(n)。

归并排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(nlogn) ,空间复杂度为 O(n) ,是稳定排序。

详细资料可以参考:
《图解排序算法(四)之归并排序》
《归并排序的空间复杂度?》

快速排序

快速排序的基本思想是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据
都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

代码实现:

function quickSort(array, start, end) {

  let length = array.length;

  // 如果不是数组或者数组长度小于等于1,直接返回,不需要排序 
  if (!Array.isArray(array) || length <= 1 || start >= end) return;

  let index = partition(array, start, end); // 将数组划分为两部分,并返回右部分的第一个元素的索引值

  quickSort(array, start, index - 1); // 递归排序左半部分
  quickSort(array, index + 1, end); // 递归排序右半部分
}


function partition(array, start, end) {

  let pivot = array[start]; // 取第一个值为枢纽值,获取枢纽值的大小


  // 当 start 等于 end 指针时结束循环
  while (start < end) {

    // 当 end 指针指向的值大等于枢纽值时,end 指针向前移动
    while (array[end] >= pivot && start < end) {
      end--;
    }

    // 将比枢纽值小的值交换到 start 位置
    array[start] = array[end];

    // 移动 start 值,当 start 指针指向的值小于枢纽值时,start 指针向后移动
    while (array[start] < pivot && start < end) {
      start++;
    }

    // 将比枢纽值大的值交换到 end 位置,进入下一次循环
    array[end] = array[start];
  }

  // 将枢纽值交换到中间点
  array[start] = pivot;

  // 返回中间索引值
  return start;
}

这一种方法是填空法,首先将第一个位置的数作为枢纽值,然后 end 指针向前移动,当遇到比枢纽值小的值或者 end 值
等于 start 值的时候停止,然后将这个值填入 start 的位置,然后 start 指针向后移动,当遇到比枢纽值大的值或者
start 值等于 end 值的时候停止,然后将这个值填入 end 的位置。反复循环这个过程,直到 start 的值等于 end 的
值为止。将一开始保留的枢纽值填入这个位置,此时枢纽值左边的值都比枢纽值小,枢纽值右边的值都比枢纽值大。然后在递
归左右两边的的序列。

当每次换分的结果为含 ⌊n/2⌋和 ⌈n/2⌉−1 个元素时,最好情况发生,此时递归的次数为 logn,然后每次划分的时间复杂
度为 O(n),所以最优的时间复杂度为 O(nlogn)。一般来说只要每次换分都是常比例的划分,时间复杂度都为 O(nlogn)。

当每次换分的结果为 n-1 和 0 个元素时,最坏情况发生。划分操作的时间复杂度为 O(n),递归的次数为 n-1,所以最坏
的时间复杂度为 O(n²)。所以当排序序列有序的时候,快速排序有可能被转换为冒泡排序。

快速排序的空间复杂度取决于递归的深度,所以最好的时候为 O(logn),最坏的时候为 O(n)。

快速排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(logn) ,不是稳定排序。

详细资料可以参考:
《图解排序算法(五)之快速排序——三数取中法》
《关于快速排序的四种写法》
《快速排序的时间和空间复杂度》
《快速排序最好,最坏,平均复杂度分析》
《快速排序算法的递归深度》

堆排序

堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行
交换,此时末尾就为最大值。然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,
便能得到一个有序序列了。

function heapSort(array) {

  let length = array.length;

  // 如果不是数组或者数组长度小于等于1,直接返回,不需要排序 
  if (!Array.isArray(array) || length <= 1) return;

  buildMaxHeap(array); // 将传入的数组建立为大顶堆

  // 每次循环,将最大的元素与末尾元素交换,然后剩下的元素重新构建为大顶堆
  for (let i = length - 1; i > 0; i--) {
    swap(array, 0, i);
    adjustMaxHeap(array, 0, i); // 将剩下的元素重新构建为大顶堆
  }

  return array;
}


function adjustMaxHeap(array, index, heapSize) {
  let iMax,
    iLeft,
    iRight;

  while (true) {
    iMax = index; // 保存最大值的索引
    iLeft = 2 * index + 1; // 获取左子元素的索引
    iRight = 2 * index + 2; // 获取右子元素的索引

    // 如果左子元素存在,且左子元素大于最大值,则更新最大值索引
    if (iLeft < heapSize && array[iMax] < array[iLeft]) {
      iMax = iLeft;
    }

    // 如果右子元素存在,且右子元素大于最大值,则更新最大值索引
    if (iRight < heapSize && array[iMax] < array[iRight]) {
      iMax = iRight;
    }

    // 如果最大元素被更新了,则交换位置,使父节点大于它的子节点,同时将索引值跟新为被替换的值,继续检查它的子树
    if (iMax !== index) {
      swap(array, index, iMax);
      index = iMax;
    } else {

      // 如果未被更新,说明该子树满足大顶堆的要求,退出循环
      break;
    }
  }
}

// 构建大顶堆
function buildMaxHeap(array) {
  let length = array.length,
    iParent = parseInt(length >> 1) - 1; // 获取最后一个非叶子点的元素

  for (let i = iParent; i >= 0; i--) {
    adjustMaxHeap(array, i, length); // 循环调整每一个子树,使其满足大顶堆的要求
  }
}

// 交换数组中两个元素的位置
function swap(array, i, j) {
  let temp = array[i];
  array[i] = array[j];
  array[j] = temp;
}

建立堆的时间复杂度为 O(n),排序循环的次数为 n-1,每次调整堆的时间复杂度为 O(logn),因此堆排序的时间复杂度在
不管什么情况下都是 O(nlogn)。

堆排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(nlogn) ,空间复杂度为 O(1) ,不是稳定排序。

二叉查找树

二叉查找树是一种特殊的二叉树,相对较小的值保存在左节点中,较大的值保存在右节点中,这一特性使得查找的效率很高,
对于数值型和非数值型数据,比如字母和字符串,都是如此。

实现树节点类:

// 节点类,树的节点
class Node {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }

  show() {
    console.log(this.value);
  }
}

实现二叉查找树类:

class BinarySearchTree {

  constructor() {
    this.root = null
  }

}

实现树的节点插入方法

节点插入的基本思想是将插入节点和当前节点做比较,如果比当前节点值小,并且没有左子树,那么将节点作为左叶子节点,
否则继续和左子树进行比较。如果比当前节点值大,并且没有右子树,则将节点作为右叶子节点,否则继续和右子树进行比较。
循环这个过程直到找到合适的插入位置。


  insert(value) {

    let newNode = new Node(value);

    // 判断根节点是否为空,如果不为空则递归插入到树中
    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  insertNode(node, newNode) {

    // 将插入节点的值与当前节点的进行比较,如果比当前节点小,则递归判断左子树,如果比当前节点大,则递归判断右子树。
    if (newNode.value < node.value) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }

  }

通过递归实现树的先序、中序、后序遍历

 // 先序遍历通过递归实现
 // 先序遍历则先打印当前节点,再递归打印左子节点和右子节点。
  preOrderTraverse() {
    this.preOrderTraverseNode(this.root);
  }

  preOrderTraverseNode(node) {
    if (node !== null) {
      node.show();
      this.preOrderTraverseNode(node.left);
      this.preOrderTraverseNode(node.right);
    }
  }

  // 中序遍历通过递归实现
  // 中序遍历则先递归打印左子节点,再打印当前节点,最后再递归打印右子节点。
  inOrderTraverse() {
    this.inOrderTraverseNode(this.root);
  }

  inOrderTraverseNode(node) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left);
      node.show();
      this.inOrderTraverseNode(node.right);
    }
  }

  // 后序遍历通过递归实现
  // 后序遍历则先递归打印左子节点和右子节点,最后再打印当前节点。
  postOrderTraverse() {
    this.postOrderTraverseNode(this.root);
  }

  postOrderTraverseNode(node) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left);
      this.postOrderTraverseNode(node.right);
      node.show();
    }
  }

通过循环实现树的先序、中序、后序遍历

  // 先序遍历通过循环实现
  // 通过栈来实现循环先序遍历,首先将根节点入栈。然后进入循环,每次循环开始,当前节点出栈,打印当前节点,然后将
  // 右子节点入栈,再将左子节点入栈,然后进入下一循环,直到栈为空结束循环。
  preOrderTraverseByStack() {
    let stack = [];

    // 现将根节点入栈,开始遍历
    stack.push(this.root);

    while (stack.length > 0) {

      // 从栈中获取当前节点
      let node = stack.pop();

      // 执行节点操作
      node.show();

      // 判断节点是否还有左右子节点,如果存在则加入栈中,注意,由于中序遍历先序遍历是先访问根
      // 再访问左和右子节点,因此左右子节点的入栈顺序应该是反过来的
      if (node.right) {
        stack.push(node.right);
      }

      if (node.left) {
        stack.push(node.left);
      }
    }
  }

  // 中序遍历通过循环实现
  // 中序遍历先将所有的左子节点入栈,如果左子节点为 null 时,打印栈顶元素,然后判断该元素是否有右子树,如果有
  // 则将右子树作为起点重复上面的过程,一直循环直到栈为空并且节点为空时。
  inOrderTraverseByStack() {
    let stack = [],
      node = this.root;

    // 中序遍历是先左再根最后右
    // 所以首先应该先把最左边节点遍历到底依次 push 进栈
    // 当左边没有节点时,就打印栈顶元素,然后寻找右节点
    while (stack.length > 0 || node) {
      if (node) {
        stack.push(node);
        node = node.left;
      } else {
        node = stack.pop();
        node.show();
        node = node.right;
      }
    }
  }

  // 后序遍历通过循环来实现
  // 使用两个栈来是实现,先将根节点放入栈1中,然后进入循环,每次循环将栈顶元素加入栈2,再依次将左节点和右节点依次
  // 加入栈1中,然后进入下一次循环,直到栈1的长度为0。最后再循环打印栈2的值。
  postOrderTraverseByStack() {
    let stack1 = [],
      stack2 = [],
      node = null;

    // 后序遍历是先左再右最后根
    // 所以对于一个栈来说,应该先 push 根节点
    // 然后 push 右节点,最后 push 左节点

    stack1.push(this.root);

    while (stack1.length > 0) {
      node = stack1.pop();

      stack2.push(node);  
      
      if (node.left) {
        stack1.push(node.left);
      }
      
      if (node.right) {
        stack1.push(node.right);
      }

    }

    while (stack2.length > 0) {
      node = stack2.pop();
      node.show();
    }
  }

实现寻找最大最小节点值

 // 寻找最小值,在最左边的叶子节点上
  findMinNode(root) {
    let node = root;

    while (node && node.left) {
      node = node.left;
    }

    return node;
  }

  // 寻找最大值,在最右边的叶子节点上

  findMaxNode(root) {
    let node = root;

    while (node && node.right) {
      node = node.right;
    }

    return node;
  }

实现寻找特定大小节点值

  // 寻找特定值
  find(value) {
    return this.findNode(this.root, value);
  }

  findNode(node, value) {

    if (node === null) {
      return node;
    }
    if (value < node.value) {
      return this.findNode(node.left, value);
    } else if (value > node.value) {
      return this.findNode(node.right, value);
    } else {
      return node;
    }
  }

实现移除节点值

移除节点的基本思想是,首先找到需要移除的节点的位置,然后判断该节点是否有叶节点。如果没有叶节点,则直接删除,如
果有一个叶子节点,则用这个叶子节点替换当前的位置。如果有两个叶子节点,则去右子树中找到最小的节点来替换当前节点。


  // 移除指定值节点
  remove(value) {
    this.removeNode(this.root, value);
  }
  removeNode(node, value) {

    if (node === null) {
      return node;
    }

    // 寻找指定节点
    if (value < node.value) {
      node.left = this.removeNode(node.left, value);
      return node;
    } else if (value > node.value) {
      node.right = this.removeNode(node.right, value);
      return node;
    } else { // 找到节点

      // 第一种情况——没有叶节点
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // 第二种情况——一个只有一个子节点的节点,将节点替换为节点的子节点
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
      }

      // 第三种情况——一个有两个子节点的节点,去右子树中找到最小的节点,用它的值来替换当前节点
      // 的值,保持树的特性,然后将替换的节点去掉
      let aux = this.findMinNode(node.right);
      node.value = aux.value;
      node.right = this.removeNode(node.right, aux);
      return node;
    }
  }

链表

反转单向链表

需要将一个单向链表反转。思路很简单,使用三个变量分别表示当前节点和当前节点的前后节点,虽然这题很简单,但是却是
一道面试常考题。

思路是从头节点往后遍历,先获取下一个节点,然后将当前节点的 next 设置为前一个节点,然后再继续循环。

var reverseList = function(head) {
    // 判断下变量边界问题
    if (!head || !head.next) return head;
    // 初始设置为空,因为第一个节点反转后就是尾部,尾部节点指向 null
    let pre = null;
    let current = head;
    let next;
    // 判断当前节点是否为空
    // 不为空就先获取当前节点的下一节点
    // 然后把当前节点的 next 设为上一个节点
    // 然后把 current 设为下一个节点,pre 设为当前节点
    while(current) {
        next = current.next;
        current.next = pre;
        pre = current;
        current = next;
    }
    return pre;
};

动态规划

爬楼梯问题

有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法?

递归方法分析

由分析可知,假设我们只差最后一步就能走上第10级阶梯,这个时候一共有两种情况,因为每一步只允许走1级或2级阶梯,
因此分别为从8级阶梯和从9九级阶梯走上去的情况。因此从0到10级阶梯的走法数量就等于从0到9级阶梯的走法数量加上
从0到8级阶梯的走法数量。依次类推,我们可以得到一个递归关系,递归结束的标志为从0到1级阶梯的走法数量和从0到
2级阶梯的走法数量。

代码实现

function getClimbingWays(n) {

  if (n < 1) {
    return 0;
  }

  if (n === 1) {
    return 1;
  }

  if (n === 2) {
    return 2;
  }

  return getClimbingWays(n - 1) + getClimbingWays(n - 2);
}

使用这种方法时整个的递归过程是一个二叉树的结构,因此该方法的时间复杂度可以近似的看为 O(2^n),空间复杂度
为递归的深度 O(logn)。

备忘录方法

分析递归的方法我们可以发现,其实有很多的计算过程其实是重复的,因此我们可以使用一个数组,将已经计算出的值给
保存下来,每次计算时,先判断计算结果是否已经存在,如果已经存在就直接使用。

代码实现

let map = new Map();

function getClimbingWays(n) {

  if (n < 1) {
    return 0;
  }

  if (n === 1) {
    return 1;
  }

  if (n === 2) {
    return 2;
  }

  if (map.has(n)) {
    return map.get(n);
  } else {
    let value = getClimbingWays(n - 1) + getClimbingWays(n - 2);
    map.set(n, value);
    return value;
  }
}

通过这种方式,我们将算法的时间复杂度降低为 O(n),但是增加空间复杂度为 O(n)

迭代法

通过观察,我们可以发现每一个值其实都等于它的前面两个值的和,因此我们可以使用自底向上的方式来实现。

代码实现

function getClimbingWays(n) {

  if (n < 1) {
    return 0;
  }

  if (n === 1) {
    return 1;
  }

  if (n === 2) {
    return 2;
  }

  let a = 1,
    b = 2,
    temp = 0;

  for (let i = 3; i <= n; i++) {
    temp = a + b;
    a = b;
    b = temp;
  }

  return temp;
}

通过这种方式我们可以将算法的时间复杂度降低为 O(n),并且将算法的空间复杂度降低为 O(1)。

详细资料可以参考:
《漫画:什么是动态规划?(整合版)》

经典笔试题

1. js 实现一个函数,完成超过范围的两个大整数相加功能
主要思路是通过将数字转换为字符串,然后每个字符串在按位相加。

function bigNumberAdd(number1, number2) {

  let result = "", // 保存最后结果
    carry = false; // 保留进位结果

  // 将字符串转换为数组
  number1 = number1.split("");
  number2 = number2.split("");

  // 当数组的长度都变为0,并且最终不再进位时,结束循环
  while (number1.length || number2.length || carry) {

    // 每次将最后的数字进行相加,使用~~的好处是,即使返回值为 undefined 也能转换为 0
    carry += ~~number1.pop() + ~~number2.pop();

    // 取加法结果的个位加入最终结果
    result = carry % 10 + result;

    // 判断是否需要进位,true 和 false 的值在加法中会被转换为 1 和 0
    carry = carry > 9;
  }

  // 返回最终结果
  return result;
}

详细资料可以参考:
《JavaScript实现超范围的数相加》
《js 实现大整数加法》

2. js 如何实现数组扁平化?
// 这一种方法通过递归来实现,当元素为数组时递归调用,兼容性好
function flattenArray(array) {

  if (!Array.isArray(array)) return;

  let result = [];

  result = array.reduce(function (pre, item) {
    // 判断元素是否为数组,如果为数组则递归调用,如果不是则加入结果数组中
    return pre.concat(Array.isArray(item) ? flattenArray(item) : item);
  }, []);

  return result;
}

// 这一种方法是利用了 toString 方法,它的一个缺点是改变了元素的类型,只适合于数组中元素都是整数的情况
function flattenArray(array) {
  return array.toString().split(",").map(function (item) {
    return +item;
  })
}

详细资料可以参考:
《JavaScript专题之数组扁平化》

3. js 如何实现数组去重?
function unique(array) {
  if (!Array.isArray(array) || array.length <= 1) return;

  var result = [];

  array.forEach(function (item) {
    if (result.indexOf(item) === -1) {
      result.push(item);
    }
  })

  return result;
}


function unique(array) {
  if (!Array.isArray(array) || array.length <= 1) return;

  return [...new Set(array)];
}

详细资料可以参考:
《JavaScript专题之数组去重》

4. 如何求数组的最大值和最小值?
var arr = [6, 4, 1, 8, 2, 11, 23];
console.log(Math.max.apply(null, arr))

详细资料可以参考:
《JavaScript专题之如何求数组的最大值和最小值》

5. 如何求两个数的最大公约数?
基本思想是采用辗转相除的方法,用大的数去除以小的那个数,然后再用小的数去除以的得到的余数,一直这样递归下去,
直到余数为0时,最后的被除数就是两个数的最大公约数。

function getMaxCommonDivisor(a, b) {
  if (b === 0) return a;

  return getMaxCommonDivisor(b, a % b);
}
6. 如何求两个数的最小公倍数?
基本思想是采用将两个数相乘,然后除以它们的最大公约数

function getMinCommonMultiple(a, b){
  return a * b / getMaxCommonDivisor(a, b);
}

详细资料可以参考:
《百度 web 前端面试题之求两个数的最大公约数和最小公倍数》

7. 实现 IndexOf 方法?
function indexFun(array, val) {
  if (!Array.isArray(array)) return;

  let length = array.length;

  for (let i = 0; i < length; i++) {
    if (array[i] === val) {
      return i;
    }
  }

  return -1;
}   
8. 判断一个字符串是否为回文字符串?
function isPalindrome(str) {
  let reg = /[\W_]/g, // 匹配所有非单词的字符以及下划线
    newStr = str.replace(reg, "").toLowerCase(), // 替换为空字符并将大写字母转换为小写
    reverseStr = newStr.split("").reverse().join(""); // 将字符串反转

  return reverseStr === newStr;
}
9. 实现一个累加函数的功能比如 sum(1,2,3)(2).valueOf()
function sum(...args) {

let result = 0;

result = args.reduce(function (pre, item) {
  return pre + item;
}, 0);

let add = function (...args) {

  result = args.reduce(function (pre, item) {
    return pre + item;
  }, result);

  return add;
};

add.valueOf = function () {
  console.log(result);
}

return add;
}
10. 使用 reduce 方法实现 forEach、map、filter

 // forEach
 function forEachUseReduce(array, handler) {
   array.reduce(function (pre, item, index) {
     handler(item, index);
   });
 }
 
 // map
 function mapUseReduce(array, handler) {
   let result = [];

   array.reduce(function (pre, item, index) {
     let mapItem = handler(item, index);
     result.push(mapItem);
   });

   return result;
 }
 
 // filter
 function filterUseReduce(array, handler) {
   let result = [];

   array.reduce(function (pre, item, index) {
     if (handler(item, index)) {
       result.push(item);
     }
   });

   return result;
 }
11. 设计一个简单的任务队列,要求分别在 1,3,4 秒后打印出 “1”, “2”, “3”
 class Queue {

   constructor() {
     this.queue = [];
     this.time = 0;
   }

   addTask(task, t) {
     this.time += t;
     this.queue.push([task, this.time]);
     return this;
   }

   start() {
     this.queue.forEach(item => {
       setTimeout(() => {
         item[0]();
       }, item[1]);
     })
   }
 }
12. 如何查找一篇英文文章中出现频率最高的单词?
 function findMostWord(article) {

 // 合法性判断
 if (!article) return;

 // 参数处理
 article = article.trim().toLowerCase();

 let wordList = article.match(/[a-z]+/g),
  visited = [],
  maxNum = 0,
  maxWord = "";

 article = " " + wordList.join("  ") + " ";

 // 遍历判断单词出现次数
 wordList.forEach(function (item) {
  if (visited.indexOf(item) < 0) {
    let word = new RegExp(" " + item + " ", "g"),
      num = article.match(word).length;

    if (num > maxNum) {
      maxNum = num;
      maxWord = item;
    }
  }
 });

 return maxWord + "  " + maxNum;

 }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值