图解快速排序三种优化方式

959 阅读2分钟

image.png

这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

引言

上篇文章讲解了快速排序的三种方式:挖坑法、前后指针法,左右指针法,也点出了快速排序在实际开发中有许多优化的空间。今天这篇文章,就来介绍一下,快速排序在什么情况下性能差,有哪些可以优化的方式。

引入插入排序

考虑数组长度对快速排序算法的影响,对于小数组的排序,快速排序的性能不如插入排序,并且由于快速排序是递归的,采用这种方式大概可以节省15%的运行时间,而这个小数组的长度取多小,跟系统挂钩,一般5-15之间的任意值在大多数情况下都能令人满意。

代码贴贴:

public void quick_sort(int nums[],int l,int r){
    //递归临界条件
    if(l >= r) return;
    
    /*************优化*****************/
    //当长度小于等于7时,转换为插入排序
    if(r <= l + 7) {
        insert_sort(nums,l,r);//插入排序
        return;
    }
    /*********************************/
    
    //左右指针法
    //选取数组第一个元素为基准
    int x = nums[l];
    int ll = l,rr = r;
    while(l < r){
        //右指针向左移动,寻找比基准小的元素
        while(r > l && nums[r] >= x){
            r--;
        }
        //左指针向右移动,寻找比基准大的元素
        while(r > l && nums[l] <= x){
            l++;
        }
        //将左右指针指向元素进行交换
        if(l < r){
            int temp = nums[l];
            nums[l] = nums[r];
            nums[r] = temp;
        }
    }
    //将基准跟指针位置元素进行交换
    nums[ll] = nums[l];
    nums[l] = x;
    //递归排序左数组
    quick_sort(nums,ll,l-1);
    //递归排序右数组
    quick_sort(nums,l+1,rr);
}

三取样切分(三数取中法)

在序列相对有序的情况下,快速排序算法的性能可能会退化到O(n^2),比如下面序列:

image.png

采用挖坑法,选取第一个元素作为基准,一趟分区操作下来,只确定了一个元素是有序的。

image.png

选择的基准在我们的理想状况下应该是尽量将数组分区成左右两部分长度相差不大的序列才对。

因此,常规的选取第一个,最后一个元素作为基准的方式可能会导致性能下降,优化的方式可以采取三取样切分的方式,即将数组的第一个,中间,最后一个元素排序后的中位数作为基准进行分区划分

代码贴贴:

public void quick_sort(int nums[],int l,int r){
    //递归临界条件
    if(l >= r) return;
    
    /***************优化*******************/
    //返回基准的索引
    int index = median(l,(l+r)/2,r);//将数组的第一个,中间,最后一个元素排序后中位数的索引返回
    //将第一个元素跟基准元素进行交换
    int t = nums[index];
    nums[index] = nums[l];
    nums[l] = t;
    /*************************************/
    
    //左右指针法
    //选取基准
    int x = nums[l];
    int ll = l,rr = r;
    while(l < r){
        //右指针向左移动,寻找比基准小的元素
        while(r > l && nums[r] >= x){
            r--;
        }
        //左指针向右移动,寻找比基准大的元素
        while(r > l && nums[l] <= x){
            l++;
        }
        //将左右指针指向元素进行交换
        if(l < r){
            int temp = nums[l];
            nums[l] = nums[r];
            nums[r] = temp;
        }
    }
    //将基准跟指针位置元素进行交换
    nums[ll] = nums[l];
    nums[l] = x;
    //递归排序左数组
    quick_sort(nums,ll,l-1);
    //递归排序右数组
    quick_sort(nums,l+1,rr);
}

三向切分(三路快排)

在实际的应用中,经常会出现大量重复元素的数组,对于这些重复的元素,其实是不需要再继续排序了,但是快速排序依然会将它们切分成更小的数组,这使得快速排序的有很大的改进空间。

一个简单的想法就是将数组切分成三部分,分别对应小于、等于、大于基准的数组元素,具体的步骤如下图所示:

1、维护一个指针lt使得[lo,lt-1]中的元素都小于基准,一个指针gt使得[gt+1,hi]中的元素都大于基准,一个指针i使得[lt,i-1]中的元素都等于基准。

image.png

2、接下来将与基准相等的值都移动到中间来,按照入如下算法:

image.png

3、一顿猛如虎的操作下来,就变成了:

image.png

4、最后再对小于以及大于基准的两部分子数组进行同样步骤即可

代码贴贴:

public void quick_sort(int nums[],int lo,int hi){
    //递归临界条件
    if(lo >= hi) return;

    /***************优化*******************/
    //选取基准
    int x = nums[lo];
    int lt = lo,i = lo + 1,gt = hi;
    while(i <= gt){
        if(nums[i] < x) swap(nums,i++,lt++);
        else if(nums[i] > x) swap(nums,i,gt--);
        else i++;
    }
    /*************************************/

    quick_sort(nums,lo,lt-1);
    quick_sort(nums,gt+1,hi);
}

结尾

本文介绍了快速排序在考虑不同情况下不同的优化方式,实际开发中,基本都是对于各种可能使性能变差的情况进行了优化,不单单只是像文章中代码那样单独优化,希望这篇文章对大家有所帮助!