数据结构与算法(二)——高级排序

前言

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

一、希尔排序

      希尔排序是插入排序的一种,又称“缩小增量排序”,是插入排序算法的一种更高级的改进版本。
      前面学习插入排序的时候,我们会发现一个很不友好的事儿,如果已排序的分组元素为{2,5,7,9,10},未排序的分组元素为{1,9},那么下一个待插入元素为1,我们需要拿着1从后往前,依次和10,9,7,5,2进行交换位置,才能完成真正的插入,每次交换只能和相邻的元素交换位置。那如果我们要提高效率,可以减少交换测次数。这样的需求如何实现呢?接下来我们看看希尔排序的原理。

1.需求

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

2.排序原理

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

增长量h的确定:增长量h的值确定的规则,可以采用以下规则:

		int h = 1;
        while(h < 数组的长度/2){
            h = 2*h + 1;
        }
        //循环结束后我们就可以确定h的最大值:
        h的减少规则为:h = h/2;
3.希尔排序的API设计
类名Shell
构造方法Shell():创建Shell对象
成员方法1.public static void sort(Comparable[] a) :对数组内的元素进行排序
2.private static boolean grater(Comparable v,Comparable w):判断v是否大于w
3.private static exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值
4.希尔排序的代码实现
import java.util.Arrays;

public class Shell {

    public static void main(String[] args) {
        //原始数据
        Integer[] a = {9,1,2,5,7,4,8,6,3,5};
        sort(a);
        System.out.println(Arrays.toString(a));
    }

    /**
     * 对数组a中的元素进行排序
     * @param a
     */
    public static void sort(Comparable[] a){

        //1.根据数据a的长度,确定增长量h的初始值
        int h = 1;
        while(h < a.length/2){
            h = 2*h+1;
        }
        //2.希尔排序
       while(h >= 1){
            //排序
           //2.1 找到待插入的元素
           for(int i = h; i < a.length; i++){
               //2.2 把待插入的元素插入到有序数列中
               for(int j = i; j >= h; j-=h){
                   //待插入的元素是a[j],比较a[j]和a[j-h]
                   if(greater(a[j-h],a[j])){
                       //交换元素
                       exch(a,j-h,j);
                   }else{
                       //待插入元素已经找到了合适的位置,结束循环
                       break;
                   }
               }
           }
           //减小h的值
           h = h/2;
        }
    }

    /**
     * 比较v元素是否大于w元素
     * @return
     */
    private static boolean greater(Comparable v,Comparable w){
        return v.compareTo(w) > 0;
    }

    /**
     * 数组元素i和j交换位置
     * @param a
     * @param i
     * @param j
     */
    private static void exch(Comparable[] a,int i,int j){
        Comparable temp;
        temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}
5.希尔排序分析

      在希尔排序中,增长量并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最好的,故不对希尔排序的时间复杂度进一步分析。
      可以使用事后分析法对希尔排序和插入排序进行性能比较。测试的思想:在执行排序前记录一个时间,在排序完成后记录一个时间,两个时间的时间差就是排序的耗时。
      经测试,当需要对一个有10w数据的倒序数组进行排序时(倒序是最坏的情况),插入排序需要的时间大概是37499毫秒,而希尔排序需要的时间是30毫秒。

二、归并排序
1.递归简介

