【算法拾遗(java描述)】--- 交换排序(冒泡、快排)

交换排序基本思想:两两比较待排序的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。

应用此排序思想的有冒牌排序快速排序,其中冒泡排序属于简单算法,快速排序属于改进算法。

冒泡排序

基本思想

两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。

具体算法

前提条件:序列 s = {s0,s1,……sn-1}是n个可排序元素的序列。

  1. 令j从n-1递减到1,重复步骤2~4。
  2. 令i从1递增到j,重复步骤3。
  3. 如果元素si-1和si成反序,交换它们。
  4. 结束标记,序列{s0,s1,……sj}被排序且sj最大。

《【算法拾遗(java描述)】--- 交换排序(冒泡、快排)》

java程序

/************************* * * 冒泡排序初级版 * *************************/
public class BubbleSort {

    private void bubbleSort(int[] datas) {
        if (datas == null || datas.length < 2)
            return;
        boolean isChanged = true;// 标记是否进行了交换
        for (int j = datas.length; j > 1 && isChanged; j--) {
            isChanged = false;
            for (int i = 0; i < j - 1; i++) {
                if (datas[i + 1] < datas[i]) {
                    int temp = datas[i + 1];
                    datas[i + 1] = datas[i];
                    datas[i] = temp;
                    isChanged = true;
                }
            }
        }
    }

    /** * 测试 * @param args */
    public static void main(String[] args) {
        int[] datas = new int[] { 3, 6, 4, 2, 11, 15 };
        System.out.println("********排序前********");
        for (int i = 0; i < datas.length; i++) {
            System.out.print(datas[i]+",");
        }
        BubbleSort bubbleSort = new BubbleSort();
        bubbleSort.bubbleSort(datas);
        System.out.println("\n********排序后********");
        for (int i = 0; i < datas.length; i++) {
            System.out.print(datas[i]+",");
        }
    }

}

可以看到程序中增加了一个标记变量isChanged,目的是为了对传统的冒泡排序进行优化,如果不添加此标记位,那么在待排序列本身有序的情况下,传统算法仍会执行大量的循环比较。这会严重影响到算法的性能。

性能分析

上一篇文章已经说到,要想分析一个算法的性能,要从最好情况最坏情况平均情况三个方面加以分析。

  • 最好情况(即数据已经排好序),会执行n-1次的比较,没有数据交换,时间复杂度为O(n)。
  • 最坏情况(数据反序存放),这种情况下需要进行n-1躺排序,每趟排序要进行n-i次关键字的比较,且每次比较都必须移动记录三次来达到交换记录位置。这种情况下比较和移动次数均达到最大值,时间复杂度为O(n2)。
  • 平均情况(数据随机顺序存放),时间复杂度为O(n2)。
  • 冒泡排序属于内排序,且为稳定排序。

算法改进

其实上面程序中添加isChanged标记位已经属于一项有效的改进了。但是我们还可以对冒泡排序做如下改进:

  1. 记住最后一次交换发生位置lastExchange的冒泡排序

在每次扫描中,记住最后一次交换发生位置lastExchange(该位置之前的相邻记录均有序)。下一趟排序开始时,R[1……lastExchange-1]是有序区,R[lastExchange……n]是无序区。这样,一趟排序可能使当前有序区扩充多个纪录,从而减少排序的躺数。

快速排序

基本思想

实际上,快速排序就是采用了分治的思想,关于分治法,其基本思想是将原问题分解为若干个规模更小但结构与原问题相似的子问题,递归的解这些子问题。然后将这些子问题的解组合为原问题的解。

具体算法

