STL 泛型编程 第三周

变易算法

C++ STL的变易算法是一组能够修改容器元素数据的模板函数,可进行序列容器的复制、交换、替换、填充、移除、旋转等。这些算法对迭代器有较高的要求,具体的迭代器类型随各个算法而定,或向前迭代器、或双向迭代器、又或者是随机迭代器,以提供算法所需要的迭代器操作。应用变易算法时,先要检查容器的迭代器是否符合要求,防止产生编译错误。


元素复制算法copy补充

该算法主要用于容器之间元素的拷贝,即将迭代器区间[first,last)的元素复制到由复制目 标result给定的区间[result,result+(last-first))中。下面我们来看看它的函数原型:

1     template  
2        OutputIterator copy(  
3           InputIterator _First,   
4           InputIterator _Last,   
5           OutputIterator _DestBeg  
6        );  

参数

_First, _Last

指出被复制的元素的区间范围[ _First,_Last).

_DestBeg 

指出复制到的目标区间起始位置

返回值

返回一个迭代器,指出已被复制元素区间的最后一个位置

程序示例:

首先我们来一个简单的示例,定义一个简单的整形数组myints,将其所有元素复制到容器myvector中,并将数组向左移动一位。


 1 #include   
 2 #include   
 3 #include   
 4   
 5 using namespace std;  
 6   
 7 int main ()   
 8 {  
 9     int myints[] = {10, 20, 30, 40, 50, 60, 70};  
10     vector myvector;  
11     vector::iterator it;  
12       
13     myvector.resize(7);   // 为容器myvector分配空间  
14       
15     //copy用法一:  
16     //将数组myints中的七个元素复制到myvector容器中  
17     copy ( myints, myints+7, myvector.begin() );  
18       
19     cout << "myvector contains: ";  
20     for ( it = myvector.begin();  it != myvector.end();  ++it )  
21     {  
22         cout << " " << *it;  
23     }  
24     cout << endl;  
25   
26     //copy用法二:  
27     //将数组myints中的元素向左移动一位  
28     copy(myints + 1, myints + 7, myints);  
29   
30     cout << "myints contains: ";  
31     for ( size_t i = 0; i < 7; ++i )  
32     {  
33         cout << " " << myints[i];  
34     }  
35     cout << endl;  
36   
37     return 0;  
38 }  


从上例中我们看出copy算法可以很简单地将一个容器里面的元素复制至另一个目标容器中,上例中代码特别要注意一点就是myvector.resize(7);这行代码,在这里一定要先为vector分配空间,否则程序会崩,这是初学者经常犯的一个错误。其实copy函数最大的威力是结合标准输入输出迭代器的时候,我们通过下面这个示例就可以看出它的威力了。

1 #include 2 #include 3 #include 4 #include 5 #include 6 7 using namespace std; 8 9 int main () 10 { 11 typedef vector IntVector; 12 typedef istream_iterator IstreamItr; 13 typedef ostream_iterator OstreamItr; 14 typedef back_insert_iterator< IntVector > BackInsItr; 15 16 IntVector myvector; 17 18 // 从标准输入设备读入整数 19 // 直到输入的是非整型数据为止 请输入整数序列,按任意非数字键并回车结束输入 20 cout << "Please input element:" << endl; 21 copy(IstreamItr(cin), IstreamItr(), BackInsItr(myvector)); 22 23 //输出容器里的所有元素,元素之间用空格隔开 24 cout << "Output : " << endl; 25 copy(myvector.begin(), myvector.end(), OstreamItr(cout, " ")); 26 cout << endl; 27 28 return 0; 29 }

元素变换transform补充

    template

     OutIt transform(InIt first, InIt last, OutIt x, Unop uop);

将一个迭代器区间[first,last)中元素,执行一元函数对象op操作,变换的结果放到[x,x+(last-first))中。

    template

    OutIt transform(InIt1 first1, InIt1 last1, InIt2 first2,OutIt x, Binop bop);

    将一个迭代器区间[first1,last1)中元素*i,依次与[first2,first2+(last1-first1))区间中元素*j执行二元函数操作bop(*i,*j),变换的结果放到[x,x+(last1-first1))中。


