每日算法 — 使用java实现鸡尾酒排序:双向冒泡与有序边界检测

鸡尾酒排序(Cocktail Shaker Sort),又称双向冒泡排序或摇摆排序,是冒泡排序的改进版本。传统冒泡排序单向扫描,而鸡尾酒排序交替进行正向和反向扫描,一次遍历可将最大元素”冒泡”到末尾、最小元素”沉底”到开头。本文用Java实现鸡尾酒排序,讲解有序边界检测优化与复杂度分析。

算法核心思想

冒泡排序每一轮只能确定一个最大元素的位置。鸡尾酒排序的改进在于:

  • 正向扫描:从左到右,将当前未排序区间的最大值交换到右边界
  • 反向扫描:从右到左,将当前未排序区间的最小值交换到左边界
  • 边界收缩:记录最后一次交换的位置,缩小下一轮扫描区间

这种双向扫描使得算法对”乌龟型”数据(小元素在末尾)更加友好。

与冒泡排序的对比

特性 冒泡排序 鸡尾酒排序
扫描方向 单向 双向交替
每轮确定元素 1个最大 1个最大 + 1个最小
对逆序小元素 需多轮移动 反向一轮即可
最优时间复杂度 O(n) O(n)
平均时间复杂度 O(n²) O(n²)

Java完整实现

import java.util.Arrays;

/**
 * 鸡尾酒排序(Cocktail Shaker Sort)
 * 双向冒泡排序,交替进行正向和反向扫描
 */
public class CocktailShakerSort {

    /**
     * 基础版鸡尾酒排序
     * 双向扫描,每轮确定最大和最小元素位置
     */
    public static void sort(int[] arr) {
        if (arr == null || arr.length <= 1) return;

        int n = arr.length;
        boolean swapped;

        for (int i = 0; i < n / 2; i++) {
            swapped = false;

            // 正向扫描:将最大值移到未排序区间末尾
            for (int j = i; j < n - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                    swapped = true;
                }
            }

            // 如果正向扫描没有交换,说明已有序
            if (!swapped) break;

            // 反向扫描:将最小值移到未排序区间开头
            for (int j = n - 2 - i; j > i; j--) {
                if (arr[j] < arr[j - 1]) {
                    swap(arr, j, j - 1);
                    swapped = true;
                }
            }

            // 如果反向扫描也没有交换,说明已有序
            if (!swapped) break;
        }
    }

    /**
     * 优化版鸡尾酒排序:记录最后一次交换位置,缩小扫描边界
     * 这是实际应用中推荐的版本
     */
    public static void sortOptimized(int[] arr) {
        if (arr == null || arr.length <= 1) return;

        int n = arr.length;
        int left = 0;           // 左边界
        int right = n - 1;      // 右边界
        int lastSwapLeft = 0;   // 最后一次交换的左位置
        int lastSwapRight = 0;  // 最后一次交换的右位置

        while (left < right) {
            lastSwapRight = left;

            // 正向扫描:将最大值移到right位置
            for (int i = left; i < right; i++) {
                if (arr[i] > arr[i + 1]) {
                    swap(arr, i, i + 1);
                    lastSwapRight = i; // 记录最后一次交换位置
                }
            }

            // 如果没有发生交换,提前结束
            if (lastSwapRight == left) break;

            right = lastSwapRight; // 缩小右边界
            lastSwapLeft = right;

            // 反向扫描:将最小值移到left位置
            for (int i = right; i > left; i--) {
                if (arr[i] < arr[i - 1]) {
                    swap(arr, i, i - 1);
                    lastSwapLeft = i;
                }
            }

            // 如果没有发生交换,提前结束
            if (lastSwapLeft == right) break;

            left = lastSwapLeft; // 缩小左边界
        }
    }

    /**
     * 通用版:支持任意Comparable类型
     */
    public static <T extends Comparable<T>> void sortGeneric(T[] arr) {
        if (arr == null || arr.length <= 1) return;

        int left = 0;
        int right = arr.length - 1;
        int lastSwapLeft, lastSwapRight;

        while (left < right) {
            lastSwapRight = left;

            for (int i = left; i < right; i++) {
                if (arr[i].compareTo(arr[i + 1]) > 0) {
                    T temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                    lastSwapRight = i;
                }
            }

            if (lastSwapRight == left) break;
            right = lastSwapRight;
            lastSwapLeft = right;

            for (int i = right; i > left; i--) {
                if (arr[i].compareTo(arr[i - 1]) < 0) {
                    T temp = arr[i];
                    arr[i] = arr[i - 1];
                    arr[i - 1] = temp;
                    lastSwapLeft = i;
                }
            }

            if (lastSwapLeft == right) break;
            left = lastSwapLeft;
        }
    }

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

    /**
     * 打印数组
     */
    public static void printArray(int[] arr) {
        System.out.println(Arrays.toString(arr));
    }

    public static void main(String[] args) {
        // 测试用例1:包含"乌龟型"数据的数组(小元素在末尾)
        int[] arr1 = {5, 1, 4, 2, 8, 0, 2};
        System.out.println("原始数组: " + Arrays.toString(arr1));
        sortOptimized(arr1.clone());
        System.out.println("排序结果: " + Arrays.toString(arr1));

        // 测试用例2:完全逆序(鸡尾酒排序的优势场景)
        int[] arr2 = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
        System.out.println("\n逆序数组: " + Arrays.toString(arr2));
        sortOptimized(arr2);
        System.out.println("排序结果: " + Arrays.toString(arr2));

        // 测试用例3:近乎有序的数组
        int[] arr3 = {1, 2, 3, 5, 4, 6, 7, 8, 9, 10};
        System.out.println("\n近乎有序: " + Arrays.toString(arr3));
        sortOptimized(arr3);
        System.out.println("排序结果: " + Arrays.toString(arr3));

        // 性能对比:鸡尾酒排序 vs 冒泡排序
        System.out.println("\n=== 性能对比 ===");
        int size = 5000;
        int[] testArr = new int[size];
        for (int i = 0; i < size; i++) {
            testArr[i] = (int)(Math.random() * size);
        }

        // 冒泡排序
        int[] bubbleArr = testArr.clone();
        long start1 = System.currentTimeMillis();
        bubbleSort(bubbleArr);
        long time1 = System.currentTimeMillis() - start1;

        // 鸡尾酒排序
        int[] cocktailArr = testArr.clone();
        long start2 = System.currentTimeMillis();
        sortOptimized(cocktailArr);
        long time2 = System.currentTimeMillis() - start2;

        System.out.printf("数组大小: %d%n", size);
        System.out.printf("冒泡排序耗时: %d ms%n", time1);
        System.out.printf("鸡尾酒排序耗时: %d ms%n", time2);

        // 验证排序正确性
        boolean isSorted = true;
        for (int i = 1; i < cocktailArr.length; i++) {
            if (cocktailArr[i] < cocktailArr[i - 1]) {
                isSorted = false;
                break;
            }
        }
        System.out.println("排序正确性: " + (isSorted ? "通过" : "失败"));
    }

    /**
     * 标准冒泡排序(用于对比)
     */
    private static void bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            boolean swapped = false;
            for (int j = 0; j < n - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                    swapped = true;
                }
            }
            if (!swapped) break;
        }
    }
}

