java八大经典排序算法
排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。我们这里说说八大排序就是内部排序。
当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。
8种排序之间的关系:
当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短。
直接插入
基本思想
直接插入排序是将未排序的数据插入至已排好序序列的合适位置。
具体流程如下:
1、首先比较数组的前两个数据,并排序;
2、比较第三个元素与前两个排好序的数据,并将第三个元素放入适当的位置;
3、比较第四个元素与前三个排好序的数据,并将第四个元素放入适当的位置;
......
4、直至把最后一个元素放入适当的位置。
例子
{4,5,1,2,8,6,7,3,10,9}
取无序区间的第一个,从右向左扫描有序区间比较,方括号内可视为有序区间。
第一次:[4],5,1,2,8,6,7,3,10,9
第二次:[4,5],1,2,8,6,7,3,10,9
第三次:[1,4,5],2,8,6,7,3,10,9
第四次:[1,2,4,5],8,6,7,3,10,9
第五次:[1,2,4,5,8],6,7,3,10,9
第六次:[1,2,4,5,6,8],7,3,10,9
第七次:[1,2,4,5,6,7,8],3,10,9
第八次:[1,2,3,4,5,6,7,8],10,9
第九次:[1,2,3,4,5,6,7,8,10],9
第十次:[1,2,3,4,5,6,7,8,9,10]
算法分析
直接插入排序算法的空间复杂度为O(1)。
最好的情况,要比较的无序序列原本就是顺序有序的,那么要比较的次数是n-1,移动了0次,时间复杂度O(n)。
最坏的情况,要比较的无序序列原本就是逆序有序的,那么要比较的次数是(n+2)(n-1)/2,移动的次数(n+4)(n-1)/2,时间复杂度O(n²)。
直接插入排序的平均复杂度为O(n²)。
直接插入排序是稳定的。
Java实现
/**
* 直接插入排序
* 时间复杂度:O(n^2)
*@param data
*/
public static void insertSort(int[] data){
int temp;
for (int i = 1; i < data.length; i++) {
temp = data[i];//待插入数据
int j;
for(j = i - 1; j >= 0; j--) {
//判断是否大于temp,大于则后移一位
if(data[j] > temp) {
data[j+1] = data[j];
}else{
break;
}
}
data[j + 1] = temp;
}
}
希尔排序
基本思想
希尔排序严格来说是基于插入排序的思想,又被称为缩小增量排序。
具体流程如下:
1、将包含n个元素的数组,分成n/2个数组序列,第一个数据和第n/2+1个数据为一对...
2、对每对数据进行比较和交换,排好顺序;
3、然后分成n/4个数组序列,再次排序;
4、不断重复以上过程,随着序列减少并直至为1,排序完成。
假如有初始数据:25 11 45 26 12 78。
1、第一轮排序,将该数组分成 6/2=3 个数组序列,第1个数据和第4个数据为一对,第2个数据和第5个数据为一对,第3个数据和第6个数据为一对,每对数据进行比较排序,排序后顺序为:[25, 11, 45, 26,12, 78]。
2、第二轮排序,将上轮排序后的数组分成6/4=1个数组序列,此时逐个对数据比较,按照插入排序对该数组进行排序,排序后的顺序为:[11, 12, 25, 26, 45, 78]。
算法分析
对于插入排序而言,如果原数组是基本有序的,那排序效率就可大大提高。另外,对于数量较小的序列使用直接插入排序,会因需要移动的数据量少,其效率也会提高。因此,希尔排序具有较高的执行效率。
希尔排序并不稳定,O(1)的额外空间,时间复杂度为O(n²)。
Java实现
/**
* 希尔(shell)算法
* 时间复杂度:O(n^2)
*@param data
*/
public static voidshellSortSmallToBig(int[] data) {
int j = 0;
int temp = 0;
for (int increment = data.length / 2; increment > 0; increment /= 2){
for (int i = increment; i < data.length; i++) {
temp = data[i];
for (j = i - increment; j >= 0; j -= increment) {
if (temp < data[j]) {
data[j + increment] =data[j];
} else {
break;
}
}
data[j + increment] = temp;
}
}
}
简单选择
基本思想
选择排序是一种简单直观的排序算法,其基本原理如下:对于给定的一组记录,经过第一轮比较后得到最小的记录,然后将该记录的位置与第一个记录的位置交换;接着对不包括第一个记录以外的其他记录进行第二次比较,得到最小记录并与第二个位置记录交换;重复该过程,知道进行比较的记录只剩下一个为止。
算法分析
时间复杂度:假设有n个数据,数据交换的次数最多为n-1次,但程序的总体的比较次数较多。所以综合考虑有直接选择排序的时间复杂度为O(n2)(n的平方)。所以当记录占用字节数较多时,通常比直接插入排序的执行速度快些。
空间复杂度:直接选择排序的空间复杂度很好,它只需要一个附加单元用于数据交换,所以其空间复杂度为O(1)。
稳定性:由于在直接选择排序中存在着不相邻元素之间的互换,因此,直接选择排序是一种不稳定的排序方法。以数组{49,38,65,97,76,13,27,49}为例,
Java实现
/**
* 简单选择排序
* 时间复杂度:O(n^2)
*@param data
*/
public static void selectSort(int[] data) {
for (int i = 0; i < data.length; i++) {
int temp = data[i];
int flag = i; //将当前下标定义为最小值下标
for (int j = i + 1; j < data.length; j++) {
if (data[j] < temp) { //a[j] < temp 从小到大排序;a[j] >temp 从大到小排序
temp = data[j];
flag = j; //如果有小于当前最小值的关键字将此关键字的下标赋值给flag
}
}
if (flag != i) {
data[flag] = data[i];
data[i] = temp;
}
}
}
堆排序
基本思想
堆是一种特殊的树形数据结构,其每个节点都有一个值,通常提到的堆都是指一颗完全二叉树,根结点的值小于(或大于)两个子节点的值,同时,根节点的两个子树也分别是一个堆。
堆排序就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的 n-1 个序列重新构造成一个堆,这样就会得到 n 个元素中次大的值。如此反复执行,便能得到一个有序序列了。
堆排序的实现需要解决的两个关键问题:
(1)将一个无序序列构成一个堆。
(2)输出堆顶元素后,调整剩余元素成为一个新堆。
大根堆排序算法的基本操作
① 初始化操作:将R[1..n]构造为初始堆;
②每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
注意:
①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。
②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。
算法分析
堆排序的运行时间主要耗费在初始构建堆和在重建堆时反复筛选上。在构建对的过程中,因为我们是完全二叉树从最下层最右边的非终端节点开始构建,将它与其孩子进行比较和若有必要的互换,对每个非终端节点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
在正式排序时,第i次取堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个节点到根节点的距离为这里写图片描述),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。
所以总体来说,堆排序的时间复杂度为O(nlogn),由于堆排序对原始记录的状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)。这在性能上显然要远远好过于冒泡、简单选择、直接插入的时间复杂度了。
空间复杂度上,它只有一个用来交换的暂存单元,也非常的不错。不过由于记录的比较与交换是跳跃式进行的,因此堆排序也是一种不稳定的排序方法。
另外,由于出事构建堆所需要的比较次数比较多,因此,他并不适合待排序序列个数较少的情况。
Java实现
/**
* 构建大顶堆
*/
public static void adjustHeap(int[] data,int i, int len) {
inttemp, j;
temp = data[i];
for (j = 2 * i; j < len; j *= 2) { //沿关键字较大的孩子结点向下筛选
if (j < len && data[j] < data[j + 1]) {
++j; //j为关键字中较大记录的下标
}
if (temp >= data[j]) {
break;
}
data[i] = data[j];
i = j;
}
data[i] = temp;
}
/**
* 堆排序
* 时间复杂度:O(n^2)
*@param data
*/
public static void heapSort(int[] data) {
int i;
for (i = data.length / 2 - 1; i >= 0; i--) { //构建一个大顶堆
adjustHeap(data, i, data.length - 1);
}
for (i = data.length - 1; i >= 0; i--) { //将堆顶记录和当前未经排序子序列的最后一个记录交换
int temp = data[0];
data[0] = data[i];
data[i] = temp;
adjustHeap(data, 0, i - 1); //将a中前i-1个记录重新调整为大顶堆
}
}
冒泡排序
基本思想
设排序表长为n,从后向前或者从前向后两两比较相邻元素的值,如果两者的相对次序不对(A[i-1]> A[i]),则交换它们,其结果是将最小的元素交换到待排序序列的第一个位置,我们称它为一趟冒泡。下一趟冒泡时,前一趟确定的最小元素不再参与比较,待排序序列减少一个元素,每趟冒泡的结果把序列中最小的元素放到了序列的”最前面”。
算法分析
冒泡排序的时间复杂度为O(n^2),空间复杂度为O(1),它是一种稳定的排序算法。
1..如果我们的数据正序,只需要走一趟即可完成排序。所需的比较次数C和记录移动次数M均达到最小值,即:Cmin=n-1;Mmin=0;所以,冒泡排序最好的时间复杂度为O(n)。
2.如果很不幸我们的数据是反序的,则需要进行n-1趟排序。每趟排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
冒泡排序的最坏时间复杂度为:O(n2) 。
综上所述:冒泡排序总的平均时间复杂度为:O(n2) 。
Java实现
/**
* 冒泡排序
* 时间复杂度:O(n^2)
*@param data
*/
public static void bubbleSort(int[] data){
int j , k;
int flag = data.length ;//flag来记录最后交换的位置,也就是排序的尾边界
while (flag > 0){//排序未结束标志
k = flag; //k 来记录遍历的尾边界
flag = 0;
for(j=1; j<k; j++){
if(data[j-1] > data[j]){//前面的数字大于后面的数字就交换
//交换a[j-1]和a[j]
int temp;
temp = data[j-1];
data[j-1] = data[j];
data[j]=temp;
//表示交换过数据
flag = j;//记录最新的尾边界
}
}
}
}
快速排序
基本思想
快速排序是对冒泡排序的一种改进。首先在数组中选择一个基准点,然后分别从数组的两端扫描数组,设两个指示标志(lo指向起始位置,hi指向末尾),首先从后半部分开始,如果发现有元素比该基准点的值小,就交换lo和hi位置的值,然后从前半部分开始扫秒,发现有元素大于基准点的值,就交换lo和hi位置的值,如此往复循环,直到lo>=hi,然后把基准点的值放到hi这个位置。一次排序就完成了。以后采用递归的方式分别对前半部分和后半部分排序,当前半部分和后半部分均有序时该数组就自然有序了。
假设要排序的数组是A[1]……A[N],首先任意选取一个数据(通常选用第一个数据)作为关键数据,然后将所有比它的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一躺快速排序。一趟快速排序的算法是:
1)、设置两个变量I、J,排序开始的时候I:=1,J:=N;
2)以第一个数组元素作为关键数据,赋值给X,即X:=A[1];
3)、从J开始向前搜索,即由后开始向前搜索(J:=J-1),找到第一个小于X的值,两者交换;
4)、从I开始向后搜索,即由前开始向后搜索(I:=I+1),找到第一个大于X的值,两者交换;
5)、重复第3、4步,直到I=J。
算法分析
快速排序最“快”的地方在于左右两边能够快速同时递归排序下去,所以最优的情况是基准值刚好取在无序区的中间,这样能够最大效率地让两边排序,同时最大地减少递归划分的次数。此时的时间复杂度仅为O(NlogN)。
快速排序也有存在不足的情况,当每次划分基准值时,得到的基准值总是当前无序区域里最大或最小的那个元素,这种情况下基准值的一边为空,另一边则依然存在着很多元素(仅仅比排序前少了一个),此时时间复杂度为:O(n2) 。
Java实现
/**
* 将数组的某一段元素进行划分,小的在左边,大的在右边
* @param data
* @param start
* @param end
* @return
*/
public static intdivide(int[] data, int start, int end){
//每次都以最右边的元素作为基准值
int base = data[end];
//start一旦等于end,就说明左右两个指针合并到了同一位置,可以结束此轮循环。
while(start < end){
while(start < end &&data[start] <= base) {
//从左边开始遍历,如果比基准值小,就继续向右走
start++;
}
//上面的while循环结束时,就说明当前的a[start]的值比基准值大,应与基准值进行交换
if(start < end){
//交换
int temp = data[start];
data[start] = data[end];
data[end] = temp;
//交换后,此时的那个被调换的值也同时调到了正确的位置(基准值右边),因此右边也要同时向前移动一位
end--;
}
while(start < end &&data[end] >= base) {
//从右边开始遍历,如果比基准值大,就继续向左走
end--;
}
//上面的while循环结束时,就说明当前的a[end]的值比基准值小,应与基准值进行交换
if(start < end){
//交换
int temp = data[start];
data[start] = data[end];
data[end] = temp;
//交换后,此时的那个被调换的值也同时调到了正确的位置(基准值左边),因此左边也要同时向后移动一位
start++;
}
}
//这里返回start或者end皆可,此时的start和end都为基准值所在的位置
return end;
}
/**
* 快速排序
* 时间复杂度:O(n^2)
* @param data
* @param start
* @param end
*/
public static voidquickSort(int[] data, int start, int end){
if(start > end){
//如果只有一个元素,就不用再排下去了
return;
}else{
//如果不止一个元素,继续划分两边递归排序下去
int partition = divide(data, start,end);
quickSort(data, start, partition-1);
quickSort(data, partition+1, end);
}
}
归并排序
基本思想
归并排序就是利用归并的思想实现的排序方法。而且充分利用了完全二叉树的深度是的特性,因此效率比较高。其基本原理如下:对于给定的一组记录,利用递归与分治技术将数据序列划分成为越来越小的半子表,在对半子表排序,最后再用递归方法将排好序的半子表合并成为越来越大的有序序列。
经过第一轮比较后得到最小的记录,然后将该记录的位置与第一个记录的位置交换;接着对不包括第一个记录以外的其他记录进行第二次比较,得到最小记录并与第二个位置记录交换;重复该过程,知道进行比较的记录只剩下一个为止。
工作原理:
(1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置
(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
(4)重复步骤3直到某一指针达到序列尾
(5)将另一序列剩下的所有元素直接复制到合并序列尾
算法分析
一趟归并需要将数组 a[]中相邻的长度为h的有序序列进行两两归并.并将结果放到temp[]中,这需要将待排序列中的所有记录扫描一遍,因此耗费O(n),而又完全二叉树的深度可知,整个归并排序需要进行()次,因此总的时间复杂度为O(nlogn),而且这是归并排序算法中最好、最坏、平均的时间性能。
由于归并排序在归并过程中需要与原始序列同样数量的存储空间存放归并结果以及递归时深度为的栈空间,因此空间复杂度为O(n+logn).
另外,对代码进行仔细研究,发现merge函数中有if (a[i] < a[j]) 的语句,说明它需要两两比较,不存在跳跃,因此归并排序是一种稳定的排序算法。
也就是说,归并排序是一种比较占内存,但却效率高且稳定的算法。
Java实现
/**
* 归并排序
* @param data
* @param low
* @param mid
* @param high
*/
public static voidmerge(int[] data, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i= low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high){
if (data[i] < data[j]) {
temp[k++] = data[i++];
} else {
temp[k++] = data[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = data[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = data[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++){
data[k2 + low] = temp[k2];
}
}
/**
* 归并排序
* 时间复杂度:O(nlog2n)
* @param data
* @param low
* @param high
*/
public static voidmergeSort(int[] data, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
mergeSort(data, low, mid);//左边
mergeSort(data, mid + 1, high);//右边
merge(data, low, mid, high);//左右归并
}
}
基数排序
基本思想
像选择排序、插入排序、快速排序等都是基于两个元素的比较进行排序的。而基数排序无需进行元素比较,基于队列处理就能够达到排序的目的。
基数排序不是基于排序关键字来比较排序项,而是基于排序关键字的结构。对于排序关键字中的每一个数字或字符的每一种可能取值,都会创建一个单独的队列。队列的数目就称为基数。
例如:要排序全部由小写字母组成的字符串,则基数就是26,就会用到26个单独的队列。如果对十进制数进行排序,则基数应该是10。
为什么不是所有的排序都使用基数排序算法呢?
1.基数排序算法要根据给定问题特别设计;
2.如果排序关键字中的数字数目与列表中元素的数目接近,那么算法的时间复杂度接近O(n平方);
3.基数影响空间复杂度。
算法分析
在基数排序中,没有任何元素的比较和交换,元素只是在每一轮中从一个队列移动到另一个队列。对于给定的基数,遍历数据的轮次是一个常数,它与排序关键字的数目无关,于是,基数排序算法的时间复杂度为O(n)。
Java实现
/**
* 基数排序
* 时间复杂度:O(nlog2n)
* @param data
*/
public static voidradixSort(int[] data){
String temp;
int numObj;
int digit,num;
Queue<Integer>[] digitQueue =(LinkedList<Integer>[])(new LinkedList[10]);
for(int digitVal = 0; digitVal <= 9;digitVal++){
digitQueue[digitVal] =(Queue<Integer>)(new LinkedList<Integer>());
}
//sort
for(int pos = 0; pos <= 3; pos++){
for(int scan = 0; scan <data.length; scan++){
temp = String.valueOf(data[scan]);
digit =Character.digit(temp.charAt((3 - pos)), 10);
digitQueue[digit].add(newInteger(data[scan]));
}
num = 0;
for(int digitVal = 0; digitVal <= 9;digitVal++){
while(!(digitQueue[digitVal]).isEmpty()){
numObj =digitQueue[digitVal].remove();
data[num] = numObj;
num++;
}
}
}
}