排序  Sorting

   注意: 对于被排序的元素,需要提供operator<的操作

   整体排序

   部分排序:first到mid 中有序,mid到last无序;

  两分搜索:排序是二分搜索的基础;  注意:二分搜索的复杂度是LOG2N;

  merge算法:两个序列合并在一起,前提是两个序列是排好序的;

Heap算法 及补充


堆并不是STL的组件,但是经常充当着底层实现结构。比如优先级队列(Priority Queue)等等。

堆是一种完全二叉树,因此我们可以用数组来存储所有节点。在这里的实现中,采用了一个技巧:将数组中索引为0的元素保留,设置为极大值或者为极小值(依据大顶堆或者小顶堆而定)。那么当某个节点的索引是i时,其左子节点索引为2*i,右子节点索引为2*i+1.父节点是i/2(这里/表示高斯符号,取整)。这种以数组表示树的方式,我们成为隐式表述法(implicit reprentation)。我们这里用C++ STL中的容器vector实现替代数组的功能。

STL 泛型编程 第三周_第1张图片


堆分为大顶堆和小顶堆。这里介绍并实现的是大顶堆。


堆的主要相关算法介绍


push_heap算法

此操作是向堆中添加一个节点。为了满足完全二叉树的条件,新加入的元素一定放在最下面的一层作为叶节点,并填补在由左至右的第一个空格,在这里放在底层容器vector的end()处。

很显然,新元素的加入很可能使得堆不在满足大顶堆的性质---每个节点的键值都大于或等于其子节点的键值。为了调整使得其重新满足大顶堆的特点,在这里执行一个上溯(percolate up)操作:将新节点与父节点比较,如果其键值比父节点大,就交换父子的位置,如此一直上溯,直到不需要交换或者到根节点为止。


pop_heap算法

此操作取走根节点。对于大顶堆,取得的是堆中值最大的节点,对于小顶堆,取得的是堆中值最小的节点。STL实现并不是将这个节点直接删除,而是将其放在底层容器vector的尾端。而原尾端的节点插入到前面的适当位置。

我们首先保存原vector尾端的节点值,然后将根节点值存储在此处。为了实将原尾端节点的值插入适当位置,重新构建大顶堆,我们实施如下调整堆的操作:

先执行下溯(percolate down)操作:从根节点开始将空洞节点(一开始是根节点)和较大子节点交换,并持续向下进行,直到到达叶节点为止。然后将已保存的原容器vector尾端节点赋给这个已到达叶层的空洞节点。

注意,到这里并没有结束。因为这时候可能还没有满足大顶堆的特性。还需要执行一次上溯操作。这样,便重新构建了大顶堆。


make_heap算法

此操作是依据已有的各元素构建堆。

其中,各元素已经存放在底层容器vector中。

构建堆实质是一个不断调整堆(即前面pop_heap算法中的调整堆的操作)的过程---通过不断调整子树,使得子树满足堆的特性来使得整个树满足堆的性质。

叶节点显然需要调整,第一个需要执行调整操作的子树的根节点是从后往前的第一个非叶结点。从此节点往前到根节点对每个子树执行调整操作,即可构建堆。


sort_heap算法

堆排序算法。执行此操作之后,容器vector中的元素按照从小到大的顺序排列。

构建大顶堆之后,不断执行pop_heap算法取出堆顶的元素,即可。因为每次取得的是最大的元素,将其放在容器vector的最尾端。所以到最后vector中的元素是从小到大排列的。

