八大排序 [建议收藏 !!!]
一. 插入排序
1.1 直接插入排序
1.1.1基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
1.1.2 代码实现
// 一下代码均以c语言实现 。
void InsertS ort(int *a, int n)
{
//第一个元素已经有序,故从下标为1的元素开始排序
for (int i = 1; i < n; ++i) {
int tmp = a[i];
int j = i ;
for (; j > 0; --j) {
//如果出现逆序,即插入的数小于前面的数,就往后移动一位
if (j - 1 >= 0 && a[j - 1] > tmp) a[j] = a[j - 1];
// 否则就跳出循环, 插入到正确的位置
else break ;
}
a[j] = tmp j;
}
}
1.1.3复杂度分析
最好情况就是已经有序, 也就是每次元素往前插入数据时都不需要移动,故时间复杂度为O(n) 。
最坏情况就是降序, 每次插入都会出现逆序,移动的次数依次为1 , 2 , 3 ,4 … n - 1次 , 有求和公式可得(n - 1 + 1 ) * (n - 1) / 2 , 时间复杂度为O(n ^ 2) ;
空间复杂度为O(1) ,它是一种稳定的排序。
1. 2 希尔排序( 缩小增量排序 )
1.2.1基本思想
在上面的直接插入排序中我们会发现:
1.普通插入排序的时间复杂度最坏情况下为O(n ^ 2),此时待排序列为逆序,或者说接近逆序。
2.普通插入排序的时间复杂度最好情况下为O(n),此时待排序列为升序,或者说接近升序。
于是希尔这个科学家就想:若是能先将待排序列先进行一次预排序,使待排序列接近有序,然后再对该序列进行一次直接插入排序,那么这时候的时间复杂度就会接近O(n)。
1.2.2代码实现
void ShellSort(int *a, int n) {
int gap = n;
while (gap > 1) {
gap = gap / 3 + 1; // 保证最后一次的增量为1 ,即直接排序
// int gap = n / 2 ; 这里每次的增量也可以缩小一倍。
for (int i = gap; i < n; i++) {
int tmp = a[i];
int j = i
for (; j > 0; j -= gap) {
if (a[j - gap] > tmp) a[j] = a[j - gap];
else {
break;
}
a[j] = tmp;
}
}
}
}
1.2.3 复杂度分析
时间复杂度: O(n ^ 1.3) , 记住即可。
空间复杂度: O(1) 。
稳定性: 希尔排序不稳定 , 很容易造成相同元素的相对位置发生改变 。
二. 选择排序
2.1 简单选择排序
2.1.1 基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
2.1.2 代码实现
void SelectSort(int *a, int n) {
for (int i = 0; i < n - 1; ++i) {
// 假设当前为最小的元素 。
int min = i;
for (int j = i + 1; j < n; ++j) {
// 如果有比min还要小的元素, 则重新给min赋值 。
if (a[j] < a[min]) min = j;
}
if (min != i) swap(&a[min], &a[i]);
}
}
2.1.3 复杂度分析
不管最好还是最坏 ,其时间复杂度均为O(n ^ 2 ) 。
空间复杂度: O(1) 。
稳定性 : 简单选择排序不稳定。
2.2堆排序
2.2.1基本思想
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
那么如何建堆? 我们叫向下调整算法 。
向下调整算法建堆的条件: 1 . 首先左右子树一定是堆(这里默认大堆)
2.若双亲结点小于孩子节点, 则与左右孩子中最大的进行交换,也就是向下调整;重复以上操作。反之,不做处理。
2.2.2 代码实现
void adjustDown(int* a ,int n , int parent)
{
int child = parent * 2 + 1 ;
while(child < n )
{
// 比较左右子节点, 选择更大的那个节点。
if(child + 1 < n && a[child] < a[child+1] ) child++ ;
if(a[parent] < a[child])
{
swap[&a[parent] , &a[child]) ;
parent = child ;
child = child * 2 + 1 ;
}
// 若双亲结点大于孩子节点, 就跳出向下调整 。
else break ;
}
}
void HeapSort(int *a, int n) {
// 向下调整建大堆 , 从第一个非终端节点开始调整工。
for(int i = n - 1 - 1 >> 1; i >= 0; --i) AdjustDown(a, n, i);
//依次选出最大的元素,将其放到序列最后。
int end = n - 1;
while (end >= 0) {
swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
2.2.3 复杂度分析
时间复杂度 ; O (nlogn) 。
空间复杂度 : O (1) 。
稳定性: 堆排序是一种不稳定的排序算法。
三. 交换排序
3.1 冒泡排序
3.1.1 基本思想
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
3.1.2 代码实现
void bubbleSort(int* a ,int n)
{
//一般情况比较 n - 1 , 从0 到 n - 2 。
for(int i = 0 ; i < n - 1 ; ++ i )
{
//定义一个标志,表示是否逆序而交换 , 默认未交换0
int flag = 0 ;
// 从0 比较到 n - i - 2 , 每次都要选出一个最值到最终正确的位置。 故经过每一趟的排序,比较次数也会相对应的减少 。
for(int j = 0 ; j < n - i - 1 ; ++ i )
{
//出现逆序, 则交换两数位置, 将flag置为1 )
if(a[j] > a[j+1])
{
swap(&a[j] , &a[j+1] ;
flag = 1 ;
}
}
// 如果flag = 0 , 则表示未交换, 序列已经有序, 跳出循环。
if(flag == 0 ) break ;
}
}
3.1.3 复杂度分析
最好时间复杂度: 经过一趟试探性的排序, 在有序的情况下未经交换,break出循环,故时间复杂度为O(n) ;
最坏时间复杂度: 降序的情况, 每趟排序都要交换, 故时间复杂度为O(n ^ 2 ) .
稳定性: 冒泡排序是一种稳定的排序 。
3.1 快速排序
3.1.1 基本思想
任 取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
3.1.2 代码实现
方法一 : 双指针写法
void quickSort(int* a ,int l , int r)
{
if(l >= r ) return ;
int x = a[l + r >> 1 ] ;
int i = l - 1 , j = r + 1 ;
while(l < r)
{
do i ++ ; while(a[i] < x ) ;
do j -- ; while(a[j] > x ) ;
if(l < r) swap(&a[i] , &a[j]) ;
}
//此写法并没有将x放到它的最终位置, 只是简单的把数组分为啦小于等于x 和 大于等于x 两部分。
quickSort(a , l , j ) ;
quickSort(a , j+1 , r ) ;
}
方法二:当数据量大的话容易发生超时 。
void quickSort(int* a ,int l , int r )
{
if(l >= r) return ;
int tmp = a[l] ;
int j = l ;
//将序列分成两组,小于tmp 和大于等于 tmp的两个序列
for(int i = l + 1 ; i <= r ; ++ i)
if(tmp > a[i]) swap(a[++j] , a[i] ) ;
//将中间值放置到最终位置
swap(&a[l],&a[j]) ;
quickSort(a , l , j - 1) ;
quickSort(a, j + 1 , r ) ;
}
3.1.3 复杂度分析
最好时间复杂度: 也就是乱序的时候时间复杂度是O(nlogn) ;
最坏时间复杂度: 接近有序的情况下, 一直选择最小或最大的值作为中间值, 致使时间复杂度为o(n ^ 2 ) ; 稳定性: 快排是一种不稳定的排序。
四. 归并排序
4.1.1 基本思想
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
4.1.2 代码实现
递归写法:
void _mergeSort(int* a ,int l , int r , int* tmp)
{
assert(a) ;
//如果只有一个数或者区间不存在,即返回。
if(l >= r) return ;
int mid = l + r >> 1 ;
_mergeSort(a , l , mid , tmp ) ;
_mergeSort(a , mid+1 , r , tmp ) ;
int index = 0 ;
int i = l , j = mid + 1 ;
//将两个有序的子序列进行归并
while(i <= mid && j <= r )
{
if(a[i] < a[j])tmp[index++] = a[i] ;
else tmp[inde++] = a[j] ;
}
while(i <= mid) tmp[index++] = a[i] ;
while(j <= r) tmp[index++] = a[j] ;
//写回到原数组
for(int i = 0 ; i < index ; ++ i ) a[l++] = tmp[i] ;
}
void mergeSort(int*a , int n )
{
assert(a) ;
// 创建临时数组存放临时数据
int* tmp = (int*) malloc( sizeof(int)) ;
_mergeSort(a , 0 , n - 1 , tmp) ;
free(tmp) ;
tmp = NULL ;
}
4.1.3 复杂度分析
时间复杂度: O(nlogn) .
空间复杂度: O(n) .
稳定性: 归并排序是一种稳定排序算法。
五. 计数排序
5.1.1 基本思想
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
5.1.2 代码实现
void countSort(int* a ,in n )
{
int min = a[0] , max = a[0] ;
// 求出这组数据的范围
for(int i = 1 ; i < n ; ++ i )
{
if(a[i] < min) min = a[i] ;
if(a[i] > max) max = a[i] ;
}
int range = max - min + 1 ;
// 开辟适合大小的数组空间大小
int* tmp = (int*)calloc(range , sizeof(int) ) ;
//统计相同元素出现的个数 , 将元素值减去最小值后对应到相对应的下标。
for(int i = 0 ;i < n ; ++ i )
tmp[a[i] - min] ++ ;
// 根据统计结果将序列收到原来的序列中
int k = 0 ;
for(int i = 0 ; i < range ; ++ i )
while(tmp[i]--) a[k++] = i + min ;
}
5.1.3 复杂度分析
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)