- 七大常用排序算法
- 冒泡排序
- 插入排序
- 希尔排序
- 快速排序
- 堆排序
- 归并排序
- 桶排序
- 总结
前言: 在学习这些排序算法前我们都应该了解一些关于时间 复杂度和 空间复杂度的相关知识了,下面简略的介绍一下,顺便在提一下一个新的性质来衡量算法的标准 稳定性
-
时间复杂度:对于排序算法来说,就是随着排序规模的增加,排序时间增加的速度满足的一个函数关系,这个函数关系是不包括函数的低阶项,和最高项前面的系数的
–
tips: 递归的O()
注意:子问题需要相同$ T(N)=a*T(N/b)+O(N^d) $
T(N) T(N/b) a O(N^d) 母问题 子问题的规模 子问题被调用次数 出了递归调用子问题的时间复杂度 -
l o g b a < d log_b a l o g b a > d log_ba>d logba>d l o g b a = d log_ba=d logba=d O ( N d ) O(N^d) O(Nd) O ( N l o g b a ) O(N^{log_ba}) O(Nlogba) ( N b ∗ l o g N ) (N^b*log N) (Nb∗logN)
-
空间复杂度:随着排序规模的正价,排序所需要的空间的函数关系,他与定义的变量与数组指针等有关系,可以非常直观的看出,这个函数关系同样是不包括函数的低阶项,和最高项前面的系数的
-
稳定性:就是在排序的过程中不改变相同元素的次序,那么怎么来理解呢?
这对于基础数据的作用作为衡量指标意义不是特别大,但是对于对象等在实际的应用中应用非常广泛,例如:当我们在淘宝购物的时候,我们会先以好评率排序然后按照价格排序,这样我们就可以得到物美价廉的商品,但是如果两个排序都没有稳定性,这是无法实现的,就算实现了,所需要的时间和空间开销也是非常恐怖的了。
冒泡排序应该是我们接触的第一个排序,排序的过程就像水里的气泡一样越向水面气泡越大,这个是非常经典的排序算法这里以升序为例,降序不过是吧水面和水底进行了换位置,虽然这在现实生活中是违背物理规律的 但是水面和水底只是一个形象的比喻而已
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( N 2 ) O(N^2) O(N2) | O ( 1 ) O(1) O(1) | √ |
每一次遍历就可以把最大的数放到追后面
//交换两个数 void swap(int *a,int* b) { int c=*a; *a=*b; *b=c; } //排序算法 void BubbleSort(int *arr,int length) { for(int i=0;i插入排序arr[x+1]) { swap(&arr[x],&arr[x+1]); } } } }
思想:
插入排序相当于摸纸牌,每次摸一张把他插到相应的位置上。
每一次插入一个新的牌和前面的牌进行比较,如果一直到不在比前一张大结束或者到数组的开始结束。
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( N 2 ) O(N^2) O(N2) | O ( 1 ) O(1) O(1) | √ |
void swap(int* a,int *b) { int c=*a; *a=*b; *b=c; } void insertsort(int *arr,int length) { for(int i=0;i0;x--) { if(arr[x] 希尔排序
- 思想:
希尔排序相当于插入排序的一个优化,插入排序每次遍历的步长为一,而希尔排序是把序列按照下标进行分类,然后进行插入排序的算法,这可以形象的看成每一次遍历的步长为gap,gap要视元素的个数而定,可以更快的让较大的数跑到后面,然后每次让gap缩小,缩小到1后就成了完完全全的插入排序或者更小的数跑到前面,希尔排序的时间复杂度比较复杂,直接记着就好
时间复杂度 空间复杂度 稳定性 O ( N 1.2 ) O(N^{1.2}) O(N1.2)~ O ( N 1.5 ) O(N^{1.5}) O(N1.5) O ( 1 ) O(1) O(1) ×
下面举一个例子:
如果gap=3
那么下标的分组为:0 3 6 9 1 4 7 10 2 5 8 11对每一组进行插入排序后:
gap=gap/2//此时gap=1也就是插入排序然后变为:
排序完成
下面给一个动图的例子
注意事项:
1. gap每次缩小应该能保证最后缩小到1进行插入排序不然排序过程无法完成,用每次除2一定最后可以缩小得到1 如果要除3可能得不到1 6/3=2 2/3=0 导致排序无法完成 可以gap=gap/3+1来进行保证
2. gap的选取一定要合理不然无法达到特别明显的速度提升还损失稳定性得不偿失
代码实现:
void pai(int *a,int n)//希尔排序 { int gap=n; while(gap>1) { gap=gap/2; for (int i = 0; i < n-gap; i++) { int end=i; int gold=a[end+gap]; while(end>=0) { if(a[end]>gold) { a[end+gap]=a[end]; end=end-gap; } else { break; } } a[end+gap]=gold; } } }快速排序
- 快速排序思想
快排产生于荷兰国旗问题,他是找寻一个目标值让这个目标值左边的数都比他小,右边的数都比他大,这样便找到了这个数原来应该存在的位置,然后递归调用拓展到数组的每一个元素便实现了排序,听着很简单对吧,但是千万别大意啊,这个排序的应用非常广泛,实现也有一些需要闭坑的点。传统意义的快排时间复杂度 O ( N 2 ) O(N^2) O(N2)空间复杂度 O ( l o g N ) O(log_N) O(logN)经过改进后时间复杂度也是比较难求的
时间复杂度 空间复杂度 稳定性 O ( N ∗ l o g N ) O(N*log_N) O(N∗logN) O ( l o g N ) O(log_N) O(logN) ×
举一个具体的例子,拿数组最右边的值当做目标值,把他当做右边界(大于区)大于区的扩增是指针–,左边界为-1:
- 当i指针指向的值大于目标值的时候与大于区的下一个元素进行交换,右边界–;
- 当i指针指向的值等于目标值的时候 i++;
- 当i指向的值小于目标值与小于区前一个元素进行交换小于区扩张 i++;
- 最后把数组最右边的元素和大于区的前一个元素进行交换 右边界++就可以了
.
.
.
最后一定要返回左右边界的指针
中间省略了一点图,目标值一定要随机进行选择优化,不要固定为最后一个值不然有一定几率 O ( N 2 ) O(N^2) O(N2)。代码实现:
srand(time(NULL) + rand());//产生随机数 swap(arr, l + rand() % (r - l + 1), r);//把随机数放到最后//快排 void swap(int* arr, int l, int r) { int c = arr[l]; arr[l] = arr[r]; arr[r] = c; //printf("arr[%d]=%d arr[%d]=%dn",l,arr[l],r,arr[r]); } int* partition(int* arr, int l, int r) { int less = l - 1;//小于目标值的左边界 int more = r;//大于目标值的右边界 while (l < more) { if (arr[l] < arr[r]) { swap(arr, ++less, l++); } else if (arr[l] > arr[r]) { swap(arr, l, --more); } else if (arr[l] == arr[r]) { l++; } } swap(arr, more++, r); int* p = (int*)malloc(sizeof(int) * 2); p[0] = less + 1; p[1] = --more; return p; } void quicksort(int* arr, int l, int r) { if (l == r || !arr) { return; } if (l < r) { srand(time(NULL) + rand());//产生随机数 swap(arr, l + rand() % (r - l + 1), r); //printf("arrx[%d]=%dn",r,arr[r]); int* px = (int*)malloc(sizeof(int) * 2); px = partition(arr, l, r); //printf("p[0]=%d p[1]=%d n",p[0],p[1]); quicksort(arr, l, px[0] - 1); quicksort(arr, px[1] + 1, r); } }堆排序在讲解堆排序之前我们应该了解的两种二叉树就是什么是满二叉树什么是完全二叉树
完全二叉树:设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,
第 h 层所有的结点都连续集中在最左边满二叉树:深度为k且有2^k-1个结点的二叉树称为满二叉树
而我们所说的堆排序就需要构建完全二叉树(用数组的方式进行构建)下面给出两个重要的关系:
假设父亲的下标为n
当然这个父亲节点不可以为叶子节点
- 左孩子=2n+1
- 右孩子=2n+2
完全二叉树的前n-1层的节点个数一定是 1 + 2 1 + 2 2 ∗ … … 2 n − 1 1+2^1+ 2^2*……2^n-1 1+21+22∗……2n−1
堆分为大根堆和小根堆
- 最大堆(大根堆):根结点的键值是所有堆结点键值中最大者。
- 最小堆(小根堆):根结点的键值是所有堆结点键值中最小者。
排升序要用小根堆,排降序要用大跟堆
堆排序的核心是向下调整算法
就是一个二叉树的左右子树都满足大根堆或者是小根堆
就以 小根堆为例: 要保证是自己是小根堆前提要自己的孩子为小根堆,这显然是一个递归问题 然后把堆顶的元素和最后一个元素交换然后把最后一个元素t出推因为堆顶的元素一定大于或者小于堆中的任意一个元素,继续进行调整就可以了算法思路: 1 .先调最底的树 堆的长度/2就是最后一个元素的父亲 左孩子和右孩子的最大值成为与父节点的值进行交换领整个数组成为一个小根堆或者大根堆 2 .在每一次堆顶的元素和堆底元素交换后调整堆,因为交换前,堆顶元素的左孩子和右孩子也一定为小根堆或者都为大根堆,调整一次即可重复这个过程
时间复杂度 空间复杂度 稳定性 O ( N ∗ l o g N ) O(N*log_N) O(N∗logN) O ( 1 ) O(1) O(1) × void swap(int*a ,int*b) { int c=*a; *a=*b; *b=c; } void just(int*a,int root,int n) { int child=2*root+1;; int parent=root; while(2*parent+1归并排序=0;i--) { just(a,i,n); } int end=n-1; while(end>0) { swap(&a[0],&a[end]); just(a,0,end); end--; } } 先让一个数组的左边有序,数组的右边有序,然后在让一个数组的右边有序,然后再让整个数组有序就可以了,这显然也是一个递归调用的问题,每一次都把数组分成两部分重复解决这个问题数组的右边有序,然后在让一个数组的右边有序,然后再让整个数组有序就可以了这个排序不像堆排序那样好理解让我们看图解,同时应该看着代码看着图进行理解
时间复杂度 空间复杂度 稳定性 O ( N ∗ l o g N ) O(N*log_N) O(N∗logN) O ( N ) O(N) O(N) √
那么每一次递归如何让他有序呢?
代码实现:
void merge(int *arr,int L,int M,int R) { int *help=(int*)malloc(sizeof(int)*(R-L+1)); int i=0; int p1=L; int p2=M+1; while (p1<=M&&p2<=R) { help[i++]=arr[p1]<=arr[p2]?arr[p1++]:arr[p2++]; } while (p1<=M) { help[i++]=arr[p1++]; } while (p2<=R) { help[i++]=arr[p2++]; } for(int i=0;i桶排序>1); //merge(arr,L,mid,R); process(arr,L,mid); process(arr,mid+1,R); merge(arr,L,mid,R); } 这个排序有点特殊需要具体问题具体分析,需要开一个数组和原来数组元素类型数量一样,然后对每一个元素进行计数,不在详细讲
总结
看图理解就好
排序 时间复杂度 空间复杂度 稳定性 冒泡 O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1) √ 插入排序 O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1) √ 希尔排序 O ( N 1.2 ) O(N^{1.2}) O(N1.2)~ O ( N 1.5 ) O(N^{1.5}) O(N1.5) O ( 1 ) O(1) O(1) × 快速排序 O ( N ∗ l o g N ) O(N*log_N) O(N∗logN) O ( l o g N ) O(log_N) O(logN) × 堆排序 O ( N ∗ l o g N ) O(N*log_N) O(N∗logN) O ( 1 ) O(1) O(1) × 归并排序 O ( N ∗ l o g N ) O(N*log_N) O(N∗logN) O ( N ) O(N) O(N) √