上文介绍了分治法应用实现合并排序,本篇介绍一下分治法应用快速排序算法的理解介绍。
快速排序又称分划交换排序,其设计方法与合并排序不同。其分解方法是:在待排序的序列中选择一个元素作为分划元素,称之为主元。在经过一趟特殊分划规则处理后,分划元素左侧元素都不大于主元,右侧元素都不小于主元,此过程被称为一次分划操作。一次划分后,原序列被划分为两个待排序的子序列,在将两个子序列排序后合并成一个序列,则其为排序完成的序列。
快速排序算法中,使用分划操作将一个问题分解为两个独立的子问题。当子序列为空或有一个元素时,被认为自问题足够小。因为空序列或者只有一个元素的子序列不需要进行任何处理自然是有序的,所以能够被认为足够小。而快速排序算法在子问题足够小时,根据算法本身的主元的性质,在左侧的自然是小的,在右侧的自然是大的,所以快速排序在合并时很容易。 只需要将原有的子序列按分治的顺序合并即可。
合并排序和分治排序都运用了分治策略,合并排序分解方法十分简单,只需将原数列一分为二即可,但合并时需要调用merge函数合并,而快速排序分解方法相对困难,但合并十分简单。这是合并排序和快速排序的不同。
下面介绍快速排序的关键——分划操作。一次分划操作可以划分四步:第一步先定义两个下标变量,i自左向右移动,j自右向左移动,i初始指向数列的最左元素,j指向数列的最右元素,移动i下标直至遇到一个元素不小于主元,i下标停止移动并指向该元素;第二步移动j 下标直至遇到一个不大于主元的函数;第三步交换i和j所指的元素;第四步,重复以上操作直至i>=j,将主元和j下标指向的元素对调即可。以上为一次分划操作的执行过程。注意i和j的大小判断在对应元素交换之前,最后一次的i>=j时,所指元素是不需要对调的,若主元最大,则最后一位元素与主元对调。以下以一段数组演示过程。
以72 26 27 88 42 80 72 48 60 +∞为例:
第一步 :i移至88处 j移至60处 此时i<j 交换对应元素 此时数组为 72 26 27 60 42 80 72 48 88 +∞
第二步: i移至80处 j移至48处 此时i<j 交换对应元素 此时数组为 72 26 27 60 42 48 7280 88 +∞
第三步: i移至72处 j移至72处 此时i>=j 将主元和j指向元素对调 此时元素为 72 26 57 60 42 48 72 80 88 +∞
此为一次分划操作。
以下为一次分划函数的代码:
template <class T>
int SortableList<T>::Partition(int left, int right)
{
int i = left, j = right + 1;//i指向左侧,j指向右侧以正无穷防止越界
do
{
do i ++; while(a[i] < a[left]); //找到不小于主元的数
do j --; while(a[j] > a[left]);//找到不大于主元的数
if(i < j)
Swap(a[i], a[j]);//i下标小于j下标时 交换两个元素
}while(i < j);
Swap(a[left], a[j]); //交换主元和j下标指向的元素
return j;
}
快速排序主体函数:
template <class T>
void SortableList<T>::QuickSort(int left, int right)
{
if(left < right) //当递归至最底层子序列只剩最后一个元素时退出 此时数组已排列完成
{
int j = RPartition(left, right);//j指向的元素
QuickSort(left, j - 1);//左侧排序
QuickSort(j + 1, right);//右侧排序
}
}
快速排序算法还可以进行改进,因为当以第一个元素为主元时,此时若数列为递增或者递减数列,此时会出现时间效率最低的情况。此时采用随机选取元素作为主元或者选取中间元素作为主元的方法。笔者选择的随机选择元素作为主元的方法。
快速排序在数组元素个数下降到一定程度时,效率反而不如普通的直接插入法。所以可以选择在子数列元素小于10(举例)时采用直接插入法提高效率。
以下为整体代码:
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
void Swap(int &a, int &b)
{
int t = a;
a = b;
b = t;
}
template <class T>
class SortableList
{
public:
SortableList(int m)
{
n = m;
}
void QuickSort();
void Input();
void Init();
void Output();
private:
int RPartition(int left, int right);
int Partition(int left, int right);
void QuickSort(int left, int right);
T l[1000];//输入的数组值
T a[1000];//实际排序对象
int n;
};
template<class T>
void SortableList<T>::Input()
{
for(int i = 0; i < n; i++)
cin >> l[i];
}
//Init()函数的作用是在两路合并排序结束后将序列恢复到初始序列
//再进行快速排序
template<class T>
void SortableList<T>::Init()
{
for(int i =0; i < n; i++)
a[i] = l[i];
}
template<class T>
void SortableList<T>::Output()
{
for(int i = 0; i < n; i++)
cout << a[i] << " ";
cout << endl << endl;
}
//快速排序
template <class T>
int SortableList<T>::RPartition(int left, int right)
{
srand((unsigned)time(NULL));
int i = rand() % (right - left) + left;//以上下界中间的一个随机元素作为主元
Swap(a[i], a[left]);
return Partition(left, right);
}
template <class T>
int SortableList<T>::Partition(int left, int right)
{
int i = left, j = right + 1;//i指向左侧,j指向右侧以正无穷防止越界
do
{
do i ++; while(a[i] < a[left]); //找到不小于主元的数
do j --; while(a[j] > a[left]);//找到不大于主元的数
if(i < j)
Swap(a[i], a[j]);//i下标小于j下标时 交换两个元素
}while(i < j);
Swap(a[left], a[j]); //交换主元和j下标指向的元素
return j;
}
template <class T>
void SortableList<T>::QuickSort(int left, int right)
{
if(left < right) //当递归至最底层子序列只剩最后一个元素时退出 此时数组已排列完成
{
int j = RPartition(left, right);//j指向的元素
QuickSort(left, j - 1);//左侧排序
QuickSort(j + 1, right);//右侧排序
}
}
template<class T>
void SortableList<T>::QuickSort()
{
QuickSort(0, n - 1);
}
int main()
{
int m;
cout << "数组长度n: ";
cin >> m;
SortableList<int> List(m);
cout << "输入" << m << "个数字:" << endl;
List.Input();
List.Init();//恢复初始状态
cout << "快速排序后:" << endl;
List.QuickSort();
List.Output();
return 0;
}
此次借考试之机好好总结一下快速排序算法的执行过程和原理。
特此记下,以备回顾。