【算法基础】快速排序算法 - JAVA

一、算法基础

1.1 什么是快速排序

快速排序(Quick Sort)是一种高效的分治排序算法,由英国计算机科学家Tony Hoare于1960年提出。它的核心思想是:

  • 选择一个基准元素(pivot)
  • 将数组分成两部分:小于基准的元素和大于基准的元素
  • 递归地对这两部分进行排序

快速排序是实际应用中最常用的排序算法之一,平均情况下时间复杂度为O(n log n) ,空间复杂度为O(log n)

1.2 快速排序的基本思想

  1. 选择基准:从数组中选择一个元素作为基准(通常是第一个元素、最后一个元素或中间元素)
  2. 分区操作:将数组中小于基准的元素放在基准的左边,大于基准的元素放在右边
  3. 递归排序:对基准左右两部分分别进行递归排序

这个过程被称为分区(Partition),是快速排序的核心操作。

1.3 时间复杂度分析

  • 最佳情况:O(n log n) ------ 每次分区操作都将数组均匀地分成两部分
  • 平均情况:O(n log n) ------ 大多数情况下的性能表现
  • 最差情况:O(n²) ------ 当数组已经有序或几乎有序时,选择第一个或最后一个元素作为基准

二、快速排序的实现

2.1 基本实现

java 复制代码
public class QuickSort {
    public static void sort(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        quickSort(arr, 0, arr.length - 1);
    }
    
    private static void quickSort(int[] arr, int left, int right) {
        if (left < right) {
            // 获取分区点
            int pivotIndex = partition(arr, left, right);
            
            // 递归排序左半部分
            quickSort(arr, left, pivotIndex - 1);
            
            // 递归排序右半部分
            quickSort(arr, pivotIndex + 1, right);
        }
    }
    
    private static int partition(int[] arr, int left, int right) {
        // 选择最右边的元素作为基准
        int pivot = arr[right];
        
        // i是小于基准区域的边界
        int i = left - 1;
        
        // 遍历区间,将小于基准的元素放到左边
        for (int j = left; j < right; j++) {
            if (arr[j] <= pivot) {
                i++;
                // 交换元素
                swap(arr, i, j);
            }
        }
        
        // 将基准元素放到正确的位置
        swap(arr, i + 1, right);
        
        // 返回基准元素的索引
        return i + 1;
    }
    
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

2.2 递归过程分析

假设有数组:[8, 4, 2, 9, 5, 7, 6, 1, 3]

  1. 第一次分区

    • 选择基准值 3(最后一个元素)
    • 分区后:[1, 2, 3, 9, 5, 7, 6, 8, 4]
    • 基准索引:2
  2. 左子数组递归:[1, 2]

    • 选择基准值 2
    • 分区后:[1, 2]
    • 基准索引:1
  3. 右子数组递归:[9, 5, 7, 6, 8, 4]

