排序算法总结

〇、排序前言

0.安利一波我的gitHub仓库,实现了该文章的代码,以及一些算法和数据结构的实现

https://github.com/lxycomeon/JavaAlgorithms.git

1.排序就是将一组对象按照某种逻辑顺序重新排列的过程。对于一个数组元素,元素也可能是对象,其中每个元素都有一个主键,排序算法的目的就是将所有元素的主键进行排列(通常是按照大小或者是字母顺序)。排序后索引较大的主键大于等于索引较小的主键。

2.评价排序算法好坏的一些定义

  1. 时间复杂度:指执行该算法所需要的计算工作量,也即是语句执行次数。其中还包括最坏,最好,平均时间复杂度。
  2. 空间复杂度:指执行该算法所需要的内存空间。
  3. 稳定性:假定在待排序的记录序列中,存在多个相同关键字的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法为稳定的,否则称为不稳定排序算法。若果排序的对象是一个简单的数字,那么稳定性将毫无意义,但是若果是一个复杂对象,比如由多个数字属性或者其他属性,比如按照销量高低排序的基础上仍然按照价格高低的排序,则需要使用稳定性算法。

3.多数情况下一个排序算法只会通过两个方法操作数据,那就是比较方法和交换方法,在下面的排序算法的模板中我们通过实现数组元素对象Comparable接口中的compareTo方法实现这两个方法。该排序算法模板适用于任何实现了Comparable接口的数据类型,此方法的好处是Java中很多常用的数据类型都实现了该接口,因此我们可以直接用这些数据类型的数组作为参数调用我们的排序方法。

package sort;

public class SortUtils {

	public static void sort(Comparable[] a) {
		
	}
	
	//对数组两个元素进行比较
	private static boolean less(Comparable v,Comparable w) {
		return v.compareTo(w)<0;
	}
	
	//交换数组元素的位置
	private static void exch(Comparable[] a,int i ,int j) {
		Comparable t = a[i]; a[i] = a[j];a[j]= t;
	}
	
	//展示数组元素以,隔开
	private static void show(Comparable[] a) {
		for (int i = 0; i < a.length; i++) {
			System.out.println(a[i]);
		}
	}
	
	//测试数组元素是否有序
	private static boolean isSorted(Comparable[] a) {
		for (int i = 1; i < a.length; i++) {
			if(less(a[i],a[i-1])) return false;
		}
		return true;
	}
	
	//测试sort中的排序算法
	public static void main(String[] args) {

	}

}

一、冒泡排序

冒泡排序是一种极其jian简单的排序算法。它重复的走访要排序的元素,依次比较相邻的两个元素,如果他们的顺序错误就把他们调换过来,直到meiy没有元素需要再次交换,排序完成。这个算法的名字由来是因为越小或者越大的元素会经由慢慢浮到数列顶端。

冒泡排序算法的步骤:

1.比较相邻的元素,如果前一个比后一个大,就把他们两个调换位置。

2.对每一对相邻的元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后最后的元素会是最大的数。

3.针对所有的元素重复以上步骤,除了最后那个最大的数。

4.持续每次对越来越少的元素重复上面的步骤,直到没有一对数字需要比较,排序完成。

冒泡排序代码:

	public static void BubbleSort(Comparable[] a) {
		for (int i = 0; i < a.length-1; i++) {
            boolean didSwap;    //保证最好复杂度,若为正序则直接返回结果
			for (int j = 1; j < a.length-i; j++) {
                didSwap = false;
				if(less(a[j],a[j-1])) {
                        exch(a,j,j-1);
                        didSwap = true;
                    }  
                if(didSwap == false)
                    return;  
			}
		}
	}

冒泡排序算法总结:

1.该算法时间复杂度的计算,若我们数据本来就是正序的,那么比较次数即为(n-1)n,交换次数为0.所以其时间复杂度按照上面的代码运行也应该是O(n^2);但是很多地方介绍说冒泡排序时间复杂度的最好情况为O(n),找了一些资料,发现那个是改进后的冒泡排序算法,添加了一个辅助变量(didSwap ),在比较时若没有发生交换,则证明该原始数据为正序,直接fanh返回就可以了。

2.该算法没有用到额外的辅助空间,所以空间复杂度为O(1);且该算法为稳定的。

二、选择排序

选择排序是这样的一个思想:首先找到数组中最小的那个元素,其次将它和数组的第一个元素交换位置。然后再从剩下的元素中找到最小的元素,将它和数组的第二个元素交换位置,如此类推,直到将整个数组排序完毕。因为它是不断的选择剩余元素中最小的,所以叫做选择排序。

选择排序算法的步骤:

1.记录数组中第一个数到最后一个数当中最小的数的下标位置,并将其与第一个元素交换位置。

