Tetris-ing un array


99

Considera il seguente 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

qual è il modo più breve ed elegante per rilevare il percorso di base comune, in questo caso

/www/htdocs/1/sites/

e rimuoverlo da tutti gli elementi nell'array?

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

4
Potrebbe valere la pena provare: en.wikibooks.org/wiki/Algorithm_implementation/Strings/… (l'ho provato e funziona).
Richard Knop

1
Awwww! Un così grande contributo brillante. Ne prenderò uno per risolvere il mio problema in questione, ma sento che per scegliere davvero una risposta accettata giustificata, dovrò confrontare le soluzioni. Potrebbe volerci un po 'prima che riesca a farlo, ma sicuramente lo farò.
Pekka

titolo divertente: D btw: perché non riesco a trovarti nell'elenco dei moderatori nominati? @Pekka
The Surrican

2
nessuna risposta accettata per due anni?
Gordon

1
@Pekka Avvicinarsi a tre anni da quando questa non ha una risposta accettata :( Ed è un titolo così fantastico che me ne sono ricordato un attimo fa e ho cercato su Google "tetrising an array".
Camilo Martin

Risposte:


35

Scrivi una funzione longest_common_prefixche accetta due stringhe come input. Quindi applicalo alle stringhe in qualsiasi ordine per ridurle al loro prefisso comune. Poiché è associativo e commutativo, l'ordine non ha importanza per il risultato.

Questo è lo stesso di altre operazioni binarie come ad esempio l'addizione o il massimo comune divisore.


8
+1. Dopo aver confrontato le prime 2 stringhe, utilizzare il risultato (percorso comune) per confrontare con la terza stringa e così via.
Milan Babuškov

23

Caricali in una struttura dati trie. Partendo dal nodo genitore, vedi quale sta avendo un conteggio figli maggiore di uno. Una volta trovato quel nodo magico, smantella la struttura del nodo genitore e hai il nodo corrente come root.


10
L'operazione che carica i dati nella struttura ad albero del trie che descrivi non includerebbe un po 'l'algoritmo per trovare il prefisso comune più lungo, rendendo così superfluo l'utilizzo di una struttura ad albero? Vale a dire perché controllare l'albero per più bambini quando è possibile rilevarlo durante la costruzione dell'albero. Perché allora un albero? Voglio dire, se inizi già con un array. Se puoi cambiare lo spazio di archiviazione usando solo un trie invece di array, immagino che abbia senso.
Ben Schwehn

2
Penso che se stai attento, la mia soluzione è più efficiente della costruzione di un trie.
starblue

Questa risposta è sbagliata. Ci sono soluzioni banali pubblicate nella mia e altre risposte che sono O (n).
Ari Ronen

@ el.pescado: I tentativi sono di dimensione quadratica con la lunghezza della stringa sorgente nel caso peggiore.
Billy ONeal

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

Questa è di gran lunga la migliore soluzione pubblicata, ma necessitava di miglioramenti. Non ha preso in considerazione il percorso comune più lungo precedente (forse iterando su più stringa del necessario) e non ha preso in considerazione i percorsi (quindi per /usr/libe /usr/lib2ha dato /usr/libcome percorso comune più lungo, piuttosto che /usr/). Ho (si spera) risolto entrambi.
Gabe

7

Bene, considerando che puoi usare XORin questa situazione per trovare le parti comuni della stringa. Ogni volta che si xo due byte uguali, si ottiene un nullbyte come output. Quindi possiamo usarlo a nostro vantaggio:

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

Dopo quel singolo ciclo, la $lengthvariabile sarà uguale alla parte base comune più lunga tra l'array di stringhe. Quindi, possiamo estrarre la parte comune dal primo elemento:

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

E il gioco è fatto. Come una funzione:

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

Si noti che utilizza più di un'iterazione, ma quelle iterazioni vengono eseguite nelle librerie, quindi nei linguaggi interpretati questo avrà un enorme guadagno di efficienza ...

Ora, se vuoi solo percorsi completi, dobbiamo troncare all'ultimo /carattere. Così:

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

Ora, potrebbe tagliare eccessivamente due corde come /foo/bare /foo/bar/bazverrà tagliato a /foo. Ma a meno di aggiungere un altro round di iterazione per determinare se il carattere successivo è uno / o alla fine della stringa, non riesco a vedere un modo per aggirare questo ...


3

Un approccio ingenuo sarebbe quello di far esplodere i percorsi al /e successivo confrontare ogni elemento negli array. Quindi, ad esempio, il primo elemento sarebbe vuoto in tutti gli array, quindi verrà rimosso, l'elemento successivo sarà www, è lo stesso in tutti gli array, quindi viene rimosso, ecc.

Qualcosa di simile a (non testato)

$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
        }
    }
}

Successivamente devi solo far implodere di $exploded_pathsnuovo gli elementi :

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

Il che mi dà:

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

Questo potrebbe non scalare bene;)


3

Ok, non sono sicuro che sia a prova di proiettile, ma penso che funzioni:

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