代码实现

  
  
  
  
  1. #include   
  2. #include   
  3. #include   
  4.   
  5. #define MAX_VALUE 999999 //某个很大的值,存放在vector的第一个位置(最大堆)  
  6.   
  7. const int StartIndex = 1;//容器中堆元素起始索引  
  8.   
  9. using namespace std;  
  10.   
  11. //堆类定义  
  12. //默认比较规则less  
  13. template  >  
  14. class MyHeap{  
  15. private:  
  16.     vector heapDataVec;//存放元素的容器  
  17.     int numCounts;//堆中元素个数  
  18.     Compare comp;//比较规则  
  19.   
  20. public:  
  21.     MyHeap();  
  22.   
  23.     vector getVec();  
  24.   
  25.     void initHeap(ElemType *data,const int n);//初始化操作  
  26.     void printfHeap();//输出堆元素  
  27.     void makeHeap();//建堆  
  28.     void sortHeap();//堆排序算法  
  29.     void pushHeap(ElemType elem);//向堆中插入元素  
  30.     void popHeap();//从堆中取出堆顶的元素  
  31.     void adjustHeap(int childTree,ElemType adjustValue);//调整子树  
  32.     void percolateUp(int holeIndex,ElemType adjustValue);//上溯操作  
  33. };  
  34.   
  35. template   
  36. MyHeap::MyHeap()  
  37. :numCounts(0)  
  38. {  
  39.     heapDataVec.push_back(MAX_VALUE);  
  40. }  
  41.   
  42. template   
  43. vector MyHeap::getVec()  
  44. {  
  45.     return heapDataVec;  
  46. }  
  47.   
  48. template   
  49. void MyHeap::initHeap(ElemType *data,const int n)  
  50. {  
  51.     //拷贝元素数据到vector中  
  52.     for (int i = 0;i < n;++i)  
  53.     {  
  54.         heapDataVec.push_back(*(data + i));  
  55.         ++numCounts;  
  56.     }  
  57. }  
  58.   
  59. template   
  60. void MyHeap::printfHeap()  
  61. {  
  62.     cout << "Heap : ";  
  63.     for (int i = 1;i <= numCounts;++i)  
  64.     {  
  65.         cout << heapDataVec[i] << " ";  
  66.     }  
  67.     cout << endl;  
  68. }  
  69.   
  70. template   
  71. void MyHeap::makeHeap()  
  72. {  
  73.     //建堆的过程就是一个不断调整堆的过程,循环调用函数adjustHeap依次调整子树  
  74.     if (numCounts < 2)  
  75.         return;  
  76.     //第一个需要调整的子树的根节点多音  
  77.     int parent = numCounts / 2;  
  78.     while(1)  
  79.     {  
  80.         adjustHeap(parent,heapDataVec[parent]);  
  81.         if (StartIndex == parent)//到达根节点  
  82.             return;  
  83.   
  84.         --parent;  
  85.     }  
  86. }  
  87.   
  88. template   
  89. void MyHeap::sortHeap()  
  90. {  
  91.     //堆排序思路  
  92.     //每执行一次popHeap操作,堆顶的元素被放置在尾端,然后针对前面的一次再执行popHeap操作  
  93.     //依次下去,最后即得到排序结果  
  94.     while(numCounts > 0)  
  95.         popHeap();  
  96. }  
  97.   
  98. template   
  99. void MyHeap::pushHeap(ElemType elem)  
  100. {  
  101.     //将新元素添加到vector中  
  102.     heapDataVec.push_back(elem);  
  103.     ++numCounts;  
  104.   
  105.     //执行一次上溯操作,调整堆,以使其满足最大堆的性质  
  106.     percolateUp(numCounts,heapDataVec[numCounts]);  
  107. }  
  108.   
  109. template   
  110. void MyHeap::popHeap()  
  111. {  
  112.     //将堆顶的元素放在容器的最尾部,然后将尾部的原元素作为调整值,重新生成堆  
  113.     ElemType adjustValue = heapDataVec[numCounts];  
  114.     //堆顶元素为容器的首元素  
  115.     heapDataVec[numCounts] = heapDataVec[StartIndex];  
  116.     //堆中元素数目减一  
  117.     --numCounts;  
  118.   
  119.     adjustHeap(StartIndex,adjustValue);  
  120. }  
  121.   
  122. //调整以childTree为根的子树为堆  
  123. template   
  124. void MyHeap::adjustHeap(int childTree,ElemType adjustValue)  
  125. {  
  126.     //洞节点索引  
  127.     int holeIndex = childTree;  
  128.     int secondChid = 2 * holeIndex + 1;//洞节点的右子节点(注意:起始索引从1开始)  
  129.     while(secondChid <= numCounts)  
  130.     {  
  131.         if (comp(heapDataVec[secondChid],heapDataVec[secondChid - 1]))  
  132.         {  
  133.             --secondChid;//表示两个子节点中值较大的那个  
  134.         }  
  135.   
  136.         //上溯  
  137.         heapDataVec[holeIndex] = heapDataVec[secondChid];//令较大值为洞值  
  138.         holeIndex = secondChid;//洞节点索引下移  
  139.         secondChid = 2 * secondChid + 1;//重新计算洞节点右子节点  
  140.     }  
  141.     //如果洞节点只有左子节点  
  142.     if (secondChid == numCounts + 1)  
  143.     {  
  144.         //令左子节点值为洞值  
  145.         heapDataVec[holeIndex] = heapDataVec[secondChid - 1];  
  146.         holeIndex = secondChid - 1;  
  147.     }  
  148.     //将调整值赋予洞节点  
  149.     heapDataVec[holeIndex] = adjustValue;  
  150.   
  151.     //此时可能尚未满足堆的特性,需要再执行一次上溯操作  
  152.     percolateUp(holeIndex,adjustValue);  
  153. }  
  154.   
  155. //上溯操作  
  156. template   
  157. void MyHeap::percolateUp(int holeIndex,ElemType adjustValue)  
  158. {  
  159.     //将新节点与其父节点进行比较,如果键值比其父节点大,就父子交换位置。  
  160.     //如此,知道不需要对换或直到根节点为止  
  161.     int parentIndex = holeIndex / 2;  
  162.     while(holeIndex > StartIndex && comp(heapDataVec[parentIndex],adjustValue))  
  163.     {  
  164.         heapDataVec[holeIndex] = heapDataVec[parentIndex];  
  165.         holeIndex = parentIndex;  
  166.         parentIndex /= 2;  
  167.     }  
  168.     heapDataVec[holeIndex] = adjustValue;//将新值放置在正确的位置  
  169. }  


  
  
  
  
  1. #include "Heap.h"  
  2.   
  3. #include   
  4.   
  5. using namespace  std;  
  6.   
  7. int main()  
  8. {  
  9.     const int n = 9;  
  10.     int data[n] = {0,1,2,3,4,8,9,3,5};  
  11.   
  12.     MyHeap *intHeapObj = new MyHeap;  
  13.   
  14.     intHeapObj->initHeap(data,n);  
  15.     intHeapObj->printfHeap();  
  16.   
  17.     intHeapObj->makeHeap();  
  18.     intHeapObj->printfHeap();  
  19.   
  20.     intHeapObj->pushHeap(7);  
  21.     intHeapObj->printfHeap();  
  22.   
  23.     intHeapObj->popHeap();  
  24.     cout << "The top of heap :" << intHeapObj->getVec().back() << endl;  
  25.     intHeapObj->getVec().pop_back();  
  26.     intHeapObj->printfHeap();  
  27.   
  28.     intHeapObj->sortHeap();  
  29.     cout << "Sorted data :";  
  30.     for (int i = 1;i <= n;++i)  
  31.         cout << intHeapObj->getVec()[i] << " ";  
  32.     cout << endl;  
  33.         delete intHeapObj;  
  34.     return 0;  
  35. }  


