Summe von n vielen Summen berechnen
lima-city → Forum → Programmiersprachen → Programmieren mit .NET & Mono
ausgabe
check
code
eingabe
ende
ergebnis
frage
funktion
index
knoten
methode
programm
regel
speichern
string
suchen
summe
system
url
zahl
-
Hallo liebes Forum,
das Summenzeichen Σ ist mit seinen Grenzen ähnlich wie eine for-Schleife in der Informatik. Also wollte ich das Summenzeichen in ein kleines C# Programm implementieren. Dies soll die letztendliche Summe nur ausformulieren, allerdings nicht ausrechnen. Nun soll aber nicht nur eine einfache Summe, sondern auch die Summe einer Summer oder eben beliebig tief die Summe von Summen ausgerechnet werden.
Also z.B. ∑_i^n∑_j^n∑_k^n…
Wobei das Programm das Summenzeichen als sum(i;n;Term) entgegennimmt.
Nun soll das Programm bspw. sum(0;4,i*sum(2;5;i)) als 0*(2+3+4+5)+1*(2+3+4+5)+2*(2+3+4+5)+3*(2+3+4+5)+4*(2+3+4+5) darstellen.
Eine Einfache Summe lässt sich so auch gut darstellen, aber die Summe von einer Summe nicht.
Meine Überlegung war, dass Summenzeichen auflösen in eine Funktion zu packen die sich selbst wiederaufruft, wenn sie im Term ein sum(i;n;Term) findet.
Leider klappt das alles noch nicht so wie es soll. Hat jemand von euch vllt. Anregungen?
Es kommt immer ein Stack overflows beim Durchlauf mit zwei oder mehr Summen, aber ich komme einfach nicht darauf, wo der Fehler liegt
using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Summenzeichen { class Summe { static void Main(string[] args) { Console.WriteLine("Bitte geben Sie die auszuformulierende Summe an: \n\n"); String eingabe = Console.ReadLine(); String ausgabe = ""; string[] param = getParams(eingabe); if (param[0] != "") { ausgabe = summe(param[2], Convert.ToInt32(param[0]), Convert.ToInt32(param[1])); } Console.WriteLine("Ergebnis: " + ausgabe); Console.ReadLine(); } static private String summe(String value, int zaehler, int n) { string ausgabe = ""; value = value.Replace(")", ""); for (int i = zaehler; i <= n; i++) { ausgabe += value.Replace("i", i.ToString()); if(searchSum(ausgabe) != "") { ausgabe += searchSum(ausgabe); } if (i != n) { ausgabe += " + "; } } return ausgabe; } static private String[] getParams(String value) { string[] ausgabe = { "", "", "" }; StringBuilder sb = new StringBuilder(value); string searchForSum = "sum("; int firstCharacterSum = value.IndexOf(searchForSum); if (firstCharacterSum != -1) { sb = sb.Remove(0, firstCharacterSum + 4); ausgabe[0] = sb.ToString().Split(';')[0]; ausgabe[1] = sb.ToString().Split(';')[1]; int secondCharacterSum = sb.ToString().IndexOf(searchForSum); if (secondCharacterSum != -1) { sb = sb.Remove(0, secondCharacterSum); ausgabe[2] = sb.ToString(); } else { ausgabe[2] = sb.ToString().Split(';')[2]; } } return ausgabe; } static private String searchSum(string value) { string[] param = getParams(value); if (param[0] != "") { return summe(param[2], Convert.ToInt32(param[0]), Convert.ToInt32(param[1])); } else { return ""; } } } }
Beitrag zuletzt geändert: 22.9.2016 23:21:54 von computerkurs2011 -
Diskutiere mit und stelle Fragen: Jetzt kostenlos anmelden!
lima-city: Gratis werbefreier Webspace für deine eigene Homepage
-
Stack Overflow lässt vermuten, dass du dir irgendwie eine Endlosschleife eingebaut hast.
-
Was Stack Overflow heißt, dass weiß ich auch, nur ich weiß nicht wie bzw. warum dort eine Endlosschleife entsteht und wie ich es anders lösen soll. Daher meine Frage ans Forum nach Lösungsvorschlägen/-ansätzen.
-
Es sieht nach endloser Rekursion aus. Das entsteht, wenn du keine Abbruchbedingung hast, oder, wenn du in einem Rekursionsschritt keinen Fortschritt erzielst. Bei dir wird das wohl bei summe/searchSum eine falsche Abbruchbedingung sein.
Prüfen kannst du das alles mit assert-Befehlen, indem du entsprechende Bedingungen einfügst. In deinem Fall musst du dir eine Fortschrittsbedingung ausdenken, die eine Aussage darüber trifft, ob du einen derartigen Fortschritt machst, dass du je eine Abbruchbedingung erreichen kannst.
Deinen konkreten Beispielfall würde ich ja so lösen (ist Python-Code, weil ich kein C# kann):def summe(start, end, f): return "+".join([ f(x) for x in range(start, end + 1) ]) result = summe(0, 4, lambda i: "%s*(%s)" % \ (i, summe(2, 5, lambda x: str(x)))) print("result: '%s'" % result)
Das liefert folgendes Ergebnis, völlig ohne der Möglichkeit, jemals eine endlose Rekursion zu erzeugen:result: '0*(2+3+4+5)+1*(2+3+4+5)+2*(2+3+4+5)+3*(2+3+4+5)+4*(2+3+4+5)'
Wenn du wirklich die Eingabe sinnvoll parsen und effizient verarbeiten willst, dann solltest du auch deine Parse-Funktionen vergessen und das anders lösen. Eine Möglichkeit wäre z.B. das Zerlegen der Eingabe in »Tokens«, die entweder ein Symbol wie »*« oder »+«, eine Zahl, ein Trennzeichen wie »;«, eine Klammer auf/zu (also »(« oder »)«) oder ein Name (z.B. »sum«) sind. Dann liest du im nächsten Schritt immer Token für Token, analysiert die Struktur, und erstellst einen Baum (Abstract Syntax Tree, kurz AST). Den kannst du dann mit sehr wenig Aufwand entweder direkt interpretieren (eher langsam), oder eine Art Bytecode erzeugen, die sich schneller ausführen lässt.
Ein einfacher Parser mit AST-Interpreter könnte beispielsweise so aussehen (in Java): Sums.java
Es wäre dabei übrigens einfacher, das Ergebnis direkt zu berechnen, anstatt das als Text auszugeben. -
Da ich das Thema Grammatiken nur sehr kurz in der Schule hatte und das Studium erst in einem Monat los geht, verstehe ich zwar den Ansatz in welche Richtung deine Lösung geht, den Java Code verstehe ich aber nur teilweise auch wenn ich schon Erfahrungen mit Java habe.
Wenn ich das richtig verstehe hast du den Konstruktor von Token dreifach überladen und Token dient als „Behältnis“ für Operatoren oder nummerische Werte?
Der Context erschließt sich mir nicht so wirklich. Was Maps bzw. HashMaps sind weiß ich aber was der Context genau tut außer mit get einen Wert zwischen zu speichern und mit set wieder auszuliefern habe ich noch nicht rausgefunden.
Check() prüft ob es sich um den richtigen TokenType handelt, wenn mich nicht alles täuscht.
Scan() sucht nach Tokens mithilfe von next() ?
Frage: warum haben scan() und check() keinen return, sondern speichern den Wert in sym. Wird dieser nicht immer wieder überschrieben?
Error() erschließt sich von selbst.
Was expr und summand 100% machen erschließt sich mir nicht ganz durch das -1 im case in Zeile 180.
Und wie genau funktionieren deine Nodes?
Sorry für die vielen Nachfragen, aber jetzt interessiert mich dein Code doch sehr. Gerade weil er auch etwas behandelt, dass im Studium doch sehr wahrscheinlich auf mich zukommen wird und ich auch gerne lernen würde wie man Grammatiken bzw. syntaktische und semantische Informationen über Syntaxbäume analysieren kann.
MfG
cpk2011
P.S. Könnte ich die Ausgabe nicht am Ende, ganz böse, in ein eval packen und dann ausrechnen lassen?
public static void main(String[] args) throws Exception { String sums = new Sums().run("sum(i;0;4;i*sum(i;2;5;i*sum(i;7;8;i)))"); System.out.println(sums); ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript"); System.out.println("Ergebnis: "+engine.eval(sums)); }
Beitrag zuletzt geändert: 22.9.2016 22:57:48 von computerkurs2011 -
computerkurs2011 schrieb:
Ja. Ein
Wenn ich das richtig verstehe hast du den Konstruktor von Token dreifach überladen und Token dient als „Behältnis“ für Operatoren oder nummerische Werte?
enthält die nötige Information über genau einen Token. Der überladene Konstruktor ist dazu da, dass man die 3 Arten von Tokens ohne unnötigem Schreibaufwand erstellen kann: ein Token, das nur einen Typ hat (z.B. eine Klammer auf; erster Konstruktor), ein Token mit einem String-Wert (z.B. ein Name; zweiter Konstruktor) und ein Token mit einem Zahlenwert (z.B. eine Zahl; dritter Konstruktor). Dadurch kann man z.B.Token
aber auchToken t = new Token(TokenType.LPAR);
schreiben, ohne immer alle 3 Werte (Typ, String und Zahl) übergeben zu müssen.Token t = new Token(TokenType.IDENT, "sum");
Der Context erschließt sich mir nicht so wirklich. Was Maps bzw. HashMaps sind weiß ich aber was der Context genau tut außer mit get einen Wert zwischen zu speichern und mit set wieder auszuliefern habe ich noch nicht rausgefunden.
Die (Hash)Map speichert immer zu einem Schlüssel einen Wert. Jetzt kannst du aber verschachtelt mehrere Summenfunktionen aufrufen, und dabei die Index-Variable in jeder Ebene überschreiben. Der
sucht deshalb immer zuerst in seinem eigenen Scope, und wenn er dort nichts findet, dann sucht er im übergeordneten Scope. Das ist nötig, damit z.B. das funktioniert:Context
Dassum(i; 0; 1; i * sum(i; 0; 1; i))
hat dabei vor der zweiteni
den Wert des Indexes der ersten Summe, während es in der zweitensum
den Wert des Indexes der zweiten Summe hat.sum
Check() prüft ob es sich um den richtigen TokenType handelt, wenn mich nicht alles täuscht.
Korrekt, und zusätzlich ruft es
auf, um zum nächsten Token zu springen.scan()
Scan() sucht nach Tokens mithilfe von next() ?
Mit
wird ein weiteres Token gelesen, und das zuletzt gelesene in der Variablenscan()
, welche immer das zuletzt gelesene Token enthält, gespeichert.t
Frage: warum haben scan() und check() keinen return, sondern speichern den Wert in sym. Wird dieser nicht immer wieder überschrieben?
Falls du genau schaust siehst du, dass
ein Return in jedem Zweig hat.next()
hingegen hat kein Return, da es nur die Variablen der Klasse aktualisiert. Ein Return ist deshalb hier nicht nötig (was würdest du da denn überhaupt zurückgeben wollen?).scan()
wird in dieser Methode zwar jedes Mal überschrieben, aber da es eine Variable der Klasse ist, haben auch alle anderen Methoden darauf Zugriff. Auch hier: wenn du genau schaust, siehst du, dass andere Methoden den Wert vonsym
nutzen. Theoretisch könnte man sich den auch sparen, und jedes Malsym
schreiben.la.type
Was expr und summand 100% machen erschließt sich mir nicht ganz durch das -1 im case in Zeile 180.
Um das besser zu verstehen, solltest du dir wohl die EBNF deiner Syntax denken, dann wird das schnell klar:
Jede Funktion mit einem Namen einer Regel aus der EBNF parst genau diesen Teil der EBNF.EXPR := SUMMAND { ( "+" | "-" ) SUMMAND } . SUMMAND := FACTOR { ( "*" | "/" FACTOR } . FACTOR := CALL | number | "(" EXPR ")" . CALL := ident [ "(" ident ";" number ";" number ";" EXPR ")" ] .
parst also einen gesamten Ausdruck, währendexpr()
einen Summanden parst.summand()
Eine Besonderheit gibt es in der Regel
: da sowohl ein Funktionsaufruf, als auch eine Variable jeweils mit einem Namen beginnt, ist nicht klar, um was es sich handelt. Erst anhand des nachfolgenden (eventuell fehlenden)CALL
kann dies entschieden werden. Es kann dadurch wieder ausschließlich anhand des nächsten Zeichens festgestellt werden, welche Regel benutzt werden muss. Da das für jede Regel gilt, handelt es sich hier um einen LL(1)-Parser, falls du das je irgendwo hören/lesen solltest (LL wegen der Art des Parsers, in dem Fall rekursiver Abstieg, und 1, weil anhand eines Vorgriffssymbols entschieden werden kann, welche Regel benutzt werden muss).(
Durch diese Grammatik ist übrigens auch sichergestellt, dass Vorrangregeln (z.B. * vor +) korrekt befolgt werden.
Der Fall mit
im-1
sorgt lediglich dafür, dass am Ende der Eingabe einnext()
Token erstellt wird, sodass ein unerwartetes Ende immer zu einem Syntaxfehler führen wird.EOF
Außerdem prüft die Funktion
ob am Ende auch wirklich einparse()
erreicht wurde. Damit wird sichergestellt, dass nach einem Ausdruck nicht noch weitere Tokens übrig geblieben sind.EOF
Und wie genau funktionieren deine Nodes?
Stell dir einen
als einen Knoten in einem Baum vor, und stell dir vor, dass jeder solche Knoten einem Teil deiner Eingabe entspricht. Wenn du beispielsweiseNode
eingibst, so wirst du einen Knoten mit1 + 2
bekommen (abgebildet durch einen+
), welcher 2 Kindknoten (je eine Zahl, also einBinOpNode
) hat. Wenn du jetzt den Knoten mit der Addition nach dem Ergebnis der Berechnung fragst (mitNumberNode
), so wird er zuerst die beiden Kindknoten nach deren Ergebnis fragen (der linke Kindknoten wird 1, der rechte wird 2 zurückgeben), anschließend die Berechnung (in dem Fall die Addition) durchführen, und das schließlich Ergebnis zurückliefern.execute()
P.S. Könnte ich die Ausgabe nicht am Ende, ganz böse, in ein eval packen und dann ausrechnen lassen?
Das könnte man, wäre aber äußerst ineffizient. Viel einfacher wäre es, in den Nodes nicht einen String zu bestimmen, sondern direkt das Ergebnis zu berechnen (jeweils in der
public static void main(String[] args) throws Exception { String sums = new Sums().run("sum(i;0;4;i*sum(i;2;5;i*sum(i;7;8;i)))"); System.out.println(sums); ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript"); System.out.println("Ergebnis: "+engine.eval(sums)); }
-Methode, und als Rückgabetyp natürlichexecute()
o.ä. stattint
).String
-
Diskutiere mit und stelle Fragen: Jetzt kostenlos anmelden!
lima-city: Gratis werbefreier Webspace für deine eigene Homepage