二种快排稳定实现

三路快排

voidquickSort(int[] a, int left, int right) {

    if (right <= left)

        return;

 

    /*

    * 工作指针

    * p指向序列左边等于pivot元素的位置

    * q指向序列右边等于Pivot元素的位置

    * i指向从左向右扫面时的元素

    * j指向从右向左扫描时的元素

    */

    int p, q, i, j;

    int pivot;// 锚点

    i = p = left;

    j = q = right -1;

    /*

    * 每次总是取序列最右边的元素为锚点

    */

    pivot =a[right];

    while (true) {

        /*

         * 工作指针i从右向左不断扫描,找小于或者等于锚点元素的元素

         */

        while (i < right && a[i] <=pivot) {

            /*

             * 找到与锚点元素相等的元素将其交换到p所指示的位置

             */

            if (a[i] == pivot) {

               swap(a, i, p);

               p++;

            }

            i++;

        }

        /*

         * 工作指针j从左向右不断扫描,找大于或者等于锚点元素的元素

         */

        while (left <= j && a[j] >=pivot) {

            /*

             * 找到与锚点元素相等的元素将其交换到q所指示的位置

             */

            if (a[j] == pivot) {

               swap(a, j, q);

               q--;

            }

            j--;

        }

        /*

         * 如果两个工作指针i j相遇则一趟遍历结束

         */

        if (i >= j)

            break;

 

        /*

         * 将左边大于pivot的元素与右边小于pivot元素进行交换

         */

        swap(a, i,j);

        i++;

        j--;

    }

    /*

    * 因为工作指针i指向的是当前需要处理元素的下一个元素

    * 故而需要退回到当前元素的实际位置,然后将等于pivot元素交换到序列中间

    */

    i--;

    p--;

    while (p >= left) {

        swap(a, i,p);

        i--;

        p--;

    }

    /*

    * 因为工作指针j指向的是当前需要处理元素的上一个元素

    * 故而需要退回到当前元素的实际位置,然后将等于pivot元素交换到序列中间

    */

    j++;

    q++;

    while (q <= right) {

        swap(a, j,q);

        j++;

        q++;

    }

 

    /*

    * 递归遍历左右子序列

    */

    quickSort(a,left, i);

    quickSort(a, j,right);

}

 

privatevoidquick(int[] a) {

    if (a.length > 0) {

       quickSort(a, 0, a.length - 1);

    }

}

 

privatevoidswap(int[] arr, int a, int b) {

    int temp = arr[a];

    arr[a] =arr[b];

    arr[b] = temp;

}


双关键字快排

其实如果理解了快排的原理,双关键字快排其实是很简单的。

因为快排的思想是在待排序序列中选取一个记录,让它左边的都小于等于它,右边的都大于等于它,如此递归。

那么双关键字的思想就顺其自然:在待排序序列中选取一个记录,让它左边的第一关键字小于它,或者第一关键字等于它但是第二关键字小于它;右边的第一关键字大于它,或者第一关键字等于它但是第二关键字大于它,如此递归。(读起来有点绕,但是对照上一句仔细读读还是可以读懂的)

下面附上代码:

 1 #include<stdio.h>

 2 #include<stdlib.h>

 3

 4void myqsort(int *a,int *b,int left,int right)

 5 {

 6     inti=left;

 7     intj=right;

 8     intmid=a[(left+right)/2];

 9     intmidb=b[(left+right)/2];

10     int temp;

11     while (i<j)

12     {

13         while(a[i]<mid||a[i]==mid&&b[i]<midb) i++;

14         while(a[j]>mid||a[j]==mid&&b[j]>midb) j--;

15         if (i<=j)

16         {

17             temp=a[i];

18             a[i]=a[j];

19             a[j]=temp;

20             temp=b[i];

21             b[i]=b[j];

22             b[j]=temp;

23             i++;

24             j--;

25         }

26     }27     if(i<right) myqsort(a,b,i,right);

28     if (left<j) myqsort(a,b,left,j);

29 }

30

31intmain()

32 {

33     int n;

34     scanf("%d",&n);

35     int i;

36     int *a=(int *)malloc(n*sizeof(int));

37     int *b=(int *)malloc(n*sizeof(int));   

38     for (i=0;i<n;i++)

39     {

40           scanf("%d",a+i);

41           *(b+i)=i;

42       }

43       myqsort(a,b,0,n-1);

44       for(i=0;i<n;i++)

45     {

46         printf("%d ",*(a+i));

47     }

48     free(a);

49     free(b);

50     return0;

51 }

其实这个程序就把不稳定的快排变成了稳定的快排了!(因为添加了一个关键字,对于相等的就实现了本来在前面的还在前面)


时间复杂度推导

在最优情况下,Partition每次都划分得很均匀,如果排序n个关键字,其递归树的深度就为.log2n.+1(.x.表示不大于x的最大整数),即仅需递归log2n次,需要时间为T(n)的话,第一次Partiation应该是需要对整个数组扫描一遍,做n次比较。然后,获得的枢轴将数组一分为二,那么各自还需要T(n/2)的时间(注意是最好情况,所以平分两半)。于是不断地划分下去,我们就有了下面的不等式推断。

1.    T(n)≤2T(n/2) +n,T(1)=0  

2.    T(n)≤2(2T(n/4)+n/2) +n=4T(n/4)+2n  

3.    T(n)≤4(2T(n/8)+n/4) +2n=8T(n/8)+3n  

