八大排序算法总结

      (此文章源代码及更多算法的代码都在git

       排序有内部排序和外部排序之分,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。我们这里说的八大排序算法均为内部排序。


1.   直接插入排序(Insert Sort)

       基本思想:将待排序的无序数列看成是一个仅含有一个元素的有序数列和一个无序数列,将无序数列中的元素逐次插入到有序数列中,从而获得最终的有序数列。

       算法流程:

       1)对第i趟排序,数组前i-1项元素为已排序,将第i项元素插入已排序数组

       2)找到合适的位置后,更大/更小的元素向右移动一个位置,插入当前数据

       3)以i=1...n重复执行步骤1和2

       复杂度分析:

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

       2) 空间复杂度: O(1)

       Java实现代码:

	public static void InsertSort(int[] a){
		int j;
		for(int i = 1; i < a.length; i++){
			int temp = a[i];
			for(j = i; j > 0 && temp < a[j-1]; j--){
				a[j] = a[j-1];
			}
			a[j] = temp;
		}
	}
	
	//数组局部排序
	public static void InsertSort(int[] a, int left, int right){
		int j;
		for(int i = left + 1; i < right; i++){
			int temp = a[i];
			for(j = i; j > 0 && temp < a[j-1]; j--){
				a[j] = a[j-1];
			}
			a[j] = temp;
		}
	}

2.   希尔排序(Shell Sort)

       希尔排序是1959 年由D.L.Shell 提出来的,是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。  

       基本思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对所有元素进行一次直接插入排序。

       算法流程:

       1)希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序。

       2)随着增量逐渐减少,每组包含的关键词越来越多,直至增量减至1。

       复杂度分析:

       1) 时间复杂度:O(n^s),1<s<2,在元素基本有序的情况下,效率很高。希尔排序是一种不稳定的排序算法。

       2) 空间复杂度:O(1)

       Java实现代码:

public static void ShellSort(int[] a){
		int gap, i, j;
		for(gap = a.length / 2; gap > 0; gap /= 2){
			for(i = gap; i < a.length; i++){
				int temp = a[i];
				for(j = i; j >= gap && temp < a[j - gap]; j -= gap){
					a[j] = a[j - gap];
				}
				a[j] = temp;
			}
		}
}

3.   简单选择排序(Select Sort)

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

       算法流程:

       1)从待排序的数据元素中选出最小/最大的一个元素,存放在序列的起始位置

       2)以i=1...n重复步骤1。

       复杂度分析:

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

       2) 空间复杂度: O(1)

       Java实现代码:

	public static void SelectSort(int[] a){
		for(int i = 0; i < a.length; i++){
			int min = i;
			for(int j = i + 1; j < a.length; j++){
				if (a[j] < a[min]) { //如果有小于当前最小值的元素
					min = j;
                }
			}
			if (i != min){
				swap(a, i, min);
			}
		}
	}
	
	private static void swap(int[] a, int x, int y){
		int temp = a[x];
		a[x] = a[y];
		a[y] = temp;
	}

4.   堆排序(Heap Sort)

       基本思想:构造大顶/小顶堆,将堆顶元素放置到堆最后位,将堆的大小缩减1并调整出新的大顶/小顶堆,重复操作直至堆的大小为1。

       算法流程:

       1)以线性时间建立一个堆

       2)调整堆/下滤(每次将最大/最小元素"上浮"至堆顶)

       3)将堆中的最后元素与堆顶元素交换,然后将堆的大小缩减1并进行步骤2

       复杂度分析:

       1) 时间复杂度: O(nlogn)

       2) 空间复杂度: O(1)

       Java实现代码:

	public static void HeapSort(int[] a){
		int i;
		int len = a.length;
		for(i = len / 2 - 1; i >= 0; i--){  /*调整堆*/
			adjustMinHeap(a, i, len);  
		}
		for(i = len - 1; i > 0; i--){
			swap(a, 0, i);                    /*删除最大值*/
			adjustMinHeap(a, 0, i);         /*调整堆*/ 
		}
	}
	
	/**
	 * 描述:返回左孩子
	 * @param i  堆中的元素的下标
	 * @return   左孩子的下标
	 */
	private static int leftChild(int i){
		return 2 * i + 1;
	}
	
	/**
	 * 描述:调整小顶堆
	 * @param a  需比较的项目的数组
	 * @param i  堆调整的序号
	 * @param n  堆的大小
	 */
	private static void adjustMinHeap(int[] a,int i,int n){
		int temp;
		int child;
		
		for(temp = a[i]; leftChild(i) < n; i = child){
			child = leftChild(i);
			if(child != n - 1 && a[child] < a[child+1]){
				child++;
			}
			if(a[child] > temp){
				a[i] = a[child];
			}else{
				break;
			}
		}
		a[i] = temp;
	}
	/**
	 * 交换堆中的值
	 * @param a
	 * @param x
	 * @param y
	 */
	private static void swap(int[] a, int x, int y){
		int temp = a[x];
		a[x] = a[y];
		a[y] = temp;
	}