      递归通常把一个大型复杂的问题,层层转换为一个与原问题相似的,规模较小的问题来求解。递归策略只需要少量的程序就可以描述出解题过程的多次重复计算,大大减少了程序的代码量。
      但是在递归中,不能无限制的调用自己,必须要有边界条件,能够让递归结束,因为每一次递归调用都会在栈内存开辟新的空间,重新执行方法,如果递归的层级太深,很容易造成栈内存溢出。假设在有边界的情况下,根据条件需要进行10w次的递归,则需要栈内存开辟10w个新的空间,此时可能会出现栈内存溢出(java.lang.StackOverflowError)。

2.归并排序

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

3.需求

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

4.排序原理

(1)尽可能地一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止;
(2)将相邻的两个子组进行合并成一个有序的大组;
(3)不断地重复步骤2,直到最终只有一个组为止。
在这里插入图片描述

5.归并排序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 mergge(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 exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值
成员变量1.private static Comparable[] assist:完成归并操作需要的辅助数组
6.归并排序代码实现

在这里插入图片描述

import java.util.Arrays;

//归并排序
public class Merge {

    public static void main(String[] args) {
        //原始数据
        Integer[] a = {8,4,5,7,1,3,6,2};
        sort(a);
        System.out.println(Arrays.toString(a));
    }

    //归并所需要的辅助数组
    private static Comparable[] assist;

    /**
     * 比较v元素是否小于w元素
     * @param v
     * @param w
     * @return
     */
    private static boolean less(Comparable v,Comparable w){
        return v.compareTo(w) < 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;
    }

    /**
     * 对数组中的元素进行排序
     * @param a
     */
    public static void sort(Comparable[] a){
        //1.初始化辅助数组assist
        assist = new Comparable[a.length];
        //2.定义一个lo变量,和hi变量,分别记录数组中最小的索引和最大的索引
        int lo = 0;
        int hi = a.length - 1;
        //3.调用sort重载方法完成数组a中,从索引lo到索引hi的元素的排序
        sort(a,lo,hi);
    }

    /**
     * 对数组a中从to到hi的元素进行排序
     * @param a
     * @param lo
     * @param hi
     */
    private static void sort(Comparable[] a,int lo,int hi){
        //做安全性校验
        if(hi <= lo){
            return;
        }

        //对lo到hi之间的数据分为两个组
        int mid = (hi + lo)/2;

        //分别对每一组数据进行排序
        sort(a,lo,mid);
        sort(a,mid+1, hi);

        //再把两个小组中的数据进行归并
        merge(a,lo,mid,hi);
    }

    /**
     * 对数组中从lo到mid为一组,从mid+1到hi为一组,对这两组数据进行归并
     * @param a
     * @param lo
     * @param mid
     * @param hi
     */
    private static void merge(Comparable[] a,int lo,int mid,int hi){
        //定义三个指针
        int i = lo;
        int p1 = lo;
        int p2 = mid + 1;

        //遍历,移动p1指针和p2指针,比较对应索引处的值,找出最小的那个,放到辅助索引数组的对应索引处
        while(p1 <= mid && p2 <= hi){
            //比较对应索引处的值
            if(less(a[p1],a[p2])){
                assist[i++] = a[p1++];
            }else{
                assist[i++] = a[p2++];
            }
        }
        //遍历,如果p1的指针没有走完,那么顺序移动p1指针,把对应的元素放到辅助数组的对应索引处
        while(p1 <= mid){
            assist[i++] = a[p1++];
        }
        //遍历,如果p2的指针没有走完,那么顺序移动p2指针,把对应的元素放到辅助数组的对应索引处
        while(p2 <= hi){
            assist[i++] = a[p2++];
        }
        //把辅助数组中的元素拷贝到原数组中
        for(int index=lo;index <= hi;index++){
            a[index] = assist[index];
        }

    }

}
7.归并排序时间复杂度分析

在这里插入图片描述

8.归并排序的优缺点

      需要申请额外的数组空间,导致空间复杂度提升,是典型的以空间换时间的操作。效率上和希尔排序差不多,上述希尔排序与插入排序做对比的例子来测试归并排序,需要时间是70毫秒左右。

三、快速排序

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

1.需求

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

2.排序原理

(1)首先设定一个分界值,通过该分界值将数组分成左右两部分;
(2)将大于或等于分界值的数据放到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小的值,右边放置较大值。右侧的数组数据也可以做类似处理。
(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。
在这里插入图片描述

3.快速排序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.private static void partition(Comparable [ ] a,int lo,int mid,int hi):对数组a中,从索引lo到索引hi之间的元素进行分组,并返回分组界限对应的索引
4.private static boolean less(Comparable v,Comparable w):判断v是否小于w
5.private static exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值
4.切分原理

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

4.快速排序代码实现
import java.util.Arrays;

/**
 * 快速排序
 */
public class Quick {

    public static void main(String[] args) {
        Integer[] a = {6,1,2,7,9,3,4,5,8};
        sort(a);
        System.out.println(Arrays.toString(a));
    }

    /**
     * 比较v元素是否小于w元素
     * @param v
     * @param w
     * @return
     */
    private static boolean less(Comparable v,Comparable w){

        return v.compareTo(w) < 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;
    }

    //对数组内的元素进行排序
    public static void sort(Comparable[] a){
        int lo = 0;
        int hi = a.length - 1;
        sort(a,lo,hi);
    }

    //对数组a中从索引lo到索引hi之间的元素进行排序
    private static void sort(Comparable[] a,int lo,int hi){
        //安全性校验
        if(hi <= lo){
            return;
        }

        //需要对数组中lo索引到hi索引处的元素进行分组(左子组和右子组)
        int partition = partition(a,lo,hi);//返回的是分组的分界值所在的索引,分界值位置交换后的索引

        //让左子组有序
        sort(a,lo,partition - 1);

        //让右子组有序
        sort(a,partition + 1,hi);
    }

    //对数组a中,从索引lo到索引hi之间的元素进行分组,并返回分组界限对应的索引
    public  static int partition(Comparable[] a,int lo,int hi){
        //确定分界值
        Comparable key = a[lo];
        //定义两个指针,分别指向将切分元素的最小索引处和最大索引处的下一个位置
        int left = lo;
        int right = hi + 1;

        //切分
        while(true){
            //先从右往左扫描,移动right指针,找到一个比分界值小的元素,停止
            while(less(key,a[--right])){
                if(right == lo){
                    break;
                }
            }

            //再从左往右扫描,移动left指针,找到一个比分界值大的元素,停止
            while(less(a[++left],key)){
                if(left == hi){
                    break;
                }
            }
            //判断left>=right,如果是,则证明元素扫描完毕,结束循环,如果不是,则交换元素即可
            if(left >= right){
                break;
            }else{
                exch(a,left,right);
            }
        }
        exch(a,lo,right);
        return right;
    }
}
5.快速排序和归并排序的区别

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

四、排序的稳定性
1.稳定性的定义

      数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,如果使用某种排序算法后,能够保证A元素依然在B元素的前面,可以说该算法是稳定的。

在这里插入图片描述

2.稳定性的意义

      如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。例如要排序的内容是一组商品对象,第一次排序按照价格由低到高排序,第二次排序按照销量由高到低排序,如果第二次排序使用稳定性算法,就可以使得相同销量的对象依旧保持着价格由高到低的顺序展现,只有销量不同的对象才需要重新排序。这样既可以保持第一次排序的原有意义,而且可以减少系统开销。

3.常见排序算法的稳定性
  • 冒泡算法
          只有当arr[i] > arr[i+1]的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定排序算法。
  • 选择排序
          选择排序是给每个位置选择当前元素最小的,例如有数据{5(1),8,5(2),2,9},第一遍选择到的最小元素为2,所以5(1)会和2进行位置交换位置,此时5(1)到了5(2)后面,破坏了稳定性,所以选择排序是一种不稳定的排序算法。
  • 插入排序
          比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找,直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好后的顺序,所以插入排序是稳定的。
  • 希尔排序
          希尔排序是按照不同步长对元素进行插入排序,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入顺序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
  • 归并排序
          归并排序在归并的过程中,只有arr[i]<arr[i+1]的时候才会交换位置,如果两个元素相等则不会交换位置,所以它并不会破坏稳定性,归并排序是稳定的。
  • 快速排序
          快速排序需要一个基准值,在基准值的右侧找一个比基准值小的元素,在基准值的左侧找一个比基准值大的元素,然后交换这两个元素,此时会破坏稳定性,所以快速排序是一种不稳定的算法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值