匹配字符串、匹配字符串开头
典型用途:电话号码的号码头匹配或号码匹配
当初写这个就是原来做号码头匹配用的是逐个比较,对每个号码要去跟每个号码头匹配一次,本来号码头也没几个嘛,感觉不出什么,谁知道某一天号码头变成了几万个……这一下子程序就比鼻涕还慢了,赶紧连夜写新算法啊。
目录
一、介绍
一般我们写完整匹配,用二分查找也就差不多了,再快也不会比二分查找快多少。但是遇上号码头匹配就傻眼了,没法二分啊,大部分写业务的程序员只能遍历搜索了,这个性能啊,很难接受了。
所以呢,得用逐字节匹配的查找表,比较次数最多是查找表的最长字符串的字节数,这个性能,不仅解决了号码头匹配的问题,用在完整匹配上也不比二分差。
当然了,这是空间换时间的算法,所需内存是相当多的。
本代码支持完整匹配、最短匹配和最长匹配,每个匹配项可以带一个数据(比如做号码替换的时候数据就是替换号码)。
二、空间换时间(复杂度换时间)
空间换时间是常见的程序优化方法,把数据展开,不用每次都重新计算。
典型如大小写转换,一般算法是写几个if判断,毕竟只有字母需要处理,代码只有两行,不额外占用空间,但最快的方法是用一个256长的数组,直接预设结果,只要把字符值作为下标去访问就可以了。
把数据的关联关系直接做成指针替代搜索也是常见的空间换时间的方法,当然同时增加的还有程序的复杂度,不过程序员不就是靠这个吃饭的吗。
三、完整代码
代码如下:
//ByteMatch.h
//索引查找表
#pragma once
#include <iostream>
#include <string>
#include <vector>
using namespace std;
#define LOG cout<<"["<<__LINE__<<"] "
class CByteMatch
{
private:
bool m_isUsed;
bool m_isIncludeFinish;
string m_exdata;//如果m_isIncludeFinish为true则这里放对应的数据
int m_base;//子项位置的偏移
vector<CByteMatch > m_sub;
//matchlen 匹配的字节数
//matchlenwithdata 匹配的完整串的长度,与datastr对应
//datastr 匹配的串对应的数据
bool _Match(char const* str, bool isFull, long& matchlen, long& matchlenwithdata, string const*& datastr)const
{
int c = (unsigned char)str[0];
if (this->m_isIncludeFinish)
{
matchlenwithdata = matchlen;
datastr = &m_exdata;
}
if (0 == c)
{
return this->m_isIncludeFinish;
}
else
{
if (!isFull && this->m_isIncludeFinish)
{
return true;
}
if (c - m_base >= 0 && c - m_base < static_cast<long>(m_sub.size()) && m_sub[c - m_base].m_isUsed)
{
++matchlen;
return m_sub[c - m_base]._Match(str + 1, isFull, matchlen, matchlenwithdata, datastr);
}
}
return false;
}
public:
CByteMatch() { clear(); }
~CByteMatch() { clear(); }
void clear()
{
m_sub.clear();
m_isUsed = false;
m_isIncludeFinish = false;
m_exdata = "";
m_base = 0;
}
//构造匹配表,逐条加入
void Insert(char const* str, char const* data = NULL)
{
long c = (unsigned char)str[0];
m_isUsed = true;
if (0 == c)
{
m_isIncludeFinish = true;
if (NULL != data)m_exdata = data;
}
else
{
if (m_sub.size() == 0)
{
m_sub.reserve(10);
m_sub.resize(1);
m_base = (int)c;
}
else if (c < m_base)
{
CByteMatch tmp;
m_sub.insert(m_sub.begin(), m_base - c, tmp);
m_base = (int)c;
}
else if (c >= m_base + static_cast<long>(m_sub.size()))
{
CByteMatch tmp;
m_sub.insert(m_sub.end(), c - m_base - m_sub.size() + 1, tmp);
}
else
{
}
m_sub[c - m_base].Insert(str + 1, data);
}
}
//打印匹配表,供调试
void Print(int level = 0)const
{
string head;
head.assign(level, ' ');
if (!m_isUsed)LOG << head << "no used" << endl;
if (m_isIncludeFinish)LOG << head << " : " << m_exdata << endl;
for (int i = 0; i < static_cast<long>(m_sub.size()); ++i)
{
if (m_sub[i].m_isUsed)
{
LOG << head << (char)(unsigned char)(m_base + i) << endl;
m_sub[i].Print(level + 1);
}
}
}
//完整匹配
bool FullMatch(char const* str, string* pdatastr = NULL)const
{
long matchlen = 0;
long matchlenwithdata = 0;
string const* pdata = NULL;
bool ret = _Match(str, true, matchlen, matchlenwithdata, pdata);
if (NULL != pdatastr && NULL != pdata)
{
(*pdatastr) = (*pdata);
}
return ret;
}
//第一个匹配(最短)
bool FirstMatch(char const* str, string* pmatchstr = NULL, string* pdatastr = NULL)const
{
long matchlen = 0;
long matchlenwithdata = 0;
string const* pdata = NULL;
bool ret = _Match(str, false, matchlen, matchlenwithdata, pdata);
if (ret)
{
if (NULL != pmatchstr)
{
(*pmatchstr).assign(str, matchlenwithdata);
}
if (NULL != pdatastr && NULL != pdata)
{
(*pdatastr) = (*pdata);
}
}
return ret;
}
//最大匹配(最长)
bool MaxMatch(char const* str, string* pmatchstr = NULL, string* pdatastr = NULL)const
{
long matchlen = 0;
long matchlenwithdata = 0;
string const* pdata = NULL;
_Match(str, true, matchlen, matchlenwithdata, pdata);
if (NULL != pmatchstr)
{
(*pmatchstr).assign(str, matchlenwithdata);
}
if (NULL != pdatastr && NULL != pdata)
{
(*pdatastr) = (*pdata);
}
return NULL != pdata;
}
};
此代码可在vs2022控制台项目下运行,main函数和测试代码(其实测试代码我是和源代码放在一起的,方便使用,这里为了方便说明挪到main函数前面)如下:
#include "ByteMatch.h"
class CByteMatchTest
{
private:
CByteMatch data;
public:
//构造匹配表
void Init()
{
data.Insert("010", "0108");
data.Insert("11", "ab");
data.Insert("111", "abc");
}
//测试,old意思是“旧号码”,因为最初这个代码是用来做号码替换的,wantMaxMatch意思是预期的最大匹配结果,wantFullMatch和wantFirstMatch分别是预期的完整匹配和最短匹配结果,如果三个结果都符合预期,说明测试通过
bool TestOne(char const* old, bool wantMaxMatch, bool wantFullMatch, bool wantFirstMatch)
{
string matchstr, datastr;
bool ret;
if (ret = data.MaxMatch(old, &matchstr, &datastr))
{
LOG << "MaxMatch匹配到 [" << old << "] [" << matchstr << "] [" << datastr << "]" << endl;
}
else
{
LOG << "MaxMatch未匹配 [" << old << "]" << endl;
}
if (ret != wantMaxMatch)
{
LOG << "预期 " << (wantMaxMatch ? "匹配" : "无匹配") << " 测试失败" << endl;
return false;
}
if (ret = data.FullMatch(old, &datastr))
{
LOG << "FullMatch匹配到 [" << old << "] [" << datastr << "]" << endl;
}
else
{
LOG << "FullMatch未匹配 [" << old << "]" << endl;
}
if (ret != wantFullMatch)
{
LOG << "预期 " << (wantFullMatch ? "匹配" : "无匹配") << " 测试失败" << endl;
return false;
}
if (ret = data.FirstMatch(old, &matchstr, &datastr))
{
LOG << "FirstMatch匹配到 [" << old << "] [" << matchstr << "] [" << datastr << "]" << endl;
}
else
{
LOG << "FirstMatch未匹配 [" << old << "]" << endl;
}
if (ret != wantFirstMatch)
{
LOG << "预期 " << (wantFirstMatch ? "匹配" : "无匹配") << " 测试失败" << endl;
return false;
}
return true;
}
//测试全部通过返回true,否则返回false
bool test()
{
Init();
string matchstr;
string datastr;
if (!TestOne("010123456", true, false, true))return false;
if (!TestOne("0108", true, false, true))return false;
if (!TestOne("011123456", false, false, false))return false;
if (!TestOne("010", true, true, true))return false;
if (!TestOne("0101", true, false, true))return false;
if (!TestOne("01", false, false, false))return false;
if (!TestOne("1111", true, false, true))return false;
return true;
}
};
int main()
{
CByteMatchTest zu;
if(!zu.test())return __LINE__;
cout << "执行成功" << endl;
return 0;
}
多说几句:国内大部分公司不讲究测试,更不用提模块测试了,作为程序员的自我修养,我们应该自己写测试代码,并且和源代码一起提供,未必要用什么测试框架,只要写个测试函数放进去就可以了。如果我不是随手运行了一下,我也不敢说这代码一定还能运行、还是正确的。
四、关键点解释
4.1 核心结构
数据结构组织为一棵树,核心就是树节点:
class CByteMatch
{
private:
bool m_isUsed;
bool m_isIncludeFinish;
string m_exdata;//如果m_isIncludeFinish为true则这里放对应的数据
int m_base;//子项位置的偏移
vector<CByteMatch > m_sub;
。。。。。。
CByteMatch是树节点也是整个树。
m_sub是所有子节点,也就是下一级。由于大部分情形并不会用到0-255全部,所以用了m_base来指出第一个,这样,m_sub所需的空间只是第一个用到的数据到最后一个用到的数据,浪费的空间就比较少了。
m_isIncludeFinish指示存在一个在这里结束的值。比如两个数据,“12”和“123”,那么数据结构如下:
1(不包括结束)
----2(包括结束)
--------3(包括结束)
如果匹配上之后存在关联数据(一般是替换),可以存放在 m_exdata。
4.2 算法
算法很容易理解,逐个字节匹配,比较次数和字节数有关,与数据量无关。
class CByteMatch
{
。。。。。。
//构造匹配表,逐条加入
void Insert(char const* str, char const* data = NULL);
//打印匹配表,供调试
void Print(int level = 0)const;
//完整匹配
bool FullMatch(char const* str, string* pdatastr = NULL)const;
//第一个匹配(最短)
bool FirstMatch(char const* str, string* pmatchstr = NULL, string* pdatastr = NULL)const;
//最大匹配(最长)
bool MaxMatch(char const* str, string* pmatchstr = NULL, string* pdatastr = NULL)const;
};
Insert()用于构造数据
Print()用于打印数据
FullMatch()完整匹配,str结束同时遇到m_isIncludeFinish才算匹配,比如数据有“12”和“123”,那么“12345”返回匹配失败
FirstMatch()遇到第一个m_isIncludeFinish就结束,比如数据有“12”和“123”,那么“12345”返回匹配结果是“12”
MaxMatch()最长匹配,比如数据有“12”和“123”,那么“12345”返回匹配结果是“123”
三种匹配方式都会返回匹配到的值对应的附加数据,后两种还会返回匹配到的值。
(这里是文档结束)