Κατασκευή δένδρου από πίνακα

Βρήκες ή ψάχνεις κάτι ενδιαφέρον για τους τομείς των Μαθηματικών, της Φυσικής ή της Πληροφορικής; Για πέρνα να τα πούμε...

Συντονιστές: kostas213, markelos, Tulis

Απάντηση
Άβαταρ μέλους
drcypher
Portal Administrator
Portal Administrator
Δημοσιεύσεις: 2299
Εγγραφή: Τετ Νοέμ 01, 2006 7:33 am
Real Name: Κώτσος Φίλ
Gender: Male
Τοποθεσία: Μπροστά στην οθόνη

Κατασκευή δένδρου από πίνακα

Δημοσίευση από drcypher »

Λοιπόν, έχουμε το εξής πρόβλημα: Υπάρχει ένας πίνακας που έχει (με τυχαία σειρά) μοναδικά αντικείμενα-ζεύγη αριθμών (i, p), i>=1, p>=0

Το i είναι ο "αύξων αριθμός" (id) του συγκεκριμένου αντικειμένου, και είναι μοναδικό. Το p είναι κάτι σαν δείκτης προς το αντικείμενο-ζεύγος στο οποίο το εν λόγω υπάγεται.

Ζητείται αλγόριθμος που θα παραγάγει ένα δένδρο (έστω σαν ouput, αν και θα μπορούσε να ζητηθεί ως δομή δεδομένων) στο οποίο κάθε αντικείμενο θα βρίσκεται κάτω από το πατρικό του (δηλ. από το αντικείμενο με i=p).

Τα αντικείμενα με p=0 σημαίνει ότι δεν έχουν πατρικό στοιχείο, οπότε θα βρίσκονται στην κορυφή του δένδρου. Κάθε αντικείμενο έχει ΜΟΝΟ έναν γονικό κόμβο p (προφανώς).

Π.χ. έχουμε τα αντικείμενα (20, 53), (10, 0), (2, 10), (3, 0), (53, 2), (4, 2), (9, 3), (15, 53), (7, 4),(13, 10)

Αυτά θα πρέπει να εμφανιστούν ως

Κώδικας: Επιλογή όλων

10
   2
     4
       7
    53
      15
      20
  13
 3
   9
Δηλ. τα 10, 3 είναι στην κορυφή, τα 2,13 ανήκουν στο 10, το 9 στο 3, τα 4,53 ανήκουν στο 2, το 7 στο 4 και τα 15,20 στο 53.

Αυτό θα πρέπει να γίνει για αυθαίρετα μεγάλο input.

Ουσιαστικά πρόκειται για παραγωγή δενδροειδούς δομής σαν το δένδρο καταλόγων ενός filesystem, μόνο που το input είναι της παραπάνω μορφής.

Το πρόβλημα λύνεται (και ίσως σχετικά "εύκολα") με αναδρομή... γίνεται, όμως, να λυθεί με βρόχο;
Από τούδε και στο εξής ως στρογγυλοί αριθμοί ορίζονται τα πολλαπλάσια του 5 και οι δυνάμεις του 2.
Άβαταρ μέλους
drcypher
Portal Administrator
Portal Administrator
Δημοσιεύσεις: 2299
Εγγραφή: Τετ Νοέμ 01, 2006 7:33 am
Real Name: Κώτσος Φίλ
Gender: Male
Τοποθεσία: Μπροστά στην οθόνη

Δημοσίευση από drcypher »

Τελικά, με την βοήθεια "pointers" η δουλειά μπορεί να γίνει με βρόχο. Το πρόβλημα, όμως, είναι ότι εν γένει δε θέλει κανείς πάντα να διαβάζει όλα τα στοιχεία. Αντ' αυτού θα ήταν προτιμότερο να ζητάει κανείς έναν κατάλογο και να του επιστρέφεται μόνο ο κατάλογος και οι υποκατάλογοί του.

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