2.记录数组中第i个数到最后一个数当中最小的数的下标位置,并将其与第i个元素交换位置。

3.知道u等于数组chan长度,也即此时整个数组排序完毕。

选择排序算法的代码:

	//选择排序
	public static void Selectionsort(Comparable[] a) {
		int N = a.length;
		for (int i = 0; i < a.length; i++) {
			int min = i;	//最小元素的索引
			for (int j = i+1; j < a.length; j++) {
				if(less(a[j], a[min])) min = j;
			}
            exch(a, i, min);
		}
		
	}

选择排序算法总结:

1.选择排序的运行时间与输入数组的随机性无关,由于每次为了找出最小的元素都要将数组扫描一遍且并不能为下一遍提供什么信息,所以无论是排序一个已经有序的数组还是一个元素随机排列的数组,它所用的时间都是一样长的。但是排序算法的数据移动是最少的,只用了N此交换,与数组的大小具有线性关系。

2.排序算法的时间复杂度为O(n^2)无论在什么情况都是这样,也没有用到额外的空间,空间复杂度为O(1),并且由于最小元素要与数组中的其他元素进行位置交换,所以可能会改变其他元素的相对次序,所以它是一个不稳定的算法。

三、插入排序算法

插入排序算法就是类似于我们抓扑克牌,将每一张排插入到其他已经有序的牌中的适当位置,在算法的实现中,我们还需要反复的将已经排序好的元素zhub逐步向后挪位,为新插入的元素提供插入的空间。

插入排序算法的步骤:

1.从第一个元素开始,认为该元素已经被排序了。

2.依次取出接下来的每个未排序的元素,在已经排序的元素序列中从后往前扫描,若该(已排序)元素大于新的元素则将元素移动到下一个位置,直到找到已经排序的元素小于或者等于新插入元素的位置。则将新的元素插入到该位置。

3.重复步骤,直到所有元素插入完成。

插入排序算法的代码:

//插入排序
	public static void InsertionSort(Comparable[] a) {
		int N = a.length;
		for (int i = 0; i < a.length; i++) {
			for (int j = i; j >0 && less(a[j], a[j-1]); j--) {
				exch(a, j, j-1);
			}
		}
		
	}

插入排序算法总结:

1.插入排序算法的时间复杂度和排序数组的有序性有很大的关系,若原数组为有序或者部分有序时,十分高效。对于原升序的数组,其时间复杂度为O(n),最坏的情况就是对降序数组进行排序,此时时间复杂度为O(n^2).平均时间复杂度也为O(n^2)。

2.对于插入排序,如果每个都比较的操作代价比交换操作大的话,可以采用二分查找来减少比较的次数,可以称为二分插入排序,由于已经插入的数组是排好序的,所以我们可以用二分法确定待插入新数据的合适位置。

3.插入排序没有用到额外的空间,空间复杂度为O(1),并且它是一个稳定的算法。

四、希尔排序算法

希尔排序算法是一种基于插入排序的快速的排序算法,对于大规模的乱序数组插入排序很慢,因为它只能交换相邻的元素。而希尔排序为了加快速度对插入排序进行了改进,交换不相邻的元素以对数组的局部进行排序,并最终利用插入排序将局部有序的数组排序。它将每个元素可以一次性地朝着最终位置前进一大步,然后再取越来越小的步长进行排序,最后一步就是普通的插入排序,但此刻需要排序的数据几乎是已经排列好的了,此时的插入排序

很快。

希尔排序的思想是使数组中任意间隔为h的元素都是有序的,这样的数组被称为h有序数组。一个h有序数组就是h个相互独立的有序数组编织在一起组成的一个数组。

希尔排序算法的步骤:

1.选择一个增量序列比如h=3*h+1,其中h<=排序数组长度,一个递增的增量序列

2.根据增量序列的个数k,对序列进行k趟排序;

3.每趟排序,根据对应的增量h,将待排序数组分割为若干长度为m的子序列,分别对各子表进行直接插入排序。当最后增量因子为1时,整个序列作为一个表来处理。即完成最后的插入排序。

希尔排序算法的代码:

//希尔排序
	public static void ShellSort(Comparable[] a) {
		int N = a.length;
		int h = 1;
		while(h<N/3) h = 3*h+1;	//构建一个递增数列h,每排序一次得到一个h有序数组,称为初始增量
		while( h >= 1) {//将数组变为h有序数组
			for (int i = h; i < N; i++) {
				for (int j = i; j >= h && less(a[j], a[j-h]); j-=h) {
					exch(a, j, j-h);
				}
			}
			h = h/3;		//递减增量
		}
		
	}

希尔排序算法的总结:

