数组排序、排序算法

sort 方法

数组的 sort 方法 Array.prototype.sort(compareFn) ,如果不传 compareFn 回调,默认排序是 将元素转换为字符串,然后按它们的 UTF-16 码元值升序排序

sort 方法 不会处理 undefined 和 空槽 empty ,不管传没传 compareFnundefined 和空槽都 直接跳过,并且移到数组末尾,并是是 所有 undefined 都放在 所有 empty 之前。

compareFn 的常用写法 升序排序是 (a, b) => a - b ,降序是 (a, b) => b - a ,这样看上去好像只有返回正数值,才会进行元素位置交换,

但实际上, compareFn 要求是 自对称的 ,即 compareFn 的返回值 必须正数负数都能出现,而且 compareFn(a, b)compareFn(b, a) 返回值 必须是对称的正负数值,不能只返回正数,或只返回负数,参数顺序交换则返回值的正负也要相反

如果 compareFn 不满足这种对称性,则 sort 方法无法保证预期的结果,

对于这种 不规范的 compareFn 不同引擎的处理也不一样,无法保证兼容性,例如V8引擎不会处理只返回正数的,而对只返回负数的才会进行元素位置交换。

排序算法

sort 的默认的排序算法,在 Chrome 70 版本以前,在V8引擎的实现,默认使用的是 快排 QuickSort 和 插排 Insertion Sort

对于超过10个元素的,使用 快排 QuickSort (不稳定),而 10个及10个以内的,使用 插入排序 InsertionSort (稳定),

Chrome 70开始,改成了统一使用 TimSort (稳定),不管多少个元素。

TimSort 源于 归并排序 MergeSort ,归并排序通常是递归,TimSort 改成了循环迭代,并且有一些优化,对于划分出来的一些很短的且无序的子序列,也会使用 插入排序 进行 “加速处理” 。

面试手写参考

// 公用方法
const swap = (arr, i, j) => {
  if (i === j) return;
  // [arr[i], arr[j]] = [arr[j], arr[i]];
  const temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
};

1. 冒泡排序

const bubbleSort = (arr) => {
  if (!Array.isArray(arr)) return;
  const len = arr.length;
  // 外层循环控制轮数,一共需要 len - 1 轮
  for (let i = 0; i < len - 1; i++) {
    // 每一轮都把最大的冒泡到最后
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        swap(arr, j, j + 1);
      }
    }
  }
};

bubble-sort

2. 选择排序

const selectionSort = (arr) => {
  if (!Array.isArray(arr)) return;
  const len = arr.length;
  let minIndex = 0;
  // 每个位置依次填入最小值,每一轮都选出一个最小值,填入对应位置,一共需要 len - 1 轮
  for (let i = 0; i < len - 1; i++) {
    minIndex = i;
    // 每一轮都从 i + 1 开始找最小值
    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    // 找到最小值后,和 i 位置交换,之后下一轮重置 minIndex
    swap(arr, i, minIndex);
  }
};

selection-sort

3. 插入排序

const insertionSort = (arr) => {
  if (!Array.isArray(arr)) return;
  const len = arr.length;
  // 从第二个位置开始,提出当前索引的值,依次向前比较,
  // 如果是更大的值,就向后移位,腾出插入空间,直到找到合适的位置插入
  for (let i = 1; i < len; i++) {
    const target = arr[i];
    let j = i - 1;
    while (j >= 0 && arr[j] > target) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = target;
  }
};

insertion-sort

4. 归并排序

// 归并
const merge = (left, right) => {
  const res = [];
  while (left.length && right.length) {
    if (left[0] <= right[0]) {
      res.push(left.shift());
    } else {
      res.push(right.shift());
    }
  }
  return [...res, ...left, ...right];
};

const mergeSort = (arr) => {
  // if (!Array.isArray(arr)) throw new Error('请输入数组'); // 递归中不好重复判断
  const len = arr.length;
  // 递归地进行左右拆分,直到拆分到单个元素
  if (len <= 1) return arr;
  const mid = Math.floor(len / 2);
  const left = arr.slice(0, mid);
  const right = arr.slice(mid);
  return merge(mergeSort(left), mergeSort(right));
};

merge-sort

5. 快速排序

// 5. 快排
const quickSort = (arr) => {
  // 阮氏快排 空间复杂度需要优化 非in-place(原地)排序,每轮递归都需要额外的空间
  // if (!Array.isArray(arr)) throw new Error('请输入数组'); // 递归中不好重复判断
  const len = arr.length;
  if (len <= 1) return arr;
  // 每轮递归都选择一个基准值,将数组分为左右两部分,左边的值都小于等于基准值,右边的值都大于等于基准值
  const pivotIndex = Math.floor(len / 2);
  const pivot = arr[pivotIndex];
  const left = [];
  const right = [];
  for (let i = 0; i < len; i++) {
    if (i === pivotIndex) continue;
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return [...quickSort(left), pivot, ...quickSort(right)];
};

// 优化:双指针法,空间复杂度 O(1)  in-place排序
const quickSort2 = (arr, left = 0, right = arr.length - 1) => {
  if (!Array.isArray(arr)) throw new Error('请输入数组');
  if (typeof left !== 'number' || typeof right !== 'number') throw new Error('请输入正确的左右边界');
  // 每轮递归都选择一个基准值,将数组分为左右两部分,左边的值都小于等于基准值,右边的值都大于等于基准值
  if (left >= right) return;
  // 先假设最左边为基准值
  const pivot = arr[left];
  let i = left; // left + 1; // 左指针 (考虑边界情况,这里不能取left+1,否则 left+1 的位置就没有参与比较了)
  let j = right; // 右指针
  while (i < j) {
    // 从右往左 找第一个小于基准值的元素 即需要往左边移的元素
    /* (考虑边界情况,最好先从右往左找,否则最后一次i++ 后,i j相遇时,都指向一个
       应该在右边的值了,导致基准值归位swap时需要 跟 i-1 交换,而不是 i) */
    while (i < j && arr[j] >= pivot) {
      j--;
    }
    // 从左往右 找第一个大于基准值的元素 即需要往右边移的元素
    while (i < j && arr[i] <= pivot) {
      i++;
    }
    // j 一个需要往左边移,i 一个需要往右边移, 交换
    if (i < j) {
      swap(arr, i, j);
    }
    // 继续循环 直到 i j 相遇
  }
  // 基准值归位  i j 相遇的位置就是基准值应该放的位置
  swap(arr, left, i);
  // 递归处理左右两边
  quickSort2(arr, left, i - 1);
  quickSort2(arr, i + 1, right);
};

quick-sort

test

// test
const fn = (sortFunc) => {
  const arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
  console.time('sort');
  const res = sortFunc(arr);
  console.timeEnd('sort');
  console.log(res || arr);
};
fn(bubbleSort);
fn(selectionSort);
fn(insertionSort);
fn(mergeSort);
fn(quickSort);
fn(quickSort2);

相关参考

https://blog.youkuaiyun.com/zyb18507175502/article/details/130772628
https://www.jianshu.com/p/7019c9621914
https://visualgo.net/zh
https://yongdanielliang.github.io/animation/web/QuickSortNew.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值