Κεφάλαιο 5: Αυτοαναφορικές Δομές & Linked Lists

Δομές που «δείχνουν» σε δομές ίδιου τύπου

5.1 Τι είναι οι αυτοαναφορικές δομές

Μια αυτοαναφορική δομή (self-referential structure) είναι μια struct που περιέχει pointer προς δομή του ίδιου τύπου.

Με απλά λόγια: η δομή «δείχνει» σε άλλη δομή ίδιου τύπου. Σκεφτείτε το σαν μια αλυσίδα — κάθε κρίκος γνωρίζει ποιος είναι ο επόμενος.

📌 Πού χρησιμοποιούνται;

Οι αυτοαναφορικές δομές αποτελούν τη βάση για πολλές δομές δεδομένων:

  • Linked Lists (Συνδεδεμένες Λίστες)
  • Trees (Δέντρα)
  • Graphs (Γραφήματα)

Linked Lists (Συνδεδεμένες Λίστες)

Φανταστείτε ένα τρένο: κάθε βαγόνι είναι συνδεδεμένο με το επόμενο. Κάθε κόμβος «δείχνει» στον επόμενο, σχηματίζοντας μια αλυσίδα. Ο τελευταίος κόμβος δείχνει σε NULL (τέλος).

A
B
C
NULL

🌍 Πού τις συναντάμε στην καθημερινότητα;

  • Playlist μουσικής — κάθε τραγούδι «ξέρει» ποιο είναι το επόμενο
  • Undo/Redo — κάθε ενέργεια συνδέεται με την προηγούμενη (π.χ. Ctrl+Z στο Word)
  • Browser history — κάθε σελίδα δείχνει στην προηγούμενη (κουμπί «Πίσω»)
  • Ουρά εκτύπωσης — τα έγγραφα εκτυπώνονται με τη σειρά που μπήκαν

Trees (Δέντρα)

Σκεφτείτε ένα οικογενειακό δέντρο: ένας γονέας μπορεί να έχει πολλά παιδιά. Στην πληροφορική, κάθε κόμβος «δείχνει» σε δύο ή περισσότερα παιδιά-κόμβους. Ξεκινάμε πάντα από τη ρίζα (root) και κατεβαίνουμε προς τα φύλλα.

/* root / \ B C / \ D E */

🌍 Πού τα συναντάμε στην καθημερινότητα;

  • Φάκελοι στον υπολογιστή — κάθε φάκελος περιέχει υποφακέλους (Documents → Projects → myApp)
  • Οργανόγραμμα εταιρείας — διευθυντής → τμηματάρχες → υπάλληλοι
  • HTML σελίδα — κάθε στοιχείο περιέχει παιδιά-στοιχεία (<html> → <body> → <div>)
  • Τουρνουά — οι αγώνες σχηματίζουν δέντρο μέχρι τον τελικό

Graphs (Γραφήματα)

Σκεφτείτε ένα χάρτη πόλεων: κάθε πόλη συνδέεται με πολλές άλλες, χωρίς συγκεκριμένη ιεραρχία. Κάθε κόμβος μπορεί να «δείχνει» σε οποιονδήποτε άλλο — ακόμα και πίσω στον εαυτό του. Είναι η πιο γενική μορφή σύνδεσης.

/* A --- B | / | | / | C --- D */

🌍 Πού τα συναντάμε στην καθημερινότητα;

  • Google Maps — δρόμοι που συνδέουν διασταυρώσεις, βρίσκει τη συντομότερη διαδρομή
  • Κοινωνικά δίκτυα — Facebook/Instagram: κάθε χρήστης συνδέεται με φίλους, που συνδέονται με δικούς τους φίλους
  • Δίκτυο Wi-Fi — συσκευές που επικοινωνούν μεταξύ τους μέσα στο ίδιο δίκτυο
  • Σύστημα πτήσεων — αεροδρόμια συνδεδεμένα με απευθείας πτήσεις
📝 Σημείωση: Σε αυτό το κεφάλαιο θα ασχοληθούμε μόνο με Linked Lists, γιατί είναι η πιο απλή και θεμελιώδης δομή. Τα δέντρα και τα γραφήματα αποτελούν θέματα προχωρημένων μαθημάτων.

5.2 Η δομή Node & η Linked List

Ας δούμε γραμμή-γραμμή τη δομή ενός κόμβου με αναλυτικά σχόλια, για να καταλάβεις ακριβώς τι κάνει κάθε μέρος:

// Ορισμός μιας δομής (struct) που περιγράφει έναν κόμβο μιας Linked List struct Node { int data; // Μεταβλητή που αποθηκεύει την τιμή του κόμβου. // Μπορεί να είναι οποιοδήποτε δεδομένο (εδώ είναι int). // Παράδειγμα: 5, 10, 20 κλπ. struct Node *next; // Pointer (δείκτης) που δείχνει στη διεύθυνση μνήμης // του επόμενου κόμβου στη λίστα. // Δηλαδή συνδέει αυτόν τον κόμβο με τον επόμενο. // Αν είναι ο τελευταίος κόμβος τότε next = NULL. };

🔎 Πώς φαίνεται στη μνήμη

Φαντάσου ότι έχουμε τη λίστα:

10
20
30
NULL

Τότε οι κόμβοι είναι περίπου έτσι στη μνήμη:

Node1 [data:10 | next: address Node2] Node2 [data:20 | next: address Node3] Node3 [data:30 | next: NULL]

💡 Δηλαδή:

  • data → αποθηκεύει την τιμή
  • next → δείχνει στον επόμενο κόμβο

📌 Γιατί γράφουμε struct Node *next ;

Επειδή κάθε κόμβος πρέπει να δείχνει σε άλλον κόμβο του ίδιου τύπου, ο pointer πρέπει να είναι pointer σε Node. Δηλαδή:

Node -> Node -> Node -> NULL

📌 typedef με Node

Με το typedef μπορούμε να δώσουμε νέο (συντομότερο) όνομα στον τύπο. Αντί να γράφουμε struct Node παντού, γράφουμε απλά Node:

// Ορισμός δομής ΚΑΙ typedef μαζί σε μία πρόταση typedef struct Node { int data; // η τιμή του κόμβου struct Node *next; // pointer στον επόμενο κόμβο } Node;

🔎 Τι κάνει κάθε μέρος:

  • typedef → λέει στον compiler: «θα δώσω νέο όνομα σε έναν τύπο»
  • struct Node { ... } → ορίζει τη δομή κανονικά (όπως πριν)
  • Node; στο τέλος → αυτό είναι το νέο όνομα του τύπου
Προσοχή: Μέσα στη δομή γράφουμε struct Node *next (με τη λέξη struct), γιατί εκείνη τη στιγμή το typedef δεν έχει ολοκληρωθεί ακόμα — ο compiler δεν γνωρίζει ακόμα το όνομα Node ως τύπο.

📌 Παράδειγμα δημιουργίας ενός κόμβου

#include <stdio.h> // Κόμβος (Node) μιας συνδεδεμένης λίστας struct Node { int data; // Τα δεδομένα που αποθηκεύει ο κόμβος struct Node *next; // Το "next" είναι δείκτης (pointer) σε struct Node // Το '*' σημαίνει ότι ΔΕΝ αποθηκεύει κόμβο, αλλά τη διεύθυνση (address) ενός κόμβου // Δηλαδή δείχνει στον επόμενο κόμβο της λίστας // Αν next = NULL, τότε δεν υπάρχει επόμενος κόμβος (τέλος λίστας) }; int main() { // Εδώ δημιουργούμε 3 μεταβλητές τύπου struct Node struct Node n1; struct Node n2; struct Node n3; // Βάζουμε τιμές στους κόμβους n1.data = 10; n2.data = 20; n3.data = 30; // Συνδέουμε τους κόμβους μεταξύ τους n1.next = &n2; // ο n1 δείχνει στον n2 n2.next = &n3; // ο n2 δείχνει στον n3 n3.next = NULL; // τελευταίος κόμβος // Εκτύπωση τιμών printf("%d\n", n1.data); printf("%d\n", n2.data); printf("%d\n", n3.data); return 0; }
10 20 30

📌 Οπτικά

+-------+--------+ | data | next | +-------+--------+ | 10 | *----> επόμενος κόμβος +-------+--------+

5.3 Παράδειγμα: Playlist με Linked List

#include <stdio.h> // Κόμβος που αναπαριστά ένα τραγούδι struct Node { char title[50]; // τίτλος τραγουδιού char artist[50]; // καλλιτέχνης int duration; // διάρκεια σε δευτερόλεπτα struct Node *next; // δείχνει στο επόμενο τραγούδι }; int main() { // δημιουργούμε 3 τραγούδια struct Node song1; struct Node song2; struct Node song3; // συμπληρώνουμε δεδομένα song1.duration = 210; song2.duration = 180; song3.duration = 240; // συνδέουμε τα τραγούδια song1.next = &song2; song2.next = &song3; song3.next = NULL; // εκτύπωση playlist printf("Song1 duration: %d\n", song1.duration); printf("Song2 duration: %d\n", song2.duration); printf("Song3 duration: %d\n", song3.duration); return 0; }
Song1 duration: 210 Song2 duration: 180 Song3 duration: 240

