# 合并 2 个有序数组

# 描述

给你 2 个不降序数组,合并之后得到的数组仍然保持不降序的性质。

# 思路分析

首先,我们需要需要一个新的数组存储合并之后的结果,因为 JS 的数组是自动扩容的,所以我们不需要考虑数组的容量问题。然后定义两个偏移变量,用于判断合并的进程。当这两个偏移量都在对应数组的范围内时,我们分别读取两个数组当前偏移位置的数据,把小的那个加入到结果中,同时对应的偏移量+1。当一个数组已经合并完成之后,另外一个数组可能还有内容,我只需要直接将其内容追加到结果中即可。

# 算法实现

/**
 * 合并2个有序数组
 * @param {number[]} arr1
 * @param {number[]} arr2
 */
function merge(arr1, arr2) {
  let offset1 = 0;
  let offset2 = 0;
  let offset = 0;
  let newArr = [];
  // 当两个数组都还没有处理完成的时候
  while (offset1 < arr1.length && offset2 < arr2.length) {
    let val1 = arr1[offset1];
    let val2 = arr2[offset2];
    if (val1 >= val2) {
      newArr[offset++] = arr2[offset2++];
    } else {
      newArr[offset++] = arr1[offset1++];
    }
  }
  /**
   * 这两个while不可能同时成立,只有可能成立一个,将数组长度较大的剩余部分拷贝给新数组
   */
  while (offset1 < arr1.length) {
    newArr[offset++] = arr1[offset1++];
  }
  while (offset2 < arr2.length) {
    newArr[offset++] = arr2[offset2++];
  }
  return newArr;
}

请记住这个范式,在将来的归并排序中,我们还要用到它。

# 合并 K 有序个数组

# 描述

给你 K 个不降序数组,合并之后得到的数组仍然保持不降序的性质。

# 思路分析

朴素法:每次从这个 K 数组中出一个最小的,如果当前数组为空则跳过,直到所有的数组都为空,即可完成合并。

建堆法:每次从这个 K 个数组的第 i 中出一个数,将其插入到一个最小堆中,当第 i 个数组为空时,继续处理下一个数组,直到所有的数组都处理完成,然后将堆中的数组依次出堆,则可得到最后的结果。

归并法

每次选两个数组进行归并,将本轮归并的结果添加到一个新数组中,然后对新数组再次归并,重复这个过程,直到得到的新数组为 1 个,则这最后的一个数组则是结果。

# 算法实现

# 建堆法的算法实现:

朴素法效率较低,此处就不展示代码了,有兴趣的读者可以自行实现。

# 建堆法的算法实现:

/**
 * 抽象堆
 */
class Heap {
  get count() {
    return this.size;
  }
  /**
   * 定义哨兵的最值,所有插入堆的元素都必须和这个值比较
   */
  SENTRY;

  /**
   * 定义一个存储数据的内存空间
   */
  data = [];

  /**
   * 当前堆的元素个数
   */
  size = 0;

  /**
   * 比较函数, 通过先前和当前元素的比较,决定是否将当前元素提置先前元素前
   */
  compare;

  /**
   * 自身用于比较的函数
   * @param preVal 被比较的值
   * @param curVal 当前值
   */
  selfCompare(preVal, curVal) {
    return this.compare(preVal, curVal);
  }

  validInitParams() {
    if (this.SENTRY === void 0) {
      throw `can not insert queue before the sentry been set `;
    }
    if (this.compare === void 0) {
      throw `can not insert queue before the compare callback been set `;
    }
  }

  constructor(...initElements) {
    // 初始化数组元素
    initElements.forEach((v, i) => {
      this.data[i + 1] = v;
      this.size++;
    });
    this.buildHeap();
  }

  /**
   * 设置哨兵元素
   * @param sentryEle
   */
  setSentry(sentryEle) {
    this.SENTRY = sentryEle;
    this.data[0] = this.SENTRY;
  }

  /**
   * 设置比较函数
   * @param compareFunc 比较函数
   */
  setCompare(compareFunc) {
    this.compare = compareFunc;
  }

  /**
   * 获取堆中最小的元素
   * @returns
   */
  getTop() {
    if (this.size == 0) {
      throw `can not get element from an empty heap`;
    }
    return this.data[1];
  }

  /**
   * 判断堆是否为空
   * @returns
   */
  isEmpty() {
    return this.size === 0;
  }

  /**
   * 向堆中插入一个合法值
   * @param val
   */
  insertQueue(val) {
    this.validInitParams();
    if (this.selfCompare(this.SENTRY, val)) {
      throw `can not insert val bigger or smaller than ${this.SENTRY}`;
    }
    // 堆的容量扩充1
    this.size++;
    // 让i指向当前新位置
    let i = this.size;
    // 因为有哨兵的关系,不需要添加约束条件 i > 0
    // this.#data[Math.floor(i / 2)] > val
    while (this.selfCompare(this.data[Math.floor(i / 2)], val)) {
      this.data[i] = this.data[Math.floor(i / 2)];
      i = Math.floor(i / 2);
    }
    this.data[i] = val;
  }

