zhqherm

导航

常用排序算法(3) - 快速排序

常用排序算法(3) - 快速排序

快速排序

算法描述

快速排序,是历史上实践中最快的泛型排序算法,虽然其平均运行时间为O(n log n),而最坏情形下性能为O(n^2),但由于高度优化的内部循环,一般经过优化的快速排序不会出现这种最坏情形。快速排序也是一种分治的递归算法,它的基本步骤如下:

  • 从无序数组中选出一个值作为基准值
  • 把大于基准值的数放在数组右边,小于基准值的数放在数组左边
  • 递归的将左边和右边的数组递归进行快速排序
  • 由此就可得到一个有序数组

可以看出,上面算法描述中有一些点,是没有固定做法的。

  1. 如何选取基准值
  2. 如何将数组分割为大于、等于、小于的三部分

所以,算法中这些没有固定做法的点正是快速排序的可以优化的关注点。

实现

  • 首先,是最简单的快速排序实现,算法中直接选用第0个元素作为基准值。
    但是在这样的情况下,如果输入是预排序或者反序的,那么每次都依然会把所有元素放到同一个分组里面,而无法产生实质的分治策略。

    //快速排序(效率一般的实现)
    template <typename Comparable>
    void quickSort1(vector<Comparable>& a, int left, int right)
    {
        if (left >= right) return;
    
        auto tmp = std::move(a[left]);
        int i = left;
        int j = right;
        while (i < j)
        {
            while (i < j && a[j] >= tmp) --j;
            if (i < j)
            {
                //i是空位,可以把后面的元素放到a[i]位置,移动后,j位置变成了空位
                a[i] = std::move(a[j]);
                ++i;
            }
            while (i < j && a[i] < tmp) ++i;
            if (i < j)
            {
                //j是空位,可以把前面的元素放到a[j]位置,移动后,i位置重新变成空位
                a[j] = std::move(a[i]);
                --j;
            }
        }
        
        a[i] = std::move(tmp);
    
        //递归排序前半部分和后半部分
        quickSort1(a, left, i - 1);
        quickSort1(a, i + 1, right);
    }
    
  • 根据上面的分析,先对如何选取基准值做一些优化,这里使用常见的三数中值分割法。选取左边,右边和中间位置的值,用他们的中值作为基准值。

    //三数中值选取,并将三个数排序,最左边是三数中最小的值,最右边是三数中最大的值,然后把中值放到right - 1的位置上,那么整个比较就只需要从 left 到right - 1。
    template <typename Comparable>
    int median3Num(vector<Comparable>& a, int left, int right)
    {
    	int mid = (left + right) / 2;
    	if (a[left] > a[mid]) std::swap(a[left], a[mid]);
    	if (a[mid] > a[right]) std::swap(a[mid], a[right]);
    	if (a[left] > a[mid]) std::swap(a[left], a[mid]);
    
    	std::swap(a[mid], a[right - 1]);
    	return a[right - 1];
    }
    

    而对于如何将数组分割,跟上面的普通做法相近,但是并不需要有空出一个位置来存放数字的概念,而只需要两边 i 和 j 找到各自不符合的元素后,交换位置即可同时完成两个数字的定位。

    //快速排序
    template <typename Comparable>
    void quickSort(vector<Comparable>& a)
    {
    	if (a.size() < 2) return;
    
    	quickSort(a, 0, a.size() - 1);
    }
    
    template <typename Comparable>
    void quickSort(vector<Comparable>& a, int left, int right)
    {
    	if (left + 10 <= right)
    	{
    		auto& pivot = median3Num(a, left, right);
    
    		int i = left;
    		int j = right - 1;
    		while (i < j)
    		{
    			while (a[++i] < pivot) {}
    			while (a[--j] > pivot) {}
    			if (i < j) std::swap(a[i], a[j]);
    		}
    		
            //恢复基准值位置
    		std::swap(a[i], a[right - 1]);
    		quickSort(a, left, i - 1);  //排序小于基准值的元素
    		quickSort(a, i + 1, right); //排序大于基准值的元素
    	}
    	else InsertionSort(a, left, right); //当数组内数字较少时直接使用插入排序
    }
    
    //插入排序(带左右边界)
    template <typename Comparable>
    void InsertionSort(vector<Comparable>& a, int left, int right)
    {
    	for (int i = left + 1; i <= right; ++i)
    	{
    		auto tmp = std::move(a[i]);
    		int j = i;
    		for (; j > left && tmp < a[j - 1]; --j)
    		{
    			a[j] = std::move(a[j - 1]);
    		}
    		a[j] = std::move(tmp);
    	}
    }
    

    可以看到上面快速排序最后在元素较少时直接使用了插入排序,这是因为较少时,如果数组接近有序,插入排序的复杂度下限可以到O(n),而不再需要递归进行快速排序。这也是一个优化,而许多相关文章的说法是数组元素在10~20左右取值都是合理的。

后记

除了在元素较少时使用插入排序,同样的还有当递归层次较深时转而使用堆排序,堆排序的时间复杂度同样可以到O(n log n),所以整体的空间复杂度和时间复杂度都是可控的。STL里面的排序算法也是综合了3种排序的实现。

posted on 2019-12-08 23:48  zhqherm  阅读(209)  评论(0编辑  收藏  举报