Η ιδέα είναι η εξής: Χρειάζομαι ένα virtual folder tree. Όλα κι όλα τα folders (σε όποια σχέση μεταξύ τους κι αν βρίσκονται) δεν ξεπερνάνε π.χ. τα 50, δηλ. μιλάμε για μικρό αριθμό, οπότε δεν θα υπάρχει μεγάλη επιβάρυνση σε μνήμη αν προστεθούν κάποια δεδομένα θέσης και διάταξης για κάθε κατάλογο. Επιπλέον, γίνεται περίπου 1 αλλαγή στον πίνακα (π.χ. καταχώρηση, τροποποίηση, διαγραφή directory) για κάθε 10.000 αναγνώσεις.

Συνεπώς αν η περίπτωση του βρόχου είναι ταχύτερη και ευκολότερη, αλλά η μετατροπή σε βρόχο στοιχείζει μέχρι μερικές χιλιάδες φορές περισσότερο από την αναδρομή, τότε νομίζω αξίζει.

Μετά απο μια εγγραφή/τροποποίηση/διαγραφή στο table των folders, η διαδικασία "γραμμικοποίησης" μοιάζει με την παρακάτω. Τουλάχιστον όπως την έχω σκεφτεί, αν γνωρίζετε κάτι καλύτερο είμαι ανοιχτός, αν και το κόστος είναι μικρό, γιατί αυτό εκτελείται σπάνια.

Παρατήρηση: Όπως αναφέρω παραπάνω, η σειρά με την οποία έρχονται τα folders από την είσοδο (π.χ. mysql) είναι τυχαία, συνεπώς δεν μπορεί κανείς να υποθέσει ότι θα έρχονται πρώτα τα πατρικά folders και μετά τα "παιδιά" τους. Γι' αυτό ο πρώτος βρόχος διαβάζει την είσοδο και προετοιμάζει μια στοιχειώδη δενδροειδή διάταξη.

Συγκεκριμένα, τα δεδομένα έρχονται ως εξής:

Κώδικας: Επιλογή όλων

(17,14)
(13,0)
(12,0)
(20,19)
(8,0)
(18,14)
(1,13)
(10,5)
(3,0)
(21,0)
(2,13)
(4,13)
(5,0)
(16,14)
(14,0)
(15,9)
(19,0)
(9,5)
όπου ο πρώτος αριθμό είναι το id του καταλόγου ($int_chlid) και ο δεύτερος είναι ο πατρικός κατάλογος ($int_parent). Τα folder με $int_parent=0 δεν έχουν πατέρα, είναι top-level.

Ο κώδικας είναι σε php

Κώδικας: Επιλογή όλων

$rows =& $tbl_f->rowsSelectAll('*', 'caption');

$arr_folders = array();
$arr_tree = array();
foreach ($rows as $row) {

	$int_parent = $row->parent_ptr->intVal();
	$int_child = $row->id->intVal();

	// Initialize parent, info will be filled as child
	if (!isset($arr_folders[$int_parent])) {
		$arr_folders[$int_parent] = array('ch'=>array());
		$arr_parent =& $arr_folders[$int_parent];

		// Possible tree top
		$arr_tree[$int_parent] =& $arr_parent;

	// Parent is there, just fetch it
	} else
		$arr_parent =& $arr_folders[$int_parent];

	// Create non existent child
	if (!isset($arr_folders[$int_child])) {
		$arr_folders[$int_child] = array(
			'ch'=>array(),
			'it'=>array('caption'=>$row->caption->strVal()));
		$arr_child =& $arr_folders[$int_child];
	// Child was initialized as a parent, fill info
	} else {
		$arr_child =& $arr_folders[$int_child];
		if (!isset($arr_child['it']))
			$arr_child['it'] = array('caption'=>$row->caption->strVal());

		// Not a possible top
		unset($arr_tree[$int_child]);
	}

	$arr_parent['ch'][$int_child] =& $arr_child;
}
Μετά απ' αυτόν τον κώδικα έχουν δημιουργηθεί τα παρακάτω:
1. Ένας πίνακας $arr_folder για κάθε κατάλογο όπου έχει τις πληροφορίες κάθε καταλόγου (id, caption)
2. Ένα δένδρο (σαν php hash) που περιέχει εμφωλευμένα τα folders ως εξής:

Κώδικας: Επιλογή όλων

[0]
      [13]
             [1]
             [2]
             [4]
      [12]
      [8]
      [3]
      [21]
      [5]
             [10]
             [9]
                    [15]
      [14]
             [17]
             [18]
             [16]
      [19]
             [20]
Όπως φαίνεται μέχρι εδώ, καταφέραμε με έναν βρόχο να βάλουμε στη θέση τους όλα τα στοιχεία. Αλλά για να γίνει αυτό έπρεπε να διαβαστεί όλη η είσοδος. Το μείζον ερώτημα, εδώ, είναι το εξής: Τι δεδομένα θα μπορούσε να προσθέσει κανείς σε κάθε folder ώστε να μπορεί με ένα απλό ερώτημα να διαβάζει απ' ευθείας από την είσοδο μόνο αυτό και τα folder που βρίσκονται από κάτω τους;

Μια ιδέα που έχω είναι αυτή: Εκτός από το id και τον πατέρα (και όποια άλλα στοιχεία υπάρχουν, π.χ. caption, κτλ., μπορούν να προστίθενται τρεις ακόμη αριθμοί για κάθε folder:
1. Ένας αριθμός διάταξης (order)
2. ο αριθμός των υποκαταλόγων κάθε folder
3. Το επίπεδο nesting του κάθε καταλόγου

Για να προστεθούν, βέβαια, αυτά τα στοιχεία, καταλήγουμε εν τέλει σε αναδρομή, καθώς πρέπει να διαβαστεί το δένδρο που κατασκευάσαμε. Ο κώδικας είναι ο παρακάτω:

Κώδικας: Επιλογή όλων

function int_tree($arr_folder, $int_level = 0, $int_order = 0)
{
	global $arr_chain;

	// Normal folder, print it, then hangin nodes, next level
	if (isset($arr_folder['it'])) {
		$i = 1;
		$arr_chain[] =& $arr_folder;
		$arr_folder['it']['level'] = $int_level;
		$arr_folder['it']['order'] = $int_order;
	// Semi folder, print hanging nodes, same level
	} else
		$i = 0;

	$int_order += $i;

	// Count subfolders
	foreach ($arr_folder['ch'] as $arr_subfolder) {
		$int_order = int_tree($arr_subfolder, $int_level + $i, $int_order);
	}

	if (isset($arr_folder['it'])) {
		$arr_folder['it']['sub'] = $int_order - $arr_folder['it']['order'] - 1;
//		print(str_repeat('    ', $int_level));
//		print($arr_folder['it']['caption']."\n");
	}

	return $int_order;
}

$arr_chain = array();
$int_order = 1;
foreach($arr_tree as $arr_topfolder) {
	$int_order = int_tree($arr_topfolder, 0, $int_order);
}
Μετά απ' αυτό, το δένδρο που είχαμε προηγουμένως, έχει επιπλέον τα εξής στοιχεία:

Κώδικας: Επιλογή όλων

[0]
      [13]  (order: 1 / children: 3 / level: 1)
             [1]   (order: 2 / children: 0 / level: 2)
             [2]   (order: 3 / children: 0 / level: 2)
             [4]   (order: 4 / children: 0 / level: 2)
      [12]  (order: 5 / children: 0 / level: 1)
      [8]   (order: 6 / children: 0 / level: 1)
      [3]   (order: 7 / children: 0 / level: 1)
      [21]  (order: 8 / children: 0 / level: 1)
      [5]   (order: 9 / children: 3 / level: 1)
             [10]  (order:10 / children: 0 / level: 2)
             [9]   (order:11 / children: 1 / level: 2)
                    [15]  (order:12 / children: 0 / level: 3)
      [14]  (order:13 / children: 3 / level: 1)
             [17]  (order:14 / children: 0 / level: 2)
             [18]  (order:15 / children: 0 / level: 2)
             [16]  (order:16 / children: 0 / level: 2)
      [19]  (order:17 / children: 1 / level: 1)
             [20]  (order:18 / children: 0 / level: 2)
Όπως φαίνεται ο αριθμός διάταξης (order) δείχνει την θέση στην οποία θα πρέπει να εμφανιστεί (ή να διαβαστεί, αν υπάρχει ταξινόμηση) κάθε folder. Π.χ. το [13] (order: 1) θα πρέπει να εμφανιστεί πριν από τα [1], [2], [4] (order: 2, 3, 4 αντίστοιχα).

Επιπλέον, ο αριθμός των παιδιών (children ή subfolders) δείχνει τον *συνολικό* αριθμό των subfolders, ανεξαρτήτως βάθους, και όχι τα αμέσως επόμενου επιπέδου παιδιά του. Π.χ., ο [13] έχει 3 children, προφανές. Από την άλλη ο [5] έχει 2 παιδιά να κρέμονται από κάτω του, αλλά το ένα από αυτά (το [9]) έχει και αυτό άλλο ένα. Οπότε συνολικά κάτω από τον [5] υπάρχουν 3 παιδιά.

Τέλος, το level δείχνει πόσο μέσα θα πρέπει να εμφανίζεται το κάθε folder. Εν ολίγοις προστέθηκαν στην δομή τα στοιχεία προβολής. Και εφ' όσον αυτό που κάνεις συνέχεια είναι να προβάλλεις, η πραγματική δομή δε σε ενδιαφέρει τελικά εκείνη την ώρα.

Υπέθεσα όταν το έγραφα ότι αυτό θα είχε ένα πλεονέκτημα. Π.χ., θέλω να ζητήσω από την MySQL τον κατάλογο 5 και όλους τους υποκαταλόγους του, ανεξαρτήτως βάθους. Ένα κριτήριο είναι το εξής:

Θέλω τον [5]. Έχει order 9. Θέλω επίσης όλους τους υποκαταλόγους του, δηλ. τους [10], [9], [15]. Αυτά έχουν orders 10, 11, 12. Δηλ. το order του [5] συν τον αριθμό των children του, δηλ. 3.

Οπότε το query θα ήταν "Δώσε μου όλα τα folders με order ανάμεσα σε 9 και 9+3".

Αυτό, όμως, προϋποθέτει ότι έχεις κάνει ένα ερώτημα για να διαβάσεις το order και το children του [5].

Οπότε το τελικό ερώτημα είναι: Πως γίνεται με ένα και μόνο query να πάρεις και τον [5] και τους υποκαταλόγους του;
Από τούδε και στο εξής ως στρογγυλοί αριθμοί ορίζονται τα πολλαπλάσια του 5 και οι δυνάμεις του 2.
Άβαταρ μέλους
sparc
Δημοσιεύσεις: 391
Εγγραφή: Τετ Νοέμ 01, 2006 9:46 am
Real Name: Γιώργος
Gender: Male
Τοποθεσία: Ε204_κ.Φυσικής!!!

Δημοσίευση από sparc »

Λοιπόν, στα γρήγορα μπορώ να σκεφτώ μέθοδο με 3 πεδία {x,y,i}
τα x,y συντεταγμένες, το i auto_increment unique id.
Συγκεκριμένα το χ είναι η ρίζα του (υπό)δέντρου στο οποίο είναι (σε κάποιο βάθος) κλαδί το i, αταξινόμητο εν γένει αφου είναι μεταβλητό χωρίς κάποιο πρότυπο. Προσοχή είναι η ρίζα δλδ το πρώτο στοιχείο και όχι ο πατέρας!!!
Το y αντιστοιχεί στο βάθος και είναι γενικό ως προς το σύνολο και όχι ειδικό ως προς το σιγκεκριμένο υποδέντρο δλδ αντιστοιχεί σε βάθος ως προς το στοιχείο 0 όποιο και αν είναι αυτό.

Ως προς την λογική ο αλγόριθμος είναι απλός. Στην αρχή λαμβάνεις τα στοιχεία ταξινομημένα ως προς y.

Κώδικας: Επιλογή όλων

 SELECT x,y,z FROM table ORDER BY y;
Ως αποτέλεσμα τα πρώτα στοιχεία (με ίδια, ίσως 0 τιμή) είναι αναγκαστικά ρίζες αφού δεν υπάρχει κάτι σε μικρότερο βάθος, το αμέσως επόμενο είναι το πρώτο παιδί σε κάποιο από τα προϋπάρχοντα κλαδιά-ρίζες κ.ο.κ.

Για την εμφάνηση στην οθόνη μπορεί να γίνει χρήση πίνακα ή css χωρίς πρόβλημα αφού όλα γίνονται σε στήλες, ως προς το βάθος y.

Όταν γίνει αλλαγή θέσης σε ένα στοιχείο Σ(χ1,y1,i1) ξέρουμε τον πατέρα Π(χ2,y2,i2) που αυτό θα έχει στη νέα θέση. Οπότε θα έχουμε ως νέο χ το χ2 του πατέρα, το νέο y θα είναι (το y2 του πατέρα + 1) και το i είναι πάντα σταθερό i1.

Όταν ζητηθεί ανάκτηση υποδέντρου μπορεί να γίνει με ένα σύνθετο query διασταύρωσης (δλδ query me subqueries)

Κώδικας: Επιλογή όλων

SELECT x,y,i FROM tree WHERE x = (SELECT x as x2 FROM tree WHERE i = i2) AND y > (SELECT y as y2 FROM tree WHERE i = i2) ORDER BY y

όπου το i2 είναι το input της μεθόδου, το ID του πατέρα κλαδιού όπου θα μπει το στοιχείο.
υπάρχει τρόπος τα 2 subqueries να γίνουν ένα (με allias) αλλά αυτή τη στιγμή μου διαφεύγει, επίσης η mySql κάνει caching οπότε ουσιαστικά θα εκτελεστούν μία φορά ωστόσο παραμένει ελάχιστα πιο αργό από την περίπτωση του allias.

Επίσης όλα αυτά θα μπορούσαν να γίνουν με κάποιον ΑΔΤ κατευθείαν σε php αλλά αν υπάρχουν αρκετά requests ταυτόχρονα η κατανάλωση σε RAM θα είναι μεγάλη οπότε προσωπικά θα το απέφευγα. H mysql μπορεί να τα βγάλει πέρα με άνω όριο μερικές δεκάδες requests ταυτόχρονα.

Στον αντίποδα, ο ιδανικότερος τρόπος είναι με AJAX και σταδιακή ανάπτυξη του δέντρου, έχω γράψει παλαιότερα ένα παρόμοιο, αρκετά optimized & advanced (επεμβαίνει δραστικά στο memory allocation για να βελτιστοποιήσει το response time), αλγόριθμο, δυστυχώς local αλλά μπορεί να μεταφραστεί άνετα σε php και Javascript εφόσον τα δεδομένα που ανταλλάσονται είναι μικρά strings και smallint αριθμοί. Ωστόσο υπάρχουν ειδικές περιπτώσεις.

Η μορφή σταδιακής ανάπτυξης ευνοεί συνθήκες έντονης διακλάδωσης με μικρό αριθμό παιδιών ανά κλαδί.
Αντιθέτως αν η πλιοψηφία των κλαδιών είναι κατανεμημένη σε μικρό αριθμό κλαδιών και το συνολικό βάθος είναι μικρό τότε ευνοϊκότερη είναι η εξ αρχής ολική ανάπτυξη του δέντρου.
I think therefore I am? Could be! Or is it really someone else who thinks he's me?
Reymond Smullyan - This book needs no title
Στενή είναι η αρετή, δεν μπορώ να αναπνεύσω· μικρός, στενός είναι ο Παράδεισος, δε με χωράει· σαν άνθρωπος μου φαίνεται ο Θεός σας, δεν τον θέλω!
Ν. Καζαντζάκης - Ασκητική
Απάντηση

Επιστροφή στο “Ζητήματα Μαθηματικών - Φυσικής - Πληροφορικής”