  /**
   * 获取堆中的最小元素
   * @returns {T}
   */
  deleteQueue() {
    if (this.isEmpty()) {
      throw "can not delete element from empty heap";
    }
    // 取出堆顶的元素
    let firstVal = this.data[1];
    let temp = this.data[this.size--];
    this.data[1] = temp;
    // JavaScript语言需要进行这一步,让数组的规模缩小,释放空间
    this.data.length--;
    this.percDown(1);
    return firstVal;
  }

  /**
   * 下滤:将堆中以堆data[p]为根的子堆调整为最小堆
   * @param p 根节点索引
   */
  percDown(p) {
    let parent, child;
    let temp = this.data[p]; /* 取出根结点存放的值 */
    for (parent = p; parent * 2 <= this.size; parent = child) {
      child = parent * 2;
      /* child指向左右子结点的较?者 */
      if (child != this.size && this.selfCompare(this.data[child], this.data[child + 1])) {
        child++;
      }
      /* 找到了合适位置 */
      if (this.selfCompare(this.data[child], temp)) {
        break;
      } else {
        /* 下滤X */
        this.data[parent] = this.data[child];
      }
    }
    this.data[parent] = temp;
  }

  /**
   * 构建堆
   */
  buildHeap() {
    /* 调整data中的元素,使满足最堆的有序性  */
    /* 这里所有size个元素已经存在data[]中 */
    /* 从最后一个结点的父节点开始,到根结点1 */
    for (let i = Math.floor(this.size / 2); i > 0; i--) {
      this.percDown(i);
    }
  }
}

/**
 * 堆元素为number的最小堆
 */
class SimpleMinHeap extends Heap {
  constructor(...initElements) {
    super(...initElements);
    this.setSentry(-Infinity);
    this.setCompare((preVal, curVal) => {
      return preVal >= curVal;
    });
  }

  deleteMin() {
    return this.deleteQueue();
  }

  getMin() {
    return this.getTop();
  }
}

/**
 * 合并K个有序数组
 * @param {number[][]} arrs
 */
function mergeKArray(arrs) {
  let minHeap = new SimpleMinHeap();
  let offset = 0;
  while(offset < arrs.length) {
    let currentArr = arrs[offset];
    // 处理一个数组,直至为空
    while(currentArr.length) {
      const ele = currentArr.shift();
      minHeap.insertQueue(ele);
    }
    offset++;
  }
  let newArr = [];
  let idx = 0;
  // 输出堆,得到最终结果
  while(!minHeap.isEmpty()) {
    const ele = minHeap.deleteMin();
    newArr[idx++] = ele;
  }
  return newArr;
}

上述代码看起来比较长,主要是我们实现了一个(实际开发中,这是一个可以封装进成熟的代码库的操作,并不需要我们自己实现),但是其实大家只需要看关键的函数mergeKArray

# 归并法的算法实现

归并法我们需要把上面合并 2 个有序数组的代码 copy 过来进行一下简单的修改,因为在归并的过程中,有可能剩下的数组只有可能存在一个了。修改点非常简单,只需要在第二个数组上加上默认值即可。

/**
 * 合并2个有序数组
 * @param {number[]} arr1
 * @param {number[]} arr2 可选参数,若不传递该参数,则相当于将原数组copy一份
 */
function merge(arr1, arr2 = []) {
  let offset1 = 0;
  let offset2 = 0;
  let offset = 0;
  let newArr = [];
  // 当两个数组都还没有处理完成的时候
  while (offset1 < arr1.length && offset2 < arr2.length) {
    let val1 = arr1[offset1];
    let val2 = arr2[offset2];
    if (val1 >= val2) {
      newArr[offset++] = arr2[offset2++];
    } else {
      newArr[offset++] = arr1[offset1++];
    }
  }
  /**
   * 这两个while不可能同时成立,只有可能成立一个,将数组长度较大的剩余部分拷贝给新数组
   */
  while (offset1 < arr1.length) {
    newArr[offset++] = arr1[offset1++];
  }
  while (offset2 < arr2.length) {
    newArr[offset++] = arr2[offset2++];
  }
  return newArr;
}

/**
 * 合并K个有序数组
 * @param {number[][]} arrs
 */
function mergeKArray(arrs) {
  let mergedArr = arrs;
  // 如果归并结果大于1,则需要继续进行归并
  while (mergedArr.length > 1) {
    // 本轮的归并结果
    const mergePassArr = [];
    for (let i = 0; i < mergedArr.length; i += 2) {
      // 得到二路归并的结果
      const newArr = merge(mergedArr[i], mergedArr[i + 1]);
      mergePassArr.push(newArr);
    }
    // 将本轮的归并结果给最终的合并结果,使之可以继续下一轮归并
    mergedArr = mergePassArr;
  }
  // 如果归并0个数组,则返回空,否则返回正常的归并结果
  return mergedArr.length ? mergedArr[0] : [];
}