三种高级排序及其实现(希尔,归并,选择)

高级排序

之前我们学习过基础排序,包括冒泡排序,选择排序还有插入排序,并且对他们在最坏情况下的时间复杂度做了分析,发现都是O(N^2),而平方阶通过我们之前学习算法分析我们知道,随着输入规模的增大,时间成本将急剧上 升,所以这些基本排序方法不能处理更大规模的问题,接下来我们学习一些高级的排序算法,争取降低算法的时间 复杂度最高阶次幂。

希尔排序

希尔排序是插入排序的一种,又称“缩小增量排序”,是插入排序算法的一种更高效的改进版本。

前面学习插入排序的时候,我们会发现一个很不友好的事儿,如果已排序的分组元素为{2,5,7,9,10},未排序的分组元素为{1,8},那么下一个待插入元素为1,我们需要拿着1从后往前,依次和10,9,7,5,2进行交换位置,才能完成真正的插入,每次交换只能和相邻的元素交换位置,直观的想法就是一次交换,把1放到更前面的位置,比如一次交换就能把1插入到2和5之间,这样一次交换1就向前走了5个位置,可以减少交换的次数,这样的需求如何实现呢?接下来我们来看看希尔排序的原理。

需求

排序前:{9,1,2,5,7,4,8,6,3,5}

排序后:{1,2,3,4,5,5,6,7,8,9}

排序原理

  1. 选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组;
  2. 对分好组的每一组数据完成插入排序;
  3. 减小增长量,最小减为1,重复第二步操作。

    增长量h的确定:增长量h的值每一固定的规则,我们这里采用以下规则:
int h=1
while(h<5){
    h=2h+1//3,7
}
//循环结束后我们就可以确定h的最大值;
h的减小规则为:
h=h/2

希尔排序的API设计:

类名Shell
构造方法Shell():创建Shell对象
成员方法1.public static void sort(Comparable[] a):对数组内的元素进行排序
2.private static boolean greater(Comparable v,Comparable w):判断v是否大于w
3.private static void exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值

希尔排序代码

/**
 * 希尔排序
 */
public class Shell {
    /**
     * 对数组a的元素进行排序
     * @param a
     */
    public static void sort(Comparable[] a){
        int N = a.length;
        // 确定增长量h的最大值
        int h = 1;
        while (h<N/2){
            h = h*2+1;
        }
        //当增长量h小于1,排序结束
        while (h>=1){
            // 找到待插入元素(也就是每趟排序索引值和当前h值相等的元素)
            for (int i = h; i < N; i++) {
                // a[i]就是待插入的元素
                // 把a[i]插入到a[i-h],a[i-2h],a[i-3h]...序列中
                for (int j = i; j >=h ; j-=h) {
                    // a[j]就是待插入元素,依次和a[j-h],a[j-2h],a[j-3h]进行比较,如果a[j]小,那么交换位置,如果不小于,a[j]大,则插入完成。
                    // 因为索引为j和j-h,j-2h,j-3h是同组的元素
                    if (greater(a[j-h],a[j])){
                        exch(a,j-h,j);
                    }
                }
            }
        }
    }

    /*
        比较v元素是否大于w元素
    */
    private static boolean greater(Comparable v,Comparable w){
        return v.compareTo(w)>0;
    }
    
    /*
        数组元素i和j交换位置
    */
    private static void exch(Comparable[] a,int i,int j){
        Comparable t = a[i];
        a[i]=a[j];
        a[j]=t;
    }
}

希尔排序的时间复杂度分析

在希尔排序中,增长量h并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最 好的,对于希尔排序的时间复杂度分析,已经超出了我们课程设计的范畴,所以在这里就不做分析了。

归并排序

递归

正式学习归并排序之前,我们先学习一下递归算法。

定义:

定义方法时,在方法内部调用方法本身,称之为递归

public void show(){
    System.out.println("aaaa");
    show();
}
作用:

它通常把一个大型复杂的问题,层层转换为一个与原问题相似的,规模较小的问题来求解。递归策略只需要少量的 程序就可以描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

注意事项

在递归中,不能无限制的调用自己,必须要有边界条件,能够让递归结束,因为每一次递归调用都会在栈内存开辟 新的空间,重新执行方法,如果递归的层级太深,很容易造成栈内存溢出。

需求

请定义一个方法,使用递归完成求N的阶乘;

