十六进制字符串转换成byte数组_表达式树实现支持通配符的字节数组搜索

一、废话

字节数组搜索,即在一个字节数组中搜索一个更短的子数组。C#中并没有直接提供字节数组的搜索算法,而如果用Linq扩展方法的话效率又很低,且不支持通配符,本文简述了如何利用 表达式树 实现支持通配符的字节数组搜索。

字节数组的搜索算法可以参考字符串的搜索算法,而且实现起来比后者更加简单高效。因为元素的范围就那么大,预处理快速且不占空间。

任何字符串搜索算法都可以使用,这里我选择了一个Sunday算法的改进版本,具体可以参考 《Sunday字符串匹配算法的效率改进》 一文,当然懒得看也没关系,各位读者老爷一定明白这类算法的核心就是(先做预处理然后)一旦发现不匹配就跳过尽可能多的元素。我在后文也会给出简单的描述。

完整的项目可以在 这里 看到。

二、算法简述

Sunday算法的思路和实现都非常简单,这个改进版本也是如此。具体来说分为两大块骤:

  1. 根据模式串Pattern的长度进行预处理,生成一个跳过表。
  2. 遍历目标数组并进行比照时,如果不匹配,则根据跳过表跳过相应数量的元素。

具体细节恕我表达能力差,直接看代码吧:

//改进的Sunday算法,被称作RoSunday 
private static int RoSunday(byte[] source, byte[] pattern)
{
    var count = source.Length;
    var patternLength = pattern.Length;
    var pattMaxIdx = patternLength - 1;
    var maxLen = count - patternLength + 1;
    var moveTable = new int[256];


    //下面两个for循环用于初始化跳过表
    for (int i = 0; i < 256; i++)
    {
        //默认的跳过长度是Pattern的长度
        moveTable[i] = patternLength;
    }
    for (int i = 0; i < patternLength; i++)
    {
        //针对Pattern的每个元素,计算它对应的跳过长度。
        //其实就是元素索引的翻转
        moveTable[pattern[i]] = pattMaxIdx - i;
    }

    var index = 0;
    while (index < maxLen)
    {
        var mov = moveTable[source[index + pattMaxIdx]];

        if (mov < patternLength)
        {
            index += mov;

            for (var i = 0; i < patternLength; i++)
            {
                if (source[index + i] != pattern[i])
                {
                    ++index;
                    continue;
                }
                return index;
            }
        }
        else
        {
            index += patternLength;
        }
    }
    return -1;
}

如果不需要支持通配符,那么上面的代码稍微优化一下就能够直接使用了。

三、支持通配符的字符串Pattern的格式设计

  • 字节数组中的每个元素必须用两位十六进制数表示,如1表示为01,16表示为10
  • 任何空格(0x20)都会被忽略。例如 010203,0102 03,01 02 03 都是等价的。
  • ? 表示通配符。它必须成对出现,或者搭配一位十六进制数。如 ?? 表示255,1? 表示0x10~0x1F , ?1 表示 0x11, 0x21, 0x31......0xF1。

这种设计的目标就是为了偷懒,便于解析。

如果你不嫌麻烦,可以给字符串Pattern设计更宽松的规则,如果你有时间而且有耐心去搞量词处理,甚至可以试着弄一个正则表达式版本,无非就是先转语法树。之前我想着搞个大新闻,花了一个星期写了个极烂的正则表达式的实现,速度慢不说,关键是大部分语法都用不到……

其实这种通配符的设计可以理解成一个只有 连(Concat) 的正则表达式,对于我来说足够了,因为就是写修改器的时候用……

四、增加通配符搜索的支持

支持通配符,无非就是把byte[]形式的Pattern换成了string形式。 在第二节的代码中,对于Pattern的内容有要求的地方只有两处:

//针对Pattern的每个元素,计算它对应的跳过长度。
//其实就是元素索引的翻转
moveTable[pattern[i]] = pattMaxIdx - i;

以及

if (source[index + i] != pattern[i]){}

为了方便说明我们先定义一个示例Patern:A1 B? ?C ??

然后先看第二处,它的作用是依次比照主串sourcepattern的元素,根据规则,描述示例Pattern的方法就不言而喻了:

if (source[index + i++] != 0xA1 || (source[index + i++] & 0xF0) != 0xB0 || (source[index + i++] & 0x0F) != 0x0C){}

原始的代码位于一个循环之内,对于想要支持通配符,这种设计肯定不行,只有把循环转化为以上的形式。至于最后的?? ,直接无视即可。

再来看第一处,初始化跳转表。对于通配符而言,它需要处理的不再是某一个元素,而是某个范围内的元素。

?? 一切都匹配:

for (int j = 0; j < 256; i++)
{
    moveTable[j] = badMove;
}

?C则是:

 for (int j = 0x0C; j < 256; j+=0x10)
{
    moveTable[j] = badMove;
}

B?则是:

 for (int j = 0xB0; j< 256; i++)
{
    moveTable[j] = badMove;
}

而上面三段代码中badMove的值也很好确定,还是以示例Pattern为例,这个串去掉所有空格后是 A1B??C?? ,长度为8,除以2后即是这个Pattern的“实际意义上的长度”……

没错,定义规则时我的第一原则就是方便解析……

五、Pattern的解析与生成表达式树

