Java常用排序方式总结

排序的基本概念

  • **排序(sorting)**的功能是将一个数据元素的任意序列,重新排列成一个按关键字有序的序列
  • 在待排序的序列中存在多个具有相同关键字的元素。
    • 假设Ki=Kj(1≤ i≤ n,1≤ j≤ n,i≠j),若在排序之前的序列中Ri在Rj之前,经过排序后得到的序列中Ri仍然在Rj之前,则称所用的排序方法是稳定的
    • 当相同关键字元素的前后关系在排序中发生变化,则称所用的排序方法是不稳定的

插入类排序

直接插入排序

它的基本思想是:仅有一个元素的序列总是有序的,因此,对n 个记录的序列,可从第二个元素开始直到第n 个元素,逐个向有序序列中执行插入操作,从而得到n 个元素按关键字有序的序列。以关键字序列{ 26 , 53 , 48 , 11 , 13 , 48 , 32 , 15}为例,直接插入排序的过程如图9-1 所示。

  • 直接插入排序的时间复杂度为O(n2),并且是一个稳定的排序方法

折半插入排序

  • 排序元素数量n 很大,则不宜采用直接插入排序方法
  • 在有序序列中确定插入位置,则可以不断二分有序序列来确定插入位置,即搜索插入位置的方法可以使用折半查找实现。

算法9-2 binInsertSort
输入:数据元素数组r,数组r 的待排序区间[low…high]
输出:数组r 以关键字有序
代码:

public void binInsertSort(Object[] r, int low, int high){
    for (int i=low+1; i<=high; i++){
        Object temp = r[i]; //保存待插入元素
        int hi = i-1; int lo = low; //设置初始区间
        while (lo<=hi){ //折半确定插入位置
            int mid = (lo+hi)/2;
            if(strategy.compare(temp,r[mid])<0)
            hi = mid - 1;
            else lo = mid + 1;
        }
        for (int j=i-1;j>hi;j--) r[j+1] = r[j]; //移动元素
        r[hi+1] = temp; //插入元素
    }//for
}
  • 折半插入排序所需的辅助空间与直接插入排序相同,从时间上比较,折半插入排序仅减少了元素的比较次数,但是并没有减少元素的移动次数,因此折半插入排序的时间复杂度仍为O(n2)。

希尔排序

希尔排序又称为“缩小增量排序”,它也是一种属于插入排序类的排序方法,是一种对直接插入排序的改进,但在时间效率上却有较大的改进。

  • 直接插入排序的时间复杂度为O(n2),但是在待排序元素序列有序时,其时间复杂度可提高至O(n)
  • 希尔排序的基本思想是:首先将待排序的元素分为多个子序列,使得每个子序列的元素个数相对较少,对各个子序列分别进行直接插入排序,待整个待排序序列“基本有序”后,再对所有元素进行一次直接插入排序。
  • 希尔排序的排序过程:
    1. 选择一个步长序列t1,t2,…,tk,其中ti>tj(i<j),tk=1;
    2. 按步长序列个数 k,对待排序元素序列进行 k趟排序;
    3. 每趟排序,根据对应的步长ti,将待排序列分割成ti个子序列,分别对各子序列进行直接插入排序。

希尔排序的过程如图9-2 所示

  • 在每趟排序过程中子序列的划分并不是简单的逐段划分,而是将间隔某个步长的元素组成一个子序列

算法9-3 shellSort
输入:数据元素数组r,数组r 的待排序区间[low…high],步长序列delta
输出:数组r 以关键字有序
代码:

public void shellSort(Object[] r, int low, int high, int[] delta){
    for (int k=0;k<delta.length;k++)
    	shellInsert(r, low, high, delta[k]); //一趟步长为delta[k]的直接插入排序
}
private void shellInsert(Object[] r, int low, int high, int deltaK){
    for (int i=low+deltaK; i<=high; i++)
        if (strategy.compare(r[i],r[i-deltaK])<0){ //小于时,需将r[i] 插入有序表
            Object temp = r[i];
            int j = i-deltaK;
            for(; j>=low&&strategy.compare(temp,r[j])<0; j=j-deltaK)
            r[j+deltaK] = r[j]; //记录后移
            r[j+deltaK] = temp; //插入到正确位置
        }
}
  • 希尔排序的时间复杂度为Ο(n3/2),其中t为希尔排序的趟数,1≤k≤t≤⎣log (n+1)⎦。
  • 实际的应用中,在选择步长序列时应当注意:应使步长序列中的步长值互质,并且最后一个步长值必须等于1。

