匹配的位字符串

Matching bit strings

本文关键字:字符串      更新时间:2023-10-16

我需要实现一个字符串搜索算法,该算法可以在比特文本中找到比特模式(匹配可能不是字节/单词对齐的)。首先,我实现了Boyer-Moore算法,但对我来说,比较单个比特太慢了。因此,我尝试实现一个基于块的版本,如本文所述,该版本可以比较整个字节/单词,但它变得复杂且难以管理(部分原因是我不完全理解自己在做什么)

有人能很好地实现这样的算法吗?

我的具体用例是模式长度N >= 32、文本窗口2N和压缩到int s中的位。在这种情况下,N也是字符大小N % 8 == 0的倍数。我预处理了一次,并在更改文本时使用了多次,比如Boyer-Moore。我只需要第一场比赛。性能是关键。

编辑:在成功实现Blocked Boyer-Moore算法后,我没有注意到任何改进(我的逐点版本更快!)这可能是我自己的错误,因为我一直在绞尽脑汁,并将其优化到没有很多行评论就没有意义的地步,但它仍然更慢。

概述

我是SO社区的新手,但我期待着有所回报。

有趣的问题。我提出了一个实现,它只进行基于字节的比较(借助预先计算的比特模式和比特掩码),而不是在比较时执行昂贵的比特操作。因此,它应该相当快。它没有实现为Boyer-Moore算法讨论的任何移位规则(性能优化),因此可以进一步改进。

尽管此实现确实取决于模式位的数量%CHAR_BIT==0——在8位机器上,满足您的标准N%8==0,但该实现将找到非字节对齐的位模式。(它目前也需要8位字符(CHAR_bit==8),但在不太可能的情况下,您的系统不使用8位字符,通过将所有数组/向量从uint8_t更改为CHAR,并调整它们包含的值以反映正确的位数,可以很容易地进行调整。)

考虑到搜索不做任何比特处理(除了固定预先计算的字节掩码之外),它应该具有相当高的性能。

算法摘要

简而言之,要搜索的模式是指定的,实现将其移位一位并记录移位后的模式。它还计算移位模式的掩码,对于非字节对齐的比特模式,为了获得正确的行为,需要忽略比较开始和结束时的一些比特。

对每个移位位置中的所有模式位进行搜索,直到找到匹配或到达数据缓冲器的末尾。

