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

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

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



前言

学习快速排序,最重要的就是学会分治思想,对于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),因为它不需要额外存储空间来创建子数组。


总结

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

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

目录
相关文章
|
22天前
|
算法 安全 数据安全/隐私保护
Android经典实战之常见的移动端加密算法和用kotlin进行AES-256加密和解密
本文介绍了移动端开发中常用的数据加密算法,包括对称加密(如 AES 和 DES)、非对称加密(如 RSA)、散列算法(如 SHA-256 和 MD5)及消息认证码(如 HMAC)。重点讲解了如何使用 Kotlin 实现 AES-256 的加密和解密,并提供了详细的代码示例。通过生成密钥、加密和解密数据等步骤,展示了如何在 Kotlin 项目中实现数据的安全加密。
56 1
|
22天前
|
机器学习/深度学习 存储 算法
强化学习实战:基于 PyTorch 的环境搭建与算法实现
【8月更文第29天】强化学习是机器学习的一个重要分支,它让智能体通过与环境交互来学习策略,以最大化长期奖励。本文将介绍如何使用PyTorch实现两种经典的强化学习算法——Deep Q-Network (DQN) 和 Actor-Critic Algorithm with Asynchronous Advantage (A3C)。我们将从环境搭建开始,逐步实现算法的核心部分,并给出完整的代码示例。
54 1
|
23天前
|
算法 安全 数据安全/隐私保护
Android经典实战之常见的移动端加密算法和用kotlin进行AES-256加密和解密
本文介绍了移动端开发中常用的数据加密算法,包括对称加密(如 AES 和 DES)、非对称加密(如 RSA)、散列算法(如 SHA-256 和 MD5)及消息认证码(如 HMAC)。重点展示了如何使用 Kotlin 实现 AES-256 的加密和解密,提供了详细的代码示例。
31 2
|
24天前
|
机器学习/深度学习 算法 数据挖掘
【白话机器学习】算法理论+实战之决策树
【白话机器学习】算法理论+实战之决策树
|
30天前
|
算法 搜索推荐 Java
算法实战:手写归并排序,让复杂排序变简单!
归并排序是一种基于“分治法”的经典算法,通过递归分割和合并数组,实现O(n log n)的高效排序。本文将通过Java手写代码,详细讲解归并排序的原理及实现,帮助你快速掌握这一实用算法。
37 0
|
1月前
|
存储 算法 搜索推荐
【初阶数据结构篇】冒泡排序和快速排序(中篇)
与直接插入排序法相比,比较次数一致,但冒泡排序的交换需要执行三次,而直接插入排序因为使用了tmp临时变量存储要插入的数据,只用执行一次,所以直接插入排序法效率明显更高。
|
30天前
|
数据采集 搜索推荐 算法
【高手进阶】Java排序算法:从零到精通——揭秘冒泡、快速、归并排序的原理与实战应用,让你的代码效率飙升!
【8月更文挑战第21天】Java排序算法是编程基础的重要部分,在算法设计与分析及实际开发中不可或缺。本文介绍内部排序算法,包括简单的冒泡排序及其逐步优化至高效的快速排序和稳定的归并排序,并提供了每种算法的Java实现示例。此外,还探讨了排序算法在电子商务、搜索引擎和数据分析等领域的广泛应用,帮助读者更好地理解和应用这些算法。
21 0
|
1月前
|
消息中间件 存储 算法
这些年背过的面试题——实战算法篇
本文是技术人面试系列实战算法篇,面试中关于实战算法都需要了解哪些内容?一文带你详细了解,欢迎收藏!
|
1月前
|
算法
【初阶数据结构篇】二叉树算法题
二叉树是否对称,即左右子树是否对称.
|
1月前
|
算法 索引
【初阶数据结构篇】单链表算法题进阶
深拷贝应该正好由 n 个全新节点组成,其中每个新节点的值都设为其对应的原节点的值。