5.   冒泡排序(Bubble Sort)

       基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。每一趟排序后的效果都是讲没有沉下去的元素给沉下去。

       算法流程:

       1)从第一个元素开始,依次比较相邻两个元素,将较大/较小元素通过交换放在数组的后方每次冒泡效果为将一个最大/最小         元素"上浮"到数组第i位(i=n...1)

       2)重复步骤1

       复杂度分析:

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

       2) 空间复杂度: O(1)

       Java实现代码:

	public static void BubbleSort(int[] a){
		for(int i = a.length-1; i > 0; i--){
			for(int j = 0; j < i; j++){
				if(a[j] > a[j+1]){
					swap(a,j,j+1);
				}
			}
		}
	}
	
	private static void swap(int[] a, int x, int y){
		int temp = a[x];
		a[x] = a[y];
		a[y] = temp;
	}

6.   快速排序(Quick Sort)

  基本思想:快速排序算法的基本思想为分治思想。

  1)通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小

  2)继续对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

  算法流程:

  1)先从数列中取出一个数作为基准数;

  2)根据基准数将数列进行分区,小于基准数的放左边,大于基准数的放右边;

  3)重复分区操作,知道各区间只有一个数为止。

  复杂度分析:

  1) 时间复杂度:O(nlogn),若初始数列基本有序时,快排序反而退化为冒泡排序。

  2) 空间复杂度: O(1)

  Java实现代码:

	public static void QuickSort(int[] a, int left, int right){
		int pivotpos;  //划分后基准的位置
		if(left < right){
			pivotpos = Partition(a, left ,right);
			QuickSort(a, left, pivotpos-1);
			QuickSort(a, pivotpos+1, right);
		}
	}
	
	/**
	 * 普通选择基准
	 */
	private static int Partition(int[] a, int i, int j){
		//调用Partition(a,left,right)时,对a[left...right]做划分
		//并返回基准记录的位置
		int pivot = a[i];  //用区间的第一个记录作为基准
		
		while(i < j){  //从区间两端交替向中间扫描,直至i=j为止
			while(i < j && a[j] >= pivot){   //pivot相当于在位置i上
				j--;
			}
			if(i < j){   //表示找到a[j]<pivot,交换a[i]和a[j]
				a[i++] = a[j];
			}
			while(i < j && a[i] <= pivot){ //pivot相当于在位置j上
				i++;     //从左到右扫描,查找第一个大于pivot的数组元素
			}
			if(i < j){   //表示找到a[i]>pivot,交换a[i]和a[j]
				a[j--] = a[i];
			}	
		}
		
		a[i] = pivot;
		return i;
	}

 快排改进:

       1)三数中值分割法选择基准点/枢纽元

       2)选择截止范围(cutoff range)以加快排序(当数组较小时,插入排序比快速排序快)

       改进后Java实现代码:

	private final static int CUTOFF = 10;  //截断范围
	
	public static void QuickSort(int[] a, int left, int right){
		if(left + CUTOFF <= right){
			int pivot = median3(a, left, right);
			
			//开始划分
			int i = left, j = right - 1;
			for( ; ; ){
				while(a[++i] < pivot){}
				while(a[--j] > pivot){}
				if(i < j){
					swap(a, i, j);
				}
				else{
					break;
				}
			}
			
			swap(a, i, right-1);    //储存基准点
			
			QuickSort(a, left, i - 1);    //将较小元素排序
			QuickSort(a, i + 1, right);   //将较大元素排序
		}
		else{   //在子数组里调用插入排序
			InsertSort.InsertSort(a, left, right);
		}
	}
	
	/**
	 * 将三个数排序且隐藏基准点
	 * 返回三个数的中值
	 */
	private static int median3(int[] a, int left, int right) {
		int center = (left + right) / 2;
		if(a[center] < a[left]){
			swap(a, left, center);
		}
		if(a[right] < a[left]){
			swap(a, left, right);
		}
		if(a[right] < a[center]){
			swap(a, center, right);
		}
		
		//将基准点放置在right-1位置上
		swap(a, center, right - 1);
		return a[right-1];
	}

	private static void swap(int[] a, int x, int y){
		int temp = a[x];
		a[x] = a[y];
		a[y] = temp;
	}