4.    ……  

5.    T(n)≤nT(1)+(log2n)×nO(nlogn) 

也就是说,在最优的情况下,快速排序算法的时间复杂度为O(nlogn)

在最坏的情况下,待排序的序列为正序或者逆序,每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。如果递归树画出来,它就是一棵斜树。此时需要执行n‐1次递归调用,且第i次划分需要经过n‐i次关键字的比较才能找到第i个记录,也就是枢轴的位置,因此比较次数为 ,最终其时间复杂度为O(n2)

 

快速排序平均需要大约2NlnN次比较,来对长度为n的排序关键字唯一的序列进行排序。 证明也比较简单:假设CN为快速排序平均花在比较上的时间,初始C0=C1=0,对于N>1的情况,有:其中N+1是分割时的比较次数, 表示将序列分割为0,和N-1左右两部分的概率为1/N, 划分为1,N-2左右两部分的概率也为1/N,都是等概率的。然后对上式左右两边同时乘以N,整理得到:

然后,对于N为N-1的情况:

两式相减,然后整理得到:

然后左右两边同时除以N(N+1),得到:

可以看到,这是一个递归式,我们将 递归展开得到:

然后处理一下得到:

就空间复杂度来说,主要是递归造成的栈空间的使用,最好情况,递归树的深度为log2n,其空间复杂度也就为O(logn),最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n),平均情况,空间复杂度也为O(logn)。

可惜的是,由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。

 

 



<think>好的,用户想了解如何用Java实现快速排序算法。首先,我需要回忆一下快速排序的基本原理。快速排序是基于分治法的,选择一个基准元素,将数组分成两部分,一部分比基准小,另一部分比基准大,然后递归地对这两部分进行排序。 接下来,我得考虑Java的具体实现。通常,快速排序有递归和非递归两种方式,但用户可能更关注递归实现,因为这是基础。根据引用[2]和[4],基准的选择可以是最后一个元素或者第一个元素,这里可能需要说明不同的选择方式对性能的影响,比如基准选择不当可能导致最坏情况的时间复杂度。 然后,分区的实现是关键。需要遍历数组,将小于基准的元素移到左边,大于的移到右边。这个过程中,双指针法比较常见,比如i和j指针,i记录小于基准的边界,j遍历数组,交换元素的位置。这个过程需要详细解释,可能用户会困惑如何具体移动指针。 另外,引用[1]提到优化方法,比如三数取中法选择基准,或者在小数组时切换成插入排序。虽然用户主要问的是实现,但可能后续问题会涉及优化,这里可以简要提及,但重点放在基础实现上。 考虑到用户给出的示例中有Python代码,Java的实现需要结构清晰,包含递归函数和分区函数。代码注释也很重要,帮助用户理解每一步的作用。例如,引用[2]中的代码选择最后一个元素作为基准,然后进行分区,再递归处理左右子数组。 可能需要注意边界条件,比如数组长度为0或1的情况,递归终止条件是否正确。同时,测试用例也是必要的,用户可能想知道如何验证代码的正确性,比如使用示例数组进行排序测试。 最后,用户可能对时间复杂度和稳定性感兴趣,需要简要说明快速排序的平均时间复杂度是$O(n \log n)$,最坏情况是$O(n^2)$,以及如何通过优化避免最坏情况。不过根据用户的问题,这部分可能属于后续的相关问题,但可以在回答中稍微提及,以展示全面性。 总结步骤:1. 确定基准;2. 分区操作;3. 递归排序左右子数组。代码结构包括主函数、递归函数和分区函数。需要确保代码正确,并解释关键步骤,比如分区的逻辑和指针移动。</think>以下是用Java实现快速排序算法的分步讲解: ### 一、基础实现 ```java public class QuickSort { public static void sort(int[] arr) { quickSort(arr, 0, arr.length - 1); } private static void quickSort(int[] arr, int low, int high) { if (low < high) { int pivotIndex = partition(arr, low, high); quickSort(arr, low, pivotIndex - 1); // 排序左半区 quickSort(arr, pivotIndex + 1, high); // 排序右半区 } } private static 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; } private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } ``` ### 二、关键步骤解析 1. **基准选择**:示例代码选择数组最后一个元素作为基准(`arr[high]`)[^2] 2. **分区过程**: - 使用双指针`i`和`j`遍历数组 - `i`标记小于基准元素的边界位置 - 当`arr[j] < pivot`时,将元素交换到左半区 3. **递归排序**:将数组分为基准左侧(较小元素)和右侧(较大元素)后,分别递归处理 ### 三、优化建议 1. **三数取中法**:选择首、中、尾三个元素的中值作为基准,避免最坏情况$O(n^2)$时间复杂度[^1] 2. **小数组优化**:当子数组长度小于设定阈值(如10)时,改用插入排序 3. **尾递归优化**:减少递归调用栈深度 ### 四、使用示例 ```java public static void main(String[] args) { int[] arr = {9, 7, 5, 11, 12, 2, 14, 3, 10, 6}; QuickSort.sort(arr); System.out.println(Arrays.toString(arr)); // 输出: [2, 3, 5, 6, 7, 9, 10, 11, 12, 14] } ``` ### 五、复杂度分析 - 平均时间复杂度:$O(n \log n)$ - 空间复杂度:$O(\log n)$(递归调用栈) - 不稳定排序算法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值