Διάλεξη ΙΙΙ — Προγραμματισμός στη C II
Δείκτες (Pointers)  |  Ταξινόμηση & Αναζήτηση Πινάκων

📌 Κεφάλαιο 3: Δείκτες, Ταξινόμηση & Αναζήτηση

Δείκτες (Pointers) — Ταξινόμηση πινάκων (Bubble Sort, Selection Sort) — Αναζήτηση (Linear & Binary Search)

3.1 Δείκτες και Συναρτήσεις — Call by Reference

Στη C, όταν περνάμε μεταβλητές σε συναρτήσεις, περνάμε αντίγραφα (call by value). Αν θέλουμε η συνάρτηση να αλλάξει πραγματικά τις μεταβλητές μας, πρέπει να περάσουμε τις διευθύνσεις τους — δηλαδή δείκτες (call by reference).

3.1.1 Το Πρόβλημα — swap χωρίς Δείκτες

#include <stdio.h> /* ΛΑΘΟΣ swap — δεν αλλάζει τίποτα! */ void swap_wrong(int a, int b) { int temp = a; a = b; b = temp; /* οι a, b είναι ΑΝΤΙΓΡΑΦΑ — αλλάζουν μόνο τοπικά */ } int main() { int x = 5, y = 10; swap_wrong(x, y); printf("x=%d, y=%d\n", x, y); /* x=5, y=10 — δεν άλλαξαν! */ return 0; }
x=5, y=10

3.1.2 Η Λύση — swap με Δείκτες

#include <stdio.h> /* ΣΩΣΤΟ swap — χρησιμοποιούμε δείκτες */ void swap(int *a, int *b) { int temp = *a; /* αποθηκεύουμε τιμή από τη διεύθυνση a */ *a = *b; /* βάζουμε στο a αυτό που έχει το b */ *b = temp; /* βάζουμε στο b αυτό που είχε το a */ } int main() { int x = 5, y = 10; printf("Πριν: x=%d, y=%d\n", x, y); swap(&x, &y); /* περνάμε τις ΔΙΕΥΘΥΝΣΕΙΣ */ printf("Μετά: x=%d, y=%d\n", x, y); return 0; }
Πριν: x=5, y=10 Μετά: x=10, y=5
🔑 Κανόνας: Αν θέλεις μια συνάρτηση να αλλάξει τις τιμές μεταβλητών της main(), πρέπει:
• Στη δήλωση της συνάρτησης: οι παράμετροι να είναι δείκτες → void func(int *a)
• Στην κλήση της συνάρτησης: να περνάμε διευθύνσεις → func(&x)
• Μέσα στη συνάρτηση: να χρησιμοποιούμε *a για να διαβάσουμε/αλλάξουμε τιμή

3.2 Δείκτες και Πίνακες

Στη C, το όνομα ενός πίνακα είναι στην ουσία ένας δείκτης στο πρώτο στοιχείο του πίνακα. Αυτό σημαίνει ότι arr και &arr[0] δείχνουν στην ίδια θέση μνήμης.

#include <stdio.h> int main() { int arr[] = {10, 20, 30, 40, 50}; int *ptr = arr; /* ίδιο με: int *ptr = &arr[0]; */ /* Πρόσβαση μέσω δείκτη */ printf("*ptr = %d\n", *ptr); /* 10 — πρώτο στοιχείο */ printf("*(ptr+1) = %d\n", *(ptr+1)); /* 20 — δεύτερο στοιχείο */ printf("*(ptr+2) = %d\n", *(ptr+2)); /* 30 — τρίτο στοιχείο */ /* Ισοδύναμα: arr[i] == *(arr + i) */ for (int i = 0; i < 5; i++) { printf("arr[%d] = %d | *(ptr+%d) = %d\n", i, arr[i], i, *(ptr + i)); } return 0; }
*ptr = 10 *(ptr+1) = 20 *(ptr+2) = 30 arr[0] = 10 | *(ptr+0) = 10 arr[1] = 20 | *(ptr+1) = 20 arr[2] = 30 | *(ptr+2) = 30 arr[3] = 40 | *(ptr+3) = 40 arr[4] = 50 | *(ptr+4) = 50
✅ Βασικός κανόνας: Η έκφραση arr[i] είναι ακριβώς ισοδύναμη με *(arr + i). Ο compiler μετατρέπει τη μία μορφή στην άλλη εσωτερικά. Η ptr + i δεν προσθέτει i bytes, αλλά i × sizeof(int) bytes, δηλαδή μετακινείται κατά i θέσεις.

3.2.1 Αριθμητική Δεικτών

Η C υποστηρίζει πράξεις σε δείκτες. Όταν προσθέτουμε ή αφαιρούμε έναν ακέραιο σε/από δείκτη, αυτός μετακινείται κατά τόσες θέσεις (όχι bytes):