无论如何,Pattern必须要进行解析才能够使用。而解析完毕之后,通配符和主串中元素的比对也需要更多的操作,效率会变低。所以一个更好的办法是,在解析Pattern的同时,直接生成表达式树,然后编译成委托的形式直接调用。

微软提供的文档中 表达式树 的介绍和 API文档 相当完善, 这里就不详细介绍了,这里直接上代码。

这里的代码仅作示例,不考虑Pattern错误的情况。

我们的Pattern是支持空格分隔的,所以要先去掉空格:

pattern = pattern.Replace(" ", string.Empty);

然后将跳过表初始化为默认值:

for (int i = 0; i < 256; i++)
{
    moveTable[i] = pattern.Length / 2;
}

然后定义最初的表达式:

//注意这里的判断:要先确定余下的source元素数量是否大于patern的实际长度,以防止溢出。
Expression exp = Expression.LessThanOrEqual(
   Expression.Constant(pattern.Length / 2, typeof(int)),
   Expression.Subtract(Expression.ArrayLength(ExpParamSource), ExpParamSourceIndex));

设置一些常用的变量:

var patternMaxIndex = pattern.Length / 2 - 1; 
var ExpParamSource = Expression.Parameter(typeof(byte[]), "source");
var ExpParamSourceIndex = Expression.Parameter(typeof(int), "sourceIndex");
var ExpArrayItemIterator = Expression.ArrayIndex(ExpParamSource, Expression.PostIncrementAssign(ExpParamSourceIndex));

然后就开始解析Pattern,同时设置跳过表,并生成表达式树了:

for (int idx = 0; idx < pattern.Length; idx += 2) //两个一组
{
     badMove = patternMaxIndex - (idx / 2);
     //.....
}

在上面的循环中,如果pattern[idx]pattern[idx+1]表示一个数字hexNum

moveTable[hexNum] = badMove;
exp = Expression.AndAlso(
    exp,
    Expression.Equal(
    ExpArrayItemIterator,
    Expression.Constant(hexNum, typeof(byte))));

pattern[idx]pattern[idx+1]表示 ??

for (int j = 0; j < 256; i++)
{
    moveTable[j] = badMove;
}
exp = Expression.AndAlso(
    exp,
    Expression.Block(
    Expression.PreIncrementAssign(ExpParamSourceIndex),
    Expression.Constant(true, typeof(bool))));

pattern[idx]表示问号,而pattern[idx+1]表示一位数字lowDigit

for (int j = lowDigit; j < 256; j += 0x10)
{
    moveTable[j] = badMove;
}
exp = Expression.AndAlso(
    exp,
    Expression.Equal(
    Expression.And(
    ExpArrayItemIterator,
    Expression.Constant((byte) 0x0F, typeof(byte))),
    Expression.Constant((byte) lowDigit, typeof(byte))));

pattern[idx]表示一位数字highDigit,而pattern[idx+1]表示问号:

for (int j = highDigit; j< 256; i++)
{
    moveTable[j] = badMove;
}
exp = Expression.AndAlso(
    exp,
    Expression.Equal(
    Expression.And(
    ExpArrayItemIterator,
    Expression.Constant((byte) 0xF0, typeof(byte))),
    Expression.Constant((byte) highDigit, typeof(byte))));

关于表达式树的创建,看着代码有些多,其实很简单,就是利用 Expression.AndAlso 把每次的判断串起来……严格的说这离树还差得远呢,就是一坨连起来的二元运算。

全部工作做完之后,编译表达式:

var comparedFunc = Expression.Lambda<Func<byte[], int, bool>>(
                exp, ExpParamSource, ExpParamSourceIndex)
                .Compile();

最后,把第二节的代码略作修改,作为算法框架:

private static int RoSunday(byte[] source, int[] moveTable, Func<byte[], int, bool> compareFunc, int patternLength)
{
    var count = source.Length;
    var pattMaxIdx = patternLength - 1;
    var maxLen = count - patternLength + 1;

    var index = 0;
    while (index < maxLen)
    {
        var mov = moveTable[source[index + pattMaxIdx]];

        if (mov < patternLength)
        {
            index += mov;
            if (compareFunc(source, index)) return index;
            ++index;
        }
        else
        {
            index += patternLength;
        }
    }
    return -1;
}

通过这种方式实现的支持通配符的搜索,除了初始化以及第一次调用时花费的时间略长外,之后的执行都是相当快速的。

六、其他

虽然一提到字符串搜索算法很多人首先想到的就是BM,其实Sunday算法的效果意外的好。

我用 我的实现 和另一个纯C#写的标准BM实现做了速度测试,100字节~16M的千次随机数据,结果如下:

Total: bytes pattern cost is 1309.7017 , string pattern cost is 948.357200000001 , wildcard string pattern cost is 32906.4641, Boyer Moore algorithm implementation is 1440.9692
Total: bytes pattern cost is 676.126699999998 , string pattern cost is 608.047799999999 , wildcard string pattern cost is 14577.6418, Boyer Moore algorithm implementation is 929.0357

前一条是Debug下的,后一条是Release下的。显而易见的是,(这个改进的)Sunday算法优势很大,尽管通配符功能的效率又量级的差距,但也能控制在20毫秒以内,是可以接受的范围。

(全文完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值