快速排序/快速选择算法

Posted 允歆辰丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快速排序/快速选择算法相关的知识,希望对你有一定的参考价值。

目录

一.快速排序

1.基本介绍

2.基本思路

1.关于中轴值的选择

2.小数组情形

3.时间复杂度

3.代码实现

1.随机值分割

2.三数中值分割法

2.快速选择

1.基本介绍

2.基本思路

3.代码实现

1.随机值分割

2.三数中值分割法

3.数组中的第K个最大元素

1.题目描述

2.思路分析

3.代码实现

1.直接使用快速排序

2.快速选择

4.数组中的第K个最大元素

1.题目描述

2.思路分析

3.代码实现

1.直接使用快速排序

2.快速选择


一.快速排序

1.基本介绍

快速排序(Quicksort〉是对冒泡排序的一种改进,都属于交换排序。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分(每次选择中轴值),中轴值左边的元素小于中轴值,中轴值右边的元素全部大于中轴值(但不要求有序),然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

2.基本思路

我们每次选择一个中轴值,将中轴值和最后一个元素交换位置,对left到right-1的元素进行交换,当r<=l,此时l停留到的位置和right位置上的元素(中轴值)进行交换,这个时候,l停留的位置上是中轴值,并且左边的元素比中轴值小,右边的元素比中轴值大,然后递归对左边和右边的元素进行快速排序,直到left>=right的时候停止,此时数组便是有序的了.

1.关于中轴值的选择

大部分的可能给出的是选择第一个元素作为中轴值,但是这种做法有一定的弊端,如果是数组的元素是随机排序的,是可以接受的,但是如果数组的元素是预排序或者反序的,这样所有的元素不是全被划入到中轴值左边,就是划入到中轴值右边,那么时间复杂度就会很高

一种有效的方法时是采用随机产生一个下标为[left,right]的下标,这种情况下虽然可能会产生上面描述的情况,但总的来说可能性还是很小的.

还有一种方法是:三数中值分割法,我们选取三个数,使得三个数的最大值作为中轴值进行分割,一般来说我们选取左端,中端,右端的三个元素的终止作为中轴值,这种选取的方法还是

2.小数组情形

对于很小的数组(数组元素个数小于20),快速排序不如插入排序,因为快速排序是递归的.我们通常的做法是对于小的数组采用插入排序,一种好的截止范围是n=10,同时这种做法也避免了三数中值分割法的错误情况,比如最终数组的元素只有一个元素或者两个元素,而无法三数分割

3.时间复杂度

快速排序的平均时间复杂度:O(nlogn) 

最好情况:O(nlogn) 

最坏情况:O()

空间复杂度:O(logn)

同时快速排序不稳定,也就说元素相同的时候,原本在前边的元素,可能会最终被排序到后边

3.代码实现

1.随机值分割

    public void quickSort(int[] nums, int left, int right) 
        if (left >= right)
            return;
        int index = (int) (left + Math.random() * (right - left + 1));
        int pivot = nums[index];//随机值生成index
        swap(nums, index, right);
        int l = left, r = right - 1;
        while (true) 
            while (l < right && nums[l] < pivot) //找到第一个比pivot大的数
                l++;
            
            while (r > 0 && nums[r] > pivot) //
                r--;
            
            if (l < r)
                swap(nums, l++, r--);
            else
                break;
        
        swap(nums, l, right);
        quickSort(nums, left, l - 1);
        quickSort(nums, l + 1, right);


    

2.三数中值分割法

    public static final int CUTOFF = 10;

    public int median(int[] nums, int left, int right) 
        int center = (left + right) / 2;
        if (nums[left] > nums[center])
            swap(nums, left, center);//此时center大于left
        if (nums[left] > nums[right])
            swap(nums, left, right);//此时left为最小
        if (nums[center] > nums[right])
            swap(nums, center, right);
        //把center值放到right-1的位置
        swap(nums, center, right - 1);
        return nums[right - 1];


    

    public void quickSort2(int[] nums, int left, int right) 
        //cutoff为截断值
        if (left + CUTOFF <= right) 
            int pivot = median(nums, left, right);//随机值生成index
            int l = left+1, r = right - 2;
            while (true) 
                while (nums[l] < pivot) //找到第一个比pivot大的数
                    l++;
                
                while (nums[r] > pivot) //
                    r--;
                
                if (l < r)
                    swap(nums, l++, r--);
                else
                    break;
            
            swap(nums, l, right-1);
            quickSort2(nums, left, l - 1);
            quickSort2(nums, l + 1, right);
         else 
            insertSort(nums, left, right);
        


    
    public void insertSort(int[] nums, int left, int right) 
        int j;
        for (int i = left; i <= right; ++i) 
            int temp = nums[i];
            //寻找插入的位置
            for (j = i; j > left && temp < nums[j - 1]; j--) 
                nums[j] = nums[j - 1];
            
            nums[j] = temp;
        
    


    public void swap(int[] nums, int i, int j) 
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    

2.快速选择

1.基本介绍

快速选择算法其实是快速排序算法的变形,主要用于解决寻找第k大元素或者第k小元素,当我们需要寻找找到(排序后)第k个元素的时候,不要求数组的所有元素必须有序,这个时候我们可以选择使用快速选择算法,因为快速选择算法的时间复杂度比直接使用快速排序的时间复杂度低

快速选择的时间复杂度:O(n)

快速排序的时间复杂度:O(nlog(n))

①中轴值最终能交换到第k个位置,说明我们找到了第k个元素

②中轴值最终的位置大于第k个位置,此时我们只需要对中轴值左边的元素进行快速排序

③中轴值最终的位置小于第k个位置,此时我们只需要对中轴值右边的元素进行快速排序

2.基本思路

快速选择的基本思路和快速排序算法还是一样的,无非就是多加了一个判断,判断是对中轴值左边进行快速排序,还是右边进行.

3.代码实现

1.随机值分割

    public void quickSelect(int[] nums, int left, int right, int k) 
        if (left >= right)
            return;
        int index = (int) (left + Math.random() * (right - left + 1));
        int pivot = nums[index];//随机值生成index
        swap(nums, index, right);
        int l = left, r = right - 1;
        while (true) 
            while (l < right && nums[l] < pivot) //找到第一个比pivot大的数
                l++;
            
            while (r > 0 && nums[r] > pivot) //
                r--;
            
            if (l < r)
                swap(nums, l++, r--);
            else
                break;
        
        swap(nums, l, right);
        if (l > k) 
            quickSelect(nums, left, l - 1, k);
         else if (l < k) 
            quickSelect(nums, l + 1, right, k);
        


    

2.三数中值分割法

    public static final int CUTOFF = 10;

    public int median(int[] nums, int left, int right) 
        int center = (left + right) / 2;
        if (nums[left] > nums[center])
            swap(nums, left, center);//此时center大于left
        if (nums[left] > nums[right])
            swap(nums, left, right);//此时left为最小
        if (nums[center] > nums[right])
            swap(nums, center, right);
        //把center值放到right-1的位置
        swap(nums, center, right - 1);
        return nums[right - 1];


    

    public void quickSelect2(int[] nums, int left, int right, int k) 
        if (left + CUTOFF <= right) 
            int pivot = median(nums, left, right);
            int l = left + 1, r = right - 2;
            while (true) 
                while (l < right && nums[l] < pivot) //找到第一个比pivot大的数
                    l++;
                
                while (r > 0 && nums[r] > pivot) //
                    r--;
                
                if (l < r)
                    swap(nums, l++, r--);
                else
                    break;
            
            swap(nums, l, right - 1);
            if (l > k) 
                quickSelect2(nums, left, l - 1, k);
             else if (l < k) 
                quickSelect2(nums, l + 1, right, k);
            
         else 
            insertSort(nums, left, right);
        


    

    public void insertSort(int[] nums, int left, int right) 
        int j;
        for (int i = left; i <= right; ++i) 
            int temp = nums[i];
            //寻找插入的位置
            for (j = i; j > left && temp < nums[j - 1]; j--) 
                nums[j] = nums[j - 1];
            
            nums[j] = temp;
        
    


    public void swap(int[] nums, int i, int j) 
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    

3.数组中的第K个最大元素

1.题目描述

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

力扣:力扣

2.思路分析

看到题目我们就可以想到这一题可以使用快速选择算法肯定会更加高效,寻找第k个最大的元素,也就是寻找到nums.length-k个元素是什么即可,当然我们也可以直接快速排序,直接返回nums.length-k个元素

3.代码实现

1.直接使用快速排序

   public int findKthLargest(int[] nums, int k) 
        quickSort(nums, 0, nums.length - 1);
        return nums[nums.length - k];


    

    public void quickSort(int[] nums, int left, int right) 
        if (left >= right)
            return;
        int index = (int) (left + Math.random() * (right - left + 1));
        int pivot = nums[index];//随机值生成index
        swap(nums, index, right);
        int l = left, r = right - 1;
        while (true) 
            while (l < right && nums[l] < pivot) //找到第一个比pivot大的数
                l++;
            
            while (r > 0 && nums[r] > pivot) //
                r--;
            
            if (l < r)
                swap(nums, l++, r--);
            else
                break;
        
        swap(nums, l, right);
        quickSort(nums, left, l - 1);
        quickSort(nums, l + 1, right);

    

    public  void swap(int[] nums, int i, int j) 
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    

2.快速选择

    public int findKthLargest(int[] nums, int k) 
        quickSelect(nums, 0, nums.length - 1,nums.length-k);
        return nums[nums.length - k];


    

    public void quickSelect(int[] nums, int left, int right, int k) 
        if (left >= right)
            return;
        int index = (int) (left + Math.random() * (right - left + 1));
        int pivot = nums[index];//随机值生成index
        swap(nums, index, right);
        int l = left, r = right - 1;
        while (true) 
            while (l < right && nums[l] < pivot) //找到第一个比pivot大的数
                l++;
            
            while (r > 0 && nums[r] > pivot) //
                r--;
            
            if (l < r)
                swap(nums, l++, r--);
            else
                break;
        
        swap(nums, l, right);
        if (l > k ) 
            quickSelect(nums, left, l - 1,k);
         else if (l < k ) 
            quickSelect(nums, l + 1, right,k);
        


    

    public  void swap(int[] nums, int i, int j) 
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    

4.数组中的第K个最大元素

1.题目描述

给你一个二维矩阵 matrix 和一个整数 k ,矩阵大小为 m x n 由非负整数组成。

矩阵中坐标 (a, b) 可由对所有满足 0 <= i <= a < m0 <= j <= b < n 的元素 matrix[i][j]下标从 0 开始计数)执行异或运算得到。