#include <stdio.h> int main() { int arr[] = {10, 20, 30, 40}; int *p = arr; /* p δείχνει στο arr[0] */ printf("*p = %d\n", *p); /* 10 */ p++; /* p μετακινείται στο arr[1] */ printf("*p = %d\n", *p); /* 20 */ p += 2; /* p μετακινείται στο arr[3] */ printf("*p = %d\n", *p); /* 40 */ p--; /* p πάει πίσω στο arr[2] */ printf("*p = %d\n", *p); /* 30 */ return 0; }
*p = 10 *p = 20 *p = 40 *p = 30

3.3 Πέρασμα Πίνακα σε Συνάρτηση

Όταν περνάμε πίνακα σε συνάρτηση, στην ουσία περνάμε δείκτη στο πρώτο στοιχείο. Γι' αυτό η συνάρτηση μπορεί να αλλάξει τα στοιχεία του πίνακα:

#include <stdio.h> /* * Συνάρτηση: double_array * ------------------------ * Δέχεται: * - έναν δείκτη σε ακέραιο πίνακα (int *arr) * - το μέγεθος του πίνακα (int size) * * Κάνει: * Πολλαπλασιάζει κάθε στοιχείο του πίνακα επί 2. * Επειδή περνάμε τον πίνακα ως δείκτη, οι αλλαγές * επηρεάζουν τον αρχικό πίνακα (όχι αντίγραφο). */ void double_array(int *arr, int size) { for (int i = 0; i < size; i++) { arr[i] *= 2; // Ισοδύναμο με: arr[i] = arr[i] * 2; } } /* * Συνάρτηση: print_array * ----------------------- * Εκτυπώνει όλα τα στοιχεία ενός πίνακα. */ void print_array(int *arr, int size) { for (int i = 0; i < size; i++) { printf("%d ", arr[i]); // Εκτύπωση κάθε στοιχείου } printf("\n"); // Αλλαγή γραμμής μετά την εκτύπωση } int main() { // Δήλωση και αρχικοποίηση πίνακα 5 ακεραίων int nums[] = {1, 2, 3, 4, 5}; printf("Πριν: "); print_array(nums, 5); // Εκτύπωση αρχικού πίνακα double_array(nums, 5); // Περνάμε τον πίνακα στη συνάρτηση. // Ο πίνακας τροποποιείται απευθείας (μέσω δείκτη). printf("Μετά: "); print_array(nums, 5); // Εκτύπωση τροποποιημένου πίνακα return 0; // Επιτυχής τερματισμός προγράμματος }
Πριν: 1 2 3 4 5 Μετά: 2 4 6 8 10
💡 Σημείωση: Η δήλωση void func(int *arr, int size) είναι ισοδύναμη με void func(int arr[], int size). Και οι δύο μορφές δέχονται δείκτη σε πίνακα. Ο compiler τις μεταχειρίζεται με τον ίδιο τρόπο.

3.4 Διαδραστικό Εργαλείο Δεικτών

Εισάγετε τιμή για το x και δείτε πώς λειτουργεί ο δείκτης:

📌 Εξερεύνηση Δεικτών

Εισάγετε τιμή και πατήστε Εκτέλεση...

3.5 Πλήρες Παράδειγμα Δήλωσης & Χρήσης Δεικτών

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

#include <stdio.h> int main() { /* ═══════════════════════════════════════════ ΜΕΡΟΣ 1: Βασική δήλωση δεικτών ═══════════════════════════════════════════ */ int a = 10; // κανονική μεταβλητή int float b = 3.14; // κανονική μεταβλητή float char c = 'A'; // κανονική μεταβλητή char int *pa = &a; // δείκτης σε int → δείχνει στο a float *pb = &b; // δείκτης σε float → δείχνει στο b char *pc = &c; // δείκτης σε char → δείχνει στο c printf("=== ΜΕΡΟΣ 1: Βασική δήλωση ===\n"); printf("a = %d, &a = %p\n", a, &a); printf("*pa = %d, pa = %p\n", *pa, pa); printf("b = %.2f, *pb = %.2f\n", b, *pb); printf("c = %c, *pc = %c\n\n", c, *pc); /* ═══════════════════════════════════════════ ΜΕΡΟΣ 2: Αλλαγή τιμής μέσω δείκτη ═══════════════════════════════════════════ */ printf("=== ΜΕΡΟΣ 2: Αλλαγή μέσω δείκτη ===\n"); printf("Πριν: a = %d\n", a); *pa = 99; // αλλάζουμε το a μέσω του δείκτη pa printf("Μετά: a = %d\n\n", a); // εκτυπώνει 99! /* ═══════════════════════════════════════════ ΜΕΡΟΣ 3: Αλλαγή του δείκτη (νέος στόχος) ═══════════════════════════════════════════ */ int x = 100, y = 200; int *ptr = &x; // ptr δείχνει στο x printf("=== ΜΕΡΟΣ 3: Αλλαγή στόχου δείκτη ===\n"); printf("ptr → x: *ptr = %d\n", *ptr); // 100 ptr = &y; // τώρα ο ptr δείχνει στο y printf("ptr → y: *ptr = %d\n\n", *ptr); // 200 /* ═══════════════════════════════════════════ ΜΕΡΟΣ 4: NULL δείκτης (ασφάλεια) ═══════════════════════════════════════════ */ int *safe_ptr = NULL; // αρχικοποίηση σε NULL printf("=== ΜΕΡΟΣ 4: NULL δείκτης ===\n"); if (safe_ptr == NULL) { printf("Ο δείκτης είναι NULL — δεν δείχνει πουθενά\n"); } safe_ptr = &a; // τώρα δείχνει κάπου ασφαλές if (safe_ptr != NULL) { printf("Τώρα δείχνει στο a: *safe_ptr = %d\n", *safe_ptr); } return 0; }
=== ΜΕΡΟΣ 1: Βασική δήλωση === a = 10, &a = 0x7ffd5a30 *pa = 10, pa = 0x7ffd5a30 b = 3.14, *pb = 3.14 c = A, *pc = A === ΜΕΡΟΣ 2: Αλλαγή μέσω δείκτη === Πριν: a = 10 Μετά: a = 99 === ΜΕΡΟΣ 3: Αλλαγή στόχου δείκτη === ptr → x: *ptr = 100 ptr → y: *ptr = 200 === ΜΕΡΟΣ 4: NULL δείκτης === Ο δείκτης είναι NULL — δεν δείχνει πουθενά Τώρα δείχνει στο a: *safe_ptr = 99