Πώς μοιάζει η playlist

Song1
next →
Song2
next →
Song3
next →
NULL

🎵 Γιατί Linked List για playlist;

Γιατί μπορείς εύκολα να:

  • Προσθέσεις τραγούδι στη μέση
  • Αφαιρέσεις τραγούδι
  • Πας στο επόμενο τραγούδι

🤔 Πότε χρησιμοποιείται η Linked List;

Η linked list δεν έχει νόημα όταν ξέρεις από πριν πόσα στοιχεία έχεις (όπως εδώ: 3 τραγούδια). Χρησιμοποιείται όταν:

✅ 1. Δεν ξέρεις πόσα στοιχεία θα έχεις

π.χ. playlist που μεγαλώνει δυναμικά

✅ 2. Θες εύκολη προσθήκη/αφαίρεση

  • να βάλεις τραγούδι στη μέση
  • να διαγράψεις τραγούδι

✅ 3. Θες να τα διατρέχεις με loop

π.χ.:

👉 Αυτό είναι το βασικό νόημα της linked list:

Δεν χρειάζεται να ξέρεις πόσα είναι.

⚖️ Σύγκριση

❌ Χωρίς linked list (στατικά) ✅ Με linked list
Ξέρεις τα τραγούδια από πριν Μπορείς να έχεις όσα τραγούδια θέλεις
Γράφεις ένα-ένα Τα διατρέχεις με loop
Δεν επεκτείνεται εύκολα Είναι πιο «δυναμικό»

5.4 Traversal — Διάσχιση της Linked List

Ξεκινάμε από το παράδειγμα της Playlist. Έχουμε μια playlist:

Song1
Song2
Song3
NULL

και έχουμε δημιουργήσει τους κόμβους:

song1.next = &song2; song2.next = &song3; song3.next = NULL;

Δηλαδή οι κόμβοι είναι συνδεδεμένοι μεταξύ τους.

⚠️ Το πρόβλημα

Αν θέλουμε να εμφανίσουμε όλα τα τραγούδια γράφουμε:

printf("%d\n", song1.duration); printf("%d\n", song2.duration); printf("%d\n", song3.duration);

⛔ Αυτό όμως έχει πρόβλημα:

  • Πρέπει να ξέρουμε πόσα τραγούδια υπάρχουν
  • Αν προστεθεί νέο τραγούδι, πρέπει να αλλάξουμε τον κώδικα

Π.χ. αν η playlist γίνει:

Song1Song2Song3Song4Song5

θα πρέπει να γράψουμε:

printf(...); printf(...); printf(...); printf(...); printf(...); // κάθε φορά που αλλάζει η λίστα!

Άρα αυτό δεν είναι πρακτικό.

💡 Η ιδέα της λύσης

Επειδή κάθε κόμβος έχει next, μπορούμε να ακολουθούμε αυτά τα pointers. Δηλαδή:

Βήματα:

  1. Ξεκινάμε από τον πρώτο κόμβο
  2. Πηγαίνουμε στον επόμενο
  3. Συνεχίζουμε μέχρι να βρούμε NULL

Αυτό λέγεται Traversal (Διάσχιση).

🧠 Η λύση

Χρησιμοποιούμε έναν pointer που κινείται στη λίστα:

// Δηλώνουμε έναν pointer τύπου Node // Το current θα "δείχνει" κάθε φορά σε ένα τραγούδι struct Node *current = &song1; // Ξεκινάμε από το πρώτο τραγούδι (song1) // Όσο ο pointer ΔΕΝ είναι NULL (δηλαδή υπάρχει κόμβος) while (current != NULL) { // Εκτυπώνουμε τη διάρκεια του τρέχοντος τραγουδιού // current->duration σημαίνει: πάρε το duration του κόμβου που δείχνει το current printf("Duration: %d\n", current->duration); // Μετακινούμαστε στο επόμενο τραγούδι της λίστας // current->next είναι ο δείκτης προς τον επόμενο κόμβο current = current->next; }

🔎 Τι συμβαίνει βήμα-βήμα

Βήμα 1 — Αρχή:

current → Song1

