几种常用对象数组的排序算法——java语言描述


常用的排序算法有

1 选择排序 

2 插入排序

3 希尔排序

4 冒泡排序

5 归并排序

6 快速排序

7 堆排序

8 基数排序


  1. 1 选择排序

       原理      

  对一个数组a来说,选择排序先找到数组最小的项,将它与a[0]交换。然后,忽略a[0],找到下一个最小的项并交换到       a[1],以此类推。图1-1显示了如何利用选择排序通过交换值完成对数组的排序。

数组a
1581025

代码实现如下:


public void selectionSort(int[] a){
	
	int length = a.length;

	for(int i = 0;i<length-1;i++){
		
        //每次循环初始化最小项的位置
		int smallest = i;
		boolean isExchange = false;
	    int temp = 0;
		
		for(int j = i+1;j<length;j++){
			
			//更新最小项的索引位置
			if(a[smallest]>a[j]){
				isExchange =true;
				smallest = j;
			}
			
		}//end for
		
		//将最小项交换到a[i]位置
		temp = a[i];
		a[i] = a[smallest];
		a[smallest] = temp;
		
		//如果没发生交换,说明数组已经有序,跳出循环
		if(!isExchange)
			break;
	}//end for
	
}//end method selectionSort

以上代码可以对整数数组进行排序,下列代码可以对所有实现Comparable接口的类的对象所组成的数组进行选择排序

T extends Comparable<? super T> 表示泛型T为实现Comparable接口的类以及同样实现Comparable接口的父类。

通配符?表示任意的类类型,但符号? super T表示T的任意父类。

万能代码

public <T extends Comparable<? super T>> void selectionSort(T[] a){
	
	int length = a.length;
	for(int index =0;index<length-1;index++) {
		
		int indexOfNextSmallest = getIndexOfSmallest(a,index,length-1);
		swap(a,index,indexOfNextSmallest);
	}
}//end method selectionSort

private <T extends Comparable<? super T>> int getIndexOfSmallest(T[] a,int first,int last) {
	
	int indexOfSmallest = first;
	
	for(int index = first+1 ;index <= last ;index++) {
		if(a[indexOfSmallest].compareTo(a[index]) > 0) {
			indexOfSmallest = index;
		}
	}//end for
	
	return indexOfSmallest;
}//end method getIndexOfSmallest
	
private void swap(Object[] a,int i,int j) {
	
	Object temp = a[i];
	a[i] = a[j];
	a[j] = temp;
}//end swap

选择排序的效率

在selectSort中,for循环执行了n-1次(n=length),所以方法getIndexOfSmallest和swap各执行了n-1次。在n-1次调用getIndexOfSmallest中,last是n-1,而first从0变到n-2。在每次调用getIndexOfSmallest时,它的循环执行了last-first次。因为last-first从(n-1)-0变到(n-1)-(n-2),所以这个循环总共执行(n-1)+(n-2)+....+1次,这个和为n(n-1)/2.因此,因为循环中每个操作是O(1)的,所以选择排序是O(n*n)的。不论数组的初始次序如何(完全有序,接近有序或完全无序),在任何情况下,选择排序也是O(n*n)的。它仅执行O(n)次交换,有很少的数据移动。


2 插入排序

原理

 插入排序将数组分隔(partition)为两部分。第一部分是有序的,初始时仅含有数组中的第一项。第二部分含有其余的项。算法从未排序的部分移走第一项,并将其插入有序部分的合适位置——从有序部分的末尾开始,朝着开头方向行进,通过将待排序项与各项进行比较来选择合适的位置 。

数组的插入排序图解如下所示:

java语言代码实现如下:

public <T extends Comparable<? super T>> void insertionSort(T[] a) {
	
	int length = a.length;
	for(int index =0;index<length-1;index++) {
		
		T unSortedEntry = a[index+1];
		//将未排序部分的第一项插入到有序部分当中
		insertInOrder(unSortedEntry,a,0,index);
	}
}

private <T extends Comparable<? super T>> 
			void insertInOrder(T unSortedEntry,T[] a,int first,int last) {
	
	int index = last;
	while(index>=first&&a[index].compareTo(unSortedEntry)>0) {
		a[index+1] = a[index];
		index--;
	}
	a[index+1]= unSortedEntry;
}//end method insertUnSortedEntry

