【数据结构与算法】【算法】三种高级排序算法
一、归并排序
什么是归并排序
归并排序就是将两个有序的子序列合并到一个序列中。是分治算法的一个典型应用,什么是分治算法呢?分治算法就是利用算法将一个原问题分解成一个个子问题(子问题与原问题的性质一样),分别计算出每个子问题的最优解,即可求出原问题的解;最简单的就是问题就是二分法问题,归并排序正是分治算法的二分法问题;
分治算法可以使用递归或者动态规划等思想,将复杂问题简单化。
归并排序的使用场景
当空间内存比较充足,数据量有比较大的时候,为了更好的完成排序效率,推荐使用归并排序,其时间复杂度为O(N*logN);
归并排序的实现方式
结合递归思想,将一个需要排序的数组,反复的两半切割成子数组,直到得到的子数组只含有一个数据项(基值条件);
然后将每个切成的子数组使用归并进行合并到一个数组中,反复执行,最后得到一个有序的数组。
public class TestMain {
public static int[] a = {1, 4, 5, 11, 2, 7, 6, 9, 8, 12, 66, 33, 55};
public static void main(String[] args) {
int[] arraySpace = new int[a.length];
recMerge(arraySpace,0,a.length-1);
for (int i = 0; i < arraySpace.length; i++) {
System.out.println(arraySpace[i]);
}
}
private static void recMerge(int[] arraySpace, int lower, int upper) {
// 基值条件:如果两个下标值相等,说明不能在分割了,直接返回
if (lower == upper) {
return;
}
// 采用二分法的分治思想,拆分两个子数组,最后采用归并排序
int mid = (lower + upper) / 2;
recMerge(arraySpace, lower,mid);
recMerge(arraySpace,mid+1,upper);
merge(arraySpace,lower,mid+1,upper);
}
private static void merge(int[] arraySpace, int lower, int mid,int upper) {
int size = upper - lower + 1; //本次归并的个数
int index = 0;
int lowerIndex = lower; // 记录一开始的位置
int midIndex = mid - 1; // 记录中间的位置
//当两边都有值时,进行比较排序
while (lower <= midIndex && mid <= upper) {
if (a[lower] < a[mid]) {
arraySpace[index++] = a[lower++];
}else {
arraySpace[index++] = a[mid++];
}
}
// 当右边比较完并完成排序,只剩左边的值,则直接插入
while (lower <= midIndex) {
arraySpace[index++] = a[lower++];
}
//当左边比较完并完成排序,只剩右边的值,则直接插入
while (mid <= upper) {
arraySpace[index++] = a[mid++];
}
// 最后将归并排序完的数组,复制到原数组中,等待下一次归并排序,直到最后完整排序完
for (int i = 0; i < size; i++) {
a[lowerIndex++] = arraySpace[i];
}
}
}
归并排序的优缺点
归并排序的优点很明显:时间复杂度比较快:O(N*logN)。
归并排序缺点也很明显,就是浪费空间。第一是需要单独一块空间存放归并数据,第二就是每次归并完都需要将其复制到原数组中,特别浪费空间,如果空间内存不足的,请慎重使用。事实上,所说的缺点大多是由于归并排序使用的递归算法,也就是分治思想,带来的问题,虽然利用分治思想可以很好的解决我们的实际问题,但是实际中它的效率并不高;因此呢,如果我们想要追求高效率,那么我们可以考虑一下怎么消除递归等方法,常用的方法一般是利用循环或者栈来代替
注意一下:
大多数递归算法都可以用栈结构来代替,例如我们的编译器在处理方法函数的调用的时候,我们会遇到方法函数内还有存在一层层的其他方法函数的调用,就跟递归的原理一样,一层层的调用,直到最后一层,然后再一层层的返回;编译器利用了栈来实现这种思想,将方法名、参数和返回值地址一层层的压入栈中,利用先进后出的特性,最后返回到最开始调用的那个方法函数里,用栈消除递归,效率更高更可靠。
二、希尔排序
什么是希尔排序
希尔排序就是由一个人叫希尔的人基于插入排序的改进的一个算法;在插入排序算法基础上增加一个新特性(在插入排序中增加对间隔元素的排序,间隔大小从n慢慢减到1范围【该间隔范围称之为间隔序列,生成这个间隔序列的方法有很多种,常用的方式是利用递归方式,使用公式h=3h+1,其中h为间隔大小,初始为1,一直递归,当间隔大小h大于数组大小即停止,然后使用公式h=(h-1)/3推倒公式生成逆置的间隔序列】,当间隔大小为1时,最后一次排序即完成最后的排序),大大提高了插入排序的执行效率。
希尔排序的使用场景
在中等规模的数据排序中(如几千个数据项),执行效率比较良好,也比较容易实现,代码比较简单,写起来并不复杂,因此这种场景下推荐使用,其平均时间复杂度为O(N*logN^2),实际取值范围为:O(N*N^1/2)到O(N*N^1/6);因为除了一些特殊情况,希尔排序的效率很难从理论上分析,这些范围都是从实验中得出来的一个评估。
希尔排序的实现
public class TestMain {
public static int[] a = {1, 4, 5, 11, 2, 7, 6, 9, 8, 12, 66, 33, 55};
public static void main(String[] args) {
shellSort(a,a.length);
for (int i = 0; i < a.length; i++) {
System.out.print(" "+a[i]);
}
}
private static void shellSort(int[] arraySort,int size) {
int h = 1; // 计算出间隔序列大小值
while (h <= size/3) { // 范围公式可以自己定义一个合适的范围
h = h*3 +1;
}
int inner,outer;
int temp;// 开辟一个临时变量空间保存插入排序的腾出空位置的元素
while (h > 0) {
for (outer = h; outer < size ; outer++) {
temp = arraySort[outer];
inner = outer;
while (inner > h-1 && arraySort[inner - h] >= temp) {
arraySort[inner] = arraySort[inner - h];
inner -= h;
arraySort[inner] = temp;
}
h = (h -1) / 3; // 逆置间隔序列
}
}
}
输出: 1 2 4 5 6 7 8 9 11 12 33 55 66
希尔排序的优缺点
从稳定性角度上看:由于需要进行多次插入排序,同一数据项可能都会发生位置变动,所以希尔排序是不稳定的。
从空间角度上:不需要大量的辅助空间,跟归并排序相比,归并排序需要两倍的空间完成排序;而且希尔排序实现起来与归并排序一样容易实现。
从时间角度看:希尔排序基于插入排序,引入新特性,减少复制次数,速度上得到很大改进,其时间复杂度范围从O(N*N^1/2)到O(N*N^1/6);相对于插入排序的O(N^2),效率是挺高的。
如何改进希尔排序的缺点
从上面分析可以看到,希尔排序在空间上利用的还是比较好的,关键是在于时间复杂度的问题,它是一个评估范围,也就是说会存在最坏的情况:其时间复杂度接近插入排序O(N^2)的时间复杂度;
因此我们可是从时间复杂度方面优化希尔排序的缺点:
- 考虑好合适的使用场景:适用于中小规模的数据量场景
- 尽量将时间复杂度降到最低:选择合适的生成间隔序列的公式,如:
If(h < 5) {
h = 1;
} else {
h = (5*h - 1) / 11;
}
上面公式利用了数字互质的约束条件:除了1以外,他们没有公约数。
三、快速排序算法
什么是快速排序
快速排序基于划分机制(什么是划分机制呢,就是将数据划分为两组,一组是所有数据项都大于关键字的,另一组是所有数据都小于关键字的,这个关键字就称之为枢纽或支点,叫pivot),将一组数据项划分为两组子数组,然后递归的调用快速排序方法为每个子数组进行排序来实现的。
那么怎么选择枢纽呢,可以根据一下几点出发:
- 选择需要排序数组中的具体某一项的值作为关键字,枢纽。
- 可以选择其中的任意一项,例如选择数组中最右端的数据项作为枢纽。
- 划分完之后,如果枢纽被插入到左右子数组之间的交界处,那么枢纽就落在排序之后的最终位置上了。
快速排序的使用场景
快速排序是最常用的、通用的、效率最快的排序算法,适用于大多数场景,其时间复杂度为O(N*logN),因此追求时间效率的,推荐使用快速排序算法。
如何实现快速排序算法
实现思想:
- 使用递归思想进行左右划分子数组,编写递归退出的基值条件:如果划分的左坐标大于或等于右坐标,则退出递归。
- 选择一个合适的枢纽值:每次划分数组的最右端数据项。
- 传递左右坐标值,和枢纽值,进行划分操作。
- 划分完,将枢纽交换到右子数组的第一个位置。并返回枢纽的位置
- 根据枢纽位置,进行对左子数组进行递归快速排序
- 根据枢纽位置,进行对右子数组进行递归快速排序
- 重复循环执行1-6步骤,当划分的数组只剩两个数据项的时候,划分完即为有序。
- 最后递归返回,整个快速排序也就完成了。
public class TestMain {
public static int[] a = {1, 4, 5, 11, 2, 7, 6, 9, 8, 12, 66, 33, 55};
public static void main(String[] args) {
quickSort(0,a.length-1,a);
for (int i = 0; i < a.length; i++) {
System.out.print(" "+a[i]);
}
}
private static void quickSort (int left,int right, int[] array) {
// 由于使用递归思想,此处为递归的基值条件
if (left >= right) {
return;
}
// 选择数组中的一个数据项作为枢纽,本次选择最右端的数据项作为枢纽
int pivot = array[right];
// 对数组进行左右子数组进行划分,将枢纽位置与右子数组的第一位置交换,最后返回枢纽位置
int part = partition(left,right,pivot,array);
// 对划分好的左子数组进行递归快速排序
quickSort(left,part -1, array);
// 对划分好的右子数组进行递归快速排序
quickSort(part + 1,right,array);
}
private static int partition(int left, int right, int pivot,int[] array) {
int leftPtr = left;
int rightPtr = right;
while (true) {
while (array[leftPtr] < pivot) {
++leftPtr;
}
while (rightPtr > 0 && array[--rightPtr] > pivot){
}
if (leftPtr >= rightPtr) {
break;
}
// 交换位置,移到枢纽的正确两边
swap(leftPtr,rightPtr,array);
}
// 最后将枢纽的位置与右边子数组的第一个位置进行值交换
swap(leftPtr,right,array);
// 返回枢纽位置
return leftPtr;
}
private static void swap(int index1,int index2,int[] array) {
int temp = array[index1];
array[index1] = array[index2];
array[index2] = temp;
}
}
输出:1 2 4 5 6 7 8 9 11 12 33 55 66
快速排序的优缺点
快速排序除了代码实现比较复杂一点,其他基本无缺点(相对而言),优点一大推,重点是效率高,尽管的拿去用吧,come on!!!
- 点赞
- 收藏
- 关注作者
评论(0)