3.6 Δισδιάστατοι Πίνακες & Δείκτες

Ένας δισδιάστατος πίνακας (2D array) είναι στην ουσία ένας «πίνακας από πίνακες» — ένα πλέγμα με γραμμές και στήλες, σαν πίνακα βαθμολογίας ή σκακιέρα.

📊 Παράδειγμα: Ένας πίνακας int matrix[3][4] έχει 3 γραμμές και 4 στήλες — σύνολο 12 θέσεις. Στη μνήμη αποθηκεύεται γραμμή-γραμμή (row-major order), δηλαδή πρώτα η γραμμή 0, μετά η γραμμή 1, κ.ο.κ.

3.6.1 Δήλωση & Αρχικοποίηση

#include <stdio.h> int main() { /* Δήλωση 2D πίνακα 3 γραμμών × 4 στηλών */ int matrix[3][4] = { {1, 2, 3, 4}, // γραμμή 0 {5, 6, 7, 8}, // γραμμή 1 {9, 10, 11, 12} // γραμμή 2 }; /* Εκτύπωση ολόκληρου του πίνακα */ printf("Ο πίνακας 3×4:\n"); for (int i = 0; i < 3; i++) { // γραμμές for (int j = 0; j < 4; j++) { // στήλες printf("%3d ", matrix[i][j]); } printf("\n"); } /* Πρόσβαση σε μεμονωμένο στοιχείο */ printf("\nmatrix[1][2] = %d\n", matrix[1][2]); // 7 printf("matrix[2][0] = %d\n", matrix[2][0]); // 9 return 0; }
Ο πίνακας 3×4: 1 2 3 4 5 6 7 8 9 10 11 12 matrix[1][2] = 7 matrix[2][0] = 9

3.6.2 Πώς Αποθηκεύεται στη Μνήμη

Ο πίνακας matrix[3][4] αποθηκεύεται συνεχόμενα στη μνήμη — γραμμή-γραμμή. Η σχέση δεικτών είναι:

matrix[i][j] == *(*(matrix + i) + j) Στη μνήμη (γραμμή-γραμμή): [1][2][3][4] | [5][6][7][8] | [9][10][11][12] γραμμή 0 | γραμμή 1 | γραμμή 2 Διεύθυνση στοιχείου [i][j] = βάση + (i × στήλες + j) × sizeof(int)
💡 Τι σημαίνει αυτός ο τύπος;

Ο 2D πίνακας αποθηκεύεται στη μνήμη σαν μία συνεχόμενη γραμμή. Ο compiler πρέπει να υπολογίσει πού βρίσκεται κάθε στοιχείο. Ας τον αναλύσουμε κομμάτι-κομμάτι:

βάση: η διεύθυνση του πρώτου στοιχείου [0][0] στη μνήμη
i × στήλες: «πόσα στοιχεία πρέπει να προσπεράσω για να φτάσω στη γραμμή i». Π.χ. αν έχω 4 στήλες και θέλω τη γραμμή 2, προσπερνάω 2×4 = 8 στοιχεία
+ j: μετά μετράω j θέσεις μέσα στη γραμμή για τη στήλη που θέλω
× sizeof(int): πολλαπλασιάζω επί το μέγεθος κάθε στοιχείου σε bytes (4 bytes για int) γιατί η μνήμη μετράει σε bytes, όχι σε στοιχεία

Παράδειγμα: Για τον πίνακα matrix[3][4], πού βρίσκεται το matrix[2][1];
βάση + (2 × 4 + 1) × 4 = βάση + 9 × 4 = βάση + 36 bytes
Δηλαδή: προσπέρνα 2 γραμμές (8 στοιχεία) + 1 στήλη = 9ο στοιχείο × 4 bytes = 36 bytes μετά την αρχή.

