快速排序
快速排序其实存在有两个版本
版本1 Lomuto版本
Lomuto分区方案
- 基准选择
- lomuto分区通常选择数组的最后一个元素作为基准值
- 效率
- lomuto易于实现,但是比Hoare方法效率低,特别是在处理大量重复元素的数组时。
代码实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package sorting;
public class QuickSortLomuto { void swap(int[] nums, int i , int j){ int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; }
public 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; }
public void quickSort(int[] arr, int low, int high){ if (low < high) { int pi =partition(arr,low,high); quickSort(arr,low,pi-1); quickSort(arr,pi+1,high); } } }
|
版本2 Hoare版本
hoare分区
hoare版本是以双指针方法来执行分区,这样会比lomuto版本更少的元素转换
原理: 一般设置nums[left] 为基准,然后左指针往右走,走到大于基准的数停下,右指针往左走,走到小于基准的数停下,最后i==j时 与基准交换,然后开始继续递归
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| void swap(int[] nums, int i , int j){ int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } int partition(int[] nums, int left, int right){ int i = left, j = right; while (i < j){ while (i < j && nums[j] >= nums[left]){ j--; } while (i < j && nums[i] <= nums[left]){ i++; } swap(nums,i,j); } swap(nums,i,left); return i; } void quickSort(int[] nums, int left, int right){ if (left >= right){ return; } int pivot = partition(nums,left,right); quickSort(nums,left,pivot-1); quickSort(nums,pivot+1,right); }
|
版本3 Hoare版本优化
原因 快速排序在某些输入下的时间效率可能降低。举一个极端例子,假设输入数组是完全倒序的,由于选择最左端元素为基准,那么哨兵在划分完之后,基准数被交换到最后端,导致左子数组长度为n-1 如此递归,每轮哨兵规划完成之后,右子数组的长度都为0
解决方案 优化哨兵划分中的基准数选取策略
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13
| int medianThree(int[] nums, int left, int mid , int right){
if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) return left; else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) return mid; else return right; }
|
哨兵实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int partitionThree(int[] nums, int left, int right){ int med = medianThree(nums,left,(left+right)/2,right); swap(nums,left,med); int i = left, j = right; while (i < j){ while (i < j && nums[j] >= nums[left] ) j--; while (i < j && nums[i] <= nums[left]) i++; swap(nums,i,j); } swap(nums,i,left); return i; }
|
hoare版本 尾递归优化
原因 在某些输入下,快速排序可能占用的额空间较多。以完全倒序的输出数组为例子,由于每轮哨兵划分后右子数组长度为0,递归树的高度会达到n-1,此时需要占用O(n)大小的栈帧空间
为了防止栈帧空间的积累,我们可以在每轮哨兵排序后,比较两个子数组的长度,仅对较短的子数组进行递归。由于较短子数组的长度不会超过 𝑛/2 ,因此这种方法能确保递归深度不超过 log 𝑛 ,从而将最差空 间复杂度优化至 𝑂(log 𝑛) 。
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void quickSotrWei(int[] nums, int left, int right){ if (left >= right) return;
while (left < right){ int pivot = partitionThree(nums,left,right); if (pivot - left < right - pivot){ quickSort(nums,left,pivot-1); left = pivot+1; }else { quickSort(nums,pivot+1,right); right = pivot -1; } } }
|