按照STL算法适配的主题,第二个适配的快速排序,老规矩,首先上代码,包含头文件两个iterator.h algorithm_sort.h 以及主程序main.cpp,如果你看过我之前这个系列的文章,就知道文件名没有改变,只是在以前的基础上做添加,出于篇幅考虑,之前的代码不再上传。
代码清单
iterator.h
#ifndef _ITERATOR_
#define _ITERATOR_
#include <cstddef>
using namespace std;
namespace XP
{
// iterator template
template <class Category, class T, class Distance = ptrdiff_t,
class Pointer = T*, class Reference = T&>
struct iterator
{
typedef Category iterator_categroy;
typedef T value_type;
typedef Distance difference_type;
typedef Pointer pointer;
typedef Reference reference;
};
// iterator_traits template
template <class I>
struct iterator_traits
{
typedef typename I::iterator_categroy iterator_categroy;
typedef typename I::value_type value_type;
typedef typename I::difference_type difference_type;
typedef typename I::pointer pointer;
typedef typename I::reference reference;
};
// partial specializations of value_type for original pointer
template <class T>
struct iterator_traits<T*>
{
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef T* pointer_type;
typedef T& reference_type;
typedef random_access_iterator_tag iterator_categroy;
};
// partial specializations of value_type for original const pointer
template <class T>
struct iterator_traits<const T*>
{
typedef T value_type; // not const T
typedef ptrdiff_t difference_type;
typedef const T* pointer_type;
typedef const T& reference_type;
typedef random_access_iterator_tag iterator_category;
};
}
#endif
algorithm_sort.h
#ifndef _ALOGRITHM_SORT_
#define _ALOGRITHM_SORT_
#include "iterator.h"
#include <algorithm>
#include <vector>
#pragma region quick_sort
template <class Iterator>
inline Iterator _pivot(Iterator _First, Iterator _Last)
{
if (_Last - _First <= 2) return _First;
iterator_traits<Iterator>::difference_type difference_type;
Iterator it_mid = _First + (_Last - _First) / 2;
/// 取中位数
if ((*_First >= *it_mid || *_First >= *(_Last - 1))
&& (*_First < *it_mid || *_First < *(_Last - 1)))
return _First;
else if ((*(_Last - 1) >= *it_mid || *(_Last - 1) >= *_First)
&& (*(_Last - 1) < *it_mid || *(_Last - 1) < *_First))
return _Last - 1;
else return it_mid;
}
template <class Iterator, class Predicate>
inline Iterator _partition(Iterator _First, Iterator _Last,
Predicate _Predicate)
{
/// 三平均分区法选择中轴
Iterator pivot = _pivot(_First, _Last);
Iterator it_low = _First;
Iterator it_high = _Last - 1;
typedef iterator_traits<Iterator>::value_type value_type;
value_type temp;
value_type pivot_value = *pivot;
while (it_low < it_high){
while (it_low < it_high &&
_Predicate(pivot_value, *it_high))
it_high--;
temp = *it_high;
*it_high = *pivot;
*pivot = temp;
pivot = it_high;
while (it_low < it_high &&
_Predicate(*it_low, pivot_value))
it_low++;
temp = *it_low;
*it_low = *pivot;
*pivot = temp;
pivot = it_low;
}
return pivot;
}
template <class Iterator, class Predicate>
inline void quick_sort(Iterator _First, Iterator _Last,
Predicate _Predicate)
{
if (_Last - _First > 1) {
Iterator it_pivot = _partition(_First, _Last, _Predicate);
quick_sort(_First, it_pivot, _Predicate);
quick_sort(it_pivot + 1,_Last, _Predicate);
}
}
#pragma endregion
#endif
main.cpp
#include "stdafx.h"
#include <iostream>
#include "alogrithm_sort.h"
#include <stdlib.h>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
int ia[5] = { 0, 4, 2, 3, 1};
//merge_sort(&ia[0],ia + 5, less<>());
quick_sort(&ia[0], ia + 5, less<>());
for (int i = 0; i < 5; i++)
{
cout << ia[i] << ' ';
cout << endl;
}
vector<int> iv(ia, ia + 5);
//merge_sort(iv.begin(), iv.end(), less<>());
quick_sort(iv.begin(), iv.end(), less<>());
for (int i = 0; i < 5; i++)
{
cout << iv[i] << ' ';
cout << endl;
}
system("PAUSE");
}
算法分析
仍旧是老样子,这里摘录名师对快速排序的介绍:
快速排序是另一种基于分治技术的重要排序算法。不像合并排序那样是按照元素在数组中的位置对他们进行划分,快速排序是按照元素的值对他们进行划分。具体的说,它对给定数组中的元素进行重新排列,以得到一个快速排序的分区。在一个分区中,所有在S下标之前的元素都小于等于A[S],所有在S下标之后的元素都大于等于A[S].
显然建立了一个分区之后,A[S]已经位于他在有序数组中的最终位置,接下来我们对A[S]后和A[S]前的子数组分别进行排序。
接下来就是如何对数组进行分组,在这里我们使用的是一种基于两次扫描数组的高效方法,一次是从右到左,一次是从左到右,每次将子数组的元素和中轴进行比较。首先是中轴的选择,大多数教材和文章中为了便于理解都是选取子数组的第一个元素作为中轴,这里我们首先也默认为第一个,但是实际上为了改善平均效率,使用三平均分区法能达到更好的状态,即选择首尾和中间三个数的中位数作为中轴。
从左到右的扫描从第二个元素开始(因为第一个数是中轴),因为我们希望小于中轴的数位于中轴的左边,大于中轴的数位于中轴的右边(以升序排列为例),从左开始的扫描会跳过哪些小于中轴的元素,直到遇到第一个比中轴大的元素为止,然后与中轴交换数据。从右到左的扫描刚好相反,跳过比中轴大的元素直到遇到第一个比中轴小的元素,交换数据。扫描持续到两个扫描在中间“相遇”为止,分区返回当前中轴的位置。
适配到STL
关于iterator.h不再赘述,它的作用在于方便理解,实际上该文件中的代码在STL源码中都能找到,针对windows平台,它的位置在xutilty中,在GCC等环境下,可能是另一个名称,但是由于VS强大的编辑环境,还是选择在windows下编辑并测试,同时为了方便适配到GCC对该文件做了重新的整合也就是现在的iterator.h,包含了标准的迭代器定义,迭代器萃取器,偏特化迭代器萃取器。
algorithm_sort.h中主要包含了三个函数_pivot、_partition、quick_sort。quick_sort的函数定义与STL算法中的sort完全一样,包括扩迭代范围,比较函数。_partition是分区函数、_pivot函数则是三平均分区法取中轴函数。
首先来看quick_sort函数,较之于伪代码,它的区别在于表示下标的偏移量消失了,取而代之的就是迭代器,更通用的迭代器让我们的代码不仅可以使用数组,还可以使用各种符合STL标准的容器。另外还加上了这样的判断语句
if (_Last - _First > 1)
quick_sort(_First, it_pivot, _Predicate);
这实际上是一个迭代器的常识问题。正常情况下我们调用quick_sort传入迭代器的开始和结束位置就可以了,但是要注意的是 结束位置是一个不可用的位置,他是最后一个可用位置的结束位置,例如:
vector<int> vec;
quick_sort(vec.begin(),vec.end(), _Predicate);
这是很常见的一种调用形式,在使用的时候可能不会察觉,但是begin到end实际上是一个前闭后开区间,所以判断条件是不能写_Last > _First的,因为如果_First到_Last之间如果只有一个元素,_Last > _First这判断条件还是会成立,但是实际上已经不需要排序了(整个区间都只有一个元素,当然没有排序的必要)。
判断有必要进行排序后,首先要对当前的数据进行分区,也就是_partition函数。_partition函数首先调用了_pivot函数,用于确定当前数据的中轴。_pivot函数很简单,只是在首尾和中间三个数据中取中位数而已,这里不再赘述,但是对学习迭代器的处理可以有帮助。取得中轴之后就可以进行左右两次扫描了,但是这里开起来好像和伪代码写的不太一样。没错,肯定是不一样的,之前说过为了演示算法的方便起见,我们是以第一位作为中轴的
在算法中,从左到右的扫描和从右到左的扫描发现大于中轴或者小于中轴的情况后,直接就交换了两个扫描方向的游标的位置。这样做的前提完全是建立在,左侧第一个数据为中轴的基础上,如果要自己确定中轴,且在程序中不添加任何辅助空间,这里实际上应该改为
swap(A[i],pivot);//pivot代表中轴
swap(A[j],pivot);//pivot代表中轴
体现在我们的代码中为:
while (it_low < it_high){
while (it_low < it_high &&
_Predicate(pivot_value, *it_high))
it_high--;
temp = *it_high;
*it_high = *pivot;
*pivot = temp;
pivot = it_high;
while (it_low < it_high &&
_Predicate(*it_low, pivot_value))
it_low++;
temp = *it_low;
*it_low = *pivot;
*pivot = temp;
pivot = it_low;
}
另一个值得注意的是,我们不仅交换了中轴和低位或者高位的值,同时我们还重新设定了pivot的位置,因为每次和pivot交换,pivot代表的值的位置实际上到了low或者high位置,所以要重新设定,不然会对后面的交换产生印象,导致比对的pivot中轴实际上不是我们设定的值。
还有的老生常谈的问题就是萃取器了,萃取器在这个系列的一开始我就着重介绍过:
typedef iterator_traits<Iterator>::value_type value_type;
value_type temp;
如果觉得这段代码比较晦涩,那么建议回过头去看一下萃取器相关的文章,或者看我之前的介绍,这里就不再介绍。
分区完成后,中轴的位置已经是其最终位置。接下来就是递归了,对左边的子数组和右边子数组进行递归,在这里一定要注意迭代器范围的问题,一定要弄清楚范围量,一般我们遵循的是前闭后开的原则,但是这个也不是一定的,如果你有你的编程习惯,你可以前闭后闭,但是一定要在自己的程序中约定好
quick_sort(_First, it_pivot, _Predicate);
quick_sort(it_pivot + 1,_Last, _Predicate);
上方的pivot是中轴,中轴的位置一定排定,那么在我的子数组中不需要包含,所以左侧子数组的范围直接是_First,it_pivot(后闭不包含it_pivot),同理右侧也不需要包含,
但是由于右侧的前是与pivot相关,是闭区间,我们是不想包含it_pivot中轴的,所以右侧是it_pivot,这种小细节虽然很小,但是往往决定了程序的成功失败与否,是尤其需要注意的。
接下来是最后的测试,跟之前写有关归并排序一样,同样测试原生数组和vector,测试代码如下:
int ia[5] = { 0, 4, 2, 3, 1};
//merge_sort(&ia[0],ia + 5, less<>());
quick_sort(&ia[0], ia + 5, less<>());
for (int i = 0; i < 5; i++)
{
cout << ia[i] << ' ';
cout << endl;
}
vector<int> iv(ia, ia + 5);
//merge_sort(iv.begin(), iv.end(), less<>());
quick_sort(iv.begin(), iv.end(), less<>());
for (int i = 0; i < 5; i++)
{
cout << iv[i] << ' ';
cout << endl;
}
system("PAUSE");
看过我之前归并排序的肯定明白我为什么要注释merge_sort了,你也可以看出,这样的排序十分通用,在程序形式上几乎完全一样,只是函数名发生了变化,我们关心的对象从如何让一个算法适配一个容器变成了完全的专注于算法的设计,容器?随便你使用,反正我都能适配。好了,话不多说,看测试结果
可见设计可行无误。