3.6.3 Πέρασμα 2D Πίνακα σε Συνάρτηση

Όταν περνάμε δισδιάστατο πίνακα σε συνάρτηση, πρέπει να καθορίσουμε τον αριθμό των στηλών — ο compiler τον χρειάζεται για να υπολογίσει τη διεύθυνση κάθε στοιχείου:

#include <stdio.h> /* * Συνάρτηση: print_matrix * Δέχεται δισδιάστατο πίνακα με 4 στήλες. * ΣΗΜΑΝΤΙΚΟ: ο αριθμός στηλών (4) ΠΡΕΠΕΙ να δηλωθεί! */ void print_matrix(int mat[][4], int rows) { for (int i = 0; i < rows; i++) { for (int j = 0; j < 4; j++) { printf("%3d ", mat[i][j]); } printf("\n"); } } /* * Συνάρτηση: sum_matrix * Υπολογίζει το άθροισμα ΟΛΩΝ των στοιχείων. */ int sum_matrix(int mat[][4], int rows) { int sum = 0; for (int i = 0; i < rows; i++) for (int j = 0; j < 4; j++) sum += mat[i][j]; return sum; } int main() { int grades[3][4] = { {85, 90, 78, 92}, // Φοιτητής 0: βαθμοί 4 μαθημάτων {70, 65, 88, 73}, // Φοιτητής 1 {95, 87, 91, 82} // Φοιτητής 2 }; printf("Πίνακας βαθμών:\n"); print_matrix(grades, 3); printf("\nΆθροισμα όλων: %d\n", sum_matrix(grades, 3)); /* Μέσος όρος κάθε φοιτητή */ for (int i = 0; i < 3; i++) { int row_sum = 0; for (int j = 0; j < 4; j++) row_sum += grades[i][j]; printf("Φοιτητής %d: Μ.Ο. = %.1f\n", i, row_sum / 4.0); } return 0; }
Πίνακας βαθμών: 85 90 78 92 70 65 88 73 95 87 91 82 Άθροισμα όλων: 996 Φοιτητής 0: Μ.Ο. = 86.2 Φοιτητής 1: Μ.Ο. = 74.0 Φοιτητής 2: Μ.Ο. = 88.8
⚠️ Σημαντικό: Στη δήλωση παραμέτρου int mat[][4], η πρώτη διάσταση (γραμμές) μπορεί να παραλειφθεί, αλλά η δεύτερη (στήλες) είναι υποχρεωτική. Χωρίς αυτή, ο compiler δεν μπορεί να υπολογίσει πού βρίσκεται κάθε στοιχείο στη μνήμη.

3.6.4 Πρόσβαση 2D Πίνακα μέσω Δείκτη

Μπορούμε να χρησιμοποιήσουμε δείκτη για να προσπελάσουμε τα στοιχεία ενός 2D πίνακα, εκμεταλλευόμενοι το γεγονός ότι αποθηκεύονται συνεχόμενα στη μνήμη:

#include <stdio.h> int main() { int matrix[2][3] = { {10, 20, 30}, {40, 50, 60} }; /* Δείκτης στο πρώτο στοιχείο (τύπου int) */ int *ptr = &matrix[0][0]; printf("Με δείκτη (γραμμικά στη μνήμη):\n"); for (int i = 0; i < 2 * 3; i++) { printf("*(ptr+%d) = %d\n", i, *(ptr + i)); } printf("\nΑντιστοίχιση [γραμμή][στήλη]:\n"); int rows = 2, cols = 3; for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { /* Τύπος: στοιχείο[i][j] = *(ptr + i*cols + j) */ printf("[%d][%d]=%d ", i, j, *(ptr + i * cols + j)); } printf("\n"); } return 0; }
Με δείκτη (γραμμικά στη μνήμη): *(ptr+0) = 10 *(ptr+1) = 20 *(ptr+2) = 30 *(ptr+3) = 40 *(ptr+4) = 50 *(ptr+5) = 60 Αντιστοίχιση [γραμμή][στήλη]: [0][0]=10 [0][1]=20 [0][2]=30 [1][0]=40 [1][1]=50 [1][2]=60
✅ Βασικός τύπος: Για να προσπελάσουμε το στοιχείο [i][j] ενός 2D πίνακα με δείκτη: *(ptr + i * αριθμός_στηλών + j). Αυτό δουλεύει γιατί ο πίνακας αποθηκεύεται γραμμή-γραμμή στη μνήμη.

3.7 Ασκήσεις Δεικτών

Δοκιμάστε πρώτα μόνοι σας — και μετά ανοίξτε τη λύση!

📝 Άσκηση 1: Εύρεση μέγιστου και ελάχιστου μέσω δεικτών

Γράψτε μια συνάρτηση find_min_max που δέχεται πίνακα ακεραίων και επιστρέφει μέσω δεικτών το μέγιστο και το ελάχιστο στοιχείο.

📝 Άσκηση 2: Αντιστροφή πίνακα με δείκτες

