常用的九种排序算法

排序算法,本文整理了之前个人学习中的九种常用的排序算法,用作个人引导复习使用,包括的方法如下图所示:
九种排序算法

1. 冒泡排序(Bubble Sort)

原理: 比较相邻的元素。如果第一个比第二个大,就交换他们两个。对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,做完后,最后的元素会是最大的数。所有的元素重复以上的步骤,直到没有任何一对数字需要比较。
动图:
冒泡排序(冒大泡)

function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) { // 外层一共判断i次,冒泡len-1次变有序
for (let j = 0; j < arr.length - 1 - i; j++) { // 内层判断j次,j是未排序的数量,随i大j慢慢变小
if (arr[j] > arr[j + 1]) { // 相邻元素两两对比,如果前面的比后面大的话就交换
[arr[j + 1], arr[j]] = [arr[j], arr[j + 1]]; // 冒泡随时交换
}
}
}
return arr;
}

2. 选择排序(Selection Sort)

原理: 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。重复第二步,直到所有元素均排序完毕。
动图:
选择排序

function selectionSort(arr) {
for (let i = 0; i < arr.length - 1; i++) { // 排序len个数据排len-1次就有序了
let maxIndex = i;
for (let j = i + 1; j < arr.length; j++) { // 得对最后一个元素也进行选择,看是否是最大的数
if (arr[j] > arr[maxIndex]) { // 寻找最大的数
maxIndex = j; // 将最大数的索引保存
}
}
[arr[i], arr[maxIndex]] = [arr[maxIndex], arr[i]]; // 等到找到一轮最大的后交换
}
return arr;
}

3. 直接插入排序(Insertion Sort)

原理: 插入排序是一种最简单直观的排序算法,将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
动图:
插入排序

function insertionSort(arr) {
for (let i = 1; i < arr.length; i++) { // 假设第一个元素为有序序列
let preIndex = i - 1, currentNum = arr[i]; // 设有序最后一个位置;当前的数值
while (preIndex >= 0 && arr[preIndex] < currentNum) { // 比较所有有序和当前的数值
arr[preIndex + 1] = arr[preIndex]; // 符合条件有序位置的变换
preIndex--;
}
arr[preIndex + 1] = currentNum; // 插入
}
return arr;
}

4. 希尔排序(Shell Sort)

原理: 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
步骤:
直接插入排序的逆序的话首末端排序效率很低,希尔排序跳跃式分组,通过增量将元素分组,这样的话移动次数大大降低,然后缩小增量,直到增量为1;基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
希尔排序

function shellSort(arr) {
//增量gap,并逐步缩小增量
for (let gap = Math.floor(arr.length / 2); gap > 0; gap = Math.floor(gap / 2)) {
//对各个增量组成的序列进行直接插入排序操作
for (let i = gap; i < arr.length; i++) { // 和直插中一样,i假设之前的为有序部分,从i的无序部分开始;
let j = i - gap; //gap代表直插中无序的第一个,j代表有序的最后一个,类似直插的preIndex
let currentNum = arr[j + gap]; // j+gap为直接插入排序中后面无序的第一个元素位置,currentNum是直插的当前数值
while (j >= 0 && arr[j] < currentNum) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = currentNum;
}
}
return arr;
}

5. 快速排序(Quick Sort)

原理: 快速排序使用分治法,快排在平均状况下时间复杂度为Ο(nlogn),极限情况下是n的平方,但是这种状况极少而且快排明显比其他Ο(nlogn)的算法更快;
步骤:
1 从数列中挑出一个元素,称为 “基准”(pivot);
2 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
快速排序

