数据结构与算法:查找、排序、动态规划、数学

1 查找表

查找表是同一数据类型构成的集合。只进行查找操作的称为静态查找表;在查找的同时进行插入和删除操作的称为动态查找表

查找算法衡量好坏的依据为:查找成功时,查找的关键字和查找表中比较过的数据元素的个数的平均值,称为平均查找长度(Average Search Length,用 ASL 表示)。计算公式为

其中 Pi 为第 i 个数据元素被查找的概率,所有元素被查找的概率的和为 1;Ci 表示在查找到第 i 个数据元素之前已进行过比较的次数。比如线性查找中,若表中有 n 个数据元素,从后往前查找,查找第一个元素时需要比较 n 次;查找最后一个元素时需要比较 1 次,所以有 Ci = n – i + 1 。

对于查找算法来说,查找成功和查找失败的概率是相同的。查找算法的平均查找长度应该为查找成功时的平均查找长度加上查找失败时的平均查找长度。计算公式为:

1.1 顺序查找

算法思想:在原始顺序表中添加目标关键字作为“监视哨”,然后从表中的第一个数据元素开始,逐个同记录的目标关键字做比较,如果匹配成功,则查找成功;反之,如果直到表中最后一个关键字查找完才成功匹配,则说明原始顺序表中查找失败。

实现

// 哨兵法
function sequentialSearch(nums: number[], target: number): number {
  nums.push(target);
  let i = 0;
  while (nums[i] !== target) {
    i = i + 1;
  }
  return i === nums.length - 1 ? -1 : i;
}

其平均查找长度为

1.2 二分查找(折半查找)

算法思想:假设静态查找表中的数据是有序的。指针 low 和 high 分别指向查找表的第一个关键字和最后一个关键字,指针 mid 指向处于 low 和 high 指针中间位置的关键字。在查找的过程中每次都同 mid 指向的关键字进行比较,由于整个表中的数据是有序的,因此在比较之后就可以知道要查找的关键字的在 [low,  mid] 还是 [mid, right]。

实现:

function binarySearch(nums: number[], target: number): number {
  let low = 0;
  let high = nums.length - 1;
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    const midNum = nums[mid];
    if (midNum === target) {
      return mid;
    } else if (midNum > target) {
      high = mid - 1;
    } else {
      low = mid + 1;
    }
  }
  return -1;
}

查找成功的平均查找长度等于判定树上每一层的内节点(能查找成功的)数乘以该层的节点数,然后求和,最后除以节点数。

查找失败的平均查找长度等于每一层的外节点(查找失败的区域)数乘以乘以[外节点所在层 - 1],然后求和,除以节点数

1.3 二叉查找(搜索)树和平衡二叉树(AVL)

1.3.1 二叉查找树(二叉搜索树、二叉排序树)

空树或满足:

  1. 非空左子树所有节点值小于根节点;
  2. 非空右子树所有节点值大于根节点;
  3. 左右子树也是二叉查找树;

    是一种动态查找表,在查找过程中插入或者删除表中元素。查找的时间复杂度取决于树的高度。

插入节点:查找失败时插入位置一定位于查找失败时访问的最后一个结点的左孩子(小于)或者右孩子(大于)。

对一个查找表进行查找以及插入操作时,可以从空树构建出一个含有表中所有关键字的二叉排序树,使得一个无序序列可以通过构建一棵二叉排序树,从而变成一个有序序列(中序遍历)。

删除节点(假设为p节点):

        1. p节点为叶节点,直接删除并改变父节点指针为null;

        2. p节点只有左子树或只有右子树,直接将子树替换p节点;

        3. p节点同时有左右子树:

                a) 左子树替换p节点,同时右子树成为p节点直接前驱的右子树

                b) 或者直接将节点p的值替换为其直接前驱的值,再对原直接前驱进行删除操作(以下实现采用)。

// 比较结果的枚举值
enum Compare {
  LESS_THAN = -1,
  BIGGER_THAN = 1,
  EQUALS = 0,
}

// 规定自定义Compare的类型
type ICompareFunction<T> = (a: T, b: T) => number;

/**
 * 默认的大小比较函数
 * @param {T} a
 * @param {T} b
 * @return {Compare} 返回 -1 0 1 代表 小于 等于 大于
 */
function defaultCompare<T>(a: T, b: T): Compare {
  if (a === b) {
    return Compare.EQUALS;
  }
  return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}

function cb<T>(node: BinarySearchTreeNode<T>) {
  console.log(node);
  return node;
}

class BinarySearchTreeNode<T> {
  constructor(
    public key: T,
    public value: any = key,
    public left: BinarySearchTreeNode<T> | null = null,
    public right: BinarySearchTreeNode<T> | null = null
  ) {}
  get isLeaf() {
    return this.left === null && this.right === null;
  }
  get hasChildren() {
    return !this.isLeaf;
  }
  get hasBothChildren() {
    return this.left !== null && this.right !== null;
  }
  get hasLeftChild() {
    return this.left !== null;
  }
}

