排序算法 - JavaScript 实现

118 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第 4 天,点击查看活动详情

本文介绍了常见的几种排序算法的实现思想与编码。

冒泡排序

  • 冒泡排序比较所有相邻的两个项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样。
  • 第一趟将最大的数冒泡到最右边;
  • 第二趟将第二大的数冒泡到右边倒数第二个;
  • 。。。
function swap(arr, a, b) {
  const temp = arr[a];
  arr[a] = arr[b];
  arr[b] = temp;
}

const bubbleSort = function(arr) {
  const len = arr.length;
  if (len <= 1) return;
  for (let i = 0; i < len; i++) {
    for (let j = 0; j < len - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        swap(arr, j, j + 1);
      }
    }
  }
  return arr;
};
// 2
const bubbleSort = (arr) => {
  let len = arr.length;
  for (let i = 0; i < len - 1; i++) {
    for (let j = 1; j < len - i; j++) {
      if (arr[j] < arr[j - 1]) {
        // let temp = arr[j];
        // arr[j] = arr[j - 1];
        // arr[j - 1] = temp;
        [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
      }
    }
  }
  return arr;
};
  • 时间复杂度 O(N^2)
  • 空间复杂度 O(1)

选择排序

选择排序大致的思路是找到数据结构中的最小值并将其放到第一位,接着找到第二小的值将其放在第二位,以此类推。

const selectionSort = function(arr) {
  const len = arr.length;
  if (len <= 1) return;
  for (let i = 0; i < len - 1; i++) {
    let minIndex = i;
    // 需要注意这里的边界, 因为需要在内层进行 i+1 后的循环,所以外层需要 数组长度-1
    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j; // 找到整个数组的最小值
      }
    }
    swap(arr, i, minIndex);
  }
  return arr;
};
  • 时间复杂度 O(N^2)
  • 空间复杂度 O(1)

插入排序

思想

  • 假定第一项已经排序了
  • 接着,寻找第二项应该插入的位置,与前面的数逐一进行比较,判断第二项是应该待在原位还是插到第一位之前,根据判断结果做相应处理
  • 这样前两项就已正确排序
  • 接着,寻找第三项应该插入的位置,与前面的数逐一进行比较,判断它是该插入到第三、第二还是第一的位置
  • 以此类推
const insertionSort = function(arr) {
  const len = arr.length;
  // 外层循环: 外层循环是从1位置开始, 依次遍历到最后
  for (let i = 1; i < len; i++) {
    // 记录选出的元素, 放在变量 temp 中
    let temp = arr[i];
    // 从当前数的前一个开始比较
    let j = i - 1;
    // 内层循环: 内层循环不确定循环的次数, 最好使用 while 循环,
    // 注意 j >= 0 这个条件
    while (j >= 0 && arr[j] > temp) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = temp;
  }
  return arr;
};

希尔排序

  • 希尔排序是插入排序的一种高效的改进版, 并且效率比插入排序要更快.
  • 【重要】:希尔排序有插入排序的思想。希尔排序 当 gap = 1 时,就是 插入排序
function shellSort(arr) {
  let len = arr.length;
  // 根据长度计算增量
  let gap = Math.floor(len / 2);
  // 增量不断变小, 大于 0 就继续排序
  while (gap > 0) {
    // 实现插入排序
    for (let i = gap; i < len; i++) {
      let j = i - gap;
      let temp = arr[i];
      while (j >= 0 && arr[j] > temp) {
        arr[j + gap] = arr[j];
        j = j - gap;
      }
      arr[j + gap] = temp;
    }
    // 重新计算新的间隔
    gap = Math.floor(gap / 2);
  }
  return arr;
}

另一种写法:

const shellSort = function(arr) {
  const len = arr.length;
  // 根据长度计算增量
  let gap = Math.floor(len / 2);

  // 增量不断变量小, 大于 0 就继续排序
  while (gap > 0) {
    // 实现插入排序
    for (let i = gap; i < len; i++) {
      let j = i;
      let temp = arr[i];
      while (j > gap - 1 && arr[j - gap] > temp) {
        arr[j] = arr[j - gap];
        j -= gap;
      }
      arr[j] = temp;
    }
    // 重新计算新的间隔
    gap = Math.floor(gap / 2);
  }
  return arr;
};

