我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情
整理8个排序算法的原理,时间复杂度,稳定性,空间复杂度和代码示例。以最快方式了解排序算法方式和理解.
复杂度归类
-
时间复杂度 O(n^2)
- 冒泡排序
- 插入排序
- 选择排序
-
时间复杂度O(nlogn)
- 快速排序
- 归并排序
-
时间复杂度O(n)
- 计数排序
- 基数排序
- 桶排序
分析排序算法
-
算法的执行效率
- 最好、最坏、平均情况时间复杂度。
- 时间复杂度的系数、常数和低阶。
- 比较次数,交换(或移动)次数。
-
排序算法的稳定性
- 稳定性概念:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
- 稳定性重要性:可针对对象的多种属性进行有优先级的排序。
-
排序算法的内存损耗
- 原地排序算法:特指空间复杂度是O(1)的排序算法。
排序分析
冒泡排序
-
排序原理
- 冒泡排序只会操作相邻的两个数据。
- 每次冒泡操作都会对相邻的两个元素进行比较
- 看是否满足大小关系要求,如果不满足就让它俩互换
- 一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作
- 优化:若某次冒泡不存在数据交换,则说明已经达到完全有序,所以终止冒泡
-
性能分析
-
时间复杂度
- 最好情况时间复杂度:数据完全有序时,只需进行一次冒泡操作即可,时间复杂度是O(n)
- 最坏情况时间复杂度:数据倒序排序时,需要n次冒泡操作,时间复杂度是O(n^2)
- 平均情况时间复杂度:通过有序度和逆序度分析,时间复杂度是O(n^2)
-
稳定性:如果两个值相等,就不会交换位置,故是稳定排序算法
-
空间复杂度:每次交换仅需1个临时变量,故空间复杂度为O(1),是原地排序算法
-
-
代码示例
function bubbleSort(arr) { const len = arr.length; for (let i = 0; i < len; i++) { let flag = false; for (let j = 0; j < len - 1 - i; j++) { if (arr[j] > arr[j + 1]) { [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; flag = true; } } if (flag == false) return arr; } return arr; }
插入排序
-
排序原理
- 数组中的数据分为已排序区间和未排序区间。
- 初始已排序区间只有一个元素,就是数组的第一个元素
- 插入算法的核心思想就是取未排序区间中的元素
- 在已排序区间中找到合适的插入位置将其插入,并保证已排序区间中的元素一直有序
- 重复这个过程,直到未排序中元素为空,算法结束
-
性能分析
-
时间复杂度
- 最好情况时间复杂度:数组是有序的,不需要搬移任何数据。只要遍历一遍数组,时间复杂度是O(n)。
- 最坏情况时间复杂度:数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,这时需要移动大量的数据,时间复杂度是O(n^2)。
- 平均情况时间复杂度:而在一个数组中插入一个元素的平均时间复杂都是O(n),插入排序需要n次插入,平均时间复杂度是O(n^2)。
-
空间复杂度:插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是O(1),是原地排序算法。
-
稳定性:
- 插入排序中值相同的元素,我们可以选择将后面出现的元素
- 插入到前面出现的元素的后面,这样就保持原有的顺序不变
- 所以是稳定的,而且是原地排序算法。
-
-
代码示例
function insertSort(arr) { // 缓存数组长度 const len = arr.length; let temp; for (let i = 1; i < len; i++) { let j = i; temp = arr[i]; // 判断 j 前面一个元素是否比 temp 大 while (j > 0 && arr[j - 1] > temp) { arr[j] = arr[j - 1]; j--; } arr[j] = temp; } return arr; }
选择排序
-
排序原理
- 将数组分成已排序区间和未排序区间
- 初始已排序区间为空
- 每次从未排序区间中选出最小的元素插入已排序区间的末尾
- 直到未排序区间为空
-
性能分析
-
时间复杂度:无论是否有序,每个循环都会完整执行
- 最好情况时间复杂度:O(n^2)
- 最坏情况时间复杂度:O(n^2)
- 平均情况时间复杂度:O(n^2)
-
空间复杂度:选择排序算法空间复杂度是O(1),是一种原地排序算法。
-
稳定性:遇到相同的变量,元素位置会发生改变,他不是稳定的排序算法,相对于冒泡排序和插入排序,选择排序就稍微逊色
-
-
代码示例
function selectSort(arr) { // 缓存数组长度 const len = arr.length; // 定义 minIndex let minIndex; // i 是当前排序区间的起点 for (let i = 0; i < len - 1; i++) { // 初始化 minIndex 为当前区间第一个元素 minIndex = i; for (let j = i; j < len; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } // 如果 minIndex 对应元素不是目前的头部元素,则交换两者 if (minIndex !== i) { [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; } } return arr; }
归并排序
-
排序原理
-
使用分治思想。分治:顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了
- 分解:将n个元素分成个含n/2个元素的子序列。
- 解决:用合并排序法对两个子序列递归的排序。
- 合并:合并两个已排序的子序列已得到排序结果。
-
分割:
- 将数组从中点进行分割,分为左、右两个数组
- 递归分割左、右数组,直到数组长度小于
2
-
归并:
- 如果需要合并,那么左右两数组已经有序了。
- 创建一个临时存储数组
temp
, - 比较两数组第一个元素,将较小的元素加入临时数组
- 若左右数组有一个为空
- 那么此时另一个数组一定大于
temp
中的所有元素, - 直接将其所有元素加入
temp
-
-
性能分析
-
时间复杂度
- 最好情况时间复杂度:O(nlogn)
- 最坏情况时间复杂度:O(nlogn)
- 平均情况时间复杂度:O(nlogn)
-
空间复杂度:O(n) 临时的数组和递归时压入栈的数据占用的空间:n + logn
-
稳定性:归并最后到底都是相邻元素之间的比较交换,并不会发生相同元素的相对位置发生变化,所以他是稳定性
-
-
代码示例
function mergeSort(arr) { const len = arr.length; if (len <= 1) { return arr; } const mid = Math.floor(len / 2); const leftArr = mergeSort(arr.slice(0, mid)); const rightArr = mergeSort(arr.slice(mid, len)); arr = mergeArr(leftArr, rightArr); return arr; } function mergeArr(arr1, arr2) { let i = 0, j = 0; const res = []; const len1 = arr1.length; const len2 = arr2.length; while (i < len1 && j < len2) { if (arr1[i] < arr2[j]) { res.push(arr1[i]); i++; } else { res.push(arr2[j]); j++; } } if (i < len1) { return res.concat(arr1.slice(i)); } else { return res.concat(arr2.slice(j)); } }
快速排序
-
排序原理
-
思想
- 将排序的数据分割成独立的两部分
- 其中一部分的数据比另一部分的数据要小
- 再对这两部分数据分别进行快速排序
- 整个排序过程可以递归进行,使整个数据变成有序序列
-
步骤
- 从数组中挑出一个元素,称为 "基准"(pivot)(一般选择第一个数)
- 将比
基准
小的元素移动到数组左边,比基准
大的元素移动到数组右边 - 分别对
基准
左侧和右侧的元素进行递归排序
-
-
性能分析
-
时间复杂度
- 最好情况时间复杂度:O(NlogN)
- 最坏情况时间复杂度:O(n^2)
- 平均情况时间复杂度:O(NlogN)
-
空间复杂度:
O(logn)
(递归调用消耗) -
稳定性:原地排序、不稳定的算法
-
-
优化性能
-
三数取中法
- 从区间的首、中、尾分别取一个数,然后比较大小,取中间值作为分区点。
- 如果要排序的数组比较大,那“三数取中”可能就不够用了,可能要“5数取中”或者“10数取中”。
-
随机法:每次从要排序的区间中,随机选择一个元素作为分区点。
-
警惕快排的递归发生堆栈溢出
- 限制递归深度,一旦递归超过了设置的阈值就停止递归。
- 在堆上模拟实现一个函数调用栈,手动模拟递归压栈、出栈过程,这样就没有系统栈大小的限制。
-
-
代码示例
// 快速排序入口 function quickSort(arr, left = 0, right = arr.length - 1) { if (arr.length > 1) { const lineIndex = partition(arr, left, right); if (left < lineIndex - 1) { quickSort(arr, left, lineIndex - 1); } if (lineIndex < right) { quickSort(arr, lineIndex, right); } } return arr; } // 以基准值为轴心,划分左右子数组的过程 function partition(arr, left, right) { let pivotValue = arr[Math.floor(left + (right - left) / 2)]; let i = left; let j = right; while (i <= j) { while (arr[i] < pivotValue) { i++; } while (arr[j] > pivotValue) { j--; } if (i <= j) { swap(arr, i, j); i++; j--; } } return i; } function swap(arr, i, j) { [arr[i], arr[j]] = [arr[j], arr[i]]; }
计数排序
开辟额外空间,统计每个元素的数量
-
排序原理
- 计算出待排序序列的最大值 maxValue 与 最小值 minValue
- 开辟一个长度为 maxValue - minValue + 1 的额外空间
- 统计待排序序列中每个元素的数量
- 记录在额外空间中,最后遍历一遍额外空间
- 按照顺序把每个元素赋值到原始序列中
-
性能分析
- 时间复杂度:O ( n + k ) 排序元素是 n 个 0 到 k 之间的整数
- 空间复杂度:O ( n + k ) 计数排序需要两个额外的数组,分别用于记录元素数量与排序结果
- 稳定性:计数排序是稳定的排序算法,但不是原地排序
-
代码示例
function countingSort(nums) { var list = []; var max = Math.max(...nums); var min = Math.min(...nums); for (var i = 0; i < nums.length; i++) { var temp = nums[i]; list[temp] = list[temp] + 1 || 1; } var index = 0; for (var i = min; i <= max; i++) { while (list[i] > 0) { nums[index++] = i; list[i]--; } } return list; }
桶排序
-
排序原理
- 将要排序的数据分到几个有序的桶里
- 每个通在分别进行排序
- 每个桶排序完成后再把每个桶里的数据按照顺序依次取出,组成新的序列
- 该序列就是排好序的序列。类似归并排序中中的分治思想
-
性能分析
-
时间复杂度:桶排序是线性时间排序 O(N)
-
空间复杂度:
- 需要创建M个桶的额外空间
- 以及N个元素的额外空间,
- 桶排序的空间复杂度为 O(N+M)
-
稳定性
- 桶排序是稳定排序
- 如果桶内的排序是选择快速排序,这就不稳定的
- 不是原地排序
-
-
代码示例
function bucketSort(nums) { var num = 5; var max = Math.max(...nums); var min = Math.min(...nums); var range = Math.ceil((max - min) / num) || 1; var arr = Array.from(Array(num)).map(() => Array().fill(0)); nums.forEach((val) => { let index = parseInt((val - min) / range); index = index >= num ? num - 1 : index; let temp = arr[index]; let j = temp.length - 1; while (j >= 0 && val < temp[j]) { temp[j + 1] = temp[j]; j--; } temp[j + 1] = val; }); var res = [].concat.apply([], arr); nums.forEach((val, i) => { nums[i] = res[i]; }); return nums; }
基数排序
-
排序原理
- 以整数排序为例
- 将整数按位数划分,准备N个桶,代表 0 -N
- 根据整数个位数字的数值将元素放入对应的桶中,
- 按照输入赋值到原序列中,依次对十位、百位等进行同样的操作,最终就完成了排序的操作
-
性能分析
-
时间复杂度O(k*(n+m))
-
空间复杂度O(n+m) m 个桶与存放 n 个元素的空间
-
稳定性
- 不会改变相同元素之间的相对位置
- 元素收回的过程中是从后向前进行的
- 所以稳定的排序算法,但不是原地排序
-
-
代码示例
function radixSort(nums) { function getDigits(n) { var sum = 0; while (n) { sum++; n = parseInt(n / 10); } return sum; } var arr = Array.from(Array(10)).map(() => Array()); var max = Math.max(...nums); var maxDigits = getDigits(max); for (var i = 0, len = nums.length; i < len; i++) { nums[i] = (nums[i] + "").padStart(maxDigits, 0); var temp = nums[i][nums[i].length - 1]; arr[temp].push(nums[i]); } for (var i = maxDigits - 2; i >= 0; i--) { for (var j = 0; j <= 9; j++) { var temp = arr[j]; var len = temp.length; while (len--) { var str = temp[0]; temp.shift(); arr[str[i]].push(str); } } } var res = [].concat.apply([], arr); nums.forEach((val, index) => { nums[index] = +res[index]; }); return nums; }
排序函数实现技巧
-
数据量
- 很小,采取用时间换空间的思路
- 很多,优化快排分区点的选择
-
防止堆栈溢出,选择在堆上手动模拟调用栈解决
-
在排序区间中,当元素个数小于某个常数是,可以考虑使用O(n^2)级别的插入排序
-
用哨兵简化代码,每次排序都减少一次判断,尽可能把性能优化到极致