class BinarySearchTree<T> {
  protected root: BinarySearchTreeNode<T> | null;
  constructor(key: T, value = key, protected compareFn: ICompareFunction<T> = defaultCompare) {
    this.root = new BinarySearchTreeNode(key, value);
  }

  *inOrderTraversal(node = this.root, callback: (node: BinarySearchTreeNode<T>) => BinarySearchTreeNode<T>) {
    if (node) {
      const { left, right } = node;
      if (left) yield* this.inOrderTraversal(left, callback);
      yield callback(node);
      if (right) yield* this.inOrderTraversal(right, callback);
    }
  }

  *postOrderTraversal(node = this.root, callback: (node: BinarySearchTreeNode<T>) => BinarySearchTreeNode<T>) {
    if (node) {
      const { left, right } = node;
      if (left) yield* this.postOrderTraversal(left, callback);
      if (right) yield* this.postOrderTraversal(right, callback);
      yield callback(node);
    }
  }
  *preOrderTraversal(node = this.root, callback: (node: BinarySearchTreeNode<T>) => BinarySearchTreeNode<T>) {
    if (node) {
      const { left, right } = node;
      yield callback(node);
      if (left) yield* this.preOrderTraversal(left, callback);
      if (right) yield* this.preOrderTraversal(right, callback);
    }
  }
  /**
   * 插入元素
   */
  insert(key: T, value = key) {
    if (this.root == null) {
      // 边界情况:插入到根节点
      this.root = new BinarySearchTreeNode(key, value);
    } else {
      // 递归找到插入位置
      this.insertNode(this.root, key, value);
    }
  }

  /**
   * 递归插入方法
   */
  protected insertNode(node: BinarySearchTreeNode<T>, key: T, value = key) {
    if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
      // key 比 node.key 小就向左查
      if (node.left == null) {
        // 基线条件:左面为空直接赋值
        node.left = new BinarySearchTreeNode(key, value);
      } else {
        // 否则就接着递归
        this.insertNode(node.left, key);
      }
    } else {
      // key 比 node.key 大就向右查
      if (node.right == null) {
        // 基线条件:右面为空直接赋值
        node.right = new BinarySearchTreeNode(key, value);
      } else {
        // 否则就接着递归
        this.insertNode(node.right, key, value);
      }
    }
  }
  /**
   * 是否存在某个节点
   * @param key
   * @returns
   */
  has(node: BinarySearchTreeNode<T> | null, key: T) {
    for (const current of this.postOrderTraversal(node, cb)) {
      if (current.key === key) return true;
    }
    return false;
  }
  /**
   * 搜索某个节点
   * @param key
   * @returns
   */
  find(node: BinarySearchTreeNode<T> | null, key: T) {
    for (const current of this.postOrderTraversal(node, cb)) {
      if (current.key === key) return current;
    }
    return undefined;
  }
  /**
   * 移除指定元素
   */
  remove(key: T) {
    this.root = this.removeNode(this.root, key);
  }
  /**
   * 移除某个节点
   * @param key
   * @returns
   */
  protected removeNode(node: BinarySearchTreeNode<T> | null, key: T): BinarySearchTreeNode<T> | null {
    let current = this.find(node, key);
    if (!current) return null;
    if (current.isLeaf) {
      // 删除叶子节点
      current = null;
      return current;
    }
    if (current.hasBothChild) {
      // 有两个节点
      const aux = this.minNode(current.right)!;
      current.key = aux.key;
      this.removeNode(current.right, aux.key);
    }
    if (current.hasLeftChild) {
      // 只有左节点
      current = current.left;
      return current;
    }
    // 只有右节点
    current = current.right;
    return current;
  }
  /**
   * 返回根节点
   */
  getRoot(): BinarySearchTreeNode<T> | null {
    return this.root;
  }
  /**
   * 返回树中的最小元素
   */
  min(): BinarySearchTreeNode<T> | null {
    // 调用迭代方法
    return this.minNode(this.root);
  }
  /**
   * 返回指定子树下的最小元素
   */
  protected minNode(node: BinarySearchTreeNode<T> | null): BinarySearchTreeNode<T> | null {
    let current = node;
    // 不断向左查
    while (current != null && current.left != null) {
      current = current.left;
    }
    return current;
  }
  /**
   * 返回树中的最大元素
   */
  max(): BinarySearchTreeNode<T> | null {
    // 调用迭代方法
    return this.maxNode(this.root);
  }
  /**
   * 返回指定子树下的最大元素
   */
  protected maxNode(node: BinarySearchTreeNode<T> | null): BinarySearchTreeNode<T> | null {
    let current = node;
    // 不断向右查
    while (current != null && current.right != null) {
      current = current.right;
    }
    return current;
  }
  /**
   * 搜索元素
   */
  search(key: T): boolean {
    // 调用递归方法
    return this.searchNode(this.root, key);
  }
  /**
   * 递归搜索
   */
  private searchNode(node: BinarySearchTreeNode<T> | null, key: T): boolean {
    // 查到尽头返回 false
    if (node == null) {
      return false;
    }
    if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
      // key 比 node.key 小,向左查
      return this.searchNode(node.left, key);
    }
    if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
      // key 比 node.key 大,向右查
      return this.searchNode(node.right, key);
    }
    return true;
  }
}