交换类排序

冒泡排序

算法9-4 bubbleSort
输入:数据元素数组r,数组r 的待排序区间[low…high]
输出:数组r 以关键字有序
代码:

public void bubbleSort(Object[] r, int low, int high){
    int n = high - low + 1;
    for (int i=1;i<n;i++)
    for (int j=low;j<=high-i;j++)
    if (strategy.compare(r[j],r[j+1])>0)
    {
        Object temp = r[j];
        r[j] = r[j+1];
        r[j+1] = temp;
    }
}//end of bubbleSort
  • 起泡排序的时间复杂度为Ο(n2)

快速排序

快速排序是将分治法运用到排序问题中的一个典型例子,快速排序的基本思想是:通过一个枢轴(pivot)元素将n 个元素的序列分为左、右两个子序列Ll 和Lr,其中子序列Ll中的元素均比枢轴元素小,而子序列Lr 中的元素均比枢轴元素大,然后对左、右子序列分别进行快速排序,在将左、右子序列排好序后,则整个序列有序,而对左右子序列的排序过程直到子序列中只包含一个元素时结束,此时左、右子序列由于只包含一个元素则自然有序。

用分治法的三个步骤来描述快速排序的过程如下:

  1. 划分步骤:通过枢轴元素 x 将序列一分为二, 且左子序列的元素均小于x,右子序列的元素均大于x;
  2. 治理步骤:递归的对左、右子序列排序;
  3. 组合步骤:无

对待排序序列进行划分的做法是:使用两个指针low 和high 分别指向待划分序列r 的范围,取low 所指元素为枢轴,即pivot = r[low]。划分首先从high 所指位置的元素起向前逐一搜索到第一个比pivot 小的元素,并将其设置到low 所指的位置;然后从low 所指位置的元素起向后逐一搜索到第一个比pivot 大的元素,并将其设置到high 所指的位置;不断重复上述两步直到low = high 为止,最后将pivot 设置到low 与high 共同指向的位置。

  • 图9-4 说明了一次划分的过程。

算法9-5 partition
输入:数据元素数组r,划分序列区间[low…high]
输出:将序列划分为两个子序列并返回枢轴元素的位置
代码:

 private int partition(int[] nums,int low,int high) {
        //使用r[low]作为枢轴元素
        int privot = nums[low];
        //从两端交替向内扫描
        while (low < high) {
            while (low<high&&nums[high]>=privot) high--;
            //将比pivot 小的元素移向低端
            nums[low] = nums[high];
            while (low<high&&nums[low]<=privot) low++;
            //将比pivot 大的元素移向高端
            nums[high] = nums[low];
        }
        //设置枢轴
        nums[low] = privot;
        //返回枢轴元素位置
        return low;
    }

算法9-6 quickSort
输入:数据元素数组r,数组r 的待排序区间[low…high]
输出:数组r 以关键字有序
代码:

//快排
    private void quikSort(int[] nums,int low,int high) {
        if (low < high) {
            int privot = partition(nums, low, high);
            quikSort(nums,low,privot-1);
            quikSort(nums,privot+1,high);
        }
    }
效率分析

时间效率:快速排序算法的运行时间依赖于划分是否平衡,即根据枢轴元素pivot 将序列划分为两个子序列中的元素个数,而划分是否平衡又依赖于所使用的枢轴元素。下面我们在不同的情况下来分析快速排序的渐进时间复杂度。

算法的时间复杂度T(n) = Tp(n) + T(n-1),其中Tp(n)是对具有n个元素的序列进行划分所需的时间,由以上划分算法的过程可以得到Tp(n) = Θ(n)。T(n) =Θ(n) + T(n-1) =Θ(n2)。

在待排序序列本身已经有序或逆向有序时,快速排序的时间复杂度为Ο(n2),而在有序时插入排序的时间复杂度为Ο(n)。

  • 快速排序的最好情况是在每次划分时,都将序列一分为二,正好在序列中间将序列分成长度相等的两个子序列,算法的时间复杂度T(n) = Tp(n) + 2T(n/2),由于Tp(n) = Θ(n),由master method知道T(n) = Θ(n log n)