边界检测优化详解

优化版的核心在于记录最后一次交换位置。原理是:如果某位置之后没有发生交换,说明该位置之后的子数组已经有序。

初始: [3, 5, 2, 8, 1, 9, 4]
正向扫描后: [3, 2, 5, 1, 8, 4, 9]
           最后一次交换在索引5 (4和9交换)
           → right边界从6缩小到5
反向扫描后: [1, 3, 2, 5, 4, 8, 9]
           最后一次交换在索引1 (3和1交换)
           → left边界从0扩大到1
下一轮只需扫描 arr[1..5]

复杂度分析

情况 时间复杂度 说明
最优 O(n) 数组已有序,一轮扫描即可结束
平均 O(n²) 双向扫描减少约一半的比较次数
最坏 O(n²) 完全逆序,但反向扫描能快速处理小元素
空间 O(1) 原地排序,仅需常数额外空间

虽然最坏复杂度与冒泡排序相同,但鸡尾酒排序在以下场景表现更好:

  • “乌龟型”数据:小元素位于数组末尾时,反向扫描一次即可将其移动到正确位置
  • 数据波动大:同时存在极大值在头部和极小值在尾部的情况
  • 近乎有序:边界检测能大幅缩小扫描范围

稳定性说明

鸡尾酒排序是稳定排序。当相邻元素相等时,我们只在 arr[i] > arr[i+1] 时才交换,相等元素保持原有相对顺序。

适用场景

鸡尾酒排序适合:

  • 教学演示:直观展示排序的双向优化思想
  • 小规模数据:n < 1000时性能可接受
  • 数据近乎有序:边界检测能提前终止
  • 学习冒泡排序改进:理解如何从单向到双向优化算法

对于大规模数据,推荐使用快速排序、归并排序或TimSort(Java Arrays.sort() 的实现)。