数值算法  (具体示意图都在讲稿PPT中)

accumulate 算法:对每个元素进行累加;

inner-product:实际上就是数学上的内积,点积;但是可以通过function1 自定义乘法,function2定义加法;

partial-sum   (1)Z字形加法:  锯齿状 相加;

partial-sum   (2)partial  minus  实际上是减法

adjacent difference 相邻两个元素做差值;


内存分配器补充知识:

内存分配器的核心思想概括起来三条:

1.基本功能:首先将内存区(Memory Pool)以最小单位(chunk)定义出来,然后区分对象大小分别管理内存,小内存分成若干类(size class),专门用来分配固定大小的内存块,并用一个表管理起来,降低内部碎片(internal fragmentation)。大内存则以页为单位管理, 配合小对象所在的页,降低碎片。设计一个好的存储方案,即metadata的存储,减少对内存的占用。同时优化内存信息的存储,以使对每个size class或大内存区域的访问的性能最优且有上限(bounded limits)。

比如dlmalloc定义的是一个个bins(同size class)来存储不同大小的内存块:

STL 泛型编程 第三周_第2张图片

2.回收及预测功能: 当释放内存时,要能够合并小内存为大内存,根据一些条件,该保留的就保留起来,在下次使用时可以快速的响应。不需要保留时,则释放回系统,避免长期占用。

