数组数组化


99

考虑以下数组:

/www/htdocs/1/sites/lib/abcdedd
/www/htdocs/1/sites/conf/xyz
/www/htdocs/1/sites/conf/abc/def
/www/htdocs/1/sites/htdocs/xyz
/www/htdocs/1/sites/lib2/abcdedd

什么是检测公共基本路径的最短,最优雅的方法-在这种情况下

/www/htdocs/1/sites/

并将其从数组中的所有元素中删除?

lib/abcdedd
conf/xyz
conf/abc/def
htdocs/xyz
lib2/abcdedd

4
这可能值得尝试:en.wikibooks.org/wiki/Algorithm_implementation/Strings/…(我尝试过并且可以使用)。
理查德·诺普

1
噢!如此出色的输入。我将采取一个解决现有问题的方法,但是我觉得要真正选择一个合理的已接受答案,我必须比较解决方案。我可能需要一段时间才能做到这一点,但是我当然会的。
Pekka 2010年

有趣的标题:D btw:为什么我不能在提名主持人名单上找到你?@Pekka
Surrican 2011年

2
两年没有被接受的答案?
Gordon

1
@Pekka亲近三年,因为这有没有公认的答案:(它是这样一个真棒的标题,我刚才想起了它,并用Google搜索“tetrising的数组。”
卡米洛·马丁

Answers:


35

编写一个longest_common_prefix将两个字符串作为输入的函数。然后以任何顺序将其应用于字符串,以将其简化为它们的公共前缀。由于它是关联和可交换的,因此顺序与结果无关紧要。

这与其他二进制运算(例如加法或最大公约数)相同。


8
+1。比较前两个字符串后,使用结果(公共路径)与第三个字符串进行比较,依此类推。
米兰·巴布斯科夫

23

将它们加载到trie数据结构中。从父节点开始,查看哪个子节点数大于1。找到该魔术节点后,只需拆除父节点结构并以当前节点为根即可。


10
将数据加载到您描述的trie树结构中的操作是否会包括查找最长公共前缀的算法,从而实际上不需要使用树结构?也就是说,当您在构建树时可以检测到多个子树时,为什么要检查它呢。为什么那么一棵树呢?我的意思是如果您已经从数组开始。如果您可以将存储更改为仅使用特里而不是数组,我想这是有道理的。
Ben Schwehn 2010年

2
我认为,如果您小心一点,那么我的解决方案比构建Trie更有效。
starblue 2010年

这个答案是错误的。我的答案和其他答案中都贴有O(n)的平凡解决方案。
阿里·罗嫩

@ el.pescado:在最坏的情况下,尝试的大小是四进制,并且带有源字符串的长度。
Billy ONeal,2012年

10
$common = PHP_INT_MAX;
foreach ($a as $item) {
        $common = min($common, str_common($a[0], $item, $common));
}

$result = array();
foreach ($a as $item) {
        $result[] = substr($item, $common);
}
print_r($result);

function str_common($a, $b, $max)
{
        $pos = 0;
        $last_slash = 0;
        $len = min(strlen($a), strlen($b), $max + 1);
        while ($pos < $len) {
                if ($a{$pos} != $b{$pos}) return $last_slash;
                if ($a{$pos} == '/') $last_slash = $pos;
                $pos++;
        }
        return $last_slash;
}

这是迄今为止最好的解决方案,但需要改进。它没有考虑以前的最长公共路径(可能会遍历比所需更多的字符串),也没有考虑路径(因此/usr/lib/usr/lib2它给出/usr/lib了最长的公共路径,而不是/usr/)。我(希望)都解决了。
加布

7

好吧,考虑到您可以XOR在这种情况下使用它来查找字符串的公共部分。每当您对两个相同的字节进行异或运算时,都会得到一个空字节作为输出。因此,我们可以利用它来发挥优势:

$first = $array[0];
$length = strlen($first);
$count = count($array);
for ($i = 1; $i < $count; $i++) {
    $length = min($length, strspn($array[$i] ^ $first, chr(0)));
}

在该单循环之后,该$length变量将等于字符串数组之间的最长公共基数。然后,我们可以从第一个元素中提取公共部分:

$common = substr($array[0], 0, $length);

那里有。作为功​​能:

function commonPrefix(array $strings) {
    $first = $strings[0];
    $length = strlen($first);
    $count = count($strings);
    for ($i = 1; $i < $count; $i++) {
        $length = min($length, strspn($strings[$i] ^ $first, chr(0)));
    }
    return substr($first, 0, $length);
}

请注意,它确实使用了多个迭代,但是这些迭代是在库中完成的,因此在解释语言中这将获得巨大的效率提升...

现在,如果只需要完整路径,我们需要截断到最后一个/字符。所以:

$prefix = preg_replace('#/[^/]*$', '', commonPrefix($paths));

现在,它可能会过度剪切两个字符串,例如/foo/bar/foo/bar/baz将被剪切为/foo。但是缺少添加另一轮回合来确定下一个字符是字符串末尾/ 还是字符串末尾的方法,我看不出有办法解决...


3

天真的方法是将处的路径爆炸,/然后连续比较数组中的每个元素。因此,例如第一个元素在所有数组中都为空,因此将其删除,下一个元素将为www,在所有数组中相同,因此将其删除,依此类推。

就像是 (未经测试

$exploded_paths = array();

foreach($paths as $path) {
    $exploded_paths[] = explode('/', $path);
}

$equal = true;
$ref = &$exploded_paths[0]; // compare against the first path for simplicity

while($equal) {   
    foreach($exploded_paths as $path_parts) {
        if($path_parts[0] !== $ref[0]) {
            $equal = false;
            break;
        }
    }
    if($equal) {
        foreach($exploded_paths as &$path_parts) {
            array_shift($path_parts); // remove the first element
        }
    }
}

之后,您只需$exploded_paths再次内插元素:

function impl($arr) {
    return '/' . implode('/', $arr);
}
$paths = array_map('impl', $exploded_paths);

这给了我:

Array
(
    [0] => /lib/abcdedd
    [1] => /conf/xyz
    [2] => /conf/abc/def
    [3] => /htdocs/xyz
    [4] => /conf/xyz
)

这可能无法很好地扩展;)


3

好的,我不确定这是防弹的,但是我认为它可以工作:

echo array_reduce($array, function($reducedValue, $arrayValue) {
    if($reducedValue === NULL) return $arrayValue;
    for($i = 0; $i < strlen($reducedValue); $i++) {
        if(!isset($arrayValue[$i]) || $arrayValue[$i] !== $reducedValue[$i]) {
            return substr($reducedValue, 0, $i);
        }
    }
    return $reducedValue;
});

这将把数组中的第一个值作为参考字符串。然后,它将遍历引用字符串,并将每个字符与第二个字符串的char在相同位置进行比较。如果一个字符不匹配,则将参考字符串缩短到该字符的位置,然后比较下一个字符串。然后,该函数将返回最短的匹配字符串。

性能取决于给定的字符串。参考字符串越短,代码完成的速度就越快。我真的不知道如何将其放入公式中。

我发现Artefacto的字符串排序方法提高了性能。新增中

asort($array);
$array = array(array_shift($array), array_pop($array));

之前 array_reduce会显著提高性能。

另请注意,这将返回最长匹配的初始子字符串,该子字符串更通用,但不会为您提供通用路径。你必须跑

substr($result, 0, strrpos($result, '/'));

结果。然后您可以使用结果删除值

print_r(array_map(function($v) use ($path){
    return str_replace($path, '', $v);
}, $array));

这应该给:

[0] => /lib/abcdedd
[1] => /conf/xyz/
[2] => /conf/abc/def
[3] => /htdocs/xyz
[4] => /lib2/abcdedd

欢迎反馈。


3

您可以最快的方式删除前缀,每个字符仅读取一次:

function findLongestWord($lines, $delim = "/")
{
    $max = 0;
    $len = strlen($lines[0]); 

    // read first string once
    for($i = 0; $i < $len; $i++) {
        for($n = 1; $n < count($lines); $n++) {
            if($lines[0][$i] != $lines[$n][$i]) {
                // we've found a difference between current token
                // stop search:
                return $max;
            }
        }
        if($lines[0][$i] == $delim) {
            // we've found a complete token:
            $max = $i + 1;
        }
    }
    return $max;
}

$max = findLongestWord($lines);
// cut prefix of len "max"
for($n = 0; $n < count($lines); $n++) {
    $lines[$n] = substr(lines[$n], $max, $len);
}

确实,基于字符的比较将是最快的。所有其他解决方案都使用“昂贵的”运算符,最后它们还将进行(多个)字符比较。甚至在圣Jo的经文中也提到过
Jan Fabry

2

这具有不具有线性时间复杂度的缺点。但是,在大多数情况下,排序肯定不会花费更多时间。

基本上,这里最聪明的部分(至少我找不到它的缺点)是排序后,您只需要比较第一个路径和最后一个路径即可。

sort($a);
$a = array_map(function ($el) { return explode("/", $el); }, $a);
$first = reset($a);
$last = end($a);
for ($eqdepth = 0; $first[$eqdepth] === $last[$eqdepth]; $eqdepth++) {}
array_walk($a,
    function (&$el) use ($eqdepth) {
        for ($i = 0; $i < $eqdepth; $i++) {
            array_shift($el);
        }
     });
$res = array_map(function ($el) { return implode("/", $el); }, $a);

2
$values = array('/www/htdocs/1/sites/lib/abcdedd',
                '/www/htdocs/1/sites/conf/xyz',
                '/www/htdocs/1/sites/conf/abc/def',
                '/www/htdocs/1/sites/htdocs/xyz',
                '/www/htdocs/1/sites/lib2/abcdedd'
);


function splitArrayValues($r) {
    return explode('/',$r);
}

function stripCommon($values) {
    $testValues = array_map('splitArrayValues',$values);

    $i = 0;
    foreach($testValues[0] as $key => $value) {
        foreach($testValues as $arraySetValues) {
            if ($arraySetValues[$key] != $value) break 2;
        }
        $i++;
    }

    $returnArray = array();
    foreach($testValues as $value) {
        $returnArray[] = implode('/',array_slice($value,$i));
    }

    return $returnArray;
}


$newValues = stripCommon($values);

echo '<pre>';
var_dump($newValues);
echo '</pre>';

编辑使用array_walk重建数组的原始方法的变体

$values = array('/www/htdocs/1/sites/lib/abcdedd',
                '/www/htdocs/1/sites/conf/xyz',
                '/www/htdocs/1/sites/conf/abc/def',
                '/www/htdocs/1/sites/htdocs/xyz',
                '/www/htdocs/1/sites/lib2/abcdedd'
);


function splitArrayValues($r) {
    return explode('/',$r);
}

function rejoinArrayValues(&$r,$d,$i) {
    $r = implode('/',array_slice($r,$i));
}

function stripCommon($values) {
    $testValues = array_map('splitArrayValues',$values);

    $i = 0;
    foreach($testValues[0] as $key => $value) {
        foreach($testValues as $arraySetValues) {
            if ($arraySetValues[$key] != $value) break 2;
        }
        $i++;
    }

    array_walk($testValues, 'rejoinArrayValues', $i);

    return $testValues;
}


$newValues = stripCommon($values);

echo '<pre>';
var_dump($newValues);
echo '</pre>';

编辑

最有效,最优雅的答案可能涉及从提供的每个答案中提取功能和方法


1

我将explode基于/值,然后使用它们array_intersect_assoc来检测公共元素并确保它们在数组中具有正确的对应索引。生成的数组可以重新组合以生成公共路径。

function getCommonPath($pathArray)
{
    $pathElements = array();

    foreach($pathArray as $path)
    {
        $pathElements[] = explode("/",$path);
    }

    $commonPath = $pathElements[0];

    for($i=1;$i<count($pathElements);$i++)
    {
        $commonPath = array_intersect_assoc($commonPath,$pathElements[$i]);
    }

    if(is_array($commonPath) return implode("/",$commonPath);
    else return null;
}

function removeCommonPath($pathArray)
{
    $commonPath = getCommonPath($pathArray());

    for($i=0;$i<count($pathArray);$i++)
    {
        $pathArray[$i] = substr($pathArray[$i],str_len($commonPath));
    }

    return $pathArray;
}

这未经测试,但是,想法是该$commonPath数组只包含已与之比较的所有路径数组中所包含的路径元素。循环完成后,我们只需将其与/重新组合即可$commonPath

更新 正如Felix Kling指出的那样,array_intersect将不考虑具有相同元素但顺序不同的路径...为了解决此问题,我使用array_intersect_assoc代替了array_intersect

更新 添加的代码也可以从数组中删除公共路径(或俄罗斯方块!)。


这可能行不通。考虑/a/b/c/d/d/c/b/a。相同的元素,不同的路径。
Felix Kling

@Felix Kling我已更新为使用array_intersect_assoc也执行索引检查
Brendan Bullen 2010年

1

如果仅从字符串比较角度来看,则可以简化该问题。这可能比数组拆分更快:

$longest = $tetris[0];  # or array_pop()
foreach ($tetris as $cmp) {
        while (strncmp($longest+"/", $cmp, strlen($longest)+1) !== 0) {
                $longest = substr($longest, 0, strrpos($longest, "/"));
        }
}

这不适用于例如此设置的数组('/ www / htdocs / 1 / sites / conf / abc / def','/ www / htdocs / 1 / sites / htdocs / xyz','/ www / htdocs / 1 / sitesjj / lib2 / abcdedd',)。
Artefacto

@Artefacto:你是对的。因此,我只是对其进行了修改,以在比较中始终包含一个斜杠“ /”。使其无歧义。
mario 2010年

1

也许移植Python os.path.commonprefix(m)使用的算法会行得通吗?

def commonprefix(m):
    "Given a list of pathnames, returns the longest common leading component"
    if not m: return ''
    s1 = min(m)
    s2 = max(m)
    n = min(len(s1), len(s2))
    for i in xrange(n):
        if s1[i] != s2[i]:
            return s1[:i]
    return s1[:n]

就是说...

function commonprefix($m) {
  if(!$m) return "";
  $s1 = min($m);
  $s2 = max($m);
  $n = min(strlen($s1), strlen($s2));
  for($i=0;$i<$n;$i++) if($s1[$i] != $s2[$i]) return substr($s1, 0, $i);
  return substr($s1, 0, $n);
}

之后,您可以使用公共前缀的长度作为起始偏移量来替换原始列表的每个元素。


1

我将帽子戴上戒指……

function longestCommonPrefix($a, $b) {
    $i = 0;
    $end = min(strlen($a), strlen($b));
    while ($i < $end && $a[$i] == $b[$i]) $i++;
    return substr($a, 0, $i);
}

function longestCommonPrefixFromArray(array $strings) {
    $count = count($strings);
    if (!$count) return '';
    $prefix = reset($strings);
    for ($i = 1; $i < $count; $i++)
        $prefix = longestCommonPrefix($prefix, $strings[$i]);
    return $prefix;
}

function stripPrefix(&$string, $foo, $length) {
    $string = substr($string, $length);
}

用法:

$paths = array(
    '/www/htdocs/1/sites/lib/abcdedd',
    '/www/htdocs/1/sites/conf/xyz',
    '/www/htdocs/1/sites/conf/abc/def',
    '/www/htdocs/1/sites/htdocs/xyz',
    '/www/htdocs/1/sites/lib2/abcdedd',
);

$longComPref = longestCommonPrefixFromArray($paths);
array_walk($paths, 'stripPrefix', strlen($longComPref));
print_r($paths);

1

好吧,这里已经有一些解决方案了,只是因为它很有趣:

$values = array(
    '/www/htdocs/1/sites/lib/abcdedd',
    '/www/htdocs/1/sites/conf/xyz',
    '/www/htdocs/1/sites/conf/abc/def', 
    '/www/htdocs/1/sites/htdocs/xyz',
    '/www/htdocs/1/sites/lib2/abcdedd' 
);

function findCommon($values){
    $common = false;
    foreach($values as &$p){
        $p = explode('/', $p);
        if(!$common){
            $common = $p;
        } else {
            $common = array_intersect_assoc($common, $p);
        }
    }
    return $common;
}
function removeCommon($values, $common){
    foreach($values as &$p){
        $p = explode('/', $p);
        $p = array_diff_assoc($p, $common);
        $p = implode('/', $p);
    }

    return $values;
}

echo '<pre>';
print_r(removeCommon($values, findCommon($values)));
echo '</pre>';

输出:

Array
(
    [0] => lib/abcdedd
    [1] => conf/xyz
    [2] => conf/abc/def
    [3] => htdocs/xyz
    [4] => lib2/abcdedd
)

0
$arrMain = array(
            '/www/htdocs/1/sites/lib/abcdedd',
            '/www/htdocs/1/sites/conf/xyz',
            '/www/htdocs/1/sites/conf/abc/def',
            '/www/htdocs/1/sites/htdocs/xyz',
            '/www/htdocs/1/sites/lib2/abcdedd'
);
function explodePath( $strPath ){ 
    return explode("/", $strPath);
}

function removePath( $strPath)
{
    global $strCommon;
    return str_replace( $strCommon, '', $strPath );
}
$arrExplodedPaths = array_map( 'explodePath', $arrMain ) ;

//Check for common and skip first 1
$strCommon = '';
for( $i=1; $i< count( $arrExplodedPaths[0] ); $i++)
{
    for( $j = 0; $j < count( $arrExplodedPaths); $j++ )
    {
        if( $arrExplodedPaths[0][ $i ] !== $arrExplodedPaths[ $j ][ $i ] )
        {
            break 2;
        } 
    }
    $strCommon .= '/'.$arrExplodedPaths[0][$i];
}
print_r( array_map( 'removePath', $arrMain ) );

效果很好...类似于马克贝克,但使用str_replace


0

可能过于幼稚和笨拙,但它可以工作。我已经使用了这个算法

<?php

function strlcs($str1, $str2){
    $str1Len = strlen($str1);
    $str2Len = strlen($str2);
    $ret = array();

    if($str1Len == 0 || $str2Len == 0)
        return $ret; //no similarities

    $CSL = array(); //Common Sequence Length array
    $intLargestSize = 0;

    //initialize the CSL array to assume there are no similarities
    for($i=0; $i<$str1Len; $i++){
        $CSL[$i] = array();
        for($j=0; $j<$str2Len; $j++){
            $CSL[$i][$j] = 0;
        }
    }

    for($i=0; $i<$str1Len; $i++){
        for($j=0; $j<$str2Len; $j++){
            //check every combination of characters
            if( $str1[$i] == $str2[$j] ){
                //these are the same in both strings
                if($i == 0 || $j == 0)
                    //it's the first character, so it's clearly only 1 character long
                    $CSL[$i][$j] = 1; 
                else
                    //it's one character longer than the string from the previous character
                    $CSL[$i][$j] = $CSL[$i-1][$j-1] + 1; 

                if( $CSL[$i][$j] > $intLargestSize ){
                    //remember this as the largest
                    $intLargestSize = $CSL[$i][$j]; 
                    //wipe any previous results
                    $ret = array();
                    //and then fall through to remember this new value
                }
                if( $CSL[$i][$j] == $intLargestSize )
                    //remember the largest string(s)
                    $ret[] = substr($str1, $i-$intLargestSize+1, $intLargestSize);
            }
            //else, $CSL should be set to 0, which it was already initialized to
        }
    }
    //return the list of matches
    return $ret;
}


$arr = array(
'/www/htdocs/1/sites/lib/abcdedd',
'/www/htdocs/1/sites/conf/xyz',
'/www/htdocs/1/sites/conf/abc/def',
'/www/htdocs/1/sites/htdocs/xyz',
'/www/htdocs/1/sites/lib2/abcdedd'
);

// find the common substring
$longestCommonSubstring = strlcs( $arr[0], $arr[1] );

// remvoe the common substring
foreach ($arr as $k => $v) {
    $arr[$k] = str_replace($longestCommonSubstring[0], '', $v);
}
var_dump($arr);

输出:

array(5) {
  [0]=>
  string(11) "lib/abcdedd"
  [1]=>
  string(8) "conf/xyz"
  [2]=>
  string(12) "conf/abc/def"
  [3]=>
  string(10) "htdocs/xyz"
  [4]=>
  string(12) "lib2/abcdedd"
}

:)


@Doomsday我的答案中有一个指向Wikipedia的链接...在发表评论之前,请先尝试阅读它。
理查德·诺普

我认为最后您只比较前两个路径。在您的示例中,此方法有效,但如果删除第一个路径,它将找到/www/htdocs/1/sites/conf/一个通用匹配项。同样,该算法搜索从字符串中任何位置开始的子字符串,但是对于这个问题,您知道可以从位置0开始,这使它更加简单。
Jan Fabry
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.