Γράψτε μια συνάρτηση reverse_array που αντιστρέφει τα στοιχεία ενός πίνακα χρησιμοποιώντας δείκτες και τη swap.


3.8 Γιατί Ταξινομούμε;

Η ταξινόμηση (sorting) είναι μία από τις πιο θεμελιώδεις λειτουργίες στην πληροφορική. Σκεφτείτε ότι ψάχνετε ένα όνομα σε τηλεφωνικό κατάλογο — αν τα ονόματα ήταν σε τυχαία σειρά, θα χρειαζόσασταν ώρες! Αλλά επειδή είναι αλφαβητικά ταξινομημένα, βρίσκετε αμέσως αυτό που θέλετε.

📋 Πού χρησιμοποιείται η ταξινόμηση;
• Εμφάνιση αποτελεσμάτων σε φθίνουσα/αύξουσα σειρά (π.χ. βαθμολογίες)
• Αναζήτηση — πολύ πιο γρήγορη σε ταξινομημένα δεδομένα
• Αφαίρεση διπλότυπων, εύρεση μεσαίας τιμής, στατιστικά κ.ά.

3.9 Bubble Sort (Ταξινόμηση Φυσαλίδας)

Ο Bubble Sort είναι ο πιο απλός αλγόριθμος ταξινόμησης. Η ιδέα είναι πολύ εύκολη: διατρέχουμε τον πίνακα επαναληπτικά και συγκρίνουμε κάθε ζεύγος γειτονικών στοιχείων. Αν είναι σε λάθος σειρά, τα αντιμεταθέτουμε. Αυτό επαναλαμβάνεται μέχρι να μην χρειαστεί καμία αντιμετάθεση.

🫧 Γιατί "Φυσαλίδα"; Γιατί σε κάθε πέρασμα, το μεγαλύτερο στοιχείο "ανεβαίνει" στη σωστή θέση — όπως μια φυσαλίδα που ανεβαίνει στην επιφάνεια.

3.9.1 Πώς Λειτουργεί — Βήμα-Βήμα

Ας ταξινομήσουμε τον πίνακα {5, 3, 8, 1, 2}:

Πέρασμα 1: [5 3] 8 1 2 → [3 5] 8 1 2 ← swap! 3 [5 8] 1 2 → 3 [5 8] 1 2 ← ok 3 5 [8 1] 2 → 3 5 [1 8] 2 ← swap! 3 5 1 [8 2] → 3 5 1 [2 8] ← swap! Το 8 στη θέση του! ✅ Πέρασμα 2: (δεν κοιτάμε πια το 8) [3 5] 1 2 |8| → [3 5] 1 2 |8| ← ok 3 [5 1] 2 |8| → 3 [1 5] 2 |8| ← swap! 3 1 [5 2]|8| → 3 1 [2 5]|8| ← swap! Το 5 στη θέση του! ✅ Πέρασμα 3: (δεν κοιτάμε 5,8) [3 1] 2 |5||8| → [1 3] 2 |5||8| ← swap! 1 [3 2]|5||8| → 1 [2 3]|5||8| ← swap! Το 3 στη θέση του! ✅
💡 Γιατί κάθε πέρασμα ελέγχει λιγότερα στοιχεία;

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

Πέρασμα 1: Ελέγχουμε 4 ζεύγη (θέσεις 0–4). Μετά το πέρασμα, το 8 είναι στη θέση 4 ✅
Πέρασμα 2: Ελέγχουμε 3 ζεύγη (θέσεις 0–3). Δεν κοιτάμε το 8 γιατί είναι ήδη σωστά. Μετά, το 5 στη θέση 3 ✅
Πέρασμα 3: Ελέγχουμε 2 ζεύγη (θέσεις 0–2). Δεν κοιτάμε τα 5, 8. Μετά, το 3 στη θέση 2 ✅

Γι' αυτό στον κώδικα γράφουμε j < n - 1 - i — το - i μειώνει τα ζεύγη σε κάθε πέρασμα, αφού τα τελευταία i στοιχεία είναι ήδη ταξινομημένα.

3.9.2 Κώδικας C

#include <stdio.h> // βιβλιοθήκη για printf // Συνάρτηση ταξινόμησης Bubble Sort void bubble_sort(int *arr, int n) { // arr: δείκτης στον πίνακα (διεύθυνση πρώτου στοιχείου) // n: πλήθος στοιχείων του πίνακα for (int i = 0; i < n - 1; i++) { // Κάθε πέρασμα βάζει το μεγαλύτερο στοιχείο // στη σωστή θέση στο τέλος του πίνακα for (int j = 0; j < n - 1 - i; j++) { // Σύγκριση γειτονικών στοιχείων // Τα τελευταία i στοιχεία είναι ήδη σωστά if (arr[j] > arr[j + 1]) { // Αν είναι σε λάθος σειρά, // κάνουμε ανταλλαγή (swap) int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } // Συνάρτηση εκτύπωσης πίνακα void print_array(int *arr, int n) { // Εκτυπώνει όλα τα στοιχεία του πίνακα for (int i = 0; i < n; i++) printf("%d ", arr[i]); // εκτύπωση στοιχείου printf("\n"); // αλλαγή γραμμής } int main() { int arr[] = {64, 25, 12, 22, 11}; // Δήλωση και αρχικοποίηση πίνακα int n = 5; // Μέγεθος πίνακα (αριθμός στοιχείων) printf("Πριν: "); print_array(arr, n); // Εκτύπωση πριν την ταξινόμηση bubble_sort(arr, n); // Κλήση συνάρτησης ταξινόμησης printf("Μετά: "); print_array(arr, n); // Εκτύπωση μετά την ταξινόμηση return 0; // Τερματισμός προγράμματος }
Πριν: 64 25 12 22 11 Μετά: 11 12 22 25 64

