Bir diziyi araştırmak


99

Şu diziyi düşünün:

/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

ortak temel yolu tespit etmenin en kısa ve en zarif yolu nedir - bu durumda

/www/htdocs/1/sites/

ve dizideki tüm öğelerden kaldırılsın mı?

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

4
Bu denemeye değer olabilir: en.wikibooks.org/wiki/Algorithm_implementation/Strings/… (Denedim ve işe yarıyor).
Richard Knop

1
Awwww! Çok fazla parlak girdi. Elimdeki problemimi çözmek için bir tane alacağım, ancak gerçekten haklı kabul edilmiş bir cevap seçmek için çözümleri karşılaştırmam gerektiğini hissediyorum. Bunu yapmam biraz zaman alabilir, ama kesinlikle yapacağım.
Pekka

eğlenceli başlık: D btw: sizi neden aday moderatörler listesinde bulamıyorum? @Pekka
Surrican

2
iki yıldır kabul edilen cevap yok mu?
Gordon

1
@Pekka Kabul edilen bir cevabı olmadığından üç yıla yaklaşıyor :( Ve o kadar harika bir başlık ki bir dakika önce hatırladım ve Google'da "bir dizi tetrising" yazdım.
Camilo Martin

Yanıtlar:


35

longest_common_prefixGiriş olarak iki dizeyi alan bir işlev yazın . Daha sonra bunları ortak öneklerine indirgemek için dizelere herhangi bir sırayla uygulayın. İlişkisel ve değişmeli olduğu için, sıralamanın sonuç için önemi yoktur.

Bu, örneğin toplama veya en büyük ortak bölen gibi diğer ikili işlemlerle aynıdır.


8
+1. İlk 2 dizeyi karşılaştırdıktan sonra, 3. dizeyle karşılaştırmak için sonucu (ortak yol) kullanın vb.
Milan Babuškov

23

Bunları bir üçlü veri yapısına yükleyin. Ana düğümden başlayarak, hangisinin çocuk sahibi olduğunun birden fazla sayıldığını görün. Bu sihirli düğümü bulduğunuzda, sadece ana düğüm yapısını sökün ve geçerli düğümü kök olarak alın.


10
Verileri, tanımladığınız üçlü ağaç yapısına yükleyen işlem, en uzun ortak öneki bulmaya yönelik algoritmayı içermez ve böylece bir ağaç yapısının kullanılmasını gerçekten gereksiz kılmaz mı? Yani, ağacı oluştururken bunu tespit edebildiğiniz halde, neden birden fazla çocuk için ağacı kontrol edin. O zaman neden bir ağaç? Demek istediğim, zaten bir diziyle başlarsan. Depolamayı diziler yerine sadece bir trie kullanarak değiştirebiliyorsanız, sanırım bu mantıklı.
Ben Schwehn

2
Bence eğer dikkatli olursanız, benim çözümüm bir üçlü inşa etmekten daha verimli.
starblue

Bu cevap yanlış. Benim ve diğer cevaplarda O (n) olan önemsiz çözümler var.
Ari Ronen

@ el.pescado: Denemeler, en kötü durumda kaynak dizginin uzunluğu ile dörtlü boyuttadır.
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;
}

Bu açık arayla yayınlanan en iyi çözümdü, ancak iyileştirilmesi gerekiyordu. Önceki en uzun ortak yolu hesaba katmadı (muhtemelen dizenin gerekenden daha fazlasını yineleyerek) ve yolları hesaba katmadı (bu yüzden /usr/libve en uzun ortak yol yerine en uzun ortak yolu /usr/lib2verdi ). Ben (umarım) ikisini de düzelttim. /usr/lib/usr/
Gabe

7

Peki, XORbu durumda dizenin ortak kısımlarını bulmak için kullanabileceğinizi düşünürsek . Ne zaman x veya aynı olan iki bayt, çıktı olarak bir boş bayt elde edersiniz. Böylece bunu kendi avantajımıza kullanabiliriz:

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

Bu tek döngüden sonra $lengthdeğişken, dizeler dizisi arasındaki en uzun ortak temel parçaya eşit olacaktır. Ardından, ortak parçayı ilk elemandan çıkarabiliriz:

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

İşte buyur. İşlev olarak:

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

Birden fazla yineleme kullandığını, ancak bu yinelemelerin kitaplıklarda yapıldığını, bu nedenle yorumlanmış dillerde bunun büyük bir verimlilik kazancı olacağını unutmayın ...

Şimdi, sadece tam yollar istiyorsanız, son /karaktere kadar kısaltmamız gerekiyor . Yani:

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

Şimdi, gibi iki ipi aşırı derecede kesebilir /foo/barve /foo/bar/bazkesilebilir /foo. Bir sonraki karakteri ya Ama eğer başka bir yineleme yuvarlak ekleyerek kısa belirlemek / veya sonu dizesi, bunu etrafında bir yol göremiyorum ...


3

Saf bir yaklaşım, dizideki yolları patlatmak /ve dizilerdeki her öğeyi ardışık olarak karşılaştırmak olacaktır. Yani örneğin ilk eleman tüm dizilerde boş olacak, bu yüzden kaldırılacak, sonraki eleman olacak www, tüm dizilerde aynı olacak, dolayısıyla kaldırılacak, vb.

Gibi bir şey (denenmemiş)

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

Daha sonra öğeleri $exploded_pathstekrar içeri sokmanız yeterlidir :

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

Bu bana şunu veriyor:

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

Bu iyi ölçeklenmeyebilir;)


3

Tamam, bunun kurşun geçirmez olduğundan emin değilim, ama işe yaradığını düşünüyorum:

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

