Horn: Fehler in Rekursion

Hallo,

die folgende Funktion soll aus einer Zeichenkette mit einem vorgeschriebenem Format ein Array machen mit 26 Zahlen die
für alle Buchstaben stehen also Array[0] für A und der Inhalt
beschreibt wie oft A in der Zeichenkette $sig2 vorkommt.
$letter und $letter2 geben die Buchstabengrenzen an zwischen denen
nach der Anzahl in $sig2 gesucht wird.

Das Format von $sig2 ist denkbar einfach, die Buchstaben sind der
Reihenfolge nach sortiert. BAC wäre also nicht möglich AHZ schon.
Soll ein Buchstabe öfter vorkommen so folgt ihm die Anzahl z.B.
A8H2IQ A kommt 8 mal vor H 2 mal und I und Q je 1 mal.

Nehmen wir nun diesen String als Ausgang so übergeben wir:
build_ltrarray(0,1,"A8H2IQ");
0 steht für A und 1 für B also beginnt die Suche nach den Buchstaben bei
A und der nächste Buchstabe der in $sig2 vorkommt ist B.

Die Funktion arbeitet zunächst ohne Probleme bis wir an das Ende des Strings
gelangen.
Denn der letzte Aufruf dürfte in etwa so aussehen:
build_ltrarray(ord("Q"),ord("R")+1,"Q");

Nur folgende Zeile if ($pos_ltr_now === false) ergibt false = false, weil
$pos_ltr_now für die Position von Q in "Q" steht welche 0 ist.
Dies wird von PHP so interpretiert als ob 0 Boolean wäre und somit false.
Also wird der nächste Buchstabe gebildet also geht es weiter im Ascii-Code R,S,T,[...] und das ganze hängt sich auf.

