排序算法总结
选择排序
算法描述
首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置。然后,再在剩下的元素中找到最小元素与数组中第二个位置上的元素交换位置。如此反复,直到整个数组有序为止。这种方法被称作选择排序,因为该算法在不断的选择剩余元素中的最小者
代码
public void selectSort(T [] sortedData){
for(int i = 0; i < sortedData.length; ++i){
T minVal = sortedData[i];
int minPos = i; //索引最小元素
for(int j = i; j < sortedData.length; ++j){
//搜索最小元素
if(sortedData[j].compareTo(minVal) < 0){
minVal = sortedData[j];
minPos = j;
}
}
sortedData[minPos] = sortedData[i];
sortedData[i] = minVal;
}
}
时间复杂度
根据上面的代码,我们知道选择排序的时间复杂度是与比较次数相关,其比较次数是由两个循环所控制,为
(N−1)+(N−2)+...+2+2=N(N−1)/2∼N2/2
即为
O(n2)
的时间复杂度
算法特点
- 运行时间与输入无关,已经有序的数组排序所用时间与随机顺序的数组所用的时间一样长
- 该算法的数据移动是最少的,最多只会进行N此交换,移动次数与数组长度呈线性关系
插入排序
算法描述
通常人们整理桥牌的方法是一张一张的来,将每一张牌插入到其他已经有序的排中的适当位置。在计算机的实现中,为了给要插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位,即每次将第m个元素插入到前m-1个有序子序列中,使得前m个元素也有序。这种算法叫做插入排序
代码
public void insertSort(T [] sortedData){
for(int i = 1; i < sortedData.length; ++i){//进行排序
for(int j = i; j > 0 &&
(sortedData[j].compareTo(sortedData[j-1]) < 0);
--j){//移动
T temp = sortedData[j];
sortedData[j] = sortedData[j-1];
sortedData[j-1] = temp;
}
}
时间复杂度
根据上面的代码,我们可以看到,在最坏的情况下,即数组完全逆序的话,其交换与比较次数均为:
(N−1)+(N−2)+...+2+2=N(N−1)/2∼N2/2
即时间复杂度为
O(n2)
,最好的情况是数组完全有序,其比较次数为N,交换次数为0,时间复杂度为
O(n)
,因此我们可以看到,相比较选择排序,插入排序与输入时相关的,输入越有序,所用的时间越少
算法特点
- 运行时间与输入相关,有序与无序的运行时间会有很大的不同
- 插入排序非常适合于倒置数量很少的数组(倒置:是指数组中的两个顺序颠倒的元素,比如wolf,其共有6对倒置:(w,o),(w,l),(w,f),(o,l),(o,f),(l,f),倒置数为6)
希尔排序
算法描述
对于大规模乱序数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点的从数组的一端移动到另一端。希尔排序为了较快速度,对插入排序进行了修改,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。其思想是使数组中人以间隔为h的元素有序。即每次都是让第k个元素与第k+h个元素进行比较,插入排序可以看做是h=1的希尔排序
代码
public void shellSort(T [] sortedData){
int len = sortedData.length;
int h = 1;
while(h < len/3) //设置h值
h = 3*h + 1;
while(h >= 1){ //共进行h/3轮排序
for(int i = h; i < len; ++i){
for(int j = i; j >= h &&
sortedData[j].compareTo(sortedData[j-h])<0;
j -= h){
T temp = sortedData[j];
sortedData[j] = sortedData[j-h];
sortedData[j-h] = temp;
}
}
h = h/3;//更新h值
}
}
时间复杂度
希尔排序的运行时间是达不到平方级的,即时间复杂度要小于 O(n2) 的,已知在最坏情况下,该算法的比较次数与 n3/2 成正比。
算法特点
我们知道对于插入排序算法,其更适合于做倒置数较少的序列的排序,我们的希尔排序就是考虑到这个方面,其权衡了子数组的规模和有序性。排序之初,各个自数组都很短,排序之后数组都是部分有序的,这两种情况都非常适合于插入排序,因此希尔排序是更加高效的一种排序算法。对于希尔排序,其递增序列该如何选取一直是一个学术界比较难的问题,因此我们这里不讨论,一般取len/3即可。
和选择排序以及插入排序相比,希尔排序可以运用于大型数组,它对任意排序的数组有比较好的表现,当数组越大的时候,希尔排序的表现就越好,相对于选择排序和插入排序的优势就越大。
通常对于中等大小的数组,一般会优先选择希尔排序,因为它的代码量小而且还不需要额外的空间,对于那些更加高效的算法,其速度可能最多也只会比希尔排序快2倍,但更复杂。
归并排序
算法描述
归并排序:要将一个数组排序,可以先递归的将它分成两半分别排序,然后再将结果归并起来。该算法需要一个额外的数组来帮助其进行排序,该数组用于存储原有元素,在merge阶段,我们以该数组为跳板重新更新目标数组的值,即每次都从两个待归并数组中取最小的值放入目标数组相应位置直到两数组归并完毕。
归并排序时分治思想的典型应用,即在解决一个大问题的时候,我们将该大问题分成两个小问题来解决,这样递归的分下去直到分到了原子问题无法再分的时候,把两个原子问题解决了,把这两个问题的答案归并起来成为更大一点问题的答案,以此类推,直到最终把大问题解决了位置。
代码
public void mergeSort(T [] sortedData){
int len = sortedData.length;
ArrayList<T> tempArray = new ArrayList<T>();
for(int i = 0; i < len; ++i)
tempArray.add(sortedData[i]);
int low = 0;
int high = len-1;
sort(low, high, sortedData, tempArray); //对数组排序
}
public void sort(int low, int high, T [] sortedData, ArrayList<T> tempArray){
if(low >= high)
return;
int mid = (low+high)/2;
sort(low, mid, sortedData, tempArray);
sort(mid+1, high, sortedData, tempArray);
merge(low, high, mid, sortedData, tempArray);
}
public void merge(int low, int high, int mid, T [] sortedData, ArrayList<T> tempArray){
int i = low;
int j = mid+1;
int x = low;
for(int k = low; k <= high; ++k)
tempArray.set(k, sortedData[k]);
while(x <= high){
if(i > mid){ //左边的数组已经归并完了,剩余的位置都由右边数组填充
while(j <= high)
sortedData[x++] = tempArray.get(j++);
}
else if(j > high){ //右边的数组已经归并完了,剩余的位置都有左边数组填充
while(i <= mid)
sortedData[x++] = tempArray.get(i++);
}
else if(
tempArray.get(i).compareTo(tempArray.get(j))<0) //左边元素小于右边元素
sortedData[x++] = tempArray.get(i++);
else //左边数组对应元素大于右边数组对应元素
sortedData[x++] = tempArray.get(j++);
}
}
时间复杂度
该归并排序算法需要 1/2nlgn 至 nlgn 次比较,即时间复杂度为 O(nlgn)
算法特点
- 对于长度为
n
的数组,该归并排序算法最多需要访问数组
6nlgn 次 - 对于归并排序,其最吸引人的性质是它能够保证将任意长度
n
的数组排序所需时间和
nlgn 成正比,因此我们可以用归并排序来处理数百万升值更大规模的数组,这是插入排序和选择排序做不到的,但是它有一个缺点就是其所需要的额外空间的规模是和 n 成正比的 - 对于归并排序,因为其是使用递归来实现排序的,而对于小规模数组,递归会使得在排序的时候,方法调用过于频繁,从而导致运算速度的下降,而对于小规模数组,插入排序一般要比归并排序的速度更快,因此在进行递归时,当数组小到一定程度的时候,我们可以用插入排序代替归并排序来对小数组进行排序,通过这样的方法,我们可以使得相比于传统递归方法,在运算时间上缩短
10%∼15%
快速排序
算法描述
快速排序是一种分治排序算法,其基本思想是通过通过递归的调用切分来排序的,先将某元素a[j]放到一个合适的位置,在该位置左边的元素都要比a[j]小,在该位置右边的元素都要比a[j]大,确定了a[j]的位置之后,再递归确定其他位置上的元素.
代码
public void quickSort(T [] sortedData){
int low = 0;
int high = sortedData.length - 1;
sort(sortedData, low, high);
}
public void sort(T [] sortedData, int low, int high){
int i = low;
int j = high;
int splitPos = low; //切分点
T objectVal = sortedData[low];
while(i < j){ //寻找切分点
while(i < j && i < high &&
sortedData[--j].compareTo(objectVal) > 0);
while(i < j && j > low &&
sortedData[++i].compareTo(objectVal) < 0);
if(i >= j)
break;
T temp = sortedData[i];
sortedData[i] = sortedData[j];
sortedData[j] = temp;
} //j所停位置必然是比目标值小的位置
sortedData[splitPos] = sortedData[j];
splitPos = j;
sortedData[splitPos] = objectVal;
if(splitPos-1 > low) //对切分点左边的元素排序
sort(sortedData, low, splitPos-1);
if(splitPos+1 < high) //对切分点右边的元素排序
sort(sortedData, splitPos+1, high);
}
时间复杂度
快速排序在最好的情况下,即在每次切分都是平均切分的情况下,其时间复杂度为 O(nlgn) ,而最坏的情况下,即每次切分都是从余下元素中最小的那个元素进行切分,那么其比较次数为 n2/2 ,即时间复杂度为 O(n2) 。通常,对于大小为n的数组,该算法能够保证运算时间在 1.39nlgn 的某个常数因子的范围内。虽然归并排序也能做到这一点,但是快排一般会更快,因为它移动的次数更少。
算法特点
- 快速排序切分方法的内循环会用一个递增的索引将数组与一个定值比较,由于这个特点,所以归并排序与希尔排序通常要比快排要慢,因为这两个算法需要在内循环中移动数据;
- 快速排序的比较次数很少,其排序效率最终依赖于切分数组的效果,而这依赖于切分元素的值。对于快排,其最好的情况时每次都正好讲数组对半分,但如果每次切分不平衡的话,该算法的效率会非常的低,因此该算法适合于随机数组,这样可以减少最坏的情况发生的概率;
- 对于小数组,快排要比插入排序慢,因此当子数组小到一定程度的时候,我们可以使用插入排序来排
- 对于快排,随着数组规模的增大其运行时间会趋于平均运行时间,大幅偏离的情况非常罕见
堆排序
算法描述
堆排序可以分为两个阶段:堆的构造阶段和堆的排序阶段。其中,在堆得构造阶段,我们将原始数据重新组织安排进一个堆中,然后在排序阶段,从堆中按递减顺序取出所有元素并得到排序结果。堆排序的主要工作都是在第二阶段完成的。这里我们将堆中的最大元素删除,然后放入堆缩小后数组中空出的位置。
代码
public void Heapsort(T [] DataSorted){
CreateHeap(DataSorted); //创建堆
sinkSort(DataSorted); //下沉排序
}
public void CreateHeap(T [] dataSorted){
int len = dataSorted.length;
for(int i = len/2; i >= 1; --i){ //在堆排序中,数组的索引初始点为1
sink(dataSorted, i, len);
}
}
public void sinkSort(T [] dataSorted){
int len = dataSorted.length;
for(int i = 1; i <= len; ++i){
T temp = dataSorted[len-i];
dataSorted[len-i] = dataSorted[0];
dataSorted[0] = temp;
sink(dataSorted, 1, len-i);
}
}
public void sink(T [] dataSorted, int index, int len){
while(2*index <= len){
int trueIndex = index-1;/*映射到真实数组中,真
实数组的起始点的索引值为0*/
int leftChild = 2*index-1; /*左孩子节点在真
实数组中的索引值*/
int rightChild = 2*index; /*右孩子节点在真
实数组中的索引值*/
//在左孩子和右孩子节点之间找到值最大的那个孩子节点
int maxIndex = (rightChild == len) ?
leftChild : ((dataSorted[leftChild].compareTo(dataSorted[rightChild]) > 0) ? leftChild:rightChild);
if(dataSorted[trueIndex].compareTo(dataSorted[maxIndex]) < 0){ //删除最大元素,然后将其放到堆缩小后空出的位置
T temp = dataSorted[trueIndex];
dataSorted[trueIndex] = dataSorted[maxIndex];
dataSorted[maxIndex] = temp;
index = maxIndex+1;
}
else
break;
}
}
时间复杂度
根据上面算法,我们可以知道其在最坏的情况下需要比较的次数为 2nlgn 次,因此该算法的时间复杂度为 O(nlgn) ,它是唯一的能够同时最优的利用时间与空间的算法
算法特点
该算法通常会被利用于空间十分紧张的情况,因为它可以很高效的利用空间,但是当空间不紧张的时候,很少用它,因为它无法利用缓存。该算法中,数组元素很少与相邻的其他元素进行比较,因此缓存的命中率会远远低于大多数比较都在相邻元素之间进行的算法。
但是,用堆实现的优先队列则用的越来越多
常用结论
- 没有任何基于比较的算法能够保证在对长度为
n
的数组进行排序时,比较次数少于
lg(n!)∼nlgn 。但是对于含有以任意概率分布的重复元素的输入,归并排序无法保证最佳性能。