目录
算法的稳定性:正式学习排序之前先了解下什么是排序算法的稳定性,算法的稳定性是指若在原序列中有相同的数字,在排序后它们原本的次序不发生改变,那么就称该排序算法是稳定的。在实际应用中,往往我们排序的不是单独的数字而是包含另外的关键字(例如排序的元素是学生,包含姓名与分数,排序规则是按分数从小到大,若分数相同则按原来的排序保持),这就要求排序的算法是稳定的,因此稳定性也是排序算法需要考虑的一个重要方面。
一、选择排序
基本思想:每次从待排序的序列中选出最值,按顺序放在序列的最前面(与待排序序列的首位数字交换),n-1轮后排序完成。
时间复杂度:O(),考虑最好情况(输入就是有序序列),时间复杂度不发生改变。如有相同的数字它们原本的次序有可能会发生变化(例如:2 2 1,排序后1 2 2),因此是不稳定的排序。
(图片来自网络)例:

/**
* 从小到大排序
* @param l 排序起始下标位置
* @param r 排序结束下标位置
* @param array 要排序的数组
*/
public static void select_sort(int l, int r, int[] array) {
int temp;
for(int i=l;i<=r-1;++i) {
//假设最值位于最前面,也就是i位置
int min=i;
//选出最值
for(int j=i+1;j<=r;++j)
if(array[j]<array[min]) min=j;
//如果真正的最值不在i处,就交换这两个位置的数
if(min!=i) {
temp=array[i];
array[i]=array[min];
array[min]=temp;
}
}
}
二、冒泡排序
基本思想:依次比较相邻的数字,若逆序就交换,一趟排序后最值被确定在最后一个位置,n-1轮后排序完成。
时间复杂度:O(),考虑最好情况(输入就是有序序列),时间复杂度缩小为为O(n)。如有相同的数字它们原本的次序将不会发生变化,是稳定的排序。
(图片来自网络)例:

/**
* 从小到大排序
* @param l 排序起始下标位置
* @param r 排序结束下标位置
* @param array 要排序的数组
*/
public static void bubble_sort(int l, int r, int[] array) {
boolean flag = true;
for(int i=r;i>l;--i) {
for(int j=l;j<i;++j) {
//若逆序就交换相邻的两个数
if(array[j]>array[j+1]) {
int temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
flag=false;
}
}
//如果一趟排序下来一次交换都没有,说明序列已经有序,无需继续排序了
if(flag) break;
}
}
三、插入排序
基本思想:类似于抓牌的场景,每次抓到一个数字就将它放入手中已有的数字中正确的位置处,有n个数就要插入n-1次。
时间复杂度:O()。考虑最好情况(输入就是有序序列),时间复杂度缩小为为O(n)。如有相同的数字它们原本的次序将不会发生变化,是稳定的排序。
(图片来自网络)例:

/**
* 从小到大排序
* @param l 排序起始下标位置
* @param r 排序结束下标位置
* @param array 要排序的数组
*/
public static void insert_sort(int l, int r, int[] array) {
for(int i=l+1;i<=r;++i) {
for(int j=l;j<i;++j) {
//要插入的数
int temp=array[i];
//找到比要插入的数大的位置
if(array[j]>temp) {
//将它们全部后移一位
for(int k=i;k>j;--k)
array[k]=array[k-1];
//插入到该位置
array[j]=temp;
break;
}
}
}
}
//也可以使用以下写法,后续的希尔排序将采用如下写法
public static void insert_sort(int l, int r, int[] array) {
for(int i=l+1;i<=r;++i) {
int j=i-1, temp=array[i];
while(j>=0 && array[j]>temp) {
array[j+1]=array[j];
j--;
}
array[j+1]=temp;
}
}
四、希尔排序
基本思想:插入排序在对几乎已经排好序的数据操作时,效率很高,于是我们可以先将整个待排序的序列分割成为若干子序列分别进行插入排序,待整个序列基本有序时,再对整体序列进行插入排序。为此,我们定义一个增量gap,原序列将按照增量被划分为多个子序列,划分规则为:间隔为增量大小的数字为一组(例如增量为2,则依次把下标加2得到的所有序列中的数字为一组:0 2 4 6、1 3 5 7)。增量初始值为序列的长度/2,后续每次都将其除以2直到为1(当然,这不是唯一的设置增量的方式,甚至是较差的方式,但方便学习,想了解更多增量的设置方法可以自行百度)。对每次划分出来的子序列分别进行一次插入排序,缩小增量值,再次对所有子序列分别进行插入排序,直到增量为1时,对整个完整序列进行一次插入排序就能得到有序序列。
时间复杂度:根据不同的增量选择具有不同的时间复杂度,总体要优于直接插入排序。但是,与稳定的插入排序不同,希尔排序是不稳定的排序算法。
(图片来自网络)例:

