| Code Kitchen Allgemeines Coder-Forum rund um das Programmieren eigenständiger, ausführbarer Programme. |
Diskussion: Algebraische Struktur in Java im Forum Code Kitchen, in der Kategorie Software Home; Anzeige Hallo HaBo! Nachdem ich ein paar komplexere Aufgaben in Java bewältigen durfte, wollte ich nicht alles im Datenmüll versumpfen ...
![]() |
| | #1 (permalink) |
| Anzeige Hallo HaBo! Nachdem ich ein paar komplexere Aufgaben in Java bewältigen durfte, wollte ich nicht alles im Datenmüll versumpfen lassen und stelle es deswegen hier online. Es steht hier noch die "Anleitung" unter der die Klassen zu definieren war. Es werden hier alle möglichen Themenbereiche angeschnitten, wer also ein paar Snippets braucht, kann sich gerne bedienen. Es liegt neben dieser algebraischen Struktur noch ein Terminkalender (GUI-Entwickling mit Swing, Filtern von Terminen, Ablegen als Daten, usw.) und ein Reimwörterbuch (Konstruktion eines Baums, Umgang mit Zeichenketten, usw.) vor, falls Bedarf besteht, kann ich diese auch bereitstellen. In meinem Code sind die Kommentare recht klein gehalten, da durch diese Anleitung vieles erklärt wird. Algebraische Struktur Zunächst einmal: Keine Panik! Die Aufgabe klingt mathematisch und hat viel Aufgabentext, Sie werden aber zum einen feststellen, dass Sie kein abgeschlossenes Mathematikstudium brauchen um die Aufgabe zu lösen und zum anderen hält sich der Codieraufwand in übersichtlichen Grenzen. Soviel zur Aufmunterung, jetzt geht die Aufgabe richtig los. Eine Algebra (A, f1,f2,...,fn) ist, ganz allgemein gesprochen, eine Menge von Objekten A, das sogenannte Universum zusammen mit einer Menge von Funktionen {f1,...,fn}, die auf den Elementen des Universums arbeiten. Das Universum heißt auch Domäne der Funktionen. (Ganz allgemein gesprochen können Algebren zusätzlich auch noch eine Menge von Relationen enthalten, das lassen wir hier aber beiseite.) Beispiele für gängige Algebren, mit denen man normalerweise zu tun hat sind z.B: Die Natürlichen Zahlen zusammen mit der Additition und der Multiplikation (auf den natürlichen Zahlen). Die Menge der komplexen Zahlen zusammen mit Addition und Multiplikation (hier sind das aber Operationen auf komplexen Zahlen). Die Menge der ganzzahligen Matrizen zusammen mit der Matrixmultiplikation. Die Menge {0,1} zusammen mit den booleschen Funktionen AND und OR. und viele, viele mehr. Alle oben genannten Algebren haben nur zweistellige Funktionen. Solche Funktionen nennt man Operationen auf dem jeweiligen Universum. Außerdem sind alle oben genannten Algebren sehr speziell insofern, als sie mehr oder weniger schöne Eigenschaften haben. Diese speziellen Algebren sind für uns von besonderem Interesse. Das Ziel dieser Aufgabe ist die Entwicklung eines allgemeinen kleinen Algebrasystems, das wir dazu verwenden können um komplexere mathematische Objekte zu definieren (in unserem Fall werden das Polynome sein) und mit ihnen umgehen zu können, ohne dass dann dabei noch auf die zugrundeliegende Algebra Rücksicht genommen werden muss. Beispiel: Polynome kann man auf unterschiedlichen Ringen definieren. Z.B. gibt es die Menge der Polynome auf den ganzen Zahlen, die Menge der Polynome auf den rationalen Zahlen, oder auch Polynome auf endlichen Körpern, wie etwa dem GF2, also dem Körper mit nur 2 Elementen. Allen diesen Mengen von Polynomen ist gemeinsam, dass sie zusammen mit der Polynomaddition und der Polynommultiplikation wieder einen Ring bilden. Die Polynomaddition und die Polynommultiplikation leitet sich dabei direkt aus Operationen der den Polynomen zugrundeliegenden Algebra ab. Und zwar immer auf die gleiche Weise. Das heißt, es ist möglich die Polynomoperationen ganz abstrakt und unabhängig von der zugrundeliegenden Algebra zu definieren. Es ist also z.B. nicht notwendig die Polynomaddition für Polynome über den rationalen Zahlen zu definieren und dann auch noch die Polynomaddition für Polynome über ganzen Zahlen. Beides funktioniert gleich, es muss nur die zugrunde liegende Algebra ausgetauscht werden. Die Klasse Operation Erstellen Sie ein package jpp.algebra und in diesem package die abstrakte Klasse Operation.java. Auch alle weiteren Klassen erstellen Sie bitte in diesem package, auch wenn es dann später nicht mehr explizit gefordert wird. Code: public abstract class Operation<DomainElement> Jeder Operation hat einen Namen, der im Attribut Code: protected String name; Implementieren Sie bitte einen Konstruktor Code: public Operation(String operationName) Die folgenden Methoden sind in der Klasse Operation nur abstrakt vorhanden. In konkreten Operationen sind sie dann auch konkret zu implementieren Code: public abstract DomainElement compute(DomainElement e1, DomainElement e2); Code: public abstract boolean isAssociative(); public abstract boolean isCommutative(); Eine Operation * ist kommutativ, falls für alle Elemente X, Y der Domäne gilt: X*Y = Y*X. Diese Methode gibt genau dann true zurück, wenn die Operation kommutativ ist. Code: public abstract boolean hasNeutralElement(); Code: public abstract boolean isNeutralElement(DomainElement e); Code: public abstract DomainElement getNeutralElement(); Code: public abstract boolean elementsInvertible(); Code: public abstract DomainElement getInverseElement(DomainElement e); Code: public abstract boolean knownToDistributeOver(Operation<DomainElement> o) Diese Methode bekommt als Argument eine weitere Operation die auf der gleichen Domäne arbeitet wie die Operation dieser Methode. Sei diese Operation durch das Symbol * gegeben und sei die Operation des Arguments dieser Methode durch das Symbol + gegeben. Dann distribuiert * über +, falls für alle Elemente X,Y,Z der Domäne gilt X*(Y+Z) = X*Y + X*Z. Im Gegensatz zu den anderen abstrakten Methoden von Operation, kann für diese Methode kein Algorithmus angegeben werden, der ganz allgemein für jede mögliche gegebene Operation darüber entscheidet ob diese Operation über die gegebene distribuiert. Wir werden darum für jede konkrete Operation festlegen, über welche anderen (dann bekannten) Operationen distribuiert wird. Implementieren Sie nun noch die folgenden nicht-abstrakten Methoden. Dabei können und sollen Sie sich, wo sinnvoll, der oben definierten abstrakten Methoden bedienen. Code: public String getName() Code: public boolean isGroupOperation() * ist assoziativ. * besitzt ein neutrales Element. Jedes Element aus der Domäne von * besitzt ein inverses Element. Die Methode liefert genau dann true zurück, wenn die Algebra bestehend aus der Domäne der Operation zusammen mit der Operation eine Gruppe ist. Code: public boolean isAbelianGroupOperation() Code: public boolean isInversePair(DomainElement a, DomainElement b) Code: public boolean equals(Object other) Operationen auf ganzen Zahlen Implementieren Sie nun konkret die Addition auf ganzen Zahlen als Operation. Legen Sie dazu die Klasse Code: public class IntegerAddition extends Operation<Integer> Hinweise: Die Addition auf den ganzen Zahlen ist eine assoziative, kommutative Operation mit dem neutralen Element 0. Jedes Element besitzt ein Inverses. Obwohl Operationen auf den ganzen Zahlen denkbar sind, über die die Addition distribuiert, soll die Methode knownToDistributeOver immer false zurückgeben. Diese Klasse steht für eine konkrete Operation. Eine Instanz der Klasse hat keine Attribute, insofern ist mehr als eine Instanz für die Klasse auch nicht sinnvoll; bei der Klasse handelt es sich also um ein sogenanntes Singleton. Implementieren Sie daher bitte keinen öffentlichen Konstruktor für diese Klasse, sondern nur einen privaten, nämlich Code: private IntegerAddition() Code: public final static IntegerAddition ADD Analog verfahren Sie bitte um eine Operation für die Multiplikation auf ganzen Zahlen im package jpp.algebra zu erzeugen. Code: public class IntegerMultiplication extends Operation<Integer> public final static IntegerMultiplication MULT. Operationen auf rationalen Zahlen Nun soll eine etwas komplexere Domäne als die der ganzen Zahlen implementiert werden. Dazu entwerfen Sie bitte einen Datentyp für rationale Zahlen, die jeweils als Bruch gespeichert werden. Erzeugen Sie dafür zunächst die Klasse Code: public class RationalNumber private int numerator Das ist der Zähler der rationalen Zahl. Code: private int denominator Die Attribute bestimmen also den Wert des Bruchs. Die Klasse soll so implementiert werden, dass numerator und denominator immer einen gekürzten Bruch bilden. Darauf müssen Sie also bei der Implementation aller Methoden dieser Klasse achten. Am besten implementieren Sie dazu eine private Methode Code: private void reduce() Erzeugen Sie den Konstruktor Code: public RationalNumber(int num, int denom) Code: getNumerator() Code: getDenominator() Code: setDenominator(int d) Code: setNumerator(int n) Code: toString() Code: public boolean equals(Object other) Code: public boolean isNegative() Erzeugen Sie nun die Operationen Code: public class RationalAddition extends Operation<RationalNumber> Code: public class RationalMultiplication extends Operation<RationalNumber> Code: public static final RationalAddition ADD Code: public static final RationalMultiplication MULT Operationen auf endlichen Körpern Schließlich sollen noch Operationen auf Restklassen implementiert werden. Ist Z die Menge der ganzen Zahlen und n eine natürliche Zahl, so ist nZ = {... -2n, -n, 0, n, 2n, ...} und Zn = {k+nZ : k aus {0,...,n-1}} die Menge der Restklassen modulo n. Jedes Element X aus Zn ist also eine Menge von Zahlen, so dass es für alle y aus X ein k aus {0,...,n-1} gibt, für das gilt, dass y äquivalent k modulo n ist. Darum genügt es, wenn man die Elemente aus Zn mit den Resten modulo n identifiziert, also im obigen Beispiel würde X mit k identifiziert. Um es einfacher auszudrücken: Es sollen nun Addition und Multiplikation für die Rechnung modulo n implementiert werden. Für diese Operationen ist das Modul, also die Zahl n, die die Restklasse definiert von besonderer Bedeutung. Darum führen wir das Interface Code: public interface RestOperation Code: public int getModul() Code: public class RestAddition extends Operation<Integer> implements RestOperation Code: private int modul Code: public RestAddition(int mod) Code: public int getModul() Die Methode compute soll prinzipiell jeden Integer als Argument akzeptieren, die Ausgabe von compute soll aber stets im Bereich {0,...,n-1} liegen (hier ist n immer das Modul). Beachten Sie dabei, dass jede positive Zahl y (einschließlich der 0) äquivalent ist zu (y % n). Eine negative Zahl z ist äquivalent zu (n - ((-z) % n)). Die Methode knownToDistributeOver gibt stets false zurück. Entwickeln Sie nun noch die Klasse Code: public class RestMultiplication extends Operation<Integer> implements RestOperation extended_euclid(a,b) 1: wenn (b == 0) dann return (a,1,0) 2: (d',s',t') = extended_euclid(b, a mod b) 3: (d,s,t) = (d',t',s' - floor(a/b)*t') 4: return (d,s,t) Der Algorithmus arbeitet nur auf positiven ganzen Zahlen korrekt, erwartet also als Argument ein Paar von positiven ganzen Zahlen (a,b). Der Algorithmus gibt ein Tripel von Zahlen (d,s,t) zurück, für das gilt: d = ggT(a,b) d = s*a + t*b Ist also x aus {0,...,n-1} und ggT(x,n)=1, so ist extended_euclid(a,b) = (1,s,t), wobei 1 = s*x + t*n. Da tn == 0 (mod n) , ist also 1 = s*x modulo n. Mit anderen Worten s ist das das multiplikativ Inverse Element von x modulo n. Beachten Sie, dass die Zahlen s und t durchaus negativ sein können. Gegebenenfalls müssen Sie also ein negatives Ergebnis s noch in einen korrekten Repräsentanten aus {0,...,n-1} umrechnen (wie das gemacht wird, siehe oben). Sie wissen nun, wie sie bestimmen, ob ein einzelnes Element aus {0,...,n-1} ein multiplikativ Inverses modulo n besitzt. Zur Berechnung der Methode elementsInvertible(), müssen Sie aber wissen, ob alle Elemente aus {0,...,n-1} invertierbar sind. Das ist genau dann der Fall, wenn für alle x aus {0,...,n-1} gilt: ggT(x,n) = 1. Das, wiederum, ist genau dann der Fall, wenn n eine Primzahl ist. (Eine Primzahl ist eine Zahl größer gleich 2, die nur durch sich selbst und die 1 teilbar ist.) Die Methode elementsInvertible() muss also genau dann true zurückgeben, wenn das modul eine Primzahl ist. Das lässt sich z.B. mit dem Sieb des Eratosthenes ermitteln. Die Methode knownToDistributeOver gibt genau dann true zurück, wenn das Argument eine Addition auf der gleichen Domäne ist, also wenn die Operation eine RestAddition mit dem gleichen Modul wie die Multiplikation ist. Algebren Legen Sie nun die Klasse Code: public class Algebra<DomainElement> Eine Algebra besitzt das Attribut Code: protected Hashtable<String, Operation<DomainElement>> operations; Implementieren Sie bitte den Konstruktor Code: public Algebra() Code: protected void addOperation(Operation<DomainElement> o) Code: protected Operation<DomainElement> getOperation(String name) Wir wollen nun Klassen für spezielle Algebren entwickeln. Legen Sie zunächst die Klasse Code: public class Group<DomainElement> extends Algebra<DomainElement> Code: public Group(Operation<DomainElement> o) Des weiteren hat die Klasse die Methode Code: public Operation<DomainElement> getGroupOperation() Entwickeln Sie als nächstes die Klasse Code: public class AbelianGroup<DomainElement> extends Group<DomainElement> Schließlich entwickeln Sie bitte noch eine Klasse für einen Ring: Code: public class Ring<DomainElement> extends AbelianGroup<DomainElement> Code: public Ring(Operation<DomainElement> addition, Operation<DomainElement> multiplication) Die Ring-Klasse enthält zusätzlich die folgenden Methoden: Code: public Operation<DomainElement> getRingAddition() Code: public Operation<DomainElement> getRingMultiplication() Polynome Wir haben nun die Infrastruktur um uns mit komplexeren algebraischen Objekten zu befassen. Beispielhaft soll das mit den Polynomen geschehen. Sei R = (U, +, *) ein Ring. Ein Polynom mit einer Variable x über R ist wie folgt definiert: x ist ein Polynom und jedes c aus U ist ein Polynom über R Sind p1 und p2 Polynome über R, so auch p1 + p2 und p1 * p2. Anders ausgedrückt, gibt es für jedes Polynom p über R eine endliche Menge von Koeffizienten a0, a1, ... , ak aus U, so dass p = a0 + a1*x1 + a2*x2 + ... + ak*xk. Wir nennen ai den Koeffizienten des Exponenten i. Wir bemerken ausdrücklich, dass ai=0 sein kann für ein i aus {0,...,k}. Hier, wie im Folgenden schreiben wir das neutrale Element der Addition als 0 und das neutrale Element der Multiplikation als 1. Interessant ist, dass sehr natürliche Operationen auf der Menge der Polynome so definiert werden können, dass diese Menge zusammen mit den Operationen selbst wieder einen Ring bildet. Entwickeln Sie nun eine Klasse für Polynome: Code: public class Polynomial<DomainElement> Code: protected Ring<DomainElement> basicRing Code: protected Hashtable<Integer, DomainElement> coefficients Code: protected int degree Die Klasse hat die folgenden Konstruktoren: Code: public Polynomial(Ring<DomainElement> r, DomainElement[] coeffs) coeffs abhängt. Dieses enthält die Koeffizienten des Polynoms und zwar ist ai = coeffs[i]. Folglich ist der Grad des Polynoms höchstens coeffs.length - 1. Hat das coeffs-Array die Länge 0, so wird das 0-Polynom (alle Koeffizienten sind 0) konstruiert. Code: public Polynomial(Ring<DomainElement> r, Hashtable<Integer, DomainElement> coeffs) Schreiben Sie bitte die folgenden Methoden für die Klasse: Code: public DomainElement getCoefficient(int exponent) Code: public Ring<DomainElement> getRing() Code: public int getDegree() Code: public boolean ringCompatible(Polynomial<DomainElement> other) Code: public boolean isAdditiveNeutral() Code: public boolean isMultiplicativeNeutral() Code: public boolean equals(Object other) Code: public String toString() Nun sollen noch Addition und Multiplikation auf der Domäne der Polynome definiert werden. Entwickeln Sie dazu die Klasse Code: public class PolynomialAddition<DomainElement> extends Operation<Polynomial<DomainElement>> Code: public PolynomialAddition(Ring<DomainElement> r) Für die Polynomaddition wird der zugrundeliegende Ring benötigt, er muss also in einem Attribut gespeichert werden. Der Name der PolynomAddition soll auf "Polynom addtion based on [Basisaddition]" festgelegt werden, wobei [Basisaddition] der Name der Addition aus dem Basisring ist. Ist das Argument r gleich null, so wird eine NullPointerExcepetion geworfen. Schließlich sollen alle Methoden einer Operation implementiert werden. Dazu ist zu bemerken, dass eine Polynomaddition stets eine assoziative, kommutative Operation ist mit neutralem Element 0 (das ist das Polynom, das nur aus der 0 besteht, d.h. alle Koeffizienten sind 0). Die Summe zweier Polynome p(x) = a0 + a1*x1 +a2*x2 +... +ak*xk und q(x) = b0 + b1*x1 +b2*x2 +... +bk*xm ist definiert als p(x)+q(x) = a0+b0 + (a1 + b1)*x1 + (a2 + b2)*x2 + ... + (an + bn)*xn, wobei n = max(k,m). Bei den Symbolen "+" und "*" handelt es sich dabei um die Operationen der Addition und Multiplikation auf dem zugrundeliegenden Ring. Aus der Definition ist auch unmittelbar klar, dass jedes Polynom ein inverses bezüglich der Polynomaddition hat und wie dieses aussieht. Die Methode knownToDistributeOver soll immer false zurückgeben. Die letzte Klasse, die Sie für diese Aufgabe entwickeln sollen, ist die Polynommultiplikation. Code: public class PolynomialMultiplication<DomainElement> extends Operation<Polynomial<DomainElement>> Code: public PolynomialMultiplication(Ring<DomainElement> r) Implementieren Sie die Methoden, die die abstrakte Klasse Operation vorgibt. Dazu folgende Informationen: Bei der Polynommultiplikation handelt es sich um eine assoziative, kommutative Operation mit neutralem Element 1 (das ist das Polynom, dessen Koeffizienten alle 0 sind, bis auf den des Exponenten 0, der ist 1). Die Polynommultiplikation ist für Polynome p(x) = a0 + a1*x1 +a2*x2 +... +ak*xk und q(x) = b0 + b1*x1 +b2*x2 +... +bk*xm wie folgt definiert: p(x)*q(x) = c0 + c1*x1 +c2*x2 +... +ck+m*xk+m wobei ci = ∑s,t mit s+t=i as * bt. Das heißt, dass die Koeffizienten ci des Ergebnispolynoms die Summe der Produkte derjenigen Koeffizienten as und bt sind, deren Indizes s und t sich gerade zu i summieren. Auch hier stehen die verwendeten Symbole "+" und "*" für die Addition und die Multiplikation auf dem Basisring (bis auf das "+" in ck+m und xk+m: Hier handelt es sich um die normale Addition auf ganzen Zahlen). Die Definition der Polynommultiplikation macht sofort klar, dass der Grad des Polynoms p(x)*q(x) der Summe der Grade von p(x) und q(x) entspricht. Da das neutrale Element der Polynommultiplikation vom Grad 0 ist können nur Polynome vom Grad 0 ein inverses Element bezüglich der Multiplikation haben. Das sind genau die konstanten Polynome, also Polynome der Form a0, wobei a0 ein Element des Basisrings ist. Für diese Polynome hängt es vom Basisring ab, ob sie ein multiplikativ inverses Element besitzen oder nicht. Die Polynommultiplikation distribuiert über die Polynomaddition, vorausgesetzt die beiden Polynomringoperationen arbeiten auf dem gleichen Basisring. ---------- Viel Spaß! Grüße | |
| | |
![]() |
| - Anzeige - | |
| |
| Themen-Optionen | |
| Ansicht | |
| |