面试:排序算法之快速排序

深入浅出详解快速排序算法。涵盖“分治法”核心逻辑、基准值选取、双指针分区实现及复杂度分析,助你高效攻克算法面试重难点。

在日常开发或者面试中,排序算法总是绕不开的话题。相比于冒泡排序、选择排序这些基础算法,快速排序以其高效的执行速度和优雅的逻辑脱颖而出。它不仅是很多语言标准库中默认的排序算法,更是理解分治思想(Divide and Conquer)的绝佳案例。

核心思想:分治法

快速排序的核心可以总结为三个字:找、分、排。它采用的是分治策略,具体步骤如下:

1、找基准(Pivot):从数组中挑出一个元素作为“基准点”。(为了方便,我们通常选择数组的最后一个元素,或者第一个元素)。

2、分区(Partition): 遍历数组,把比基准小的元素全部扔到基准的左边,把比基准大的元素全部扔到基准的右边。这一步完成后,这个基准元素就待在了它最终排好序应该在的位置。

3、递归(Recursion): 把基准左边的子数组和右边的子数组,再次按照上述两步进行操作,直到子数组的长度为 1 或 0,排序完成!

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class QuickSortDemo {

    public static void main(String[] args) {
        int[] arr = {9, 3, 7, 5, 6, 4, 8, 2};
        System.out.println("排序前:");
        printArray(arr);

        // 调用快速排序,初始范围是整个数组
        quickSort(arr, 0, arr.length - 1);

        System.out.println("排序后:");
        printArray(arr);
    }

    /**
     * 快速排序主方法
     * @param arr  待排序数组
     * @param low  起始索引
     * @param high 结束索引
     */
    public static void quickSort(int[] arr, int low, int high) {
        // 递归结束条件:当子数组长度为1或0时停止
        if (low < high) {
            // 获取基准元素排序后的正确索引位置
            int pivotIndex = partition(arr, low, high);

            // 递归排序基准左边的子数组
            quickSort(arr, low, pivotIndex - 1);
            // 递归排序基准右边的子数组
            quickSort(arr, pivotIndex + 1, high);
        }
    }

    /**
     * 分区逻辑(核心)
     * @param arr  待排序数组
     * @param low  起始索引
     * @param high 结束索引
     * @return     基准元素的最终位置
     */
    private static int partition(int[] arr, int low, int high) {
        // 我们选择最右边的元素作为基准 (Pivot)
        int pivot = arr[high];
        
        // i 指向比基准小的区域的最后一个元素
        int i = low - 1;

        // 遍历当前范围内的元素(除了最后一个基准元素)
        for (int j = low; j < high; j++) {
            // 如果当前元素小于或等于基准
            if (arr[j] <= pivot) {
                i++; // 扩充小于基准的区域
                swap(arr, i, j); // 将小元素交换到前面去
            }
        }

        // 遍历结束后,将基准元素放到中间(i+1 的位置)
        swap(arr, i + 1, high);
        
        // 返回基准元素最终所在的位置
        return i + 1;
    }

    /**
     * 交换数组中两个元素的方法
     */
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    /**
     * 打印数组辅助方法
     */
    private static void printArray(int[] arr) {
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

在这段代码中,最难理解的部分通常是 partition(分区)方法:

1、这里使用了 i 和 j 两个指针。j 是一个探测指针,负责一路向右扫描;而 i 是一个边界指针,它维护着“小于基准值”的区域边界。

2、当 j 发现一个小于等于 pivot 的元素时,我们就让 i 往前走一步(i++),然后把 i 和 j 指向的元素互换。这就像是把小的元素不断地“丢”到数组的前面。

3、当 j 遍历完所有元素后,i+1 的位置就是基准元素应该在的正确位置。我们把基准元素(在 high 位置)与 i+1 互换,分区就大功告成了!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计