排序算法 - 快速排序
排序算法 - 快速排序
概要
快速排序(Quicksort)是对冒泡排序算法的一种改进。快速排序是一种基于分而治之的排序算法。
它的基本思想是: 选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
基准元素的选择,以及元素的交换,都是快速排序的核心问题。
一、基准元素的选择
最简单的方式是选择第1个元素。但是针对特殊情况比如原本逆序的数列:{8,7,6,5,4,3,2,1},期望排列成顺序数列,整个数列并没有被分成两半。如何避免这种情况?
解决办法:随机选择一个基准元素,并且让基准元素和数列首元素交换位置。
二、元素的交换
选定了基准元素以后,我们要做的就是把其他元素中小于基准元素的都交换到基准元素一边,大于基准元素的都交换到基准元素的另一边。具体实现有两种方法。
1. 双边循环法
以[4,7,5,2,6,1,3] 为例,下面讲解具体实现过程:
1)选定基准元素pivot,并且设置两个指针left(下图中的i)和right (下图中的j),指向数列的最左和最右两个元素。如下图:
2)接下来进行第1次循环,从right指针开始,让指针所指向的元素和基准元素做比较,如果大于等于pivot,则指针向左移动。如果小于pivot,则right指针停止移动,切换到left指针。
3)轮到left指针,让指针所指向的元素和基准元素做比较,如果小于等于pivot,则指针向右移动。如果大于pivot,则left指针停止移动。 让left和right指针所指向的元素进行交换。
4)接下来进入第2次循环,重新切换到right指针,向左移动。按照这个思路继续往下,
最后,等到left指针和right指针重合的时候,将left指针所指向的元素和基准元素所在位置的元素进行交换。返回left指针所在的位置,这个就是基准元素的位置,根据基准元素,分成两部分递归排序。
如下图:
下面是用java来实现的参考代码:
1 public class QuickSort { 2 public static void quickSort(int[] arr, int startIndex, int endIndex) { 3 // 递归结束条件:startIndex大等于endIndex的时候 4 if (startIndex >= endIndex) { 5 return; 6 } 7 // 得到基准元素位置 8 int pivotIndex = partition(arr, startIndex, endIndex); 9 // 根据基准元素,分成两部分递归排序 10 quickSort(arr, startIndex, pivotIndex - 1); 11 quickSort(arr, pivotIndex + 1, endIndex); 12 } 13 14 /** 15 * 分治(双边循环法) 16 * 17 * @param arr 待交换的数组 18 * @param startIndex 起始下标 19 * @param endIndex 结束下标 20 */ 21 private static int partition(int[] arr, int startIndex, int endIndex) { 22 // 取第一个位置的元素作为基准元素(也可以选择随机位置) 23 int pivot = arr[startIndex]; 24 int left = startIndex; 25 int right = endIndex; 26 27 while (left != right) { 28 //控制right指针比较并左移 29 while (left < right && arr[right] >= pivot) { 30 right--; 31 } 32 //控制left指针比较并右移 33 while (left < right && arr[left] <= pivot) { 34 left++; 35 } 36 //交换left和right指向的元素 37 if (left < right) { 38 int p = arr[left]; 39 arr[left] = arr[right]; 40 arr[right] = p; 41 } 42 } 43 44 //pivot和指针重合点交换 45 arr[startIndex] = arr[left]; 46 arr[left] = pivot; 47 48 return left; 49 } 50 51 public static void main(String[] args) { 52 int[] arr = new int[]{4, 7, 3, 5, 6, 2, 8, 1}; 53 quickSort(arr, 0, arr.length - 1); 54 System.out.println(Arrays.toString(arr)); 55 } 56 }
2. 单边循环法
双边循环法从数组两边交替遍历元素,虽然更加直观,但是代码实现相对繁琐,而单边循环法则简单得多,只从数组的一边对元素进行遍历和交换。
mark指针代表小于基准元素的区域边界。
具体实现过程:
1)从基准元素的下一个位置开始遍历数组。
2)如果遍历到的元素值大于等于基准元素时,就继续往后遍历
3)如果遍历到的元素小于基准元素值时,则需要做两件事:
- 将mark指针右移1位,因为小于pivot的区域边界增大。
- 让最新遍历到的元素和mark指针所在位置的元素交换位置,因为最新遍历的元素归属于小于pivot的区域。
最后,将pivot元素交换到mark指针所在位置,返回mark指针所在的位置,这个就是基准元素的位置,根据基准元素,分成两部分递归排序。这一轮宣告结束。
下面是用java实现的参考代码:
1 public class QuickSort { 2 public static void quickSort(int[] arr, int startIndex, int endIndex) { 3 // 递归结束条件:startIndex大等于endIndex的时候 4 if (startIndex >= endIndex) { 5 return; 6 } 7 // 得到基准元素位置 8 int pivotIndex = partition(arr, startIndex, endIndex); 9 // 根据基准元素,分成两部分递归排序 10 quickSort(arr, startIndex, pivotIndex - 1); 11 quickSort(arr, pivotIndex + 1, endIndex); 12 } 13 14 /** 15 * 分治(单边循环法) 16 * 17 * @param arr 待交换的数组 18 * @param startIndex 起始下标 19 * @param endIndex 结束下标 20 */ 21 private static int partition(int[] arr, int startIndex, int endIndex) { 22 // 取第一个位置的元素作为基准元素(也可以选择随机位置) 23 int pivot = arr[startIndex]; 24 25 //小于基准元素的区域边界 26 int mark = startIndex; 27 28 //从基准元素的下一个位置开始遍历数组 29 for (int i = startIndex + 1; i <= endIndex; i++) { 30 if (arr[i] < pivot) { 31 mark++; 32 int p = arr[mark]; 33 arr[mark] = arr[i]; 34 arr[i] = p; 35 } 36 } 37 38 arr[startIndex] = arr[mark]; 39 arr[mark] = pivot; 40 return mark; 41 } 42 43 public static void main(String[] args) { 44 int[] arr = new int[]{4, 7, 3, 5, 6, 2, 8, 1}; 45 quickSort(arr, 0, arr.length - 1); 46 System.out.println(Arrays.toString(arr)); 47 } 48 }
三、总结
1. 快速排序是从冒泡排序演变而来的,同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。
2. 是一种不稳定排序,平均时间复杂度是O(nlogn),最坏时间复杂度为O(n²),空间复杂度为O(logn)
3. 快速排序是很重要的算法,与傅里叶变换等算法并成为二十世纪十大算法
4. 核心问题:基准元素的选择以及元素的交换
四、快速排序与冒泡排序的区别
1. 排序方式
冒泡排序在每一轮只把1个元素冒泡到数列的一端。
而快速排序则在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分。
2. 使用场景
快速排序适合处理大规模数据,尤其是随机分布的数据。而冒泡排序适合小规模数据。总体来说,快速排序在效率和性能上通常优于冒泡排序,尤其在处理大数据时。
参考链接:https://algo.itcharge.cn/