请你找出 matrix 的所有坐标中第 k 大的值(k 的值从 1 开始计数)。

力扣:力扣

2.思路分析

这一题首先需要解决的就是将所有坐标的异或运算的结果表达出来,然后用一个ArrayList存起来,然后这个时候我们就可以使用快速选择求出来第k大的值了.

首先我们需要解决的就是各个位置的异或结果的值,一想到异或,并且题目中明确表达出 满足0 <= i <= a < m0 <= j <= b < n 的元素 matrix[i][j],这个时候我们可以想到使用异或前缀和进行解答,我们这个时候需要找到进行递推的异或表达式

借用力扣官方题解的一幅图片,我们可以看出来异或递推公式为

prefix[i][j] = prefix[i - 1][j] ^ prefix[i][j - 1] ^ prefix[i - 1][j - 1] ^ matrix[i - 1][j - 1];

但我们我们看出(i,j)需要上一层的元素进行递推得到,如果我们定义的前缀异或的表达式长度为二维数组的大小的话,这个时候我们需要对第一行和第一列进行初始化,这个时候是很麻烦的,这个时候我们不妨定义的长度为m+1和n+1,刚开始元素的值都为1,并且一个值异或0还是本身,正好符合本题的意思

接下来进行快速选择,和上一题一样

3.代码实现