1.3.2 平衡二叉树 (AVL)

平衡二叉树 (AVL):

        1) 满足二叉查找树的所有特性;

        2) 每个节点的平衡因子(节点的左右子树高度差)绝对值不大于1

在动态查找过程中,可能会破坏其平衡性,需要做出调整(假设距离插入节点最近的不平衡节点为a):

        1) 在a的左子树的左子树上插入节点(LL),即右旋

        2) 在a的右子树的右子树上插入节点(RR),即左旋

        3) 在a的左子树的右子树上插入节点(LR),即先左旋再右旋

        4) 在a的右子树的左子树上插入节点(RL),即先右旋再左旋

// 平衡因子枚举
enum BalanceFactor {
  UNBALANCED_RIGHT = -2, // 右重
  SLIGHTLY_UNBALANCED_RIGHT = -1, // 轻微右重
  BALANCED = 0, // 完全平衡
  SLIGHTLY_UNBALANCED_LEFT = 1, // 轻微左重
  UNBALANCED_LEFT = 2, // 右重
}

class AVLTree<T> extends BinarySearchTree<T> {
  protected root: BinarySearchTreeNode<T> | null;
  constructor(key: T, value = key, protected compareFn: ICompareFunction<T> = defaultCompare) {
    super(key, value, compareFn);
    this.root = new BinarySearchTreeNode(key, value);
  }
  /**
   * 获取节点高度
   * @param node
   * @returns
   */
  private getNodeHeight(node: BinarySearchTreeNode<T> | null): number {
    if (node === null) return 0;
    const { left, right } = node;
    return 1 + Math.max(this.getNodeHeight(left), this.getNodeHeight(right));
  }
  /**
   * 获取节点的平衡因子
   * @param node
   * @returns
   */
  private getBalanceFactor(node: BinarySearchTreeNode<T>): BalanceFactor {
    if (node === null) {
      return BalanceFactor.BALANCED;
    }
    const { left, right } = node;
    // 左子树重 减去 右子树重
    const heightDiff = this.getNodeHeight(left) - this.getNodeHeight(right);
    // 再返回对应的枚举值
    switch (heightDiff) {
      case -2:
        return BalanceFactor.UNBALANCED_RIGHT;
      case -1:
        return BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT;
      case 1:
        return BalanceFactor.SLIGHTLY_UNBALANCED_LEFT;
      case 2:
        return BalanceFactor.UNBALANCED_LEFT;
      default:
        return BalanceFactor.BALANCED;
    }
  }

  /**
   * 左子树的左子树上插入节点,左左情况: 向右单旋转
   * 左侧子节点的高度大于右侧子节点的高度时,并且左侧子节点也是平衡或左侧较重的,为左左情况
   *
   *         a                           b
   *        / \                         /   \
   *       b   c -> rotationLL(a) ->   d     a
   *      / \                         |      / \
   *     d   e                   插入节点    e   c
   *    |
   * 插入节点
   *
   * @param node  旋转前的子树根节点
   * @returns  返回旋转后的子树根节点
   */
  private rotationLL(node: BinarySearchTreeNode<T>): BinarySearchTreeNode<T> {
    const pivot = node.left;
    node.left = node.right;
    pivot!.right = node;
    return pivot!;
  }
  /**
   * 右子树的右子树上插入节点,右右情况: 向左单旋转
   * 右侧子节点的高度大于左侧子节点的高度,并且右侧子节点也是平衡或右侧较重的,为右右情况
   *     a                              c
   *    / \                            /  \
   *   b   c   -> rotationRR(a) ->    a    e
   *      / \                        / \    |
   *     d   e                      b   d 插入节点
   *         |
   *       插入节点
   *
   * @param node  旋转前的子树根节点
   * @returns  返回旋转后的子树根节点
   */
  private rotationRR(node: BinarySearchTreeNode<T>): BinarySearchTreeNode<T> {
    const pivot = node.right;
    node.right = pivot!.left;
    pivot!.left = node;
    return pivot!;
  }

