快速排序

快速排序

快速排序是对冒泡排序的改进,冒泡排序的缺陷就是一轮走完虽然也做了大量的交换,但是剩下的数据之间没有关系下一次还要再重新找一个最大的。而快速排序对它的改进是,一轮走完不会找到最大的值,而是采用了分治思想将数分为左小右大的两部分。然后这两部分再递归的进行这种轮,最终慢慢趋于有序。

快速排序.gif

基本思想

老韩思想

选取一个基准值,然后将所有的数据分为左右两部分,左边小于等于基准值,右边大于基准值。注意点,基准值是取出来做一个参照的,遍历的时候还是遍历整个数据集的,而不会跳过这个挑出来的基准值。额,感觉还是看看代码然后debug吧。

算法导论思想

这个就比较好理解了,接收参数quickSort(arr,p,r),arr就是需要排序的那个数组,arr = [p,.....,r],p和r就是这个数组的开始和结束索引。arr[r]也就是最后一个元素作为基准。然后设一个i等于p-1,这i就是指向小的那部分的最后一个的下标,由于一开始还没有所以指向p-1,就是表示溢出。开始循环for j = p to r - 1,这个意思就是循环p到r-1,要将它们处理分到小部分还是大部分,取出的那个下标用j来保存。每个j对应的数据取出来之后,如果它的值小于等于基准,那么就放入到小部分,大于基准就放入大部分。

需要知道以下,k是任意一个下标

  • p<=k<=i表示小于等于基准那部分
  • i大于基准的部分
  • j
  • k == r是基准,基准是不需要处理(分部分)的

最终,j == r的时候分好部分了,最后将基准和大部分的第一个值进行交换就能够完成一轮处理,然后可以进行递归。进行快排的元素小于等于1的时候就完成了排序。

代码实现

这里有两种实现思想

尚硅谷老韩

package _6_sort;
import java.util.Arrays;
import java.util.Date;
public class QuickSort {
    public static void quickSort(int[] data, int left, int right) {
        //取第一个数作为基准,基准随便选一个都可以的
        int pivot = data[left], l = left, r = right;
        int temp;
        while (l < r) {
            //找到左边一个大于等于基准的
            // 如果没有这个等于,可能基准就是最大值的话,
            // 然后找不到了(会一直找下去),还需要加一个限制不让下标溢出
            while (data[l] < pivot) l++;
            //找到右边一个小于等于基准的
            while (data[r] > pivot) r--;
            if (r <= l)
                //已经分好了,就两种情况,一:两边都找到了基准(说明基准左边都是小于等于基准的,右边都是大于等于基准的)
                // ,二:已经分好了
                break;
            //交换
            temp = data[r];
            data[r] = data[l];
            data[l] = temp;
            //注意:虽然两个都是符合的,而且不需要下次再试的了,但是如果两个同时移位的话,会出现问题,
            //就是比如说现在基准是4,l指向2,r指向了4,他们中间有个3,如果两个同时进行移位的话就出现
            //l、r同时指向一个不是基准的值,这样下面进行处理的时候就不能又两个同时移动了,也就是出现了两种情况
            //就又得if-else进行判断然后分别处理了,还得判断这个不是基准的值到底小于基准还是大于基准(麻烦)
            if (data[l] == pivot) r--;
            if (data[r] == pivot) l++;
            //为什么data[l] == pivot却要移动r,也就是为什么移动不是pivot那个值呢?
            //这也是为了想要等于跳出这个循环必须由上面的if来完成,保障等于的时候一定指向pivot
            //如果此时现在基准是4,l指向一个2,r指向一个4然后他们两个是挨着的,那移动了r的话
            //就直接l==r出去了,然后此时data[r]等于2,经过下面的处理必然又出错了

        }
        //如果此时r==l(此时这个位置肯定等于基准值,这个位置不需要再排序了,值是处于中间位置的),
        // 那么需要进行移位,不然递归的时候会出现两个部分重合的情况
        if (r == l) {
            r--;
            l++;
        }
        if (left < r) {//如果这一部分小于等于1的话就不用再递归了,直接是有序的
            quickSort(data, left, r);
        }
        if (right > l) {
            quickSort(data, l, right);
        }
    }
//测试代码
    public static void main(String[] args) {
//        int[] arr = RandomProducer.produceRandom(-500, 100000000, 8000000);
        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 8, 9, 1, 7, 2, 3, 5, 4, 8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
//        System.out.println("排序前");
//        System.out.println(Arrays.toString(arr));
        Date from = new Date();
        quickSort(arr, 0, arr.length - 1);
//        ShiftShellSort(arr);
//        System.out.println("排序后");
        System.out.println(Arrays.toString(arr));
        System.out.println("运行了" + (new Date().getTime() - from.getTime()) + "毫秒");
    }
}

算法导论第三版实现方式

package _6_sort.AlgorithmBooks;

import _6_sort.RandomProducer;

import java.util.Arrays;
import java.util.Date;

/**
 * author waigo
 * create 2021-01-28 11:31
 */
public class QuickSort {
    //partition(A,p,r),这个方法用来分开那个区域
    //quickSort(A,p,r),处理区域[p,...,r] E A
    public static int partition(int[] A, int p, int r) {
        int x = A[r];
        int i = p - 1;
        for (int j = p; j < r; j++) {
            if (A[j] <= x) {
                i++;
                exchange(A, i, j);
            }
        }
        exchange(A, i + 1, r);
        return i + 1;
    }

    public static void exchange(int[] A, int aIndex, int bIndex) {
        if(aIndex!=bIndex){
            int temp = A[aIndex];
            A[aIndex] = A[bIndex];
            A[bIndex] = temp;
        }
    }