Questo prenderà il primo valore nell'array come stringa di riferimento. Quindi itererà sulla stringa di riferimento e confronterà ogni carattere con il carattere della seconda stringa nella stessa posizione. Se un carattere non corrisponde, la stringa di riferimento verrà accorciata alla posizione del carattere e verrà confrontata la stringa successiva. La funzione restituirà quindi la stringa corrispondente più breve.

Le prestazioni dipendono dalle stringhe fornite. Prima si accorcia la stringa di riferimento, più velocemente finirà il codice. Non ho davvero idea di come metterlo in una formula.

Ho scoperto che l'approccio di Artefacto per ordinare le corde aumenta le prestazioni. Aggiunta

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

prima che array_reduceaumenterà significativamente le prestazioni.

Nota anche che questo restituirà la sottostringa iniziale corrispondente più lunga , che è più versatile ma non ti darà il percorso comune . Devi correre

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

sul risultato. E poi puoi usare il risultato per rimuovere i valori

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

che dovrebbe dare:

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

Feedback benvenuti.


3

Puoi rimuovere il prefisso nel modo più veloce, leggendo ogni carattere solo una volta:

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

In effetti, un confronto basato sui caratteri sarà il più veloce. Tutte le altre soluzioni utilizzano operatori "costosi" che alla fine faranno anche confronti (multipli) di caratteri. È stato anche menzionato nelle scritture del Santo Gioele !
Jan Fabry

2

Ciò ha il vantaggio di non avere complessità temporale lineare; tuttavia, per la maggior parte dei casi l'ordinamento non sarà sicuramente l'operazione che richiede più tempo.

Fondamentalmente, la parte intelligente (almeno non sono riuscito a trovare un difetto) qui è che dopo l'ordinamento dovrai solo confrontare il primo percorso con l'ultimo.

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>';

EDIT Variante del mio metodo originale utilizzando un array_walk per ricostruire l'array

$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>';

MODIFICARE

È probabile che la risposta più efficiente ed elegante implichi l'acquisizione di funzioni e metodi da ciascuna delle risposte fornite


1

Vorrei explodei valori basati su / e quindi utilizzarli array_intersect_assocper rilevare gli elementi comuni e assicurarmi che abbiano l'indice corrispondente corretto nell'array. La matrice risultante potrebbe essere ricombinata per produrre il percorso comune.

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

Ciò non è stato testato, ma l'idea è che l' $commonPatharray contenga solo gli elementi del percorso che sono stati contenuti in tutti gli array di percorsi che sono stati confrontati con esso. Quando il ciclo è completo, lo ricombiniamo semplicemente con / per ottenere il vero$commonPath

Aggiornamento Come sottolineato da Felix Kling, array_intersectnon prenderò in considerazione percorsi che hanno elementi comuni ma in ordini diversi ... Per risolvere questo problema, ho usato al array_intersect_assocposto diarray_intersect

Aggiornamento Aggiunto codice per rimuovere anche il percorso comune (o tetris!) Dall'array.


Questo probabilmente non funzionerà. Considera /a/b/c/de /d/c/b/a. Stessi elementi, percorsi diversi.
Felix Kling

@Felix Kling Ho aggiornato per utilizzare array_intersect_assoc che esegue anche un controllo dell'indice
Brendan Bullen

1

Il problema può essere semplificato se solo visto dall'angolo di confronto delle stringhe. Questo è probabilmente più veloce della suddivisione in array:

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

Non funzionerà, ad esempio, con questo array impostato ('/ www / htdocs / 1 / sites / conf / abc / def', '/ www / htdocs / 1 / sites / htdocs / xyz', '/ www / htdocs / 1 / sitesjj / lib2 / abcdedd ',).
Artefacto

@Artefacto: avevi ragione. Quindi l'ho semplicemente modificato per includere sempre una barra finale "/" nel confronto. Lo rende non ambiguo.
mario

1

Forse il porting dell'algoritmo os.path.commonprefix(m)utilizzato da Python potrebbe funzionare?

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]

Cioè, uh ... qualcosa di simile

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

Dopodiché puoi semplicemente substr ogni elemento della lista originale con la lunghezza del prefisso comune come offset iniziale.


1

Lancio il cappello sul ring ...

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

Utilizzo:

$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

Bene, ci sono già alcune soluzioni qui ma, proprio perché è stato divertente:

$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>';

Produzione:

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

Funziona bene ... simile a Mark Baker ma usa str_replace


0

Probabilmente troppo ingenuo e noobish ma funziona. Ho usato questo algoritmo :

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

Produzione:

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 C'è un link a wikipedia nella mia risposta ... prova a leggerlo prima di commentare.
Richard Knop

Penso che alla fine si confrontino solo i primi due percorsi. Nel tuo esempio funziona, ma se rimuovi il primo percorso, troverà /www/htdocs/1/sites/conf/una corrispondenza comune. Inoltre, l'algoritmo cerca sottostringhe che iniziano in un punto qualsiasi della stringa, ma per questa domanda sai che puoi iniziare dalla posizione 0, il che lo rende molto più semplice.
Jan Fabry
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.