十种经典排序算法
0 算法概述
0.1 算法分类:
排序算法可以分为两大类:
1.比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
2.非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
0.2 算法复杂度:
0.3 相关描述概念:
1.稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
2.不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
3.时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律
4.空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数
1 冒泡排序
1.1 概述:
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.2 冒泡排序关键点:
冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求,如果不满足就让它俩互换。
1.3 图例演示:
1.4 TypeScript代码实现:
class BubbleSort {
// 冒泡排序
public static bubbleSort(a: Array<number>, n: number): Array<number> {
if (n <= 1) return a;
for (let i: number = 0; i < n; i++) {
for (let j: number = 0; j < n - i - 1; j++) { // 每次冒泡都将最大的冒泡到了最后所以只需要n-i-1次相邻比较
if (a[j] > a[j + 1]) { // 相邻比较:小的放前面,大的放后面
let temp: number = a[j]
a[j] = a[j + 1]
a[j + 1] = temp
}
}
}
return a
}
}
2 插入排序
2.1 概述:
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
2.2 插入排序关键点:
从层面上将插入排序将数组数据分成已排序区间和未排序区间。初始已排序区间只有一个元素,即数组第一个元素。在未排序区间取出一个元素插入到已排序区间的合适位置,直到未排序区间为空。
2.3 图例演示:
2.4 TypeScript代码实现:
class InsertionSort {
// 插入排序
public static insertionSort(a: Array<number>, n: number): Array<number> {
if (n <= 1) return a;
// i为每次未排序数组的开始索引(即待插入值索引开始为0而已排序数组开始为空)
for (let i: number = 0; i < n; i++) {
let value: number = a[i] // 记录当前待插入数值
// j为每次已排序数组的结束索引(即是当前待插入数值的前一位索引)
let j: number = i - 1
// j的结束索引大于等于开始索引
for (; j >= 0; j--) {//往后遍历
if (a[j] > value) {
a[j + 1] = a[j] // 大于当前插入值的有序值往后移动
} else {
break // 结束往后移动
}
}
// 当前的插入值就是
a[j + 1] = value
}
return a;
}
}
3 希尔排序
3.1 概述:
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
3.2 希尔排序关键点:
将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,与插入排序的不同之处在于它会优先比较距离较远的元素。
3.3 图例演示:
3.4 步骤说明:
1. 选择一个增量序列t1,t2,…,tk,其中t1>t2>…,tk=1。
2. 按增量序列个数k,对序列进行k趟排序。
3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
3.5 TypeScript代码实现:
class ShellSort {
// 希尔排序
public static shellSort(a: Array<number>, n: number): Array<number> {
// gap记录待插入数组中元素之间的距离也就是偏移量(开始时按两两分组进行待插入排序,排序完成该次分组后则往下分组按gap/2继续执行直到gap小于等于0为止,即最后一次插入排序是对整个数组进行插入操作因为只有1/2才会小于等于0)
for (let gap: number = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i: number = gap; i < n; i++) {
let j: number = i
let value = a[i] // 因为偏移的索引指向的就是插入排序数组中的未排序当前值
while (j - gap >= 0 && value < a[j - gap]) { // 未排序当前值大于当前插入数组的有序值往后移动
a[j] = a[j - gap]
j = j - gap
}
a[j] = value
}
}
return a;
}
}
4 选择排序
4.1 概述:
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
4.2 选择排序关键点:
从层面上将数组分成已排序区间和未排序区间。初始已排序区间为空。每次从未排序区间中选出最小的元素插入已排序区间的末尾,直到未排序区间为空。
4.3 图例演示:
4.4 TypeScript代码实现:
class SelectionSort {
// 选择排序
public static selectionSort(a: Array<number>, n: number): Array<number> {
if (n <= 1) return a;
for (let i: number = 0; i < n - 1; i++) {
let minIndex: number = i
let j: number = i + 1
for (; j < n; j++) { // 往后搜索最小值的索引
if (a[minIndex] > a[j]) { // 当前值比最小值还要小
minIndex = j // 设置当前值的索引为最小值索引
}
}
if (minIndex != i) { // 如果最小值索引改变,则进行交换
let temp: number = a[i]
a[i] = a[minIndex]
a[minIndex] = temp
}
}
return a;
}
}
5 归并排序
5.1 概述:
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
5.2 归并排序关键点:
先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
5.3 图例演示:
5.4 步骤说明:
1.把长度为n的输入序列分成两个长度为n/2的子序列;
2.对这两个子序列分别采用归并排序;
3.将两个排序好的子序列合并成一个最终的排序序列;
5.5 TypeScript代码实现:
class MergeSort {
// 归并排序算法
public static mergeSort(a: Array<number>, n: number): Array<number> {
if (n <= 1) return a;
MergeSort.mergeSortInternally(a, 0, n - 1) // 开始索引为0,结束索引为n-1
return a;
}
// 递归调用函数(p为开始索引,r为结束索引)
private static mergeSortInternally(a: Array<number>, p: number, r: number): void {
// 递归终止条件(开始索引已经大于或等于结束索引)
if (p >= r) return;
// 取p到r之间的中间位置q
let q: number = Math.floor((p + r) / 2);
// 分治递归
MergeSort.mergeSortInternally(a, p, q);
MergeSort.mergeSortInternally(a, q + 1, r);
// 将A[p...q]和A[q+1...r]合并为A[p...r]
MergeSort.merge(a, p, q, r);
}
// 合并左右数组(p为开始索引,q为中点索引,r为结束索引)
private static merge(a: Array<any>, p: number, q: number, r: number): void {
// 初始化变量i, j, k
let i: number = p;
let j: number = q + 1;
let tmp: Array<number> = []; // 申请一个临时数组
let k: number = 0; // 记录临时数组的开始索引
/**
* 合并关键四步骤(1.使用新建数组依次存储两个数组两两对比的最小值2.如果两两数组中只有一边已遍历完则还剩将剩下的放入到新建数组中去3.将新建数组已经排序好的值依次覆盖原数组上)
*/
// 1 排序
while (i <= q && j <= r) { // 左右数组的开始索引未曾超过结束索引的位置
if (a[i] <= a[j]) {// 当前左边数组值的小于等于右边数组值
tmp[k++] = a[i++]; // 左边值赋给tmp,并且左边数组和tmp数组的开始索引开始往下移动
} else {// 否则当前右边的小于等于左边的
tmp[k++] = a[j++]; // 右边值赋给tmp,并且右边数组和tmp数组的开始索引开始往下移动
}
}
// 2 判断哪个子数组中有剩余的数据,将剩余的数据拷贝到临时数组tmp
let start: number = i; // 开始索引记录为当前左边数组的开始索引
let end: number = q; // 结束索引记录为当前左边数组的结束索引
if (j <= r) { // 如果右边数组的开始索引小于等于结束索引(表明i<=q一定为false,即左边的已经遍历完了)
start = j; // 开始索引记录为当前右边数组的开始索引
end = r; // 结束索引记录为当前右边数组的结束索引
}
while (start <= end) {
tmp[k++] = a[start++];
}
// 3 将tmp数组内容拷贝回a[p...r]
for (i = 0; i <= r - p; ++i) {
a[p + i] = tmp[i];
}
}
}
6 快速排序
6.1 概述:
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
6.2 快速排序关键点:
如果要排序数组中下标从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为pivot (分区点) 。我们遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将pivot放到中间。经过这一步骤之后,数组p到r之间的数据就被分成了三个部分,前面p到q-1之间都是小于pivot的,中间是pivot,后面的q+1到r之间是大于pivot的。
6.3 图例演示:
6.4 步骤说明:
1.从数列中挑出一个元素,称为 “基准”(pivot);
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
6.5 TypeScript代码实现:
class QuickSort {
// 快速排序
public static quickSort(a: Array<number>, n: number): Array<number> {
let start: number = 0 // 开始索引
let end: number = n - 1 // 结束索引
if (start >= n) return a;
// 计算中间索引
let q: number = Math.floor((start + end) / 2)
let left: Array<number> = [] // 记录小于中间索引值的左边数组
let right: Array<number> = [] // 记录大于中间索引值的右边数组
let l: number = 0, r: number = 0
for (let i: number = 0; i < a.length; i++) {
if (i == q) continue;
if (a[i] < a[q]) {
left[l++] = a[i]
} else {
right[r++] = a[i]
}
}
// 分治递归
return QuickSort.quickSort(left, left.length).concat([a[q]], QuickSort.quickSort(right, right.length))
}
}
7 堆排序
7.1 概述:
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
7.2 堆排序关键点:
堆的排序是堆建立完成之后进行的,假设数组存储成大根堆的形式之后,第一次将A[0]与A[n-1]交换,然后将A[0~n-2]数组进行堆恢复操作(做堆调整)。第二次将A[0]与A[n-2]交换,然后将A[0~n-3]数组进行堆恢复操作(做堆调整)。重复这样的操作,直到A[0]与A[1]交换,排序结束。
7.3 图例演示:
7.4 步骤说明:
1.将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
2.将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
3.由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成;
7.5 TypeScript代码实现:
class HeapSort {
public static heapSort(a: Array<number>, n: number): Array<number> {
// 初始化大顶堆,从第一个非叶子结点开始
for (let i = Math.floor(n / 2 - 1); i >= 0; i--) {
HeapSort.shiftDown(a, i, n);
}
// 排序,每一次for循环找出一个当前最大值,数组长度减一
for (let i = Math.floor(n - 1); i > 0; i--) {
// 根节点与最后一个节点交换
let temp: number = a[0]
a[0] = a[i]
a[i] = temp
HeapSort.shiftDown(a, 0, i); // 从根节点开始调整,并且最后一个结点已经为当
// 前最大值,不需要再参与比较,所以第三个参数
// 为 i,即比较到最后一个结点前一个即可
}
return a;
}
private static shiftDown(a: Array<number>, i: number, n: number): void {
let temp: number = a[i]; // 当前父节点
// j<n 的目的是对结点 i 以下的结点全部做顺序调整
for (let j: number = 2 * i + 1; j < n; j = 2 * j + 1) {
temp = a[i]; // 将 A[i] 取出,整个过程相当于找到 A[i] 应处于的位置
if (j + 1 < n && a[j] < a[j + 1]) {
j++; // 找到两个孩子中较大的一个,再与父节点比较
}
if (temp < a[j]) {
// 如果父节点小于子节点:交换;否则跳出
let temp: number = a[i]
a[i] = a[j]
a[j] = temp
i = j; // 交换后,temp 的下标变为 j
} else {
break;
}
}
}
}
8 计数排序
8.1 概述:
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
8.2 计数排序关键点:
利用数组中的索引是天然有序的特征来进行排序。
8.3 图例演示:
8.4 步骤说明:
1.找出待排序的数组中最大和最小的元素;
2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1;
8.5 TypeScript代码实现:
class CountSort {
public static countSort(a: Array<number>, n: number): Array<number> {
// 初始化统计数组长度为其里面最大元素+1的值并设置所有元素值为0
let count_arr: Array<number> | null = new Array(n).fill(0)
// 统计数组中每个元素出现的次数
for (let value of a) {
count_arr[value]++;
}
a = []
// 重新赋值给数组a(该过程相当于排序赋值)
for (let i: number = 0; i < count_arr.length; i++) {
// 循环数字次数
for (let j: number = count_arr[i]; j > 0; j--) {
a.push(i)
}
}
count_arr = null
return a;
}
}
9 桶排序
9.1 概述:
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)
9.2 桶排序关键点:
将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了
9.3 图例演示:
9.4 步骤说明:
1.设置一个定量的数组当作空桶;
2.遍历输入数据,并且把数据一个一个放到对应的桶里去;
3.对每个不是空的桶进行排序;
4.从不是空的桶里把排好序的数据拼接起来;
9.5 TypeScript代码实现:
class BucketSort {
// 桶排序
public static bucketSort(a: Array<number>, k: number, s: number): Array<number> {//A是排序数组,k是桶子数量,s是桶子空间尺度即每个桶容量
// 创建桶
let buckets: Array<any> = Array.from({ length: k }, () => [])
// 将元素放入对应桶子
for (let i: number = 0; i < a.length; i++) {
// 计算需要放入桶子序号(即利用映射函数将数据分配到各个桶中)
let idx: number = ~~(a[i] / s)
buckets[idx].push(a[i])
}
// 对每个桶子进行排序
for (let i: number = 0; i < buckets.length; i++) {
// 此处选取插入排序节省空间
InsertionSort.insertionSort(buckets[i], buckets[i].length)
}
// 将每个桶子数据进行合并返回
return [].concat(...buckets)
}
}
10 基数排序
10.1 概述:
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前
10.2 基数排序关键点:
按照低位先排序后收集再按照高位排序再后再收集…依次类推直到最高位最终完成排序
10.3 图例演示:
10.4 TypeScript代码实现:
class RadixSort {
public static radixSort(a: Array<number>, n: number): Array<number> {
//定义一个二维数组,表示10个桶,每个桶就是一个一维数组
//说明
//1,二维数组包含10个一维数组,
//2.为了防止在放入数的时候,数据溢出,则每个一维数组(桶)
//大小定为n
//3.很明确,基数排序是使用空间换时间的经典算法
let bucket: Array<Array<number>> = new Array(10);
for (let i: number = 0; i < bucket.length; i++) {
bucket[i] = new Array(n);
}
//为了记录每个桶中,实际存了多少个数据,我们定义一个
//一维数组来记录每个桶的每次放入的数据个数
//可以这里理解
//比如:bucketElementCounts[0],记录的就是bucket[0]桶的放入数据个数
let buckeElementCounts = new Array(10).fill(0);
//1.得到数组中最大的位数
let max = a[0];
for (let i = 1; i < n; i++) {
if (a[i] > max) {
max = a[i]
}
}
//得到最大是几位数
let maxLength: number = (max + '').length;
for (let i = 0, n = 1; i < maxLength; i++, n = n * 10) {
//每一轮,对每个元素的各个位数进行排序处理,
//第一次是个位,第二次是十位,第三次是百位
for (let j = 0; j < n; j++) {
//取出每个元素的各位的值
let digitOfElement = Math.floor(a[j] / n) % 10;
bucket[digitOfElement][buckeElementCounts[digitOfElement]] = a[j];
buckeElementCounts[digitOfElement]++;
}
//按照这个桶的顺序(以为数组的下标依次取出数据,放入原来数组)
let index = 0;
//遍历每一桶,并将桶中的数据,放入原数组
for (let k = 0; k < buckeElementCounts.length; k++) {
//如果桶中有数据,我们才放入原数组
if (buckeElementCounts[k] !== 0) {
//循环该桶即第k个桶,即第k个一维数组,放入
for (let l = 0; l < buckeElementCounts[k]; l++) {
//取出元素放入a
a[index] = bucket[k][l];
//a下标后移
index++;
}
//每轮处理后,下标要清0
buckeElementCounts[k] = 0;
}
}
}
return a
}
}