  /**
   * 左子树的右子树上插入节点, 左右情况: 先左转子节点后右转
   * 左侧子节点的高度大于右侧子节点的高度,并且左侧子节点右侧较重
   *
   *       a                           a                              e
   *      / \                         / \                         /       \
   *     b   c -> rotationRR(b) ->   e   c -> rotationLL(a) ->   b         a
   *    / \                         /                          /     \      \
   *   d   e                       b                          d  插入节点     c
   *       |                     /  \
   *     插入节点               d  插入节点
   *
   * @param node
   */
  private rotationLR(node: BinarySearchTreeNode<T>): BinarySearchTreeNode<T> {
    // 先把节点左子左转
    node.left = this.rotationRR(node.left!);
    // 再把节点本身右转
    return this.rotationLL(node);
  }
  /**
   * 右子树的左子树上插入节点, 右左情况: 先右转子节点后左转
   * 右侧子节点的高度大于左侧子节点的高度,并且右侧子节点左侧较重
   *
   *       a                           a                              d
   *      / \                         / \                           /     \
   *     b   c -> rotationLL(c) ->   b   d -> rotationRR(a) ->    a        c
   *        / \                         |  \                    /   \       \
   *       d   e                   插入节点 c                   b  插入节点   e
   *       |                                \
   *     插入节点                            e
   *
   * @param node
   */
  private rotationRL(node: BinarySearchTreeNode<T>): BinarySearchTreeNode<T> {
    // 先把节点左子左转
    node.left = this.rotationRR(node.left!);
    // 再把节点本身右转
    return this.rotationLL(node);
  }

  /**
   * 对子树进行平衡
   */
  keepBalance(node: BinarySearchTreeNode<T> | null): BinarySearchTreeNode<T> | null {
    if (node === null) return node;
    // 校验树是否平衡
    const balanceState = this.getBalanceFactor(node);
    const { left, right } = node;

    if (left && balanceState === BalanceFactor.UNBALANCED_LEFT) {
      // 左左情况
      if (
        this.getBalanceFactor(left) === BalanceFactor.BALANCED ||
        this.getBalanceFactor(left) === BalanceFactor.SLIGHTLY_UNBALANCED_LEFT
      ) {
        return this.rotationLL(node);
      } else if (this.getBalanceFactor(left) === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT) {
        // 左右情况
        return this.rotationLR(node);
      }
    } else if (right && balanceState === BalanceFactor.UNBALANCED_RIGHT) {
      // 右右情况
      if (
        this.getBalanceFactor(right) === BalanceFactor.BALANCED ||
        this.getBalanceFactor(right) === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT
      ) {
        return this.rotationRR(node);
      } else if (this.getBalanceFactor(right) === BalanceFactor.SLIGHTLY_UNBALANCED_LEFT) {
        // 右左情况
        return this.rotationRL(node);
      }
    }
    return node;
  }
  /**
   * @description: 插入节点的递归方法,递归插入完后,需要校验树是否仍然平衡,若不平衡则需要旋转
   * @param {Node} node 要插入到的节点
   * @param {T} key 要插入的键
   * @return {Node} 为了配合 insert 方法,一定要返回节点
   */
  protected insertNode(node: BinarySearchTreeNode<T> | null, key: T, value = key): BinarySearchTreeNode<T> | null {
    // 与二叉搜索树的插入方式一致
    if (node == null) {
      return new BinarySearchTreeNode<T>(key, value);
    }
    if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
      node.left = this.insertNode(node.left, key);
    } else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
      node.right = this.insertNode(node.right, key);
    } else {
      return node; // 重复的 key
    }
    // 校验树是否平衡
    return this.keepBalance(node);
  }
  /**
   * @description:  删除节点的递归方法,递归完成后也需要再平衡
   * @param {Node} node 要从中删除的节点
   * @param {T} key 要删除的键
   * @return {Node} 同样为了配合remove方法,一定要返回节点
   */
  protected removeNode(node: BinarySearchTreeNode<T> | null, key: T): BinarySearchTreeNode<T> | null {
    // 与二叉搜索树的删除方式一致
    if (!node) return null;
    if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
      node.left = this.removeNode(node.left, key);
    } else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
      node.right = this.removeNode(node.right, key);
    } else {
      if (node.isLeaf) {
        // 删除叶子节点
        node = null;
      } else if (node.hasBothChildren) {
        const aux = this.minNode(node.right)!;
        node.key = aux.key;
        node.right = this.removeNode(node.right, aux.key);
      } else if (node.hasLeftChild) {
        node = node.left;
      } else {
        node = node.right;
      }
    }
    // 校验树是否平衡
    return this.keepBalance(node);
  }
}

1.4 B-树和B+树

1.4.1 B-树(B树)

是多叉查找树,一颗m阶B树满足(m = 3指的是度最大为3):

        1. 根节点至少有子节点;

        2. 每个中间节点(非叶、根节点)都包含 k - 1 个元素和 k 个孩子(m/2 <= k <= m);

        3. 每一个叶子节点都包含 k - 1 个元素(m/2 <= k <= m);

        4. 所有的叶子节点都位于同一侧;

        5. 每个节点中的元素从小到大排列,节点当中的 k - 1 个元素划分k个孩子所属的值域范围。

        进行数值的比较是在内存(内存运算速度比磁盘IO快)中进行的,B树的比较次数虽然比二叉查找树多,然而由于B树每个节点存储的数据更多,磁盘加载次数更少,效率更高。磁盘加载次数可能与树的高度相同(因为不同的节点可能存储在不同的磁盘页中),所以选择这种矮胖的树形结构。注意:受到内存大小和磁盘页数限制,并不是越矮胖越好。

