js 常见算法(二)

141 阅读8分钟

计数排序、基数排序、归并排序、桶排序、堆排序思路,代码实现,及各算法之间的比较……


一 计数排序

计数排序的基本思想是,计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

计数排序,是一种牺牲空间换取时间的排序算法,一定范围内整数排序时,快于任何比较排序算法。

步骤:

  • 找出乱序数组中最大的元素,建立一个最大元素 + 1长度的新数组;新数组的下标对应乱序数组的值。
  • 统计乱序数组中每个值出现的次数,依次放入新数组中;
  • 遍历新数组,依次去除新数组中的元素。

计数排序.gif

计数排序的代码实现

// 计数排序方法
function countingSort(arr, max) {
  // 创建一个新数组,用来统计数组中每个元素出现的次数
  let middle = new Array(max + 1);
  // 遍历数组,把每个元素出现的次数记录在新数组的相应位置
  for(let i = 0,len = arr.length;i < len;i ++) {
    // 如果元素未出现过,则置为1
    if (!middle[arr[i]]) {
      middle[arr[i]] = 1;
    } else {
      // 已经出现过的元素次数 + 1
      middle[arr[i]] ++;
    }
  }
  // 从第几个元素开始排序
  let startIndex = 0;
  // 遍历新数组,依次取出元素(新数组的下标对应乱序数组的值)
  for(let j = 0;j <= max;j ++) {
    while (middle[j] > 0) {
      arr[startIndex] = j;
      // 对应元素的次数 - 1
      middle[j] --;
      // 排序索引增加
      startIndex ++;
    }
  }
  return arr;
}
// 验证
countingSort([9, 9, 3, 3, 7, 7, 4, 8, 1, 8, 1, 7, 8, 4, 7, 2, 0, 4], 9);

计数排序改进:

/**
* 如果元素数组是这样的:[90, 99, 99, 90, 91, 91, 96, 96, 98, 98, 93, 93, 92, 92]
* 那么上述方法会创建一个长度为 100 的数组,消耗较多的内存
*/
function countingSort(arr) {
  // 定义最大值和最小值为数组第一个元素
  let max = arr[0], min = arr[0];
  // 查找数组中的最大值和最小值
  for (let i = 0,len = arr.length; i < len; i ++) {
    if (arr[i] > max) {
      max = arr[i]
    }
    if (arr[i] < min) {
      min = arr[i]
    }
  }
  // 根据最大值和最小值创建新的数组 新数组长度为 max - min + 1
  let middle = new Array(max - min + 1);
  let startIndex = 0;
  for(let i = 0,len = arr.length; i < len; i ++) {
    // 新添加,我们要处理的索引为当期值减去最小值之后的索引。之后的步骤类似
    let index = arr[i] - min;
    if (!middle[index]) {
      middle[index] = 1
    } else {
      middle[index]++
    }
  }
  for(let j = 0; j <= max - min; j ++) {
    while (middle[j] > 0) {
      arr[startIndex] = j + min;
      middle[j] --;
      startIndex++
    }
  }
  return arr;
}
countingSort([90, 99, 99, 90, 91, 91, 96, 96, 98, 98, 93, 93, 92, 92]);

二 基数排序

基数排序,是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

基数排序属于稳定的排序,在某些时候,基数排序效率高于其他稳定性排序方法。

基数排序的两种实现方式:

  • LSD: 从元素的最低位开始排序;
  • MSD: 从元素的最高位开始排序。

LSD 的实现及图解

  1. 第一次循环,根据个位排序。

LSD1.jpg

  1. 第二次循环,根据十位排序。

LSD2.jpg

  1. 第三次循环,根据百位排序

LSD3.jpg