3.优化多线程下性能问题:针对多线程环境下,每个线程可以独立占有一段内存区间,被称为TLS(Thread Local Storage),这样线程内操作时可以不加锁,提高性能。下图是MSDN上贴出的关于TLS的原理图,可以参考:

STL 泛型编程 第三周_第3张图片

*另外测试工具也是必不可少,比如tcmalloc的heap profile, jemalloc则结合valgrind。FireFox在移植jemalloc到Android时,特别关掉了TLS,想必是考虑到它对于线程单一应用的副作用。

上面这些思路对于各个分配器而言基本是一致,但具体如何组织size classes, 如果以一个固定步长,必将形成一个巨大且效率低下的表,原因参考第一张图就明白了。很多年前,就有专门的论文对此做了评定(链接)。另外还有如何定位内存块? 如何解决多线程下的false cache line问题? 不同的分配器使用了不同的算法和数据结构来实现。它们所使用的算法统称为DSA, Dynamic Storage Algorithms。

具体的算法实现可以在下面的参考列表中找到对应的文档, 也可以先看附16,文中分别对DSA Algorithms和DSA Operational Model做了描述,概括的很好,会有一个总体的印象。作者将DSA算法分为五类:

1. Sequential Fit

是基于一个单向或双向链表管理各个blocks的基础算法,因为和blocks的个数有关,性能比较差。这一类算法包括Fast-Fit, First-Fit, Next-Fit, and Worst-Fit。

2. Segregated Free List(离散式空闲列表)

使用一个数组,每个元素是存储特定大小内存块的链表,它们所代表的大小并不是连续的,所以称为离散。经典的dlmalloc使用的就是这个算法。数据元素,参照上面的图就可以理解了。TLSF算法则是基于此进行了改进。

3. Buddy System

这是由一代大师Donald Knuth提出,后续产生许多的改进版本。最大的作用是解决外部碎片(external fragmentation), 详细的算法,参考这篇(浅析Linux内核内存管理之Buddy System)。

4. Indexed Fit

以某种数据结构为每个block建立索引,以求可以快速存取。一般以一个二叉树结构实现。比如使用Balanced Tree的Best Fit allocator, 以及基于Cartesian tree 的Stephenson Fast-Fit allocator。这类算法的性能比较高,也比较稳定。

5. Bitmap Fit

这类算法只是索引方法不同,使用以位图式字节表示存储单元的状态。它的好处是使用一小块连续的内存,响应性能更好。Half-Fit就属于这类算法。

随着技术演进,现在主流的allocators, 基本上都是综合运用了两类以上的算法。

另外一些基础算法也是相似的,比如以二叉树组织列表的算法,也就是in-place, 笛卡尔树 和red-block的差异。在线程上,则因为实现的不同,会导致内存占用的差异。比如jemalloc在释放时,并不需要在原来的分配线程执行释放,只是被放回到分配线程的free list中去,ptmalloc则必须回到分配线程里执行释放,性能就相对弱一些。 tcmalloc则设计了算法,让一个线程可以从它的邻居那里偷一些空间来(这个过程称为transfer cache),这样可以有效地利用线程间的内存。

优劣势对比

ptmalloc 劣势:多线程下的性能及内存占用(线程间内存无法共享),并且内存用于存储metadata开销较大,在小内存分配上浪费比较多。优势:算是标准实现。

tcmalloc 劣势:因为算法的设计,占用的内存较大。优势:多线程下的性能。参考附6。

jemalloc 优势: 内存碎片率低,多核下性能较tcmalloc更好。参考附17。

时间有限,没有再深入研究,后面有空再补充一下。在实际应用中,还是有一些参数可以调整的,前提是要熟悉其实现,特别是性能评估的方法。

转自:csdn博客  Horky

as发射点发

你可能感兴趣的:(STL 泛型编程 第三周)