时间:O(N*lgn)->最坏n^2(有序,逆序)
空间:logN N*2
Hoare
Hoare法与其他快速排序算法的不同之处在于它使用两个指针(分别指向数组的起始位置和结束位置),并通过交换元素的方式来确定基准值的最终位置。
具体步骤如下:
- 选择一个基准值,通常是待排序数组的第一个元素。
- 设定两个指针,一个指向数组的起始位置,称为
left
,另一个指向数组的结束位置,称为right
。- 从
right
开始,向左移动直到找到一个小于等于基准值的元素。从left
开始,向右移动直到找到一个大于等于基准值的元素。- 如果
left
指针小于等于right
指针,则交换这两个元素,然后分别将left
和right
指针向后、向前移动一位。- 重复,直到
left
指针大于right
指针。- 将基准值与
right
指针所指向的元素交换,此时基准值左边的元素都小于等于它,右边的元素都大于等于它。- 对基准值左边和右边的子数组分别递归执行上述步骤,直到每个子数组只有一个元素或为空。
public static int partition2(int[] nums,int left,int right){//Hoare int temp=nums[left]; int i=left; while(left<right){ while(left<right&&nums[right]>=temp){ right--; } while(left<right&&nums[left]<=temp){ left++; } int t=nums[left]; nums[left]=nums[right]; nums[right]=t; } int t=nums[i]; nums[i]=nums[left]; nums[left]=t; return left; }
挖坑
具体步骤如下:
- 选择一个基准值,通常是待排序数组的第一个元素。
- 从数组的右端开始,设定一个指针
right
指向最右边的元素,然后从右往左扫描数组,找到第一个小于基准值的元素,将该元素填入基准值所在的位置,即将基准值挖空形成一个坑。- 接着从数组的左端开始,设定一个指针
left
指向最左边的元素,然后从左往右扫描数组,找到第一个大于基准值的元素,将该元素填入上一步挖空的坑中,并将该元素所在的位置再次挖空。- 重复步骤2和步骤3,直到
left
和right
指针相遇。- 将基准值填入最后一个挖空的坑中,此时基准值左边的元素都比它小,右边的元素都比它大。
- 对基准值左边和右边的子数组分别递归执行上述步骤,直到每个子数组只有一个元素或为空。
挖坑法快速排序的核心思想是通过挖空和填坑的方式,将待排序数组划分为左右两个部分,左边部分的元素都小于基准值,右边部分的元素都大于基准值,然后对左右子数组进行递归排序,最终完成整个数组的排序。
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 86180
* Date: 2023-10-28
* Time: 1:38
*/
public class sort {
public static void quickSort(int[] nums){
quick(nums,0,nums.length-1);
}
public static void quick(int[] nums,int start,int end){
if(start>=end){//1 2 3 4 s=0,e=-1
return;
}
int pivot=partition(nums,start,end);
quick(nums,start,pivot-1);
quick(nums,pivot+1,end);
}
public static int partition(int[] nums,int left,int right){
int temp=nums[left];
while(left<right){
while(left<right&&nums[right]>=temp){//>=是为了6,1,2,6的情况,不加=会导致6和6一直在交换。
right--;
}
nums[left]=nums[right];
while(left<right&&nums[left]<=temp){//left<right是解决1,2,3的情况
left++;
}
nums[right]=nums[left];
}
nums[left]=temp;//l和r相遇。
System.out.println(Arrays.toString(nums));
return left;//相遇位置就是基准。
}
public static void main(String[] args) {
int[] nums={6,1,2,7,9,3,4,5,10,8};
quickSort(nums);
System.out.println(Arrays.toString(nums));
}
}
每次进行快排的结果
[5, 1, 2, 4, 3, 6, 9, 7, 10, 8] [3, 1, 2, 4, 5, 6, 9, 7, 10, 8] [2, 1, 3, 4, 5, 6, 9, 7, 10, 8] [1, 2, 3, 4, 5, 6, 9, 7, 10, 8] [1, 2, 3, 4, 5, 6, 8, 7, 9, 10] [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
问题
为啥左边作为基准,就要从右边先开始?
就比如我们把Hoare中的代码,
while (left < right && nums[left] <= temp) { left++; }放在
while (left < right && nums[right] >= temp) { right--; }
前面就会出问题得到不是我们想要的代码
6,1,2,7,9,3,4,5,10,8
如果我们先移动 `left` 指针,那么 `left` 指针会一直向右移动,直到找到一个比基准值大的元素或者 `left` 和 `right` 指针相遇。这样会导致 `left` 指针停在一个比基准值大的位置上,而 `right` 指针可能停在一个比基准值小的位置上,违反了我们的划分目标。
第一次快排,最后r指针最后会停在一个比6大地方,l++,会导致l和r相遇,此时nums[l]=nums[r]>6,交换6和9就导致了左边出现比6大的数字
为啥还要对快排进行优化
对快速排序进行优化的主要目的是提高其在各种情况下的性能和效率。尽管快速排序通常是一种高效的排序算法,但在某些情况下,它可能会出现性能下降的情况。下面是一些导致性能下降的情况:
已排序或近乎有序的数组:如果输入数组已经是有序的或近乎有序的,传统的快速排序算法可能会导致分割不均匀,使得递归调用的层数变多,从而降低了性能。
大量重复元素的数组:当待排序的数组包含大量重复元素时,传统的快速排序算法可能会导致分割不均匀。这是因为传统算法只考虑基准元素的大小,而没有考虑相等元素的情况,从而导致划分不均匀,影响性能。
小规模子数组:当待排序的子数组变得很小(通常小于一定阈值)时,快速排序的递归调用开销可能会超过插入排序的开销。在这种情况下,使用插入排序或其他简单排序算法可能更加高效。
递归调用栈溢出:传统的快速排序算法使用递归来处理子数组,当待排序的数组规模非常大时,递归调用栈可能会溢出,导致程序崩溃。这种情况可以通过尾递归优化或迭代方式实现来解决。
以上情况中的任何一种都可能导致快速排序的性能下降。为了克服这些问题,可以采取相应的优化措施,如随机选择基准元素、三数取中法。
三数取中
private static void sort1(int[] array,int start,int end){
//防止只有左子树或者右子树的情况
if(start >= end){
return;
}
//在找基准之前,解决划分不均匀的问题,将关键值改变为中间大小的值后,能解决单分支的情况
int index = findMidValOfIndex(array,start,end);
swap(array,start,index);
int povit = partion(array,start,end);
sort(array,start,povit-1);
sort(array,povit+1,end);
}
/*
找到中位数
*/
private static int findMidValOfIndex(int[] array,int start,int end){
int midIndex = (start+end)/2;
if(array[start] < array[end]){
if(array[midIndex] < array[start]) {
return start;
} else if (array[end] < array[midIndex]) {
return end;
}
else {
return midIndex;
}
}
else {
if (array[midIndex] > array[start]){
return start;
} else if (array[midIndex] < array[end]) {
return end;
}
else {
return midIndex;
}
}
}
随机选基准
private static void sort2(int[] array,int start,int end){
//防止只有左子树或者右子树的情况
if(start >= end){
return;
}
if(( end-start+1) <= 15){
//插入排序减少后几层 的递归
insertSort1(array,start,end);
}
//在找基准之前,解决划分不均匀的问题,将关键值改变为中间大小的值后,能解决单分支的情况
int index = findMidValOfIndex(array,start,end);
swap(array,start,index);
int povit = partion(array,start,end);
sort(array,start,povit-1);
sort(array,povit+1,end);
}
public static void insertSort1(int[] array,int left,int right){
for (int i = left+1; i <= right ; i++) {
int j = i-1;
int tmp = array[i];
for (; j >= left ; j--) {
if(array[j] > array[i]){
array[j+1] = array[j];
}
else{
break;
}
}
array[j+1] = tmp;
}
}
非递归实现快排
import java.util.Stack;
public class QuickSortNonRecursive {
public static void quickSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
Stack<Integer> stack = new Stack<>();
stack.push(0);
stack.push(arr.length - 1);
while (!stack.isEmpty()) {
int end = stack.pop();
int start = stack.pop();
int pivotIndex = partition(arr, start, end);
if (pivotIndex - 1 > start) {
stack.push(start);
stack.push(pivotIndex - 1);
}
if (pivotIndex + 1 < end) {
stack.push(pivotIndex + 1);
stack.push(end);
}
}
}
private static int partition(int[] arr, int start, int end) {
int pivot = arr[end];
int i = start - 1;
for (int j = start; j < end; j++) {
if (arr[j] < pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, end);
return i + 1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {5, 2, 9, 1, 7, 6, 8};
quickSort(arr);
for (int num : arr) {
System.out.print(num + " ");
}
}
}