【数据结构排序算法篇】----快速排序【实战演练】

简介: 【数据结构排序算法篇】----快速排序【实战演练】

作为一名对技术充满热情的学习者,我一直以来都深刻地体会到知识的广度和深度。在这个不断演变的数字时代,我远非专家,而是一位不断追求进步的旅行者。通过这篇博客,我想分享我在某个领域的学习经验,与大家共同探讨、共同成长。请大家以开放的心态阅读,相信你们也会在这段知识之旅中找到启示。



前言

学习快速排序,最重要的就是学会分治思想,对于pivot的选择有很多种,这让我们产生了如何使用更加合适的pivot时代码变得更加高效,下面我们就开始谈谈快速排序。


一、什么是快速排序

快速排序是一个高效的排序算法,由C.A.R. Hoare在1960年提出。因其平均时间复杂度为O(n log n)而被广泛使用,并且还具有原地排序(不需要额外大量的内存空间)的优点。这种算法的基本思想是分治策略,通过一个称为"pivot"(中枢或枢轴)的元素来将原数组分割成两个子数组。

快速排序的基本步骤包括:

  1. 选择pivot(枢轴元素)
    从数组中选择一个元素作为pivot。选择方法有多种,例如选择第一个元素、最后一个元素、中间的元素,或者随机选择一个元素。选择不同的pivot可能会影响算法的性能。
  2. 分区操作
    重新排列数组,使得所有比pivot小的元素都排在pivot的前面,而所有比pivot大的元素都排在后面。这一步结束后,pivot就位于其最终排序后的位置。
  3. 递归排序子数组
    递归地在pivot左侧和右侧的子数组上重复前面的步骤,直到子数组的大小缩减到1或0,此时算法结束。

在最坏的情况下,例如当输入数组已经是排序状态或逆排序状态时,每次分区只缩减一个元素,这使得快速排序的时间复杂度退化为O(n^2)。然而,通过随机选择pivot,可以降低这种最坏情况发生的概率,使得快速排序在平均情况下展现出良好性能。

快速排序通常被认为在实际应用中比其他O(n log n)排序算法,如归并排序或堆排序,更快。因为其内部循环(inner

loop)可以有效地在多种现代架构上实现。尽管如此,在选择快速排序的实现时,还应考虑数据的特点以及实际性能表现。

二、三数取中

这里的数组是 {8, 3, 1, 7, 0, 10, 2},我们将使用“三数取中”方法来选择pivot,这在实践中是一个比较好的折衷方法。

在“三数取中”方法中,我们将比较数组的第一个元素、中间元素和最后一个元素,然后选择这三个数的中位数作为pivot。在我们的示例数组中,这将是 8(第一个元素),7(中间元素),和 2(最后一个元素),所以pivot将是 7

以下是Java代码演示了如何对这个数组执行快速排序:

public class QuickSortExample {
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // 找到pivot所在位置
            int pivotIndex = partition(arr, low, high);
            // 对pivot左边的数组递归排序
            quickSort(arr, low, pivotIndex - 1);
            // 对pivot右边的数组递归排序
            quickSort(arr, pivotIndex + 1, high);
        }
    }
    private static int partition(int[] arr, int low, int high) {
        // “三数取中”作为pivot
        int pivot = medianOfThree(arr, low, high);
        
        // 将pivot移到数组末尾
        swap(arr, high, medianOfThreeIndex(arr, low, high));
        int border = low - 1; // 边界
        for (int i = low; i < high; i++) {
            // 所有小于pivot的元素移到左边
            if (arr[i] < pivot) {
                border++;
                swap(arr, i, border);
            }
        }
        // 把pivot换回合适位置
        swap(arr, border + 1, high);
        
        // 返回pivot位置
        return border + 1;
    }
    
    private static int medianOfThree(int[] arr, int low, int high) {
        int middle = low + (high - low) / 2;
        
        int a = arr[low];
        int b = arr[middle];
        int c = arr[high];
        
        if ((a - b) * (c - a) >= 0) return a;
        else if ((b - a) * (c - b) >= 0) return b;
        else return c;
    }
    
    private static int medianOfThreeIndex(int[] arr, int low, int high) {
        int middle = low + (high - low) / 2;
        
        int a = arr[low];
        int b = arr[middle];
        int c = arr[high];
        
        if ((a - b) * (c - a) >= 0) return low;
        else if ((b - a) * (c - b) >= 0) return middle;
        else return high;
    }
    // 交换数组中的两个元素
    private static void swap(int[] arr, int index1, int index2) {
        int temp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = temp;
    }
    public static void main(String[] args) {
        int[] array = {8, 3, 1, 7, 0, 10, 2};
        quickSort(array, 0, array.length - 1);
        for (int value : array) {
            System.out.print(value + " ");
        }
    }
}