3.9.3 Animated Bubble Sort

Δείτε βήμα-βήμα πώς ταξινομείται ο πίνακας {5, 3, 8, 1, 2}:

🫧 Bubble Sort — Βήμα-Βήμα
Σύγκριση γειτονικών στοιχείων — αντιμετάθεση αν χρειάζεται
Βήμα 0
Πατήστε ▶ Επόμενο για να ξεκινήσετε.
⚠️ Πολυπλοκότητα: Ο Bubble Sort κάνει O(n²) συγκρίσεις στη χειρότερη περίπτωση — δηλαδή για πίνακα 1000 στοιχείων, μπορεί να χρειαστεί ~1.000.000 συγκρίσεις! Είναι καλός για εκμάθηση αλλά αργός για μεγάλα δεδομένα.

3.10 Selection Sort (Ταξινόμηση Επιλογής)

Ο Selection Sort είναι ένας ακόμα απλός αλγόριθμος. Η ιδέα: σε κάθε βήμα, βρίσκουμε το ελάχιστο στοιχείο του μη ταξινομημένου τμήματος και το βάζουμε στη σωστή θέση.

📋 Βήμα 1: Εύρεση ελαχίστου

Σαρώνουμε τον μη ταξινομημένο πίνακα και βρίσκουμε το μικρότερο στοιχείο.

🔄 Βήμα 2: Τοποθέτηση

Αντιμεταθέτουμε το ελάχιστο με το πρώτο στοιχείο του μη ταξινομημένου τμήματος.

3.10.1 Πώς Λειτουργεί — Βήμα-Βήμα

Ας ταξινομήσουμε τον πίνακα {29, 10, 14, 37, 13}:

Πέρασμα 1: Ψάχνουμε ελάχιστο σε ΟΛΟΝ τον πίνακα (θέσεις 0–4) [29 10 14 37 13] → ελάχιστο = 10 (θέση 1) swap θέση 0 ↔ θέση 1 [10| 29 14 37 13] ← Το 10 στη θέση του! ✅ Πέρασμα 2: Ψάχνουμε ελάχιστο στις θέσεις 1–4 10 |[29 14 37 13] → ελάχιστο = 13 (θέση 4) swap θέση 1 ↔ θέση 4 [10 13| 14 37 29] ← Το 13 στη θέση του! ✅ Πέρασμα 3: Ψάχνουμε ελάχιστο στις θέσεις 2–4 10 13 |[14 37 29] → ελάχιστο = 14 (θέση 2) ήδη στη σωστή θέση — κανένα swap! [10 13 14| 37 29] ← Το 14 στη θέση του! ✅ Πέρασμα 4: Ψάχνουμε ελάχιστο στις θέσεις 3–4 10 13 14 |[37 29] → ελάχιστο = 29 (θέση 4) swap θέση 3 ↔ θέση 4 [10 13 14 29 37] ← Ταξινομημένος! 🎉
💡 Παρατηρήστε τη διαφορά με τον Bubble Sort:

• Ο Bubble Sort συγκρίνει γειτονικά ζεύγη και κάνει πολλά swaps σε κάθε πέρασμα.
• Ο Selection Sort βρίσκει πρώτα το ελάχιστο και κάνει μόνο 1 swap ανά πέρασμα (ή κανένα, αν ήδη είναι στη σωστή θέση). Γι' αυτό είναι πιο αποδοτικός σε swaps.

3.10.2 Κώδικας C