分析:
1!: 1
2!: 2*1=2*1!
3!: 3*2*1=3*2!
4!: 4*3*2*1=4*3!
...
n!: n*(n-1)*(n-2)...*2*1=n*(n-1)!
所以,假设有一个方法factorial(n)用来求n的阶乘,那么n的阶乘还可以表示为n*factorial(n-1)
代码实现
public class Test {
    public static void main(String[] args) throws Exception {
    int result = factorial(5);
   	 	System.out.println(result);
    }
    public static int factorial(int n){
        if (n==1){
            return 1;
        }
    	return n*factorial(n-1);
    }
}

归并排序

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子 序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序 表,称为二路归并。

需求

排序前:{8,4,5,7,1,3,6,2}

排序后:{1,2,3,4,5,6,7,8}

排序原理
  1. 尽可能的一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止
  2. 将相邻的两个子组进行并合成一个有序的大组
  3. 不断的重复步骤2,直到最终只有一个组为止。
归并排序API设计:
类名Merge
构造方法Merge():创建Merge对象
成员方法1.public static void sort(Comparable[] a):对数组内的元素进行排序
2.private static void sort(Comparable[] a, int lo, int hi):对数组a中从索引lo到索引hi之间的元素进 行排序
3.private static void merge(Comparable[] a, int lo, int mid, int hi):从索引lo到所以mid为一个子 组,从索引mid+1到索引hi为另一个子组,把数组a中的这两个子组的数据合并成一个有序的大组(从 索引lo到索引hi)
4.private static boolean less(Comparable v,Comparable w):判断v是否小于w
5.private static void exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值
成员变量1.private static Comparable[] assist:完成归并操作需要的辅助数组
归并原理

第一次填充(2比5小,所以2放入辅助数组,然后指针p2后移)

第二次填充(5比8大,5放入辅助数组然后指针p1后移)

第三次填充

第四次填充

第五次填充(指针p1已经无法后移,于是将8放入,指针p2后移)

第六次填充(指针p1已经无法后移,于是将9放入,指针p2也无法后移)

把辅助数组中排序好的元素拷贝到原数组中

归并排序代码实现
public class Merge {
    private static Comparable[] assist;//归并所需要的辅助数组

    /**
     * 对数组a中的元素进行排序
     * @param a
     */
    public static void sort(Comparable[] a){
        assist = new Comparable[a.length];
        // 低处的索引
        int lo  = 0;
        // 高出的索引
        int hi = a.length-1;
        sort(a,lo,hi);
    }

    /**
     * 对数组a中从lo到hi的元素进行排序
     * @param a
     * @param lo
     * @param hi
     */
    private static void sort(Comparable[] a,int lo,int hi){
        if (hi<lo){
            return;
        }
        int mid = lo + (hi - lo) / 2;
        // 对lo到mid之间的元素进行排序;
        sort(a,lo,mid);
        // 对mid+1到hi之间的元素进行排序
        sort(a, mid+1,hi);
        // 从lo到mid这组数据和mid到hi这组数据进行归并
        merge(a,lo,mid,hi);
    }

    private static void merge(Comparable[] a,int lo,int mid,int hi){
        // lo到mid这组数据和mid+1到hi这组数据归并到辅助数组assist对应的索引处
        int i = lo;   //u元素 定义一个指针,指向assist数组中开始填充数据索引
        int p1 = lo;  // 定义一个指针,指向第一组数据的第一个元素
        int p2 = mid + 1;// 定义一个指针,指向第二个数组的第一个元素
        // 比较左边小组和右边小组中的元素大小,哪个小,就把哪个数据填充到assist数组中
        while (p1<=mid&&p2<=hi){
            if (less(a[p1],a[p2])){
                // 赋值并且指针移动
                assist[i++] = a[p1++];
            }else {
                // 赋值并且指针移动
                assist[i++] = a[p2++];
            }
        }
        // 上面的循环结束后,如果退出循环的条件是p1<=mid,则证明左边小组中的数据已经归并完毕
        // 如果跳出循环的条件是p2<=hi,则证明右边的小组的数据已经填充完毕;
        // 所以需要把未填充完毕的数据继续填充到assist中,下面两个循环,智慧执行其中的一个
        while (p1<=mid){
            assist[i++] = a[p1++];
        }
        while (p2<=hi){
            assist[i++] = a[p2++];
        }
        // 到现在为止,assist数组中,从lo到hi的元素都是有序的,再把数据拷贝到a数组中对应的索引处
        for(int index = lo;index<=hi;index++){
            a[index] = assist[index];
        }
    }