归并排序

思想

  • 这是一种分治算法。将原始数组切分成较小的数组,直到每个小数组只有一项,
  • 然后在将小数组归并为排好序的较大数组,直到最后得到一个排好序的最大数组。
const mergeSort = function(arr) {
  const len = arr.length;
  // 当任意数组分解到只有一个时返回。
  if (len <= 1) return arr;
  const middle = Math.floor(len / 2);
  // 递归 分解
  const left = mergeSort(arr.slice(0, middle));
  const right = mergeSort(arr.slice(middle));
  // 合并
  return merge(left, right);
};
const merge = (left, right) => {
  let result = [];
  let i = 0;
  let j = 0;
  // 判断2个数组中元素大小,依次插入数组
  while (i < left.length && j < right.length) {
    result.push(left[i] <= right[j] ? left[i++] : right[j++]);
  }
  return [...result, ...left.slice(i), ...right.slice(j)];
};

性能分析

  • 时间复杂度:最好、平均、最坏 O(nlogn)
  • 空间复杂度: O(n), 稳定

快速排序

思想

  • 首先,从数组中选一个主元(pivot)---主元的选取方式后面进行说明
  • 划分 partition 操作---创建两个指针,左边一个指向数组第一个值,右边一个指向数组最后一个值。移动左指针直到找到一个比主元大的值,移动右指针直到找到一个比主元小的值,然后交换它们,重复这个过程,直到左指针超过了右指针。这个过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后。
  • 算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的子数组)重复之前的两个步骤,直至数组已完全排序。

实现一

const quickSort = (arr, left = 0, right = arr.length - 1) => {
  if (arr.length > 1) {
    let index = partition(arr, left, right);
    if (left < index - 1) {
      quickSort(arr, left, index - 1);
    }
    if (index < right) {
      quickSort(arr, index, right);
    }
  }
  return arr;
};
// 划分过程
const partition = (arr, left, right) => {
  const pivot = arr[right];
  let i = left,
    j = right - 1;
  while (i <= j) {
    while (arr[i] < pivot) {
      i++;
    }
    while (arr[j] > pivot) {
      j--;
    }
    if (i <= j) {
      [arr[i], arr[j]] = [arr[j], arr[i]];
      i++;
      j--;
    }
  }
  [arr[i], arr[right]] = [arr[right], arr[i]];
  return i;
};

实现二

function quickSort(arr, left = 0, right = arr.length - 1) {
  if (arr.length > 1) {
    let index = partition(arr, left, right);
    if (left < index - 1) {
      quickSort(arr, left, index - 1);
    }
    if (index < right) {
      quickSort(arr, index, right);
    }
  }
  /* if (left >= right) return
	let index = partition(arr, left, right)
	quick(arr, left, index - 1)
	quick(arr, index, right) */

  return arr;
}
// 划分过程
function partition(arr, left, right) {
  // 选取中间那个数作为主元
  const pivotIndex = Math.floor((left + right + 1) / 2);
  // 注意:这样会发生栈溢出
  // const pivotIndex = Math.floor((left + right) / 2)

  const pivot = arr[pivotIndex];
  // 将 主元 暂时与最右边的数交换,这样主元待在最后一个位置不动,等全部交换完毕之后,
  // 再将 主元 一次性 放回到 正确的位置
  swap(arr, pivotIndex, right);
  // 如果直接以最后一个数作为 pivot,上述三行代码 直接替换为 const pivot = arr[right]

  let i = left;
  let j = right - 1;
  while (i <= j) {
    while (arr[i] < pivot) {
      i++;
    }
    while (arr[j] > pivot) {
      j--;
    }
    /*
		if (i < j) {    // 测试不通过
		 */
    if (i <= j) {
      swap(arr, i, j);
      i++;
      j--;
    }
  }
  // 将枢纽放在正确的位置
  swap(arr, i, right);
  return i;
}

const swap = (arr, i, j) => {
  const temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
};