#include <stdio.h> /* * Η συνάρτηση selection_sort ταξινομεί έναν πίνακα * σε αύξουσα σειρά (από το μικρότερο στο μεγαλύτερο). * * arr = ο πίνακας * n = πόσα στοιχεία έχει ο πίνακας */ void selection_sort(int *arr, int n) { // Πηγαίνουμε θέση-θέση στον πίνακα for (int i = 0; i < n - 1; i++) { // Υποθέτουμε ότι το μικρότερο στοιχείο // βρίσκεται στην τωρινή θέση i int min_idx = i; // Ψάχνουμε στο υπόλοιπο του πίνακα // αν υπάρχει μικρότερο στοιχείο for (int j = i + 1; j < n; j++) { // Αν βρούμε μικρότερο αριθμό, // κρατάμε τη θέση του if (arr[j] < arr[min_idx]) { min_idx = j; } } // Αν βρέθηκε μικρότερο στοιχείο, // κάνουμε ανταλλαγή (swap) if (min_idx != i) { int temp = arr[i]; // κρατάμε προσωρινά το arr[i] arr[i] = arr[min_idx]; // βάζουμε το μικρότερο στη θέση i arr[min_idx] = temp; // βάζουμε το παλιό arr[i] στη θέση του } } } int main() { // Δημιουργούμε έναν πίνακα με 5 αριθμούς int arr[] = {29, 10, 14, 37, 13}; int n = 5; printf("Πριν: "); // Εμφανίζουμε τον πίνακα πριν την ταξινόμηση for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); // Ταξινομούμε τον πίνακα selection_sort(arr, n); printf("Μετά: "); // Εμφανίζουμε τον πίνακα μετά την ταξινόμηση for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); return 0; // Το πρόγραμμα τελείωσε σωστά }
Πριν: 29 10 14 37 13 Μετά: 10 13 14 29 37

3.10.3 Σύγκριση Bubble vs Selection Sort

ΧαρακτηριστικόBubble SortSelection Sort
ΠολυπλοκότηταO(n²)O(n²)
SwapsΠολλά (σε κάθε σύγκριση)Λίγα (1 ανά πέρασμα)
ΣταθερότηταΣταθερός (stable)Ασταθής (unstable)
ΙδέαΣύγκριση γειτονικώνΕύρεση ελαχίστου
Καλύτερος γιαΣχεδόν ταξινομημέναΕλάχιστα swaps

3.11 Γραμμική Αναζήτηση (Linear Search)

Η πιο απλή μέθοδος αναζήτησης: ελέγχουμε ένα-ένα τα στοιχεία του πίνακα μέχρι να βρούμε αυτό που ψάχνουμε ή μέχρι να τελειώσει ο πίνακας.

3.11.1 Κώδικας C

#include <stdio.h> /* Επιστρέφει τη θέση (index) αν βρεθεί, αλλιώς -1 */ int linear_search(int *arr, int n, int target) { for (int i = 0; i < n; i++) { if (arr[i] == target) { return i; /* βρέθηκε στη θέση i */ } } return -1; /* δεν βρέθηκε */ } int main() { int arr[] = {4, 2, 7, 1, 9, 3}; int n = 6; int pos = linear_search(arr, n, 7); if (pos != -1) printf("Βρέθηκε στη θέση %d\n", pos); else printf("Δεν βρέθηκε\n"); pos = linear_search(arr, n, 5); if (pos != -1) printf("Βρέθηκε στη θέση %d\n", pos); else printf("Δεν βρέθηκε\n"); return 0; }
Βρέθηκε στη θέση 2 Δεν βρέθηκε
✅ Πλεονεκτήματα: Η γραμμική αναζήτηση λειτουργεί σε οποιονδήποτε πίνακα — δεν χρειάζεται να είναι ταξινομημένος! Απλή υλοποίηση, αλλά αργή σε μεγάλους πίνακες: O(n).

3.12 Δυαδική Αναζήτηση (Binary Search)

Η δυαδική αναζήτηση είναι πολύ πιο γρήγορη από τη γραμμική, αλλά απαιτεί ο πίνακας να είναι ταξινομημένος. Η ιδέα: κοιτάμε το μεσαίο στοιχείο — αν είναι αυτό που ψάχνουμε, τέλεια! Αν όχι, ψάχνουμε μόνο στο μισό πίνακα.

📖 Παράδειγμα από τη ζωή: Όταν ψάχνετε μια λέξη στο λεξικό, δεν ξεκινάτε από τη σελίδα 1. Ανοίγετε κάπου στη μέση, κοιτάτε αν η λέξη σας είναι πριν ή μετά, και συνεχίζετε μόνο στο κατάλληλο μισό. Αυτό κάνει η δυαδική αναζήτηση!

3.12.1 Πώς Λειτουργεί

Ψάχνουμε το 23 στον πίνακα: [2, 5, 8, 12, 16, 23, 38, 56, 72, 91] Βήμα 1: mid = (0+9)/2 = 4 → arr[4] = 16 < 23 → ψάχνουμε ΔΕΞΙΑ Βήμα 2: mid = (5+9)/2 = 7 → arr[7] = 56 > 23 → ψάχνουμε ΑΡΙΣΤΕΡΑ Βήμα 3: mid = (5+6)/2 = 5 → arr[5] = 23 == 23 → ΒΡΕΘΗΚΕ! θέση 5

3.12.2 Κώδικας C

