排序算法之聊聊你所不知道的冒泡排序

223

什么是冒泡排序

冒泡排序是一种基础的交换排序,其思想是,把相邻的元素两两比较,当一个元素大于右侧相邻的元素时,交换它们的位置:当一个元素小于或等于右侧的元素时,位置不变。

冒泡排序是一种稳定排序,值相等的元素并不会打乱原有的顺序,由于该排序算法的每一轮都要遍历元素,总共遍历(元素数量 - 1)轮,所以平均时间复杂度是O(n^2)。

工欲善其事,必先利其器

在讲述冒泡排序之前,我们先来搭建一个简单的排序算法编写的环境,这个环境可以帮助我们更加方便地编写和测试排序算法。

SortUtils

一个排序算法的工具类,其中包含了排序算法编写和测试的一些基本操作。

public class SortUtils {

    /**
     * 随机生成一个整形数组
     *
     * @param size   数组大小
     * @param rangeL 数组左边范围(包含)
     * @param rangeR 数组右边范围(包含)
     * @return 一个整形数组
     */
    public static int[] generateRandomArray(int size, int rangeL, int rangeR) {
        int[] arr = new int[size];
        for (int i = 0; i < size; i++) {
            arr[i] = (int) (Math.random() * (rangeR - rangeL + 1)) + rangeL;
        }
        return arr;
    }

    /**
     * 生成一个近乎有序的整形数组
     *
     * @param size      数组大小
     * @param swapTimes 将有序数组打乱次数
     * @return 一个近乎有序的整形数组
     */
    public static int[] generateAlmostOrderlyArray(int size, int swapTimes) {
        int[] arr = new int[size];
        for (int i = 0; i < size; i++) {
            arr[i] = i;
        }
        for (int i = 0; i < swapTimes; i++) {
            int index1 = (int) (Math.random() * size);
            int index2 = (int) (Math.random() * size);
            swap(arr, index1, index2);
        }
        return arr;
    }

    /**
     * 判断一个数组是否有序,有序返回true,无序返回false
     *
     * @param arr 数组
     * @return 是否有序
     */
    public static boolean isOrderly(int[] arr) {
        return isOrderAsc(arr) || isOrderDesc(arr);
    }

    /**
     * 判断一个数组是否顺序有序,有序返回true,无序返回false
     *
     * @param arr 数组
     * @return 是否有序
     */
    private static boolean isOrderAsc(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            if (arr[i] > arr[i + 1]) {
                return false;
            }
        }
        return true;
    }

    /**
     * 判断一个数组是否倒序有序,有序返回true,无序返回false
     *
     * @param arr 数组
     * @return 是否有序
     */
    private static boolean isOrderDesc(int[] arr) {
        for (int i = arr.length - 1; i > 0; i--) {
            if (arr[i] > arr[i - 1]) {
                return false;
            }
        }
        return true;
    }

    /**
     * 拷贝一个整形数组
     *
     * @param arr 整形数组
     * @return 新的整形数组
     */
    public static int[] copyIntArray(int[] arr) {
        int[] copyArr = new int[arr.length];
        System.arraycopy(arr, 0, copyArr, 0, arr.length);
        return copyArr;
    }

    /**
     * 交换数组中的两个索引的位置
     *
     * @param arr 整形数组
     * @param i   第一个索引
     * @param j   第二个索引
     */
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    /**
     * 打印数组
     *
     * @param arr 整形数组
     */
    public static void printArray(int[] arr) {
        for (int i : arr) {
            System.out.print(i + "\t");
        }
        System.out.println();
    }
}

Sort函数式接口

一个基于 JDK8 Lambda 特性的函数式接口,所以该排序算法环境的搭建基于 JDK8+ 的版本。

@FunctionalInterface
public interface Sort {

    /**
     * 排序方法
     *
     * @param arr 数组
     */
    void sort(int[] arr);
}

SortRunner

SortRunner 中有用于运行 Sort 函数式接口的方法,可以在该类的方法下编写一些特定的操作,用于辅助排序算法编写和测试。

public class SortRunner {

    /**
     * 运行排序算法的方法
     *
     * @param arr  一个数组
     * @param sort Sort函数式接口
     */
    public static void run(int[] arr, Sort sort) {
        System.out.print("数组排序前:");
        SortUtils.printArray(arr);
        long startTime = System.currentTimeMillis();
        sort.sort(arr);
        long endTime = System.currentTimeMillis();
        if (SortUtils.isOrderly(arr)) {
            System.out.print("数组排序后:");
            SortUtils.printArray(arr);
            System.out.println("数组排序所需时间为:" + (endTime - startTime) + "毫秒");
        } else {
            System.out.println("数组排序失败");
        }
        System.out.println();
    }
}

冒泡排序

普通冒泡排序

普通的冒泡排序是把相邻的元素两两比较,直到所有元素都比较结束为止

System.out.println("普通冒泡排序");
SortRunner.run(SortUtils.generateRandomArray(10000, 100, 100000),
        (arr) -> {
            for (int i = 0; i < arr.length - 1; i++) {
                for (int j = 0; j < arr.length - i - 1; j++) {
                    if (arr[j] > arr[j + 1]) {
                        SortUtils.swap(arr, j, j + 1);
                    }
                }
            }
        });

这是最为常见的冒泡排序,编写简单,易于理解,但是在一些特殊情况下,可能会浪费一些性能。