//
//  BitStringMatch.cpp
//
#include "stdafx.h"
#include <iostream>
#include <cstdint>
#include <vector>
#include <memory>
#include <cassert>
int _tmain(int argc, _TCHAR* argv[])
{
    //Enter text and pattern data as appropriate for your application.  This implementation assumes pattern bits % CHAR_BIT == 0
    uint8_t text[] = { 0xcc, 0xcc, 0xcc, 0x5f, 0xe0, 0x1f, 0xe0, 0x0c }; //1010 1010, 1010 1010, 1010 1010, 010*1 1111, 1110 0000, 0001 1111, 1110 0000, 000*0 1010 
    uint8_t pattern[] = { 0xff, 0x00, 0xff, 0x00 }; //Set pattern to 1111 1111, 0000 0000, 1111 1111, 0000 0000
    assert( CHAR_BIT == 8 ); //Sanity check
    assert ( sizeof( text ) >= sizeof( pattern ) ); //Sanity check
    std::vector< std::vector< uint8_t > > shiftedPatterns( CHAR_BIT, std::vector< uint8_t >( sizeof( pattern ) + 1, 0 ) );  //+1 to accomodate bit shifting of CHAR_BIT bits.
    std::vector< std::pair< uint8_t, uint8_t > > compareMasks( CHAR_BIT, std::pair< uint8_t, uint8_t >( 0xff, 0x00 ) );
    //Initialize pattern shifting through all bit positions
    for( size_t i = 0; i < sizeof( pattern ); ++i ) //Start by initializing the unshifted pattern
    {
        shiftedPatterns[ 0 ][ i ] = pattern[ i ];
    }
    for( size_t i = 1; i < CHAR_BIT; ++i )  //Initialize the other patterns, shifting the previous vector pattern to the right by 1 bit position
    {
        compareMasks[ i ].first >>= i;  //Set the bits to consider in the first...
        compareMasks[ i ].second = 0xff << ( CHAR_BIT - i ); //and last bytes of the pattern
        bool underflow = false;
        for( size_t j = 0; j < sizeof( pattern ) + 1; ++j )
        {
            bool thisUnderflow = shiftedPatterns[ i - 1 ][ j ] & 0x01 ? true : false; 
            shiftedPatterns[ i ][ j ] = shiftedPatterns[ i - 1][ j ] >> 1;
            if( underflow ) //Previous byte shifted out a 1; shift in a 1
            {
                shiftedPatterns[ i ][ j ] |= 0x80;  //Set MSb to 1
            }
            underflow = thisUnderflow;
        }
    }
    //Search text for pattern
    size_t maxTextPos = sizeof( text ) - sizeof( pattern );
    size_t byte = 0;
    bool match = false;
    for( size_t byte = 0; byte <= maxTextPos && !match; ++byte )
    {
        for( size_t bit = 0; bit < CHAR_BIT && ( byte < maxTextPos || ( byte == maxTextPos && bit < 1 ) ); ++bit )
        {
            //Compare first byte of pattern
            if( ( shiftedPatterns[ bit ][ 0 ] & compareMasks[ bit ].first ) != ( text[ byte ] & compareMasks[ bit ].first ) )
            {
                continue;
            }
            size_t foo = sizeof( pattern );
            //Compare all middle bytes of pattern
            bool matchInProgress = true;
            for( size_t pos = 1; pos < sizeof( pattern ) && matchInProgress; ++pos )
            {
                matchInProgress = shiftedPatterns[ bit ][ pos ] == text[ byte + pos ];
            }
            if( !matchInProgress )
            {
                continue;
            }
            if( bit != 0 )  //If compare failed or we're comparing the unshifted pattern, there's no need to compare final pattern buffer byte
            {
                if( ( shiftedPatterns[ bit ][ sizeof( pattern ) ] & compareMasks[ bit ].second ) != ( text[ byte + sizeof( pattern ) ] & compareMasks[ bit ].second ) )
                {
                    continue;
                };
            }
            //We found a match!
            match = true;   //Abandon search
            std::cout << "Match found!  Pattern begins at byte index " << byte << ", bit position " << CHAR_BIT - bit - 1 << ".n";
            break;
        }
    }
    //If no match
    if( !match )
    {
        std::cout << "No match found.n";
    }
    std::cout << "nPress a key to exit...";
    std::getchar();
    return 0;
}

我希望这会有所帮助。

如果N很大(比如说,大于16位),那么很容易对位模式的8个移位副本进行初步搜索(截断模式以消除"部分字节")。然后,您可以通过查看相邻的位来细化结果。字节搜索(针对8个移位的拷贝)可以使用Boyer-Moore或类似有效的算法来完成。

如果您想知道:8字节搜索可能比1位搜索更快,因为每个字节比较只需要一条指令,而进行位搜索所需的位操作每位需要更多的指令。但是,您应该始终进行配置以确保安全。

性能将高度依赖于位模式的类型。例如,如果"键"位序列和"搜索"位序列非常不同,那么一些字节移位解决方案,甚至您的逐位解决方案将非常快。因为在绝大多数比特偏移处,第一次比较将失败,您可以继续进行下一次比较。

如果序列高度相似,则需要更复杂的算法。例如,想象一下,100万个比特都是10101010101010……除了中间的某个位置1被翻转为零,例如…101000101……并且你正在寻找一个以"…101000"结尾的10k比特序列,那么字节移位比较算法会做得很差,因为它们必须比较大量字节(8次),然后才能匹配50万次。

因此,你的数据的统计特征在这里很重要。此外,如果键字符串被多次使用,或者您期望有多个匹配,那么预处理缓冲区可以加快速度。

例如,您可以对缓冲区进行一次转换,将每对字节和每个字节中的位数相加,然后对每个键串进行转换。然后可以扫描这两个缓冲区。对于可能匹配的字符串,密钥字符串的每个字节中的位数必须始终小于每个字节对中的位数,并且密钥字符串的每一个字节中的位数必须始终小于搜索字符串的每个比特中的位计数。

如果你的钥匙串很大,那么你可以标记低位和高位的"锚"并扫描它们。例如,我正在比较一个10k密钥字符串,该字符串在偏移量x、y、z处有两个0字节。然后,我可以扫描我的搜索字符串,寻找偏移量为零的单个字节的位数。那真的很快。