1.直接使用快速排序

    public int kthLargestValue(int[][] matrix, int k) 
        int m = matrix.length;
        int n = matrix[0].length;
        int[][] prefix = new int[m + 1][n + 1];
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 1; i <= m; ++i) 
            for (int j = 1; j <= n; ++j) 
                prefix[i][j] = prefix[i - 1][j] ^ prefix[i][j - 1] ^ prefix[i - 1][j - 1] ^ matrix[i-1][j-1];
                list.add(prefix[i][j]);

            
        
        quickSort(list, 0, list.size() - 1);
        return list.get(list.size() - k);


    

    public void quickSort(List<Integer> list, int left, int right) 
        if (left >= right)
            return;
        int index = (int) (left + Math.random() * (right - left + 1));
        int pivot = list.get(index);//随机值生成index
        swap(list, index, right);
        int l = left, r = right - 1;
        while (true) 
            while (l < right && list.get(l) < pivot) //找到第一个比pivot大的数
                l++;
            
            while (r > 0 && list.get(r) > pivot) //
                r--;
            
            if (l < r)
                swap(list, l++, r--);
            else
                break;
        
        swap(list, l, right);
        quickSort(list, left, l - 1);
        quickSort(list, l + 1, right);
        



    

    public void swap(List<Integer> list, int i, int j) 
        int temp = list.get(i);
        list.set(i, list.get(j));
        list.set(j, temp);
    