    //A = [p,....,r]
    public static void quickSort(int[] A, int p, int r) {
        if (p < r) {
            int q = partition(A, p, r);
            quickSort(A, p, q - 1);
            quickSort(A, q + 1, r);
        }
    }
//测试代码
    public static void main(String[] args) {
        int[] arr = RandomProducer.produceRandom(-500, 100000000, 8000000);
//        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
//        System.out.println("排序前");
//        System.out.println(Arrays.toString(arr));
        Date from = new Date();
        quickSort(arr, 0, arr.length - 1);
//        ShiftShellSort(arr);
//        System.out.println("排序后");
//        System.out.println(Arrays.toString(arr));
        System.out.println("运行了" + (new Date().getTime() - from.getTime()) + "毫秒");
    }
}

可以看出,算法导论这种实现方式的好处是不需要考虑那么多条件,只要循环不变量设置好,后面就只要处理判断分边的问题了。经过测试,虽然实现的方式不同,但是由于算法的思想内核是相同的,所以这两种方式的速度不相伯仲。而快排比起希尔来说真的是又上了一个档次,我的电脑测试800,0000条数据的排序才用了1200ms左右。而移位式希尔排序800,0000条数据需要差不多2500ms。

不愧是快速排序,还真是名不虚传。不过我们这个一次只能处理完一个就是那个pivot,而和pivot相同的还得走下面的流程再次递归,所以可以改良。

上面讲的就是1.0版的快排,核心是"01partition"的操作,下面来介绍一个新玩意儿,荷兰国旗问题,就是将一个数组分为三块,左边小于pivot,中间等于pivot,右边大于pivot,这就是升级版的partition.


荷兰国旗.png

思想:还和partition一样整一个指针smallTop指向最小区的头,不过现在需要一个指针指向最大区的头bigLow,我们这里的pivot就取最后一个值,所以最大区的头一开始指向最后一个位置,若是pivot从外面传进来就可以指向r+1(r指的是处理区域的最右边)位置,要灵活一点。然后一个指针j从p位置(处理区域开始位置)开始遍历,若是小于pivot则进入小于区(j指针后移),等于pivot就直接后移,大于的时候需要注意,由于从大于区换下来的元素是还没看过的,所以要停在原地继续看这个新换来的。这里的循环结束条件和partition可不同,不能再设置为jpivot。嗯,由于我们这里设置r位置为pivot,所以swap(bigLow++,r);

show you my code.

/**
 * @param arr 待处理的数组
 * @param p 处理区域[p...r]
 * @param r
 * @return 下标0:小于x的top 下标1:大于x的low
 * 经过荷兰国旗处理,[p...smallTop] 小于x,[smallTop+1...bigLow-1]等于x,[bigLow...r]大于0
 */
public static int[] netherlangsFlag(int[] arr, int p, int r) {
    int smallTop = p - 1, bigLow = r;
    int j = p, pivot = arr[r];
    while (j < bigLow) {
        if (arr[j] < pivot) swap(arr, ++smallTop, j);
        else if (arr[j] > pivot) swap(arr, --bigLow, j--);//这个bigLow位置的元素还没看过,所以j需要留在原地再看一遍
        j++;//等于的时候直接移动j不用进行交换操作
    }
    swap(arr, bigLow++, r);
    return new int[]{smallTop, bigLow};
}

利用荷兰国旗操作升级快排2.0

/**
 * quickSort2.0版本,使用荷兰国旗操作进行优化,一次将等于x的全部搞定
 * @param arr
*/
public static void quickSort2(int[] arr) {
    process2(arr, 0, arr.length - 1);
}
public static void process2(int[] arr, int p, int r) {
    if (p < r) {
        int[] flag = netherlangsFlag(arr, p, r);
        int smallTop = flag[0], bigLow = flag[1];
        process2(arr, p, smallTop);
        process2(arr, bigLow, r);
    }
}

升级点,一次解决了等于pivot的一堆数据。这就是最经典的快排。

不过上面两个算法其实是O(N2)的算法,注意算法的时间复杂度是按最差情况来算的,假设一个算法直接就是有序的然后进行快排,那么每次处理完成的都是最后一个,然后剩下全部进入下一轮处理,这个时间复杂度就是O(N2),那么升级点就是让它不要每次都只是处理最后一个,而是随机挑一个位置做pivot,这好处就是下一轮两边可能数量是比较均衡的,这样就减少了递归的次数。

额,你可能会说了,既然算法按照最差情况来估时间复杂度,那最差情况能不能就是每次随机找到的都是最后一个。这里还得解释一下,这个最差情况说的是同一批数据进行处理时流程是固定的,就是像1.0和2.0版本,不管你怎么试都会是最后一个做pivot。但是3.0版本这个随机pivot是随机生成的,你无法保证会出现在哪里,所以最差情况就不能说它全生成在r位置。经过研究,随机快排也就是快排3.0,时间复杂度为O(N*logN),空间复杂度为O(logN)

2.0-->3.0,show you my code.

其实其他和2.0都一样,就加一个随机pivot选择

public static void process3(int[] arr, int p, int r) {
    if (p < r) {
        //(int) (Math.random() * (r - p + 1)) --> [0,r-p] + p -->[p,r]
        int pivotIdx = (int) (Math.random() * (r - p + 1)) + p;//[p,r],+p别忘了,偏移量
        swap(arr, pivotIdx, r);//x放在r位置
        int[] flags = netherlangsFlag(arr, p, r);
        process3(arr, p, flags[0]);
        process3(arr, flags[1], r);
    }
}

你可能感兴趣的:(快速排序)