#include <stdio.h> /* Δυαδική αναζήτηση — ΑΠΑΙΤΕΙ ταξινομημένο πίνακα! Επιστρέφει θέση αν βρεθεί, αλλιώς -1 */ int binary_search(int *arr, int n, int target) { int left = 0; int right = n - 1; while (left <= right) { int mid = (left + right) / 2; if (arr[mid] == target) { return mid; /* βρέθηκε! */ } else if (arr[mid] < target) { left = mid + 1; /* ψάχνουμε δεξιά */ } else { right = mid - 1; /* ψάχνουμε αριστερά */ } } return -1; /* δεν βρέθηκε */ } int main() { int arr[] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91}; int n = 10; int pos = binary_search(arr, n, 23); if (pos != -1) printf("Βρέθηκε στη θέση %d\n", pos); else printf("Δεν βρέθηκε\n"); pos = binary_search(arr, n, 50); if (pos != -1) printf("Βρέθηκε στη θέση %d\n", pos); else printf("Δεν βρέθηκε\n"); return 0; }
Βρέθηκε στη θέση 5 Δεν βρέθηκε

3.12.3 Animated Δυαδική Αναζήτηση

Δείτε πώς η δυαδική αναζήτηση βρίσκει το 23 στον πίνακα:

🔍 Binary Search — Αναζήτηση του 23
Πίνακας: [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
Βήμα 0
Πατήστε ▶ Επόμενο για να ξεκινήσετε.

3.12.4 Σύγκριση Γραμμικής vs Δυαδικής Αναζήτησης

ΧαρακτηριστικόΓραμμική (Linear)Δυαδική (Binary)
ΠολυπλοκότηταO(n)O(log n)
Πίνακας 1.000 στοιχείων~1.000 βήματα~10 βήματα!
Πίνακας 1.000.000 στοιχείων~1.000.000 βήματα~20 βήματα!
Απαιτεί ταξινόμηση;ΟΧΙΝΑΙ
ΥλοποίησηΠολύ απλήΛίγο πιο σύνθετη
✅ Πρακτικός κανόνας: Αν θα κάνετε πολλές αναζητήσεις στα ίδια δεδομένα, αξίζει να ταξινομήσετε πρώτα και να χρησιμοποιείτε δυαδική αναζήτηση. Η αρχική ταξινόμηση κοστίζει O(n²) αλλά κάθε αναζήτηση μετά γίνεται O(log n) αντί O(n).

3.13 Πλήρες Παράδειγμα: Ταξινόμηση + Αναζήτηση

Ας συνδυάσουμε όλα τα παραπάνω σε ένα πρόγραμμα:

#include <stdio.h> /* ─── Bubble Sort ─── */ void bubble_sort(int *arr, int n) { for (int i = 0; i < n-1; i++) for (int j = 0; j < n-1-i; j++) if (arr[j] > arr[j+1]) { int t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t; } } /* ─── Binary Search ─── */ int binary_search(int *arr, int n, int target) { int left = 0, right = n - 1; while (left <= right) { int mid = (left + right) / 2; if (arr[mid] == target) return mid; else if (arr[mid] < target) left = mid + 1; else right = mid - 1; } return -1; } int main() { int grades[] = {85, 42, 97, 63, 71, 55, 88, 30}; int n = 8; /* Βήμα 1: Ταξινόμηση */ printf("Αρχικοί βαθμοί: "); for(int i=0;i<n;i++) printf("%d ",grades[i]); printf("\n"); bubble_sort(grades, n); printf("Ταξινομημένοι: "); for(int i=0;i<n;i++) printf("%d ",grades[i]); printf("\n"); /* Βήμα 2: Αναζήτηση */ int target = 71; int pos = binary_search(grades, n, target); if (pos != -1) printf("Ο βαθμός %d βρέθηκε στη θέση %d\n", target, pos); else printf("Ο βαθμός %d δεν βρέθηκε\n", target); return 0; }
Αρχικοί βαθμοί: 85 42 97 63 71 55 88 30 Ταξινομημένοι: 30 42 55 63 71 85 88 97 Ο βαθμός 71 βρέθηκε στη θέση 4

3.14 Διαδραστικό Εργαλείο Ταξινόμησης

Δοκιμάστε τον Bubble Sort με δικά σας δεδομένα:

🔄 Ταξινόμηση Πίνακα

Εισάγετε αριθμούς και πατήστε Ταξινόμηση...

3.15 Ασκήσεις Ταξινόμησης & Αναζήτησης

📝 Άσκηση 3: Μέτρηση εμφανίσεων

Γράψτε μια συνάρτηση count_occurrences που μετράει πόσες φορές εμφανίζεται ένας αριθμός σε ταξινομημένο πίνακα.

📝 Άσκηση 4: Ταξινόμηση + εύρεση Median

Γράψτε πρόγραμμα που ταξινομεί πίνακα βαθμών και βρίσκει τη μεσαία τιμή (median).

💡 Υπόδειξη: Αν n μονός, median = arr[n/2]. Αν n ζυγός, median = (arr[n/2 - 1] + arr[n/2]) / 2.0

📝 Άσκηση 5: Selection Sort σε φθίνουσα σειρά

Τροποποιήστε τον Selection Sort ώστε να ταξινομεί σε φθίνουσα σειρά (από μεγαλύτερο σε μικρότερο).

💡 Υπόδειξη: Αντί για ελάχιστο, ψάξτε το μέγιστο!