User Tools

Site Tools


Sidebar

1. บทนำ

2. การวิเคราะห์ Algorithm

3. List, Stack and Queue

4. Tree

5. Hashing

6. Priority Queues

7. การจัดเรียง (Sorting)

8. The Disjoint Set

9. Graph Algorithms

dsa:tpreliminaries

4.1 Preliminaries

การกำหนดนิยามให้แก่ tree สามารถทำได้หลายวิธี แต่วิธีที่ดูจะเป็นธรรมชาติคือการกำหนดนิยามแบบ recursive กล่าวคือ tree เป็น collection ของโนด (nodes) ซึ่ง collection ดังกล่าวนี้อาจเป็น collection ว่าง (empty) หรือมิฉะนั้น tree ก็จะประกอบด้วยโนดพิเศษหนึ่งโนด (ให้เป็นโนด r) และเรียกโนดนี้ว่า โนดราก (root) และอาจไม่มีหรือมีทรีย่อย (subtree) ในกรณีที่ถ้ามันไม่เป็น tree ว่าง มันก็จะประกอบด้วยทรี (ซึ่งเป็นทรีย่อย) $T1, T2, . . . , Tk$, โดยที่แต่ละทรีย่อยก็จะมีรากของมันเองที่เชื่อมต่อกับ r โดยตรงด้วยตัวเชื่อมที่เรียกว่า edge

โนดรากของทรีย่อยเรียกว่าเป็น child ของ r, และ r เป็น parent ของรากแต่ละรากของทรีย่อย รูปที่ 4.1 แสดงรูปของทรีที่ได้จากนิยามแบบ recursive ที่กล่าวมานี้


รูปที่ 4.1 รูปทรี

เส้นทาง (path) จากโนด n1 ถึง nk กำหนดได้ด้วยลำดับของโนด $n_1, n_2, ... , n_k$ ที่ $n_i$ เป็น parent ของ $n_{i+1}$ เมื่อ $1 < i < k$ และความยาว (length) ของเส้นทาง (path) ดังกล่าวนี้คือ จำนวน edges ของเส้นทางนั้น ซึ่งเท่ากับ k – 1 ความยาว (length) ของเส้นทางจากโนดใด ๆ ไปยังตัวมันเองมีค่าเป็นศูนย์ เส้นทางจากราก (root) ไปยังโนดใด ๆ โนดหนึ่งมีเพียงเส้นทางเดียวเท่านั้น

ความลึก (depth) ของโนด ni ใด ๆ คือ ความยาวของเส้นทางจากโนดรากไปยังโนด ni นั้น ๆ ดังนั้นโนดรากจึงมีค่า depth เป็น 0 (ศูนย์) ความสูง (height) ของโนด ni ใด ๆ คือ เส้นทางที่ยาวที่สุดจากโนด ni ไปยังโนด leaf ใด ๆ ดังนั้น ทุก ๆ leaves จึงมีความสูง (height ) เป็น 0 (ศูนย์) ความสูงของทรีมีค่าเท่ากับความสูงของโนดราก


รูปที่ 4.2 รูปทรี

สำหรับทรีในรูปที่ 4.2, โนด E มี depth เท่ากับ 1,และมี height = 2; โนด F มี depth = 1 และมี height = 1; ค่า height ของทรี คือ 3 ค่า depth ของทรีมีค่าเท่ากับค่า depth ของโนดที่เป็น leaf ที่อยู่ลึกที่สุดซึ่งจะเท่ากับค่าของ height ของทรีเสมอ

ถ้ามีเส้นทางจากโนด n1 ไปยัง n2 เรียกว่า n1 เป็น ancestor ของโนด n2 และโนด n2 เป็น descendant ของโนด n1 ถ้าโนด n1 $\neq$ n2 ก็จะเรียกโนด n1 ว่าเป็นโนด proper ancestor ของโนด n2 และโนด n2 เป็นโนด proper descendant ของโนด n1

4.1.1 Implementation of Trees

เนื่องจากเราไม่สามารถทราบล่วงหน้าได้ว่าโนดแต่ละโนดจะมีโนดลูกจำนวนเท่าใด ดังนั้นเราจึงไม่สามารถสร้างโครงสร้างโนดที่ประกอบด้วยค่าข้อมูลและเส้นเชื่อม (link) ไปยังโนดลูกได้ เพื่อแก้ปัญหานี้ด้วยการกำหนดให้ลูกของโนดใด ๆ โนดหนึ่งอยู่ linked list ของโนด รูปที่ 4.3 แสดงการประกาศโนดของทรี

