十大排序算法(Java实现)

目录

概述

一、插入排序

1)直接插入排序

2)希尔排序

二、交换排序

1)冒泡排序

2)快速排序

三、选择排序

1)简单选择排序

2)堆排序

四、归并排序

五、计数排序

六、桶排序

七、基数排序

各类排序的时间复杂度、空间复杂度、稳定性总结


概述

排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

我们这里说的十大排序就是内部排序。


一、插入排序

1)直接插入排序

基本思想:

插入排序的基本思想就是将无序序列插入到有序序列中。例如要将数组a=[49,38,65,97,76,13,27,49]排序,可以将49看做是一个有序序列,将[38,65,97,76,13,27,49]看做一个无序序列。无序序列中38比49小,于是将38插入到49的左边,此时有序序列变成了[38,49],无序序列变成了[65,97,76,13,27,49]。无序序列中65比49大,于是将65插入到49的右边,有序序列变成了[38,49,65],无序序列变成了[97,76,13,27,49]。以此类推,最终数组按照从小到大排序。

代码实现:

public void InsertSort(int[] a){
	for(int i=1;i<a.length;i++){
		if(a[i]<a[i-1]){//<,需要将a[i]插入有序子表
			int j;
			int temp=a[i];
			for(j=i-1;j>=0 && temp<a[j];j--)
				a[j+1]=a[j];//记录后移
			a[j+1]=temp;//插入到正确位置(temp赋给a[j+1],不是赋给a[j]。j--后不符合条件退出循环,所以j要加1)
		}
	}
}

时间复杂度:O(n^2)

插入排序是稳定的

其他插入排序有折半插入排序,2-路插入排序,表插入排序

 

2)希尔排序

基本思想

希尔排序是对插入排序的优化,先将待排记录序列分割成为若干子序列分别进行插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序。希尔排序的划分子序列不是像归并排序那种的二分,而是采用的叫做增量的技术(d=5,d=3,d=1)。

代码实现

public void ShellSort(int[] a){
	int d;//d为增量
	for(d=a.length/2;d>=1;d/=2){
		//插入排序的一轮
		for(int i=d;i<a.length;i++){
			if(a[i]<a[i-d]){
				int j;
				int temp=a[i];
				for(j=i-d;j>=0 && temp<a[j];j-=d)
					a[j+d]=a[j];
				a[j+d]=temp;
			}
		}
	}
}

时间复杂度:希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。增量因子序列可以有各种取法,但增量因子中除1 外没有公因子,且最后一个增量因子必须为1。

希尔排序是不稳定的

 

 

二、交换排序

1)冒泡排序

基本思想

相邻两节点进行比较,大的向后移一个,经过第一轮两两比较和移动,最大的元素移动到了最后,第二轮次大的位于倒数第二个,依次进行。这是最基本的冒泡排序。

代码实现

public void BubbleSort(int[] a){
	for (int i=0;i<a.length-1;i++) {//外层循环控制排序的趟数
		for(int j=0;j<a.length-1-i;j++){//内层循环控制每趟排序
			if(a[j]>a[j+1]){
				int temp=a[j];
				a[j]=a[j+1];
				a[j+1]=temp;
			}
		}
	}
}

时间复杂度:O(n^2)

冒泡排序是稳定的

 

2)快速排序

基本思想

a)一趟排序的过程

b)排序的全过程

快速排序是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,已达到整个序列有序。一趟快速排序的具体过程可描述为:从待排序列中任意选取一个记录(通常选取第一个记录)作为基准值(pivot),然后将记录中关键字比它小的记录都安置在它的位置之前,将记录中关键字比它大的记录都安置在它的位置之后。这样,以该基准值为分界线,将待排序列分成两个子序列。

代码实现(递归):

 public int Patition(int[] a,int low,int high){//一趟快速排序
	int pivot=a[low];
	while(low<high){
		while(low<high && pivot<=a[high])
			high--;
		if(low<high){
			a[low]=a[high];
			low++;
		}
		while(low<high && pivot>=a[low])
			low++;
		if(low<high){
			a[high]=a[low];
			high--;
		}
	}
	a[low]=pivot;
	return low;
}
	
