十大排序算法
稳定性作用
我们平时自己在使用排序算法时用的测试数据就是简单的一些数值本身。没有任何关联信息。这在实际应用中一般没太多用处。实际应该中肯定是排序的数值关联到了其他信息,比如数据库中一个表的主键排序,主键是有关联到其他信息.另外比如对英语字母排序,英语字母的数值关联到了字母这个有意义的信息。
可能大部分时候我们不用考虑算法的稳定性。两个元素相等位置是前是后不重要。但有些时候稳定性确实有用处.它体现了程序的健壮性.比如你网站上针对最热门的文章或啥音乐电影之类的进行排名.由于这里排名不会像我们成绩排名会有并列第几名之说.所以出现了元素相等时也会有先后之分。如果添加进新的元素之后又要重新排名了。之前并列名次的最好是依然保持先后顺序才比较好。
算法分类
分类1:
非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
分类2
- 内排序:所有排序在内存中完成
- 外排序:由于数据太大,因此数据以及排序在磁盘中进行
复杂度总结
随便找了个表,希尔排序那里不太准确
一、 冒泡排序
1、 动图演示
2、 代码实现
public class Bubble {
//冒泡算法 -- 交换排序
//加标志位判断是否已经排好序
public static void sort(int[] list) {
boolean flag = true;
for (int j = list.length - 1; j > 0; j--){
for (int i = 0; i < j; i++) {
if (list[i] > list[i + 1]) {
int temp = list[i];
list[i] = list[i + 1];
list[i + 1] = temp;
flag = false;
}
}
if(flag) break;
}
}
public static void main(String[] args) {
int[] list = { 3, 12, 4, 5, 3, 7, 2 };
sort(list);
System.out.println("Bubble Sorted : "+Arrays.toString(list));
}
}
3、算法分析
- 最佳情况:O(n)(加标志位,若已经排好序直接退出)
- 最坏情况:O(n2)
- 平均情况:O(n2)
- 稳定性:稳定
二、 选择排序
1、 动图演示
冒泡进化:不用每次交换,【选择】较小值交换即可
2、 代码实现
//简单选择排序 -- 选择排序
//冒泡进化:不用每次交换,【选择】较小值交换即可
public static void sort(int[] list) {
for(int i = 0;i < list.length;i++){
int minIndex = i;
for(int j = i+1;j < list.length;j++)
if(list[j] < list[minIndex])
minIndex = j;
int temp = list[i];
list[i] = list[minIndex];
list[minIndex] = temp;
}
}
3、算法分析
- 最佳情况:O(n2)
- 最坏情况:O(n2)
- 平均情况:O(n2)
- 稳定性:不稳定,可能被动与其他较大值交换,相同元素前后位置改变
三、 插入排序
1、 动图演示
2、 代码实现
//直接插入排序 -- 插入排序
public static void sort(int[] list) {
for(int i = 1;i < list.length;i++){
int j = i-1,temp = list[i];
while(j >= 0 && temp < list[j]){
//向后移动
list[j+1] = list[j];
j--;
}
//放到合适位置
list[j+1] = temp;
}
}
3、算法分析
- 最佳情况:O(n) (while内不执行)
- 最坏情况:O(n2)
- 平均情况:O(n2)
- 稳定性:稳定
四、 希尔排序
- 简单插入高效改进版本
- 分组 + 插入排序 + 组数递归
- 类似归并,分组数变化相反,“缩小增量排序”
1、 动图演示
2、代码实现
public static void sort(int[] list) {
for(int gap = list.length / 2;gap > 0;gap /= 2){
for(int i = gap;i < list.length;i++){
//某个分组上插入排序
//和前面简单插入写法不同:简单插入是前后交换(复杂),Shell是往后移动
int temp = list[i],pre = i - gap;
while(pre >= 0 && list[pre] > temp){
list[pre+gap] = list[pre];
pre -= gap;
}
list[pre+gap] = temp;
}
}
}
3、算法分析
- 一般情况:O(n2)(亚)
- 其他情况:1. Hibbard的增量序列:1,3,7,15… 2k-1最坏时间复杂度是O(n(1.5))
2. Sedgewick的增量序列:1, 5, 19, 41, 109… 94^k - 92^k + 1 或者 4^k - 32^k + 1,最坏时间复杂度是O(n(4/3))* - 稳定性:不稳定。由于多次插入排序,一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
五、 归并排序
- 分治算法(Divide and Conquer)
- N-路归并
1、 动图演示
https://i-blog.csdnimg.cn/blog_migrate/a6bc359bf1406223e2925a5056ccd085.gif
2、 代码实现
- 1.将给定的数组一份为二
- 2.对两部分数组再使用归并排序使其有序
- 3.最后再将两部分数组合并保存临时数组(可全局,也可在每次递归时新建动态长度数数组)
public static int[] sort(int[] list) {
if (list.length < 2)
return list;
int mid = list.length / 2;
int[] left = Arrays.copyOfRange(list, 0, mid);
int[] right = Arrays.copyOfRange(list, mid, list.length);
//包含递归分组
return merge(sort(left),sort(right),list.length);
}
public static int[] merge(int[] left,int[] right,int len) {
int[] temp = new int[len];
int i = 0,j = 0,t = 0;
while(t < len){
if(i == left.length)
temp[t++] = right[j++];
else if(j == right.length)
temp[t++] = left[i++];
else if(left[i] < right[j])
temp[t++] = left[i++];
else
temp[t++] = right[j++];
}
return temp;
}
3、算法分析
推导:归并排序复杂度推导
- 时间复杂度:O(nlog2n)
- 空间复杂度:O(n) (临时的数组和递归时压入栈的数据占用的空间:n + logn)
- 稳定性:稳定
六、 快速排序
- 挖坑填数+分治法
- 设置第一一位为基准值(哨兵),分大小两部分
- 从左右两端依次分别找较小/大值,每次交换后i/j成为坑
- 扩展: 随机基准值
1、 动图演示
2、 代码实现
public static void quickSort(int[] list, int left, int right) {
if(left >= right)
return ;
//排序并返回基准元素位置
int pivot = list[left]; // 哨兵
int i = left, j = right;
while (i < j) {
while (i < j && list[j] >= pivot)
j--;
if (i < j)
list[i++] = list[j];
while (i < j && list[i] <= pivot)
i++;
if (i < j)
list[j--] = list[i];
}
list[i] = pivot;// 填坑(i,j相同)
quickSort(list, left, i - 1);
quickSort(list, i + 1, right);
}
3、算法分析
- 平均复杂度:O(nlog2n) [平均log2n轮,每轮查询次数O(n) ,则复杂度O(nlog2n)(同归并递归)]
- 最佳情况:O(nlog2n)
- 最差情况:O(n2)(数组倒序,枢纽元选第一位)
七、 堆排序
1、 动图演示
- 建堆
- 重排:从(倒数)第一个非叶子节点(index = n/2-1)开始,小的叶子节点换到父节点
2、 代码实现
public static void sort(int[] list) {
int len = list.length;
if (len < 1)
return;
// 1. 构建大根堆(第一次建堆已经是排好序的形式,因此下面直接取根(最大值))
// 可以和chongpai合并在while里
buildMaxHeap(list, len);
// 2. 取队首与末尾交换 + 重排堆
int t = 0;
while (len > 0) {
// swap(list,0,len-1);
t = list[0];
list[0] = list[len - 1];
list[len - 1] = t;
// 重新弄建堆,只需堆root重排即可
adjustHeap(list, 0,len-1);
len--;
}
}
private static void buildMaxHeap(int[] list, int len) {
for (int i = len / 2 - 1; i >= 0; i--) {
adjustHeap(list, i, len);
}
}
private static void adjustHeap(int[] list, int i,int len) {
int t = 0;
// 一直循环到i节点以下都正确排序(也可递归:maxIndex != i)
for (;2 * i + 1 < len;i = 2*i+1) {
int maxIndex = 2 * i + 1;// 大值索引先记为左子
if (2 * i + 2 < len && list[2 * i + 2] > list[2 * i + 1])
maxIndex = 2 * i + 2;
if (list[maxIndex] > list[i]) {
t = list[maxIndex];
list[maxIndex] = list[i];
list[i] = t;
}
//由于是从下往上比较,因此如果当前父节点大于双子,那么下面一定已经排好序
else
break;
}
}
3、算法分析
- 稳定性:不稳定(多次无序交换)
- O(nlog2n)
- 复杂度分析:
一. 初始化建堆
初始化建堆只需要对二叉树的非叶子节点调用adjusthead()函数,由下至上,由右至左选取非叶子节点来调用adjusthead()函数。那么倒数第二层的最右边的非叶子节点就是最后一个非叶子结点。
假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;高层也是这样逐渐递归。
那么总的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要下调比较的次数。
S = 2^(k-2) * 1 + 2(k-3)2……+2(k-2)+2(0)*(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
S = 2^k -k -1;又因为k为完全二叉树的深度,而log(n) =k,把此式带入;
得到:S = n - log(n) -1,所以时间复杂度为:O(n)
二. 排序重建堆
在取出堆顶点放到对应位置并把原堆的最后一个节点填充到堆顶点之后,需要对堆进行重建,只需要对堆的顶点调用adjustheap()函数。
每次重建意味着有一个节点出堆,所以需要将堆的容量减一。adjustheap()函数的时间复杂度k=log(n),k为堆的层数。所以在每次重建时,随着堆的容量的减小,层数会下降,函数时间复杂度会变化。重建堆一共需要n-1次循环,每次循环的比较次数为log(i),则相加为:log2+log3+…+log(n-1)+log(n)≈log(n!)。可以证明 log(n!)和nlog(n) 是同阶函数,所以时间复杂度为O(nlogn)
八、 计数排序
1、 动图演示
- 只对整数排序
1. 找最大/小值
2. 统计
2、 代码实现
3、算法分析
九、 桶排序
计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
1、 动图演示
2、 代码实现
3、算法分析
十、 基数排序
- 基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;
- 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的.
1、 动图演示
感觉不常用,空间复杂度*O(k)*可能很大,排序速度快,O(n+k),k是数据范围
2、 代码实现
3、算法分析