class TreeNode 
{
  Object 	  element;
	TreeNode  firstChild;
	TreeNode  nextSibling;
};

รูปที่ 4.3 การประกาศโนดของทรี

รูปที่ 4.4 แสดงรูปแบบของโครงทรีของทรีในรูปที่ 4.2 ที่เกิดจากนิยามของทรีจากรูปที่ 4.3 ลูกศรที่ชี้ลงด้านล่างคือเส้นเชื่อม firstChild links ส่วนเส้นลูกศรในแนวนอนคือเส้นเชื่อม nextSibling links ในรูปไม่ได้แสดง null links (มีจำนวนมาก) เอาไว้ด้วย


รูปที่ 4.4 First child/next sibling ของทรีในรูปที่ 4.3

4.1.2 Tree Traversals with an Application

มีการประยุกต์ใช้ทรีในงานต่าง ๆ จำนวนมาก ที่นิยมใช้กันมากอย่างหนึ่งคือในโครงสร้าง directory ในระบบปฏิบัติการของเครื่องคอมพิวเตอร์ (computer operating systems) ต่าง ๆ เช่นใน UNIX, VAX/VMS, และ DOS รูปที่ 4.5 แสดงรูปแบบทั่วไปของระบบ file ของ UNIX


รูปที่ 4.5 รูปแบบทั่วไปของระบบ file ของ UNIX

โนดรากของไดเร็กทอรีคือ /usr (เครื่องหมายดอกจันที่ปรากฏที่ชื่อหมายความว่ามันเป็นไดเร็กทอรี) ใน /usr มีโนดลูก 3 โนดซึ่งล้วนแต่เป็นไดเร็กทอรีและไม่มีโนดลูกที่เป็นแฟ้มปกติ ชื่อแฟ้ม /usr/mark/book/ch1.r เข้าถึงได้หรือได้มาโดยท่องไปทางลูกตัวซ้าย 3 ครั้ง เครื่องหมาย / หมายถึง edge ของทรี

สมมติเราต้องการพิมพ์รายการชื่อแฟ้มทั้งหมดในไดเร็กทอรีโดยรูปแบบที่ต้องการ คือ แฟ้มที่อยู่ที่ระดับความลึก di จะถูกพิมพ์ให้ย่อหน้าเข้าไปด้วยระยะเว้น di ด้วย pseudocode ของอัลกอริทึมแสดงในรูปที่ 4.6 หัวใจของอัลกอริทึมคือฟังก์ชัน listAll ที่เป็นแบบ recursive ฟังก์ชัน listAll ต้องเริ่มต้นทำงานด้วยค่า depth= 0 ซึ่งจะไม่มีการย่อยหน้าสำหรับโนดราก

private	void listAll(int dept)
{
	/* 1 */	 	printName( dept );	// Print the name
	/* 2 */		if ( isDirectory() )
	/* 3 */		    for each file c in this directory
	/* 4 */		         c.listAll( dept + 1 );
}
 
public void listAll()
{
	listAll( 0 ); 	
}	

รูปที่ 4.6 pseudocode ของอัลกอริทึมแสดงรายการแฟ้มในระบบแฟ้ม

ถ้ารายการที่อ่านเป็นไดเร็กทอรีก็จะให้มีการทำงานประมวลผล children ทั้งหมดในแบบrecursive ทีละตัวเสียก่อน children เหล่านี้อยู่ลึกลงไปหนึ่งระดับดังนั้นจึงต้องมีการย่อหน้า ผลที่ได้แสดงดังรูปที่ 4.7

/usr
   mark
     book
       ch1.r
       ch2.r
       ch3.r
     courses
       cop3530
         fall88
           syl.r
         spr89
           syl.r
         sum89
           syl.r
     junk.c
   alex
     junk.c
   bill
     work
     course
       cop3212
         fall88
           grade
           prog1.r
           prog2.r
         fall89
           prog1.r
           prog2.r
           grade

รูปที่ 4.7 ผลที่ได้จากการแสดงรายการไดเร็กทอรีแบบ preorder

