Vererbung
Aufgabe
Die Blutgruppe eines Menschen wird durch zwei Allele (d.h. verschiedene Formen eines Gens) bestimmt. Die drei möglichen Allele sind A, B und O (im Deutschen wird statt “O” normalerweise die Zahl “0” verwendet). Jeder Mensch besitzt zwei Allele, die gleich (z.B. AA oder BB), oder verschieden (z.B. AB) sein können. Jedes Elternteil eines Kindes gibt zufällig eines seiner beiden Allele an sein Kind weiter. Welche Blutgruppe sich aufgrund der vorhandenen Allele letztlich ergibt, haben Sie im eingebetteten Video erfahren - falls Sie es sich angeschaut haben (für die Aufgabe nicht zwingend erforderlich). In dieser Aufgabe geht es jedoch weniger um die letztliche Blutgruppe (d.h. die beobachtbaren Merkmale des Blutes) als vielmehr um die Vererbung der Allele (d.h. den Genotyp). Da es drei verschiedene Allele gibt und jedes Elternteil eines seiner beiden Allele an das eigene Kind weitergibt, sind die möglichen Allelkombinationen demnach: OO, OA, OB, AO, AA, AB, BO, BA und BB.
Wenn zum Beispiel ein Elternteil die Allele AO und das andere Elternteil die Allele BB hat, dann wären die möglichen Allele des Kindes AB und OB, je nachdem, welches Allel von jedem Elternteil vererbt wurde. Hat ein Elternteil die Allele AO und das andere die Allele OB, dann wären die möglichen Allele des Kindes AO, OB, AB und OO.
In dieser Aufgabe wollen wir die Vererbung der Allele für jedes Mitglied einer Familie simulieren.
Demo
Aufgabenmaterial
Für diese Aufgabe werden Sie ein von von uns zur Verfügung gestelltes Codegerüst vervollständigen.
Aufgabenmaterial herunterladen
Öffnen Sie VS Code entsprechend Ihrem Setup.
Öffnen Sie Ihr Terminalfenster und führen Sie dann cd
aus. Die Eingabeaufforderung Ihres Terminalfensters sollte wie folgt aussehen:
$
Geben Sie dann
wget https://inf.zone/download/exercises/05/inheritance.zip
ein und führen Sie den Befehl mit der Eingabetaste aus, um eine ZIP-Datei namens inheritance.zip
in den aktuellen Ordner herunterzuladen. Achten Sie darauf, dass Sie das Leerzeichen zwischen wget
und der folgenden URL nicht übersehen, und auch kein anderes Zeichen!
Führen Sie jetzt
unzip inheritance.zip
aus um das ZIP-Archiv in einen Ordner namens inheritance
zu extrahieren. Sie brauchen die ZIP-Datei nicht mehr, also können Sie
rm inheritance.zip
ausführen. Antworten Sie mit “y”, gefolgt von der Eingabetaste, um die heruntergeladene ZIP-Datei zu entfernen.
Führen Sie dann
cd inheritance
aus, um in dieses Verzeichnis zu wechseln. Ihre Eingabeaufforderung sollte nun wie folgt aussehen:
inheritance/ $
Wenn alles wie beschrieben funktioniert hat, sollten Sie nach dem Ausführen von
ls
eine Datei, inheritance.c
, sehen.
Überblick über inheritance.c
Betrachten wir zunächst die Definition eines Typs namens person
. Jede Person hat ein Array von zwei parents
, von denen jedes ein Zeiger auf eine andere person
-struct
ist. Jede Person hat auch ein Array mit zwei alleles
, von denen jedes ein char
ist (entweder A
, B
oder O
).
// Each person has two parents and two alleles
typedef struct person
{
struct person *parents[2];
char alleles[2];
}
person;
Werfen wir nun einen Blick auf die main
-Funktion. Die Funktion beginnt mit dem “Seeding” (d.h. der Bereitstellung eines Anfangsinputs) eines Zufallszahlengenerators, den wir später zur Erzeugung von Zufallsallelen verwenden werden.
// Seed random number generator
srand(time(0));
Diese Funktion ändert die Folge von Pseudozufallszahlen, die beim Aufruf der Funktion rand()
zurückgegeben werden.
Der Aufruf von srand(time(0))
bewirkt, dass die Funktion rand()
bei jedem Programmaufruf Zahlen in einer anderen Reihenfolge ausgibt, da time
, d.h. die Systemzeit, bei jedem Aufruf anders ist. Daher sollte diese Funktion ein Mal vor der ersten Verwendung von rand()
aufgerufen werden. Da dies bereits im Codegerüst geschieht, brauchen Sie sich um nichts weiteres zu kümmern, d.h. Sie müssen nicht jedes Mal, bevor Sie rand()
aufrufen, srand()
erneut aufrufen. Einzig relevant ist noch folgende Information: Wenn die Funktion rand()
im weiteren Verlauf des Codes aufgerufen wird, gibt diese pseudo-zufällige Zahlen zwischen 0
und RAND_MAX
(im Falle des CS50-Setups steckt dahinter die Zahl 32767
) zurück - die benötigte Bibliothek <stdlib.h>
ist ebenfalls bereits im Codegerüst eingebunden.
Die Funktion rand()
werden Sie später noch benötigen. rand()
kommt aber auch in der bereits implementierten Funktion random_allele()
vor:
// Randomly chooses a blood type allele.
char random_allele()
{
int r = rand() % 3;
if (r == 0)
{
return 'A';
}
else if (r == 1)
{
return 'B';
}
else
{
return 'O';
}
}
Wieso wird hier rand() % 3
verwendet?
Die main
-Funktion ruft dann die Funktion create_family
auf, um die Erstellung von person
-struct
s für eine Familie mit 3 Generationen (d.h. eine Person, ihre Eltern und ihre Großeltern) zu initiieren.
// Create a new family with three generations
person *p = create_family(GENERATIONS);
Wir rufen dann print_family
auf, um jedes dieser Familienmitglieder mit seinen jeweiligen Allelen auszugeben.
// Print family tree of blood types
print_family(p, 0);
Schließlich wird die Funktion free_family
aufgerufen, um Speicher mit free
freizugeben, der zuvor mit malloc
allokiert wurde.
// Free memory
free_family(p);
Die Vervollständigung dieser beiden Funktionen create_family
und free_family
ist nun Ihre Aufgabe!
Details zur Umsetzung
Vervollständigen Sie die Implementierung von inheritance.c
, so dass eine Familie mit einer bestimmten Generationsgröße entsteht und jedem Familienmitglied “ABO”-Allele zugewiesen werden. Der ältesten Generation werden die Allele zufällig zugewiesen.
- Die Funktion
create_family
nimmt einenint
(generations
) als Eingabe und sollte (etwa mitmalloc
) Platz für eineperson
-struct
für jedes Familienmitglied zuweisen (entsprechend der spezifizierten Generationentiefegenerations
) und einen Zeiger auf dieperson
in der jüngsten Generation zurückgeben.- Zum Beispiel sollte
create_family(3)
einen Zeiger auf eine Person mit zwei Eltern zurückgeben, wobei jedes Elternteil auch zwei Eltern hat. - Jeder
person
solltenalleles
zugewiesen werden. Die älteste Generation sollte Allele haben, die zufällig ausgewählt werden (durch den Aufruf der Funktionrandom_allele
), und jüngere Generationen sollten ein Allel (zufällig ausgewählt) von jedem Elternteil erben. - Jeder
person
solltenparents
zugewiesen werden. Die älteste Generation sollte beideparents
aufNULL
gesetzt haben, und bei jüngeren Generationen sollteparents
ein Array aus zwei Zeigern sein, die jeweils auf ein anderes Elternteil zeigen.
- Zum Beispiel sollte
Hilfestellung
Klicken Sie auf die folgenden Tipps, um einige Ratschläge zu erhalten!
Vervollständigen der create_family
Funktion
Die Funktion create_family
sollte einen Zeiger auf eine person
zurückgeben, die ihre Allele über die angegebene Anzahl von generations
geerbt hat.
- Überlegen Sie zunächst, warum dieses Problem eine gute Gelegenheit für eine Rekursion bietet.
- Um die Allele der jetzigen Person zu bestimmen, müssen Sie zunächst die Allele ihrer Eltern ermitteln.
- Um die Allele dieser Eltern zu bestimmen, müssen Sie zunächst die Allele ihrer Eltern bestimmen. Und so weiter, bis Sie die letzte Generation erreicht haben, die Sie simulieren möchten.
Um dieses Problem zu lösen, finden Sie mehrere TODOs im im Code des Aufgabenmaterials.
Zuerst sollten Sie Speicher für eine neue Person zuweisen. Erinnern Sie sich, dass Sie malloc
verwenden können, um Speicher zuzuweisen, und sizeof(person)
, um die Anzahl der benötigten Bytes zu erhalten.
// Allocate memory for new person
person *new_person = malloc(sizeof(person));
Als nächstes ist zu prüfen, ob noch Generationen zu erzeugen sind, d.h. ob generations > 1
ist.
Wenn generations > 1
, dann gibt es weitere Generationen, die noch erstellt werden müssen. Wir haben in dem zur Verfügung gestellten Code bereits zwei neue Eltern, parent0
und parent1
, durch rekursiven Aufruf von create_family
erzeugt. Sie sollten sich in der Funktion create_family
dann um das Setzen der Zeiger auf die Eltern der neu erstellten Person kümmern. Anschließend sollten Sie der neuen Person beide alleles
zuweisen, indem Sie zufällig ein Allel von jedem Elternteil auswählen.
- Erinnern Sie sich, dass Sie die Pfeilnotation verwenden können, um über einen Zeiger auf eine Variable zuzugreifen. Wenn zum Beispiel
p
ein Zeiger auf eine Person ist, dann kann auf den Zeiger auf das erste Elternteil dieser Person mitp->parents[0]
zugegriffen werden. - Für die zufällige Zuweisung von Allelen ist die Funktion
rand()
nützlich (Hinweis: Der Eintrag zurand()
ist im Handbuch nicht entsprechend aufbereitet; Sie können für weitere Informationen auch den leichter zugänglichen Handbucheintrag zurandom()
lesen; einziger Unterschied neben dem Namen: Die Funktion gibt einenlong
statt einenint
zurück). Diese Funktion gibt eine ganze Zahl zwischen0
undRAND_MAX
, oder32767
, zurück. Um eine Pseudo-Zufallszahl zu erzeugen, die entweder0
oder1
ist, können Sie den Ausdruckrand() % 2
verwenden.
// Create two new parents for current person by recursively calling create_family
person *parent0 = create_family(generations - 1);
person *parent1 = create_family(generations - 1);
// Set parent pointers for current person
new_person->parents[0] = parent0;
new_person->parents[1] = parent1;
// Randomly assign current person's alleles based on the alleles of their parents
new_person->alleles[0] = parent0->alleles[rand() % 2];
new_person->alleles[1] = parent1->alleles[rand() % 2];
Nehmen wir an, es gibt keine weiteren Generationen mehr zu simulieren. Das heißt, generations == 1
. Wenn dies der Fall ist, gibt es keine Eltern für diese Person. Beide Elternteile der neuen Person sollten auf NULL
gesetzt werden, und jedes allele
sollte zufällig generiert werden.
// Set parent pointers to NULL
new_person->parents[0] = NULL;
new_person->parents[1] = NULL;
// Randomly assign alleles
new_person->alleles[0] = random_allele();
new_person->alleles[1] = random_allele();
Schließlich sollte Ihre Funktion einen Zeiger auf die neu erstellte person
zurückgeben.
// Return newly created person
return new_person;
Implementierung der free_family
-Funktion
Die Funktion free_family
sollte als Eingabe einen Zeiger auf eine person
akzeptieren, Speicher für diese Person freigeben und dann rekursiv Speicher für alle ihre Vorfahren freigeben.
- Da es sich um eine rekursive Funktion handelt, sollten Sie zunächst den Basisfall behandeln. Ist die Funktionseingabe
NULL
, so gibt es nichts freizugeben, so dass die Funktion sofortreturn
en kann. - Andernfalls sollten Sie beide Elternteile der Person rekursiv mit
free
freigeben, bevor Sie das Kind freigeben.
Achtung Spoiler: Der folgende Code zeigt Ihnen, wie Sie genau das umsetzen können!
// Free `p` and all ancestors of `p`.
void free_family(person *p)
{
// Handle base case
if (p == NULL)
{
return;
}
// Free parents recursively
free_family(p->parents[0]);
free_family(p->parents[1]);
// Free child
free(p);
}
Testen
Wenn Sie ./inheritance
ausführen, sollte Ihr Programm die in der Aufgabenstellung beschriebenen Regeln einhalten. Das Kind sollte zwei Allele haben, eines von jedem Elternteil. Die Eltern sollten jeweils zwei Allele haben, eines von jedem ihrer Elternteile.
Im folgenden Beispiel hat das Kind der Generation 0 von beiden Eltern der Generation 1 ein O-Allel erhalten. Der erste Elternteil hat vom ersten Großelternteil ein A und vom zweiten Großelternteil ein O erhalten. Analog hat der zweite Elternteil von seinen Großeltern ein O und ein B erhalten.
$ ./inheritance
Child (Generation 0): alleles OO
Parent (Generation 1): alleles AO
Grandparent (Generation 2): alleles OA
Grandparent (Generation 2): alleles BO
Parent (Generation 1): alleles OB
Grandparent (Generation 2): alleles AO
Grandparent (Generation 2): alleles BO
Korrektheit
Führen Sie in Ihrem Terminal den folgenden Befehl aus, um die Korrektheit Ihrer Arbeit zu überprüfen:
check50 -l cs50/problems/2024/x/inheritance
Style
Führen Sie den folgenden Befehl aus, um den Stil Ihres Codes mit style50
zu analysieren:
style50 inheritance.c