并提前感谢您的帮助。
背景 - 我正在编写一个PHP脚本,需要找出调用者试图达到的目的地。电话号码前缀是标识目的地的字符串。对于每个调用,程序必须找到与字符串匹配的最长前缀。例如,数字30561234567将匹配305但不会匹配3057或304.如果存在3056,则它将是首选匹配。
在研究了几个数据结构之后,每个节点存储一个数字并包含指向其他10个可能选择的指针的树似乎是理想的。这可以实现为一个数组数组,其中脚本可以检查3,在那里找到一个数组,然后在该新数组上检查0,找到另一个数组,依此类推,直到找到一个值而不是数组。该值将是目标ID(脚本的输出)。
要求 - 性能绝对至关重要。检查这些前缀所花费的时间会延迟调用,并且每个服务器都必须处理大量调用,因此数据结构必须存储在内存中。目前大约有6000个前缀。
问题 - 每次服务器接收调用时都会运行该脚本,因此数据必须保存在某种缓存服务器中。在检查了memcached和APC(高级PHP缓存)之后,我决定使用APC,因为它[更快] [3](它是一个本地内存存储)
我遇到的问题是数组数组最多可以变为10个数组,并且将是一个非常复杂的数据结构,如果我将缓存作为对象添加,将需要很长时间来反序列化
但是,如果我将每个单独的数组分别添加到缓存中(有一些逻辑命名结构可以很容易地找到它,就像数组3的3,那么30代表数组30,305代表那个补丁之后的数组等等)。 。)我将不得不从缓存中多次获取不同的数组(每次调用最多10个),这使得我经常点击缓存。
我是以正确的方式来做这件事的吗?也许有另一种解决方案?或者我提议的方法之一优于另一方法?
感谢您输入,
Alex
答案 0 :(得分:2)
我看到的方式,使用简单的数组结构应该可以正常工作......
示例代码:(请注意,对于性能,前缀是数组中的键,而不是值)
// $prefixes = array(3=>1, 30=>1, 304=>1,305=>1,3056=>1,306=>1,31=>1, 40=>1);
function matchNumber($number)
{
$prefixes = getPrefixesFromCache();
$number = "$number";
// try to find the longest prefix matching $number
while ($number != '') {
if (isset($keys[$number]))
break;
// not found yet, subtract last digit
$number = substr($number, 0, -1);
}
return $number;
}
另一种方法是直接查询缓存中的数字 - 在这种情况下,可以进一步优化:
Snippet :(请注意,query_cache_for()应替换为缓存机制使用的任何函数)
function matchNumber($number)
{
$temp = "$number";
$found = false;
while (1) {
$temp = substr($temp, 0, ceil(strlen($temp)/2) );
$found = query_cache_for($temp);
if ($found)
break;
if (strlen($temp) == 1)
return FALSE; // should not happen!
}
while ($found) {
$result = $temp;
// add another digit
$temp .= substr($number, strlen($temp), 1);
$found = query_cache_for($temp);
}
return $result;
}
这种方法具有明显的优势,即每个前缀都是缓存中的单个元素 - 密钥可以是'asterix_prefix_< number>'例如,该值不重要(1个工作)。
答案 1 :(得分:2)
以下是N-ary树结构的一些示例代码;
class PrefixCache {
const EOS = 'eos';
protected $data;
function __construct() {
$this->data = array();
$this->data[self::EOS] = false;
}
function addPrefix($str) {
$str = (string) $str;
$len = strlen($str);
for ($i=0, $t =& $this->data; $i<$len; ++$i) {
$ch = $str[$i];
if (!isset($t[$ch])) {
$t[$ch] = array();
$t[$ch][self::EOS] = false;
}
$t =& $t[$ch];
}
$t[self::EOS] = true;
}
function matchPrefix($str) {
$str = (string) $str;
$len = strlen($str);
$so_far = '';
$best = '';
for ($i=0, $t =& $this->data; $i<$len; ++$i) {
$ch = $str[$i];
if (!isset($t[$ch]))
return $best;
else {
$so_far .= $ch;
if ($t[$ch][self::EOS])
$best = $so_far;
$t =& $t[$ch];
}
}
return false; // string not long enough - potential longer matches remain
}
function dump() {
print_r($this->data);
}
}
然后可以将其称为
$pre = new PrefixCache();
$pre->addPrefix('304');
$pre->addPrefix('305');
// $pre->addPrefix('3056');
$pre->addPrefix('3057');
echo $pre->matchPrefix('30561234567');
根据需要执行(返回305;如果取消注释3056,则返回3056)。
请注意,我为每个节点添加了一个terminal-flag;这可以避免错误的部分匹配,即如果你添加前缀3056124,它将正确匹配3056而不是返回305612。
每次避免重新加载的最佳方法是将其转换为服务;但是,在这样做之前,我会测量APC的运行时间。它可能足够快。
亚历克斯:你的答案绝对正确 - 但不适用于这个问题:)答案 2 :(得分:1)
由于您只使用数字,因此直接使用字符串效率很低。
您可以执行二进制搜索算法。如果您存储所有前缀(以数字方式),填充到15个位置然后按顺序,您可以扫描6000个代码,大约是log2(6000)〜= 13步。
例如,如果您有以下代码:
您可以将以下内容存储在数组中:
步骤如下:
一些示例代码可以看到它的实际效果:
// Example for prefixes 0100,01,012,0127,0200
$prefixes = array('0100','0101','0120','0127','0200');
$prefix_lengths = array(4,2,3,4,4);
$longest_length_prefix = 4;
echo GetPrefix('01003508163');
function GetPrefix($number_to_check) {
global $prefixes;
global $prefix_lengths;
global $longest_length_prefix;
$stripped_number = substr($number_to_check, 0, $longest_length_prefix);
// Binary search
$window_floor = 0;
$window_ceiling = count($prefixes)-1;
$prefix_index = -1;
do {
$mid_point = ($window_floor+$window_ceiling)>>1;
if ($window_floor==($window_ceiling-1)) {
if ($stripped_number>=$prefixes[$window_ceiling]) {
$prefix_index=$window_ceiling;
break;
} elseif ($stripped_number>=$prefixes[$window_floor]) {
$prefix_index=$window_floor;
break;
} else {
break;
}
} else {
if ($stripped_number==$prefixes[$mid_point]) {
$prefix_index=$mid_point;
break;
} elseif ($stripped_number<$prefixes[$mid_point]) {
$window_ceiling=$mid_point;
} else {
$window_floor=$mid_point;
}
}
} while (true);
if ($prefix_index==-1 || substr($number_to_check, 0, $prefix_lengths[$prefix_index])!=substr($prefixes[$prefix_index],0, $prefix_lengths[$prefix_index])) {
return 'invalid prefix';
} else {
return substr($prefixes[$prefix_index], 0, $prefix_lengths[$prefix_index]);
}
}
答案 3 :(得分:0)
我是通过使用字符串,目标的哈希表来实现的,其中键是表示目标前缀的字符串。关键因素是必须对哈希表进行排序,以便首先检查最长的字符串。一旦找到匹配的前缀,就知道呼叫目的地。
我实际上还有一轮正则表达式,用于更复杂的目的地,并检查目标前缀之前的正则表达式。
我没有测量到比赛所需的时间,但我怀疑最多15分钟。检查desitnation然后用户余额并最终设置呼叫时间限制的整个过程大约需要150ms。就我而言,我使用的是FastAGI和C#Windows服务。只要你花费不到500毫秒,它就会让你的用户无法接受。
答案 4 :(得分:0)
我还运行了一个电话应用程序...我所做的是提供一个内部REST API来调用,这将缓存已知的电话号码并执行所有前缀检查。
此外,我假设您正在寻找国家/地区代码。 NANP只有几个重叠的国家代码。所以我首先看一下NANP,并快速匹配以下数字的数量(7)以确保,否则我会回到国家代码。然后我粗略地了解每个国家通过正则表达式应该有多少电话号码。
我正在使用XML文档并在XPath上进行匹配,然后在可能的情况下缓存XPath结果。
使用REST API的一个很酷的事情是,它可以用来清理数字,然后再将它们存储在数据库中进行计费。
这不是一门精确的科学,但似乎有效。
答案 5 :(得分:0)
找到最长的公共子序列是动态编程的经典应用。解决方案是O(n)。 http://en.wikipedia.org/wiki/Longest_common_subsequence_problem