B树的基本操作:

        1. 插入:首先自顶向下查找元素的插入位置。如果插入后节点元素个数等于m。对当前节点的中心节点向上升级,直到当前节点元素个数小于m。

        2. 删除:首先自顶向下查找删除位置,删除后如果不满足B树的特性,找出父节点和其子节点的中位数进行向上升级

1.4.2  B+树

是多叉查找树,一颗m阶B+树满足(m = 3指的是度最大为3):

        1. 中间节点的元素个数和其子树个数相等,均为k(m/2 <= k <= m),且中间节点的索引元素值是子节点中元素的最大(或最小)值;

        2. 所有叶子节点包括了全部索引元素,和指向卫星数据的指针,且所有叶子节点形成有序链表;

B+树基本操作:

        1. 插入:空树直接插入;找到插入位置插入后,节点元素个数等于m就进行分裂(中位数升级),直到当前节点元素个数小于m。(插入7):

        2. 删除:首先自顶向下查找删除位置,删除后如果个数小于Math.ceil(m/2) – 1,进行向兄弟节点借或与兄弟节点合并,自底向上判断节点个数是否小于Math.ceil(m/2) – 1。(删除7):

 

B+树非叶子节点不存在卫星数据(索引元素所指向的数据记录,在B-树中,无论是中间节点还是叶子结点都带有卫星数据,而在B+树当中,只有叶子节点带有卫星数据,其余中间节点仅仅是索引,没有任何数据关联),因此可以存储更多的中间节点,意味着在数据量相同情况下,B+树比B树存储更矮胖,磁盘IO次数更少。

补充:在数据库的聚簇索引(Clustered Index)中,叶子节点直接包含卫星数据。在非聚簇索引(NonClustered Index)中,叶子节点带有指向卫星数据的指针。

B+树的优点:

  1. 所有查询都需要到叶子节点,查询性能稳定。
  2. 叶子节点形成有序链表,相较B树便于范围查询。

文件系统和数据库的索引都是存在硬盘上的,并且如果数据量大的话,不一定能一次性加载到内存中,所以要用到B树或B+树的多路存储分页加载。

MySQL采用B+树索引的原因:进行多条数据的查询或范围查询时,对于B树需要做中序遍历跨层访问磁盘页,而B+树全部数据都在叶子节点且叶子节点之间是链表结构,找到首尾即可取出所有数据。Hash索引对等值查询具有优势,效率O(1),前提是哈希冲突较少。Hash无序性不适用于范围查询,查询出来的数据需要再次排序(模糊匹配like本质也是范围查询)。多列联和索引的最左匹配规则只能是B+树,首先是先对第一列排序,对于第一列相同的再按照第二列排序,依次类推。

MySQL中的 B+Tree 索引结构图如下(叶子节点循环双向链表):

2 排序

2.1 冒泡排序(bubble sort)

算法思想:基于交换,以从前往后冒泡为例,第一趟冒泡从第一个元素开始,到倒数第二个元素,比较相邻的元素,如果前一个元素大于后一个即交换。下一趟从第一个元素开始,只到倒数第三个元素(因为上一趟倒数第一个元素确定其最终位置)。依此类推。由于相同的元素在冒泡过程中不会交换,属于稳定排序,时间复杂度O(n2),空间复杂度O(1)。

优化:有序的标志即一趟冒泡没有发生元素的交换,因此可以提前中止。同时,从算法思想可得,有序区的长度至少等于冒泡的趟数,甚至大于。在一趟冒泡过程中,最后一次交换的下标即是有序区第一个元素,下一趟的冒泡只需要遍历至该下标的上一个元素。

实现:

// 冒泡排序
function bubbleSort(nums: number[]): number[] {
  const len = nums.length;
  let firstOrderedIndex = len - 1; // 有序区的左边界
  let lastExchangeIndex = 0; // 每一趟最后一次交换的位置;
  for (let i = 0; i < len; i++) {
    let isExchange = false;
    for (let j = 0; j < firstOrderedIndex; j++) {
      if (nums[j] > nums[j + 1]) {
        [nums[j], nums[j + 1]] = [nums[j + 1], nums[j]];
        lastExchangeIndex = j; // 发生交换,更新有序去边界
        isExchange = true;
      }
    }
    firstOrderedIndex = lastExchangeIndex;
    if (!isExchange) break; // 若没发生交换,说明一件有序
  }
  return nums;
}

2.2 简单插入排序(insert sort)

算法思想:对于具有n个元素的待排序序列,第k趟假设前k(k >=1)个已经有序,后n - k个元素是无序,选取第k+1个元素,在前k个元素中寻找它的插入位置(即第一个比它大的元素的位置)进行插入。由于插入位置在有序区中比它大的第一个元素处,属于稳定排序,时间复杂度O(n2),空间复杂度O(1)。

优化:查找插入位置时,使用二分查找。