性能分析

  • 时间复杂度:O(nlogn)
  • 空间复杂度: O(1)

实现三

function quickSort(arr) {
  return quick(arr, 0, arr.length - 1);
}

function quick(arr, left, right) {
  let index;
  if (arr.length > 1) {
    index = partition(arr, left, right);
    if (left < index - 1) {
      quick(arr, left, index - 1);
    }
    if (index < right) {
      quick(arr, index, right);
    }
  }
  return arr;
}
// 划分过程
function partition(arr, left, right) {
  // 选取中间那个数作为主元
  const pivot = arr[Math.floor((left + right + 1) / 2)];
  // 注意:这样会发生栈溢出
  // const pivot = arr[Math.floor((left + right) / 2)]

  const pivot = arr[pivotIndex];
  // 将 主元 暂时与最右边的数交换,这样主元待在最后一个位置不动,等全部交换完毕之后,
  // 再将 主元 一次性 放回到 正确的位置
  swap(arr, pivotIndex, right);
  // 如果直接以最后一个数作为 pivot,上述三行代码 直接替换为 const pivot = arr[right]
  let i = left;
  let j = right - 1;
  while (i <= j) {
    while (arr[i] < pivot) {
      i++;
    }
    while (arr[j] > pivot) {
      j--;
    }
    /*
		if (i < j) {    // 测试不通过
		 */
    if (i <= j) {
      swap(arr, i, j);
      i++;
      j--;
    }
  }
  // 将枢纽放在正确的位置
  swap(arr, i, right);
  return i;
}

const swap = (arr, i, j) => {
  const temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
};

性能分析

  • 时间复杂度:O(nlogn)
  • 空间复杂度: O(1)

如何用快排思想在 O(n)内查找第 K 小的元素?

解题:如果用快排的思想,按照从小到大排序,如果要找第 K 小的数,左边就应该有 K-1 个数,主元就是第 K 的数。此时它的下标是 k-1 ?

/**
 * 第k小的数
 * @param {array} arr
 * @param {number} k
 */
function kthNum(arr, k) {
  const len = arr.length;
  if (k > len) {
    return -1;
  }
  let p = partition(arr, 0, len - 1);
  while (p !== k - 1) {
    if (p > k - 1) {
      p = partition(arr, 0, p - 1);
    } else {
      p = partition(arr, p + 1, len - 1);
    }
  }
  return arr[p];
}

function partition(arr, left, right) {
  const pivot = arr[right];
  let i = left;
  let j = right - 1;
  while (i <= j) {
    while (arr[i] < pivot) {
      i++;
    }
    while (arr[j] > pivot) {
      j--;
    }
    if (i <= j) {
      swap(arr, i, j);
      i++;
      j--;
    }
  }
  // 将枢纽放在正确的位置
  swap(arr, i, right);
  return i;
}

function swap(arr, i, j) {
  if (i === j) return;
  let tmp = arr[i];
  arr[i] = arr[j];
  arr[j] = tmp;
}

如何用快排思想在 O(n)内查找第 K 大的元素?

/**
 * @param {array} arr
 * @param {number} k
 */
function kthNum(arr, k) {
  const len = arr.length;
  if (k > len) {
    return -1;
  }
  let p = partition(arr, 0, len - 1);
  while (p + 1 !== k) {
    if (p + 1 > k) {
      p = partition(arr, 0, p - 1);
    } else {
      p = partition(arr, p + 1, len - 1);
    }
  }
  return arr[p];
}

function partition(arr, left, right) {
  const pivot = arr[right];
  let i = left;
  let j = right - 1;
  while (i <= j) {
    while (arr[i] > pivot) {
      i++;
    }
    while (arr[j] < pivot) {
      j--;
    }
    if (i <= j) {
      swap(arr, i, j);
      i++;
      j--;
    }
  }
  // 将枢纽放在正确的位置
  swap(arr, i, right);
  return i;
}

function swap(arr, i, j) {
  if (i === j) return;
  let tmp = arr[i];
  arr[i] = arr[j];
  arr[j] = tmp;
}

参考