java实现快速排序

    科技2024-04-21  8

    什么是快速排序

    快速排序(Quicksort)使用分治思想对冒泡排序作了改进,效率非常高。其基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

    快速排序的实现

    从快速排序的基本思想可以分析出其实现思路: 一、选取一个枢轴元素(也叫基准元素) 二、将数组分割成两部分,一部分数据都小于或等于枢轴元素,另一部分数据都大于枢轴元素 三、对分割的子数组递归地执行步骤一二,直到无法分割

    快排代码实现 private static void quickSort(int[] arr, int start, int end) { // 递归终止条件 if (start >= end) { return; } // 第一步,找出分区后枢轴的下标,比如[2,1,3],枢轴为2,分区后枢轴的下标为1 int pivotIndex = partition(arr, start, end); // 第二步,对左子数组排序 quickSort(arr, start, pivotIndex - 1); // 第三步,对右子数组排序 quickSort(arr, pivotIndex + 1, end); }

    至此,快排的主干代码已经完成。接下来只要知道如何实现分区并返回枢轴元素的下标,我们的代码就可以完整实现,其实这才是实现快排要思考的核心问题

    如何实现分区并返回枢轴元素的下标? 这里总结两种方法

    挖坑填数 ① 图解分析 选取第一个元素3作为枢轴元素,并将其保存起来,int pivot = a[0]; 此时,我们可以认为在下标为0的地方挖了个坑【蓝色填充表示一个坑】;为了对坑有较为深刻的印象,这里提个简单的问题:如何将数组中元素a[3]放到a[0]所在的位置,而不丢失a[0]?那就是把a[0]先保存起来,可以定义个变量保存,可以将a[0]移动到其他位置,随便什么方式都行,然后a[0]=a[3]。所以这里的枢轴元素挖坑的方式是定义一个变量将枢轴元素保存起来,而后续其他位置元素挖坑的方式则是将其所在位置的元素移动到数组的其他位置(也就是坑所在位置) 定义两个指针,left指向数组左端,right指向数组右端; left用于从左往右扫描数组,寻找比pivot大的元素 right用于从右往左扫描数组,寻找比pivot小的元素 先从右往左开始扫描,寻找比pivot小的数准备填坑 left=0,right=6,此时a[right]<pivot,将a[right]填到a[left],并将left右移 此时数组变为 (对以上操作提两个问题: 1.为什么要先从右往左扫描 2.为什么要将left右移) 此时,right指向的是新的坑 转换扫描方向,从左往右扫描,寻找比pivot大的数准备填坑 left=1,right=6,a[left]<pivot,不满足条件,left右移 left=2,right=6,a[left]>pivot,满足条件,将a[left]填到a[right],并将right左移 此时数组变为 此时,left指向的是新的坑 转换扫描方向,从右往左扫描,寻找比pivot小的数准备填坑 left=2,right=5,a[right]>pivot,不满足条件,right左移 left=2,right=4,a[right]<pivot,满足条件,将a[right]填到a[left],并将left右移 此时数组变为 此时,right指向的是新的坑 转换扫描方向,从左往右扫描,寻找比pivot大的数准备填坑 left=3,right=4,a[left]>pivot,满足条件,将a[left]填到a[right],并将right左移 此时数组变为 此时,left=right,left和right同时指向的是新的坑 当left和right相遇时,pivot入坑 此时数组变为 此时pivot左边的数全部小于等于pivot,pivot右边的数全部大于pivot,一趟分区完成,对于子序列分区也是如此,重复上述步骤即可

    ② 思路总结 1、取a[0]=3为基准元素,将基准元素保存起来,此时相当于在a[0]挖了个坑。int pivot = a[0]; 2、定义两个扫描指针(引用),left指向数组左端,right指向数组右端 3、right从右往左扫描,如果a[right]<pivot,将a[right]填入a[0]并右移left,此时a[right]变成一个坑 如果a[right]>pivot,right继续左移,直到找到比基准元素小的数 4、left从左往右扫描,如果a[left]>pivot,将a[left]填入a[right]坑中并左移right,此时a[left]变成一个坑 如果a[left]<pivot,left继续右移,直到找到比基准元素大的数 5、当left=right,扫描完成 6、将基准元素填入坑中,此时基准元素左边的数全部小于基准元素,基准元素右边的数全部大于基准元素,分区完成 ③ 代码实现

    private static int partition(int[] arr, int start, int end) { // 确定枢轴元素 int pivot = arr[start]; // 定义两个指针(引用),一个指向数组左端,一个指向数组右端 int left = start; int right = end; while (left < right) { // 从右往左扫描,寻找比枢轴元素小的,并填入坑中 while (left < right && arr[right] >= pivot) { right--; } if (left < right) { arr[left++] = arr[right]; } // 从左往右扫描,寻找比枢轴元素大的,并填入新坑中 while (left < right && arr[left] < pivot) { left++; } if (left < right) { arr[right--] = arr[left]; } } // 扫描完成后,将枢轴元素填入新坑中 arr[left] = pivot; return left; }

    左右指针 ① 图解分析 选取第一个元素为枢轴元素 int pivot=3 定义两个指针(引用) left从左往右扫描,寻找大于pivot的数,left不必扫描pivot right从右往左扫描,寻找小于等于pivot的数 先从左往右扫描,直到a[left]>pivot 再从右往左扫描,直到a[right]<=pivot 交换a[left]和a[right] 此时,a[left]>pivot,a[right]<pivot,交换a[left]和a[right] 数组变为 继续扫描 此时,a[left]>pivot,a[right]<pivot,交换a[left]和a[right] 数组变为 继续从左往右扫描l eft=4,a[left]>pivot,满足条件,停止扫描 从右往左扫描,right=3,a[right]<pivot,满足条件,停止扫描 此时left>right,不做交换,退出扫描 交换枢轴元素和a[right] 此时pivot左边的数全部小于等于pivot,pivot右边的数全部大于pivot,一趟分区完成,对于子序列分区也是如此,重复上述步骤即可 ② 思路总结

    ③ 代码实现

    private static int partition(int[] arr, int start, int end) { // 枢轴元素 int pivot = arr[start]; // 左指针,用于从左往右扫描元素 int left = start + 1; // 右指针,用于从右往左扫描元素 int right = end; while (true) { // 从左往右扫描,寻找比枢轴大的元素 while (arr[left] <= pivot) { left++; // 防止越界 if (left == end) break; } // 从右往左扫描,寻找比枢轴小的元素 while (arr[right] > pivot) { right--; if (right == start) break; } if (left >= right) break; // 如果left<right, 交换a[left]和a[right] swap(arr, left, right); } // 交换轴枢和right所在位置元素 swap(arr, start, right); return right; } private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }

    总结

    以上快速排序的实现,还有很大的优化空间 比如,基准元素的选择 比如,当序列较短时,使用插入排序更高效 由于时间的关系,本篇暂告一段落

    参考

    白话经典算法系列之六 快速排序 快速搞定 快速排序(三种算法实现和非递归实现) 百度百科

    Processed: 0.020, SQL: 8