    • 选择基准值 4
    • 分区后:[4, 5, 7, 6, 8, 9]
    • 基准索引:0
  4. 继续递归...

最终得到排序结果:[1, 2, 3, 4, 5, 6, 7, 8, 9]

三、快速排序的优化

3.1 基准选择优化

选择好的基准可以显著提高快速排序的性能。最常用的优化方法是三数取中(Median-of-Three):

java 复制代码
private static int selectPivot(int[] arr, int left, int right) {
    int mid = left + (right - left) / 2;
    
    // 将三个元素排序
    if (arr[left] > arr[mid]) {
        swap(arr, left, mid);
    }
    if (arr[left] > arr[right]) {
        swap(arr, left, right);
    }
    if (arr[mid] > arr[right]) {
        swap(arr, mid, right);
    }
    
    // 将中间值(基准)交换到right-1位置
    swap(arr, mid, right - 1);
    return arr[right - 1];
}

3.2 小数组使用插入排序

对于小规模数组(通常小于10个元素),插入排序比快速排序更高效:

java 复制代码
private static void quickSort(int[] arr, int left, int right) {
    // 小数组使用插入排序
    if (right - left <= 10) {
        insertionSort(arr, left, right);
        return;
    }
    
    // 正常的快速排序过程
    if (left < right) {
        int pivotIndex = partition(arr, left, right);
        quickSort(arr, left, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, right);
    }
}

private static void insertionSort(int[] arr, int left, int right) {
    for (int i = left + 1; i <= right; i++) {
        int key = arr[i];
        int j = i - 1;
        
        while (j >= left && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        
        arr[j + 1] = key;
    }
}

3.3 尾递归优化

通过将递归调用替换为迭代,可以避免栈溢出:

java 复制代码
private static void quickSort(int[] arr, int left, int right) {
    while (left < right) {
        int pivotIndex = partition(arr, left, right);
        
        // 递归处理较小的子数组,迭代处理较大的子数组
        if (pivotIndex - left < right - pivotIndex) {
            quickSort(arr, left, pivotIndex - 1);
            left = pivotIndex + 1;
        } else {
            quickSort(arr, pivotIndex + 1, right);
            right = pivotIndex - 1;
        }
    }
}

四、典型应用:荷兰国旗问题

荷兰国旗问题是快速排序的一个典型应用,它要求将数组分成三个部分:小于、等于、大于某个值的元素。这种分区方法也称为三向切分

4.1 问题描述

给定一个数组和一个基准值,将数组重新排列,使得所有小于基准的元素在左边,等于基准的元素在中间,大于基准的元素在右边。

4.2 解法实现

java 复制代码
public static void dutchFlagPartition(int[] arr, int pivot) {
    int left = 0;       // 小于pivot的区域右边界
    int current = 0;    // 当前处理的元素
    int right = arr.length - 1;  // 大于pivot的区域左边界
    
    while (current <= right) {
        if (arr[current] < pivot) {
            // 小于pivot的元素放左边
            swap(arr, left, current);
            left++;
            current++;
        } else if (arr[current] > pivot) {
            // 大于pivot的元素放右边
            swap(arr, current, right);
            right--;
            // 注意:此时不增加current,因为交换来的元素还未处理
        } else {
            // 等于pivot的元素保持原位
            current++;
        }
    }
}

4.3 应用到快速排序

使用三向切分可以优化快速排序,特别是处理有大量重复元素的数组:

java 复制代码
private static void quickSort3Way(int[] arr, int left, int right) {
    if (left >= right) {
        return;
    }
    
    // 记录小于、等于、大于pivot的三个区域边界
    int lt = left;      // 小于pivot的区域右边界
    int gt = right;     // 大于pivot的区域左边界
    int i = left + 1;   // 当前处理的元素
    int pivot = arr[left];
    
    while (i <= gt) {
        if (arr[i] < pivot) {
            swap(arr, lt++, i++);
        } else if (arr[i] > pivot) {
            swap(arr, i, gt--);
        } else {
            i++;
        }
    }
    
    // 递归处理小于和大于的部分
    quickSort3Way(arr, left, lt - 1);
    quickSort3Way(arr, gt + 1, right);
}

五、完整实现与示例

以下是一个包含各种优化的完整快速排序实现:

java 复制代码
public class OptimizedQuickSort {
    private static final int INSERTION_SORT_THRESHOLD = 10;
    
    public static void sort(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        quickSort(arr, 0, arr.length - 1);
    }
    
    private static void quickSort(int[] arr, int left, int right) {
        // 小数组使用插入排序
        if (right - left <= INSERTION_SORT_THRESHOLD) {
            insertionSort(arr, left, right);
            return;
        }
        
        // 使用三数取中法选择基准
        medianOfThree(arr, left, right);
        int pivot = arr[right - 1];
        
        // 分区
        int i = left;
        int j = right - 1;
        for (;;) {
            while (arr[++i] < pivot) {}
            while (arr[--j] > pivot) {}
            if (i >= j) break;
            swap(arr, i, j);
        }
        
        // 将基准放回正确位置
        swap(arr, i, right - 1);
        
        // 递归排序
        quickSort(arr, left, i - 1);
        quickSort(arr, i + 1, right);
    }
    
    private static void medianOfThree(int[] arr, int left, int right) {
        int mid = left + (right - left) / 2;
        
        if (arr[left] > arr[mid]) {
            swap(arr, left, mid);
        }
        if (arr[left] > arr[right]) {
            swap(arr, left, right);
        }
        if (arr[mid] > arr[right]) {
            swap(arr, mid, right);
        }
        
        // 将基准(中间值)放到right-1位置
        swap(arr, mid, right - 1);
    }
    
    private static void insertionSort(int[] arr, int left, int right) {
        for (int i = left + 1; i <= right; i++) {
            int key = arr[i];
            int j = i - 1;
            
            while (j >= left && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            
            arr[j + 1] = key;
        }
    }
    
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
    public static void main(String[] args) {
        int[] arr = {8, 4, 2, 9, 5, 7, 6, 1, 3};
        System.out.println("原始数组: " + Arrays.toString(arr));
        
        sort(arr);
        
        System.out.println("排序后数组: " + Arrays.toString(arr));
    }
}

六、总结

核心要点

  1. 分治思想:快速排序采用分治策略,通过分区操作将问题分解为子问题
  2. 基准选择:基准元素的选择直接影响算法效率,好的选择可以避免最坏情况
  3. 就地排序:快速排序是一种原地排序算法,不需要额外的数组空间
  4. 不稳定性:快速排序不是稳定排序算法,相同元素的相对顺序可能改变

优缺点

优点:

  • 平均情况下,性能优于其他 O(n log n) 排序算法
  • 不需要额外的存储空间(原地排序)
  • 对缓存友好,局部性好

缺点:

  • 最坏情况下,时间复杂度为 O(n²)
  • 不稳定排序,无法保持相等元素的相对顺序
  • 对于小数组,可能不如插入排序高效

适用场景

  • 需要高效排序大数组时
  • 内存受限、不能使用额外空间时
  • 当平均性能比最坏情况性能更重要时
  • 不要求排序稳定性时

快速排序是一种高效且实用的排序算法,在大多数情况下都表现出色。通过本文介绍的各种优化技巧,可以使快速排序在实际应用中获得最佳性能。掌握快速排序是每位程序员的基本功,也是理解分治算法思想的重要一步。

相关推荐
xin007hoyo几秒前
算法笔记.求约数
c++·笔记·算法
阿黄学技术11 分钟前
ReentrantLock实现公平锁和非公平锁
java·开发语言·算法
汤姆_51115 分钟前
【c语言】字符串函数
c语言·算法
_Itachi__23 分钟前
LeetCode 热题 100 54. 螺旋矩阵
算法·leetcode·矩阵
柏木乃一35 分钟前
平衡二叉搜索树模拟实现1-------AVL树(插入,删除,查找)
c++·学习·程序人生·算法·二叉搜索树·avl树·平衡二叉搜索树
智者知已应修善业1 小时前
【51单片机6位数码管显示时间与秒表】2022-5-8
c语言·c++·经验分享·笔记·单片机·算法·51单片机
八股文领域大手子1 小时前
第 7 篇:跳表 (Skip List):简单务实的概率性选手
java·数据结构·windows·算法·leetcode·链表·动态规划
小O的算法实验室1 小时前
2019年SEVC SCI1区TOP:维度学习粒子群算法TSLPSO,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
心.c1 小时前
最小单调子序列的长度+联通最小乘积
数据结构·c++·算法·leetcode
vibag2 小时前
启发式算法-遗传算法
java·算法·启发式算法·智能算法