执行这段代码后,打印在控制台的应该是已经排序好的数组 0 1 2 3 7 8 10。代码示例中使用了递归和分治的方法,将问题简化,在每一步将数组划分为小于pivot和大于pivot的两个部分,然后递归排序这两部分。

三、考研例题

题目:考虑以下未排序的数组:

15, 22, 13, 27, 12, 10, 20, 25

使用快速排序算法对上述数组进行排序。请手动执行快速排序的第一次分区操作,并给出以下分区后的数组状态以及枢轴的最终位置。

在此问题中,我们规定每次分区操作的枢轴选择数组的最后一个元素。

分步解答:

  1. 初始化数组和枢轴(pivot):
[15, 22, 13, 27, 12, 10, 20, 25]
Pivot = 25
  1. 设置左指针 left 和右指针 right
left = 0        (指向数组的第一个元素)
right = 6       (指向枢轴的前一个元素)
  1. 执行分区操作,移动左右指针,并在必要时交换元素。我们按顺序遍历,直到 left 指针大于 right 指针停止。
    在第一个步骤中,遍历到 left = 5right = 0,由于 12 小于 25,我们不需要对它们进行交换。此时,数组的状态仍然相同。
[15, 22, 13, 27, 12, 10, 20, 25]
left ->                      ^   
right -> ^
  1. 在第二次遍历中,left 指针挪到值为 15 的位置,而 right 指针仍然指向 15。由于 15 小于枢轴,left 继续向前移动,而 right 不动。
  2. 继续进行,left 指针移动到值为 22 的位置。由于 22 小于 25left 继续向前移动。
  3. left 指针达到值为 27 时,该值大于枢轴值,right 仍然指向值为 15 的位置。
  4. 移动 right 指针向左,找到第一个小于枢轴 25 的元素(20),然后交换 left 指针和 right 指针的值。
    交换后的数组:
[15, 20, 13, 12, 10, 22, 27, 25]
left ->                            ^   
right ->                      ^
  1. 注意到现在 leftright 都指向了 22
  2. 因为 leftright 相遇了,分区操作结束。根据快速排序算法的规则,枢轴值(25)应该和 left (或者 right,因为它们在同一位置)指针的值交换位置。
    最终数组的状态:
[15, 20, 13, 12, 10, 22, 25, 27]
                           ^
                        pivot

在这个分区操作之后,枢轴值 25 的新索引位置是 6。数组被分成两部分:索引 05 和索引 7 的元素。注意,现在枢轴值 25 正在其最终排列中的正确位置。递归地对左右两侧的子数组进行相同的分区操作,直到整个数组排序完毕。


请注意,这只是考试中可能遇到的快速排序手动执行的一种简单示例。在不同版本的快速排序中,枢轴的选择和分区逻辑可以有所不同,因此学生应该熟悉算法的不同变体。

四、Java面试题

在大型技术公司的Java面试中,关于快速排序的问题可能不会局限于基本的算法实现,而会涉及算法的优化、特殊情况处理、空间和时间复杂度分析等。以下是在Java面试中可能遇到的与快速排序相关的问题。

面试题

  1. 在Java中实现快速排序算法。
  2. 快速排序的时间复杂度是多少?最好、平均和最坏情况下的时间复杂度分别是什么?
  3. 快速排序的空间复杂度是多少?
  4. 如何选择枢轴(pivot)以优化快速排序的性能?
  5. 快速排序算法如何避免退化为最坏情况的复杂度?
  6. 请解释快速排序不稳定性的含义。你能实现一个稳定的快速排序算法吗?
  7. 对已几乎有序的数组进行快速排序时,你会如何优化你的算法?
  8. 如果数组包含大量重复元素,你如何修改快速排序算法以提高效率?
  9. 请比较快速排序和归并排序,并讨论它们的优劣。
  10. 描述三向切分(3-way partitioning)快速排序的原理以及它解决的问题。

一个典型的快速排序面试题可能要求你实际编写代码并解释它。例如:

题目:给定一个整数数组,编写一个Java函数来实现快速排序,并对该数组进行原地(in-place)排序。

示例答案

