php代码实现AC自动机
被百度面试官问到这么一个问题:
有五亿个文件文档,另外还有10万个敏感词,怎么判断这五亿个文件里是否有包含敏感词?.......这个我第一念头真不知道怎么办,最后还是问了面试官,面试官告诉我涉及到了一个算法。那就是AC自动机,我通过百度了一批AC自动机的文章,也大概了解到了它的进化历程......关于这方面的文章原理描述这里就不搬了,就当了解有这么一个算法能应对一些类似开发中的需求就好了,这里就复制了别人的代码备用。
class Node { public $value; // 节点值 public $is_end = false; // 是否为结束--是否为某个单词的结束节点 public $childNode = array(); // 子节点 public $fail = 0; // 失败指针 public $failIndex = 0; // 失败数组指针 public $trie = 0; // 结构树层级 public $parent = false; // 父节点 // 添加孩子节点--注意:可以不为引用函数,因为PHP对象赋值本身就是引用赋值 public function &addChildNode($value, $is_end = false) { $node = $this->searchChildNode($value); if (empty($node)) { // 不存在节点,添加为子节点 $node = new Node(); $node->value = $value; $node->parent = &$this; $node->trie = $this->trie + 1; $this->childNode[] = $node; } if (!$node->is_end) { $node->is_end = $is_end; } return $node; } // 查询子节点 public function searchChildNode($value) { foreach ($this->childNode as $k => $v) { if ($v->value == $value) { // 存在节点,返回该节点 return $this->childNode[$k]; } } return false; } } // 添加字符串 function addString(&$head, $str) { $node = null; for ($i = 0; $i < strlen($str); $i++) { if ($str[$i] != ' ') { $is_end = $i != (strlen($str) - 1) ? false : true; // 如果最后一位就是false; if ($i == 0) { $node = $head->addChildNode($str[$i], $is_end); } else { $node = $node->addChildNode($str[$i], $is_end); } } } } // 获取所有字符串--递归 function getChildString($node, $str_array = array(), $str = '') { if ($node->is_end == true) { $str_array[] = $str; } if (empty($node->childNode)) { return $str_array; } else { foreach ($node->childNode as $k => $v) { $str_array = getChildString($v, $str_array, $str . $v->value); } return $str_array; } } // 字符串多模匹配 function search($p, $head, &$failArray) { $i = 0; $res = []; while ($i < strlen($p)) { $head = searchWords($head, $p[$i], $res, $failArray, $i); $i++; } return $res; } function searchWords(&$head, $value, &$res, &$failArray, &$i) { foreach ($head->childNode as $k => $v) { if ($v->value == $value) { // 成功存入 if ($v->is_end == true) { $res[getWords($head->childNode[$k])][] = $i; } // fail节点也是指向一个结束节点 if ($failArray[$v->fail]->is_end == true) { $res[getWords($failArray[$v->fail])][] = $i; } // 跳转fail if (empty($v->childNode)) { return $failArray[$v->fail]; } // 继续下一级匹配 return $head->childNode[$k]; } } // fail指针正在后退,没到root节点主指针不动 if ($head->failIndex) { $i--; } // 失败指向fail 对比指针不动 return $failArray[$head->fail]; } // 获取完整字符 function getWords($node) { $str = ''; while ($node->parent) { $str .= $node->value; $node = $node->parent; } return strrev($str); } // 构造fail指针 function buildFailIndex(&$node, $fail_array = [], &$failTrie) { $fail_array[] = $node; if (!isset($failTrie[$node->trie])) { $failTrie[] = []; } ($failTrie[$node->trie])[] = &$node; $node->failIndex = count($fail_array) - 1; if (empty($node->childNode)) { return $fail_array; } else { foreach ($node->childNode as $k => $v) { $fail_array = buildFailIndex($node->childNode[$k], $fail_array, $failTrie); } return $fail_array; } } // 结构树 function trie(&$failArray, &$failTrie) { foreach ($failTrie as $k => $v) { // 层数循环 foreach ($v as $k1 => $v1) { // 每层每个节点循环 $failTrie[$k][$k1]->fail = buildTrie($failTrie[$k][$k1], $failArray); } } } function buildTrie(&$node, &$failArray) { if ($node->failIndex == 0 || $node->parent->failIndex === 0) { return 0; } // 循环问题 foreach ($failArray[$node->parent->fail]->childNode as $k => $v) { if ($v->value == $node->value) { return $v->failIndex; } } return 0; } /* 调用测试开始 */ $head = new Node; // 添加单词 addString($head, 'say'); addString($head, 'she'); addString($head, 'sher'); addString($head, 'h'); addString($head, 'her'); // fail二维指针数组(方便遍历) $failTrie = []; // 创建一个fail指针数组 $failArray = buildFailIndex($head, [], $failTrie); trie($failArray, $failTrie); var_dump(search('hshershrewr', $head, $failArray)); 复制代码