function radixSort (arr) {
  // 在数组中找最大值
  let max = arr[0];
  let len = arr.length;
  for(let i = 0; i < len; i ++) {
    if (max < arr[i]) {
      max = arr[i]
    }
  }

  // 获取最大值的位数,如:10 两位数  100 三位数   1000四位数
  let digits = `${max}`.length;
  // 根据位数来决定循环次数
  for (let i = 0; i < digits; i ++) {
    sort(arr, i)
  }
  console.log(arr)
}
function sort(arr, round) {
  // 创建一个元素容器 (桶)
  let result = [];
  // 根据位数创建对应的桶
  for (let i = 0; i < 10; i++) {
    result[i] = [];
  }
  // 将数组中的数放进对应的桶子中
  for (let i = 0; i < arr.length; i++) {
    let middle = arr[i] / (Math.pow(10, round));
    // 得到目标桶的坐标
    let index = Math.floor(middle % 10);
    // 把元素放入到对应的桶中
    result[index].push(arr[i]);
  }

  let index = 0;
  // 在桶中将元素取出,放入原数组
  for (let i = 0; i < result.length; i++) {
    for (let j = 0; j < result[i].length; j++) {
      // 从每个桶中取元素
      arr[index++] = result[i][j];
    }
  }
}

radixSort([10, 5, 5, 50, 0, 155, 422, 5, 1, 4, 254]);

MSD 的实现及图解

MSD.jpg

function radixSort (arr) {
  let max = arr[0];
  let len = arr.length;
  for(let i = 0;i < len;i ++) {
    if (max < arr[i]) {
      max = arr[i]
    }
  }

  let digits = `${max}`.length;
  sort(arr, digits - 1);
  // 得到有序数组
  console.log(arr)//[ 0, 1, 4, 5, 5, 5, 10, 50, 155, 254, 422 ]
}
function sort(arr, round) {
  if (round < 0) {
    return;
  }
  let result = [];
  // 根据位数创建对应的桶
  for (let i = 0; i < 10; i++) {
    result[i] = [];
  }
  // 将数组中的数放进对应的桶子中
  for (let i = 0; i < arr.length; i++) {
    let middle = arr[i] / (Math.pow(10, round));
    // 得到目标桶的坐标
    let index = Math.floor(middle % 10);
    // 把元素放入到对应的桶中
    result[index].push(arr[i]);
  }

  // 如果存在某一位未排序,递归排序
  for (let j = 0,len = result.length;j < len;j ++) {
    if (result[j].length > 1) {
      sort(result[j], --round)
    }
  }
  let index = 0;
  // 直到左右元素都排序结束,将桶中将元素取出,放入原数组
  for (let i = 0; i < result.length; i++) {
    for (let j = 0; j < result[i].length; j++) {
      arr[index++] = result[i][j];
    }
  }
}
radixSort([10, 5, 5, 50, 0, 155, 422, 5, 1, 4, 254]);

三 归并排序

归并排序,采用分治法(Divide and Conquer)先使子序列有序,再将已有序的子序列合并,得到完全有序的序列。

实现步骤:

  • 把长度为 n 的数组分成两个长度为 n/2 的子数组。
  • 以相同方法继续拆分子数组,直至最后的子数组长度为 1。
  • 子数组间两两排序,合并,直到得到一个有序数组。

归并排序.jpg

function mergeSort(arr) {
  const len = arr.length;
  // 如果数组长度为1,则递归终止
  if (len === 1) {
    return arr;
  }
  let left = [];
  let right = [];
  const middle = Math.floor(len / 2);

  for (let i = 0;i < len;i ++) {
    i < middle ? left.push(arr[i]) : right.push(arr[i])
  }

  // 将数组拆分到只有单元素的时候才开始合并, 注意递归
  return merge(mergeSort(left), mergeSort(right));
}
// 合并子数组的函数
function merge(left, right) {
  let result = [];
  let l = 0;
  let r = 0;

  // 根据left和right中元素的大小排序
  while(l < left.length && r < right.length) {
    // 如果 左 < 右 将左数组中的元素放入结果数组
    if (left[l] < right[r]) {
      result.push(left[l]);
      l++;
    } else {
      // 如果 左 > 右 将右数组中的元素放入结果数组
      result.push(right[r]);
      r++;
    }
  }

  /*
  *	在上面循环只处理了 左/右 中的一个
  * 下面的循环处理另一个
  */
  while (l < left.length) {
    result.push(left[l]);
    l++;
  }
  while(r < right.length) {
    result.push(right[r]);
    r++;
  }
  return result;
}

// 排序好的数组
mergeSort([10, 5, 4, 50, 0, 155, 422, 90]); // [ 0, 4, 5, 10, 50, 90, 155, 422]

四 桶排序

桶排序,是计数排序的升级版,先将数组元素分发到有限数量的桶里,每个桶分别排序,最后合并得到一个有序数组的过程。

