在本文中将介绍我们平时用到的各种排序算法,并详细介绍它们的排序过程。并在最后附带所有这些排序算法的时间复杂度和空间复杂度。
1. 插入排序
1.1 插入排序–直接插入排序
插入排序的思想是:每一轮都把一个元素,按照其关键字的大小插入到它前面已经排序的子序列中,使得插入后的子序列仍是排序的,直到所有的元素插入为止。直接插入排序的思想如下:从第1个元素开始(Java数组从0开始),每次都和前面已经排序好的子序列去比较,将其放到适当的位置,直到最后一个放好。关键代码如下:
//直接插入排序
public class straightInsertionSort {
public static void main(String[] args) {
int[] arr ={32,26,87,72,26,17,100,10,3,6};
System.out.print("未排序的原序列为:");
打印(arr);
StraightInsertionSort(arr);
}
public static void StraightInsertionSort(int[] arr){
for(int i=1;i<arr.length;i++){//从第1个元素开始(Java中数组从0开始),扫描n-1次
int temp=arr[i],j;//把第i个待插入的元素拿出来,后面会用来和前面已经排序好的子序列进行比较
for(j=i-1;j>=0&&temp<arr[j];j--)//从前面已经排序好的子序列里,从后向前比较
arr[j+1]=arr[j];//如果temp比arr[j]小,就将当前元素后移,直到temp>=arr[j]
arr[j+1]=temp;//此时temp需要放到arr[j]后面,所以是arr[j+1]=temp
System.out.print("第"+i+"趟排序结果为:");
打印(arr);
}
}
public static void 打印(int[] arr){//Java中的方法名完全可以用汉字,但笔者不建议这么做
for(int a:arr)
System.out.print(a+"\t");
System.out.println();
}
}
最后的排序结果如下:
直接插入排序的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),空间复杂度为
O
(
1
)
O(1)
O(1),直接插入排序算法稳定。
1.2 插入排序–希尔排序
希尔排序(shell sort)又称为缩小增量排序,它是分组的直接插入排序。希尔排序将一个数据序列按一定的距离进行分组,这段距离称为增量。
假设数组为
a
0
,
a
1
,
a
2
,
.
.
.
,
a
n
−
2
,
a
n
−
1
a_0,a_1,a_2,...,a_{n-2},a_{n-1}
a0,a1,a2,...,an−2,an−1,设增量
δ
\delta
δ为3,则在该增量下数据分组为:
{
a
0
,
a
0
+
δ
,
a
0
+
2
δ
,
a
0
+
3
δ
,
.
.
.
.
}
\{a_0,a_{0+\delta},a_{0+2\delta},a_{0+3\delta},....\}
{a0,a0+δ,a0+2δ,a0+3δ,....},
{
a
1
,
a
1
+
δ
,
a
1
+
2
δ
,
a
1
+
3
δ
,
.
.
.
.
}
\{a_1,a_{1+\delta},a_{1+2\delta},a_{1+3\delta},....\}
{a1,a1+δ,a1+2δ,a1+3δ,....},
{
a
2
,
a
2
+
δ
,
a
2
+
2
δ
,
a
2
+
3
δ
,
.
.
.
.
}
\{a_2,a_{2+\delta},a_{2+2\delta},a_{2+3\delta},....\}
{a2,a2+δ,a2+2δ,a2+3δ,....}共3组数据,对这3组数据都进行直接插入排序。由此可看出增量是几就可以分出几组。在一个组内采用直接插入排序算法进行排序。增量的初值一般设置为数组长度的一半,以后每趟增量逐渐缩小,随着增量的减少,组数也减少,组内元素个数增加,整个数组趋近于有序。最后缩小为1,即这时只有一组,囊括了整个数组,再进行最后一次直接插入排序即可。增量的变化规律有多种方案,在本文将初始增量设为数组的一半,以后增量每次都减半。
希尔排序的增量减半的源码如下:
//希尔排序
public class ShellSort {
public static void main(String[] args) {
int[] arr ={32,26,87,72,26,17,100,10,3,6};
System.out.print("未排序的原序列为-------------:");
打印(arr);
shellSort(arr);
}
public static void shellSort(int[] arr){
for(int delta=arr.length/2;delta>0;delta/=2){
for(int i=delta;i<arr.length;i++){//i每次加1的意思会迷惑很多人,这个意思是同时分多个组并行地直接插入排序,i每增加1,就会切换到另一组
int temp=arr[i],j;//将当前值赋给temp
for(j=i-delta;j>=0&&temp<arr[j];j-=delta)//每次向前移动delta,寻找同一组的数据进行比较
arr[j+delta]=arr[j];
arr[j+delta]=temp;
System.out.print("增量为"+delta+"时"+"第"+(i%delta+1)+"组的第"+(i/delta)+"次插入排序结果为:");
打印(arr);
}
}
}
public static void 打印(int[] arr){//Java中的方法名完全可以用汉字,但笔者不建议这么做
for(int a:arr)
System.out.print(a+"\t");
System.out.println();
}
}
观察如上代码,发现只是将直接插入排序每次增量为1的地方改变为增量delta,然后在最外层增加一层控制增量delta变化的for循环即可。将每次发生的插入排序都打印,并打印出组别和排序次数,看起来会更直观一些,读者只需要认真分析如下结果,就会一目了然:
希尔排序的时间复杂度分析比较复杂,它取决于如何选取增量序列。它的空间复杂度是
O
(
1
)
O(1)
O(1),算法不稳定。
2. 交换排序
交换排序有冒泡排序和快速排序。
2.1 交换排序–冒泡排序
冒泡排序的思想是比较相邻的两个元素,如果反序,则交换。如果按照升序排序,每一趟扫描的数据子序列中最大的值将会交换到最后的位置,就像是在冒泡泡。冒泡排序的子序列变化规律如下:
第1趟扫描序列:
{
a
0
,
a
1
,
.
.
.
,
a
n
−
2
}
\{a_0,a_1,...,a_{n-2}\}
{a0,a1,...,an−2}
第2趟扫描序列:
{
a
0
,
a
1
,
.
.
.
,
a
n
−
3
}
\{a_0,a_1,...,a_{n-3}\}
{a0,a1,...,an−3}
第3趟扫描序列:
{
a
0
,
a
1
,
.
.
.
,
a
n
−
4
}
\{a_0,a_1,...,a_{n-4}\}
{a0,a1,...,an−4}
…
第n-2趟扫描序列:
{
a
0
,
a
1
}
\{a_0,a_1\}
{a0,a1}
第n-1趟扫描序列:
{
a
0
}
\{a_0\}
{a0}
为了避免后期无效的扫描(比如一个近乎有序的序列根本不需要n-1次扫描),增加一个是否产生交换的标记变量,如果本次扫描未发生交换,则证明数组已经有序,可以终止了。冒泡排序的源码如下:
//冒泡排序
public class BubbleSort {
public static void main(String[] args) {
int[] arr ={32,26,87,72,26,17,100,10,3,6};
System.out.print("未排序的原序列为:");
打印(arr);
bubbleSort(arr);
}
public static void bubbleSort(int[] arr){
boolean exchange=true;//首先将其置为true,便于for循环的正常执行
for(int i=1;i<arr.length&&exchange;i++){//发生交换时再进行下一轮,最多发生n-1轮扫描
exchange=false;
for(int j=0;j<arr.length-i;j++){//该for循环走完一次发生了一趟比较、交换
if(arr[j]>arr[j+1]){//反序就交换
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
exchange=true;//发生交换将交换标志置为true
}
}
System.out.print("第"+i+"趟排序结果为:");
打印(arr);
}
}
public static void 打印(int[] arr){//Java中的方法名完全可以用汉字,但笔者不建议这么做
for(int a:arr)
System.out.print(a+"\t");
System.out.println();
}
}
当一个序列近乎有序时,有了交换标志不需要再扫描n-1次:
则增加了交换标志,可减少不必要的扫描。冒泡排序算法的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),它需要一个辅助空间用于交换两个元素,空间复杂度为
O
(
1
)
O(1)
O(1),冒泡排序算法是稳定的。
2.2 交换排序–快速排序
快速排序是一种分区交换排序算法。用到了分治法的思想。快速排序(quick sort)的基本思想是:在序列中选择一个值作为比较的基准值,每趟从序列的两端开始交替进行,将小于基准值的元素交换到序列前端,将大于基准值得元素交换到序列后端,介于两者之间的位置即为基准值的位置。同时序列被划分为两个子序列,再对子序列用同样的方法进行排序,直到子序列长度为1,完成排序。
一般选取第一个元素作为基准值,这样前面的元素就空了下来,这时需要从后面从后往前找,找到比基准值小的,就放到前面空出的位置,这样后面的位置就空出来一个,再从前面找比基准值大的,这样依次往复,直到前后交汇,交汇点即为基准值得位置。基准值将序列一分为二,它不再参与后续两个子序列的操作。快速排序的源码如下:
//快速排序
public class QuickSort {
static int num=0;
public static void main(String[] args) {
int[] arr ={38,26,97,19,66,1,5,49};
System.out.print("未排序的原序列为:");
打印(arr);
quickSort(arr,0,arr.length-1);
}
public static void quickSort(int[] arr,int start,int end){
if(start<end){//首先得是个有效的序列,start>end不是序列,start=end只有一个元素,没必要排序
int i=start,j=end;
int temp=arr[i];//将序列的第一个值作为基准值
while(i!=j){//当i==j时停止循环
while(i<j&&temp<=arr[j])//从后往前找,如果i<j,而且基准值小于序列后端的arr[j],就向前移动
j--;
if(i<j)
arr[i++]=arr[j];//如果此时i仍然小于j,说明找到了一个元素比基准值小的,将其放置到前端空出的位置,然后i向后移动一位
while(i<j&&arr[i]<=temp)//这个时候后端的位置空出了一个,需要从前往后找比基准值大的元素
i++;
if(i<j)
arr[j--]=arr[i];//找到了之后将这个较大的元素放置到后面空出的 位置
}
arr[i]=temp;//这时的i就是基准值得最终位置
System.out.print("第"+(++num)+"趟排序结果为:");
打印(arr);
quickSort(arr,start,j-1);//前端的子序列再排序
quickSort(arr,i+1,end); //后端的子序列再排序
}
}
public static void 打印(int[] arr){//Java中的方法名完全可以用汉字,但笔者不建议这么做
for(int a:arr)
System.out.print(a+"\t");
System.out.println();
}
}
快速排序的平均时间复杂度为
O
(
n
∗
l
o
g
2
n
)
O(n*log_2n)
O(n∗log2n),空间复杂度为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n),该算法是不稳定的。
3. 选择排序
选择排序有直接选择排序和堆排序两种
3.1 选择排序–直接选择排序
直接选择排序(straight select sort)的基本思想是:按升序排序,第一趟从n个元素里找出最小的元素放到最前面,下一趟从n-1个元素里找出最小元素放在前面,以此类推,经过n-1次扫描,完成排序。源码如下:
//直接选择排序
public class StraightSelectSort {
public static void main(String[] args) {
int[] arr ={32,26,87,72,26,17,100,10,3,6};
int[] arr1 ={32,26,87,72,26,17,100,10,3,6};
System.out.print("未排序的原序列为:");
打印(arr);
straightSelectSort1(arr);
straightSelectSort2(arr1);
}
public static void straightSelectSort1(int[] arr){//这是一种交换次数比较多的一种写法
int num=0;//用来记录交换次数,可不写
for(int i=0;i<arr.length-1;i++){//从i=0开始,逐步去确定arr[0],arr[1],...arr[n-2]的值
for(int j=i+1;j<arr.length;j++)//从i的后面开始查找,查找比arr[i]小的元素
if(arr[j]<arr[i]){//如果存在,则直接交换arr[i]和arr[j]的值,这样的交换次数会很多,影响效率
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
num++;//记录交换次数,可不写
}
System.out.print("第"+(i+1)+"趟排序结果为:");
打印(arr);
}
System.out.println("交换次数为:"+num);
}
public static void straightSelectSort2(int[] arr){//这是一种只记录下标的方式,可以大大减少交换次数
int num=0;//用来记录交换次数,可不写
for(int i=0;i<arr.length-1;i++){
int min=i;//记录当前位置下标
for(int j=i+1;j<arr.length;j++)
if(arr[j]<arr[min])//如果找到比arr[min]更小的值,则记录其下标
min=j;
if(min!=i){//上述for循环执行完毕后,已经找到当前序列的最小值的下标,根据下标进行交换
int temp=arr[i];
arr[i]=arr[min];
arr[min]=temp;
num++;//记录交换次数,可不写
}//这样最多只产生n-1次交换
System.out.print("第"+(i+1)+"趟排序结果为:");
打印(arr);
}
System.out.println("交换次数为:"+num);
}
public static void 打印(int[] arr){//Java中的方法名完全可以用汉字,但笔者不建议这么做
for(int a:arr)
System.out.print(a+"\t");
System.out.println();
}
}
从以下的运行结果可明显看出,记录下标的方式可明显减少交换次数,提高运行效率。
直接选择排序时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。空间复杂度为
O
(
1
)
O(1)
O(1)。算法不稳定。
3.1 选择排序–堆排序
4. 归并排序
5. 总结
思想 | 排序算法 | 时间复杂度 | 最好情况 | 最坏情况 | 空间复制度 | 稳定性 |
---|---|---|---|---|---|---|
插入 | 直接插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
插入 | 希尔排序 | O ( n ( l o g 2 n ) 2 ) O(n(log_2n)^2) O(n(log2n)2) | O ( 1 ) O(1) O(1) | 不稳定 | ||
交换 | 冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
交换 | 快速排序 | O ( n ∗ l o g 2 n ) O(n*log_2n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_2n) O(n∗log2n) | O ( n 2 ) O(n^2) O(n2) | O ( l o g 2 n ) O(log_2n) O(log2n) | 不稳定 |
选择 | 直接选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
选择 | 堆排序 | O ( n ∗ l o g 2 n ) O(n*log_2n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_2n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_2n) O(n∗log2n) | O ( 1 ) O(1) O(1) | 不稳定 |
归并 | 归并排序 | O ( n ∗ l o g 2 n ) O(n*log_2n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_2n) O(n∗log2n) | O ( n ∗ l o g 2 n ) O(n*log_2n) O(n∗log2n) | O ( n ) O(n) O(n) | 稳定 |