
AC算法PHP实现、关键字搜索
根据🔗AC算法原理,PHP的实现,我们定义下面几个类
NodeData:用来存储节点的值,之所以顶一个类来存,是为了避免PHP的参数传递是默认是值传递导致无法修改节点的值
TrieNode:前缀树的一个节点,记录节点的跳转条件和目标节点引用、失败节点引用、节点数据等
Trie:前缀树,主要方法都在这
最重要的 Trie,关键的几个方法
addWord($word, $data):构建关键字和节点数据的关系
makeAutomation():构建失败转移关系
match($str):传入关键字,找结果
最终代码
<?php
/**
* Aho-Corasick算法
*/
namespace AC;
use Exception;
// 节点
class TrieNode {
public $end = false; // 是否是结束节点
public $nodeData = null; // 当前节点的数据,这是一个NodeData对象
public $deep = 0; // 深度
public $next = []; // 根据字符,成功转到的下一个状态节点
public $failNode = null; // 失败时跳转的下一个状态
public function __construct() {
$this->nodeData = new NodeData();
}
public function toString() {
return "end: " . ($this->end ? 'true' : 'false') . ", deep: " . $this->deep . ", next: [" . implode(',', array_keys($this->next)) . "], failNode: " . (empty($this->failNode) ? 'false' : 'true') . "\n";
}
}
// 节点的数据,将数据封装成一个对象,是为了使用对象的引用传值方式
class NodeData {
public $data = null;
}
// trie树
class Trie {
private $isMake = false; // 是否已经创建了自动机
private $root; // 根节点
private $nodeArray = []; // 节点数组
private $oneDeepNodeArray = []; // 深度是1的节点数组
public function __construct() {
$this->root = new TrieNode();
$this->nodeArray[] = $this->root;
}
/**
* 添加一个关键字以及它对应的数据
* @param string $word 关键字
* @param mixed $data 关键字对应的数字
*/
public function addWord($word, $data) {
// 如果自动机已创建,则不可再添加
if ($this->isMake) {
throw new Exception("Automaton created, no more can be added.");
}
$len = mb_strlen($word);
$pos = $this->root;
for ($i = 0; $i < $len; $i++) {
$ch = mb_substr($word, $i, 1);
if (!isset($pos->next[$ch])) {
$node = new TrieNode();
$node->deep = $i + 1;
$this->nodeArray[] = $node;
// 如果是深度为1
if ($node->deep === 1) {
$node->failNode = $this->root; // 设置失败跳转节点为根节点
$this->oneDeepNodeArray[] = $node;
}
$pos->next[$ch] = $node;
} else {
$node = $pos->next[$ch];
}
if ($i === $len - 1) {
$node->end = true;
$node->nodeData->data = $data;
}
$pos = $pos->next[$ch];
}
}
/**
* 检查关键字是否存在
* @param string $word 关键字
* @return bool 存在则返回true,否则返回false
*/
public function checkWord($word) {
$len = mb_strlen($word);
$pos = $this->root;
$ok = false;
for ($i = 0; $i < $len; $i++) {
$ch = mb_substr($word, $i, 1);
if (isset($pos->next[$ch])) {
$pos = $pos->next[$ch];
} else {
break;
}
if ($i === $len - 1 && $pos->end) {
$ok = true;
}
}
return $ok;
}
/**
* 获取关键字的数据
* @param string $word 关键字
* @return NodeData 关键字存在则返回对应节点的数据对象,否则返回null
*/
public function getNodeData($word) {
$len = mb_strlen($word);
$pos = $this->root;
$data = null;
for ($i = 0; $i < $len; $i++) {
$ch = mb_substr($word, $i, 1);
if (isset($pos->next[$ch])) {
$pos = $pos->next[$ch];
} else {
break;
}
if ($i === $len - 1 && $pos->end) {
$data = $pos->nodeData;
}
}
return $data;
}
/**
* 修改关键字对应的数据对象(NodeData)的数据
* @param string $word 关键字
* @param mixed $data 数据
* @return bool 关键字存在则返回true,否则返回false
*/
public function setNodeData($word, $data) {
$len = mb_strlen($word);
$pos = $this->root;
$isOk = false;
for ($i = 0; $i < $len; $i++) {
$ch = mb_substr($word, $i, 1);
if (isset($pos->next[$ch])) {
$pos = $pos->next[$ch];
} else {
break;
}
if ($i === $len - 1 && $pos->end) {
$pos->nodeData->data = $data;
$isOk = true;
}
}
return $isOk;
}
/**
* 构建自动机,计算每个节点的失败跳转节点
*/
public function makeAutomation() {
if ($this->isMake) {
return;
}
// 队列初始化为全部是深度为1的节点
$nodeQuery = $this->oneDeepNodeArray;
$front = 0; // 队列头指针
while (!empty($nodeQuery)) {
$r = $nodeQuery[$front];
unset($nodeQuery[$front]);
$front++;
foreach ($r->next as $ch => $s) {
$nodeQuery[] = $r->next[$ch]; // 入队
// 寻找$s的失败跳转节点
$state = $r->failNode; // $s的上一个节点的失败跳转节点
while (!isset($state->next[$ch]) && $state->deep !== 0) { // 如果这个节点输入$ch也没有可到达的节点,并且不是根节点,则继续找这个节点的失败跳转节点
$state = $state->failNode;
}
if (isset($state->next[$ch])) {
$r->next[$ch]->failNode = $state->next[$ch];
} else {
$r->next[$ch]->failNode = $state;
}
}
}
$this->isMake = true;
}
/**
* 对字符串进行匹配
* @param string $str 要进行匹配的字符串
* @return array 返回数组,元素是匹配到的关键字对应的开始位置以及关键字对应的数据
*/
public function match($str) {
$result = [];
$strLen = mb_strlen($str);
$node = $this->root;
for ($i = 0; $i < $strLen; $i++) {
$ch = mb_substr($str, $i, 1);
while (!isset($node->next[$ch]) && $node->deep !== 0) { // 没有可跳转的节点,并且该节点不是跟节点,则跳转到失败跳转节点
$node = $node->failNode;
}
if (isset($node->next[$ch])) { // 有匹配的则跳转到下一个节点
$node = $node->next[$ch];
if ($node->end) {
$result[] = [
'start_index' => $i + 1 - $node->deep,
'data' => $node->nodeData->data
];
}
}
}
return $result;
}
}
$trie = new Trie();
$trie->addWord("西红柿", "西红柿");
$trie->addWord("菠萝", "菠萝");
$trie->addWord("西红柿子", "西红柿子");
$trie->addWord("西瓜", "西瓜");
$trie->addWord("西瓜汁", "西瓜汁");
$trie->makeAutomation();
$ret = $trie->match("我喜欢吃西红柿子和西瓜汁哈哈哈");
var_dump($ret);
$ret = $trie->match("不不不,我更细化菠萝蜜");
var_dump($ret);
本文是原创文章,采用 CC 4.0 BY-SA 协议,完整转载请注明来自 KK元空间
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果