1、排序的概述
- 排序是计算机程序设计的一个重要的操作,它的功能是将一个数据元素(或记录)的任意序列,重新排列成为一个按关键字有序的序列。
- 若在排序前m领先于n,则称所用的 排序方法是稳定的 ;反之,若可能使排序后的n领先于m,则称所用的 排序方法是不稳定的 。
- 由于待排序的记录数量不同,使得排序过程中涉及的存储器不同,可将排序方法分为两大类:一类是 内部排序 ,指的是待排序记录存放在计算机随机存储器中进行的排序过程;另一类是 外部排序 ,指的是待排序记录的数量很大,以至内存不能一次容纳所有的记录,在排序过程中尚需对外存进行访问的排序过程,我们在这里主要讨论的是内部排序。
- 内部排序的方法很多,但就其全面性能而言,很难提出一种被认为是最好的方法,每一种方法都有自己的优缺点,适合在不同的环境(如记录的初始排队状态等)下使用。如果按排序过程中依据的不同原则对内部排序方法进行分类,则大致可以分为:插入排序、交换排序、选择排序、归并排序和计数排序等五类。
2、插入排序
① 直接插入排序
- 它是一种最简单的排序方式,它的基本操作是将一个记录插入到一个有序序列当中,从而得到一个新的,记录数增1的有序列表当中。
- 算法描述如下:
package org.westos.MyDemo2;
import java.util.Arrays;
public class MyTest3
{
public static void main(String[] args)
{
//直接插入排序
int[] arr={3,2,1,0,10,30,7,8};
//外层循环控制轮数
for (int i = 1; i < arr.length; i++)
{
//里层循环控制比较插入
int j=i;
while(j>0&&arr[j]<arr[j-1])
{
arr[j]=(arr[j]+arr[j-1])-(arr[j-1]=arr[j]);
j--;
}
}
System.out.println(Arrays.toString(arr));
}
}
//输出:[0, 1, 2, 3, 7, 8, 10, 30]
- 复杂度:
② 希尔排序 - 希尔排序又称为“缩小增量排序”,一种属于插入排序类的方法,但在时间效率上较前几种排序方法有较大的改进。
- 基本思想是:先将原表按增量ht分组,每个子文件按照直接插入法排序,同样,用下一个增量ht/2将文件再分为子文件,再用直接插入法排序,直到ht=1时整个文件排好序。
- 关键:找到合适的增量,以下的例子第一趟增量为数组长度的一半。
- 从上述排序的过程可见,希尔排序的一个特点是:子序列的构成不是简单地“逐段分割”,而是将相隔某个“增量”的记录组成一个子序列。如上例中,第一趟排序时增量为4,第二趟排序时增量为2,第三趟排序时增量为1,由于在前两趟的插入排序中记录的关键字是和同一子序列中的前一个记录的关键字进行比较,因此关键字较小的记录就不是一步一步往前挪,而是跳跃式往前移动,从而使得在进行最后一趟增量为1的插入排序时,序列已经基本有序,只要作记录的少量比较和移动即可完成排序,因此希尔排序的时间复杂度较直接插入排序低。
- 复杂度描述:
- 算法描述:
package org.westos.MyDemo2;
import java.util.Arrays;
public class MyTest5
{
public static void main(String[] args)
{
//希尔排序:它是对插入排序的一个优化,核心的思想是合理的选取增量,经过一轮排序后,
//就会让序列大致有序,然后在不断地缩小增量,进行插入排序,直到增量为1,整个的排序结束
//直接插入排序其实就是增量为1的希尔排序
int[] arr={46,55,13,42,17,94,5,70};
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void shellSort(int[] arr)
{
//定义一个增量
/*int h=4;
//直接插入排序
for (int i = h; i < arr.length; i++)
{
//里层循环控制比较插入
int j=i;
while(j>h-1&&arr[j]<arr[j-h])
{
arr[j]=(arr[j]+arr[j-h])-(arr[j-h]=arr[j]);
j-=h;
}
}*/
//46,55,13,42,17,94,5,70
int h=arr.length/2;
while(h>=1)
{
//h=4, h=2
for (int i = h; i < arr.length; i++)//i=4,i<8 i=5,i<8 i=6,i<8 i=7,i<8
{
//里层循环控制比较插入
//j=4, j=5, j=6, j=7,
int j=i;
//j>3&&17<46 j>3&&94!<55 j>3&&5<13 j>3&&70!<42
while(j>h-1&&arr[j]<arr[j-h])
{
//17,55,13,42,46,94,5,70
//17,55,5,42,46,94,13,70
arr[j]=(arr[j]+arr[j-h])-(arr[j-h]=arr[j]);
j-=h;//j=0 j=2
}
}
h=h/2;
}
}
}
//输出结果为:[5, 13, 17, 42, 46, 55, 70, 94]
- 改进算法:
希尔排序的思想是合理的选取增量,增量取数组长度的一半。但后来有人发现了更高效的选取增量的方式,即克努特序列(Knuth序列),公式:int h=1; h=3*h+1;
public static void main(String[] args)
{
//希尔排序:它是对插入排序的一个优化,核心的思想是合理的选取增量,经过一轮排序后,
//就会让序列大致有序,然后在不断地缩小增量,进行插入排序,直到增量为1,整个的排序结束
//直接插入排序其实就是增量为1的希尔排序
int[] arr={46,55,13,42,17,94,5,70};
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void shellSort(int[] arr)
{
int jiange=1;
while(jiange<=arr.length/3)
jiange=jiange*3+1;
for (int h = jiange; h > 0; h=(h-1)/3)
for (int i = h; i < arr.length; i++)
for (int j = i; j > h-1; j-=h)
if(arr[j]<arr[j-h])
arr[j]=(arr[j]+arr[j-h])-(arr[j-h]=arr[j]);
}
3、归并排序
归并排序(2-路归并排序)
- 原理:假设初始序列中有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2个长度为2或1的有序子序列,再两两归并,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2-路归并排序。
- 核心思想:分而治之,与快速排序和堆排序相比,归并排序的最大特点是形式简洁,但是实用性差。
- 算法描述如下:
package org.westos.MyDemo2;
import java.util.Arrays;
public class MyTest6
{
public static void main(String[] args)
{
//归并排序
//原始待排数组
int[] arr={10,30,2,1,0,8,7,5,19,29};
//我们先给一个左右两边是有序的一个数组,先来进行归并操作
//int[] arr={4,5,7,8,1,2,3,6};
//拆分
chaifen(arr,0,arr.length-1);
//归并
guibing(arr,0,arr.length/2-1,arr.length-1);
System.out.println(Arrays.toString(arr));
}
private static void chaifen(int[] arr, int i, int j)
{
//计算中间索引
int mid=(i+j)/2;
if(i<j)
{
chaifen(arr,i,mid);
chaifen(arr,mid+1,j);
guibing(arr,i,mid,j);
}
}
private static void guibing(int[] arr, int start, int mid, int end)
{
//定义一个临时数组
int[] tempArr=new int[end-start+1];
//定义左边数组的起始索引
int i=start;
//定义右边数组的起始索引
int j=mid+1;
//定义临时数组的起始索引
int index=0;
//比较左右两个数组的元素大小,往临时数组中放
while(i<=mid&&j<=end)
{
if(arr[i]<=arr[j])
{
tempArr[index]=arr[i];
i++;
}
else
{
tempArr[index]=arr[j];
j++;
}
index++;
}
//处理剩余元素
while(i<=mid)
{
tempArr[index]=arr[i];
i++;
index++;
}
while(j<=end)
{
tempArr[index]=arr[j];
j++;
index++;
}
//System.out.println(Arrays.toString(tempArr));
//将临时数组中的元素取到原数组当中
for (int k = 0; k < tempArr.length; k++)
{
arr[k+start]=tempArr[k];
}
}
}
//输出结果为:[0, 1, 2, 5, 7, 8, 10, 19, 29, 30]
- 该算法中使用了递归,虽然形式简洁,但是实用性差,它是一种稳定的算法,但是在内部排序的过程中确很少使用这种算法。
4、快速排序
① 冒泡排序
- 这里我们讨论一种藉助“交换”进行排序的算法,其中最简单的一种就是人们所熟知的冒泡排序。
- 原理:数组元素两两比较,交换位置,大元素往后放,经过一轮比较之后,最大的元素就出现在了最大的索引处。
- 算法描述:
public static void main(String[] args)
{
//冒泡排序
int[] arr={24,69,80,57,13};
for (int i = 0; i < arr.length - 1; i++)
{
for (int j = 0; j < arr.length-1-i; j++)
{
if(arr[j]>arr[j+1])
{
arr[j]=(arr[j]+arr[j+1])-(arr[j+1]=arr[j]);
}
}
}
System.out.println(Arrays.toString(arr));
}
② 快速排序
- 快速排序是对冒泡排序的一种改进。它的基本思想是:分治法——比大小,再分区
- 步骤:1、从数组中取出一个数,作为基准数 2、分区:将比这个数大或等于的数字放到它的右边,比他小的放在他的左边。 3、再对左右区间重复第二步,直到各区间只有一个数。
- 实现思路:挖坑填数。
1、将基准数挖出形成第一个坑
2、由后向前找出比他小的数,找到后挖出此数填到第一个坑中
3、由前向后找比他大或等于的数,找到后也挖出此数填到前一个坑中
4、重复2、3步骤
- 代码描述:
package org.westos.MyDemo2;
import java.util.Arrays;
public class MyTest4
{
public static void main(String[] args)
{
//快速排序
int[] arr={10,3,5,6,1,0,100,40,50,8};
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
private static void quickSort(int[] arr, int start, int end)
{
//找出分左右两区的索引位置,然后对左右两区进行递归调用
if(start<end)
{
int index=getIndex(arr,start,end);
quickSort(arr,start,index-1);
quickSort(arr,index+1,end);
}
}
private static int getIndex(int[] arr, int start, int end)
{
int i=start;
int j=end;
int x=arr[i];
while(i<j)
{
while(i<j&&arr[j]>=x)
{
j--;
}
if(i<j)
{
arr[i]=arr[j];
i++;
}
while(i<j&&arr[i]<x)
{
i++;
}
if(i<j)
{
arr[j]=arr[i];
j--;
}
}
arr[i]=x;
return i;
}
}
- 快排的平均性能优于前面的各种排序方法,它只需要一个栈空间来实现递归。
5、选择排序
① 选择排序
- 原理:从0索引开始,依次和后面的元素进行比较,小的元素往前面放,经过一轮比较后,最小的元素就会出现在最小索引处。
- 代码描述:
public static void main(String[] args)
{
//选择排序
int[] arr={24,69,80,57,13};
for (int i = 0; i < arr.length - 1; i++)
{
for (int j = i+1; j < arr.length; j++)
{
if(arr[i]>arr[j])
{
arr[i]=(arr[i]+arr[j])-(arr[j]=arr[i]);
}
}
}
System.out.println(Arrays.toString(arr));
}
② 堆排序
- 堆排序只需要一个记录大小的辅助空间,每个待排序的记录仅占有一个存储空间。
- 原理:是利用堆这种数据结构而设计的一种算法,基本思想:
1、将待排序的序列构成一个大顶堆,此时整个序列的最大值就是顶堆的根节点
2、将其与末尾元素进行交换,此时末尾就是最大值。
3、然后将剩余的n-1个元素重新构成一个堆了,这样会得到n个元素的次小值
4、如此反复执行,就会得到一个有序序列了。 - 大顶堆:根节点都会大于左右两个子节点
小顶堆:两个子节点都大于根节点
- 代码描述:
package org.westos.MyDemo2;
import java.util.Arrays;
public class MyTest8
{
public static void main(String[] args)
{
//堆排序
//定义一个数组jn
int[] arr={1,0,6,7,2,3,4};
//调整成为大顶堆的方法
//定义开始调整的位置
int startIndex=(arr.length-1)/2;
//循环开始调
for (int i = startIndex; i >= 0; i--)
{
toMaxHeap(arr,arr.length,i);
}
//经过上面的操作之后,已经把数组变成一个大顶堆,把根元素和最后一个元素进行调换
for (int i = arr.length-1; i > 0; i--)
{
//进行调换
arr[0]=(arr[0]+arr[i])-(arr[i]=arr[0]);
//换完之后,再把剩余元素调成大顶堆
toMaxHeap(arr,i,0);
}
System.out.println(Arrays.toString(arr));
}
/**
*
* @param arr 要排序的数组
* @param size 调整的元素个数
* @param index 从哪里开始调整
*/
private static void toMaxHeap(int[] arr, int size, int index)
{
//获取左右子节点的索引
int leftNodeIndex=index*2+1;
int rightNodeIndex=index*2+2;
//查找最大节点所对应的索引
int maxIndex=index;
if(leftNodeIndex<size&&arr[leftNodeIndex]>arr[maxIndex])
{
maxIndex=leftNodeIndex;
}
if(rightNodeIndex<size&&arr[rightNodeIndex]>arr[maxIndex])
{
maxIndex=rightNodeIndex;
}
//我们来调换位置
if(maxIndex!=index)
{
arr[index]=(arr[index]+arr[maxIndex])-(arr[maxIndex]=arr[index]);
//调换完之后可能影响下面的子树不是大顶堆,我们需要再次调换
toMaxHeap(arr,size,maxIndex);
}
}
}
6、基数排序
- 原理:不同于之前的各类排序,前面介绍的排序方法或多或少的是通过使用比较和移动记录的方法来实现排序,而基数排序的实现不需要进行对关键字的比较,只需要对关键字进行“分配”和“收集”两种操作即可完成。
- 代码描述:
package org.westos.MyDemo2;
import java.util.Arrays;
public class MyTest7
{
public static void main(String[] args)
{
//基数排序,通过分配再收集的方式进行排序
int[] arr={2,1,5,21,31,444,23,33,47,10,903,124,987,100};
//得确定排序轮次
//获取数组中的最大值
//int max=getMax(arr);
//基数排序
sortArray(arr);
System.out.println(Arrays.toString(arr));
}
private static void sortArray(int[] arr)
{
//定义二维数组,放10个桶
int[][] tempArr=new int[10][arr.length];
//统计数组
int[] c=new int[10];
int max=getMax(arr);//获取数组中的最大值
int len=String.valueOf(max).length();//确定轮次
for (int i = 0,n=1; i < len; i++,n*=10)
{
for (int j = 0; j < arr.length; j++)
{
//获取每个位的上数字
int ys=arr[j]/n%10;
tempArr[ys][c[ys]]=arr[j];
c[ys]++;
}
//取出桶中元素
int index=0;
for (int k = 0; k < c.length; k++)
{
if(c[k]!=0)
{
for (int h = 0; h < c[k]; h++)
{
//从桶中取出元素放回原数组
arr[index]=tempArr[k][h];
index++;
}
}
c[k]=0;//清除上一次统计的个数
}
}
}
private static int getMax(int[] arr)
{
int max=arr[0];
for (int i = 1; i < arr.length; i++)
{
if(arr[i]>max)
max=arr[i];
}
return max;
}
}
7、各种排序的比较讨论
(1)从平均时间性能而言,快速排序最佳,其所需时间最省,但快速排序在最坏情况
下的时间性能不如堆排序和归并排序。而后两者相比较的结果是,在n较大时,归并排序所需时间较堆排序省,但它所需的辅助存储量最多。
(2)上表中的“简单排序”包括除希尔排序之外的所有插入排序,起泡排序和简单选择排序,其中以直接插入排序为最简单,当序列中的记录“基本有序”或n值较小时,它是最佳的排序方法,因此常将它和其他的排序方法,诸如快速排序、归并排序等结合在一起使用。
(3)基数排序的时间复杂度也可写成O(d·n)。因此,它最适用于n值很大而关键字较小的序列,若关键字也很大,而序列中大多数记录的”最高位关键字”均不同,则亦可先按“最高位关键字”不同将序列分成若干“小”的子序列,而后进行直接插入排序。
(4)从方法的稳定性来比较,基数排序是稳定的内排方法,所有时间复杂度为O(n*n)的简单排序法也是稳定的,然而,快速排序、堆排序和希尔排序等时间性能较好的排序方法都是不稳定的。一般来说,排序过程中的“比较”是在“相邻的两个记录关键字”间进行的排序方法是稳定的。值得提出的是,稳定性是由方法本身决定的,对不稳定的排序方法而言,不管其描述形式如何,总能举出一个说明不稳定的实例来。反之,对稳定的排序方法,总能找到一种不引起不稳定的描述形式。由于大多数情况下排序是按记录的主关键字进行的,则所用的排序方法是否稳定无关紧要。若排序按记录的次关键字进行,则应根据问题所需慎重选择排序方法及其描述算法。
综上所述,在本章讨论的所有排序方法中,没有哪一种是绝对最优的。有的适用于n较大的情况,有的适用于n较小的情况,有的……因此,在实用时需根据不同情况适当选用,甚至可将多种方法结合起来使用。