การท่องไป (traversal) ในทรีด้วยวิธีการแบบนี้เรียกว่า preorder traversal การท่องไปในทรีแบบนี้จะทำงานโนดนั้น ๆ ก่อนที่จะไปทำงานต่อไปที่โนดลูกของมัน จากโปรแกรมจะเห็นว่ามีการทำงานของบรรทัด 1 หนึ่งครั้งต่อหนึ่งโนด, เนื่องจากมีการส่งออก output โนดละเพียงครั้งเดียว เนื่องจากบรรทัดที่ 1 ทำงานหนึ่งครั้งต่อหนึ่งโนดดังนั้นบรรทัดที่ 2 ก็จะทำงานหนึ่งครั้งต่อโนดเช่นกัน บรรทัดที่ 4 ทำงานมากที่สุดเพียงครั้งเดียวสำหรับโนดลูกแต่ละโนดของแต่ละโนดนั้น ๆ แต่จำนวน children จะมีจำนวนน้อยกว่าจำนวนโนดอยู่ 1 สุดท้ายจะมีการทำงานในลูป 1 รอบต่อการทำงานของบรรทัดที่ 4, รวมกับการทำงานหนึ่งครั้งเมื่อจบลูป ดังนั้นงานที่ทำทั้งหมดจึงเป็นค่าคงที่สำหรับแต่ละโนด ถ้ามีชื่อแฟ้ม $N$ ชื่อที่ต้องพิมพ์รายงานออกจึงต้องใช้ running time เป็น $O(N)$

การท่องไปใน tree อีกแบบที่ใช้บ่อย คือ postorder ใน postorder traversal จะมีการทำงานที่โนดนั้น ๆ ภายหลัง (post) หลังจากการทำงานที่โนดลูกทั้งหมดของมันก่อน รูปที่ 4.8 เป็นโครงสร้างไดเร็กทอรีเดียวกับตัวอย่างก่อนหน้า แต่ได้แสดงตัวเลขจำนวน disk blocks ที่ใช้เอาไว้ในวงเล็บด้วยเท่านั้น

ถ้าเราต้องการคำนวณจำนวน blocks ทั้งหมดที่ใช้โดยแฟ้มทั้งหมดในทรี วิธีการปกติคือหาจำนวน blocks ใน subdirectories ก่อน และจะได้จำนวน blocks ทั้งหมดเท่ากับจำนวนบล็อกทั้งหมดใน subdirectories บวกกับบล็อกที่ใช้ของ directory นั้น รูปที่ 4.9 แสดง pseudocode ที่ทำงานตามวิธีที่กล่าวมานี้


รูปที่ 4.8 โครงสร้างไดเร็กทอรีเดียวกับตัวอย่างก่อนหน้า แสดงตัวเลขจำนวน disk blocks ที่ใช้เอาไว้ในวงเล็บ

public int size()
{
/*1*/ 	int totalSize = sizeOfThisFile;
/*2*/    	if ( isDirectory() )
/*3*/       	for each file c in this directory;
/*4*/     			totalSize += c.size();
/*5*/ 	return( totalSize ); 
}

รูปที่ 4.9 pseudocode เพื่อคำนวณขนาดของบล็อก

ถ้า object ขณะนั้นไม่ใช่ไดเร็กทอรีก็ให้ค่ากลับเป็นจำนวน blocks ที่ใช้ มิฉะนั้น (ก็จะเป็น directory) ก็จะรวมจำนวนบล็อกที่ใช้โดยไดเร็กทอรีเข้ากับจำนวนบล็อก (recursively) ที่ใช้ในโนดลูกทั้งหมด รูปที่ 4.10 แสดงขนาดของไดเร็กทอรีหรือแฟ้มที่ได้จากอัลกอริทึมในรูปที่ 4.9

            ch1.r             3
            ch2.r             2
            chr.3	            4
        book	               10
                  syl.r	      1
               fall88	        2
                   syl.r	    5
                spr89	        6
                   syl.r	    2
                sum89	        3
            cop3530	         12
        course	             13
        junk.c	              6
    mark	                   30
        junk.c	              8
    alex	                    9
        work	                1
                    grades	  3
                    prog1.r	  4
                    prog2.r	  1
                fall88	      9
                    prog2.r	  2
                    prog1.r	  7
                    grades	  9
                fall89	     19
            cop3212	         29
        course	             30
    bill	                   32
/usr	                       72

รูปที่ 4.10 ขนาดของไดเร็กทอรีจากฟังก์ชัน size()

dsa/tpreliminaries.txt · Last modified: 2021/09/08 21:41 by wasu

Page Tools