快速排序-Java实现

 

最基本的快速排序

这里介绍最经典的快速排序思想,也是最基础的一种

快速排序(Quick Sort)思想

通过一趟排序将无序序列分割成独立的左右两部分,左边部分均为右边部分小,然后再分别对这两部分序列进行排序,可以通过递归的调用完成。

在快速排序中,通过定义一个基准值,左右指针,一般取左指针指向的数为基准值,然后从右指针开始遍历找比基准值小的数,找到了就和左指针的数交换;然后通过左指针遍历找比基准大的数,找到了就和右指针的数交换,循环往复,基准值将会归位,左右指针也会指向基准值,这时候就退出循环,开始递归左右序列。

左边取基准

右左交替遍历(右边找到比基准小的,交换到左边去;左边找到比基准大的,交换到右边去):

之所以先取右边,因为如果先取左边,就是一个向右的过程,所有的元素已经在基准的右边了

本身左边就是找比基准大的数,然后交换到右边去,这就完全不合理了,没意义。

如果从右边开始遍历找比基准小的数,通过交换,小的数就在基准的左边了

然后再遍历左指针,遇到比基准大的数,和基准值进行交换

直到左右指针相遇,那么此时基准的左边就是刚刚已经排序的小数了,而右边是大数,指针指向的也是基准值了

例如:[ 5 ,1,9,3,7,4,8,6, 2 ] pivot:5 left:arr[0]:5 right:arr[8]:2

第一轮比较:2<5 交换 [2, 1 ,9,3,7,4,8,6, 5 ] 移动左指针 left:arr[1]:1 right:arr[8]:5

第二轮比较:1<5 不交换 继续移动左指针 left:arr[2]:9 right:arr[8]:5

第三轮比较:9>5 交换 [2,1, 5 ,3,7,4,8, 6 ,9] 移动右指针 left:arr[2]:5 right:arr[7]:6

第四轮比较:5<6 不交换 继续移动右指针 left:arr[2]:5 right:arr[6]:8

第五轮比较:5<8 不交换 继续移动右指针 left:arr[2]:5 right:arr[5]:4

第六轮比较:5>4 交换 [2,1,4, 3 ,7, 5 ,8,6,9] 移动左指针 left:arr[3]:3 right:arr[5]:5

第七轮比较:3<5 不交换 继续移动左指针 left:arr[4]:7 right:arr[5]:5

第八轮比较:7>5 交换 [2,1,4,3, 5 , 7 ,8,6,9] 移动右指针 left:arr[4]:5 right:arr[4]:5

左右指针相遇 该轮排序完成,返回基准索引4

两指针相遇即完成一轮排序