Die Funktion ist evtl. etwas kompliziert wenn jemand eine Lösung kennt oder schon einmal Vereinfachungen kennt immer raus damit ;).

  
function build_ltrarray($letter,$letter2,$sig2)  
//Zerlegt eine Signatur2 in ein Buchstaben Array  
	{  
	//Falls nicht vorhanden Array kreiren  
	if (!isset($barray))  
		{  
		$barray = array();  
		for ($i = 0; $i < 26;$i++)  
			array_push($barray,0);  
		}  
	$pos_ltr_now = strpos($sig2,$letter+65);  
	$pos_ltr_nxt = strpos($sig2,$letter2+65);  
  
	if (strlen($sig2) > 1 or chr($letter+65) <> $sig2)  
{  
	//Wenn aktueller Buchstabe nicht in Signatur rekursiver neu Aufruf  
	if ($pos_ltr_now === false)  
		$this->build_ltrarray($letter+1,$letter2,$sig2);  
	//Ebenso für nächsten Buchstaben suchen  
	if ($pos_ltr_nxt === false or $pos_ltr_now == $pos_ltr_nxt)  
		$this->build_ltrarray($letter,$letter2+1,$sig2);  
	//Anzahl Buchstabe in Array einfügen  
	switch ($pos_ltr_nxt-$pos_ltr_now)  
		{  
		case 1:  
		//Buchstabe nur einmal vorhanden  
		$barray[$letter] = $barray[$letter]+1;  
		break;  
		default:  
		//Buchstabe x mal vorhanden  
		$barray[$letter] = $barray[$letter]+substr($sig2,$pos_ltr_now+1,$pos_ltr_nxt-1);  
		break;  
		}  
	//$sig2 kürzen  
	$sig2 = substr($sig2,$pos_ltr_nxt,strlen($sig2)-$pos_ltr_nxt);  
  
	if (strlen($sig2) >= 1)  
		$this->build_ltrarray($letter2,$letter2+1,$sig2);  
}  
	}  

  1.   
    
    > function build_ltrarray($letter,$letter2,$sig2)  
    > //Zerlegt eine Signatur2 in ein Buchstaben Array  
    > 	{  
    > 	//Falls nicht vorhanden Array kreiren  
    > 	if (!isset($barray))  
    
    

    Auf den ersten Blick sieht es so als wäre $barray nicht GLOBAL.
    Ansonsten üb doch erstmal Rekursionen mit einem einfacheren
    Problem.

  2. Hallo Hendrik,

    Du hast Dir viel Mühe gegeben und eine entsprechend ausführliche Antwort verdient.

    Nehmen wir nun diesen String als Ausgang so übergeben wir:
    build_ltrarray(0,1,"A8H2IQ");

    Denn der letzte Aufruf dürfte in etwa so aussehen:
    build_ltrarray(ord("Q"),ord("R")+1,"Q");

    Nur folgende Zeile if ($pos_ltr_now === false) ergibt false = false, weil
    $pos_ltr_now für die Position von Q in "Q" steht welche 0 ist.

    Dies wird von PHP so interpretiert als ob 0 Boolean wäre und somit false.

    nein. Deswegen

      
    
    >     $pos_ltr_nxt  = [link:http://de2.php.net/manual/de/function.strpos.php@title=strpos]($sig2,$letter2+65);  
    
          //...  
    
    >     if ($pos_ltr_nxt === false or $pos_ltr_now == $pos_ltr_nxt)  
    > 		$this->build_ltrarray($letter,$letter2+1,$sig2);  
    
    

    verwendest Du ja den strikten Vergleich mit ===.

    0 === false => false. Der Grund für den Nichtabbruch liegt irgendwo anders, allerdings hast Du Dir noch viele weitere Fallstricke eingebaut, an denen Deine Methode scheiterte (den schlechten Rat der Sackkarre mit eingeschlossen, aber dazu später mehr).

    Nehmen wir uns den nächsten Abschnitt vor:

        // Aufruf mit build_ltrarray(0, 8, "A8H2IQ")  
      
        $sig2 = "A8H2IQ";  
        $letter = 0;  
        $pos_ltr_now = 0;  
        $letter2 = 8;        // entspricht I;  
        $pos_ltr_nxt = 4;  
        $bletter[$letter] = 0;  
      
        // Kontrollausgabe  
        echo $sig2, "<br>\n";  
      
        //Anzahl Buchstabe in Array einfügen  
        switch ($pos_ltr_nxt-$pos_ltr_now)  
            {  
            case 1:  
            //Buchstabe nur einmal vorhanden  
            $barray[$letter] = $barray[$letter]+1;  
            break;  
            default:  
            //Buchstabe x mal vorhanden  
            $barray[$letter] = $barray[$letter]+substr($sig2,$pos_ltr_now+1,$pos_ltr_nxt-1);  
      
            // Kontrollausgaben  
            echo substr($sig2, $pos_ltr_now + 1, $pos_ltr_nxt - 1), "<br>\n";  
            echo $barray[$letter], "<br>\n";  
            break;  
      
            }  
        //$sig2 kürzen  
        $sig2 = substr($sig2,$pos_ltr_nxt,strlen($sig2)-$pos_ltr_nxt);  
      
        // Kontrollausgabe  
        echo $sig2;  
    
    

    Die Ausgabe sieht wie folgt aus:

    A8H2IQ
    8H2
    8
    IQ

    Oha: die Anzahl von A stimmt, aber H wird komplett unterschlagen :-( Nicht gut.

    Aber noch lustiger wird es mit einer minimal veränderten Signatur:

    $sig2 = "A8E2IQ";

    Die Ausgabe lautet nun:

    A8E2IQ
    8E2
    800
    IQ

    Jetzt stimmt noch nicht einmal die Anzahl von A, weil 8E2 zu 800 ausgewertet wird :-)

    Spätestens jetzt solltest Du merken, dass die Logik Deiner Funktion zu wünschen übrig lässt.

    Jetzt komme ich zu dem Problem, das die Sackkarre bereits angesprochen hat:
    Selbst wenn Du Deine Rekursion sauber beendest, lässt Du PHP fleißig arbeiten, aber ohne zählbares Ergebnis. Das ist ein Problem der Gültigkeit und Lebensdauer von Variablen. Ich will es Dir an einer einfachen rekursiven Funktion aufzeigen. Eine Anmerkung noch: Da Du $this verwendest, gehe ich davon aus, dass es sich um eine Methode einer Klasse handelt:

    class demo {  
        function beispiel_scalar($tiefe) {  
            // Ist die Variable noch nicht gesetzt, so weise ihr den Wert 3 zu  
            if (!isset($wert)) {  
                $wert = 3;  
            }  
    	else {  
                // sonst erhöhe sie um 1  
                // $wert += 1  
                // ist übersichtlicher als  
                // $wert = $wert + 1  
                $wert += 1;  
            }  
            // Gib die erreichte Tiefe und den aktuellen Wert aus  
    	echo "Tiefe: ", $tiefe, " - Wert: ", $wert, "<br>\n";  
      
            // Falls wir noch nicht ganz unten angelangt sind (in Tiefe 3)  
            if ($tiefe < 3) {  
                // gehen wir eine Etage tiefer  
                $this->beispiel_scalar($tiefe + 1);  
            }  
        }  
    }  
      
    $a = new demo();  
    // Wir gehen vom Erdgeschoss (Tiefe 0) nach unten  
    $a->beispiel_scalar(0);  
    
    

    // Ausgabe:
    Tiefe: 0 - Wert: 3
    Tiefe: 1 - Wert: 3
    Tiefe: 2 - Wert: 3
    Tiefe: 3 - Wert: 3

    Da ich hier wie Du auch nur lokale Variablen verwendet habe, ist bei jedem neuen Aufruf von beispiel_scalar() die Variable $wert zu Beginn neu erzeugt, sie ist nicht gesetzt und es wird jedesmal der if-Zweig abgearbeitet.

    Eine Möglichkeit, solche Werte über die Ausführung der Funktion zu erhalten, ist es, diese als Eigenschaft der Klasse zu deklarieren und anschließend zu nutzen:

    class demo2 {  
      
        $wert = NULL;  
        // isset(null) => false  
      
        function beispiel_scalar($tiefe) {  
            // Ist die Variable noch nicht gesetzt, so weise ihr den Wert 3 zu  
            // Auf eine Objekteigenschaft greifen wir mit $this zu:  
            if (!isset($this->wert)) {  
                $this->wert = 3;  
            }  
    	else {  
                // sonst erhöhe sie um 1  
                $this->wert += 1;  
            }  
            // Gib die erreichte Tiefe und den aktuellen Wert aus  
    	echo "Tiefe: ", $tiefe, " - Wert: ", $this->wert, "<br>\n";  
      
            // Falls wir noch nicht ganz unten angelangt sind (in Tiefe 3)  
            if ($tiefe < 3) {  
                // gehen wir eine Etage tiefer  
                $this->beispiel_scalar($tiefe + 1);  
            }  
        }  
    }  
      
    $a = new demo2();  
    // Wir gehen vom Erdgeschoss (Tiefe 0) nach unten  
    $a->beispiel_scalar(0);  
    
    

    Ausgabe:
    Tiefe: 0 - Wert: 3
    Tiefe: 1 - Wert: 4
    Tiefe: 2 - Wert: 5
    Tiefe: 3 - Wert: 6

    Beim ersten Aufruf ergibt isset($this->wert) false, die Variable wird gesetzt, in den weiteren Aufrufen wird der else-Zweig abgearbeitet, weil die Eigenschaft jetzt gesetzt ist.

    Aber Vorsicht: Ein weiterer Aurfuf der Methode läßt weiterzählen:
    Kopiere einfach die Zeile

    $a->beispiel_scalar(0);  
    
    

    so dass die Methode zweimal aufgerufen wird, lasse das Skript lauten und die Ausgabe lautet:

    Tiefe: 0 - Wert: 3
    Tiefe: 1 - Wert: 4
    Tiefe: 2 - Wert: 5
    Tiefe: 3 - Wert: 6
    Tiefe: 0 - Wert: 7
    Tiefe: 1 - Wert: 8
    Tiefe: 2 - Wert: 9
    Tiefe: 3 - Wert: 10

    Vergleichbar gilt, dass bei einem zweiten Aufruf Deiner build_ltrarray-Methode die neuen Werte zu den alten dazuaddiert werden. Wahrscheinlich wird es sinnvoller sein, wenn Deine Methode das gewünschte Array zurückgibt.

    Falls Du meine Ausführungen verstanden hast, könnten wir an einen neuen Ansatz für Deine Funktion gehen. Falls Du meine Ausführungen nicht ganz verstanden hast oder nicht nachvollziehen kannst, erläutere bitte, wo Du Verständnisschwierigkeiten hast oder hängengeblieben bist, damit wir es Dir besser erklären können.

    Freundliche Grüße

    Vinzenz

    1. Falls Du meine Ausführungen verstanden hast, könnten wir an einen neuen Ansatz für Deine Funktion gehen. Falls Du meine Ausführungen nicht ganz verstanden hast oder nicht nachvollziehen kannst, erläutere bitte, wo Du Verständnisschwierigkeiten hast oder hängengeblieben bist, damit wir es Dir besser erklären können.

      Freundliche Grüße

      Vinzenz

      Ich hoffe mal, dass ich es richtig verstanden habe. Zunächst habe ich mal das $barray zu einem $this->barray gemacht.
      Und unter der Funktion ein return $this->barray; eingefügt.
      Und das $this->barray wird an einer besser passenden Stelle mit Nullen gefüllt.

      Trotzdem hängt sich die Rekursion weiterhin auf, was habe ich übersehen?!

      1. Hallo,

        Falls Du meine Ausführungen verstanden hast, könnten wir an einen neuen Ansatz für Deine Funktion gehen. Falls Du meine Ausführungen nicht ganz verstanden hast oder nicht nachvollziehen kannst, erläutere bitte, wo Du Verständnisschwierigkeiten hast oder hängengeblieben bist, damit wir es Dir besser erklären können.

        Ich hoffe mal, dass ich es richtig verstanden habe. Zunächst habe ich mal das $barray zu einem $this->barray gemacht.
        Und unter der Funktion ein return $this->barray; eingefügt.

        Wenn Du das Array als Eigenschaft anlegst, erübrigt sich prinzipiell gesehen die Rückgabe. Wie in meinem Beispiel demonstriert, darfst Du die Funktion nur dann zweimal aufrufen, wenn Du das Array vor dem zweiten Aufruf wieder mit Nullen füllst (sprich: neu initialisierst).

        Trotzdem hängt sich die Rekursion weiterhin auf, was habe ich übersehen?!

        eine ganze Menge:
        Wie von mir bereits gezeigt, könnte Deine Funktion ohnehin nur dann ein sinnvolles Ergebnis liefern, wenn letter2 genau um 1 größer ist als letter.

        Außerdem musst Du bedenken, dass für die Variablen letter, letter1, sig2 das Gleiche gilt. Wenn die Funktion aus einem rekursiven Aufruf, innerhalb dessen z.B. die Signatur gekürzt wurde, wieder zurückkehrt und weitermacht, dann hat sig2 wieder den gleichen Wert wie vor dem rekursiven Aufruf, ist also wieder länger.

        Grundsätzlich halte ich hier iteratives Vorgehen für viel einfacher und intuitiver als die Rekursion. Geh' einfach Zeichen für Zeichen durch:

        Lies abwechselnd die Buchstaben und ihre Anzahl ein.
        Dabei gilt
            Folgt auf einen Buchstaben ein weiterer Buchstabe oder das Signaturende
                so ist die Anzahl dieses Buchstaben 1
            Sonst
                lies solange Zeichen ein, wie Ziffern aufeinander folgen
                Der in eine Integer umgewandelte Wert dieser Ziffernfolge ist
                die Anzahl
            Ende wenn

        Freundliche Grüße

        Vinzenz