在平均情况下,快速排序的时间复杂度T(n) = kn ㏑ n,其中k 为某个常数,经验证明,在所有同数量级的排序方法中,快速排序的常数因子k 是最小的。因此就平均时间而言,快速排序被认为是目前最好的一种内部排序方法

空间效率:虽然从时间上看快速排序的效率优于前述算法,然而从空间上看,在前面讨论的算法中都只需要一个辅助空间,而快速排序需要一个堆栈来实现递归。若每次划分都将序列均匀分割为长度相近的两个子序列,则堆栈的最大深度为log n,但是,在最坏的情况下,堆栈的最大深度为n

选择类排序

简单选择排序

简单选择排序的基本思想非常简单,即:第一趟,从n 个元素中找出关键字最小的元素与第一个元素交换;第二趟,在从第二个元素开始的n-1 个元素中再选出关键字最小的元素与第二个元素交换;如此,第k 趟,则从第k 个元素开始的n-k+1 个元素中选出关键字最小的元素与第k 个元素交换,直到整个序列按关键字有序。

效率分析
  • **空间效率:**显然简单选择排序只需要一个辅助空间。
  • **时间效率:**在简单选择排序中,所需移动元素的次数较少,在待排序序列已经有序的情况下,简单选择排序不需要移动元素
  • **算法改进思想:**从上述效率分析中可以看出,简单选择排序的主要操作是元素间的比较操作,因此改进简单选择排序应从减少元素比较次数出发。

树型选择排序

基本思想是:先把待排序的n个元素两两进行比较,取出较小者,若轮空则直接进入下一轮比较;然后在⎡n/2⎤个较小者中,采用同样的方法进行比较,再选出较小者;如此反复,直到选出关键字最小的元素为止。

这个过程可以使用一颗具有n个结点的完全二叉树来表示,最终选出的关键字最小的元素就是这棵二叉树的根结点

例如图9-6(a)给出了对8 个元素的关键字{ 26 , 53 , 48 , 11 , 13 , 48 , 32 , 15}选出最小关键字元素的过程。

效率分析

时间效率:在树型选择排序过程中为找到关键字最小的元素一共进行了n-1 次比较。叶子结点的完全二叉树其高度为⎡log n⎤,由此可知除最小关键字外,每选择一个次小关键字需要进行⎡log n⎤次比较。因此树型选择排序的时间复杂度T(n) = (n-1) ⎡log n⎤ +(n-1) = Ο(n log n)

**空间效率:**与简单选择排序相比,使用了更多的辅助空间

算法改进思想:树型选择排序的缺点是使用了较多的辅助空间,以及和∞进行多余比较,为弥补树型选择排序的这些缺点,J.W.J.Williams 在1964 年提出了进一步的改进方法,即堆排序

堆排序

  • n个元素的序列{k1 , k2 , … , kn},当且仅当满足下列关系时,称之为堆。