public class QuickSort {
    public void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // 分区操作,返回枢轴所在位置
            int pivotIndex = partition(arr, low, high);
            // 递归对枢轴左侧的子数组进行快速排序
            quickSort(arr, low, pivotIndex - 1);
            // 递归对枢轴右侧的子数组进行快速排序
            quickSort(arr, pivotIndex + 1, high);
        }
    }
    private int partition(int[] arr, int low, int high) {
        int pivot = arr[high];
        int i = low - 1; // 小于枢轴的元素的索引
        for (int j = low; j < high; j++) {
            if (arr[j] <= pivot) {
                i++;
                // 如果当前元素小于或等于枢轴,则交换它和小于枢轴的元素的索引
                swap(arr, i, j);
            }
        }
        // 将枢轴元素放到正确的位置
        swap(arr, i + 1, high);
        return i + 1;
    }
    private void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    // 对外公开的快速排序接口
    public void sort(int[] arr) {
        if (arr != null && arr.length > 1) {
            quickSort(arr, 0, arr.length - 1);
        }
    }
    // 驱动函数,用于测试
    public static void main(String[] args) {
        QuickSort sorter = new QuickSort();
        int[] array = {15, 3, 2, 1, 9, 5, 7, 8, 6};
        sorter.sort(array);
        for (int num : array) {
            System.out.print(num + " ");
        }
    }
}

记住这只是一个快速排序的基础实现。在实际的面试中,你可能需要讨论更多高级话题,如递归栈的空间复杂度、尾递归优化、转换为迭代版本等。此外,面试官可能会要求你写出不同枢轴选择策略的代码实例,如随机选择枢轴、中位数的中位数等。

五、思考

在快速排序的递归过程中,如何对左右子数组进行分区操作?

解答

在快速排序算法中,分区操作是核心步骤。在递归过程中,每一步的目的是将数组分成两个子数组,使得一个子数组中的所有元素都比枢轴(pivot)小,而另一个子数组中的所有元素都比枢轴大。以下是一个常见的分区步骤描述:

  1. 选择枢轴(pivot)
    首先你需要选择一个枢轴。这可以是数组中的任何一个元素,比如最后一个元素、第一个元素、中间元素、或者是三者的中位数。
  2. 初始化指针
    接着,初始化两个指针,一个是 left,从数组的开始位置指起,另一个是 right,从枢轴前一个位置指起。
  3. 分区操作:接下来进行分区操作。比较left指针指向的元素和枢轴的值:
  • 如果 left 指针指向的元素小于等于枢轴的值,则移动 left 指针向右。
  • 如果 right 指针指向的元素大于枢轴的值,则移动 right 指针向左。
  • left 指针指向的元素大于枢轴的值,且 right 指针指向的元素小于或等于枢轴的值时,交换这两个元素的位置,然后各自移动指针。
  1. 交换枢轴
    left 指针和 right 指针相遇时,所有在 left 指针左侧的元素都应该比枢轴小,而右侧的元素都比枢轴大。此时,交换枢轴和 left 指针指向的元素(或者 right 指针指向的元素,这取决于具体实现),这样枢轴元素即被放置到了正确的位置,这个位置就是分区的划分点。
  2. 递归排序子数组
    最后,使用递归方法分别对枢轴左侧和右侧的子数组进行排序。递归的基本情况是当子数组只剩下一个元素或没有元素时,这表明该子数组已有序或者不需要排序。

以上描述的分区过程会在每次递归调用中不断重复,直到整个数组有序。这种方法称为“原地分区”(in-place

partitioning),因为它不需要额外存储空间来创建子数组。


总结

对快速排序的总结就到这里,我们通过面试题和考研中出现的例题,来带大家深刻的理解什么是快速排序,在各种场景下我们会遇到不同的题目,我们将要如何应对呢,这些都需要我们掌握和理解快排的算法逻辑,需要搭配大量的练习,希望同学们可以多练练。

感谢大家抽出宝贵的时间来阅读博主的博客,新人博主,感谢大家关注点赞,祝大家未来的学习工作生活一帆风顺,加油!!!

目录
相关文章
|
10天前
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
38 4
|
28天前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
60 2
|
1月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
26 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
24天前
|
存储 缓存 算法
前端算法:优化与实战技巧的深度探索
【10月更文挑战第21天】前端算法:优化与实战技巧的深度探索
20 1
|
1月前
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
21 1
|
1月前
|
搜索推荐 算法
数据结构与算法学习十四:常用排序算法总结和对比
关于常用排序算法的总结和对比,包括稳定性、内排序、外排序、时间复杂度和空间复杂度等术语的解释。
20 0
数据结构与算法学习十四:常用排序算法总结和对比
|
1月前
|
搜索推荐 Java Go
深入了解快速排序算法
深入了解快速排序算法
32 2
|
1月前
|
机器学习/深度学习 搜索推荐 算法
探索数据结构:初入算法之经典排序算法
探索数据结构:初入算法之经典排序算法
|
1月前
|
存储 搜索推荐 算法
【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
|
1月前
|
算法 Python
Python算法编程:冒泡排序、选择排序、快速排序
Python算法编程:冒泡排序、选择排序、快速排序