2.快速选择

   public int kthLargestValue(int[][] matrix, int k) 
        int m = matrix.length;
        int n = matrix[0].length;
        int[][] prefix = new int[m + 1][n + 1];
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 1; i <= m; ++i) 
            for (int j = 1; j <= n; ++j) 
                prefix[i][j] = prefix[i - 1][j] ^ prefix[i][j - 1] ^ prefix[i - 1][j - 1] ^ matrix[i-1][j-1];
                list.add(prefix[i][j]);

            
        
        quickSelect(list, 0, list.size() - 1, list.size() - k);
        return list.get(list.size() - k);


    

    public void quickSelect(List<Integer> list, int left, int right, int k) 
        if (left >= right)
            return;
        int index = (int) (left + Math.random() * (right - left + 1));
        int pivot = list.get(index);//随机值生成index
        swap(list, index, right);
        int l = left, r = right - 1;
        while (true) 
            while (l < right && list.get(l) < pivot) //找到第一个比pivot大的数
                l++;
            
            while (r > 0 && list.get(r) > pivot) //
                r--;
            
            if (l < r)
                swap(list, l++, r--);
            else
                break;
        
        swap(list, l, right);
        if (l > k) 
            quickSelect(list, left, l - 1, k);
         else if (l < k) 
            quickSelect(list, l + 1, right, k);
        



    

    public void swap(List<Integer> list, int i, int j) 
        int temp = list.get(i);
        list.set(i, list.get(j));
        list.set(j, temp);
    

 

Java实现快速排序

1、排序算法简介
下面看一下七种内排序(待排序数据放在内存)算法的分类。
插入排序类
  • 直接插入排序

  • 希尔排序

选择排序类
  • 简单选择排序

  • 堆排序

交换排序类
  • 冒泡排序

  • 快速排序

归并排序类
  • 归并排序


根据算法的简单性进行分类:

  • 简单算法:冒泡、简单选择、直接插入

  • 改进算法:希尔、堆、归并、快速


排序算法的性能主要受三个方面影响:

  • 时间性能

  • 辅助空间

  • 算法的复杂性


七种排序算法的各种指标对比如下图所示。


这七种排序算法,应该说没有一种是绝对占优的。冒泡排序逻辑最简单,这个相信大家都会,而堆排序我也写过一篇文章了专门介绍 

本文介绍的快速排序算法,可以认为是最慢的冒泡排序的升级。


2、Java实现快速排序的代码

