深入理解排序算法(一):初级排序算法

[本系列博文会对常见的排序算法进行分析与总结,并会在最后提供几道相关的一线互联网企业面试/笔试题来巩固所学及帮助我们查漏补缺。项目地址:https://github.com/absfree/Algo。由于个人水平有限,叙述中难免存在不清晰准确的地方,希望大家可以指正,谢谢大家:)]

一、概述

我们在日常开发中经常需要对一组数据对象进行排序,这里的数据对象不仅包括数字,还可能是字符串等抽象数据类型(Abstract Data Type)。由于排序是很多其他操作(比如二分查找)能够高效进行的基础,因此我们有必要掌握好常见的排序算法,本系列文章会分析几种最常用的排序算法,并进一步探索排序的本质,从而能够更加全面透彻的理解各种排序算法。本系列博文会用Java来描述各种排序算法的实现,由于我们的侧重点在与分析各项算法的原理及其一般实现,因此我们假定待比较的数据对象均为int类型(然而在实际应用中我们应该假定它们为Comparable类型)。若未加特殊说明,我们所介绍的排序算法都会按照升序排列。

二、初级排序算法

本篇博文我们介绍四种初级排序算法,分别是冒泡排序、选择排序、插入排序和希尔排序。之所以称之为初级,是因为相比于归并排序、快速排序等高级排序算法,它们的实现较简单,不过(对于大规模数据集的)性能也较差些。

1. 冒泡排序

假如我们现在按身高升序排队,一种排队的方法是:从第一名开始,让两人相互比身高,若前者高则交换位置,更高的那个在与剩下的人比,这样一趟下来之后最高的人就站到了队尾。接着重复以上过程,直到最矮的人站在了队列首部。我们把队头看作水底,队尾看作水面,那么第一趟比较下来,最高的人就像泡泡一样从水底”冒“到水面,第二趟比较则是第二高的人……排队的过程即为对数据对象进行排序的过程(这里我们排序的”指标“是身高),上述过程即描述了冒泡排序的思想。从以上过程我们可以看到,若对n个人进行排队,我们需要n-1趟比较,而且第k趟比较需要进行n-k次比较。通过这些信息,我们能够很容易的算出冒泡排序的复杂的。首先,排序算法通常都以数据对象的两两比较作为”关键操作“,这里我们可以得出,冒泡排序需要进行的比较次数为: (n-1) + (n-2) + ... + 1 = n*(n-1) / 2,因此冒泡排序的时间复杂度为O(n^2)。

理解了冒泡排序的原理,就不难实现它了,具体实现代码如下:

public class Bubble {
    public static void sort(int[] a) {
        int N = a.length;
        for (int i = 0; i < N; i++)  {
            for (int j = 0; j < N - i - 1; j++) {
                if (a[j] > a[j+1]) {
                    exchange(a, j, j+1);
                }
            }
        }
    }

    private static void exchange(int a[], int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void main(String[] args) {
        int N = 20;
        int[] a = new int[N];
        for (int i = 0; i < N; i++) {
            a[i] = StdRandom.uniform(0, 1000);
        }
        sort(a);
        for (Integer i : a) {
            StdOut.print(i + " ");
        }
    }
}

关于冒泡排序有一点需要注意的是,在最好情况下(即输入数组已经完全有序),冒泡排序的时间复杂度能够提升到O(N)。我们只需增加一个boolean型变量isOrdered,在第一轮排序中一旦a[j] > a[j+1],就把isOrdered设为false,否则isOrdered设为true,然后我们在每趟排序前检查isOrdered,一旦发现它为false,即认为排序已完成。

2. 选择排序

回到上面我们提到的排队问题,除了上面提到的方法,还有这样一种排队的方法,让目前队头的人依次与其后的每个人进行比较,比较后较矮的那个人继续与后面的人进行比较,这样第一趟比较下来,就能够找到最矮的人, 然后把这个最矮的人和当前队头的人交换一下位置。然后第二趟比较,让第二名依次与后面比较,可以找到第二矮的人,然后让第二矮的人和当前队列第二名交换位置,依此类推,一共进行n-1趟比较后,就能完成整个排队过程。根据上述描述,我们可以知道,第k趟比较需要进行的数组元素的两两比较的次数为n-k次,所以共需要的比较次数为n*(n-1) / 2,因此选择排序算法的时间复杂度与冒泡排序一样,也为O(n^2)。选择排序的Java描述如下:

public class Selection {
     public static void sort(int[] a) {
         int N = a.length;
         for (int i = 0; i < N - 1; i++) {
             int min = i;
             for (int j = i + 1; j < N; j++) {
                 if (a[j] < a[min]) {
                     min = j;
                 }
             }
             exchange(a, i, min);
         }
     }

    private static void exchange(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

 }

3. 插入排序

回想下我们平时打扑克抓牌的过程,通常我们用右手抓牌,每抓一张牌,就放到左手上,抓下一张牌后,会把这张牌依次与左手上的牌比较,并把它插入到一个合适的位置(通常按照牌面大小)。上述的过程即为插入排序的过程,假设待排序数组为a,我们从a[1]开始,让a[1]与a[0]比较,若a[1]较小,则让a[1]和a[0]交换位置,此时a[0]和a[1]就相当于已经放入左手中的牌。然后我们再让a[2]与a[1]、a[0]比较,并为它找到一个合适的位置,以此类推,直到为数组的最后一个元素也找到了合适的位置。

理解了插入排序的思想后,我们便能够得到它的时间复杂度。对于n个元素,一共需要进行n-1轮比较,而第k轮比较需要进行k次数组元素的两两比较,因此共需要进行的比较次数为:1 + 2 + ... + (n-1),所以插入排序的时间复杂度同冒泡排序一样,也为O(n^2)。插入排序的Java描述如下:

public class Insertion {
    public static void sort(int[] a) {
        int N = a.length;
        int i, j;
        for (i = 1; i < N; i++) {
            for (j = i - 1; j >= 0 && a[i] < a[j]; j--) {

            }
            //这里跳出内层循环,a[i]应被插入到a[j]后
            int tmp = a[i];
            for (int k = i; k > j + 1; k--) {
                a[k] = a[k-1];
            }
            a[j+1] = tmp;
        }
    }

}

我们来简单地解释下以上代码。以抓牌过程来举例,i为刚抓的牌的索引,i-1即为我们刚排好的牌中的最后一张的索引,j为左手中当前正与我们刚抓的牌进行比较的牌的索引。在内层循环中,我们从左手已排好牌中的最后一张开始,若发现刚抓的牌比当前牌的牌面大,就再与前一张比较(j--),直到刚抓的牌大于等于当前牌的牌面,就会跳出内层循环,这时我们把a[i]插入到a[j]后,就把刚抓的牌插入到已排好牌中的合适的位置了。重复以上过程就能完成待排序数组的排序。

关于插入排序我们需要注意的是,在平均情况下以及最坏情况下,它的时间复杂度均为O(n^2),而在最好情况下(输入数组完全有序),插入排序的时间复杂度能够提升至O(N)。实际上,排序的本质就是消除逆序对,所谓逆序对,就是不符合我们所要求的排序顺序的两个数。比如说[1,3,4,2]为待排序数组,那么它的逆序数为2——(3,2)和(4,2)都是降序的,不符合我们升序的要求。插入排序对于部分有序的数组的排序尤为有效,所谓部分有序,指的是待排序数组的逆序数小于数组尺寸的某个倍数。若我们待排序数组完全有序时,每一轮排序都只需比较一次,就能找到待排序元素在已排序数组中的合适的位置,而部分有序时,比较的次数也能控制在数组尺寸的常数倍之内。因此,插入排序对于部分有序的数组十分高效,也很适合小规模的数组。

4. 希尔排序

希尔排序是对插入排序的一种改进,它的核心思想是将待排序数组中任意间隔为h的元素都变为有序的,这样的数组叫做h有序数组。比如数组[5, 3, 2, 8, 6, 4, 7, 9, 5], 我们可以看到a[0]、a[3]、a[6]是有序的,a[1]、a[4]、a[7]是有序的,a[2]、a[5]、a[8]是有序的,因此这个数组是一个h有序数组(h=3)。根据h有序数组的定:义,我们可以知道,当h=1时,相应的h有序数组就是一个已经排序完毕的数组了。希尔排序的大致过程如下:把待排序数组分割为若干子序列(一个子序列中的元素在原数组中间隔为h,即中间隔了h-1个元素),然后对每个子序列分别进行插入排序。然后再逐渐减小h,重复以上过程,直至h变为足够小时,再对整体进行一次插入排序。由于h足够小时,待排序数组的逆序数已经很小,所以再进行一次希尔排序是很快的。希尔排序通常要比插入排序更加高效。

实现希尔排序时,我们需要选取一个h的取值序列,这里我们直接采用算法(第4版) (豆瓣)一书中提供的h取值序列(1,4,13,40,121, ...)。即h =  3 * k + 1,其中k为[0, N/3)区间内的整数。希尔排序的Java描述如下:

public class Shell {
    public static void sort(int[] a) {
        int N = a.length;
        int h = 1;
        while (h < N / 3) {
            h = 3 * h + 1; //h的取值序列为1, 4, 13, 40, ...
        }
        while (h >= 1) {
            int n, i ,j, k;
            //分割后,产生n个子序列
            for (n = 0; n < h; n++) {
                //分别对每个子序列进行插入排序
                for (i = n + h; i < N; i += h) {
                    for (j = i - h; j >= 0 && a[i] < a[j]; j -= h) {

                    }
                    int tmp = a[i];
                    for (k = i; k > j + h; k -= h) {
                        a[k] = a[k-h];
                    }
                    a[j+h] = tmp;
                }
            }
            h = h / 3;
        }
    }

}

实际上,h的取值序列的选取会影响到希尔排序的性能,不过以上我们选取的h值序列在通常情况下性能与复杂的取值序列相接近,但是在最坏情况下的性能要差一些。分析希尔排序的复杂度不是一件容易的事,这里我们引用《算法》一书中关于希尔排序复杂度的结论:

使用递增序列1, 4, 13, 40, 121, 364, ...的希尔排序所需的比较次数不会超过数组尺寸的若干倍乘以递增序列的长度。

也就是说,在通常情况下,希尔排序的复杂度要比O(n^2)好得多。实际上,最坏情况下希尔排序所需要的比较次数与O(n^1.5)成正比,在实际使用中,希尔排序要比插入排序和选择排序、冒泡排序快得多。而且尽管待排序数组很大,希尔排序也不会比快速排序等高级算法慢很多。因此当需要解决排序问题而用没有现成系统排序函数可用时,可以优先考虑希尔排序,当希尔排序确实满足不了对性能的要求时,在考虑使用快速排序等算法。

到这里,我们要介绍的基本排序算法就介绍完了,再介绍快速排序、归并排序、堆排序等高级排序算法前,我们先来简单地介绍下如何比较各种排序算法的实际性能,这也能够帮助我们直观的看到希尔排序相比与插入排序等的性能优势。

三、比较不同排序算法的性能

尽管插入排序和选择排序的复杂度都为O(n^2),但是它们所包含的常数系数是不同的,因而这两种算法的实际执行时间之比应该是一个常数,下面我们来设计实验来测试下以上我们介绍的几种基本排序算法的实际执行性能。相关代码如下:

public class SortCompare {
    public static double time(String alg, int[] a) {
        long startTime = System.currentTimeMillis();
        if (alg.equals("Insertion")) {
            Insertion.sort(a);
        } else if (alg.equals("Selection")) {
            Selection.sort(a);
        } else if (alg.equals("Bubble")) {
            Bubble.sort(a);
        } else if (alg.equals("Shell")) {
            Shell.sort(a);
        }
        long endTime = System.currentTimeMillis();
        return (double) (endTime - startTime) / 1000.0;
    }

    public static double timeRandomInput(String alg, int N, int T) {
        //使用alg指定的排序算法将长度为N的数组排序,共排序T次,并计算总时间
        double total = 0.0;
        int[] a = new int[N];
        for (int t = 0; t < T; t++) {
            for (int i = 0; i < N; i++) {
                a[i] = StdRandom.uniform(10 * N);
            }
            total += time(alg, a);
        }
        return total;
    }

    public static void main(String[] args) {
        String alg1 = args[0];
        String alg2 = args[1];
        int N = Integer.parseInt(args[2]);
        int T = Integer.parseInt(args[3]);
        double t1 = timeRandomInput(alg1, N, T);
        double t2 = timeRandomInput(alg2, N, T);
        StdOut.printf("For %d random ints\n %s is", N, alg1);
        StdOut.printf(" %.1f times faster than %s", t2/t1, alg2);
    }
}

我们来对1000个数进行排序,来比较下以上介绍的算法的性能。我这里得到的输出结果如下:

For 1000 random ints
 Shell is 4.9 times faster than Insertion

For 1000 random ints
 Shell is 7.6 times faster than Selection

For 1000 random ints
  Shell is 11.7 times faster than Bubble

我们可以直观的看到,希尔排序要比其他三种排序都快,而插入排序要比选择排序、冒泡排序快,冒泡排序在实际执行性能最差。

基本排序算法对于中小规模的数据集的排序在一般情况下足够用了,但是对于大规模数据集的排序,我们还是很有必要使用一些较高级的排序算法,在接下来的博文中我们会详细介绍它们。

四、参考资料

算法(第4版) (豆瓣)

时间: 2024-12-29 07:07:29

深入理解排序算法(一):初级排序算法的相关文章

[读书笔记]算法(Sedgewick著)·第二章.初级排序算法

本章开始学习排序算法 1.初级排序算法 先从选择排序和插入排序这两个简单的算法开始学习排序算法.选择排序就是依次找到当前数组中最小的元素,将其和第一个元素交换位置,直到整个数组有序. 1 public static void sort(Comparable a[]){ 2 int N = a.length; 3 for(int i = 0; i < N; i ++){ 4 int min = i; //最小元素索引 5 for(int j = i + 1; j < N; j++){ 6 if(

算法手记(5)初级排序算法

排序是将一组对象按照一定的规则重新排列的过程.即使目前完全可以使用标准库中的排序函数,学习排序算法仍然有着较大意义:   排序算法的学习可以帮助你全面了解比较算法性能的方法: 类似的技术上能有效解决其他类型的问题: 排序算法通常是我们解决问题的第一步: 更重要的是这些算法都很经典,优雅和高效. 排序在商业数据处理分析和现代科学中占有重要的地位,其中快速排序算法被誉为20世纪科学和工程领域十大算法之一.今天我们要看的就是相对简单但很经典的初级排序算法,包括选择排序,插入排序及Shell排序. 准备

初级排序算法1-定义排序规则

初级排序算法-定义排序规则 排序就是将一组对象按照某种逻辑序列重新排列的过程. Table of contents 介绍 为什么学它 排序算法类的模板 验证 性能评估 介绍 现在计算机的广泛使用使得数据无处不在,而整理数据的第一步通常就是进行排序 所有的计算机都实现了各种排序算法以供系统和用户使用 为什么学它 即使你只是使用标准库中的排序算法,学习排序算法仍然有三大实际意义 对排序算法的分析将有助于你全面理解比较算法性能的方法 类似的技术也能有效解决其他类型的问题 排序算法常常是我们使用算法解决

初级排序算法之选择排序

初级排序算法 本质是对要排序的数组进行嵌套循环,内层循环负责局部的排序,外层循环负责剩余的无序元素的递减.所以你只要理解嵌套循环和比较大小就能很快的掌握初级排序算法. 选择排序 一个无序的数组 a = [0, 4, 6, 3, 8, 2, 3, 9], 你也可以把a的元素想象成任何现实中可比较的具体物体.例如,有10根长短不一的木条,我们如何对它们进行排序?一个最直接的思想,先拿出最短的放到最前面,在剩余的木条中再拿出最短的放在第二位...直到最后一根木条.从中我们可以看出,1. 我们需要再一次

算法(第4版)-2.1 初级排序算法

2.1.1 游戏规则 1. 排序成本模型:在研究排序算法时,我们需要计算比较和交换的数量.对于不交换元素的算法,我们会计算访问数组的次数. 2. · 原地排序算法:除了函数调用所需的栈和固定数目的实例变量之外无需额外内存的原地排序算法: · 其他排序算法:需要额外内存空间来储存另一份数组副本. 2.2.2 选择排序 public class Selection { public static void sort(Comparable[] a) { // 将a[]按升序排列 int N = a.l

“《算法》第4版第2章‘排序’”:初级排序算法(选择、冒泡、插入、希尔)

<算法>第4版作者是Robert Sedgewick 和 Kevin Wayne. 1. 选择排序 选择排序可以说是最简单的排序方法.首先,找到数组中最小的那个元素:其次,将它与数组的第一个元素交换位置(如果第一个元素就是最小元素,那么它就和自己交换):再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置.如此往复,直到将整个数组排序. 该书中提出一个命题:对于长度为N的数组,选择排序需要大约N2/2次比较和N次交换.  程序如下: 1 void SelectionSort::s

三大初级排序算法

1.冒泡排序 冒泡排序是最慢的排序算法.在实际运用中它是效率最低的算法.它通过一趟又一趟地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升.它是O(n^2)的算法. 2.插入排序 插入排序通过把序列中的值插入一个已经排序好的序列中,直到该序列的结束. 3.shell排序(希尔排序) Shell排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入排序,以减少数据交换和移动的次数.平均效率是O(nlogn). 下面给出两个关于shell排序的链接: 算法系列15天速

算法—3.希尔排序

对于大规模乱序数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移动到另一端.例如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要N-1次移动.希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序. 1.基本思想 希尔排序的思想是使数组中任意间隔为h的元素都是有序的.这样的数组被称为h有序数组.换句话说,一个h有序数组就是h个互相独立的有序数组编织在一起组成的一个数组(见下图).在进行排

排序算法之希尔排序

文章转载自http://www.cnblogs.com/chengxiao/ 希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法.希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一.本文会以图解的方式详细介绍希尔排序的基本思想及其代码实现. 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序:随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组