实现步骤:

  • 设置空桶
  • 将数据放到对应的空桶中
  • 将每个不为空的桶进行排序
  • 拼接不为空的桶

桶排序.jpg

function bucketSort (arr) {
  if (arr.length <= 1) {
    return arr;
  }
  // 默认创建 5 个桶容器
  const bucketCount = 5;

  // 初始化需要的参数
  let len = arr.length;
  // 用来排序的桶
  let barrel = [];
  // 用来存放排序结果
  let result = [];
  let max = arr[0];
  let min = arr[0];

  // 寻找到数组中的最大值和最小值
  for (let i = 1; i < len; i++) {
    arr[i] >= max ? max = arr[i] : arr[i] <= min ? min = arr[i] : '';
  }

  // 求出每一个桶的数值范围
  let space = (max - min + 1) / bucketCount;

  // 将数值装入桶中
  for (let i = 0; i < len; i++) {
    // 找到相应的桶序列
    let index = Math.floor((arr[i] - min) / space);
    // 判断是否桶中已经有数值
    if (barrel[index]) {
      let bucket = barrel[index];
      let k = bucket.length - 1;

      // 使用插入排序方法将数组从小到大排列
      while (k >= 0 && barrel[index][k] > arr[i]) {
        barrel[index][k + 1] = barrel[index][k];
        k--
      }
      barrel[index][k + 1] = arr[i];

    } else {
      // 否则,新建容器并添加数据
      barrel[index] = [];
      barrel[index].push(arr[i]);
    }
  }
  // 开始合并数组
  let n = 0;
  while (n < bucketCount) {
    // 将不为空的数组合并
    if (barrel[n]) {
      result = result.concat(barrel[n]);
    }
    n++;
  }
  return result;
}

//开始排序
bucketSort([12, 4, 3, 2, 5, 84, 34, 52, 42, 45, 6, 7, 86, 68, 67]);

五 堆排序

堆排序,是用堆这种数据结构来实现排序的过程,根据 小根堆(大根堆) 移除堆顶元素的过程使得数组有序。

实现步骤

  • 创建一个最大堆。
  • 将堆顶元素放到数组的末尾
  • 将剩下的元素再次调整为一个最大堆。
  • 重复上述步骤,直至数组有序。
// 排序方法
function heapSort(arr) {
  // 获得数组的长度
  let len = arr.length;

  // 首先创建一个最大堆
  createHeap(arr, len);

  while(len > 0) {
    let last = len - 1;
    
    // 将最大元素保存到数组末尾
    swap(arr, 0, last);

    len--;

    // 然后将剩下的元素继续构建为一个新的最大堆
    adjustHeap(arr, 0, len);
  }
  // 返回排序好的数组
  return arr;
}

function createHeap(arr, len) {
  // 构建最大堆。。Math.floor(len / 2) => 数组后半部分的元素都是叶节点,无需进行大小判断
  for (let i = Math.floor(len / 2); i >= 0; i--) {
    adjustHeap(arr, i, len);
  }
}
// 调整堆
function adjustHeap(arr, i, len) {
  // 获得根元素的左子元素坐标
  let left = 2 * i + 1;

  // 获得根元素的右子元素坐标
  let right = 2 * i + 2;

  // 保存做大值坐标
  let largest = i;

  if (left < len && arr[left] > arr[largest]) {
    largest = left;
  }

  if (right < len && arr[right] > arr[largest]) {
    largest = right;
  }

  // 如果 i 不是最大值
  if (largest !== i) {
    // 将最大值放到父节点
    swap(arr, i, largest);

    // 递归调整。一步步将堆调整到最大堆
    adjustHeap(arr, largest, len);
  }
}

// 交换函数
function swap(arr, i, j) {
  [arr[i], arr[j]] = [arr[j], arr[i]];
}

六 算法比较

算法比较.png

说明

  • 稳定:如果 a 原本在 b 前面,而 a = b,排序之后 a 仍然在 b 的前面;

  • 不稳定:如果 a 原本在 b 的前面,而 a = b,排序之后 a 可能会出现在 b 的后面;

  • 内排序(in-place):所有排序操作都在内存中完成;

  • 外排序(out-place):由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;


参考:yancy__JAVA资讯库