实现:

// 简单插入排序
function insertSort(nums: number[]): number[] {
  const len = nums.length;
  for (let i = 1; i < len; i++) {
    const element = nums[i];
    // 查找插入位置
    let left = 0;
    let right = i - 1;
    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      if (element < nums[mid]) {
        // 插入点在左半区
        right = mid - 1;
      } else {
        // 值相同时, 切换到右半区,保证稳定性
        left = mid + 1;
      }
    }
    // 插入位置 left 之后的元素全部后移一位
    for (let j = i; j > left; j--) {
      nums[j] = nums[j - 1];
    }
    nums[left] = element;
  }
  return nums;
}

2.3 选择排序(select sort)

算法思想:对于具有n个元素的待排序序列,第k趟假设前k(k >=1)个已经有序,后n - k个元素是无序,从这n-k元素中选择最小的元素和第k个元素交换。因此这是在时间复杂度上最稳定的排序算法,无论如何都是O(n2),空间复杂度O(1)。由于最小元素交换时可能导致相等元素相对位置改变([31, 5, 32, 2, 9] => [2, 5, 32, 31, 9]),属于不稳定排序

实现:

// 选择排序
function selectSort(nums: number[]): number[] {
  const len = nums.length;
  for (let i = 0; i < len; i++) {
    // 从 n - i 个元素中找出最小值
    let minIndex = i; // 初始化最小元素下标为 n - i 个元素 中的第一个
    for (let j = i + 1; j < len; j++) {
      if (nums[j] < nums[minIndex]) {
        // 当前元素更小,更新下标
        minIndex = j;
      }
    }
    // 交换
    [nums[i], nums[minIndex]] = [nums[minIndex], nums[i]];
  }
  return nums;
}

2.4 快速排序(quick sort)

算法思想:选取一个基准元素pivot,基准元素将待排序数组分为小于和大于的左右独立的两部分。然后再递归的分别对左右两个独立部分进行相同的操作。比如选择基准元素为左侧第一个元素,通过使用一个指针指向基准元素的下一个元素,同时遍历基准元素右侧元素,若元素小于基准元素,则交换当前遍历元素和指针指向元素,且移动该指针,最终指针所在位置就是大于等于基准元素的第一个元素位置。遍历结束,交换指针前一位元素和基准元素,基准元素到达最终位置。由于在左边等于基准元素的元素可能会被放到右边,属于不稳定排序,时间复杂度O(nlogn),空间复杂度O(logn)。

实现:

// 快速排序
function quickSort1(nums: number[]) {
  function partition(nums: number[], left: number, right: number) {
    let index = left + 1; // 最左边元素为基准元素,index 是为了找到大于基准元素的第一个元素位置
    for (let i = index; i <= right; i++) {
      if (nums[i] < nums[left]) {
        // 区域内的值小于基准值
        i !== index && ([nums[i], nums[index]] = [nums[index], nums[i]]);
        index = index + 1; // 每发现一个比基准元素小的,index 右移一位
      }
    }
    [nums[left], nums[index - 1]] = [nums[index - 1], nums[left]];
    return index - 1;
  }
  function quickSortCore(nums: number[], left: number, right: number) {
    if (left < right) {
      const partitionIndex = partition(nums, left, right);
      quickSortCore(nums, left, partitionIndex - 1);
      quickSortCore(nums, partitionIndex + 1, right);
    }
  }
  quickSortCore(nums, 0, nums.length - 1);
  return nums;
}

function quickSort2(nums: number[]): number[] {
  let len = nums.length;
  if (len <= 1) return nums; // 递归退出条件
  const pivot = nums.splice(Math.floor(len / 2), 1)[0]; // 中间为基准元素
  const left: number[] = [];
  const right: number[] = [];
  for (let item of nums) {
    // 分成左右两部分
    if (item < pivot) {
      left.push(item);
    } else {
      right.push(item);
    }
  }
  // 递归进行左右两部分排序
  return quickSort2(left).concat(pivot, quickSort2(right));
}

function quickSort3(nums: number[]): number[] {
  function partition(nums: number[], left: number, right: number) {
    const pivot = nums[left];
    while (left < right) {
      while (left < right && nums[right] >= pivot) {
        right = right - 1;
      }
      nums[left] = nums[right];
      while (left < right && nums[left] <= pivot) {
        left = left + 1;
      }
      nums[right] = nums[left];
    }
    nums[left] = pivot;
    return left;
  }
  function quickSortCore(nums: number[], left: number, right: number) {
    if (left < right) {
      const partitionIndex = partition(nums, left, right);
      quickSortCore(nums, left, partitionIndex - 1);
      quickSortCore(nums, partitionIndex + 1, right);
    }
  }
  quickSortCore(nums, 0, nums.length - 1);
  return nums;
}

2.5 归并排序(merge sort)