7.   归并排序(Merge Sort)

      基本思想:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

      算法流程:

      1)将数组分为n等份(算法中为2),对各子数组递归调用归并排序

      2)等分为2份时为2路归并,最后子数组排序结束后,将元素合并起来,复制回原数组。

      复杂度分析:

      1) 时间复杂度: O(nlogn)

      2) 空间复杂度: O(n)

      Java实现代码:

	/*public型的mergeSort是private型递归方法mergeSort的驱动程序*/
	public static void MergeSort(int[] a){
		int[] tempArray = new int[a.length];   //若数组元素为对象类型,需创建Comparable类的数组,再强转为该对象类型
		
		MergeSort(a, tempArray, 0, a.length - 1);
	}
	
	/**
	 * 递归调用归并排序
	 */
	private static void MergeSort(int[] a, int[] tempArray, int left, int right){
		if(left < right){
			int center = (left + right) / 2;
			MergeSort(a, tempArray, left, center);
			MergeSort(a, tempArray, center + 1, right);
			merge(a, tempArray, left, center + 1, right);   //子数组排序结束后,将子数组合并
		}
	}
	
	/**
	 * 合并左右的半分子数组
	 * @param a          需排序数组
	 * @param tempArray  临时存储数组
	 * @param leftPos    左半子数组开始的下标
	 * @param rightPos   右半子数组开始的下标
	 * @param rightEnd   右半子数组结束的下标
	 */
	private static void merge(int[] a, int[] tempArray, int leftPos, int rightPos, int rightEnd) {
		int leftEnd = rightPos - 1;
		int tempPos = leftPos;
		int num = rightEnd - leftPos + 1;
		
		//主循环
		while(leftPos <= leftEnd && rightPos <= rightEnd){
			if(a[leftPos] <= a[rightPos]){
				tempArray[tempPos++] = a[leftPos++];
			}else{
				tempArray[tempPos++] = a[rightPos++];
			}
		}
		/*比较结束后,只会有一个子数组元素未完全被合并*/
		while(leftPos <= leftEnd){        //复制左半子数组剩余的元素
			tempArray[tempPos++]  = a[leftPos++];
		}
		while(rightPos <= rightEnd){      //复制右半子数组剩余的元素
			tempArray[tempPos++]  = a[rightPos++];
		}
		
		//将元素从临时数组赋值回原数组
		for(int i = 0; i < num; i++, rightEnd--){
			a[rightEnd] = tempArray[rightEnd];
		}
		
	}

8.   基数排序(Radix Sort)

       基本思想:基数排序不需要进行记录关键字之间的比较。基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优先级不同的关键字。

       算法流程:

       1)最高位优先(Most Significant Digit first)法,简称MSD法:先按排序分组,同一组中记录,关键码相等,再对各组按排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码对各子组排序后。再将各组连接起来,便得到一个有序序列。

       2)最低位优先(Least Significant Digit first)法,简称LSD法:先从开始排序,再对进行排序,依次重复,直到对排序后便得到一个有序序列。

       复杂度分析:

       1) 时间复杂度:O(d(r+n)),r和d分别为关键字的基数和长度

       2) 空间复杂度: O(rd+n)

	public static void RadixSort(String[] a, int maxLen){
		final int BUCKETS = 256;
		
		ArrayList<String>[] wordsByLength = new ArrayList[maxLen + 1];
		ArrayList<String>[] buckets = new ArrayList[BUCKETS];
		
		//初始化数组列表
		for(int i = 0; i < wordsByLength.length; i++){
			wordsByLength[i] = new ArrayList<>();
		}
		
		for(int i = 0; i < BUCKETS; i++){
			buckets[i] = new ArrayList<>();
		}
		//根据字符串长度加入对应的桶中
		for(String s:a){
			wordsByLength[s.length()].add(s);
		}
		//将桶中元素加入依序数组
		int idx = 0;
		for(ArrayList<String> wordList:wordsByLength){
			for(String s:wordList){
				a[idx++] = s;
			}
		}
		/*根据字符串长度从大到小查看pos位置上的字符,加入buckets调整后,再入数组*/
		int startingIndex = a.length;
		for(int pos = maxLen - 1; pos >= 0; pos --){
			startingIndex -= wordsByLength[pos + 1].size();
			
			for(int i = startingIndex; i < a.length; i++){
				buckets[a[i].charAt(pos)].add(a[i]);
			}
			
			idx = startingIndex;
			for(ArrayList<String> thisBucket:buckets){
				for(String s:thisBucket){
					a[idx++] = s;
				}
				thisBucket.clear();
			}
		}
	}

9.   各种排序算法性能比较表

1.各种排序的稳定性,时间复杂度和空间复杂度总结:


2.排序算法的稳定性:

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

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

       稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。

       不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

3.选择排序算法准则:

   每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。

   选择排序算法的依据:

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

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

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

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

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

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

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

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

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

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

  (2)当n较大,内存空间允许,且要求稳定性:归并排序

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

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

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

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

  (5)基数排序
     它是一种稳定的排序算法,但有一定的局限性:
     1、关键字可分解;
     2、记录的关键字位数较少,如果密集更好;
     3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。



参考资料:

1.   《Java程序员面试笔试宝典》

2.   《数据结构与算法分析:Java语言描述》

3.   http://www.cnblogs.com/maybe2030/p/4715042.html#_label7

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值