1.希尔排序是第一个突破了O(n^2)时间复杂度的算法,它更高效的原因是它权衡了子数组的规模和有序性,排序之初各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。

2.希尔排序的平均时间复杂度为O(n^1.3),最好为O(n),最坏为O(n^2),并且它一个不稳定的排序,因为每个子数组进行排序时可能对原顺序进行dalu打乱,辅助空间为O(1)。

3.希尔排序中涉及到了一个递增序列,但是如何选择递增序列。很多论文研究了各种不同的递增序列,但是都无法证明某个序列是最好的。所以一般使用上述所用的递增序列。

五、快速排序算法

快速排序可以说是应用最为广泛的一种排序算法了,因为它实现简单并且适用于各种不同的输入数据且在一般的应用中都比其他排序算法都要快得多。它是一种分治的排序算法,它将一个数组分为两个子数组,将两部分独立的排序。相比于归并排序,归并排序是将数组分为两个子数组分别排序,并将有序的子数组归并以将整个数组排序,归并排序的递归调用发生在处理整个数组之前;而快速排序是当两个子数组都有序时整个数组也就是自然有序了。它的递归调用发生在处理整个数组之后,且切分的位置要取决于数组的内容

快速排序算法步骤:

1.从序列数组中挑出一个元素作为基准(一般可以取数组的第一个元素)。

2.把所有比基准值小的元素放在基准的前面,所有比基准值大的元素放在基准的后面,相同的数可以放到任意一边。称为分区操作。

3.对每个分区递归的进行步骤1-2,递归的结束条件是分区后序列的大小是0或者1,这时候整体就已经被排序好了

快速排序算法的代码:

//快速排序
	//快速排序的切分
	private static int partition(Comparable[] a, int lo ,int hi) {
		int i = lo ,j = hi+1;	//定义一个左右扫描指针
		Comparable v = a[lo];	//先找到一个切分的元素
		while(true) {	//扫描左右,检查扫描是否结束并交换元素
			while (less(a[++i], v)) 
				if(i == hi) break;
			
			while(less(v, a[--j]))
				if(j == lo) break;
			if(i >= j) break;
			exch(a, i, j);
		}
		
		exch(a, lo, j);	//将v=a[j]放入正确位置
		return j;		//完成切分 ,即a[lo...j-1] <= a[j] <= a[j+1...hi]
	}
	
	private static void QuickSort(Comparable[] a) {
		//为了保证快速排序的性能,先将输入的数组进行打乱.消除对输入的依赖性
		List list = new ArrayList<>();
		for (int i = 0; i < a.length; i++) {
			list.add(a[i]);
		}
		Collections.shuffle(list);
		for (int i = 0; i < a.length; i++) {
			a[i] = (Comparable) list.get(i);
		}
		list = null;	//释放内存
		
		QuickSort(a,0,a.length-1);
	}
	//递归实现快排
	private static void QuickSort(Comparable[] a, int lo, int hi) {
		if(hi <= lo ) return;
		int j = partition(a, lo, hi);//切分
		QuickSort(a, lo, j-1);		//将左半部分进行排序
		QuickSort(a, j+1, hi);		//将切分后的右半部分进行排序
	}
	

快速排序算法的总结:

1.快速排序中关键的部分在于切分,因为每一次切分总是能够排定一个元素,所以当切分数组的大小为1或者0时,可以证明递归能够正确的将数组排序。

2.快速排序算法的缺点就是比较脆弱,因为选择切分元素会对算法的性能有很大的影响,所以为了避免低劣的性能,我们在排序z之前利用shuffle函数将数组进行了打乱操作。

3.该算法在平均状况下的时间复杂度为O(nlogn),最坏情况为O(n^2),并且他是不稳定的算法,不稳定发生在基准交换位置的时候,可能有与基准相等的元素的原始顺序被打乱。并且Java系统提供的Array。sort函数对于基础数据类型排序时就使用了三向快速排序。但是对于非基础类型,使用了归并排序,因为非基础类型对数据的稳定性有一些要求,所以使用了稳定的归并排序。

六、归并排序算法

归并排序算法的一般思路就是基于一个归并的简单操作,将两个有序的数组归并成一个更大的有序数组。所以归并排序采用了递归排序算法,先将待排序数组分为两半,归并排序最吸引人的性质是它能够保证将任意长度为N的数组排序所需要的时间和NlogN成正比,它的主要缺点则是它所需要的额外的空间和N成正比。

归并排序的实现分为递归实现非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。

归并排序算法步骤:

归并排序算法主要依赖归并操作,下面主要写一下归并的操作步骤

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

归并排序的代码:

//归并排序
	private static Comparable[] aux;	//归并排序辅助空间

	//归并操作   //将a[lo,mid]和a[mid+1,hi]归并
	public static void merge( Comparable[] a, int lo, int mid,int hi) {
		
		int i = lo,j = mid+1;
		for (int k = lo; k <= hi; k++) {
			aux[k] = a[k]; 
		}
		
		for (int k = lo; k <= hi; k++) {
			if	    (i > mid) 				a[k] = aux[j++];
			else if (j > hi) 				a[k] = aux[i++];
			else if (less(aux[j],aux[i])) 	a[k] = aux[j++];
			else  						 	a[k] = aux[i++];
		}
	}
	
	public static void MergeSort(Comparable[] a) {
		aux = new Comparable[a.length];//一次性分配所需要的额外空间
		MergeSort(a,0,a.length-1);
	}
	
	//通过递归的归并排序程序
	private static void MergeSort(Comparable[] a, int lo, int hi) {
		if(hi <= lo) return;
		int mid = lo + (hi-lo)/2;
		MergeSort(a,lo,mid);		//将左半边排序
		MergeSort(a, mid+1, hi);	//将右半边排序
		merge(a, lo, mid, hi);		//将结果归并
	}
	
	//非递归形式的归并排序(自底向上,比较适合用链表组织的数据结构)
	public static void MergeSort1(Comparable[] a) {
		int N = a.length;
		aux = new Comparable[N];
		for (int sz = 1; sz < N; sz=sz+sz) {
			for (int lo = 0; lo < N-sz; lo += sz+sz) {
				merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
			}
		}
	}

归并排序算法总结:

1.归并排序是稳定的排序算法,它的平均时间复杂度为O(nlogn),无论在好的情况还是坏的情况都是NlogN。

七、堆排序算法

堆排序算法是基于堆这种数据结构所设计的一种选择排序算法,并且它可以利用优先队列这种数据结构改造为堆排序算法,将所有元素插入一个查找最小元素的优先队列,然后再重复调用删除最小元素的操作将它们按顺序取出即变成了一个堆排序的操作。堆是一种近似与完全二叉树的结构,但通常利用一维数组去实现该结构。当每个父节点的值总是大于它的孩子结点,这个时候就叫做最大堆。

堆排序算法步骤:

1.由输入的无序数组构造一个最大堆(构造一个最大堆只需要扫描一半的元素将其上浮到合适的位置,因为叶子结点没有孩子结点,所以所有的叶子结点满足最大堆),作为一个初始的无序区。

2.把顶堆元素和尾堆(数组尾部)元素进行互换。

3.把堆的尺寸缩小1,并调用maxHeapify(0,A)从新的堆顶元素进行堆的调整。

4.重复步骤2-3,直到堆的尺寸为1,即排序完成。

堆排序算法的代码实现:

//堆排序
	public static int heapSize;		//需要额外定义一个静态变量表示堆大小
	public static int parent(int i) {return (i - 1) / 2;}
	public static int left(int i) {return 2 * i + 1;}
	public static int right(int i) {return 2 * i + 2;}
	public static void maxHeapify(int i, Comparable[] A){
	    int l = left(i);
	    int r = right(i);
	    int largest = i;
	    if (l <= heapSize - 1 && less(A[i], A[l])   )
	        largest = l;
	    if (r <= heapSize - 1 && less(A[largest], A[r]) )
	        largest = r;
	    if (largest != i) {
	        exch(A, i, largest);
	        maxHeapify(largest,A);
	    }
	}
	
	public static void buildMaxHeap(Comparable [] a){
		heapSize =a.length; 
		
	     for (int i = parent(heapSize - 1); i >= 0; i--)
	         maxHeapify(i,a);        
	 }
	
	public static void heapsort(Comparable[] A){
	    buildMaxHeap(A);
	    
	    int step = 1;
	    for (int i = heapSize - 1; i > 0; i--) {
	        exch(A, i, 0);	//将顶堆元素与尾堆元素互换
	        heapSize--;		//堆的尺寸缩小1
	        System.out.println("Step: " + (step++) + Arrays.toString(A));
	        maxHeapify(0,A);	//从新的顶堆元素进行堆调整,重新调整为最大堆
	    }        
	}

堆排序算法总结:

1.堆排序是我们所知道的唯一的能够同时最优的利用空间和时间的方法,在最坏的情况下它也能够保证2NlogN的时间复杂度,但现在系统的许多应用很少使用它,因为它无法利用缓存。

2.堆排序是一个不稳定的排序算法,不稳定性发生在顶堆元素与A【i】交换位置的时刻。时间复杂度都是在nlogN。

八、排序算法总结

九、参考文档

1.博客:http://www.cnblogs.com/eniac12/p/5329396.html

2.算法(第四版)书籍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值