算法思想:对于具有n个元素的待排序序列,分成两个长度为n/2的子序列,分别对两个子序列进行归并排序,将排序好的子序列合并。递归划分至子序列长度为1,两路合并。由于相等的元素在合并的过程中位置不变,属于稳定排序。时间复杂度O(nlogn),空间复杂度O(n)

实现:

// 归并排序
function mergeSort(nums: number[]): number[] {
  const len = nums.length;
  // 只有一个元素,说明有序
  if (len < 2) return nums;
  // 划分成两个等长的子区域
  const mid = Math.floor(len / 2);
  // 合并两个有序数组
  function merge(left: number[], right: number[]): number[] {
    // 存放合并后的结果
    const merged: number[] = [];
    // 两个子序列的遍历指针
    let i = 0;
    let j = 0;
    const leftSize = left.length;
    const rightSize = right.length;
    while (i < leftSize && j < rightSize) {
      const leftElement = left[i];
      const rightElement = right[j];
      if (leftElement <= rightElement) {
        merged.push(leftElement);
        i = i + 1;
      } else {
        merged.push(rightElement);
        j = j + 1;
      }
    }
    while (i < leftSize) {
      merged.push(left[i]);
      i = i + 1;
    }
    while (j < rightSize) {
      merged.push(right[j]);
      j = j + 1;
    }
    return merged;
  }
  return merge(mergeSort(nums.slice(0, mid)), mergeSort(nums.slice(mid)));
}

2.6 计数排序(counting sort)

算法思想:找到要排序数组的最大值和最小值。以最大值 - 最小值 + 1为长度创建一个计数数组。遍历要排序的数组,以当前遍历的元素 - 最小值为索引,在计数数组中自增出现次数。遍历计数数组,判断当前遍历到的元素是否大于0,如果大于0就取当前遍历到的索引 + 最小值,替换待排序数组中的元素。计数排序适合用来排序范围不大的数字时间复杂度为O(n + m),空间复杂度为O(m)。

优化:上述方法只适合用来排序范围不大的数字且无法保证重复时的稳定性,需要进行优化—需要对计数数组中所有的计数进行累加(从计数数组的第二个元素开始,每一项和前一项相加,作为计数数组当前项的值);最后,通过反向遍历排序数组,填充目标排序数组:将每个元素放在目标排序数组的当前元素减去最小值的索引处的统计值对应的目标排序数组的索引处,每放一个元素就将统计数组中当前元素减去最小值的索引处的统计值减去1。

实现:

// 计数排序
function countingSort1(nums: number[]): number[] {
  const len = nums.length;
  const max = Math.max(...nums);
  const min = Math.min(...nums);
  const counterLen = max - min + 1;
  const counter = new Array(counterLen).fill(0);
  // 统计出现次数
  for (let i = 0; i < len; i++) {
    const index = nums[i] - min;
    counter[index] = counter[index] + 1;
  }
  let sortedIndex = 0;

  for (let i = 0; i < counterLen; i++) {
    while (counter[i] > 0) {
      nums[sortedIndex] = i + min;
      sortedIndex = sortedIndex + 1;
      counter[i] = counter[i] - 1;
    }
  }
  return nums;
}

// 计数排序
function countingSort2(nums: number[]): number[] {
  const len = nums.length;
  const max = Math.max(...nums);
  const min = Math.min(...nums);
  const counterLen = max - min + 1;
  const counter = new Array(counterLen).fill(0);
  // 统计出现次数
  for (let i = 0; i < len; i++) {
    const index = nums[i] - min;
    counter[index] = counter[index] + 1;
  }
  // 累加统计值
  for (let i = 1; i < len; i++) {
    counter[i] = counter[i] + counter[i - 1];
  }
  const sortedNums = new Array(len);
  for (let i = len - 1; i >= 0; i--) {
    // 可以简化为 sortedNums[--counter[nums[i] - min]] = nums[i];
    const counterIndex = nums[i] - min;
    counter[counterIndex] = counter[counterIndex] - 1;
    const index = counter[counterIndex];
    sortedNums[index] = nums[i];
  }
  return nums;
}

2.7 桶排序(bucket sort)

算法思想:桶排序在所有元素平分到各个桶中时的表现最好。如果元素非常稀疏,则使用更多的桶会更好。如果元素非常密集,则使用较少的桶会更好。创建桶,数量等于原始数组的元素数量,这样,每个桶的长度为 (最大值- 最小值) / (桶数量 - 1),然后通过(元素值 - 最小值 )/ 桶长度,将原始数组中的每个桶分布到不同的桶中,对每个桶中的元素执行某个排序算法使桶内有序,最后将所有桶合并成排序好后的结果数组。

实现:

// 桶排序
function bucketSort(nums: number[]): number[] {
  const max = Math.max(...nums);
  const min = Math.min(...nums);
  const bucketCount = nums.length;
  // 桶的间隔 = (最大值 - 最小值)/ 元素个数
  const bucketSize = (max - min) / (bucketCount - 1);
  const buckets: number[][] = new Array(bucketCount);
  for (let i = 0; i < bucketCount; i++) {
    buckets[i] = [];
  }
  // 将每个元素放到桶里
  for (let i = 0; i < bucketCount; i++) {
    // 计算需要将元素放到哪个桶中,公式为: 当前遍历到的元素值与数组的最小值的差值 / 桶间隔
    buckets[Math.floor((nums[i] - min) / bucketSize)].push(nums[i]);
  }
  // 对每个桶内部进行排序(使用内部算法)
  for (let i = 0; i < bucketCount; i++) {
    buckets[i].sort((a, b) => a - b);
  }
  const sortedNums: number[] = [];
  for (let i = 0; i < bucketCount; i++) {
    sortedNums.push(...buckets[i]);
  }
  return sortedNums;
}

2.8 堆排序(heap sort)

算法思想:利用堆的性质,对于具有n个元素的待排序序列,第k (k >= 1)趟,将n - k + 1个元素维持为大根堆,将堆顶元素和第n - k + 1个元素交换。如此,后k个元素为有序区。由于交换导致相等元素的相对位置发生改变,属于不稳定排序, 时间复杂度O(nlogn),空间复杂度O(1)

实现:

// 堆排序
function heapSort(nums: number[]): number[] {
  const len = nums.length;
  // 对非叶子节点下沉进行堆调整
  function heapfiy(nums: number[], index: number, heapSize: number) {
    // 交换节点位置,大顶堆:子节点中的较大者
    let exchange = index * 2 + 1;
    while (exchange < heapSize) {
      let right = index * 2 + 2;
      if (right < heapSize && nums[right] > nums[exchange]) {
        exchange = right;
      }
      if (nums[exchange] <= nums[index]) {
        // 子节点中较大者小于当前节点
        break;
      }
      [nums[index], nums[exchange]] = [nums[exchange], nums[index]];
      index = exchange;
      exchange = index * 2 + 1;
    }
  }
  // 对待排序数组构建具有数组长度的初始大根堆
  for (let i = Math.floor(len / 2) - 1; i > 0; i--) {
    heapfiy(nums, i, len);
  }
  // 第i趟将堆顶元素与倒数第 i 个元素交换,再调整堆顶元素
  for (let i = len - 1; i > 0; i--) {
    [nums[0], nums[i]] = [nums[i], nums[0]];
    heapfiy(nums, 0, i);
  }
  return nums;
}

2.9 基数排序(radix sort)

算法思想:根据数字的有效位或基数将整数分布到桶中。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。对于十进制数,使用10个桶用来分布元素并且首先基于个位数字进行稳定的计数排序,然后基于十位数字,然后基于百位数字。

实现:

// 基数排序
function radixSort(nums: number[], radix = 10): number[] {
  const len = nums.length;
  const max = Math.max(...nums);
  const min = Math.min(...nums);
  let significantDigit = 1;
  while ((max - min) / significantDigit >= 1) {
    // 对当前位进行计数排序
    const counter: number[] = [];
    for (let i = 0; i < radix; i++) {
      counter[i] = 0;
    }
    // 对当前位进行计数
    for (let i = 0; i < len; i++) {
      const index = Math.floor(((nums[i] - min) / significantDigit) % radix);
      counter[index] = counter[index] + 1;
    }
    // 计算累积值
    for (let i = 1; i < radix; i++) {
      counter[i] = counter[i] + counter[i - 1];
    }
    const aux = new Array(len);
    for (let i = 0; i < len; i++) {
      // 可以简化为 aux[--counter[Math.floor(((nums[i] - min) / significantDigit) % radix)]] = nums[i];
      const counterIndex = Math.floor(((nums[i] - min) / significantDigit) % radix);
      counter[counterIndex] = counter[counterIndex] - 1;
      const index = counter[counterIndex];
      aux[index] = nums[i];
    }
    nums = aux;
    significantDigit *= radix;
  }
  return nums;
}

3 动态规划与数学

动态规划(Dynamic programming,简称DP)是通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题、最优子结构性质和无后效性质的问题:

  1. 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构性质(即满足最优化原理)。
  2. 无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
  3. 子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率,降低了时间复杂度。

状态转移方程:相同问题在不同规模下的关系。寻找状态转移方程的一般性步骤:

  1. 找到相同问题(即重叠子问题),相同问题能适配不同的规模。
  2. 找到重叠子问题之间的关系。
  3. 找到重叠子问题的特殊解。

3.1 剑指offer动态规划算法题( typescript 版)

斐波那契数列(青蛙普通跳台阶)
青蛙变态跳台阶
丑数

0-1背包问题

不同路径

打家劫舍

地下城游戏

矩形覆盖

最小路径和

3.2 剑指 offer 数学算法题( typescript 版)

二进制中1的个数
数值的整数次方
求1+2+3+...+n
和为S的连续正数序列
圆圈中最后剩下的数(小孩报数问题?)
出现1的次数
剪绳子
不用加减乘除做加法
实现大数相加
实现大数相乘
十进制转k进制
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定谔的猫96

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值