常用的排序算法有
1 选择排序
2 插入排序
3 希尔排序
4 冒泡排序
5 归并排序
6 快速排序
7 堆排序
8 基数排序
-
1 选择排序
原理
对一个数组a来说,选择排序先找到数组最小的项,将它与a[0]交换。然后,忽略a[0],找到下一个最小的项并交换到 a[1],以此类推。图1-1显示了如何利用选择排序通过交换值完成对数组的排序。
15 | 8 | 10 | 2 | 5 |
代码实现如下:
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 堆排序
相关理论
- 满二叉树中所有叶子节点都在同一层上,且每个非叶子节点都恰好有俩个孩子
- 高度为h的满树有2^h-1个节点,这是能容纳的最多节点数。
- 完全二叉树直到第二层都是满的,它最后一层的叶子结点从左至右填充。
- 有n个结点的完全二叉树或满二叉树的高度是log2(n+1)向上取整。
- 在完全平衡二叉树中,每个结点的子树都有完全相同的高度,这样的树必须是满的。如果树中每个结点的子树高度差不大于1,则该树称为高度平衡树。
- 堆是其结点含有Comparable对象的一颗完全二叉树。每个结点中的数据不小于(或不大于)其后代中的数据。
- 在最大堆中,结点中的对象大于或等于后代的对象。在最小堆中,关系是小于或等于。最大堆的根含有堆中最大的对象,最大堆中的任何结点的子树仍然是最大堆。最小堆类似。堆中的结点的子树之间没有关系。
- 当二叉树是完全树时,可以使用数组高效简洁的表示它。
原理
堆是一颗其结点有特定排列次序的完全二叉树。我们可以使用层序遍历访问它的每个结点,并按照层序遍历的次序将结点数据放置到数组从下标0开始的连续位置(任何完全二叉树都可以这样向数组映射)。映射后得到的数组保存着树中结点之间的关系,其关系表现为(假设树中有n个结点):
- 堆的根节点放置在索引0位置,最后一个子节点放置在索引n-1位置。最后一个子节点的父节点在n/2 -1位置。
- 位置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)的算法,但它不适用于所有数据。