Bu, dizideki ilk değeri referans dizesi olarak alacaktır. Daha sonra, referans dizge üzerinde yineleme yapacak ve her bir karakteri aynı konumdaki ikinci dizenin karakteriyle karşılaştıracaktır. Bir karakter eşleşmezse, referans dizesi karakterin konumuna kısaltılır ve sonraki dizge karşılaştırılır. İşlev o zaman en kısa eşleşen dizeyi döndürecektir.

Performans, verilen dizelere bağlıdır. Referans dizesi ne kadar erken kısalırsa, kod o kadar çabuk biter. Yine de bunu bir formüle nasıl ekleyeceğime dair hiçbir fikrim yok.

Artefacto'nun dizeleri sıralama yaklaşımının performansı artırdığını buldum. Ekleme

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

daha önce array_reduceperformansı önemli ölçüde artıracaktır.

Ayrıca , bunun daha çok yönlü olan ancak size ortak yolu vermeyen en uzun eşleşen ilk alt dizeyi döndüreceğini unutmayın . Koşmalısın

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

sonuçta. Ve sonra sonucu, değerleri kaldırmak için kullanabilirsiniz

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

hangisi vermeli:

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

Geri bildirim hoş geldiniz.


3

Her karakteri yalnızca bir kez okuyarak öneki en hızlı şekilde kaldırabilirsiniz:

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

Gerçekten de, karakter tabanlı bir karşılaştırma en hızlısı olacaktır. Diğer tüm çözümler, sonunda (çoklu) karakter karşılaştırmaları yapacak olan "pahalı" operatörler kullanır. Kutsal Joel'in yazılarında bile bahsedildi !
Jan Fabry

2

Bu, doğrusal zaman karmaşıklığına sahip olmama avantajına sahiptir; ancak çoğu durumda sıralama kesinlikle daha fazla zaman alan operasyon olmayacaktır.

Temel olarak, zekice olan kısım (en azından bunda bir hata bulamadım) buradaki, sıraladıktan sonra yalnızca ilk yolu son ile karşılaştırmanız gerekecek olmasıdır.

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

Diziyi yeniden oluşturmak için bir array_walk kullanarak orijinal yöntemimin Varyantını DÜZENLE

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

DÜZENLE

En etkili ve zarif cevap, muhtemelen verilen cevapların her birinden işlevler ve yöntemler almayı içerir.


1

Ben ediyorum explodedeğerler temelinde / ve sonra kullanmak array_intersect_assocortak öğeleri tespit etmek ve onlar dizideki doğru gelen endeksine sahip olmak için. Ortaya çıkan dizi, ortak yolu üretmek için yeniden birleştirilebilir.

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

Bu test edilmemiştir, ancak fikir, $commonPathdizinin yalnızca kendisiyle karşılaştırılan tüm yol dizilerinde bulunan yolun öğelerini içermesidir. Döngü tamamlandığında, doğru olanı elde etmek için onu / ile yeniden birleştiririz.$commonPath

Güncelleme Felix Kling'in belirttiği gibi array_intersect, ortak unsurları olan yolları farklı sıralarda dikkate almıyoruz ... Bunu çözmek için array_intersect_assocyerine kullandımarray_intersect

Güncelleme Diziden ortak yolu (veya tetris it!) Kaldırmak için kod eklendi.


Bu muhtemelen işe yaramayacak. Düşünün /a/b/c/dve /d/c/b/a. Aynı unsurlar, farklı yollar.
Felix Kling

@Felix Kling Aynı zamanda bir dizin kontrolü de gerçekleştiren array_intersect_assoc'u kullanmak için güncelledim
Brendan Bullen

1

Sadece dizi karşılaştırma açısından bakıldığında sorun basitleştirilebilir. Bu muhtemelen dizi bölmeden daha hızlıdır:

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

Bu, örneğin bu dizi dizisiyle çalışmaz ('/ www / htdocs / 1 / sites / conf / abc / def', '/ www / htdocs / 1 / sites / htdocs / xyz', '/ www / htdocs / 1 / sitesjj / lib2 / abcdedd ',).
Artefacto

@Artefacto: Haklıydın. Bu yüzden, karşılaştırmada her zaman bir eğik çizgi "/" içerecek şekilde değiştirdim. Belirsiz hale getirir.
mario

1

Belki de Python'un os.path.commonprefix(m)kullandığı algoritmayı taşımak işe yarar ?

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]

Bu, uh ... gibi bir şey

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

Bundan sonra, orijinal listenin her bir elemanını, başlangıç ​​ofseti olarak ortak önekin uzunluğuyla alt-üst edebilirsiniz.


1

Şapkamı ringe atacağım ...

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

Kullanım:

$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

Pekala, burada zaten bazı çözümler var ama sırf eğlenceli olduğu için:

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

Çıktı:

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

Bu iyi çalışıyor ... mark baker'a benzer ancak str_replace kullanıyor


0

Muhtemelen çok saf ve noobish ama işe yarıyor. Bu algoritmayı kullandım :

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

Çıktı:

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 Cevabımda wikipedia bağlantısı var ... Yorum yapmadan önce okumaya çalışın.
Richard Knop

Bence sonunda sadece ilk iki yolu karşılaştıracaksınız. Örneğinizde bu işe yarar, ancak ilk yolu kaldırırsanız, /www/htdocs/1/sites/conf/ortak bir eşleşme olarak bulacaktır . Ayrıca, algoritma dizede herhangi bir yerden başlayan alt dizeleri arar, ancak bu soru için 0 konumundan başlayabileceğinizi bilirsiniz, bu da onu çok daha basit hale getirir.
Jan Fabry
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.