串
ADT
这些接口中,indexOf是最主要的研究对象。
串匹配
性能评测
这个概率是随机的m长串,而T中m长度的字串最多为n个,这样计算的。
蛮力匹配
就是P在T上滑动。
j=m,则匹配成功,返回的i-j正是匹配的位置。
i==n且j!=m时,肯定是失配了,此时i-j的值会大于n-m。因为此时j的值必小于m,则失配。
这个版本的i的语义变了。
最坏情况发生的概率还不小。比如:
这种局部的匹配很气人,前面可能都匹配,最后一个不匹配,那么就前功尽弃,白费功夫。
如果字母表越小,那么大量的局部匹配的概率越大。
不过如果字符表越大,局部匹配的概率越小。在平均意义下,BF的复杂度为O(n)。
然而这只是平均意义下。
KMP
蛮力匹配低效在于T中每一个字符都可能经过m次比对,也就是局部匹配太多的问题。然而其实这种重复性局部匹配的数量可以减少。
其实就算一个局部匹配被证明为徒劳的,它的工作依然是有用的,然而BF却没有利用它。
P中前缀和T中字符局部匹配时,对应的字符串的任意子串都匹配,利用这一点可以减少比对次数。
比如P的前
j
j
j个和现在T的
i
−
j
i-j
i−j到
i
−
1
i-1
i−1为局部匹配。我们可以将它们的字符记录下来,设为R,遍历它,找到P的首字符所在的第二个位置Ps,如果整个R中无Ps,则P直接移动
i
−
j
i-j
i−j个位置,如果匹配到,则移动匹配到的秩r-1。
为何事第二个位置呢?看图:
这里的意思就是我们可以将P移动的距离咧成表的形式。其实这也是在说,当T中的
x
x
x失配后,P又应该拿谁和
x
x
x比对。
这个
0
<
j
0<j
0<j让人摸不着头脑。
也就是要求R(N,候选集合)的某个固定长度的前缀和同样长度的后缀匹配。
这样的t可能有多个。KMP应该选取哪一个t呢?j是固定的,显然t越大,右移越大 ,越快,但是可能漏掉一些情况,所以要找t最大的,这样避免了回溯。
俗话也说:步子太大,小心扯蛋。
参考https://www.cnblogs.com/zhangtianq/p/5839909.html
其实最大的t也就是首字母要尽量和第二个出现的匹配。
next表的构造
关于
n
e
x
t
[
j
+
1
]
≤
n
e
x
t
[
j
[
+
1
next[j+1]\leq next[j[+1
next[j+1]≤next[j[+1,
这个其实没那么难想,因为如果j+1的N一定是比j的N长一个单位,那么假设j的0-t和j-t-j匹配,那么j+1后面加了一个,那么显然最好的就是匹配长度加1,所以是小于等于。取等的条件就是P[j]等于j对应的P[t],也就是P[j]=P[next[j]]。也就是自匹配要增加一个长度。
如果不等:
这个递归其实是在将自匹配的长度不断缩短,以找出和P[j]匹配的位置。图中表的是就是前缀不断缩短,直到首字符也不等于P[j]时,next[j+1]就等于0。这时候next[0]=-1,而我们假设有一个哨兵,它可以和任意字符匹配。
递归使用next就是将自匹配的长度不断递减的过程。
if判断0>t放前面时利用逻辑短路避免数组越界。
分析
考察T中每个字符的比对次数,应该低于m,但是低多少不知道,所以为O(mn),这未免太粗糙。
那么就可以通过k界定上界。起初的k=0,最后的k的i应该为O(n),j为常数,则最后的复杂度为O(n)。
同理,next的建立也只需O(m)。
改进
P为0 0 0 0 1,我们建立它的next表。
next[0]=-1,而next[1]=0都是确定的。
现在到了next[2]。现在N为00,自匹配结果为1。以此类推。
也就是KMP现在无法吸取第一次失配的教训。
也就是P[next[j]]的字符不应当和P[j]相同,因为P[j]已经失配过一次了。
这个操作是说,如果最长的匹配不能用(因为P[j]==P[next[j]]),那么就用次长的,求次长的操作就是再next一次。
这样的话也不满足next[j+1]=next[j]+1了,除了j=0的时候,如果P[0]!=P[1],则next[1]=0。
BF的失败情况为O(mn),Ω(n-m)(最好)。然而最好情况发生的概率其实没那么小,如果字符表够大,那么其实BF也是很好的。
KMP为:
将局部匹配的结果投影到T上,正好为O(n)(最坏)。KMP对于字符表不大
的效果明显优于BF,比如二进制,然而对于字符表大一点,KMP也不会比BF好很多。
BM
串判断匹配需要每一个位置的字符都匹配,然而失配却很简单,任意一个不匹配即可。失配的速度可以很快。
加速比对就是要加速失败的比对,尽量让失败的比对更多,排除更多的失配位置。
失败得越靠后,则能排除的对齐位置可能就越多。
从最右开始比对,如果失配,则遍历P,发现没有道,则直接移动len§。
如果最后一个匹配,则对比倒数第二个,如果失配,则移动strlen§-1。
接着从最后一个开始比对,又失败,但是这次常在P中存在,则移动至它们重合。如果是汉字,字符表的个数大于5000,那么失败的概率很大,粗略为
4999
5000
\frac{4999}{5000}
50004999。
特殊
X应该取靠后的,这样移动得不至于太快。然而这个P中X’的秩应该不大于j,否则反而还回溯了。
BM的核心之一就是bc表。
bc
外循环确实没必要,我的一种想法是将new设为static这样它的初始值全为0,至少不需要遍历初始化。
BM算法更适用于字符表很大的情况,因为这样的话,它失配的概率很大。
BM算法只吸取了教训却没有借鉴经验,那么改进的方向就是要借鉴经验。
gs
按照bc表移动的策略如上,而这没有利用已经匹配的后缀。那么我们就想着在P中去匹配这个好后缀,可以建立gs表。
这里也是移动距离尽量小,如果P中可以匹配多处好的后缀的话,取靠后的。
而且同时还要兼顾X在P中的情况。如果好后缀在P中匹配不到(除了自身位置),移动的距离就更大。
先看这个gs表的构建,从后向前。
也 如果失配,确实改移动一个。
如果在 静 失配,为何要移动8呢?因为此时好后缀为 也,而也在P[3]有,我怎么感觉需要移动4。
如果故处失配,则也是移动4个。
如果是善失配,移动8个。
下一个也的话,也是8。
以此类推。
MS[j]为以j为结尾的子串中和P的某一后缀匹配的最长者。
ss[j]为它的长度。
第一种情况,说明后缀的j+1个和前缀的j+1个匹配,而如果失配的位置为i,i在图中白色范围内失配的话,那么m-j-1是个可以考虑的移动距离,这样才能将好后缀的j+1个后缀匹配。
第二种是如果ss[j]<=j,则对于P[m-ss[j]-1]失配,值得考虑的位移距离为m-j-1,这样才能将好后缀重合。
我的构想是从后向前,ss[m]=0,而ss[m-1]就是比较P[m-1]是否等于P[m],如果等于则为1,否则为0,然后是继续到了ss[m-2]。将P[m-2]和P[m]比较,不等直接置为0,否则,比较P[m-3]和P[m-1]。以此类推,这样循环了O(m)次,但是比较次数为1+2+3+…m/2+…+3+2+1。最后也是O(m^2)。
性能
最坏是结合了bc表和gs表。
性能对比分析
BF的最好为O(n+m),最坏为O(n*m)。
KMP均为O(n+m)。
BC的跨度很大,从O(nm)到O(n/m)。
BM为O(n/m)至O(n+m)。
而 上图的Pr为匹配成功的概率,或者说字符表减小的方向。
BF如果Pr越大,则复杂度越高,因为这样局部匹配就太多了。
KMP的话Pr越大,几乎不变,稳定。
BC的话是起点低,而增长速度快。因为BC如果失败的匹配越多越好。
BM起点低斜率小,但也在增长。
Karp-Rabin算法
可能就是用哈希函数将字符串转化为整数比较。
还原的方法就是质因数分解,这是利用了质因数分解的唯一性实现一一映射。
这就是串匹配的KR算法。
然而散列会有冲突。
冲突会增加复杂度。
相邻的只有头尾不同。
快速排序
radix排序为基数排序。
tournametsort是锦标赛排序。
参考https://www.cnblogs.com/james1207/p/3323115.html
锦标赛排序又叫树型排序,属于选择排序的一种。直接选择排序之所以不够高效就是因为没有把前一趟比较的结果保留下来,每次都有很多重复的比较。锦标赛排序就是要克服这一缺点。它的基本思想与体育淘汰赛类似,首先取得n个元素的关键字,进行两两比较,得到 n/2 个比较的优胜者,将其作为第一次比较的结果保留下来,然后对这些元素再进行关键值的两两比较,…,如此重复,直到选出一个关键字最小的对象为止。
下面举个例子,假设arr[] = {3,4,1,6,2,8,7,9},我们首先需要建立一棵完全二叉树,注意如果不够arr的长度没得2的幂次方,我们需要补一些元素。注意看,数组arr的元素其实分布在叶子节点上,其他分支几点是存储了比赛的结果。根据这个分析,那么我们用n-1次比较就可以建立如下图所示的完全二叉树:
这和min-heap不一样。
根据上面的示意图,其实我们还需要一个变量来存储胜者的索引。于是结构体,应该这样定义:
struct node{
int nData;
int id;//记录胜者的索引
node(int n,int i){nData=n;id=i;}
};
那么,根据这个定义,我们再来画一下图,逗号后面记录了胜者的索引:
于是,第一次建树,就成上面这样了,我们可以轻松通过根节点得到最后的胜者(最小节点),并且同时知道胜者的索引号。下次在搜索最小值的时候,我们需要将刚才胜者值替换为最大值,然后沿着红色的线比较一遍就行了,这次只需要比较三次就行了(logn),请看下图。
然后,一直进行这个过程中就可以完成对数组的排序,排序的时间复杂度。
从这个演示可以看出这个算法真正吸引我们的地方就是当决出一个胜者后,要取得下一个胜者的比较只限于从根到刚才选出的外结点这一条路径上。可以看出除第一次比较需要n-1次外,此后选出次小,再次小…的比较都是log2 n次,故其复杂度为O(n*log2 n)。但是对于有n个待排元素,锦标赛算法需要至少2n-1个结点来存放胜者树。故,这是一个拿空间换时间的算法。
为何要拿最大数放在原来最小的位置呢?这个对于找次小的很简单。和堆排序类似。复杂度也为O(nlogn)。
快速排序的核心在于pivot的选取。
这也看出快排的不稳定性。不过它式就地算法。
性能分析
变种
G中的元素因为滚动的存在也不具有稳定性。而L因为最后一次交换,也未必有稳定性。
选取
也就是说maj不一定为众数。
或者建立大顶堆。先取出前k个元素建立大顶堆。先插入一个元素,然后删除,这样删除的为前k+1个里最大的元素,这断然不可能为第k个最小的元素。然后依次删除,最后得到的顶为第k个最小的元素,或者剩下的k个里面最大的元素。
如果k在靠近中间的位置,则又回到了O(nlogn)。
事实上,假设k=n/2,而G为小的那n/2个,而H为大的那n/2个的话,这样的交换得持续nlogn次。
用快速排序的思想:
内循环为O(n),和快速排序的每一轮并无二异。外循环的次数也为O(n)。
这样的复杂度无法接受。最坏情况就是x对应的G都为1或者L为1,老倒霉蛋了。
有没有算法在最坏情况也是O(n)?
线性选取
这个线性的系数似乎不小。
这个算法的理论意义更大,它的系数实在式太大了。这里严格的证明都需要用到差分方程的理论。
希尔排序
对列排序。
希尔排序每一步都式朝着有序的方向前进。
增量逐渐减小,逆序对的总数将会持续下降。这对于插入排序而言是一件好事情。