插入的排序的效率

对于有n项的数组,first是0且last是n-1。for循环执行n-1次,对应的方法insertInOrder被调用n-1次。所以,在insertInOrder中,begin是0且end 是0~n-2。每次调用该方法时,insertInOrder内的循环最多执行end-begin+1次。所以这个循环执行的总次数最多是1+2+...+(n-1),这个和是n(n-1)/2,故插入排序是O(n*n)的。如果数组已经有序,则insertInOrder立即退出循环此时插入排序是O(n)。所以,最优时插入排序是O(n),最坏时是O(n*n)的。数组越接近有序,插入排序要作的工作越少。


3 希尔排序

原理

希尔排序是插入排序的变体,能比Q(n*n)更快。在插入排序过程中,数组项只能移动到相邻位置。当项与正确的有序位置相距甚远时,它必须进行很多次这样的移动。希尔排序是对具有相等间隔下标的项进行排序。不是移动到相邻位置,而是移动到多个位置之外。得到的结果是几乎有序的数组——可使用普通的插入排序进行高效率排序的数组。

数组的希尔排序图解如下所示

java语言代码实现如下:

public class ShellTest {
	
	public <T extends Comparable<? super T>>void shellSort(T[] a) {
		
		int length = a.length;
		//计算希尔排序间隔
		int space = length/2;

		while(space>0) {
			//如果间隔为偶数,则加1为奇数
			if(space%2==0)
				space = space + 1;
			
			//对以一定间隔划分后的子数组们进行插入排序。
			for(int index =0; index<space;index++) {
				//对每个子数组进行排序
				incrementalInsertionSort(a,index,length-1,space);
			}
			//调整间隔大小,最后space =1时,对整个数组进行一次插入排序
			space = space/2;
			
		}//end while
		
	}//end method shellSort
	
	private <T extends Comparable<? super T>>
				void incrementalInsertionSort(T[] a,int first,int end,int space) {
		
		//采用unsorted = first+space可以防止数组越界异常
		for(int unsorted = first+space;unsorted<=end;unsorted = unsorted+space) {
			
			T nextToInsert = a[unsorted];
			int index = unsorted -space;
			while(index>=first && a[index].compareTo(nextToInsert)>0) {
				a[index +space] = a[index];
				index = index - space; 
			}
			a[index +space] = nextToInsert;
		}
	}//end method incrementalInsertionSort

希尔排序效率

希尔排序有O(n*n)的最坏情形。当space为偶数时,将其加1,则最坏情形可以改进为O(n^1.5)。


4 冒泡排序

原理

    依次比较数组相邻元素,将小的放在左边,大的放在右边。对整个数组完成一轮比较之后,数组其余元素一定小于或等于最后一个元素。忽略最后一个元素,重复上述过程。

数组的冒泡排序如下图所示

 

  

冒泡排序的java语言描述如下:

public <T extends Comparable<? super T>> void BubbleSort(T[] a) {
		
		int length = a.length;
		
		for(int index =length-1 ;index>0;index--) {
			//对数组进行一次俩俩交换排序
			exchangeSort(a,0,index);
		}//end for
	}//end method BubbleSort
	
	private <T extends Comparable<? super T>> boolean exchangeSort(T[] a,int first,int last) {
		boolean isOrder =false;
		T temp = null;
		for(int index = first;index<last;index++) {
			
			if(a[index].compareTo(a[index+1])>0) {
				temp = a[index+1];
				a[index+1]=a[index];
				a[index]=temp;
				isOrder = true;
			}
		}//end for
		return isOrder;
	}//end method exchangeSort

冒泡排序算法的效率

冒泡排序算法的时间复杂为O(N^2),上述java代码当数组越有序,排序越快。


5 归并排序

原理

归并排序将数组分为两半,分别对两半进行排序。然后将它们合并为一个有序数组。归并排序算法常常用递归方式描述。

数组的归并排序图解如下所示:

归并排序的java语言描述如下:

public <T extends Comparable<? super T>> void mergeSort(T[] a,int first,int last) {
		
		T[] tempArray = (T[])new Comparable<?>[a.length];
		mergeSort(tempArray,a,first,last);
	}
	
