zoukankan      html  css  js  c++  java
  • 常用排序算法(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种排序的实现。

  • 相关阅读:
    Sonar安装与使用说明
    oracle物化视图使用+hibernate
    CAS实现SSO单点登录原理(转)
    冒泡事件通用解法
    百度网盘搜索
    (转)mvn clean install 与 mvn install 的区别(为啥用clean)
    初识Channel
    Java 重入锁 ReentrantLock
    多线程之Future模式
    一天一个类--NIO 之Buffer
  • 原文地址:https://www.cnblogs.com/zhqherm/p/12008696.html
Copyright © 2011-2022 走看看