package com.sort;
​
import java.util.Arrays;
​
public class QuickSortOfPivot {
​
    public static void main(String[] args) {
        int[] arr = {2, 4, 6, 7, 9, 1, 3, 8};
//        int[] arr = {5, 1, 9, 3, 7, 4, 8, 6, 2};
        quickSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }
​
    public static void quickSort(int[] arr, int left, int right) {
        if (left < right) {
            int pivot = partition(arr, left, right);
            // 对小的序列继续递归    传递基准左边第一个作为下一个序列的最右边
            quickSort(arr, left, pivot - 1);
            // 对大的序列继续递归    传递基准右边第一个作为下一个序列的最左边
            quickSort(arr, pivot + 1, right);
        }
    }
​
    public static int partition(int[] arr, int left, int right) {
        if (left < right) {
            int pivotKey = arr[left];
            while (left < right) {
                // 先从右边开始找比基准小的值    一定要记得是 >= 下面是<=
                // 因为如果是这样的序列{2, 4, 6, 7, 9, 1, 8, 2},就会导致死循环
                // 因为第一次比较的时候,如果不是 >= 或者 <= 那么while就会因为条件被破坏导致无法递减right和递增left,因为会认为需要交换
                // 但是实际上2和2交换过后,之后还会继续交换,就导致了死循环
                while (left < right && arr[right] >= pivotKey) {
                    right--;
                }
                swap(arr, left, right);
                // 开始从左边找一个比基准大的值
                while (left < right && arr[left] <= pivotKey) {
                    left++;
                }
                swap(arr, left, right);
            }
        }
​
        return left;
    }
​
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

快速排序优化

优化1:优化选取枢轴

当我们选择基准值的时候,每次都是选择最左边的值,这显然不是很合理,如果最左边是最大值,那么就会导致,当这个值交换到最右边的时候,整个最左边都是小于基准值的元素,递归树就不平衡。(下图取自大话数据结构)

优化方案:

/**
 * 优化1:
 * 取左中右三个数,将三个数的中间数放到最左边。这又叫三数取中
 * 还有9数取中,也就是取9个数,分为3个数组,每个数组取出中间的数后,再取中间的数
 *
 * @param arr   待排序数组
 * @param left  左边界
 * @param right 右边界
 */
public static void randomLow(int[] arr, int left, int right) {
    // 不要使用arr.length,因为这个时候,可能是递归在调用它,数组大小虽然没变,但是子数组的边界已经修改
    int mid = left + (right - left) / 2;
    // 保证左边最小
    if (arr[left] > arr[right]) {
        swap(arr, left, right);
    }
    // 保证中间最小
    if (arr[mid] > arr[right]) {
        swap(arr, mid, right);
    }
    // 此时右边是最大的,比较左边和中间的数,使较大的放在左边
    if (arr[left] < arr[mid]) {
        swap(arr, left, mid);
    }
}
​
    public static int partition(int[] arr, int left, int right) {
        int pivotKey;
​
        // 优化一:对基准数随机化
        randomLow(arr, left, right);
        pivotKey = arr[left];
        while (left < right) {
            ...
        }
​
        return left;
    }
优化二:优化不必要的交换

在这个优化中,可以发现我们将交换修改成了覆盖:

arr[left] = arr[right];
arr[right] = arr[left];
// 将基准值归位
arr[left] = pivotKey;
public static int partition(int[] arr, int left, int right) {
        int pivotKey;
        // 对基准数随机化
        randomLow(arr, left, right);
        pivotKey = arr[left];
        while (left < right) {
            while (left < right && arr[right] >= pivotKey) {
                right--;
            }
            // 将在右边找到的比基准值大的数放到左边去
            // 这是交换的方式,会有性能开销
//            swap(arr, left, right);
            // 优化2:采用直接替换的方式减少性能开销
            // 不必担心left的数被覆盖找不到,因为第一次覆盖的是基准值,基准值被保存在pivotKey中,相当于最开始基准值的位置被置空了
            // 也不必担心right的值没有被修改而重复,因为接下来会移动left,找到一个比基准大的值,放到right的位置上,如果没有找到,
            // 那么就会left和right就会重叠,最后基准值将会覆盖上去
            arr[left] = arr[right];
            // 开始从左边找一个比基准大的值
            while (left < right && arr[left] <= pivotKey) {
                left++;
            }
//            swap(arr, left, right);
            arr[right] = arr[left];
        }
        // 将基准值归位
        arr[left] = pivotKey;
        return left;
    }
优化三:优化小数组时的排序方案

因为快速排序可以用来处理数据量非常大的情况,但是对于数组比较小的时候,反而不如使用直接插入排序(直接插入排序是简单排序中性能最好的—),因为数组较小的时候,也会使用递归,对性能上有一定的影响。(有资料认为7比较合适,也有认为50比较合理的,实际应用可适当调整)

public static final int MAX_LENGTH_INSERT_SORT = 7;
// 优化三:使用直接插入排序来优化小数组时的排序
public static void quickSort3(int[] arr, int left, int right) {
    int pivot;
    if ((right - left) > MAX_LENGTH_INSERT_SORT) {
        pivot = partition(arr, left, right);
        // 对小的序列继续递归    传递基准左边第一个作为下一个序列的最右边
        quickSort3(arr, left, pivot - 1);
        // 对大的序列继续递归    传递基准右边第一个作为下一个序列的最左边
        quickSort3(arr, pivot + 1, right);
    } else {
        InsertSortKt.insertSort(arr, left, right);
    }
}

直接插入排序:使用Kotlin拓展方法完成

// 插入排序
fun IntArray.insertSort(left:Int, right:Int) {
    for (i in left .. right) {
        val insertVal = this[i]
        // 当前元素的前一个位置
        var insertIndex = i - 1
​
        // 然后依次将该元素和之前的元素进行比较
        // insertIndex表示检查插入是否合法的位置
        // 1.insertIndex >= 0 表示检查的位置不能小于0
        // 2.insertVal < array[insertIndex] 表示待插入的元素小于检查的位置
        // 如果小于就不插入,因为说不定前面还有更小的数,只有比检查的位置的数大的时候才进行插入
        // 这样就说明比当前检查的数大,比前一个检查的数小,是合适的位置
        while (insertIndex >= 0 && insertVal < this[insertIndex]) {
            // 将检查的数后移
            // 本身insertIndex就是i-1,所以再次减1表示是当前拿到的需要排序的数的前前个位置
            this[insertIndex + 1] = this[insertIndex]
            insertIndex--
        }
​
        // 说明找到了位置,之所以+1是因为当前insertIndex是检查的数的索引,也就是说这个检查的数是比待插入的数大。
        // 或者因为找到了-1的索引,不符合条件,到达了最前面
        // 优化:如果当前找到的位置的后一位就是当前这个数,那么不需要插入,因为就是它本身
        if (insertIndex + 1 != i) {
            this[insertIndex + 1] = insertVal
        }
    }
}
优化四:优化递归操作

由于递归对性能有一定的影响,并且可能导致堆栈溢出。如果待排序的序列划分极端不平衡,递归深度将会趋近于n,这不仅仅是速度慢的问题了,还有可能导致堆栈溢出。

因此可以进行尾递归优化(对于剩下的右半部分的数组,可以对其使用迭代,也就是一直使用第一次递归,也就是左递归,直到左右指针在同一元素上,就完成了递归):

// 优化四:减少不必要的递归
public static void quickSort4(int[] arr, int left, int right) {
    int pivot;
    if ((right - left) > MAX_LENGTH_INSERT_SORT) {
        while (left < right) {
            pivot = partition(arr, left, right);
            // 对小的序列继续递归    传递基准左边第一个作为下一个序列的最右边
            quickSort4(arr, left, pivot - 1);
            // 尾递归优化,将之前的递归修改为这种方式,比如在之前执行到这个位置的时候右序列(比基准值大的序列)是[9,8,6,7]
            // 这个时候,left是4(9的索引是5),因此+1,将left指向到9的位置,然后开始循环
            // 也就是说原先是对左边部分进行了递归后再执行右边部分的递归,现在使用迭代的方式,使得每次循环都是只执行第一次的递归即可
            // 相当于一直截断,然后一直对左边进行排序,直到left和right相遇(当分割的数组只有一个数的时候,那么就完成了排序)
            left = pivot + 1;
        }
​
    } else {
        InsertSortKt.insertSort(arr, left, right);
    }
}

完整代码

package com.sort;
​
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
​
public class QuickSortOfPivot {
​
    public static final int MAX_LENGTH_INSERT_SORT = 7;
​
    public static void main(String[] args) {
        int[] arr = {2, 4, 6, 7, 9, 1, 8, 2};
//        int[] arr = {5, 1, 9, 3, 7, 4, 8, 6, 2};
//        quickSort4(arr, 0, arr.length - 1);
//        System.out.println(Arrays.toString(arr));
​
​
        int[] randomArray = new int[80000000];
        for (int i = 0; i < 80000000; i++) {
            randomArray[i] = (int) (Math.random() * 800000000);
        }
        Date date = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String formatted = simpleDateFormat.format(date);
        System.out.println("排序前的时间:" + formatted);
        quickSort(randomArray, 0, 79999999);
        Date dateAfter = new Date();
        String dateAfterFormat = simpleDateFormat.format(dateAfter);
        System.out.println("排序后的时间:" + dateAfterFormat);
//        System.out.println(Arrays.toString(randomArray));
​
    }
​
​
    /**
     * 左边取基准
     * 右左交替遍历(右边找到比基准小的,交换到左边去;左边找到比基准大的,交换到右边去):
     * 之所以先取右边,因为如果先取左边,就是一个向右的过程,所有的元素已经在基准的右边了,
     * 本身左边就是找比基准大的数,然后交换到右边去,这就完全不合理了,没意义。
     * 如果从右边开始遍历找比基准小的数,通过交换,小的数就在基准的左边了,
     * 然后再遍历左指针,遇到比基准大的数,和基准值进行交换,
     * 直到左右指针相遇,那么此时基准的左边就是刚刚已经排序的小数了,而右边是大数,指针指向的也是基准值了
     * 例如:[ 5 ,1,9,3,7,4,8,6, 2 ] pivot:5  left:arr[0]:5   right:arr[8]:2
     * 第一轮比较:2<5   交换  [2, 1 ,9,3,7,4,8,6, 5 ] 移动左指针   left:arr[1]:1   right:arr[8]:5
     * 第二轮比较:1<5   不交换 继续移动左指针 left:arr[2]:9   right:arr[8]:5
     * 第三轮比较:9>5   交换  [2,1, 5 ,3,7,4,8, 6 ,9] 移动右指针   left:arr[2]:5   right:arr[7]:6
     * 第四轮比较:5<6   不交换 继续移动右指针 left:arr[2]:5   right:arr[6]:8
     * 第五轮比较:5<8   不交换 继续移动右指针 left:arr[2]:5   right:arr[5]:4
     * 第六轮比较:5>4   交换  [2,1,4, 3 ,7, 5 ,8,6,9] 移动左指针   left:arr[3]:3   right:arr[5]:5
     * 第七轮比较:3<5   不交换 继续移动左指针   left:arr[4]:7   right:arr[5]:5
     * 第八轮比较:7>5   交换  [2,1,4,3, 5 , 7 ,8,6,9] 移动右指针   left:arr[4]:5   right:arr[4]:5
     * 左右指针相遇  该轮排序完成,返回基准索引4
     * 两指针相遇即完成一轮排序
     */
    public static void quickSort(int[] arr, int left, int right) {
        int pivot;
        if (left < right) {
            pivot = partition(arr, left, right);
            // 对小的序列继续递归    传递基准左边第一个作为下一个序列的最右边
            quickSort(arr, left, pivot - 1);
            // 对大的序列继续递归    传递基准右边第一个作为下一个序列的最左边
            quickSort(arr, pivot + 1, right);
        }
    }
​
​
    // 优化三:使用直接插入排序来优化小数组时的排序
    public static void quickSort3(int[] arr, int left, int right) {
        int pivot;
        if ((right - left) > MAX_LENGTH_INSERT_SORT) {
            pivot = partition(arr, left, right);
            // 对小的序列继续递归    传递基准左边第一个作为下一个序列的最右边
            quickSort3(arr, left, pivot - 1);
            // 对大的序列继续递归    传递基准右边第一个作为下一个序列的最左边
            quickSort3(arr, pivot + 1, right);
        } else {
            InsertSortKt.insertSort(arr, left, right);
        }
    }
​
    // 优化四:减少不必要的递归
    public static void quickSort4(int[] arr, int left, int right) {
        int pivot;
        if ((right - left) > MAX_LENGTH_INSERT_SORT) {
            while (left < right) {
                pivot = partition(arr, left, right);
                // 对小的序列继续递归    传递基准左边第一个作为下一个序列的最右边
                quickSort4(arr, left, pivot - 1);
                // 尾递归优化,将之前的递归修改为这种方式,比如在之前执行到这个位置的时候右序列(比基准值大的序列)是[9,8,6,7]
                // 这个时候,left是4(9的索引是5),因此+1,将left指向到9的位置,然后开始循环
                // 也就是说原先是对左边部分进行了递归后再执行右边部分的递归,现在使用迭代的方式,使得每次循环都是只执行第一次的递归即可
                // 相当于一直截断,然后一直对左边进行排序,直到left和right相遇(当分割的数组只有一个数的时候,那么就完成了排序)
                left = pivot + 1;
            }
​
        } else {
            InsertSortKt.insertSort(arr, left, right);
        }
    }
​
    public static int partition(int[] arr, int left, int right) {
        int pivotKey;
​
        // 对基准数随机化
        randomLow(arr, left, right);
        pivotKey = arr[left];
        while (left < right) {
            // 先从右边开始找比基准小的值    一定要记得是 >= 下面是<=
            // 因为如果是这样的序列{2, 4, 6, 7, 9, 1, 8, 2},就会导致死循环
            // 因为第一次比较的时候,如果不是 >= 或者 <= 那么while就会因为条件被破坏导致无法递减right和递增left,因为会认为需要交换
            // 但是实际上2和2交换过后,之后还会继续交换,就导致了死循环
            while (left < right && arr[right] >= pivotKey) {
                right--;
            }
            // 将在右边找到的比基准值大的数放到左边去
            // 这是交换的方式,会有性能开销
//            swap(arr, left, right);
            // 优化2:采用直接替换的方式减少性能开销
            // 不必担心left的数被覆盖找不到,因为第一次覆盖的是基准值,基准值被保存在pivotKey中,相当于最开始基准值的位置被置空了
            // 也不必担心right的值没有被修改而重复,因为接下来会移动left,找到一个比基准大的值,放到right的位置上,如果没有找到,
            // 那么就会left和right就会重叠,最后基准值将会覆盖上去
            arr[left] = arr[right];
            // 开始从左边找一个比基准大的值
            while (left < right && arr[left] <= pivotKey) {
                left++;
            }
//            swap(arr, left, right);
            arr[right] = arr[left];
        }
        // 将基准值归位
        arr[left] = pivotKey;
​
        return left;
    }
​
    /**
     * 优化1:
     * 取左中右三个数,将三个数的中间数放到最左边。这又叫三数取中
     * 还有9数取中,也就是取9个数,分为3个数组,每个数组取出中间的数后,再取中间的数
     *
     * @param arr   待排序数组
     * @param left  左边界
     * @param right 右边界
     */
    public static void randomLow(int[] arr, int left, int right) {
        // 不要使用arr.length,因为这个时候,可能是递归在调用它,数组大小虽然没变,但是子数组的边界已经修改
        int mid = left + (right - left) / 2;
        // 保证左边最小
        if (arr[left] > arr[right]) {
            swap(arr, left, right);
        }
        // 保证中间最小
        if (arr[mid] > arr[right]) {
            swap(arr, mid, right);
        }
        // 此时右边是最大的,比较左边和中间的数,使较大的放在左边
        if (arr[left] < arr[mid]) {
            swap(arr, left, mid);
        }
    }
​
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值