	private <T extends Comparable<? super T>> 
							void mergeSort(T[] tempArray,T[] a,int first,int last) {
		if(first<last) {
			
			int mid =(last-first)/2 +first;
			mergeSort(tempArray,a,first,mid);
			mergeSort(tempArray,a,mid+1,last);
			merge(tempArray,a,first,mid,last);
		}
		
	}
	private <T extends Comparable<? super T>> 
							void merge(T[] tempArray,T[] a,int first,int mid,int last) {
		
		int beginHalf1 = first;
		int endHalf1 = mid;
		int beginHalf2 = mid+1;
		int endHalf2 = last;
		int index =0;
		//当两个子数组都不为空时,让一个子数组的项与另一个子数组的项进行比较,然后将较小的项复制到临时数组中。
		while(beginHalf1<=endHalf1&&beginHalf2<=endHalf2) {
			
			if(a[beginHalf1].compareTo(a[beginHalf2])>0) {
				tempArray[index] = a[beginHalf2];
				beginHalf2++;
			}else {
				tempArray[index] = a[beginHalf1];
				beginHalf1++;
			}
			index++;
			
		}//end while
		
		//一个子数组已经全部复制到tempArray,将另一个子数组复制到tempArray
		while(beginHalf1<=endHalf1) {
			tempArray[index] = a[beginHalf1];
			beginHalf1++;
			index++;
		}
		
		while(beginHalf2<=endHalf2) {
			tempArray[index] = a[beginHalf2];
			beginHalf2++;
			index++;
		}
		
		//将tempArray中的项复制到数组a中
		copy(tempArray,a,first, last);
	}
	
	private <T extends Comparable<? super T>> 
						void copy(T[] tempArray,T[] a,int first,int last) {
		int index =0;
		for(int i =first;i<=last;i++) {
			a[i] = tempArray[index];
			index++;
		}
	}//end method copy

归并排序的时间效率

归并排序在所有情形下都是O(n logn)的。它对临时数组的需求是它的缺点。

如果排序算法不改变相等对象的相对次序,则称为是稳定的。


6 快速排序

原理

快速排序与归并排序一样是使用分治策略的数组排序。它选择数组中的一项(枢轴)来重排数组项,满足

  • 枢轴所处的位置就是在有序数组中的最终位置。
  • 枢轴前的项都小于或等于枢轴
  • 枢轴后的项都大于或等于枢轴

 这个排列称为数组的划分(partition)

数组的快速排序图解

快速排序的效率

最优情形下,每次枢轴的选择将数组划分为两个相等的子数组,此时快速排序是O(nlogn)的。所以选择枢轴时尽可能使两个子数大小差不多。快速排序在平均情形下是O(nlogn)的,归并排序总是O(nlogn)的。而实际上,快速排序可能比归并排序更快,且不需要归并排序中合并操作所以需要的额外内存。快速排序在最坏情形下是O(n*n)的,枢轴的选择将影响它的行为。实际上,一般出现的数组都接近有序,所以为了避免最坏情形,枢轴的选择采用三元中值枢轴选择。

数组的快速排序的java代码描述如下:

	private final int MIN_SIZE = 4; 
	public <T extends Comparable<? super T>> void  quickSort(T[] a ,int first ,int last) {
		
		if(last-first+1<MIN_SIZE) {
			
			//当数组长度小于4时,采用插入排序
			for(int unsortedIndex = first+1;unsortedIndex<=last;unsortedIndex++) {
	
				T nextToInsert = a[unsortedIndex];
				int orderIndex = unsortedIndex-1;
				while(orderIndex>= first&&a[orderIndex].compareTo(nextToInsert)>0) {
					a[orderIndex+1] = a[orderIndex];
					orderIndex--;
				}
				a[orderIndex+1] = nextToInsert ;
			}//end for
			
			
		}else{
			//选择枢轴,创建划分,使Smaller|Pivot|Larger
			int pivotIndex = partion(a,first,last);
			quickSort(a,first,pivotIndex-1);
			quickSort(a,pivotIndex+1,last);
		}//end if
		
		
	}//end method quickSort
	
