更改内存分配策略改善归并排序效率
2015-03-30 18:48
267 查看
归并排序是一种相当稳健的排序算法,无论何种输入序列,其期望时间复杂度和最坏时间复杂度都是Θ(nlogn),这已经达到了基于比较排序算法的渐进下界。
因此归并排序时常会用于对可能导致quicksort退化的序列排序。
归并排序是典型的分治算法,一个最常见的实现如下:
但是在实践中,归并排序花费的时间往往超过预期,对于普通的序列而言,所花费的时间甚至远远超过quicksort。
究其原因,和归并排序的内存策略有关(不断地分配new与释放free内存)。
归并排序不是原地排序,需要额外的存储空间。并且在每次merge过程中,需要动态分配一块内存以完成对两个数据堆的排序合并。并且排序完毕之后,我们需要将存储空间中的数据复制并覆盖原序列。
最后一步操作是由归并排序自身性质决定,无法优化,所以我们只能针对Merge操作。
经过分析很容易知道,对于长度为n的序列,要执行logn次的merge操作,这意味着需要进行logn次的内存分配和回收。内存操作开销较大。
如果能够一次性分配长度为n的存储空间,那么就省掉了大量的分配操作,可以极大提高效率。
由于归并的分治特性,我们需要在原来的函数基础之上,包装一层驱动函数(driver function)
为了检验性能提升,笔者对100,1000,10000,100000的数据规模分别进行测试,每次测试排序运行100次,得到如下数据表和图示。
ps:为了减少干扰,以上测试均在Release下进行。
经过测试发现,改进后的算法基本上比原始的要快30~50倍。至于表格中1000测试那行,猜测和缓存命中有关。多次测试后,基本也快出30-40倍。
以上改进可以得到一个结论:对于需要频繁分配内存的算法而言,一次性分配或者采用lazy-deletion以提高复用的策略可以大幅提高算法效率。
因此归并排序时常会用于对可能导致quicksort退化的序列排序。
归并排序是典型的分治算法,一个最常见的实现如下:
[code]void mergeSort(int a[], const int low, const int high) { if (low < high) { const int midIndex = (low + high) >> 1; // take apart MergeSort(a, low, midIndex); MergeSort(a, midIndex + 1, high); // merge two parts Merge(a, low, midIndex + 1, high); } return ; } void Merge(int a[], const int lowFirst, const int highFirst, const int highLast) { int l = lowFirst; int r = highFirst; int len = highLast - lowFirst + 1; int* pBuffer = new int[len]; assert(pBuffer != NULL); int* p = pBuffer; while (l < highFirst && r <= highLast) { *p++ = (a[l] <= a[r]) ? a[l++] : a[r++]; } while (l < highFirst) { *p++ = a[l++]; } while (r <= highLast) { *p++ = a[r++]; } p = pBuffer; for (int i = lowFirst; i <= highLast;) { a[i++] = *p++;// special case: i = 10, p[0] } delete [] pBuffer; }
但是在实践中,归并排序花费的时间往往超过预期,对于普通的序列而言,所花费的时间甚至远远超过quicksort。
究其原因,和归并排序的内存策略有关(不断地分配new与释放free内存)。
归并排序不是原地排序,需要额外的存储空间。并且在每次merge过程中,需要动态分配一块内存以完成对两个数据堆的排序合并。并且排序完毕之后,我们需要将存储空间中的数据复制并覆盖原序列。
最后一步操作是由归并排序自身性质决定,无法优化,所以我们只能针对Merge操作。
经过分析很容易知道,对于长度为n的序列,要执行logn次的merge操作,这意味着需要进行logn次的内存分配和回收。内存操作开销较大。
如果能够一次性分配长度为n的存储空间,那么就省掉了大量的分配操作,可以极大提高效率。
由于归并的分治特性,我们需要在原来的函数基础之上,包装一层驱动函数(driver function)
[code]// driver function void _mergeSort(int a[], const int count) { // allocation only once int* pTmpBuf = new int[count]; assert(pTmpBuf != nullptr); _MSort(a, pTmpBuf, 0, count - 1); delete [] pTmpBuf; return ; } // devide the sequence recuresively void _MSort(int a[], int tmpBuffer[]/*extra space*/, const int left, const int right) { if (left < right) { const int midIdx = (left + right) >> 1; _MSort(a, tmpBuffer, left, midIdx); _MSort(a, tmpBuffer, midIdx + 1, right); _Merge(a, tmpBuffer, left, midIdx + 1, right); } return ; } // merge two parts void _Merge(int a[], int tmpBuffer[], const int lBegin, const int rBegin, const int rEnd) { int l = lBegin; int r = rBegin; int bufPos = lBegin; while (l < rBegin && r <= rEnd) { tmpBuffer[bufPos++] = a[l] < a[r] ? a[l++] : a[r++]; } while (l < rBegin) { tmpBuffer[bufPos++] = a[l++]; } while (r <= rEnd) { tmpBuffer[bufPos++] = a[r++]; } for (bufPos = lBegin; bufPos <= rEnd; ++bufPos) { a[bufPos] = tmpBuffer[bufPos]; } return ; }
为了检验性能提升,笔者对100,1000,10000,100000的数据规模分别进行测试,每次测试排序运行100次,得到如下数据表和图示。
ps:为了减少干扰,以上测试均在Release下进行。
经过测试发现,改进后的算法基本上比原始的要快30~50倍。至于表格中1000测试那行,猜测和缓存命中有关。多次测试后,基本也快出30-40倍。
以上改进可以得到一个结论:对于需要频繁分配内存的算法而言,一次性分配或者采用lazy-deletion以提高复用的策略可以大幅提高算法效率。
相关文章推荐
- 一个测试ACE各种内存分配策略效率的程序
- 一个测试ACE各种内存分配策略效率的程序
- 深入垃圾收集器与内存分配策略
- JVM总结(二):JVM的内存分配策略
- Java GC 垃圾回收机制与内存分配策略
- 内存对齐分配策略(含位域模式)
- memcache内存分配策略
- JVM 垃圾收集器与内存分配策略
- JVM基础知识2 垃圾收集器与内存分配策略
- 垃圾收集器与内存分配策略
- Java内存分配与回收策略
- JVM之内存分配与回收策略
- Memcache 内存分配策略和性能(使用)状态检查
- Memcache(MC)系列(六)Memcache内存分配策略
- JVM之垃圾收集器与内存分配策略
- 垃圾收集器与内存分配策略_垃圾收集算法
- 垃圾收集器与内存分配策略
- Java垃圾回收器与内存分配策略
- 深入理解Java虚拟机JVM高级特性与最佳实践阅读总结——第三章垃圾收集器与内存分配策略
- JVM系列(6)-内存分配与回收策略