快速排序及优化

快速排序的基本思想:挖坑填数+分治法。

一、递归法

1、算法描述:

快速排序通过分治策略把一个数组一分为二。步骤为:

  1. 从数组中挑出一个元素,称为"基准"(pivot)。
  2. 重新排序数组,所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准后面。分区结束之后,该基准就处于数组的中间位置。这个称为分区(partition)操作。
  3. 递归地把小于基准值元素的子数组和大于基准值元素的子数组排序。

递归到最底部时,数组的大小是零或一(实际上为一时已经有序,不需要再分),也就是已经排序好了。

2、代码实现:

//快速排序-递归实现
    public static void quickSort(int[] a, int low, int high) {
        //已经排序完成
        if (low >= high) {
            return;
        }
        //找基准
        //三数取中优化:medianOfThree(a, int low, int high)
        int pivotIndex = partition(a,low,high);
        //左边有两个元素以上开始递归
        if(pivotIndex > low+1){
            quickSort(a, low, pivotIndex - 1);
        }
        //右边有两个元素以上开始递归
        if(pivotIndex < high-1){
            quickSort(a, pivotIndex + 1, high);
        }
    }

    public static int partition(int[] a,int left,int right){
        int pivot = a[left];
        while (left < right) {
            //从后向前找比基准小的值
            while (left < right && a[right] >= pivot) {
                right--;
            }
            a[left] = a[right];
            //从前向后找比基准大的值
            while (left < right && a[left] <= pivot) {
                left++;
            }
            a[right] = a[left];
        }
        //把基准放回数组中
        a[left] = pivot;
        //返回基准的下标
        return left;
    }

二、非递归:

我们可以借助栈来保存每一组划分后的左右下标实现非递归。

//非递归实现快排:使用栈
    public static void quickSortByStack(int[] a) {
        Stack stack = new Stack<>();
        //初始状态的左右下标入栈
        stack.push(0);
        stack.push(a.length - 1);
        while (!stack.isEmpty()) {
            //出栈两个元素用来划分待排序数组
            // 注意:进栈的时候先低位下标,出栈的时候先出来的是高位
            int high = stack.pop();
            int low = stack.pop();
            //找基准
            //三数取中优化:medianOfThree(a, int low, int high)
            int pivotIndex = partition(a, low, high);
            //保存划分后左半部分两个下标
            if (pivotIndex > low + 1) {
                stack.push(low);
                stack.push(pivotIndex - 1);
            }
            //保存划分后右半部分两个下标,增加pivotIndex >= 0,过滤partition返回的-1
            if (pivotIndex < high - 1 && pivotIndex >= 0) {
                stack.push(pivotIndex + 1);
                stack.push(high);
            }
        }
    }

    public static int partition(int[] a, int left, int right) {
        if (left >= right) {
            return -1;
        }
        //保存基准的值
        int pivot = a[left];
        while (left < right) {
            //从后向前找比基准小的元素,插入到基准位置
            while (left < right && a[right] >= pivot) {
                right--;
            }
            a[left] = a[right];
            //从前向后找比基准大的元素
            while (left < right && a[left] <= pivot) {
                left++;
            }
            a[right] = a[left];
        }
        //放置基准值
        a[left] = pivot;
        //返回基准下标
        return left;
    }

三、分析优化:

1、切换为直接插入排序

  • 快排在元素较少的时候,比直接插入排序慢。
  • 另外,随着快排的进行,数组会越来越有序,子数组长度会越来越小,而直接插入排序在元素有序的情况下效率很高。

2、三数取中法优化:

  • 上面的实现都以数组下标low对应的值作为基准调整,在数组有序或者基本有序的情况下,基准划分会非常不均匀,导致快排退化为冒泡排序,时间复杂度从O(nlog₂n)变为O(n²)。
  • 三数取中法是指从数组下标low、mid=(low+high)/2、high对应的三个值中选取基准来调整。
  • 最后的调整的结果就是a[mid] <= a[low] <= a[high]

具体代码如下:

//优化-三数取中
    public static void medianOfThree(int[] a, int low, int high) {
        int mid = (low + high) / 2;
        if (a[mid] > a[low]) {
            swap(a, a[mid], a[low]);
        }
        if (a[mid] > a[high]) {
            swap(a, a[mid], a[high]);
        }
        if (a[low] > a[high]) {
            swap(a, a[low], a[high]);
        }
    }

    public static void swap(int[] a, int i, int j) {
        a[i] ^= a[j];
        a[j] ^= a[i];
        a[i] ^= a[j];
    }

 

你可能感兴趣的:(排序算法)