比如排序到 n - i 轮的时候,这是遍历数组,发现数组已经有序了,剩下的 i -1 轮其实已经可以不进行遍历了。

所以我们可以对冒泡排序进行一些优化。

布尔类型变量判断有序

这种冒泡排序的思想是,在每一轮遍历数组前,设置一个变量用于标识数组有序,如果在该轮中,数组相邻的元素之间发生了交换,则说明此时数组仍处于无序状态,反之则有序。

// 布尔类型变量判断有序优化冒泡排序
System.out.println("布尔类型变量判断有序优化冒泡排序");
SortRunner.run(SortUtils.generateRandomArray(10000, 100, 100000),
        (arr) -> {
            for (int i = 0; i < arr.length - 1; i++) {
                boolean isSorted = true;
                for (int j = 0; j < arr.length - i - 1; j++) {
                    if (arr[j] > arr[j + 1]) {
                        SortUtils.swap(arr, j, j + 1);
                        isSorted = false;
                    }
                }
                if (isSorted) {
                    break;
                }
            }
        });

无序边界值

布尔类型变量判断有序优化冒泡排序,本质上是减少了冒泡排序的轮数,可以让冒泡排序轮询 <= n - 1 次,但是对于每一轮交换的元素个数,依然是固定的。

但是有的时候,每一轮交换元素的时候,可能交换 n - i 次,数组后段的元素其实就已经有序了,这时继续向后遍历比较是无用的。

所以为了解决这种情况,可以使用无序边界来优化冒泡排序

// 无序边界值优化冒泡排序
System.out.println("无序边界值优化冒泡排序");
SortRunner.run(SortUtils.generateRandomArray(10000, 100, 100000),
        (arr) -> {
            int lastExchangeIndex = 0;
            int sortBorder = arr.length - 1;
            for (int i = 0; i < arr.length - 1; i++) {
                boolean isSorted = true;
                for (int j = 0; j < sortBorder; j++) {
                    if (arr[j] > arr[j + 1]) {
                        SortUtils.swap(arr, j, j + 1);
                        isSorted = false;
                        lastExchangeIndex = j;
                    }
                }
                sortBorder = lastExchangeIndex;
                if (isSorted) {
                    break;
                }
            }
        });

鸡尾酒排序

在一些特殊情况下,比如对一些近似有序的数组进行排序的时候,可以考虑用鸡尾酒排序来对数据进行排序。

鸡尾酒排序的思想是:在冒泡排序比较元素和交换的过程中,使用双向遍历比较和交换元素。

// 鸡尾酒排序
System.out.println("鸡尾酒排序");
SortRunner.run(SortUtils.generateRandomArray(10000, 100, 100000),
        (arr) -> {
            for (int i = 0; i < arr.length / 2; i++) {
                // 奇数轮,从左往右排序
                boolean isSorted = true;
                for (int j = i; j < arr.length - i - 1; j++) {
                    if (arr[j] > arr[j + 1]) {
                        SortUtils.swap(arr, j, j + 1);
                        isSorted = false;
                    }
                }
                if (isSorted) {
                    break;
                }

                // 偶数轮,从右往左毕竟
                isSorted = true;
                for (int j = arr.length - i - 1; j > i; j--) {
                    if (arr[j] < arr[j - 1]) {
                        SortUtils.swap(arr, j, j - 1);
                        isSorted = false;
                    }
                }
                if (isSorted) {
                    break;
                }
            }
        });

左右无序边界值

鸡尾酒排序同样可以使用无序边界值的方法对算法进行优化,不过因为鸡尾酒排序是双向的,所以无序值也是有左右两个。

// 左右无序边界值优化鸡尾酒排序
System.out.println("左右无序边界值优化鸡尾酒排序");
SortRunner.run(SortUtils.generateRandomArray(10000, 100, 100000),
        (arr) -> {
            // 左边最后进行元素交换的索引位置
            int leftExchangeIndex = 0;
            // 左边界
            int leftSortBorder = leftExchangeIndex;
            // 右边最后进行元素交换的索引位置
            int rightExchangeIndex = arr.length - 1;
            // 右边界
            int rightSortBorder = rightExchangeIndex;
            for (int i = 0; i < arr.length / 2; i++) {
                // 奇数轮,从左往右排序
                boolean isSorted = true;
                for (int j = i; j < rightSortBorder; j++) {
                    if (arr[j] > arr[j + 1]) {
                        SortUtils.swap(arr, j, j + 1);
                        isSorted = false;
                        rightExchangeIndex = j;
                    }
                }
                rightSortBorder = rightExchangeIndex;
                if (isSorted) {
                    break;
                }

                // 偶数轮,从右往左毕竟
                isSorted = true;
                for (int j = arr.length - i - 1; j > leftSortBorder; j--) {
                    if (arr[j] < arr[j - 1]) {
                        SortUtils.swap(arr, j, j - 1);
                        isSorted = false;
                        leftExchangeIndex = j;
                    }
                }
                leftSortBorder = leftExchangeIndex;
                if (isSorted) {
                    break;
                }
            }
        });

鸡尾酒排序的效率

数组长度:100000

冒泡排序鸡尾酒排序
无序数组27412毫秒12868毫秒
近似有序13962毫秒43毫秒

以上的测试数据仅仅为简单测试,具体情况请自行测试,另外,如果排序数组长度过长,建议修改 SortRunner.run() 方法,去掉一些无用的操作步骤,减少排序测试的总体时长

最后

本文Github github.com/herenpeng/c… 已收录,欢迎Star