本文主要介绍了三个排序算法的思想原理和Java代码实现
快速排序
快排序的一个优点是其原地排序的特性,通过反复的交换元素,直接在数组中进行操作,只需要分配较少的内存来用于中间计算。
最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)
算法思想
快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立地排序。
通过划分成多个子部分进行排序,这个子部分的任务就是根据一个“基准(pivot)元素”对数组进行划分。
步骤1:选择一个基准元素。从数组中选择一个元素,把它作为基准元素。
步骤2:根据这个基准元素重新排列数组。选择了基准元素p之后,下一个任务就是对数组中的元素进行排列,使数组中p之前的所有元素都小于p,在p之后的所有元素都大于p。例如,根据上面的输入数组,下面是一种合理的重新排列元素的方式:
例如对左边的数组进行一次快排:
这样一次排序过后的时间复杂度是O(n);但是这样排完序后数组还不是有序的,接下来才是快排的核心步骤:
对于已经划分好的区域: 左边的小于基准元素的A区域+基准值+右边大于基准值的B区域;
下一步对于左边的A区域和右边的B区域进行一个递归调用,即先进行划分区域,在进行递归调用,这点和归并排序恰好相反;
如上面的例子对于小于3的部分进行一次递归调用后,其选择基准值如果是1则变成了1,2,此时对于2无法再次细分则递归结束,而对于右边的若以6为基准值,结果变为4,5 和7,8;两个部分,然后还要对于两边进行一个二次递归,因为此时还可以细分;在进行这一次后结果就排好了;
再次梳理一下快排的详细步骤:
1、选择基准值:此处需要选出一个用于划分的基准值;
如何选择,可以直接选择子数组第一个,也可以随机选择;
所以此处可以采用一种可递归调用的函数,需要通过接收待排子数组选出一个下标值返回;
基准元素本身处于正确的位置,意味着它在排序之后的输入数组中也是处于相同的位置(所有小于它的元素在它之前,所有大于它的元素在它之后)。并且,这种划分把排序问题分割成两个更小的子问题:对小于基准元素的元素进行排序(它们很方便地在自己的子数组中原地排序)以及对大于基准元素的元素进行排序(也是在它们自己的子数组中原地排序)。
2、划分数组;对于基准值和数组,需要对于该数组进行一个划分,将分出的大于基准值和小于基准值的分开;
常见的一种方式是使用一个双指针的操作方式:
(有一种是加入左右指针的方式,我这里介绍前后指针)对于待分的数组首尾加上一个前后指针,随后对于指针指向的元素进行一个比较,其中一个指针用于维护小于基准值的边界,而另一个指针用于去控制需要交换的元素;以一个例子说明
当指针均指向8时,此时8>3,所以j需要向后移动,而i不动。
当遇到2时,因为2<3,所以“8”与“2”进行交换,同时把i的值加1,这样i就位于“2”和“8”之间,
当遇到5时,5>3,所以j向后移动一位;
当遇到1时,此时又需要进行一次交换,只要把“1”与大于基准的第一个元素(“8”)进行交换并把i的值加1;
这样当遍历完成后,得到的i指向的就是两个区域的划分切点;将基准元素与这个i指针前的一个元素进行一个交换即可;
这个函数需要进行的操作是进行子数组的划分;当然也只需要返回一个基准位置即可;
还有一种不常见的,如果不在意空间可以选用这种稍好理解的方式:对输入数组A进行一遍扫描,并把它的非基准元素逐个复制到一个相同长度的新数组B中,小于基准p的元素复制到数组B的前面,大于基准p的元素复制到数组B的后面。在处理完了所有的非基准元素之后,就可以把基准元素复制到数组B中剩下的那个位置。
3、最后是递归的操作
需要做到的是一个通过前面的信息得到了划分的基准值和子数组,对于子数组又需要重新调用前面两个方法;
算法的复杂度受基准值的影响;
算法的优化
1、基准值的随机化选择
快排在它的递归调用之外所完成的主要工作发生在(1)基准值和(2)划分子程序中。我们假设前者的运行时间是Θ (n)。如果使用了中位元素作为基准元素,就可以实现输入数组的完美划分,每个递归调用最多对不超过n/2个元素的子数组进行操作。这样可以减少递归的次数;
选择子数组的第一个元素作为基准元素只耗时O(1),但可能导致QuickSort的运行时间高达Θ (n2)。选择中位元素作为基准元素可以保证总体运行时间为Θ (n log n),但这样会在选择基准元素时消耗的时间太多(如果仍然是线性时间)。有一种简单和轻量级的方法用于选择一个基准元素,使其能够实现数组划分的大致平衡,该思路的关键是使用随机化。
因为无法通过快速的选出一个中位数,所以选择使用随机的方式来提高效率,随机选择基准元素往往比固定第一个更好;随机化的QuickSort中需要非常好的运气才会选到中位元素(n分之一的概率),但是选中一个近似中位元素却不需要太多的运气;它接近于50的概率;
另外一种选择基准值的方式,快 速 三 向 切 分。(J. Bently,D. McIlroy) 用 将重复元素放置于子数组两端的方式实现一个信息量最优的排序算法。使用两个索引 p 和 q,使得a[lo…p-1] 和 a[q+1…hi] 的 元 素 都 和 a[lo]相等。使用另外两个索引 i 和 j,使得 a[p…i-1]小于 a[lo],a[j+i…q] 大于 a[lo]。在内循环中加入代码,在 a[i] 和 v 相当时将其与 a[p] 交换(并将 p 加 1),在 a[j] 和 v 相等且 a[i] 和a[j] 尚未和 v 进行比较之前将其与 a[q] 交换。添加在切分循环结束后将和 v 相等的元素交换到正确位置的代码
2、对于小数组的排序方式切换
对于小数组,插入排序比快速排序效率更高,因此如果是小数组可以尝试切换插入排序方式;
代码实现
public int[] quickSort(int[] nums,int left,int right) {
// 0个或1个元素的子数组
if(right<=left){
return nums;
}
//找到基准值移到子数组最左边;
int pivot= new Random().nextInt(right-left+1)+left;
swap(nums,left,pivot);
//新一次的切分点
int n = partition(