① { k i ≤ k 2 i k i ≤ k 2 i + 1 或 ② { k i ≥ k 2 i k i ≥ k 2 i + 1 其 中 i = 1 , 2 , … , ⌊ n / 2 ⌋ ①\left\{\begin{array}{l} k_i\leq k_{2i} \\ k_i\leq k_{2i+1} \end{array}\right. \quad 或②\left\{\begin{array}{l} k_i\geq k_{2i} \\ k_i\geq k_{2i+1} \end{array}\right. \quad 其中i=1 , 2 , … , \lfloor n/2 \rfloor {kik2ikik2i+1{kik2ikik2i+1i=1,2,,n/2

若满足条件①,则称为小顶堆,若满足条件②,则称为大顶堆

如果将序列**{k1 , k2 , … , kn}对应为一维数组**,且序列中元素的下标与数组中下标一致,即数组中下标为0 的位置不存放数据元素,此时该序列可看成是一颗完全二叉树,则堆的定义说明,在对应的完全二叉树中非终端结点的值均不大于(或不小于)其左右孩子结点的值。

例如图9-7 显示了两个堆,其对应的元素序列分别为{45 , 26 , 18 , 23 , 19 , 5 , 11 , 14}、{13 , 32 , 15 , 40 , 51 , 38}。其中(a)是一个大顶堆,(b)是一个小顶堆。

将这n 个元素按关键字建成堆,将堆顶元素输出,得到n 个元素中关键字最大(或最小)的元素。然后,再将剩下的n-1 个元素重新建成堆,再输出堆顶元素,

  • 实现对排序时需要解决两个问题:
    1. 如何将n 个元素的序列按关键字建成堆;
    2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其按关键字成为一个新堆。

我们首先第二个问题,即输出堆顶元素后,对剩余元素重新建成堆的调整过程。
设有一个具有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。具体的调整方法是:首先,将堆底元素(最后一个元素)送入堆顶,此时堆被破坏,其原因仅是根结点不满足堆的性质,而根结点的左右子树仍是堆。然后,将根结点与左、右子女中较大(或较小)的进行交换。若与左孩子交换,则左子树堆被破坏,且仅左子树的根结点不满足堆的性质;若与右孩子交换,则右子树堆被破坏,且仅右子树的根结点不满足堆的性质。继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,则堆被重建。我们称这个自根结点到叶子结点的调整过程为筛选。

例如图9-8(a)为一个大顶堆,在输出堆顶元素53 之后,将堆底元素26 送入堆顶,如图9-8(b)所示;然后将26 与36、42 中大的元素交换,交换后,以26 为根的子树已是一个堆;此时筛选结束,得到一个新堆,如图9-8(c)所示。如果继续输出堆顶元素42,然后重建堆,则结果如图9-8(d)所示。

对于待排序的初始序列{28 , 26 , 17 , 36 , 20 , 42 , 11 , 53},初始建堆的过程如图9-9 所示。(a)是由初始序列得到的完全二叉树;初始建堆首的过程,以按层从下到上的第一个非叶子结点开始,即从36 开始,对36 进行调整,过程如图(b)所示,调整结果如图(c)所示;然后对下一个非叶子结点17 进行调整,调整过程如图(c),结果如图(d)所示;继续上述过程直到根结点28 为止,对28 进行调整后,即得到一个大顶堆,结果如图(f)所示。

算法9-8 heapAdjust
输入:数据元素数组r,数组r 的待调整区间[low…high]
输出:调整r[low…high]使之成为大顶堆
代码:

//已知r[low..high]中除r[low]之外,其余元素均满足堆的定义
private void heapAdjust(Object[] r, int low, int high){
    Object temp = r[low];
    for (int j=2*low; j<=high; j=j*2){ //沿关键之较大的元素向下进行筛选
        //j 指向关键之较大的元素
        if (j<high&&strategy.compare(r[j],r[j+1])<0) j++;
        //若temp 比其孩子都大,则插入到low 所指位置
        if (strategy.compare(temp,r[j])>=0) break;
        r[low] = r[j]; low = j; //向下筛选
    }
    r[low] = temp;
}

算法9-8 heapSort
输入:数据元素数组r
输出:对r[1…length-1]排序
代码:

public void heapSort(Object[] r){
    int n = r.length - 1;
    for (int i=n/2; i>=1; i--) //初始化建堆
    heapAdjust(r,i,n);
    for (int i=n; i>1; i--){ //不断输出堆顶元素并调整r[1..i-1]为新堆
        Object temp = r[1]; //交换堆顶与堆底元素
        r[1] = r[i];
        r[i] = temp;
        heapAdjust(r,1,i-1); //调整
    }
}

注:为了代码的易读性,在算法9-8 中对数组r 进行排序时,排序的范围是[1…length-1],这一点和前面的算法是不一样的,前面的算法其排序范围是由参数指定的。当然堆排序也可以对指定范围内的元素进行排序,只不过在对下标进行操作之前都必须进行相应的处理,读者可自行设计实现指定范围的堆排序算法。

效率分析

**空间效率:**显然堆排序只需要一个辅助空间。

时间效率:首先,对于深度为k的堆,heapAdjust算法中所需执行的比较次数至多为2k次。则在初始建堆的过程中,对于具有n个元素、深度为h的堆而言,由于在i层上最多有2i个结点,以这些结点为根的二叉树深度最大为h-i,那么⎣n/2⎦次调用heapAdjust时总共进行的关键字比较次数Tinit为:
T init  = ∑ i = 1 − 1 0 2 i ⋅ 2 ( h − i ) = O ( ∑ j = 1 h 2 h − j ⋅ j ) = O ( n ∑ j = 1 h j 2 j ) = O ( n ) 因 为 ∑ i = 0 ∞ i 2 i = 2 T_{\text {init }}=\sum_{i=1-1}^{0} 2^{i} \cdot 2(h-i)=O\left(\sum_{j=1}^{h} 2^{h-j} \cdot j\right)=O\left(n \sum_{j=1}^{h} \frac{j}{2^{j}}\right)=O(n)\quad因为\sum_{i=0}^{\infty}\frac{i}{2^{i}}=2 Tinit =i=1102i2(hi)=O(j=1h2hjj)=O(nj=1h2jj)=O(n)i=02ii=2
初始化需要执行的比较操作的
次数为Ο(n)

每输出一次堆顶元素需要进行一次调整,而每次调整所需的比较次数为Ο(log n),因此n 次输出总共需要的比较次数为Ο(n log n)

归并排序

归并排序的基本思想是基于合并操作,即合并两个已经有序的序列是容易的,不论这两个序列是顺序存储还是链式存储,合并操作都可以在Ο(m+n)时间内完成(假设两个有序表的长度分别为m 和n)。为此,由分治法的一般设计步骤得到归并排序的过程为:

  1. 划分:将待排序的序列划分为大小相等(或大致相等)的两个子序列;
  2. 治理:当子序列的规模大于1 时,递归排序子序列,如果子序列规模为1 则成为有序序列;
  3. 组合:将两个有序的子序列合并为一个有序序列。

图9-10 显示了归并算法的执行过程。假设待排序序列为{4, 8, 9, 5, 2, 1, 4, 6},如图所示,归并排序导致了一系列递归的调用,而这一系列调用过程可以由一个二叉树来表示

算法9-9 merge
输入:数据元素数组a,a 待合并的两个有序区间[p…q]以及[q+1…r]
输出:将两个有序区间合并为一个有序区间
代码:

private void merge(Object[] a, int p, int q, int r){
    Object[] b = new Object[r-p+1];
    int s = p;
    int t = q+1;
    int k = 0;
    while (s<=q&&t<=r)
        if (strategy.compare(a[s],a[t])<0)
        b[k++] = a[s++];
        else
        b[k++] = a[t++];
    while (s<=q) b[k++] = a[s++];
    while (t<=r) b[k++] = a[t++];
    for (int i=0; i<b.length; i++)
    	a[p+i] = b[i];
}

假设待合并的两个子序列总长为n,则这n 个元素在从数组a 移动到b 的过程中,每个元素移动一次,而每次元素移动最多只需要一次比较;最后从数组b 移回a 也只需要n 次移动操作即可,因此,算法merge 的时间复杂度为Θ(n)。

算法9-10 mergeSort
输入:数据元素数组r
输出:对r[low…high]排序
代码:

public void mergeSort(Object[] r, int low, int high){
    if (low<high){
        mergeSort(r,low,(high+low)/2);
        mergeSort(r,(high+low)/2+1,high);
        merge(r,low,(high+low)/2,high);
    }
}
  • **空间效率:**在归并排序中,为了将子序列合并需要使用额外的存储空间,这个辅助存储空间的最大值不超过n,因此归并算法的空间复杂度为Θ(n)。
  • **时间效率:**归并算法是一个典型的分治算法,因此,它的时间复杂度可以用Master Method 来求解。通过对算法的分析我们写出算法时间复杂度的递推关系式:T(n) = 2T(n/2) + Θ(n)
    • 该递推式满足Master Method 的第二种情况,因此T(n) = Ο(n log n)。

基于比较的排序的对比

排序方法平均时间复杂度最坏时间复杂度空间复杂度稳定性
直接插入排序Ο(n2)Ο(n2)Ο(1)稳定
起泡排序Ο(n2)Ο(n2)Ο(1)稳定
快速排序Ο(n log n)Ο(n2)Ο(log n)不稳定
简单选择排序Ο(n2)Ο(n2)Ο(1)不稳定
堆排序Ο(n log n)Ο(n log n)Ο(1)不稳定
归并排序Ο(n log n)Ο(n log n)Ο(n)稳定

快速排序在最坏情况下的时间性能不如堆排序和归并排序。这一点可以通过对快速排序进行改进来避免,一种通过随机选择枢轴元素的随机快速排序,可以使得出现最坏情况出现的几率非常小,在实际的运用中可以认为不存在

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值