    /**
     * 比较v元素是否小于w元素
     * @param v
     * @param w
     * @return
     */
    public static boolean less(Comparable v, Comparable w){
        return v.compareTo(v)<0;
    }

    /**
     * 数据元素i和j交换位置
     * @param a
     * @param i
     * @param j
     */
    private static void exch(Comparable[] a,int i,int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}
算法总结:

递归为本算法的的精髓!!!

我们分析一下程序中各个方法的功能, private static void sort(Comparable[] a,int lo,int hi)方法中使用了递归,该函数中调用了两次sort函数,两次的sort方法是为了实现对数组的拆分,第一次递归,拆分成了两份,第二次的递归拆分为了四分,第三次八份,之后以此类推,所有的sort方法之后都会执行merge方法,执行一次merge就是对数组和合并并且排序,第一次执行merge函数将两个单个元素合并,第二次执行将两个双元素数组和并,第三次执行将两个三元素数组和并,以此类推。最后实现了整个归并排序。

归并排序时间复杂度分析:

归并排序是分治思想的最典型的例子,上面的算法中,对a[lo…hi]进行排序,先将它分为a[lo…mid]和a[mid+1…hi] 两部分,分别通过递归调用将他们单独排序,最后将有序的子数组归并为最终的排序结果。该递归的出口在于如果 一个数组不能再被分为两个子数组,那么就会执行merge进行归并,在归并的时候判断元素的大小进行排序。

用树状图来描述归并,如果一个数组有8个元素,那么它将每次除以2找最小的子数组,共拆log8次,值为3,所以 树共有3层,那么自顶向下第k层有2k个子数组,每个数组的长度为2(3-k),归并最多需要2^(3-k)次比较。因此每层 的比较次数为 2^k * 2(3-k)=23,那么3层总共为 32^3。 假设元素的个数为n,那么使用归并排序拆分的次数为log2(n),所以共log2(n)层,那么使用log2(n)替换上面32^3中 的3这个层数,最终得出的归并排序的时间复杂度为:log2(n)* 2^(log2(n))=log2(n)*n,根据大O推导法则,忽略底 数,最终归并排序的时间复杂度为O(nlogn);

归并排序的缺点:

需要申请额外的数组空间,导致空间复杂度提升,是典型的以空间换时间的操作。

快速排序

快速排序是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一 部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序 过程可以递归进行,以此达到整个数据变成有序序列。

需求:

排序前:{6, 1, 2, 7, 9, 3, 4, 5, 8}

排序后:{1, 2, 3, 4, 5, 6, 7, 8, 9}

排序原理

1.首先设定一个分界值,通过该分界值将数组分成左右两部分;

2.将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于 或等于分界值,而右边部分中各元素都大于或等于分界值;

3.然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两 部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。

4.重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当 左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。

快速排序API设计:

类名Quick
构造 方法Quick():创建Quick对象
成员 方法1.public static void sort(Comparable[] a):对数组内的元素进行排序
2.private static void sort(Comparable[] a, int lo, int hi):对数组a中从索引lo到索引hi之间的元素 进行排序
3.public static int partition(Comparable[] a,int lo,int hi):对数组a中,从索引 lo到索引 hi之间的元 素进行分组,并返回分组界限对应的索引
4.private static boolean less(Comparable v,Comparable w):判断v是否小于w 5.private static void exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值

切分原理

把一个数组切分成两个子数组的基本思想:

  1. 找一个基准值,用两个指针分别指向数组的头部和尾部;
  2. 先从尾部向头部开始搜索一个比基准值小的元素,搜索到即停止,并记录指针的位置;
  3. 再从头部向尾部开始搜索一个比基准值大的元素,搜索到即停止,并记录指针的位置;
  4. 交换当前左边指针位置和右边指针位置的元素;
  5. 重复2,3,4步骤,直到左边指针的值大于右边指针的值停止。

第一次切分,去基准值为int key = arr[0] = 6,left初始值为0,right初始值为9

1、先从右往左搜索,搜索一个比基准值小的元素,arr[–right]>key ,暂时没有找到,如下图

2、arr[–right]<key ,找到一个比基准值小的元素,此时right = 7,如下图

3、再从左往右搜索,搜索一个比基准值大的元素,arr[++left]<key,暂时没有找到,如下图

4、arr[++left]<key,暂时还没有找到,如下图

5、arr[++left]>key,找到一个比基准值大的元素,此是left=3,那么交换left索引处的值和right索引处的值,如下图

交换后:

6、继续从右侧往左找一个比基准值小的元素,arr[–right]<key,找到了,此时right=6

7、再从左边向右找,找到一个比基准值大的元素,arr[++left]>key,找到了,此时left=4,交换left索引处和right索引处的值,如下

交换后:

8、继续从右侧往左找一个基准值较小的元素,arr[–right]<key,找到了,此时right=5,如下

9、再从左侧右找一个比基准值大的元素,arr[++left]<key,没有找到,此时left=5,如下

10、此时left>=right这个条件满足,也就是说,所有的元素都已经扫描完毕,最终交换基准值所在处索引处的值和当前left和right共同指向的索引处的值,所以6(基准值)和中间的3进行交换

交换后

11、此时,以索引为5为界限,我们就可以把一个数组切分成两个数组了

快速排序代码实现

public class Quick {
    public static void sort(Comparable[] a){
        int lo = 0;
        int hi = a.length-1;
        sort(a,lo,hi);
    }


