Тетрисинг массива

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
занимательное название: D Кстати: почему я не могу найти вас в списке назначенных модераторов? @Pekka
The Surrican
2
нет принятого ответа в течение двух лет?
Гордон
1
@Pekka Уже почти три года, так как на этот вопрос нет принятого ответа :( И это такой классный заголовок, что я вспомнил его мгновение назад и набрал в Google "тетрисинг массива".
Камило Мартин

Ответы:

35

Напишите функцию, longest_common_prefixкоторая принимает на вход две строки. Затем примените его к строкам в любом порядке, чтобы уменьшить их до общего префикса. Поскольку он ассоциативен и коммутативен, порядок не имеет значения для результата.

Это то же самое, что и для других двоичных операций, таких как, например, сложение или наибольший общий делитель.

звездно-голубой
источник
8
+1. После сравнения первых двух строк используйте результат (общий путь) для сравнения с третьей строкой и так далее.
Милан Бабушков
23

Загрузите их в trie-структуру данных. Начиная с родительского узла, посмотрите, у какого дочернего узла больше одного. Как только вы найдете этот волшебный узел, просто демонтируйте структуру родительского узла и сделайте текущий узел корневым.

хвастун
источник
10
Разве операция, загружающая данные в описываемую вами древовидную структуру, не будет включать в себя алгоритм поиска самого длинного общего префикса, что делает ненужным использование древовидной структуры? Т.е. зачем проверять дерево на наличие нескольких дочерних элементов, если вы можете обнаружить это при построении дерева. Зачем тогда вообще дерево? Я имею в виду, если вы уже начинаете с массива. Если вы можете изменить хранилище, просто используя trie вместо массивов, я думаю, это имеет смысл.
Ben Schwehn
2
Я думаю, что если вы будете осторожны, то мое решение будет более эффективным, чем создание дерева.
starblue
Это неверный ответ. В моем и других ответах есть тривиальные решения, которые являются O (n).
Ари Ронен,
@ el.pescado: Попытки имеют квадратический размер по сравнению с длиной исходной строки в худшем случае.
Билли Онил
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;
}
Sjoerd
источник
Это, безусловно, лучшее решение, но оно требует улучшения. Он не принимал во внимание предыдущий самый длинный общий путь (возможно, повторение большей части строки, чем необходимо), и не принимал во внимание пути (поэтому for /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. Но короткие добавления другой итерации раунда , чтобы определить , если следующий символ является либо / или с истекшим строки, я не могу видеть путь вокруг этого ...

ircmaxell
источник
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;
});

Это примет первое значение в массиве как ссылочную строку. Затем он будет перебирать ссылочную строку и сравнивать каждый символ с символом второй строки в той же позиции. Если символ не совпадает, ссылочная строка будет сокращена до позиции символа, и будет сравниваться следующая строка. Тогда функция вернет самую короткую совпадающую строку.

Производительность зависит от данных струн. Чем раньше станет короче ссылочная строка, тем быстрее завершится код. Я действительно понятия не имею, как выразить это в формуле.

Я обнаружил, что подход 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);
}
Конец света
источник
Действительно, сравнение на основе символов будет самым быстрым. Во всех других решениях используются «дорогие» операторы, которые, в конце концов, также будут выполнять (множественные) сравнения символов. Об этом даже упоминается в писаниях Святого Иоиля !
Ян Фабри,
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

Обновление Как указал Феликс Клинг, array_intersectне будут рассматривать пути, которые имеют общие элементы, но в разном порядке ... Чтобы решить эту проблему, я использовал array_intersect_assocвместоarray_intersect

Обновление Добавлен код для удаления общего пути (или тетриса!) Из массива.

Брендан Буллен
источник
Вероятно, это не сработает. Считайте /a/b/c/dи /d/c/b/a. Те же элементы, разные пути.
Felix Kling
@Felix Kling Я обновился, чтобы использовать array_intersect_assoc, который также выполняет проверку индекса,
Брендан Буллен
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: Вы были правы. Поэтому я просто изменил его, чтобы всегда включать в сравнение завершающую косую черту "/". Делает это однозначным.
Марио
1

Возможно, портирование алгоритма, os.path.commonprefix(m)используемого Python, сработает?

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);
}

После этого вы можете просто подставить каждый элемент исходного списка с длиной общего префикса в качестве начального смещения.

AKX
источник
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
)
acm
источник
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 ) );

Это отлично работает ... похоже на Mark Baker, но использует str_replace

KoolKabin
источник
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 В моем ответе есть ссылка на википедию ... попробуйте сначала прочитать ее, прежде чем комментировать.
Ричард Кноп,
Я думаю, что в итоге вы сравните только первые два пути. В вашем примере это работает, но если вы удалите первый путь, он будет /www/htdocs/1/sites/conf/найден как общее совпадение. Кроме того, алгоритм ищет подстроки, начинающиеся где угодно в строке, но вы знаете, что в этом вопросе вы можете начать с местоположения 0, что значительно упрощает задачу.
Ян Фабри,