// 以下代码的实现方式是,选择一个中间的数字为基准点,用两个数组分别去保存比基准数小的值,
// 和比基准数大的值,最后递归左边的数组和右边的数组,用concat去做一个数组的合并。
let quickSort = function (arr) {
if (arr.length <= 1) {
return arr;
}
let pivot = arr.splice(Math.floor(arr.length / 2), 1)[0]; // pivot值
// let pivot = array[0]; //取第一个位置的数值为pivot基准值,但这种情况极限下的话效率低

let left = []; // 存小于pivot元素的数组
let right = []; //存大于的

for (let i = 0; i < arr.length; i++) { // 把剩下的arr的所有遍历后分到左右数组中去
if (arr[i] > pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat(pivot, quickSort(right)); // 分别递归,并用concat连接
};

优缺点:
缺点:获取基准点使用了一个splice操作,在js中splice会对数组进行一次拷贝的操作,而它最坏的情况下复杂度为O(n),而O(n)代表着针对数组规模的大小进行了一次循环操作。我们每次执行都会使用到两个数组空间,增大空间复杂度;concat操作会对数组进行一次拷贝,而它的复杂度也会是O(n),对大量数据的排序来说相对会比较慢
优点:代码简单明了,可读性强,易于理解

6. 归并排序(Merge Sort)

原理: 归并排序:将两个或者两个以上的有序表组合成一个新的有序表;待排表有n个元素,视为n个有序的子表,每个子表长度为1,两个归并后,得到n/2取上界个的长度为2或者1的有序表,然后继续合并,重复到一个长度为n的有序表终止。选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间
步骤:
如代码所示
归并排序

function mergeSort(data) { // 
//采用自上而下的归并递归方法,将完整待排序数据分为直到一个最小的1个元素,然后调用merge归并
let len = data.length; //len是待排序数据个数
if (len < 2) { // 一个就直接有序了
return data;
}
// 取中点middle,然后分为两个子数组
let middle = Math.floor(len / 2),
left = data.slice(0, middle),
right = data.slice(middle); // 拆分为两个子数组
return merge(mergeSort(left), mergeSort(right));
}

let merge = (left, right) => {
let result = [];
while (left.length && right.length) {
// 注意: 判断的条件是小于或等于,如果只是小于,那么排序将不稳定.小的加到已排序数组里
// 比较大小,然后加入到result中
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
// left right某一个数组为空后,把另一个中数据加到结果里
while (left.length) result.push(left.shift());
while (right.length) result.push(right.shift());
return result;
};

7. 桶排序 请见菜鸟教程-桶排序

8. 基数排序(Radix Sort)

原理: 基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
步骤:
如代码所示,先将待排序数据放入到根据数据生成的对应的counter数组内的桶(数组)里,然后开始对个位进行排序,然后放入arr中,个位排序结束后,重新根据十位生成counter的桶,再对十位进行排序然后再放入arr中,直到最大位数也排序完成。
基数排序

function radixSort(arr, maxDigits) { //maxDigits待排序数据的最大位数
let counter = []; // 放置桶的数组,最大为十个位置,0-9
let mod = 10, dev = 1; // 都是为了取个位,十位,,,的
for (let i = 0; i < maxDigits; i++, dev *= 10, mod *= 10) { // 个位桶排完排十位百位,依次直到maxDigits
for (let j = 0; j < arr.length; j++) {
let bucket = parseInt((arr[j] % mod) / dev); // 得到当前桶的值,好放入对应的桶内
if (counter[bucket] == null) {
counter[bucket] = [];
}
counter[bucket].push(arr[j]);
}

let pos = 0; // 位置
for (let j = counter.length - 1; j >= 0; j--) { // j代表从0到counter末尾最大为10遍历里面的数据后排序
if (counter[j]) { // 防止上面末尾从0开始有为空的地方
while (counter[j][0]) { // 判断当前的已经部分基数排序元素是否存在
arr[pos++] = counter[j].shift(); //存在的话按照pos的位置存到arr里
}
}
}
}
return arr;
}

9. 堆排序 请见数据结构之堆排序

PS:本文参考地址这里此链接

文章作者: 鐔雨
文章链接: https://caichunyu.github.io/2022/07/04/排序算法/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 鐔雨的Blog