    private static void sort(Comparable[] a,int lo,int hi){
        if (hi<lo){
            return;
        }
        // 对a数中lo到hi的元素进行切分
        int partition = partition(a,lo,hi)
        //对左边分组中的元素进行排序
        //对右边分组中的元素进行排序
        sort(a,lo,partition-1);
        sort(a,partition+1,hi);
    }

    private static int partition(Comparable[] a, int lo, int hi) {
        // 把最左边的元素当作基准值
        Comparable key = a[lo];
        // 定义一个左指针,初始指向最左边的元素
        int left = lo;
        // 定义一个右指针,初始指向左右侧的元素下一个位置
        int right = hi+1;
        // 进行切分
        while (true){
            // 先从右往左扫描,找到一个比基准值小的元素
            while (true){
                // 先从右往左扫描,找到一个比基准值小的元素
                while (less(key,a[--right])){// 循环停止,证明找到了一个比基准值小的元素
                    if (right==lo){
                        break;//已经扫描到最左边了,无需继续扫描
                    }
                }

                // 再从左往右扫描,找一个比基准值大的元素
                while (less(a[++left],key)){// 循环停止,证明找到了一个比基准值大的元素
                    if (left==hi){
                        break;// 已经扫描到了最右边,无需继续扫描
                    }
                }

                if (left>=right){
                    // 扫描完了所有元素,结束循环
                    break;
                }else {
                    // 交换right和right索引处的元素
                    exch(a,left,right);
                }
            }
            // 交换最后right索引处和基准值所在的索引处的值
            exch(a,lo,right);
            return right;
        }

    }


    /*
    数组元素i和j交换位置
    */
    private static void exch(Comparable[] a, int i, int j) {
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    /*
    比较v元素是否小于w元素
    */
    private static boolean less(Comparable v, Comparable w) {
        return v.compareTo(w) < 0;
    }
}

算法总结

快速排序算法的精髓同样是递归,在 private static void sort(Comparable[] a,int lo,int hi)方法中,使用了两次sort方法进行排序,每一次调用sort方法都是将一段数组切割为两段,并且左边的数组中所有的数字小于右边的元素。然后再递归进行对左右两边的数组进行相同的操作。

快速排序和归并排序的却别:

快速排序是另外一种分治的排序算法,它将一个数组分成两个子数组,将两部分独立的排序。快速排序和归并排序 是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并从而将整个数组排序,而快速排序的 方式则是当两个数组都有序时,整个数组自然就有序了。在归并排序中,一个数组被等分为两半,归并调用发生在 处理整个数组之前,在快速排序中,切分数组的位置取决于数组的内容,递归调用发生在处理整个数组之后。

快速排序的时间复杂度问题

快速排序的一次切分从两头开始交替搜索,直到left和right重合,因此,一次切分算法的时间复杂度为O(n),但整个快速排序的时间复杂度和切分的次数相关。

最优情况:每一次切分选择的基准数字刚好将当前序列等分。

如果我们把数组的切分看做是一个树,那么上图就是它的最优情况的图示,共切分了logn次,所以,最优情况下快 速排序的时间复杂度为O(nlogn);

最坏情况:每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得每次切分都会有一个子组,那么总 共就得切分n次,所以,最坏情况下,快速排序的时间复杂度为O(n^2);


平均情况:每一次切分选择的基准数字不是最大值和最小值,也不是中值,这种情况我们也可以用数学归纳法证 明,快速排序的时间复杂度为O(nlogn),由于数学归纳法有很多数学相关的知识,容易使我们混乱,所以这里就不对平均情况的时间复杂度做证明了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值