public void QuickSort(int[] a,int low,int high){
	if(low<high){
		int pivotLoc=Patition(a,low,high);
		QuickSort(a,low,pivotLoc-1);
		QuickSort(a,pivotLoc+1,high);
	}
}
//调用QuickSort方法是传入的low为0,high为a.length-1

时间复杂度:平均时间复杂度O(n log n),若初始记录序列按关键字有序或基本有序时,快速排序将蜕化为冒泡排序,时间复杂度为O(n^2)

快速排序是不稳定的(例如序列5 3 3 4 3 8 9 10 11,基准元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在基准元素和其他交换的时刻。)

 

 

三、选择排序

1)简单选择排序

基本思想

在要排序的一组数中,选出最小的数与第1个位置的数交换;然后在剩下的数当中再找最小的数与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。

代码实现

public void SelectionSort(int[] a){
	for(int i=0;i<a.length;i++){
		int pos=i;
		for(int j=i+1;j<a.length;j++){
			if(a[pos]>a[j])
				pos=j;
		}
		if(i!=pos){
			int temp=a[pos];
			a[pos]=a[i];
			a[i]=temp;
		}
	}
}

时间复杂度:O(n^2)

选择排序是不稳定的(序列5 8 5 2 9,第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。)

 

2)堆排序

基本思想

堆的定义如下: n个元素的序列{k1, k2, ... , kn}当且仅当满足以下条件时,称之为堆。

      

可以将堆看做是一个完全二叉树。并且,每个结点的值都大于等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于等于其左右孩子结点的值,称为小顶堆。

初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储顺序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。

因此,实现堆排序需解决两个问题:

1. 如何将n 个待排序的数建成堆;

2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。


首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。

调整小顶堆的方法:

1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶(最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。

2)将根结点与左、右子树中较小的元素进行交换。

3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).

4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).

5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。

称这个自根结点到叶子结点的调整过程为筛选。如图:


再讨论对n 个元素初始建堆的过程。

建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。

1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。

2)筛选从第个结点为根的子树开始,该子树成为堆。

3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。

如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
 

 

代码实现

//大顶堆
public void HeapAdjust(int[] a,int i,int length){
	int child=2*i+1;//child为左孩子节点的下标
	while(child<length){
		if(child+1<length && a[child]<a[child+1])//child+1为右孩子节点的下标
			child++;//child记录较大的孩子节点的下标
		if(a[i]<a[child]){//如果较大孩子节点大于父节点,那么将较大孩子节点向上移动,替换父节点
			int temp=a[i];
			a[i]=a[child];
			a[child]=temp;
			i=child;//重新设置i,即待调整的下一个节点的位置
			child=2*i+1;	
		}
		else//如果父节点大于它的孩子节点,则不需要调整,直接退出
			break;
	}
}
	
public void HeapSort(int[] a){
	//初始堆(a.length/2-1是第一个非叶子节点)
	for(int i=a.length/2-1;i>=0;i--){
		HeapAdjust(a,i,a.length);
	}
	//从最后一个元素开始对序列进行调整,直到第一个元素
	for(int i=a.length-1;i>0;i--){
		int temp=a[i];
		a[i]=a[0];
		a[0]=temp;
		HeapAdjust(a,0,i);
	}
}

时间复杂度

设树深度为k,。从根到叶的筛选,元素比较次数至多2(k-1)次,交换记录至多k 次。所以,在建好堆后,排序过程中的筛选次数不超过下式: 

                                

而建堆时的比较次数不超过4n 次,因此堆排序最坏情况下,时间复杂度也为:O(nlogn )。

堆排序是不稳定的

 

 

四、归并排序

基本思想

“归并”的含义是将两个或两个以上的有序序列组合成一个新的有序表。假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到(表示不小于x的最小整数)个长度为2(或者是1)的有序子序列,再两两归并。如此重复,直到得到一个长度为n的有序序列为止。这种排序方法称为2-路归并排序。

