面试当中,排序算法经常出现,遇到过的有冒泡排序,快速排序,快排的出现频率是非常高的,但是现场写代码的话很容易出错,今天再把常见排序算法实现总结一下,分别是以下七种:
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序:插入排序的进化版本
- 归并排序:递归
- 快速排序: 面试高频题
- 堆排序:求最大的k个数的算法
先脑图总结一波:
冒泡🫧排序
需要n-1趟排序,每次将一个最大或者最小的元素冒泡到最后。
function bubbleSort(arr) {
const len = arr.length;
// 遍历n-1次
for (let i = 0; i < len - 1; i++) {
// 比较范围是0-j,j-n是已经排序好的数据
for (let j = 0; j < len - i - 1; j++) {
// 从0开始的话和后面的元素比较
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
bubbleSort(arr);
选择排序
n-1趟排序,每趟从无序区中选择最小的记录,和有序区最后一个i做交换。
function selectionSort(arr) {
const len = arr.length;
for (let i = 0; i < len - 1; i++) {
let min = arr[i];
let minIndex = i;
for (let j = i + 1; j < len; j++) {
if (arr[j] < min) {
min = arr[j];
minIndex = j;
}
}
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
return arr;
}
arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
selectionSort(arr);
插入排序
假设第一个元素是排好序的,然后后面每个元素不断往前插入,像摆扑克牌一样, 所以要找到当前元素在前面已排序数组中的位置,然后交换过去。
function insertSort(arr) {
for (let i = 1; i < arr.length; i++) {
let tmp = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > tmp) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = tmp;
}
return arr;
}
arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
insertSort(arr);
希尔排序
插入排序的进化版,将元素分为若干个子序列分别进行直接插入排序,gap为一个序列,不断降低为1,为1后即为排完序的序列。gap的选择,一般选择n/2,不断除以2,直到1。
function shellSort(arr) {
let len = arr.length;
let gap = Math.floor(len / 2);
for (gap; gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < len; i++) {
let temp = arr[i];
let j;
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
}
}
return arr;
}
arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
shellSort(arr);
归并排序
把长度为n的输入序列非为2个长度为n/2的子序列,对两个子序列分别采用归并排序,将两个排序好的子序列合并为最终的排序序列。
function mergeSort(arr) {
const len = arr.length;
if (len < 2) {
return arr;
}
const middle = Math.floor(len / 2);
return merge(mergeSort(arr.slice(0, middle)), mergeSort(arr.slice(middle)))
}
function merge(left, right) {
let result = [];
let i = 0;
let j = 0;
while (i < left.length && j < right.length) {
if (left[i] < right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
while (i < left.length) {
result.push(left[i++]);
}
while (j < right.length) {
result.push(right[j++]);
}
return result;
}
arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
mergeSort(arr);
快速排序
思路
对快速排序又恨又爱,因为面试很容易遇到,但是如果要在面试中写出一个没错的代码,还得花点时间,很容易在边界上出问题。思路的话容易理解,选择枢纽元,没趟将小于枢纽元的放在左边,大于枢纽元的放在右边。然后不断递归。如果用双指针的写法话,容易出错的点有:
- 枢纽元的选取
- 第一个元素:最简单,要是面试就这样写吧,简单不易错。
- 随机选择的话,注意生成随机index的方法,要注意把left加回去
Math.floor(Math.random() * (right - left + 1)) + left。 - 三数中值法: 是算法导论上推荐的写法,left,middle、right,排好序,middle放到left+1的位置。图解排序算法之快速排序—三数取中法
- 选择完枢纽元后,放到left位置,i从left+1,j从right开始。找大于枢纽元的位置和小于枢纽元的位置时,left可以等于right,这样最后i指向的是比枢纽元大的位置,j指向的是比枢纽元小的元素的位置。因为枢纽元放在了最左边,所以一趟处理完后要和j进行交换。就是说放到left位置的枢纽元和j最后的位置交换。
- 递归子数组的范围处理好,要按照同样的规则处理,要是都是左闭右开都左闭右开,要是都左闭右闭,都左闭右闭,保持统一即可。
function quickSort(arr, left, right) {
if (right <= left) {
return arr;
}
// 随机选择枢纽元:易错点
const pivotIndex = Math.floor(Math.random() * (right - left + 1)) + left;
const pivot = arr[pivotIndex];
// 交换枢纽元
[arr[left], arr[pivotIndex]] = [arr[pivotIndex], arr[left]];
let i = left + 1;
let j = right;
while (i < j) {
// i最后指向比它大的元素
while (i <= j && arr[i] < pivot) {
i++;
}
// j指向比它小的元素,因为枢纽元在第一位,所以要和比它小的元素替换
while (j >= i && arr[j] > pivot) {
j--;
}
if (i < j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
if (arr[j] < pivot) {
[arr[j], arr[left]] = [arr[left], arr[j]];
}
quickSort(arr, 0, j - 1);
quickSort(arr, j + 1, right);
return arr;
}
arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
quickSort(arr, 0, arr.length - 1);
另一种写法
如果说以上按照索引的写法容易出错,那另一种避开索引的写法就容易多了,就是不传入left和right,写的时候直接将小于枢纽元的数组收集成left,大于枢纽元的数组收集成right,然后对left和right分别就行快速排序,但是要记住的是要返回排序后的数组。
function quickSort2(arr) {
if (arr.length < 2) {
return arr;
}
const pivot = arr[0];
const left = [];
const right = [];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < pivot) {
right.push(arr[i]);
} else {
left.push(arr[i]);
}
}
return [...quickSort2(right), pivot, ...quickSort2(left)];
}
arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
quickSort2(arr);
堆排序
- 建大顶堆
- 不断交换堆顶元素和最后一个元素
- 交换后将数组重新调整为大顶堆。
function heapSort(arr) {
buildHeap(arr);
for (let j = arr.length - 1; j > 0; j--) {
swap(arr, 0, j);
// 这里需要调整的堆的大小要减小,所以heapify需要加上size参数,否则范围不对
heapify(arr, 0, j);
}
return arr;
}
function buildHeap(arr) {
const index = Math.floor(arr.length / 2) - 1;
// 从第一个非叶子节点开始调整堆
for (let i = index; i >= 0; i--) {
heapify(arr, i, arr.length);
}
return arr;
}
// 调整以index为根节点的子树为大顶堆
function heapify(arr, index, size) {
const len = size;
let maxIndex = index;
const leftChild = maxIndex * 2 + 1;
const rightChild = leftChild + 1;
if (leftChild < len && arr[leftChild] > arr[index]) {
maxIndex = leftChild;
}
if (rightChild < len && arr[rightChild] > arr[maxIndex]) {
maxIndex = rightChild;
}
if (index !== maxIndex) {
swap(arr, index, maxIndex);
heapify(arr, maxIndex, size);
}
}
var arr=[91,60,96,13,35,65,46,65,10,30,20,31,77,81,22];
heapSort(arr);
总结
终于写完了,完结撒花,其他的还有计数排序、桶排序、基数排序,等以后有时间再研究吧,这几个问到的很少😄。
参考文章:
十大经典排序算法总结(JavaScript描述): 很棒的文章,算法的图过程很生动。