归并排序及其并行化

1.简介

1.1 算法思想

归并排序是分治法(Divide and Conquer)的一个典型的应用,属于比较类非线性时间排序。比较类排序中性能最佳,应用广泛。

归并排序先使每个子列有序,再将子列合并成有序列。若将两个子序列合并成一个有序列,称为二路归并。

1.2 排序过程

设有数列 {16,23,100,3,38,128,23}
初始状态:16,23,100,3,38,128,23
第一次归并后:{16,23},{3,100},{38,128},{23};
第二次归并后:{3,16,23,100},{23,38,128};
第三次归并后:{3,16,23,23,38,100,128}。
完成排序。

1.3 复杂度分析

时间复杂度:最好、最坏和平均时间复杂度都是 O(nlogn),排序性能不受待排序数据的混乱程度影响,比较稳定,这也是相对于快排的优势所在。

空间复杂度为:O(n)。合并子序列时需要用到辅助空间,长度为数列长度 n。

稳定性:稳定,从上文排序过程中可以看出,黑体 23 一直在前面。

2.二路归并实现

2.1 C++ 串行实现

/************************************************
*函数名称:mergearray
*参数:a:待归并数组;first:开始下标;mid:中间下标;
*     last:结束下标;temp:临时数组
*说明:将有二个有序数列a[first...mid]和a[mid...last]合并 
*************************************************/
void mergearray(int a[], int first, int mid, int last, int temp[])  {  
    int i = first, j = mid + 1,k =0;    
    while (i <= mid && j <= last) {  
        if (a[i] <= a[j])  
            temp[k++] = a[i++];  
        else  
            temp[k++] = a[j++];  
    }    
    while (i<= mid)  
        temp[k++] = a[i++];  
      
    while (j <= last)  
        temp[k++] = a[j++];   
    for (i=0; i < k; i++)  
        a[first+i] = temp[i];  
}  
/************************************************
*函数名称:mergesort
*参数:a:待归并数组;first:开始下标;
*     last:结束下标;temp:临时数组
*说明:实现给定数组区间的二路归并排序 
*************************************************/
void mergesort(int a[], int first, int last, int temp[]) {
    if (first < last) {  
        int mid = (first + last) / 2; 		
        mergesort(a, first, mid, temp);    //左边有序  
        mergesort(a, mid + 1, last, temp); //右边有序  
		mergearray(a, first, mid, last, temp); //再将二个有序数列合并      
    }  
}

本机测试 100 * 1024 * 1024 = 100M 个 32bits 整型,串行需要 15.536s,以下是本机软硬件参数,为 Linux 平台。

这里写图片描述

2.2 C++ 并行实现

2.2.1 并行思路

将待排序数组通过偏移量进行逻辑切分为多块,将每个块传递给多个线程调用二路归并排序函数进行排序。待各个块内有序后,再合并各个块整合成有序数列。

2.2.2 并行代码

线程函数,供创建出来的线程调用。

/*******************************************
*函数名称:merge_exec
*参数:   para指针,用于接收线程下边,表示第几个线程
*说明:   调用二路归并排序
*******************************************/
void* merge_exec(void *para) {
	int threadIndex = *(int*)para;
	int blockLen = DataNum/threadNum;
	int* temp = new int[blockLen];
	int offset = threadIndex*blockLen;
  	mergesort(randInt, offset, offset+blockLen-1, temp);
}

合并多个已经排好序的块。代码如下:

/***********************************************
*函数名称:mergeBlocks
*参数:   pDataArray:块内有序的数组 arrayLen:数组长度
*        blockNum:块数 resultArray:存放排序的结果
*说明:   合并有序的块
************************************************/
inline void mergeBlocks(int* const pDataArray,int arrayLen,const int blockNum,int* const resultArray) {
    int blockLen=arrayLen/blockNum;
    int blockIndex[blockNum];//各个块中元素在数组中的下标,VC可能不支持变量作为数组的长度,解决办法可使用宏定义
    // 初始化块内元素起始下标。
    for(int i=0;i<blockNum;++i) {
        blockIndex[i]=i*blockLen;
    }
    int smallest=0;
    // 扫描所有块内的所有元素。
    for(int i=0;i<arrayLen;++i) {
      // 以第一个未扫描完的块内元素作为最小数。
      for(int j=0; j<blockNum; ++j) {
       if(blockIndex[j]<(j*blockLen+blockLen)) {
        smallest=pDataArray[blockIndex[j]];
        break;
       }
      }
      // 扫描各个块,寻找最小数。
      for(int j=0;j<blockNum;++j) {
        if((blockIndex[j]<(j*blockLen+blockLen))&&(pDataArray[blockIndex[j]]<smallest)) {
          smallest=pDataArray[blockIndex[j]];
        }
      }
      // 确定哪个块内元素下标进行自增。
      for(int j=0;j<blockNum;++j) {
        if((blockIndex[j]<(j*blockLen+blockLen))&&(pDataArray[blockIndex[j]]==smallest)) {
          ++blockIndex[j];
          break;
        }
      }
      // 本次循环最小数放入结果数组。
      resultArray[i]=smallest;
    }
}

main 函数中创建多线程完成并行排序,代码如下:

int main(int argc,char* argv[]) {
    int threadBum=8;
    int blockNum=threadNum;
    struct timeval ts,te;
    srand(time(NULL));
    for(int i=0;i<DataNum;++i) {
      randInt[i]=rand();
    }
    pthread_t tid[blockNum],ret[blockNum],threadIndex[blockNum];
    
    //--------Two-way Merge Sort-------
    gettimeofday(&ts,NULL);
    for(int i = 0; i < threadNum; ++i) {
        threadIndex[i]=i;
        ret[i] = pthread_create(&tid[i], NULL,merge_exec,(void *)(threadIndex+i));
        if(ret[i] != 0){
             cout<<"thread "<<i<<" create error!"<<endl;
             break;
         }
    }
    for(int i = 0; i <threadNum; ++i) {
         pthread_join(tid[i], NULL);
    }
    mergeBlocks(randInt, DataNum, threadNum, resultInt);
    gettimeofday(&te, NULL);
    cout<<"MergeSort time: "<<(te.tv_sec-ts.tv_sec)*1000+(te.tv_usec-ts.tv_usec)/1000<<"ms"<<endl;
    }

8 线程情况下,测试性能为 4.223s,加速比 3.68。针对机器的缓存大小,通过提高缓存命中率,可继续进行算法优化,提高排序性能。


参考文献

白话经典算法系列之五 归并排序的实现

  • 12
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值