代码实现(递归):

    public void Merge(int[] a,int start,int mid,int end,int[] temp){
        int i=start,j=mid+1;
        int k=0;
        while(i<=mid && j<=end){
            if(a[i]<=a[j]){
                temp[k++]=a[i++];
            }else{
                temp[k++]=a[j++];
            }
        }
        while(i<=mid){
            temp[k++]=a[i++];
        }
        while(j<=end){
            temp[k++]=a[j++];
        }
        //复制回原数组
        k=0;
        while(start <= end){
            a[start++] = temp[k++];
        }
    }

    public void MergeSort(int[] a,int start,int end) {
        int[] temp = new int[a.length];//排序前先建好长度等于原数组长度的临时数组
        if (start < end) {
            int mid = (start + end) / 2;
            MergeSort(a, start, mid);//左边归并排序
            MergeSort(a, mid + 1, end);//右边归并排序
            Merge(a, start, mid, end, temp);//将两个有序的子数组合并
        }
    }

时间复杂度:O(n log n)

归并排序是稳定的

 

 

五、计数排序

基本思想

计数排序要求输入的数据必须是有确定范围的整数。

算法的步骤如下:

(1)找出待排序的数组中最大和最小的元素

(2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项

(3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)

(4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

代码实现:

   public int[] countingSort(int[] a){
        int max=max(a);
        int[] bucket=new int[max+1];
        int[] result=new int[a.length];
        for(int val:a){
            bucket[val]++;
        }

        int j=0;
        for(int i=0;i<bucket.length;i++){
            while(bucket[i]>0){//i存放的是值,bucket[i]存放的是值出现的次数
                result[j++]=i;
                bucket[i]--;
            }
        }
        return result;
    }

    public int max(int[] a) {
        int max=a[0];
        for(int i=1;i<a.length;i++){
            if(a[i]>max){
                max=a[i];
            }
        }
        return max;
    }

时间复杂度:O(n+k)

 

 

六、桶排序

基本思想

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

1)在额外空间充足的情况下,尽量增大桶的数量

2)使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

示意图:

元素分布在桶中:

然后,元素在每个桶中排序:

代码实现:

    public void bucketSort(int[] a){
        int max=Integer.MIN_VALUE;
        int min=Integer.MAX_VALUE;
        for(int i=0;i<a.length;i++){
            max=Math.max(max,a[i]);
            min=Math.min(min,a[i]);
        }
        //桶数
        int bucketNum=(max-min)/a.length+1;
        List<List<Integer>> list=new ArrayList<>();
        for(int i=0;i<bucketNum;i++){
            list.add(new ArrayList<>());
        }
        //将每个元素放入桶
        for(int i=0;i<a.length;i++){
            int num=(a[i]-min)/a.length;
            list.get(num).add(a[i]);
        }
        //对每个元素进行排序
        for(int i=0;i<list.size();i++){
            Collections.sort(list.get(i));
        }
        System.out.println(list.toString());
    }

时间复杂度:O(n+k)

 

 

七、基数排序

 


各类排序的时间复杂度、空间复杂度、稳定性总结

说明:

当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);

而快速排序则相反,时间复杂度提高为O(n^2);

原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。

一般不使用或不直接使用传统的冒泡排序。

基数排序是一种稳定的排序算法,但有一定的局限性:

1)关键字可分解。

2)记录的关键字位数较少,如果密集更好

3)如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。

 

稳定性:

排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。 

稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较。

 

选择排序算法准则:

影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:

1)待排序的记录数目n的大小;

2)记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;

3)关键字的结构及其分布情况;

4)对排序稳定性的要求。

设待排序元素的个数为n:

a)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。

快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短

堆排序 : 如果内存空间允许且要求稳定性的

归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高

b)当n较小,可采用直接插入或直接选择排序。

直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数

直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序

 

参考:

https://blog.csdn.net/a3192048/article/details/80269862

https://www.cnblogs.com/zhaoshuai1215/p/3448154.html

https://blog.csdn.net/liang_gu/article/details/80627548

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值