排序

9.1  排序的基本概念

    根据排序时待排序的数据元素数量的不同,使得排序过程中涉及的存储器不同,可以将
排序方法分为两类。一类是整个排序过程在内存储器中进行,称为内部排序;另一类是由于待排序元素数量太大,以至于内存储器无法容纳全部数据,排序需要借助外部存储设备才能完成,这类排序称为外部排序。

    如果在待排序的序列中存在多个具有相同关键字的元素。假设K i =K j (1≤ i≤ n,1≤ j≤ n,i≠j),若在排序之前的序列中R i 在R j 之前,经过排序后得到的序列中R i 仍然在R j 之前,则称所用的排序方法是 稳定的;否则,当相同关键字元素的前后关系在排序中发生变化,则称所用的排序方法是 不稳定的。
无论是稳定的还是不稳定的排序方法,均能完成排序的功能。在某些场合可能对排序有
稳定性的要求,此时就应当选择稳定的排序方法。例如,假设一组学生纪录已经按照学号有序,现在需要根据学生的成绩排序,当分数相同时要求学号小的学生在前,显然此时对分数进行排序就必须选择稳定的排序方法。

9.2 插入类排序

    插入排序的基本排序思想是:逐个考察每个待排序元素,将每一个新元素插入到前面已
经排好序的序列中适当的位置上,使得新序列仍然是一个有序序列。
在这一类排序中主要介绍三种排序方法:直接插入排序、折半插入排序和希尔排序。

9.2.1 直接插入排序

它的基本思想是:仅有一个元素的序列总是有序的,因此,对 n 个记录的序列,可从第二个元素开始直到第 n 个元素,逐个向有序序列中执行插入操作,从而得到 n 个元素按关键字有序的序列。

public class insertSort {
    public static void main(String[] args) {
        int[] arr = {26 , 53 , 48 , 11 , 13 , 48 , 32 , 15};
        insertSort is = new insertSort();
        is.insertSort(arr,0,arr.length-1);
        ArrayUtils.printArray(arr);
    }
    public void insertSort(int[] r, int low, int high){
        for (int i = low + 1;i <= high; i++){
            if (r[i] < r[i-1]){
                int temp = r[i];
                r[i] = r[i-1];
                int j = i-2;
                for (;j>=low&&temp<r[j];j--){
                    r[j+1] = r[j];
                }
                r[j+1] = temp;
            }
        }
    }
}
【效率分析】
空间效率:仅使用一个辅存单元。
时间效率:假设待排序的元素个数为 n,则向有序表中逐个插入记录的操作进行了 n-1
趟,每趟操作分为比较关键码和移动记录,而比较的次数和移动记录的次数取决于待排序列按关键码的初始排列。
⑴ 在最好情况下,即待排序序列已按关键字有序,每趟操作只需 1 次比较 0 次移动。
此时有:
总比较次数 = n-1 次
总移动次数 = 0 次
⑵ 在最坏情况下,即待排序序列按关键字逆序排序,这时在第 j 趟操作中,为插入元

素需要同前面的 j 个元素进行 j 次关键字比较,移动元素的次数为 j+1 次。此时有:

⑶ 平均情况下:即在第 j 趟操作中,插入记录大约需要同前面的 j/2 个元素进行关键字
比较,移动记录的次数为 j/2+1 次。此时有:
总比较次数 ≈ n^2 /4 次
总移动次数 ≈ n^2 /4 次
由此,直接插入排序的时间复杂度为O(n^2 ),并且是一个稳定的排序方法。

9.2.2 折半插入排序

直接插入排序的基本操作是向有序序列中插入一个元素,插入位置的确定是通过对有序序列中元素按关键字逐个比较得到的。

既然是在有序序列中确定插入位置,则可以不断二分有序序列来确定插入位置,即搜索插入位置的方法可以使用折半查找实现。