设当前待排序的的无序区为R[low……high],利用分治法可将快速排序的基本思想描述为:

  1. 分解。在R[low……high]中任选一个记录作为基准,以此基准将当前无序区划分为左、右两个较小的子区间R[low……pivotpos-1]和R[pivotpos+1……high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot.key,右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置(pivotpos)上,它无需参加后续的排序。因此,划分的关键是要求出基准记录所在的位置pivotpos,划分的结果可以简单的表示为(注意pivot = R[pivotpos]):
    R[low……pivotpos-1].keys <= R[pivotpos].key <= R[pivotpos+1……high].key(low <= pivotpos <= high)

  2. 求解。通过递归调用快速排序对左、右子区间R[low……pivotpos-1]和R[pivotpos+1……high]排序。

  3. 组合。因为当“求解”步骤中的两个递归调用结束时,其左、右两个子区间已有序,所以由上面的不等式立即知道整个数组R已有序。

《【算法拾遗(java描述)】--- 交换排序(冒泡、快排)》

java程序

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

    private void quickSort(int[] datas, int start, int end) {
        if (datas == null || datas.length < 2 || start > end)
            return;
        int i = start;
        int j = end;
        int pivot = datas[start];// 枢纽变量
        int temp;// 临时变量

        while (i < j) {
            // 从右向左寻找比枢纽变量小的
            while (i < j && datas[j] >= pivot)
                j--;
            // 从左向右寻找比枢纽变量大的
            while (i < j && datas[i] <= pivot)
                i++;
            if (i < j) {
                // 交换变量值
                temp = datas[i];
                datas[i] = datas[j];
                datas[j] = temp;
            }
        }
        // 将枢纽变量调整到正确位置
        temp = datas[j];
        datas[j] = pivot;
        datas[start] = temp;

        quickSort(datas, start, j - 1);// 对左子区间进行递归排序
        quickSort(datas, j + 1, end);// 对右子区间进行递归排序
    }

    public static void main(String[] args) {
        int[] datas = new int[] { 3, 6, 4, 2, 11, 15 };
        System.out.println("********排序前********");
        for (int i = 0; i < datas.length; i++) {
            System.out.print(datas[i] + ",");
        }

        QuikSort quikSort = new QuikSort();
        quikSort.quickSort(datas, 0, datas.length - 1);

        System.out.println("\n********排序后********");
        for (int i = 0; i < datas.length; i++) {
            System.out.print(datas[i] + ",");
        }
    }

}

性能分析

快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。

  • 最坏时间复杂度

最坏的情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目仅仅比划分前的无序区中的记录个数减少一个。

因此,快速排序必须做n-1次划分,第i次划分开始时区间长度为n-i+1,所需的比较次数为n-i(1<=i<=n-1),故总的比较次数达到最大值。此时的时间复杂度为O(n2)。

  • 最好时间复杂度

在最好情况下,每次划分所取的基准都是当前无序区的“中值”记录,每次划分的结果是基准的左、右两个无序子区间的长度大致相等。此时时间复杂度为O(nlogn)

  • 平均时间复杂度

尽管快速排序的最坏时间为O(n2),但就平均性能而言,它是基于关键字比较的内部排序算法中的速度最快者。平均时间复杂度为O(nlogn)。

  • 空间复杂度

快速排序在系统内需要一个栈来实现递归。若每次划分的较为均匀,则其递归树的高度为O(logn),股低轨后需要栈空间O(logn)。最坏情况下,递归树的高度为O(n),所需空间为O(n)。

  • 稳定性

快速排序是非稳定排序。

算法改进

基准关键字的选取

在当前无序区中选取划分的基准关键字是决定算法性能的关键。

  • “三者取中”原则

即在当前区间里,将该区间首、尾和中间位置上的关键字进行比较,取三者的中值所对应的记录作为基准,在划分开始前将该基准记录和该区间的第一个记录进行交换,此后的划分过程和上面给出的算法一样。

  • 取位于low和high之间的随机数k(low<=k<=high),用R[k]作为基准。

选取基准最好的方法是用一个随机函数产生一个位于low和high之间的随机数k(low<=k<=high),用R[k]作为基准,这相当于强迫R[low……high]中的记录是随机分布的。用此方法得到的快速排序一般称为随机的快速排序。

参考资料:《数据结构与算法分析——java语言描述》、《大话数据结构》

点赞