Βήμα 2:

current → Song2

Βήμα 3:

current → Song3

Βήμα 4 — Τέλος:

current → NULL — το loop σταματά!

📌 Συμπέρασμα

Το Traversal επιτρέπει να:

  • Διαβάζουμε όλη τη linked list
  • Χωρίς να ξέρουμε πόσα στοιχεία έχει

Απλά ακολουθούμε τα next pointers μέχρι να βρούμε NULL.

5.5 Η λύση: head

Χρησιμοποιούμε έναν pointer που δείχνει στον πρώτο κόμβο της λίστας. Αυτός ο pointer λέγεται συνήθως:

head

Οπτικά

head
Song1
Song2
Song3
NULL
Προσοχή: Ο head δεν είναι κόμβος. Είναι απλά ένας pointer στον πρώτο κόμβο.

Παράδειγμα

struct Node *head = &song1;

Τώρα το traversal γίνεται έτσι:

// Δηλώνουμε έναν pointer τύπου Node // Το current θα "δείχνει" κάθε φορά σε ένα τραγούδι struct Node *current = head; // Ξεκινάμε από το πρώτο τραγούδι (head) // Όσο ο pointer ΔΕΝ είναι NULL (δηλαδή υπάρχει κόμβος) while (current != NULL) { // Εκτυπώνουμε τη διάρκεια του τρέχοντος τραγουδιού // current->duration σημαίνει: πάρε το duration του κόμβου που δείχνει το current printf("Duration: %d\n", current->duration); // Μετακινούμαστε στο επόμενο τραγούδι της λίστας // current->next είναι ο δείκτης προς τον επόμενο κόμβο current = current->next; }

✅ Πλήρες παράδειγμα με head + traversal

#include <stdio.h> // Ορισμός κόμβου (τραγούδι) struct Node { char title[50]; char artist[50]; int duration; struct Node *next; }; int main() { // Δημιουργία τραγουδιών struct Node song1, song2, song3; // Συμπλήρωση δεδομένων song1.duration = 210; song2.duration = 180; song3.duration = 240; // Σύνδεση κόμβων (linked list) song1.next = &song2; // song1 → song2 song2.next = &song3; // song2 → song3 song3.next = NULL; // τέλος λίστας // 👉 Head pointer (αρχή λίστας) struct Node *head = &song1; // 👉 Pointer για traversal // Το current θα "περπατάει" στη λίστα struct Node *current = head; // 👉 Διατρέχουμε τη λίστα while (current != NULL) { // Εκτυπώνουμε δεδομένα του τρέχοντος κόμβου printf("Duration: %d\n", current->duration); // Πηγαίνουμε στον επόμενο κόμβο current = current->next; } return 0; }
Duration: 210 Duration: 180 Duration: 240

🔑 Γιατί είναι σημαντικό το head

Με το head μπορούμε να:

  • Ξεκινάμε το traversal
  • Προσθέτουμε κόμβους στην αρχή
  • Διαγράφουμε κόμβους
  • Περνάμε τη λίστα σε συναρτήσεις

Χωρίς head χάνουμε την αρχή της λίστας!

📌 Μικρό παράδειγμα

head102030NULL

Traversal:

current102030NULL

✅ Με μία πρόταση

Το head είναι ο pointer που κρατά τη διεύθυνση του πρώτου κόμβου της linked list και μας επιτρέπει να ξεκινήσουμε την επεξεργασία της.

5.6 Άσκηση

✏️ Άσκηση: Δημιουργία linked list με 3 κόμβους

Γράψε πρόγραμμα που:

  1. Δημιουργεί 3 κόμβους (με malloc)
  2. Τους συνδέει σε linked list
  3. Εκτυπώνει όλες τις τιμές

Χρησιμοποίησε τη δομή:

struct Node { int data; struct Node *next; };

Η λίστα σου πρέπει να μοιάζει έτσι:

?
next →
node1
?
next →
node2
?
next →
node3
NULL

📋 Ανακεφαλαίωση

Αυτοαναφορική δομή = struct με pointer σε δομή ίδιου τύπου

Χρησιμοποιούμε pointer (όχι ολόκληρη δομή) για να αποφύγουμε άπειρο μέγεθος

Η linked list τελειώνει πάντα με NULL

Το head είναι ο pointer στον πρώτο κόμβο — χωρίς αυτόν χάνουμε τη λίστα

Το Traversal μας επιτρέπει να διασχίσουμε όλη τη λίστα ακολουθώντας τα next