快速排序
-
时间复杂度O(nlogn) 对于基本有序的数据是不友好的
-
空间复杂度依照具体算法而定
-
不稳定
-
思路
-
还是用到了递归的思路,在一趟中选择第一个(其实可以随机选择一个)作为基准pivot,然后对数据进行切分将通过切分,我们要达到这样一个效果:把「切分元素」放在排序以后最终应该呆的位置。【小于等于基准值的元素】基准值【大于等于基准值的元素】,然后对左边和右边再进行一次快速排序,递归地进行下去
-
看到这里你或许还不明白,玛徳这有什么用,接下来我来给大家好好说道说道,好好理解理解快速排序
-
首先你会发现快速排序和归并排序他们的思想都是分而治之(divide and conquer),好,那么什么是分而治之呢?
基本思路是将一个大问题拆解成某几个小问题,如果可以直接解决那么就解决,如果不能则继续拆解,直到可以直接解决; 举一个栗子:斐波那契数列
private static int fib(int n){ if(n<=1){ return 1; } return fib(n-1)+fib(n-2); }
要你直接计算第n项你不好计算,那么退而求其次,计算第n-1项和第n-2项递归地进行下去,直到n<=1,那么这样我们可以直接给出答案.
-
那么请你思考,归并排序和快速排序是什么样的思路呢,他们其实很相近都是划分大区间到小区间,直至区间的长度小于某个值,那么直接进行插入排序就好(因为插入排序在小区间上是性能优异的).
-
那么不同点是什么呢,归并排序无脑地将区间进行一划为二,但是因为这样不断划分,在最终完成插入排序后,不能保证元素在最终的位置上,所以需要再进行合并这一操作来保证元素呆在最终的位置上(再最坏的情况下,可能最后一次合并,才能保证元素呆在最终排好的位置上); 但是在快速排序的情况下结果就不是这样了,快速排序每次划分时都在区间内随机找一个数,然后对区间进行调整:【小于等于基准值的元素】基准值【大于等于基准值的元素】,这样在不断划分区间,直到区间的长度小于某个值进行插入排序,在这次的插入排序后就可以保证,这个区间元素的位置就在最终排好序的位置上,所以无需再进行合并这一操作.
private static final int INSERTION_SORT_THRESHOLD = 2; private static final Random RANDOM = new Random(); private static void quickSort(int[] nums, int left, int right) { if (right - left <= INSERTION_SORT_THRESHOLD) { insertSort(nums, left, right); return; } int pivotIndex = partition(nums, left, right); quickSort(nums, left, pivotIndex - 1); quickSort(nums, pivotIndex + 1, right); } private static int partition(int[] nums, int left, int right) { //随机摇出一个索引 int randomIndex = RANDOM.nextInt(right - left + 1) + left; //和第一个索引交换 swap(nums, left, randomIndex); int j = left; for (int i = left; i <= right; i++) { //把小于pivot换到前面去,大于的换到后面去 if (nums[i] < nums[left]) { j++; swap(nums, j, i); } } //j的位置即为pivot的最终位置,交换nums[j]和nums[left],这样我们随机选择的元素就被放到了它应该在的位置上,对其 //左右的区间再进行快速排序 swap(nums, left, j); return j; } private static void swap(int[] nums, int index1, int index2) { int temp = nums[index1]; nums[index1] = nums[index2]; nums[index2] = temp; } private static void insertSort(int[] nums, int left, int right) { for (int i = left; i <= right; i++) { int temp = nums[i]; int j = i; while (j > 0 && nums[j - 1] > temp) { nums[j] = nums[j - 1]; j--; } nums[j] = temp; } }
-
双路/三路快排
-
双路快排为什么出现
-
这其实是对快速排序基础版的优化,在上面的快速排序中存在一个问题,就是当数组的重复元素非常多,刚巧这个元素被随机选中会造成左右及其不平衡,算法的性能下降
-
举个栗子 {6,1,8,3,9,4,5,6,6,6,6,6,6,6,7} 如果选择6来作一次划分结果是这样的
[5, 1, 3, 4,] 6,[8, 9, 6, 6, 6, 6, 6, 6, 6, 7],会发现前面的部分过于短,而后面的部分过于长,造成递归树不平衡
-
-
三路快排为什么出现
-
其实这又是一层优化,请你思考一个问题如果划分成这样[5, 1, 3, 4,],[6, 6, 6, 6, 6, 6, 6,6],[8,9,7]
那么6这么多个元素是不是已经排定了不需要进行排序了
-
-
解决方法
很简单修改partition的逻辑即可
双路快排partition
private static int partition(int[] nums, int left, int right) { int index = RANDOM.nextInt(right - left + 1) + left; swap(nums,left,index); int pivot = nums[left]; //小区间指针从left+1开始 int lt=left+1; //大区间指针 int gt=right; //注意这里操作的区间是[left+1,right] while (true) { //这里已经进行了一次先加操作,指针会指向小区间第一个>=pivot的元素 while (lt <= right && nums[lt] < pivot) { lt++; } //指针会指向大区间的第一个<=pivot的元素 while (gt > left && nums[gt] > pivot) { gt--; } if (lt >= gt) { break; } swap(nums, lt, gt); lt++; gt--; } swap(nums, left, gt); return gt; }
三路快排
思路通过交换将小于pivot的放在左边,大于pivot的放在右边,等于pivot的放在中间,最终lt指向最大的小于pivot的元素,gt指向最小的大于pivot的元素
int index = RANDOM.nextInt(right - left + 1) + left; swap(nums, index, left); int pivot = nums[left]; int lt = left; int gt = right + 1; //注意细节,此时i在left前一位,gt同理 int i = left + 1; while (i < gt) { if (nums[i] < pivot) { //照应前文,先相加在交换 lt++; swap(nums, lt, i); i++; } else if (nums[i] == pivot) { i++; } else { gt--; swap(nums, gt, i); } } swap(nums, left, lt); //去掉中间重复的元素,大大减少了排序的区间 //由于最后把left与lt进行了交换所以lt此时指的是pivot,我们只要小于pivot的部分,所以减一 quickSort(nums, left, lt - 1); quickSort(nums, gt, right);
-
最后来个总结
-
归并排序和快速排序都是递归的排序算法,因为归并排序每次只是简单地一份为二,最后进行插入排序的时候,不能保证将元素放到最后拍好的位置,所以需要合并这一操作(可能最后一次合并后元素才会被排定),但是快速排序由于partition的关系,使得插入排序之后,元素就已经排定了,不需要合并的操作.
两路快排是因为在某次刚好选择了一个大量在数组中重复的元素,会使得右边的区间太长,递归树太高,性能下降,所以将重复的元素分到左右两边来平衡.
三路快排是因为与pivot相等的元素,其实位置已经可以确定了,不需要参与到下一次中,把两端摘出来就行
-