给出一组含有关键字的未排序字符串,这些关键字可视为整数的二进制表示,关键字内的各个位可以用来对这组字符串进行排序。这种排序方法被称为基数排序。
请编写一个用多线程实现基数排序算法的程序:对从输入文件读取的关键字进行排序,然后将排序后的关键字输出到另一个文件。输入文件名和输出文件名应为执行程序命令行的第一和第二个参数。
输入文件中的第一行是要排序的关键字总个数 (N);后面紧跟 N 个关键字,每行一个;关键字是由 7 个可打印字符组成的字符串,不含空格 (ASCII 0x20)。文件中关键字的个数小于 2^31 - 1。排序后的输出结果必须保存在文本文件中,每行一个关键字;
计时:如果您在程序中加入计时代码来计算排序过程所用的时间并报告已用的时间,将用这个时间来计分;如果不加入计时代码,将使用整个执行时间(包括输入时间和输出时间)来计分。
输入文件样例:
8
H@skell
surVEYs
sysTEMS
HASKELL
Surveys
1234567
SURveys
systEMS
输出文件样例:
1234567
H@skell
HASKELL
SURveys
Surveys
surVEYs
sysTEMS
systEMS
串行算法
"基数排序法"(radix sort)则是属于"分配式排序"(distribution sort),基数排序法又称"桶子法"(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些"桶"中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的比较性排序法。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
显然MSD更适合并行处理,我们只需要像快速排序那样按照指定的bit位为1的和为0的分开,让为0的在前面,为1的在后面就可以了,然后递归由高到低的处理每个bit位。
本问题需要排序的为7个字符,共7 * 8 = 56 bit,加上换行符共64bit刚好可以放入一个int64变量中。
下面给出不使用小数据排序展开优化的源码,使用展开的代码较长,就不放入文档中了。
//基数排序,返回分割点不使用展开优化
__inline uint64* XPartition64(uint64* pBeg, uint64* pEnd, uint64 nBitMask)
{
//判断如果少于一个数据无需排序
if(pBeg + 1 < pEnd)
{
uint64 *i = pBeg - 1, *j = pEnd;
while ((++i) < pEnd && (*i & nBitMask) == 0);
while ((--j) > pBeg && (*j & nBitMask));
while (i < j)
{
const uint64 t = *i; *i = *j; *j = t;
do ++i; while ((*i & nBitMask) == 0);
do --j; while ((*j & nBitMask));
}
return i;
}
else
{
return NULL;
}
};
//基数排序_串行版本
void XRadixSort64Serial(uint64* pBeg, uint64* pEnd, uint64 nBitMask)
{
uint64* pSplit = XPartition64(pBeg, pEnd, nBitMask);
if(pSplit != NULL && nBitMask != XRADIX_MASK_END)
{
XRadixSort64Serial(pBeg, pSplit, XRADIX_MASK_SHIFT_64(nBitMask));
XRadixSort64Serial(pSplit, pEnd, XRADIX_MASK_SHIFT_64(nBitMask));
}
}
代码跟快速排序的代码几乎一致,只是对比较方法及分割位置的处理有些不同,用红色字体标出。
并行算法
分裂式的算法,采用TBB的Task做并行优化是个不错的选择。一次分裂出两个Task,分裂出的Task可以并行处理,彼此之间没有共享数据,可以完全并行。
// 基数排序Task
class CXRadixSort64Task: public tbb::task
{
uint64 *m_pBeg,*m_pEnd,m_nBitMask;
BOOL m_bIsContinuation;
static uint32 ms_nCutOff32;
public:
CXRadixSort64Task( uint64* pBeg, uint64* pEnd, uint64 nBitMask) :
m_pBeg(pBeg), m_pEnd(pEnd), m_nBitMask(nBitMask), m_bIsContinuation(FALSE) { }
tbb::task* execute()
{
tbb::task *pNextA = NULL, *pNextB = NULL;
if(m_pEnd - m_pBeg < ms_nCutOff32)
{ // 基数排序_串行版本
XRadixSort64Serial(m_pBeg, m_pEnd, m_nBitMask);
}
else
{
if( !m_bIsContinuation )
{ // 移动数据并得到分割点
uint64* pSplit = XPartition64(m_pBeg, m_pEnd, m_nBitMask);
if(pSplit != NULL)
{ // 分裂新的Task
pNextA = new( allocate_child() ) CXRadixSort64Task(m_pBeg, pSplit,
XRADIX_MASK_SHIFT_64(m_nBitMask));
pNextB = new( allocate_child() ) CXRadixSort64Task(pSplit, m_pEnd,
XRADIX_MASK_SHIFT_64(m_nBitMask));
m_bIsContinuation = TRUE;
recycle_as_continuation();
set_ref_count(2);
spawn(*pNextB);
}
}
}
return pNextA;
}
static void SetCutOff(uint32 nCutOff)
{
if(nCutOff < 1024)
ms_nCutOff32 = 1024;
else if(nCutOff > XRADIXSORT_CUTOFF)
ms_nCutOff32 = XRADIXSORT_CUTOFF;
else
ms_nCutOff32 = nCutOff >> 10 << 10;
}
};
// 基数排序_并行版本
void XRadixSort64Parallel(uint64* pBeg, uint64* pEnd, uint64 nBitMask)
{
// 计算CUTOFF值
uint32 nSize = (uint32)(pEnd - pBeg) / task_scheduler_init::default_num_threads() / 4;
CXRadixSort64Task::SetCutOff(nSize);
CXRadixSort64Task& xtask = *new(tbb::task::allocate_root())
CXRadixSort64Task(pBeg, pEnd, nBitMask);
tbb::task::spawn_root_and_wait(xtask);
}
在Task处理前先判断数据量的大小,如果小于ms_nCutOff32个数据可直接调用串行算法,减少Task的数量,降低TBB维护Task的开销。ms_nCutOff32的大小根据待排序数组的大小进行动态调整,最大为64k。