JayFighting 2019-12-25
第五章、关联容器
(散列表),以及以此hash_table为底层机制而完成的hash_set(散列集合)、hash_map(散列映射表)、hash_multiset(散列多键集合)、hash_multimap
(散列多键映射表)。
即讲解数据结构中的二叉树、二叉搜索树、平衡二叉树。。。
1. 每个节点或是黑色或是红色
2. 根节点是黑色
3. 每个叶节点是黑色(叶节点为空节点)
4. 如果一个节点是红色,则它的两个子节点必须是黑色
5. 从任意的一个节点到该节点的所有叶节点的路径包含相同数目的黑色节点
6.红黑树是一种平衡二叉树,当不是完全的平衡二叉树,红黑树只要求最多三次旋转来尽可能达到平衡
【也就是说没有规定左子树与右子树的高度差必须<=1!!!!!!】
关于红黑树的具体数据结构,请看[博文]
与map一样,但允许键值重复
时间复杂度是衡量算法好坏的重要指标之一。时间复杂度反映的是不确定性样本量的增长对于算法操作所需时间的影响程度,与算法操作是否涉及到样本量以及涉及了几次直接相关,如遍历数组时时间复杂度为数组长度n(对应时间复杂度为O(n)),而对数据的元操作(如加减乘除与或非等)、逻辑操作(如if判断)等都属于常数时间内的操作(对应时间复杂度O(1))。
在化简某算法时间复杂度表达式时需遵循以下规则:
对于同一样本量,可省去低阶次数项,仅保留高阶次数项,如O(n^2)+O(n)可化简为O(n^2),O(n)+O(1)可化简为O(n)
可省去样本量前的常量系数,如O(2n)可化简为O(n),O(8)可化简为O(1)
对于不同的不确定性样本量,不能按照上述两个规则进行化简,要根据实际样本量的大小分析表达式增量。如O(logm)+O(n^2)不能化简为O(n^2)或O(logm)。而要视m、n两者之间的差距来化简,比如m>>n时可以化简为O(logm),因为表达式增量是由样本量决定的。
算法额外空间复杂度指的是对于输入样本,经过算法操作需要的额外空间。比如使用冒泡排序对一个数组排序,期间只需要一个临时变量temp,那么该算法的额外空间复杂度为O(1)。又如归并排序,在排序过程中需要创建一个与样本数组相同大小的辅助数组,尽管在排序过后该数组被销毁,但该算法的额外空间复杂度为O(n)。
算法主要是由头文件<algorithm> <functional> <numeric>组成。
<algorithm>是所有 STL 头文件中最大的一个,其中常用的功能涉及到比较,交换,查找,遍历,复制,修改,反转,排序,合并等...
<numeric>体积很小,只包括在几个序列容器上进行的简单运算的模板函数.
<functional> 定义了一些模板类,用以声明函数对象。
【质变指定是算法的稳定性】
所有的STL算法都作用在由迭代器[first,last)所标示出来的区间上。所谓“质变算法”,是指运算过程中会更改区间内(迭代器所指)的元素内容。诸如拷贝(copy)、互换(swap)、替换(replace)、填写(fill)、删除(remove)、排列组合(permutation)、分割(partition)、随机重排(random shuffling)、排序(sort)等算法,都属此类。
所有的STL算法都作用在由迭代器[first,last)所标示出来的区间上。所谓“非质变算法”,是指运算过程中不会更改区间内(迭代器所指)的元素内容。
诸如查找(find)、匹配(search)、计数(count)、巡访(for_each)、比较(equal,mismatch)、寻找极值(max,min)等算法,都属此类。但是如果你在for_each(巡访每个元素)算法身上应用一个会改变元素内容的仿函数(functor)。
·inequality(判断不相等)操作符
·dereferencelm(提领,取值)操作符
·prefix increment(前置式递增)操作符
·copy(复制)行为(以便产a‘x生函数的返回值)
算法accumulate用来计算init和[first,last)内所有元素的总和。注意,你一定得提供一个初始值init,这么做的原因之一是当[first,last)为空区间时仍能获得一个明确定义的值。如果希望计算[first,1ast)中所有数值的总和,应该将init设为0.
算法adjacent_difference用来计算[first,last)中相邻元素的差额。也就是说,它将*first 赋值给*result,并针对[first+1,last)内的每个迭代器i,将*i-*(i-1)之值赋值给*(result+(i-first))。
注意,你可以采用就地(in place)运算方式,也就是令result等于first。
算法inner_product能够计算[first1,last1)和[first2,first2+
(1ast1-first1))的一般内积(generalized inner product)。注意,你一定得提供初值init。这么做的原因之一是当[first,last)为空时,仍可获得一个明确定义的结果。如果你想计算两个vectors的一般内积,应该将init设为0.
算法partial_sum用来计算局部总和。它会将*first赋值给*result,将*first和*(first+1)的和赋值给*(result+1),依此类推。注意,result可以等于first,这使我们得以完成就地(in place)计算。在这种情况下它是一个质变算法(mutating algorithm)。
运算中的总和首先初始为*first,然后赋值给*result。对于
[first+1,last)中每个迭代器i,从头至尾依序执行sum=sum+*i(第一版本)或sum=binary_op(sum,*i)(第二版本),然后再将sum赋值给*(result+(i-first))。此式所用之二元仿函数不必满足交换律(commutative)和结合律(associative)。所有运算行为的顺序都有明确设定。
本算法返回输出区间的最尾端位置:result+(last-first)。
·如果第一序列的元素较小,返回true.否则返回false。
·如果到达last1而尚未到达last2,返回true。
·如果到达last2而尚未到达last1,返回false。
·如果同时到达last1和last2(换句话说所有元素都匹配),返回false;
用来平行比较两个序列,指出两者之间的第一个不匹配点。返回一对迭代器,分别指向两序列中的不匹配点,如下图。如果两序列的所有对应元素都匹配,返回的便是两序列各自的last迭代器。缺省情况下是以equality操作符来比较元素;但第二版本允许用户指定比较操作。如果第二序列的元素个数比第一序列多,多出来的元素忽略不计。如果第二序列的元素个数比第一序列少,会发生未可预期的行为。
6.5、set相关算法
STL一共提供了四种与set(集合)相关的算法,分别是并集(union)、交集(intersection)、差集(difference)、对称差集(symmetric difference)。
算法set_union可构造s1、s2之并集。也就是说,它能构造出集合s1Us2,此集合内含s1或s2内的每一个元素。s1、s2及其并集都是以排序区间表示。返回值为一个迭代器,指向输出区间的尾端。
由于s1和s2内的每个元素都不需唯一,因此,如果某个值在s1出现n次,在s2出现m次,那么该值在输出区间中会出现max(m,n)次,其中n个来自s1,其余来自s2。
set_union 是一种稳定(stable)操作,意思是输入区间内的每个元素的相对顺序都不会改变。set-union有两个版本,差别在于如何定义某个元素小于另一个元素。
算法 set_intersection可构造s1、s2之交集。也就是说,它能构造出集合s1 n s2,此集合内含同时出现于s1和s2内的每一个元素。s1、s2及其交集都是以排序区间表示。返回值为一个迭代器,指向输出区间的尾端。
如果某个值在s1出现n次,在s2出现m次,那么该值在输出区间中会出现min(m,n)次,并且全部来自s1。
set_intersection 是一种稳定(stable)操作,意思是输出区间内的每个元素的相对顺序都和s1内的相对顺序相同。它有两个版本,差别在于如何定义某个元素小于另一个元素。
算法 set_difference可构造s1、s2之差集。也就是说,它能构造出集合s1-s2,此集合内含“出现于s1但不出现于s2”的每一个元素。s1、s2及其交集都是以排序区间表示。返回值为一个迭代器,指向输出区间的尾端。
由于s1和s2内的每个元素都不需唯一,因此如果某个值在s1出现n次,在s2出现m次,那么该值在输出区间中会出现max(n-m,0)次,并且全部来自S1。
set_difference 是一种稳定(stable)操作,意思是输出区间内的每个元素的相对顺序都和S1内的相对顺序相同。它有两个版本,差别在于如何定义某个元素小于另一个元素。第一版本使用operator<进行比较,第二版本采用仿函数comp进行比较。
算法setsymmetric_difference 可构造s1、s2之对称差集。也就是说,它能构造出集合(S1-S2)U(S2-S1),此集合内含“出现于s1但不出现于s2”
以及“出现于s2但不出现于s1”的每一个元素。S1、S2及其交集都是以排序区间表示。返回值为一个迭代器,指向输出区间的尾端。
由于s1和s2内的每个元素都不需唯一,因此如果某个值在s1出现n次,在s2出现m次,那么该值在输出区间中会出现ln-ml次。如果n>m,输出区间内的最后n-m个元素将由s1复制而来,如果n<m则输出区间内的最后m-n个元素将由s2复制而来。在STL set容器内,m≤1且n<=1。
setsymmetric_difference 是一种稳定(stable)操作,意思是输入区间内的元素相对顺序不会被改变。它有两个版本,差别在于如何定义某个元素小于另一个元素。第一版本使用operators进行比较,第二版本采用仿函数comp。
深入源代码之前,先观察每一个算法的表现,是个比较好的学习方式。以下程序示范本节每一个算法的用法。程序中有时使用STL内建的仿函数(functors,如less,greater,equeal_to)和配接器(adapters,如bind2nd),有时使用自定义的仿函数(如display,even_by_two)。
找出第一组满足条件的相邻元素。这里所谓的条件,在版本一中是指“两元素相等”,在版本二中允许用户指定一个二元运算,两个操作数分别是相邻的第一元素和第二元素。
运用equality操作符,将[first,last)区间内的每一个元素拿来和指定值value比较,并返回与value相等的元素个数。
将指定操作(一个仿函数)pred实施于[first,1ast)区间内的每一个元素身上,并将“造成pred之计算结果为true”的所有元素的个数返回。
partition 会将区间[first,last)中的元素重新排列。所有被一元条件运算pred判定为true的元素,都会被放在区间的前段,被判定为false的元素,都会被放在区间的后段。这个算法并不保证保留元素的原始相对位置。如果需要保留原始相对位置,应使用stable_partition。
移除[first,1ast)之中所有与value相等的元素。这一算法并不真正从容器中删除那些元素(换句话说容器大小并未改变),而是将每一个不与value相等(也就是我们并不打算移除)的元素轮番赋值给first之后的空间。返回值Fonwarditerator 标示出重新整理后的最后元素的下一位置。
例如序列
{0,1,0,2,0,3,0,4],如果我们执行remove(),希望移除所有0值元素,执行结果将是{1,23,4,0,3.0.4]。每一个与0不相等的元素,1,2,3,4,分别被拷贝到第一、二、三、四个位置上。第四个位置以后不动,换句话说是第四个位置之后是这一算法留下的残余数据。返回值Forwardlterator 指向第五个位置。如果要删除那些残余数据,可将返回的迭代器交给区间所在之容器的erase()member function。注意,array 不适合使用remove()和remove_if(),因为array无法缩小尺寸,导致残余数据永远存在。对array而言,较受欢迎的算法是remove_copy()和
将[first,middle)内的元素和[middle,last)内的元素互换。middle所指的元素会成为容器的第一个元素。
在序列[first,last)所涵盖的区间中,查找“连续count个符合条件之元素”所形成的子序列,并返回一个迭代器指向该子序列起始处。如果找不到这样的子序列,就返回迭代器last。上述所谓的“某条件”,在search_n版本一指的是相等条件“equality”,在search_n版本二指的是用户指定的某个二元运算(以仿函数呈现)。
例如,面对序列{10,8,8,7,2,8,7,2,2,8,7,0},查找“连续两个8”所形成的子序列起点,可以这么写:
iter1 = search_n(iv.begin(),iv.end(),2,8);
查找“连续三个小于8的元素”所形成的子序列起点,可以这么写:
iter2 = search_n(iv.begin(),iv.end(),3,8,1ess<int>();
这是二分查找(binary search)的一种版本,试图在已排序的(first,last)中寻找元素value。如果[first,last)具有与value相等的元素(s),便返回一个迭代器,指向其中第一个元素。如果没有这样的元素存在,便返回“假设这样的元素存在时应该出现的位置”。也就是说,它会返回一个迭代器,指向第一个“不小于value”的元素。如果value大于[first,last)内的任何一个元素,则返回last。以稍许不同的观点来看1ower_bound,其返回值是“在不破坏排序状态的原则下,可插入value的第一个位置”。
算法upper_bound是二分查找(binary search)法的一个版本。它试图在已排序的[first,last)中寻找value。更明确地说,它会返回“在不破坏顺序的情况下,可插入value的最后一个合适位置”。
算法binary_search 是一种二分查找法,试图在已排序的[first,last)中寻找元素value。如果[first,last)内有等同于value的元素,便返回true,否则返回false。
返回单纯的bool或许不能满足你,前面所介绍的lower_bound和upper_bound能够提供额外的信息。事实上binary_search便是利用lower_bound先找出“假设value存在的话,应该出现的位置”,然后再对比该位置上的值是否为我们所要查找的目标,并返回对比结果。
STL提供了两个用来计算排列组合关系的算法,分别是nextpermucation和 prev_permutation。首先我们必须了解什么是“下一个”排列组合,什么是“前一个”排列组合。
考虑三个字符所组成的序列(a,b,c)。这个序列有六个可能的排列组合:abc,acb,bac,bca,cab,cba。这些排列组合根据less-than操作符做字典顺序(lexicographical)的排序。也就是说,abc名列第一,因为每一个元素都小于其后的元素。
next_permutation()会取得[first,last)所标示之序列的下一个排列组合。如果没有下一个排列组合,便返回false;否则返回true。
所谓“前一个”排列组合,其意义已在上一节阐述。实际做法简述如下,其中所用的符号如图6-8所示。首先,从最尾端开始往前寻找两个相邻元素,令第一元素为*i,第二元素为*ii,且满足*i>*ii。找到这样一组相邻元素后,再从最尾端开始往前检验,找出第一个小于*i的元素,令为*j,将i,j元素对调,再将ii之后的所有元素颠倒排列。此即所求之“前一个”排列组合。
这个算法将[first,last)的元素次序随机重排。也就是说,在N!种可能的元素排列顺序中随机选出一种,此处N为last-first。
N个元素的序列,其排列方式有N!种,random_shuffle会产生一个均匀分布,因此任何一个排列被选中的机率为1/N!。这很重要,因为有不少算法在其第一阶段过程中必须获得序列的随机重排,但如果其结果未能形成“在N!个可能排列上均匀分布(uniform distribution)”,便很容易造成算法的错误。
本算法接受一个middle 迭代器(位于序列[first,last)之内),然后重新安排[first,last),使序列中的middle-first个最小元素以递增顺序排序,置于(first,middle)内。其余1ast-middle个元素安置于[middle,last)中,不保证有任何特定顺序。
STL的sort 算法,数据量大时采用Quick Sort,分段递归排序。
一旦分段后的数据量小于某个门槛,为避免Quick Sort的递归调用带来过大的额外负荷(overhead),就改用Insertion Sort。
如果递归层次过深,还会改用Heap Sort。
算法equal_range是二分查找法的一个版本,试图在已排序的[first,last)中寻找value。它返回一对迭代器i和j,其中i是在不破坏次序的前提下,value可插入的第一个位置(亦即1ower_bound),j则是在不破坏次序的前提下,value可插入的最后一个位置(亦即upper_bound)。因此,[i,j)内的每个元素都等同于value,而且[i,j)是(first,last)之中符合此一性质的最大子区间。
于是,算法lower_bound返回区间A的第一个迭代器,算法upper_bound返回区间A的最后元素的下一位置,算法equalrange则是以pair的形式将两者都返回。
如果两个连接在一起的序列[first,middle)和[middle,last)都已排序,那么inplacemerge可将它们结合成单一一个序列,并仍保有序性(sorted)。
如果原先两个序列是递增排序,执行结果也会是递增排序,如果原先两个序列是递减排序,执行结果也会是递减排序。
和merge一样,inplace_merge也是一种稳定(stable)操作。每个作为数据来源的子序列中的元素相对次序都不会变动;如果两个子序列有等同的元素,第一序列的元素会被排在第二序列元素之前。
这个算法会重新排列[first,last),使迭代器nth所指的元素,与“整个
[first,1ast)完整排序后,同一位置的元素”同值。此外并保证(nth,last)内没有任何一个元素小于(更精确地说是不大于)[first,nth)内的元素,但对于[first,nth)和[nth,last)两个子区间内的元素次序则无任何保证一—这一点也是它与partial_sort很大的不同处。以此观之,nth_element比较近似partition 而非 sort 或 partial_sort。
例如,假设有序列{22,30,30,17,33,40,17,23,22,12,20},以下操作:
nth_element(iv.begin(),iv.begin()+5,iv.end());便是将小于*(iv.begin()+5)(本例为40)的元素置于该元素之左,其余置于该元素之右,并且不保证维持原有的相对位置。获得的结果为{20,12,22,17,17,
22,23,30,30,33,40]。执行完毕后的5th个位置上的元素值22,与整个序列完整排序后{12,17,17,20,22,22,23,30,30,33,40]的5th个位置上的元素值相同。
以上的排序算法详见[博文]