前言:在读取kom压缩文件的文件头里,有一段xml文本记录文件列表,现在需要读取这段xml文本。由于1个kom文件有几个G的大小,所以需要一个针对Stream用的Sunday算法来进行匹配,但是没有找到针对Stream的Sunday算法(其实是懒得找),于是就决定自己实现一个来加深对Sunday算法的理解。
在看下面的内容前,你必须对Sunday算法的思想有所了解,不然看了也差不多是白看。
如果你只是想要复制代码,请直接看最后。
如果发现了什么Bug,或者有可以优化的地方,十分欢迎指正。
预定义:
Stream的指针是从左向右进行移动的(鄙视那些说向前向后移动的人,都不说明哪个方向是前,哪个方向是后)。
source指待匹配的Stream,是要从这里面找你想要的子串,下面统一叫主串。
pattern指目标的Byte数组,是要从主串中查找的东西,下面统一叫模式串。
下面定义了两个子串:child1和child2,他们长度相等,且比pattern的长度大1(为什么这样定义?因为这是Sunday算法)。child1和child2总是相连的,而且child1在child2前面。
下面提到的k,是指pattern在子串的右一位(child2子串末位),而且,这个k总是在子串child2上。
i是在子串child1和child2上滑动的指针(Index)
j是在pattern上滑动的指针(Index),而且每开始新的匹配时,总有j=pattern.Length-1。原因是,这里匹配时,是从右向左匹配的。事实上网上很多Sunday算法都是反向匹配的(比如这里source的指针从左向右走,匹配时就是在子串中从右向左匹配,这样更好,这里就不详细解释了)。
这里使用到Next数组,这里Next数组参照使用Next数组的Sunday算法:初始化时next[t]=pattern.Length+1,表示下一次k可以跳pattern.Length+1个值,而next[pattern[t]]=pattern.Length-t。这里不详细解释。网上查到少部分Sunday使用Next数组,但多数不使用Next数组,其实效果都一样,只是Next数组用内存空间来换取一少部分的性能。
下面以主串asdfqwvasdfqwevasdczxfwqfxcvzxcvqwerczxcvqwerewqfz和模式中zxcsaef作为例子来演示匹配过程。因为排版的问题,可能出现错位的情况,所以这里给你们展示图片。
child左边的竖线|的Index=0,右边竖线的Index=child.Length-1,也就是说,两条竖线指向的字符也属于child子串中的字符。
下面是代码的实现C#,Java和C#差不多(只要把里面的Int32替换成int,把Byte替换成byte,再把Boolean替换成boolean就是完整的Java代码了(大概))。里面关键的地方都有写注释,需要另外说明的是,为了节省空间,定义了一个temp数组指针,temp数组指针在新的匹配时是指向child2的,当i<0时,temp指向child1并使得i=temp.Length-1。
因为我的项目需要,这里还传入了一个pass流,用来储存被pass掉的字符。而且,在检索kom文件时,pattern是固定的,所以就再定义了一个next[]参数,这样就不用每次都重新生成next数组了。
当我进行两次Search时(XML头和XML尾),中间那个pass流的内容就是xml的内容,直接Read出来就可以了,不用再次截取。
/// <summary> /// 生成Next数组 /// </summary> /// <param name="pattern"></param> /// <returns></returns> public static Int32[] GetNext(Byte[] pattern) { Int32[] next = new Int32[256]; for (Int32 x = 0; x < next.Length; x++) { next[x] = pattern.Length + 1; } for (Int32 x = 0; x < pattern.Length; x++) { next[pattern[x]] = pattern.Length - x; } return next; } /// <summary> /// 在Stream流中匹配模式串pattern /// </summary> /// <param name="source"></param> /// <param name="pass"></param> /// <param name="pattern"></param> /// <param name="next"></param> /// <returns></returns> public static Boolean SearchForStream(Stream source, Stream pass, Byte[] pattern, Int32[] next = null) { if (next == null) { next = GetNext(pattern); } // child1和child2是Stream上连续的子串,长度相同,且比pattern的长度大1. Byte[] child1 = new Byte[pattern.Length + 1]; Byte[] child2 = new Byte[child1.Length]; // temp用于指向child1和child2数组,并用于child1和child2数组的指针交换 Byte[] temp = null; // k是后缀(Sunday算法里提到的子串的末位,这里统一使用“后缀”来表示)在child2中的位置(Index),开始比较时,k = pattern.Length Int32 k = pattern.Length; // pos是k值下一次的位移值,预设为0 Int32 pos = 0; // i是temp(child1和child2)上的指针(Index) Int32 i = 0; // j是pattern上的指针(Index),且每次开始新的匹配时,j都等于pattern.Length-1 Int32 j = 0; // 在Stream读取的字节数 Int32 count = 0; // 结果 Boolean result = false; // 开始 while ((count = source.Read(child2, 0, child2.Length)) > -1) { temp = child2; k = k + pos; if (k >= child2.Length) { k = k - child2.Length; } if (k > count) { pass.Write(temp, 0, count); break;// false; } else if (k < count) { // 这里不用判断k==count的原因是,k==count时,下一个count=-1,跳出循环 pos = next[temp[k]]; } i = k - 1; j = pattern.Length - 1; do { if (i == -1) { i = pattern.Length; temp = child1; } } while (j > -1 && temp[i--] == pattern[j--]); if (j == -1) { pass.Write(child2, 0, k); source.Position = source.Position - count + k; result = true; break;//true; } else { pass.Write(child2, 0, count); } temp = child1; child1 = child2; child2 = temp; } // pattern在pass的末尾,source的Position在pattern之后 pass.Position = 0; return result; }
当匹配完成时,pattern在pass流的最后,source的Postion是pattern的右一位。