快速排序算法的思想以及代码的注释都在下面,大家可以直接将代码copy到本地方便学习。


package com.bobo.sort;

/**
 * 快速排序思想:
 * 1、从数组中选取一个元素作为基准值(一般是第一个),将待排序的数组分成左右两部分,左边的部分小于基准值,右边的部分大于基准值;
 * 2、使用左右两个指针向中间移动(可以约定右指针先移动),左边的指针移到到比基准值大的下标时停下,右边的指针移动到比基准小的下标时停下,
 * 然后双方交换数组元素,继续向左、向右移动;
 * 3、当左右指针重合时,有两种情况:
 * 基准值大于重合处元素:将基准值与重合处元素交换,一轮结束,以重合处下标为分界线,分成左右两部分,递归排序;
 * 基准值小于等于重合处元素:将基准值与重合处上一个元素交换,一轮结束,以重合处上一个下标为分界线,分成左右两部分,递归排序;
 */

public class QuickSort {
    /**
     * 用于验证待排序序列,避免出现数组下标越界异常
     */

    public static boolean valid(int[] array,int base,int low,int high){
        // 避免出现数组下标越界异常
        if(base>low || base>high || low>high){
            return false;
        }
        //只有一个元素的情况:base=low=high
        if(base == low && low == high){
            return false;
        }
        //只有两个元素的情况:base<low=high
        if(base < low && low == high){
            //只有两个元素时,直接比较交换,无需走快排
            if(array[base] > array[high]){
                int temp = array[base];
                array[base] = array[high];
                array[high] = temp;
            }
            return false;
        }
        //三个及三个以上元素的情况:base<low<high
        return true;
    }

    /**
     * 快速排序
     * @param array 待排序数组
     * @param base 基准值下标
     * @param low low下标
     * @param high high下标
     */

    public static void qSort(int[] array,int base,int low,int high){
        if(!valid(array,base,low,high)){
            return;
        }
        int i=low,j=high;
        for (;;){
            // 右指针移动
            while (array[j] >= array[base] && j>i){
                j--;
            }
            // 左指针移动
            while (array[i] <= array[base] && j>i){
                i++;
            }
            // 左右指针重合时,走if
            if(j==i){
                if(array[i] < array[base]){
                    // 基准值大于重合处元素时,走if;将基准值与重合处元素交换
                    int temp = array[base];
                    array[base] = array[i];
                    array[i] = temp;
                    // 分成左右两半,递归排序
                    qSort(array,base,low,i-1);
                    qSort(array,i+1,i+2,high);
                }else if(array[i] >= array[base]){
                    // 基准值小于等于重合处元素,走else;将基准值与重合处上一个元素交换
                    int temp = array[base];
                    array[base] = array[i-1];
                    array[i-1] = temp;
                    // 分成左右两半,递归排序
                    qSort(array,base,low,i-2);
                    qSort(array,i,i+1,high);
                }
                // 一轮结束,return
                return;
            }else{
                // 左右指针没重合时,走else;交换左右指针对应的元素
                int temp = array[i];
                array[i] = array[j];
                array[j] = temp;
            }
        }
    }

    public static void main(String[] args) {
        int[] array = new int[]{-419,-419,452,451,267,-205,-114,82,-323,0};
        qSort(array,0,1,array.length-1);
        for (int i : array) {
            System.out.println(i);
        }

    }
}


为了确保代码的正确性,我专门找了力扣的一道题做验证,详情请见力扣的912题-排序数组,https://leetcode-cn.com/problems/sort-an-array/。

学习快速排序然后写代码花了一个小时,从第一次运行到通过这期间改bug也花了一个小时,幸好最终是ok的。




以上是关于快速排序/快速选择算法的主要内容,如果未能解决你的问题,请参考以下文章

排序算法——快速排序

排序算法之选择插入冒泡快速

用C语言编写函数,要实现快速排序算法或者冒泡法

数据结构与算法-快速排序

经典排序算法---快速排序

数据结构-快速排序算法