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

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

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



前言

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


总结

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

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

xxxhuxxx
+关注
目录
打赏
0
2
2
0
19
分享
相关文章
快速排序-数据结构与算法
快速排序(Quick Sort)是一种基于分治法的高效排序算法。其核心思想是通过选择基准(pivot),将数组划分为左右两部分,使得左侧元素均小于基准,右侧元素均大于基准,然后递归地对左右两部分进行排序。时间复杂度平均为 O(n log n),最坏情况下为 O(n²)(如数组已有序)。空间复杂度为 O(1),属于原地排序,但稳定性不佳。 实现步骤包括编写 `partition` 核心逻辑、递归调用的 `quickSort` 和辅助函数 `swap`。优化方法有随机化基准和三数取中法,以减少最坏情况的发生。
|
2月前
|
算法系列之数据结构-二叉树
树是一种重要的非线性数据结构,广泛应用于各种算法和应用中。本文介绍了树的基本概念、常见类型(如二叉树、满二叉树、完全二叉树、平衡二叉树、B树等)及其在Java中的实现。通过递归方法实现了二叉树的前序、中序、后序和层次遍历,并展示了具体的代码示例和运行结果。掌握树结构有助于提高编程能力,优化算法设计。
65 9
 算法系列之数据结构-二叉树
|
2月前
|
算法系列之数据结构-Huffman树
Huffman树(哈夫曼树)又称最优二叉树,是一种带权路径长度最短的二叉树,常用于信息传输、数据压缩等方面。它的构造基于字符出现的频率,通过将频率较低的字符组合在一起,最终形成一棵树。在Huffman树中,每个叶节点代表一个字符,而每个字符的编码则是从根节点到叶节点的路径所对应的二进制序列。
70 3
 算法系列之数据结构-Huffman树
|
2月前
|
算法系列之数据结构-二叉搜索树
二叉查找树(Binary Search Tree,简称BST)是一种常用的数据结构,它能够高效地进行查找、插入和删除操作。二叉查找树的特点是,对于树中的每个节点,其左子树中的所有节点都小于该节点,而右子树中的所有节点都大于该节点。
89 22
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
115 29
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
149 25
C 408—《数据结构》算法题基础篇—数组(通俗易懂)
408考研——《数据结构》算法题基础篇之数组。(408算法题的入门)
124 23
|
4月前
|
【C++数据结构——内排序】快速排序(头歌实践教学平台习题)【合集】
快速排序是一种高效的排序算法,基于分治策略。它的主要思想是通过选择一个基准元素(pivot),将数组划分成两部分。一部分的元素都小于等于基准元素,另一部分的元素都大于等于基准元素。然后对这两部分分别进行排序,最终使整个数组有序。(第一行是元素个数,第二行是待排序的原始关键字数据。本关任务:实现快速排序算法。开始你的任务吧,祝你成功!
84 7
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
134 2
|
6月前
|
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
150 58

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等