排序高级篇
1.希尔排序
随着输入规模增大,时间成本急剧上升,所以我们还要学习一些更为高级的排序算法。
它的原理是选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组;
对分组好的每一组数据完成插入排序;减小增长量,最小减为1,重复第二步操作。
- 增长量的确定:
int h = 1;
while(h<a.length/2) {
h = 2h+1;//循环结束后便可确定h的最大值
}
//h的减小规则:h=h/2
- 代码实现:
public static void sort(Comparable[] a) {
//确定增长量h的最大值
int h = 1;
while(h < a.length/2) {
h = h*2+1;
}
System.out.println("h="+h);
//sort
while(h >= 1) {
//1.找到待插入的元素
for (int i = h; i < a.length; i++) {
//2.将待插入的元素插入到有序数中
for (int j = i; j >= h; j -= h) {
//待插入的元素a[j],比较a[j]与a[j-h]
if (greater(a[j-h],a[j])) {
//exchange
exchange(a,j-h,j);
}else{//结束循环
break;
}
}
}
//减小h的值
h = h/2;
}
}
public static boolean greater(Comparable v,Comparable w) {
return v.compareTo(w) > 0;
}
private static void exchange(Comparable[] a,int i,int j) {
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。
原因是,当n值很大时数据项每一趟排序需要移动的个数很少,但数据项的距离很长。当n值减小时
每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。正是这两种情况的结合才使
希尔排序效率比插入排序高很多。Shell算法的性能与所选取的分组长度序列有很大关系。只对特定
的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。
时间复杂度分析
时间复杂度为O(n^1.5),需要注意的是增量序列的最后一个增量值必须是1.另外由于记录跳跃式的移动,希尔排序并不是一种稳定的排序方法。
2.归并排序
- 1.递归(recursion):定义方法时,在方法内部调用方法本身,称之为递归。
public static void cyl() {
System.out.println("what a amazing boy he is !");
cyl();
}
- 2.作用:将复杂(大)的问题转化为规模较小的问题来解决。递归策略只需要少量的程序代码描述出解题过程中所需的多次重复计算。在递归中,不能无限制的
调用自己,必须要有边界条件,能够让递归结束,因为每一次递归调用都会在栈内存中开辟新的空间,重新执行方法,如果递归的层级太深,很容易造成栈内存溢出。 - 3.举个栗子:使用递归求N的阶乘。
public class FactorialTest {
public static void main(String[] args) {
System.out.println(factorial(12));
//如果传入的值量级过大,会出现java.lang.StackOverflowError
}
public static long factorial(int n) {
if (n == 1) { return 1; }
return n * factorial(n-1);
}
}
- 归并排序是建立在归并操作上的一种有效的排序算法,该算法采用分治思想(Divide and Conquer)。将已有序的子序列进行合并,进而得到完全有序的序列。
- 1.原理是尽可能地将一组数据拆分成元素相等的子组,并对每一个子组继续拆分,直到拆分后每个子组的元素个数为1;
- 2.将相邻两个子组进行合并成一个有序的大组;
- 3.不断的重复步骤2,直到最终仅剩一个组为止。
- 那归并排序到底是干了一件什么事呢?小鹿给大家简单分析一下:首先呢我们拿到这个待排序的数组,先进行分组,分的组尽可能地小;之后呢对这些子组进行排序,来敲黑板划重点:我是不是在文章首部就提到此排序算法采用分治算法。
分完组之后的子组的排序会大大降低系统的工作量,想想看,如果给出的数组足够长,那么如果不去进行分组,那么一个排序所占用的时间空间可想而知。 - 代码实现 :
package DataStructure.sort.Merge;
public class MergeSort {
//辅助数组(member variable)
private static Comparable[] auxiliary;
//1.对数组内的元素进行排序
public static void sort(Comparable[] a) {
//1.初始化辅助数组auxiliary
auxiliary = new Comparable[a.length];
//2.定义low与high变量(记录最小和最大索引)
int low = 0;
int high = a.length - 1;
//3.调用sort重载方法完成a中从索引low到high的元素排序
sort(a,low,high);
}
//2.对数组a中从索引low到索引high之间的元素进行排序
private static void sort(Comparable[] a,int low,int high) {
//2.1做安全校验
if (high <= low) {
return;
}
//2.2对low至high之间的数据分为两组
int mid = (high+low)/2;
//2.3分别每组数据进行排序
//使用递归调用
sort(a,low,mid);
sort(a,mid+1,high);
//2.4最后进行归并
merge(a,low,mid,high);
}
//3.归并:从索引l到索引m为一个子组,从索引m+1到索引h为另一个子组,将数组a中的这两个子组的数据合并成一个有序的大组
private static void merge(Comparable[] a,int low,int mid,int high) {
//1. 定义三个指针
int i = low;
int p1 = low;
int p2 = mid+1;
//2. 遍历:移动p1与p2指针,
// 比较对应索引处的值,找出最小值并放到辅助数组的对应索引处
while (p1 <= mid && p2 <= high) {
//比较对应索引处的值
if (less(a[p1],a[p2])) {
auxiliary[i++] = a[p1++];
}else {
auxiliary[i++] = a[p2++];
}
}
//3.1.遍历:如果p1指针没有走完,则顺序移动p1指针,
// 把对应元素放到辅助数组的对应索引处
while (p1<=mid) {
auxiliary[i++] = a[p1++];
}
//3.1.遍历:如果p2指针没有走完,则顺序移动p1指针,
// 把对应元素放到辅助数组的对应索引处
while (p2<=high) {
auxiliary[i++] = a[p2++];
}
//4. 将辅助数组中的元素拷贝到原数组中
for (int index = low;index <= high;index++) {
a[index] = auxiliary[index];
}
}
//4.判断v是否小于w
private static boolean less(Comparable v,Comparable w) {
return v.compareTo(w) < 0;
}
}
- 时间复杂度分析:在上述算法中,对a数组进行排序,分别通过递归调用进行单独排序,最后将有序的子组归并为最终的排序结果。
该递归的出口在于如果一个数组不可再分为两个子组,然后调用merge方法归并排序。 - 假设一个数组有8个元素,那么将其递归拆分,共拆log8次,所以树(一种重要的数据结构)共有三层,自顶向下第k层有2^k个子数组,
每个数组的长度为2(3-k),归并最多需要2(3-k)次比较,因此每层的比较次数为2^k * 2(3-k)=23,三层总共比较24次。 - 总结一下:最终归并排序的时间复杂度为O(nlogn).
- 缺点:需要辅助数组,即额外申请空间,导致空间复杂度提升,是典型的空间换时间。