	private <T extends Comparable<? super T>> int  partion(T[] a ,int first ,int last) {
		
		//选择枢轴,三元中值枢轴选择策略
		int middle = (last-first)/2 +first;
		//选择枢轴
		sortFirstMiddleLast(a,first,middle,last);
		//断言:枢轴在middle位置,把枢轴放在last-1位置上
		swap(a, middle, last-1);
		int pivotIndex = last-1;
		T pivot = a[last-1];
		
		//排序两个子数组,使得a[first...endSmaller]<=pivot<=a[endSmaller+1...last-1]
		int indexFromLeft = first+1;
		int indexFromRight = last-2;
		boolean flag = false;
		
		while(!flag) {
		
			while(a[indexFromLeft].compareTo(pivot)<0)
				indexFromLeft++;
			while(a[indexFromRight].compareTo(pivot)>0)
				indexFromRight--;
			//断言:a[indexFromLeft]>= pivot&&a[indexFromRight]<= pivot
			
			if(indexFromLeft<indexFromRight) {
				swap(a,indexFromLeft,indexFromRight);
				indexFromLeft++;
				indexFromRight--;
			}else {
				//比较结束
				flag = true;
			}
			
		}//end while
		
		//交换a[indexFromLeft] 与a[pivotIndex]的值
		swap(a,indexFromLeft,pivotIndex);
		pivotIndex = indexFromLeft;
		
		return pivotIndex;
	}//end method partion
	
	private <T extends Comparable<? super T>> void  
					sortFirstMiddleLast(T[] a ,int first ,int middle, int last) {
		
		if(a[first].compareTo(a[middle])>0) 
			swap(a, first, middle);
		if(a[middle].compareTo(a[last])>0)
			swap(a, middle,last);
		//此时最后一个一定最大,再次排序第一项与中间项
		if(a[first].compareTo(a[middle])>0) 
			swap(a, first, middle);
	}
	
	private <T extends Comparable<? super T>> void  
					swap(T[] a ,int i ,int j) {
		
		T temp = a[i];
		a[i] = a[j];
		a[j] = temp;
	}

7 堆排序

相关理论

  1. 满二叉树中所有叶子节点都在同一层上,且每个非叶子节点都恰好有俩个孩子
  2. 高度为h的满树有2^h-1个节点,这是能容纳的最多节点数。
  3. 完全二叉树直到第二层都是满的,它最后一层的叶子结点从左至右填充。
  4. 有n个结点的完全二叉树或满二叉树的高度是log2(n+1)向上取整。
  5. 在完全平衡二叉树中,每个结点的子树都有完全相同的高度,这样的树必须是满的。如果树中每个结点的子树高度差不大于1,则该树称为高度平衡树。
  6. 堆是其结点含有Comparable对象的一颗完全二叉树。每个结点中的数据不小于(或不大于)其后代中的数据。
  7. 在最大堆中,结点中的对象大于或等于后代的对象。在最小堆中,关系是小于或等于。最大堆的根含有堆中最大的对象,最大堆中的任何结点的子树仍然是最大堆。最小堆类似。堆中的结点的子树之间没有关系。
  8. 当二叉树是完全树时,可以使用数组高效简洁的表示它。

原理

堆是一颗其结点有特定排列次序的完全二叉树。我们可以使用层序遍历访问它的每个结点,并按照层序遍历的次序将结点数据放置到数组从下标0开始的连续位置(任何完全二叉树都可以这样向数组映射)。映射后得到的数组保存着树中结点之间的关系,其关系表现为(假设树中有n个结点):

  1. 堆的根节点放置在索引0位置,最后一个子节点放置在索引n-1位置。最后一个子节点的父节点在n/2 -1位置。
  2. 位置i处的结点的父结点在(i-1)/2处(根节点除外)。位置i处的结点的子节点在2i+1,2i+2处。

堆排序是考虑数组按层数遍历映射得到的完全二叉树。将每个非叶子结点看作是一个半堆,将所有半堆按从右到左,从下到上的顺序依次转换为堆。最后得到一个最大堆(或最小堆)。将最大堆的根节点与最后一个叶子结点(对应数组最后一项)交换,然后忽略最后一项,得到一个半堆。再将半堆转化为最大堆,并于最后一个叶子结点交换,然后再忽略最后一项。重复上述过程,直到数组完全有序。

数组的堆排序图解如下:

堆排序的java语言描述如下:

	public <T extends Comparable<? super T>> void heapSort(T[] a) {
		
		int n = a.length;
		//将数组对应的完全二叉树转换为堆
		for(int rootIndex = n/2 -1;rootIndex>=0;rootIndex--)
			reheap(a,rootIndex,n-1);
		//交换根节点与最后一个子结点
		swap(a,0,n-1);
		//不断进行半堆到最大堆的转化,并交换根节点与最后一个子结点,然后忽略最后一个结点
		//知道数组完全有序
		for(int lastIndex =n-2;lastIndex>0;lastIndex--) {
			reheap(a, 0, lastIndex);
			swap(a, 0, lastIndex);
		}
	}
	
	public <T extends Comparable<? super T>>
						void reheap(T[] a,int rootIndex,int lastIndex) {
		
		T orphan = a[rootIndex];
		int leftChildIndex = 2*rootIndex+1;
		boolean flag = false;
		//当结点为叶子结点或大于任何子结点时,结点就在rootIndex位置
		while(!flag&&leftChildIndex<=lastIndex) {
			//筛选最大孩子
			int lagerChildIndex = leftChildIndex;
			T largerChild = a[lagerChildIndex];
			int rightChildIndex = leftChildIndex+1;
			if(rightChildIndex<= lastIndex&&a[rightChildIndex].compareTo(largerChild)>0) {
				lagerChildIndex = rightChildIndex;
				largerChild = a[lagerChildIndex];
			}
			//比较父节点与子节点的大小,父小于子,子替代父。
			if(orphan.compareTo(largerChild)<0) {
				a[rootIndex] = a[lagerChildIndex];
				rootIndex = lagerChildIndex;
				leftChildIndex = 2*rootIndex+1;
			}else {
				flag = true;
			}
		}//end while
		
		a[rootIndex] = orphan;
		
	}//end reheap

	public <T extends Comparable<? super T>>
					void swap(T[] a,int i,int j) {
		T temp = a[i];
		a[i] = a[j];
		a[j] = temp;
	}//end swap

堆排序算法效率

与归并排序和快速排序一样,堆排序是O(nlogn)算法。在这里堆排序不需要第二个数组,但归并排序需要。在大多数情况下快速排序是O(nlogn)的,最坏情况是O(n*n)的。通常选择合适的枢轴就可以避免快速排序的最坏情况,所以一般来讲,他是首选的排序方法。


8 基数排序

以上排序算法都可以对可排序的对象进行排序,基数排序(桶排序)不使用比较,但为了能进行排序,其必须限制排序的数据。对于这些受限的数据,基数排序是O(n)的,故他快于以上任何一种排序方法。但是,它不适和作为通用的排序算法,因为它将数组看作有相同长度的字符串。

原理

基数排序的java代码实现如下:

	public void radixSort(int[] a,int digit) {
		
		//创建桶
		//创建一个大小为10的list数组,数组元素为ArrayList
		//ArrayList底层为数组——可以按索引存取数据
		List[] list = new List[10];
		for(int index=0;index<list.length;index++) {
			//使用默认的集合大小3,超出时集合自动扩展
			list[index]= new ArrayList<Integer>();
		}
		
		
		for(int i=0;i<digit;i++) {
			//分别取1/10/100等以获取数字的个位/十位/百位等。
			//然后放入对应的0-9号桶中
			int divisor= (int) Math.pow(10, i);
			int position = 0;
			for(int index =0;index<a.length;index++) {
				//获取数组中的每个数,并计算要放入那个桶
				position = a[index]/divisor%10;
				//将数据放入对应的桶中
				list[position].add(a[index]);
				
			}//end for
	        
			//按桶的次序以及每个桶数据的添加顺序将数据取出并放回到数组中
			//将每个桶清空
			copy(a, list);
			
		}//end for
		
	}//end method radixSort
	
	private void copy(int[] a,List[] list) {
		int position = 0;
		//从0-9遍历每个桶,并将数据放回到数组中,ArrayList数据存取有序
		for(int index=0;index<list.length;index++ ) {
			for(Object item:list[index]) {
				a[position] = (int) item;
				position++;
			}
			//清空集合
			list[index].clear();
		}//end for
	}//end method copy

基数排序时间效率

基数排序对于某些数据是O(n)的算法,但它不适用于所有数据。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值