public class binInsertSort {
    public static void main(String[] args) {
        int[] arr = {26 , 53 , 48 , 11 , 13 , 48 , 32 , 15};
        binInsertSort bis = new binInsertSort();
        bis.binInsertSort(arr,0,arr.length-1);
        ArrayUtils.printArray(arr);
    }
    public void binInsertSort(int[] r, int low, int high){
        for (int i=low+1; i<=high; i++){
            int temp = r[i]; //保存待插入元素
            int hi = i-1; int lo = low;  //设置初始区间
            while (lo<=hi){  //折半确定插入位置
                int mid = (lo+hi)/2;
                if(temp < r[mid]) {
                    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(n^2 )。

9.2.3 希尔排序

希尔排序的基本思想是:首先将待排序的元素分为多个子序列,使得每个子序列的元素个数相对较少,对各个子序列分别进行直接插入排序,待整个待排序序列“基本有序”后,再对所有元素进行一次直接插入排序。

根据上述排序思想,下面我们给出希尔排序的排序过程:
⑴ 选择一个步长序列t 1 ,t 2 ,…,t k ,其中t i >t j (i<j),t k =1;
⑵ 按步长序列个数 k,对待排序元素序列进行 k 趟排序;
⑶ 每趟排序,根据对应的步长t i ,将待排序列分割成t i 个子序列,分别对各子序列进行直接插入排序。

当步长因子为 1 时,所有元素作为一个序列来处理,其长度为 n。
以关键字序列{ 26 , 53 , 67 , 48 , 57 , 13 , 48 , 32 , 60 , 50}为例,假设选择的步长序列为{5 , 3 , 1},则希尔排序的过程如图 9-2 所示。

因为步长序列长度为 3,因此对待排序序列一共需要进行 3 趟排序。

首先,第一趟排序中将关键字序列分成 5 个子序列{26 , 13},{53 , 48},{67 ,32},{48 , 60},{57 , 50},对它们分别进行直接插入排序,结果如图所示。

然后,进行第二趟希尔排序,此时步长为 3,则将关键字序列分成 3 个子序列{13 , 48 , 53 , 57},{48 ,50 , 67},{32 , 26 , 60},对它们进行直接插入排序后的结果如图所示。

最后,对整个序列进行一趟直接插入排序,此时得到一个关键字有序的序列,希尔排序结束。

从图 9-2 所示希尔排序的例子中可以看到,在每趟排序过程中子序列的划分并不是简单的逐段划分,而是将间隔某个步长的元素组成一个子序列。如此,在对每个子序列进行简单插入排序时,关键字较小的元素就不是一步一步向前移动,而是按步长跳跃式向前移动,从而使得在进行最后一趟步长为 1 的插入排序时,整个序列已基本有序,此时,只需要作比较少的比较和移动即可完

成排序。

public class shellSort {

    public static void main(String[] args) {
        int[] arr = {26 , 53 , 48 , 11 , 13 , 48 , 32 , 15};
        int[] delta = {5 , 3 , 1};
        shellSort ss = new shellSort();
        ss.shellSort(arr,0,arr.length-1,delta);
        ArrayUtils.printArray(arr);
    }

    public void shellSort(int[] 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(int[] r, int low, int high, int deltaK){
        for (int i=low+deltaK; i<=high; i++) {
            if (r[i] < r[i - deltaK]) { //小于时,需将 r[i] 插入有序表
                int temp = r[i];
                int j = i - deltaK;
                for (; j >= low && temp < r[j]; j = j - deltaK) {
                    r[j + deltaK] = r[j]; //记录后移  [j];
                }
                r[j + deltaK] = temp; //插入到正确位置
            }
        }
    }
}

9.3  交换类排序

9.3.1 起泡排序

起泡排序的思想非常简单。首先,将 n 个元素中的第一个和第二个进行比较,如果两个
元素的位置为逆序,则交换两个元素的位置;进而比较第二个和第三个元素关键字,如此类
推,直到比较第 n-1 个元素和第 n 个元素为止;上述过程描述了起泡排序的第一趟排序过程,
在第一趟排序过程中,我们将关键字最大的元素通过交换操作放到了具有 n 个元素的序列的
最一个位置上。然后进行第二趟排序,在第二趟排序过程中对元素序列的前 n-1 个元素进行
相同操作,其结果是将关键字次大的元素通过交换放到第 n-1 个位置上。一般来说,第 i 趟
排序是对元素序列的前 n-i+1 个元素进行排序,使得前 n-i+1 个元素中关键字最大的元素被
放置到第 n-i+1 个位置上。排序共进行 n-1 趟,即可使得元素序列按关键字有序。

public class bubbleSort {

    public static void main(String[] args) {
        int[] arr = {26 , 53 , 48 , 11 , 13 , 48 , 32 , 15};
        bubbleSort bs = new bubbleSort();
        bs.bubbleSort(arr,0,arr.length-1);
        ArrayUtils.printArray(arr);
    }
    public void bubbleSort(int[] 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 (r[j]>r[j+1])
                {
                    int temp = r[j];
                    r[j] = r[j+1];
                    r[j+1] = temp;
                }
    }//end of bubbleSort
}
【效率分析】
空间效率:仅使用一个辅存单元。

时间效率:假设待排序的元素个数为 n,则总共要进行 n-1 趟排序,对 j 个元素的子序列进行一趟起泡排序需要进行 j-1 次关键字比较。由此,起泡排序的总比较次数为

因此,起泡排序的时间复杂度为Ο(n^2 )。

9.3.2 快速排序

快速排序的基本思想是:通过一个枢轴(pivot)元素将 n 个元素的序列分为左、右两个子序列 Ll 和 Lr,其中子序列 Ll中的元素均比枢轴元素小,而子序列 Lr 中的元素均比枢轴元素大,然后对左、右子序列分别进行快速排序,在将左、右子序列排好序后,则整个序列有序,而对左右子序列的排序过程直到子序列中只包含一个元素时结束,此时左、右子序列由于只包含一个元素则自然有序。用分治法的三个步骤来描述快速排序的过程如下:
4. 划分步骤:通过枢轴元素 x 将序列一分为二, 且左子序列的元素均小于 x,右子序列的元素均大于 x;
5. 治理步骤:递归的对左、右子序列排序;
6. 组合步骤:无
从上面快速排序算法的描述中我们看到,快速排序算法的实现依赖于按照枢轴元素 x对待排序序列进行划分的过程。
对待排序序列进行划分的做法是:使用两个指针 low 和 high 分别指向待划分序列 r 的范围,取 low 所指元素为枢轴,即 pivot = r[low]。划分首先从 high 所指位置的元素起向前逐一搜索到第一个比 pivot 小的元素,并将其设置到 low 所指的位置;然后从 low 所指位置的元素起向后逐一搜索到第一个比 pivot 大的元素,并将其设置到 high 所指的位置;不断重复上述两步直到 low = high 为止,最后将 pivot 设置到 low 与 high 共同指向的位置。使用上述划分方法即可将待排序序列按枢轴元素 pivot 分成两个子序列,当然 pivot 的选择不一定必须是 r[low],而可以是 r[low..high]之间的任何数据元素。


public class quickSort {
    public static void main(String[] args) {
        int[] arr = {26 , 53 , 48 , 11 , 13 , 48 , 32 , 15};
        quickSort qs = new quickSort();
        qs.quickSort(arr,0,arr.length-1);
        ArrayUtils.printArray(arr);
    }
    public void quickSort(int[] r, int low, int high){
        if (low<high){
            int pa = partition(r,low,high);
            quickSort(r,low,pa-1);
            quickSort(r,pa+1,high);
        }
    }
    private int partition(int[] r, int low, int high){
        int pivot = r[low];  //使用 r[low]作为枢轴元素
        while (low<high){ //从两端交替向内扫描
            while(low<high&&r[high]>=pivot) high--;
            r[low] = r[high];  //将比 pivot 小的元素移向低端
            while(low<high&&r[low]<=pivot) low++;
            r[high] = r[low];  //将比 pivot 大的元素移向高端
        }
        r[low] = pivot; //设置枢轴
        return low;  //返回枢轴元素位置
    }
}

【效率分析】
时间效率:快速排序算法的运行时间依赖于划分是否平衡,即根据枢轴元素 pivot 将序
列划分为两个子序列中的元素个数,而划分是否平衡又依赖于所使用的枢轴元素。下面我们
在不同的情况下来分析快速排序的渐进时间复杂度。
快速排序的最坏情况是每次进行划分时,在所得到的两个子序列中有一个子序列为空。
此时,算法的时间复杂度T(n) = T p (n) + T(n-1),其中T p (n)是对具有n个元素的序列进行划分
所需的时间,由以上划分算法的过程可以得到T p (n) = Θ(n)。由此,T(n) =Θ(n) + T(n-1) =Θ(n 2 )。
在快速排序过程中,如果总是选择r[low]作为枢轴元素,则在待排序序列本身已经有序或逆
向有序时,快速排序的时间复杂度为Ο(n 2 ),而在有序时插入排序的时间复杂度为Ο(n)。
快速排序的最好情况是在每次划分时,都将序列一分为二,正好在序列中间将序列分成
长度相等的两个子序列。此时,算法的时间复杂度T(n) = T p (n) + 2T(n/2),由于T p (n) = Θ(n),
所以T(n) = 2T(n/2) +Θ(n),由master method知道T(n) = Θ(n log n)。
在平均情况下,快速排序的时间复杂度 T(n) = kn ㏑ n,其中 k 为某个常数,经验证明,
在所有同数量级的排序方法中,快速排序的常数因子 k 是最小的。因此就平均时间而言,快
速排序被认为是目前最好的一种内部排序方法。
快速排序的平均性能最好,但是,若待排序序列初始时已按关键字有序或基本有序,则
快速排序蜕化为起泡排序,其时间复杂度为Ο(n 2 )。为改进之,可以采取随机选择枢轴元素
pivot的方法,具体做法是,在待划分的序列中随机选择一个元素然后与r[low]交换,再将r[low]
作为枢轴元素,作如此改进之后将极大改进快速排序在序列有序或基本有序时的性能,在待
排序元素个数n较大时,其运行过程中出现最坏情况的可能性可以认为不存在。
空间效率:虽然从时间上看快速排序的效率优于前述算法,然而从空间上看,在前面讨
论的算法中都只需要一个辅助空间,而快速排序需要一个堆栈来实现递归。若每次划分都将
序列均匀分割为长度相近的两个子序列,则堆栈的最大深度为 log n,但是,在最坏的情况
下,堆栈的最大深度为 n。

9.4  选择类排序

9.4.1 简单选择排序

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


public class selectSort {
    public static void main(String[] args) {
        int[] arr = {26 , 53 , 48 , 11 , 13 , 48 , 32 , 15};
        selectSort ss = new selectSort();
        ss.selectSort(arr,0,arr.length-1);
        ArrayUtils.printArray(arr);
    }
    public void selectSort(int[] r, int low, int high){
        for (int k=low; k<high-1; k++){  //作 n-1 趟选取
            int min = k;
            for (int i=min+1; i<=high; i++){  //选择关键字最小的元素
                if (r[i] < r[min]) min = i;
            }
            if (k!=min){
                int temp = r[k]; //关键字最小的元素与元素 r[k]交换
                r[k] = r[min];
                r[min] = temp;
            }//end of if
        }//end of for(int k=0…
    }//end of selectSort
}

【效率分析】
空间效率:显然简单选择排序只需要一个辅助空间。
时间效率:在简单选择排序中,所需移动元素的次数较少,在待排序序列已经有序的情
况下,简单选择排序不需要移动元素,在最坏的情况下,即待排序序列本身是逆序时,则移
动元素的次数为 3(n-1)。然而无论简单选择排序过程中移动元素的次数是多少,在任何情况
下,简单选择排序都需要进行n(n-1)/2 次比较操作,因此简单选择排序的时间复杂度为Ο(n 2 )。
算法改进思想:从上述效率分析中可以看出,简单选择排序的主要操作是元素间的比较
操作,因此改进简单选择排序应从减少元素比较次数出发。在简单选择排序中,首先从 n
个元素的序列中选择关键字最小的元素需要 n-1 次比较,在 n-1 个元素中选择关键字最小的
元素需要 n-2 次比较……,在此过程中每次选择关键字最小的元素都没有利用以前比较操作
得到的结果。欲降低比较操作的次数,则需要把以前比较的结果记录下来,由此得到一种改
进的选择类排序算法,即树型选择排序。

9.4.2 树型选择排序

树型选择排序也称为锦标赛排序。其基本思想是:先把待排序的n个元素两两进行比较,
取出较小者,若轮空则直接进入下一轮比较;然后在⎡n/2⎤个较小者中,采用同样的方法进行
比较,再选出较小者;如此反复,直到选出关键字最小的元素为止。这个过程可以使用一颗
具有n个结点的完全二叉树来表示,最终选出的关键字最小的元素就是这棵二叉树的根结点。
例如图 9-6(a)给出了对 8 个元素的关键字{ 26 , 53 , 48 , 11 , 13 , 48 , 32 , 15}选出最小关键
字元素的过程。

在输出关键字最小的元素后,为选 出次小关键字,可以将最小关键字元素所对应的叶子结点的关键字设置为∞,然后从该叶子结点起逆行向上,将所经过的结点与其兄弟进行比较,修改从该叶子结点到根结点上各结点的值,则根结点的值即为次小关键字。例如,在图 9-6(a)的基础上输出最小关键字 11 后,输出次小关键字 13 的过程如图 9-6(b)所示。在图 9-6(b)的基础上输出最小关键字 13 后,输出次小关键字 15 的过程如图 9-6(c)所示。重复上述过程,直到所有元素全部输出为止,则得到按关键字有序的序列。
【效率分析】
时间效率:在树型选择排序过程中为找到关键字最小的元素一共进行了 n-1 次比较,此后每次找出一个关键字所需要的比较次数等于完全二叉树的高度 h,而具有 n 个叶子结点的完全二叉树其高度为⎡log n⎤,由此可知除最小关键字外,每选择一个次小关键字需要进行⎡log n⎤次比较,因此树型选择排序的时间复杂度 T(n) = (n-1) ⎡log n⎤ + (n-1) = Ο(n log n)。
空间效率:与简单选择排序相比,虽然树型选择排序减小的时间复杂度,却使用了更多的辅助空间,在树型选择排序中共使用了 n-1 个而外的存储空间存放以前的比较结果。
算法改进思想:树型选择排序的缺点是使用了较多的辅助空间,以及和∞进行多余比较,为弥补树型选择排序的这些缺点,J.W.J.Williams 在 1964 年提出了进一步的改进方法,即堆排序。

9.4.3 堆排序

 

9.5 归并排序

    归并排序的基本思想是基于合并操作,即合并两个已经有序的序列是容易的,不论这两
个序列是顺序存储还是链式存储,合并操作都可以在 Ο(m+n)时间内完成(假设两个有序表
的长度分别为 m 和 n)。为此,由分治法的一般设计步骤得到归并排序的过程为:
1. 划分:将待排序的序列划分为大小相等(或大致相等)的两个子序列;
2. 治理:当子序列的规模大于 1 时,递归排序子序列,如果子序列规模为 1 则成为有序序列;
3. 组合:将两个有序的子序列合并为一个有序序列。

图 9-10 显示了归并算法的执行过程。假设待排序序列为{4, 8, 9, 5, 2, 1, 4, 6},如图所示,
归并排序导致了一系列递归的调用,而这一系列调用过程可以由一个二叉树来表示。树中每
个结点由两个序列组成,上端为该结点所表示的递归调用的输入,而下端为相应递归调用的
输出。树中的边用表示递归调用方向的两条边取代,边上的序号表示各个递归调用发生的次
序。

归并排序中一个核心的操作是将一个序列中前后两个相邻的子序列合并为一个有序序列。

/**
 * 输入:数据元素数组 a,a 待合并的两个有序区间[p..q]以及[q+1..r]
 * 输出:将两个有序区间合并为一个有序区间
 */
public class merge {
    public static void main(String[] args) {
        int[] arr = {26 , 53 , 48 , 11 , 13 , 48 , 32 , 15};
        merge m = new merge();
        m.merge(arr,0,arr.length/2,arr.length-1);
        ArrayUtils.printArray(arr);
    }
    private void merge(int[] a, int p, int q, int r) {
        int[] b = new int[r - p + 1];
        int s = p;
        int t = q + 1;
        int k = 0;
        while (s <= q && t <= r) {
            if (a[s] < a[t]) {
                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];
        }
    }
}

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

最后从数组 b 移回 a 也只需要 n 次移动操作即可,因此,算法 merge 的时间复杂度为Θ(n)。

在算法 merge 的基础上,即可实现归并排序的算法了。

/**
 * 输入:数据元素数组 a,a 待合并的两个有序区间[p..q]以及[q+1..r]
 * 输出:将两个有序区间合并为一个有序区间
 */
public class merge {
    public static void main(String[] args) {
        int[] arr = {26 , 53 , 48 , 11 , 13 , 48 , 32 , 15};
        merge m = new merge();
        m.mergeSort(arr,0,arr.length-1);
        ArrayUtils.printArray(arr);
    }
    public void mergeSort(int[] 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);
        }
    }
    private void merge(int[] a, int p, int q, int r) {
        int[] b = new int[r - p + 1];
        int s = p;
        int t = q + 1;
        int k = 0;
        while (s <= q && t <= r) {
            if (a[s] < a[t]) {
                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)。
时间效率:归并算法是一个典型的分治算法,因此,它的时间复杂度可以用 Master Method 来求解。通过对算法的分析我们写出算法时间复杂度的递推关系式:
T(n) = 2T(n/2) + Θ(n)
该递推式满足 Master Method 的第二种情况,因此 T(n) = Ο(n log n)。
与快速排序和对排序相比,归并排序的优点是它是一种稳定的排序方法。上述算法实现是归并排序的递归形式,这种形式简洁易懂,然而实用性较差,归并排序还可以按照自底向上的方式给出其他实现。

9.6  基于比较的排序的对比

上面介绍的插入排序、交换排序、选择排序、归并排序等排序方法,都有一个共同的特点,那就是它们都是通过比较元素的大小来确定元素之间的相对位置的,即上述排序方法都是基于比较的排序方法。

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

   从时间性能上看,快速排序是所有排序算法中实际性能最好的,然而快速排序在最坏情
况下的时间性能不如堆排序和归并排序。这一点可以通过对快速排序进行改进来避免,一种
通过随机选择枢轴元素的随机快速排序,可以使得出现最坏情况出现的几率非常小,在实际
的运用中可以认为不存在。在堆排序和归并排序的比较中,当 n 较大时,归并排序所需时间
较少,然而它需要较多的辅助存储空间。

    从方法稳定性上来看,大多数时间复杂度为Ο(n 2 )的排序均是稳定的排序方法,除简单
选择排序之外。而多数时间性能较好的排序方法,例如快速排序、堆排序、希尔排序都是不
稳定的。一般来说,排序过程中的比较是在相邻的两个元素之间进行的排序方法是稳定的。
并且,排序方法的稳定性是由方法本身决定的,对于不稳定的排序方法而言,不管其描述形
式如何,总能找到一种不稳定的实例。
    综上所述,上面讨论的所有排序方法中,没有哪一个是绝对最优的,在实际的使用过程
中,应当根据不同情况选择适当的排序方法。

任何一个基于比较操作的排序方法,在最坏情况下所需要进
行的比较次数至少为 n log n 次,即算法的时间复杂度下界为 Ω(n long n)。

9.7 在线性时间内排序

通过比较确定两个元素之间相对位置的比较排序算法的时间复杂性下界为 Ο(n log n),然而当排序序列满足某种特定条件时,我们可以突破这个时间下界,在线性时间内就可以完成排序。

9.7.1 计数排序

计数排序算法的基本思想是对于给定的输入序列中的每一个元素 x,确定该序列中值小
于 x 的元素的个数。一旦确定了这一信息,就可以将 x 直接存放到最终的输出序列的正确位
置上。例如,如果输入序列中只有 9 个元素的值小于 x 的值,则 x 可以直接存放在输出序列
的第 10 个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些相同的元素放
在输出序列的同一个位置上,因此还需对上述方案作适当的修改。

假设输入的序列L的长度为n,L={L 0 , L 1 , … , L n-1 };线性表的元素属于有限偏序集S,
|S|=k且k=O(n),S={S 0 , S 1 , … S k-1 };则计数排序算法可以描述如下:
1. 扫描整个集合S,对每一个S i ∈S,找到在序列L中小于等于S i 的元素的个数C(S i );
2. 扫描整个序列L,对L中的每一个元素L i ,将L i 放在输出线性表的第C(L i )个位置
上,并将C(L i )减 1。
在实现计数排序的过程中,我们需要两个辅助数组:B[0..n-1]存放排序结果,C[0..k-1]
作为临时数组。

9.7.2 基数排序

基数排序是一种“低位优先”的排序方法,它的基本思想是通过反复的对子关键字排序
来完成排序。假设元素r[i]的关键字为key i ,key i 是由d位十进制数组成,即key i =ki 1 ki 2 … ki d ,
则每一位可以视为一个子关键字,其中ki 1 是最高位,ki d 是最低位,每一位的值都在 0 到 9
的范围内,此时基数rd =10。如果ki 1 是由d个英文字母组成,即key i =ki 1 ki 2 … ki d ,其中'a'≤ ki j
≤'z'(1≤ j ≤d),则基数rd = 26。
排序时先按最低位的值对元素进行初步排序,在此基础上再按次低位的值进行进一步排
序。依次类推,由低位到高位,每一趟都是在前一趟的基础上,根据关键字的某一位对所有
元素进行排序,直到最高位,这样就完成了计数排序的全过程。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值