/**
* 从小到大排序
* @param l 排序起始位置
* @param r 排序结束位置
* @param array 要排序的数组
*/
public static void shell_sort(int l, int r, int[] array) {
int length=r-l+1;
//确定每次的增量
for(int gap=length>>1;gap>=1;gap>>=1) {
//对每个增量分组使用插排
for(int i=gap;i<=r;++i) {
//i插入到它所在的增量分组
int temp=array[i], j=i-gap;
while(j>=0 && array[j]>temp) {
array[j+gap]=array[j];
j-=gap;
}
array[j+gap]=temp;
}
}
}
五、归并排序
基本思想:二分,将序列分为两个子序列,子序列继续拆分直到不可拆为止,先让子序列有序,然后将两个有序的子序列合并成一个有序的序列,直到只有一个有序序列为止。

时间复杂度:采用了分治法,分治法得到的时间复杂度是O(nlogn),最好与最坏情况对时间复杂度都无影响。如有相同的数字它们原本的次序将不会发生变化,是稳定的排序。
/**
* 从小到大排序
* @param l 排序起始下标位置
* @param r 排序结束下标位置
* @param array 要排序的数组
* @param assist 辅助数组
*/
public static void binary_sort(int l, int r, int[] array, int[] assist) {
if(l==r) return;
int mid=(l+r)>>1;
//分成两个子序列
binary_sort(l,mid,array,assist);
binary_sort(mid+1,r,array,assist);
int i=l,j=mid+1,tag=l;
//将两边的子序列合并成一个有序的序列
while(i<=mid && j<=r) {
if(array[i]<=array[j]) assist[tag++]=array[i++];
else assist[tag++]=array[j++];
}
//若两边子序列长度不等,就要单独复制
while(i<=mid) assist[tag++]=array[i++];
while(j<=r) assist[tag++]=array[j++];
for(int k=l;k<=r;++k) array[k]=assist[k];
}
六、快速排序
基本思想:定义一个基准数(常见地取中间位置),设置两个指针i,j分别从序列两端出发寻找第一个大(小)于基准数的值、第一个小(大)于基准数的值,将其交换,重复直至i>j。这样做地目的是,将整个序列分为两个独立的部分,其中一部分总是小于(等于)基准数,而另一部分总是大于(等于)基准数的,再分别对这两部分持续递归操作,序列最终被拆分成只包含两个数,此时若无序则必定发生交换,这样每个小部分都是有序的,而组成大序列时又按照基准数排好了顺序,因此最终将使整个序列有序。
时间复杂度:时间复杂度是O(nlogn),最坏情况下时间复杂度将下降为O(),此处有严格的证明,可以参照这个博主的分析:快速排序时间复杂度分析。如有相同的数字它们原本的次序将有可能会发生变化,是不稳定的排序(参考选择排序)。
/**
* 从小到大排序
* @param l 排序起始位置
* @param r 排序结束位置
* @param array 要排序的数组
*/
public static void quick_sort(int l, int r, int[] array) {
int i=l,j=r,mid=array[(l+r+1)>>1];
//等号必须有为了让终止条件不为i==j,否则递归的区间会在i==j时出现重合
while(i<=j) {
//没有等号、不能将mid定义成(l+r+1)/2然后这里改成array[mid],因为位于mid位置的数字是可能被交换走的!
while(array[i]<mid) i++;
while(array[j]>mid) j--;
if(i<=j) {
int temp=array[i];
array[i]=array[j];
array[j]=temp;
i++;j--;
}
}
if(l<j) quick_sort(l,j,array);
if(i<r) quick_sort(i,r,array);
}
七、堆排序
基本原理详见:基本数据结构:二叉堆 堆排序是不稳定的排序。
之前的那篇博客是用c++写的,下面给出Java的代码,将其封装成了类,能处理大小根的不同情况,添加了扩容机制,通过了10000以内数据的测试,但仍不保证没有bug存在:
public class Heap {
public int[] array;//注意下标是从1开始
private boolean type= Lower;//默认为小根堆
private static final int default_capacity = 11;//初始容量为11
private int size=0;
public static final boolean Lower = true;
public static final boolean upper = false;
//默认建堆
public Heap() {
array = new int[default_capacity];
}
/**
* @param type 指定优先级
*/
public Heap(boolean type) {
array = new int[default_capacity];
this.type=type;
}
/**
* @param array 指定堆中的内容
* @param size 指定堆的大小
*/
public Heap(int[] array,int size) {
this.array = new int[size+1];
this.size=size;
System.arraycopy(array,0,this.array,1,size);
build(size);
}
/**
* @param array 指定堆中的内容
* @param size 指定堆的大小
* @param type 指定优先级
*/
public Heap(int[] array,int size,boolean type) {
this.array = new int[size+1];
this.size=size;
this.type=type;
System.arraycopy(array,0,this.array,1,size);
build(size);
}
//交换函数
private void swap(int x,int y,int[] a){
int tmp=a[x];
a[x]=a[y]; a[y]=tmp;
}
/**
* 优先级比较函数
* @param x
* @param y
* @return 若为Lower模式,x<y返回1;若为upper模式,x>y返回1。返回1表示x的优先级更高,返回-1表示y的优先级更高,返回0表示优先级相同。
*/
private int compare(int x,int y) {
if(x>y) return (type? -1:1);
if(x<y) return (type? 1:-1);
return 0;
}
//上浮操作
private void rise(int k) {
if(k==1) return;//到达根结点就停止上浮
int father=k>>1;
if(compare(array[father],array[k])!=-1) return;
swap(father,k,array);//若父结点的优先级低于子结点,就交换它们的位置
rise(father);//父结点继续上浮
}
//下沉操作
private void sink(int k) {
int l=k<<1,r=l+1;//左子结点,右子结点
if(l>size) return;//到达叶子结点就停止下沉
int t=l;//假设左子结点优先级更高
if(r<=size) t=compare(array[l],array[r])==1? l:r;//如果右子结点存在,则比较左右子结点的优先级,取优先级更高的
if(compare(array[k],array[t])!=-1) return;
swap(k,t,array);//若父结点的优先级低于子结点,就交换它们的位置
sink(t);//交换后的子结点继续下沉
}
//高效建堆
private void build(int tail) {
int k=tail/2;
while(k!=0) {
sink(k);
k--;
}
}
//添加元素
public void push(int x) {
int oldCapacity = array.length-1;
//堆满,就执行扩容
if(oldCapacity == size) grow(oldCapacity);
array[++size]=x;
rise(size);
}
//扩容
private void grow(int oldCapacity) {
int newCapacity = oldCapacity + ((oldCapacity < 64) ?(oldCapacity + 2) :(oldCapacity >> 1));
int[] newArray = new int[newCapacity+1];
System.arraycopy(array,1,newArray,1,size);
array = newArray;
}
//删除优先级最高的元素
public void pop() {
array[1]=array[size--];
sink(1);
}
//获取优先级最高的元素
public int top() {
return array[1];
}
//获取长度
public int size() {
return size;
}
}
八、桶排序
1.计数排序
基本思想:待排序的数不是存放在数组里,而是作为数组的下标存放,数组中存放该数字在序列中出现的个数,所以叫计数排序。由于每个数组都可以类比成一个桶,数组下标就是桶的标号,当数字等于标号时就将其放入该桶中,所以也可以叫作桶排序,不过真正的桶排序要比这复杂,本文也不做介绍。该排序需要序列中的数字在一个明显的范围内。最终只要按顺序输出数组值不为0的下标即可,数组值等于多少就输出几次,时间复杂度为O(n)。
/**
* 从小到大排序
* @param l 排序起始位置
* @param r 排序结束位置
* @param array 要排序的数组
* @param limit 数据允许的最大范围0~limit
*/
public static void count_sort(int l, int r, int[] array,int limit) {
int[] b = new int[limit+1];
for(int i=0;i<=limit;++i) b[i]=0;
for(int i=l;i<=r;++i) b[array[i]]++;
int k=0;
for(int i=0;i<=limit;++i) {
while(b[i]>0) {
array[k++]=i;
b[i]--;
}
}
}
上面的代码不能适用于稳定排序的情况,于是就有了下面的改进:
array[i]是原来的序列,b[i]统计每个数字的出现次数,新加一个assist数组存放排序后的数据,之后对b[i]统计前缀和,统计前缀和的意义在于,知道了该数字前缀和就知道了该数字在排序后的位置,如下表:b[26的前缀和为10,也就是说排序后26会被排在第10位(下标从零开始就是9)。如果一个数出现了多次结果又如何?第一次遇见该数字仍然将其排在等于前缀和的位置,然后将该数字的前缀和-1,第二次遇见该数字时,它的前缀和仍然能正确指示其位置。例如:第一次遇见18时,排在第8位,前缀和-1等于7,那么第二次遇见18就将其放在第7位(参照排序后的数组可知是正确的)。那么如何维持稳定性?其实很简单,只要逆序遍历array数组即可,先遇见的相同的数字就会放在靠后的位置(不理解?自己对着下面的例子在脑海里模拟一下就知道了)。
| 排序前array[i] | 7 | 9 | 18 | 9 | 10 | 18 | 22 | 17 | 9 | 26 |
| 前缀和 | 1 | 4 | 5 | 6 | 8 | 9 | 10 |
| 出现次数 | 1 | 3 | 1 | 1 | 2 | 1 | 1 |
| b[i] | b[7] | b[9] | b[10] | b[17] | b[18] | b[22] | b[26] |
| 排序后 assist[i] | 7 | 9 | 9 | 9 | 10 | 17 | 18 | 18 | 22 | 26 |
/**
* 从小到大排序
* @param l 排序起始位置
* @param r 排序结束位置
* @param array 要排序的数组
*/
public static void bucket_sort(int l, int r, int[] array) {
int max=array[l];
for(int i=l;i<=r;++i) if(array[i]>max) max=array[i];
int[] assist = new int[r-l+1];
int[] b = new int[max+1];
for(int i=0;i<=max;++i) b[i]=0;
for(int i=l;i<=r;++i) b[array[i]]++;
//统计前缀和
for(int i=1;i<=max;++i) b[i]+=b[i-1];
//逆序遍历array
for(int i=r;i>=l;--i) {
assist[b[array[i]]-1]=array[i];
b[array[i]]--;
}
//assist只是暂时存放数据,最后仍然要将数据放回array
for(int i=l;i<=r;++i) array[i]=assist[i];
}
2.基数排序
基本原理:这里采用最低位优先(LSD)的方式实现,开始时将数字按最低位的值放入对应的桶中(这里的桶的概念参考上面的计数排序,由于每个位上的数字只有0~9,所以只用设置0~9十个桶就可以),取出时按桶的顺序依次取出,相当于按最低位给序列排了序,重复操作直到给最高位排序结束。由于每个桶只是某一位的值,我们不能通过桶的标号直接知道里面存放的数字是多少,因此除了需要一个计数的桶外,还需要记录桶中数字的值,可以通过二维数组实现,第一位是桶的标号,第二维记录是桶中的第几个数,数组的值就是存放在这里的数字。基数排序是稳定的排序。
(图片来自网络)例:

/**
* 从小到大排序
* @param l 排序起始位置
* @param r 排序结束位置
* @param array 要排序的数组
*/
public static void radix_sort(int l, int r, int[] array) {
int max=array[l];
for(int i=l;i<=r;++i) if(array[i]>max) max=array[i];
int[][] assist = new int[10][r-l+1];
//0~9
int[] b = new int[10];
for(int i=0;i<10;++i) b[i]=0;
//最大的数有几位
int length=(max+"").length();
//最大的数有几位就排序几次
for(int i=0,n=1;i<length;++i,n*=10) {
//n=1时,按个位排序,n=10时按十位排序,以此类推
for(int j=l;j<=r;++j) {
//0~9,rest即为b[]的下标
int rest=array[j]/n %10;
//array[j]被放在rest处的第b[rest]个位置
assist[rest][b[rest]]=array[j];
b[rest]++;
}
int tag=0;
//0~9
for(int k=0;k<10;++k) {
if(b[k]!=0) {
for(int t=0;t<b[k];++t)
//取出放在k处的第t个位置的数字,k对应放入时的rest,t对应放入时的b[rest]
array[tag++]=assist[k][t];
b[k]=0;
}
}
}
}
九、测试算法的正确性、稳定性
使用随机数测试排序算法的正确性:
public static void main(String[] args) {
Random rand = new Random();
int[] num = new int[100];
//随机生成0~500的随机数,数据规模为100
for(int i=0;i<100;++i)
num[i]=rand.nextInt(501);
//每次只使用一个,注释掉其它
Sort.bucket_sort(0, 99, num);
// Sort.radix_sort(0, 99, num);
// Sort.select_sort(0, 99, num);
// Sort.bubble_sort(0, 99, num);
// Sort.insert_sort(0, 99, num);
// Sort.shell_sort(0, 99, num);
// Sort.binary_sort(0, 99, num,new int[26]);
// Sort.quick_sort(0, 99, num);
//输出排序后的结果
for(int i=0;i<100;++i)
System.out.println(num[i]);
}
根据算法稳定性的描述,要测试算法的稳定性,我们需要另外定义一个类,这里定义学生类:
//学生类
public class Student {
public char name;//学生姓名
public int socre=0;//学生分数
public Student() {
}
public Student(char name,int score) {
this.name=name;
this.socre=score;
}
@Override
public String toString() {
return "name: " + name + ", socre=" + socre;
}
}
之后,将所有排序算法中的int数组,改成Student数组,按照Student的socre关键字排序,这里以稳定的计数排序为例:
public static void bucket_sort(int l, int r, Student[] array) {
int max=array[l].socre;
for(int i=l;i<=r;++i) if(array[i].socre>max) max=array[i].socre;
Student[] assist = new Student[r-l+1];
int[] b = new int[max+1];
for(int i=0;i<=max;++i) b[i]=0;
for(int i=l;i<=r;++i) b[array[i].socre]++;
for(int i=1;i<=max;++i) b[i]+=b[i-1];
for(int i=r;i>=l;--i) {
assist[b[array[i].socre]-1]=array[i];
b[array[i].socre]--;
}
for(int i=l;i<=r;++i) array[i]=assist[i];
}
仍然随机生成数据,为了方便观察稳定性,将Student的姓名按顺序设置为‘a’~‘z’,这样如果排序算法是稳定的,排序后输出的结果中相同分数的学生应该按照字母顺序排序:
public static void main(String[] args) {
Random rand = new Random();
Student[] stu = new Student[26];
//学生的姓名依次设置为'a'~'z',为了出现更多相同的分数,分数的范围要小,在0~25中随机生成
for(int i=0;i<26;++i)
stu[i]=new Student((char) ('a'+i),rand.nextInt(26));
//每次只使用一个,注意注释掉其它
Sort.bucket_sort(0, 25, stu);
// Sort.radix_sort(0, 25, stu);
// Sort.select_sort(0, 25, stu);
// Sort.bubble_sort(0, 25, stu);
// Sort.insert_sort(0, 25, stu);
// Sort.shell_sort(0, 25, stu);
// Sort.binary_sort(0, 25, stu,new Student[26]);
// Sort.quick_sort(0, 25, stu);
//输出排序结果
for(int i=0;i<26;++i)
System.out.println(stu[i]);
}
最后是所有排序算法的图表总结(图片来自于网络):

128

被折叠的 条评论
为什么被折叠?



