ดาวน์โหลดงานนำเสนอ
งานนำเสนอกำลังจะดาวน์โหลด โปรดรอ
1
List, Stack, Queue 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
2
List ก็คือมีอะไรมาเรียงกันนั่นแหละ เช่น A1,A2,A3,…,An ตัวแรก
ตัวสุดท้ายของลิสต์ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
3
เราควรจะทำอะไรกับ list ได้บ้าง
Find - หาตำแหน่งที่อยู่ของสมาชิกตัวหนึ่งๆ Insert – ใส่สมาชิกใหม่ลงในตำแหน่งที่กำหนด findKth – รีเทิร์นสมาชิกตัวที่ k Remove – เอาสมาชิกที่กำหนดออกจากลิสต์ Head – รีเทิร์นสมาชิกตัวแรกในลิสต์ Tail – รีเทิร์นลิสต์ที่เอาสมาชิกตัวแรกออกไปแล้ว Append - เอาลิสต์สองลิสต์มาต่อกัน 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
4
อะไรจะเกิด ถ้าใช้อาร์เรย์ทำ list
อย่าลืมว่า ต้องบอกขนาดอาร์เรย์ล่วงหน้า Find ใช้เวลา O(n) Find นั้นต้องหาเรียงตัว นับจากตัวแรกของอาร์เรย์ findKth ใช้เวลาคงที่ เพราะใช้ index หาได้ทันที Insert กับ remove จะใช้เวลานาน เพราะอาจต้องเลื่อนทุกๆสมาชิกในอาร์เรย์ ตัวอย่างของการใช้เวลาตอน insert -- ถ้าเราใส่สมาชิกไปที่หัวอาร์เรย์ ส่วนอื่นๆในช่องถัดไปก็ต้องเลื่อนช่องหมด ทำให้กินเวลามาก การ delete ก็เช่นเดียวกัน ถ้าเราเอาส่วนหัวอาร์เรย์ออก ส่วนอื่นๆก็ต้องเลื่อนขึ้นมาทุกๆช่อง 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
5
Linked list A1 A3 A2 node Find ใช้เวลา O(n) เหมือนเดิม เพราะยังต้องหาเรียงตัว นับจากตัวแรก findKth(i) ใช้เวลา O(i) เพราะต้องหาเรียงตัว Linked list คือมี object (เรียกอีกอย่างว่า node) ที่เก็บค่าหรือ object ตัวอื่นไว้ แล้วมี reference ต่อกันเป็นทอดๆ ตัวสุดท้ายจะไม่มี reference ไปถึงอะไร (เรียกอีกอย่างว่า มี reference เป็น null) 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
6
Linked list (ต่อ) การลบของออกจากลิสต์ทำได้ง่ายขึ้น (ไม่ต้องเลื่อนสมาชิก) แค่เอา reference ข้ามตัวที่ต้องการลบไป เช่น เดิม A1 A3 A2 เมื่อลบ A2 ออก การเปลี่ยน pointer (หรือ reference) เพื่อลบ A2 ออก ทำได้ดังนี้ เก็บตัว pointer ที่ชี้จาก A2 ไปเอาไว้ในตัวแปร แล้วให้ A2 ชี้ไปที่ null (จริงๆไม่ต้องให้ A2 ชี้ไปที่ null ก็ได้เพราะ pointer นี้ก็ไม่ทำให้เข้าถึง A2 ได้อยู่ดี) เอา pointer ที่ชี้ไปที่ A2 เปลี่ยนไปเป็นตัวแปรที่เก็บไว้ ถ้าเอา A2 ออกแล้ว เมื่อไม่มี reference เข้าถึง A2 ได้อีก Java จะลบ A2 ไปเองเพราะมี garbage collection A1 A2 A3 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
7
Linked list (ต่อ 2) การ insert ของใส่ลิสต์ ทำได้ง่ายขึ้นเช่นกัน เดิม
เมื่อใส่ x ระหว่าง A2 กับ A3 A1 A3 A2 x การ insert x ก็จะใช้การเปลี่ยน pointer (หรือ reference) เอา pointer จาก A2 ชี้ไปที่ x เอา pointer จาก x ชี้ไปที่ A3 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
8
การ insert นั้นมีปัญหานิดหน่อย
เพราะไม่มี node ตัวอื่นชี้มาที่ x ตัวที่ต้องปรับคือ reference ที่ชี้ไปที่ A1 ให้ชี้ที่ x A1 A3 A2 x ถ้าเราทำการ insert ของลงไปข้างหน้าโนดแรก หลักการยังเหมือนเดิม ต่างกันตรงรายละเอียด เนื่องจากไม่มี node ที่อยู่ก่อน A1 ดังนั้นจึง set pointer จากโนดนั้นไม่ได้ แต่เราสามารถตั้งค่า pointer จาก x มาได้ แล้วจึงตั้งค่า pointer จากหัวลิสต์ A1 มาที่ x เพื่อให้เป็นหัวลิสต์ใหม่ จริงๆแล้วการเอาของออกจากหัวลิสต์ก็จะทำให้ต้องโค้ดในกรณีพิเศษเหมือนกัน ต้องโค้ดต่างไปนิดหน่อยเป็นกรณีพิเศษ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
9
การหลีกเลี่ยงกรณีพิเศษ
ให้มีหัวลิสต์ปลอม เรียกว่า header หรือ dummy node ถ้าทำอย่างนี้ ทุกๆโนดก็จะมีโนดนำหน้า ทำให้การโค้ดเหมือนกันหมด A1 A3 A2 header ลิสต์ว่าง 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
10
โค้ดของ node class ListNode { // Constructors
ListNode( Object theElement ) this( theElement, null ); } ListNode( Object theElement, ListNode n ) element = theElement; next = n; theElement หน้านี้แสดง constructor โดยมีตัวที่สองเป็นหลัก ตัวแรกเมื่อถูกเรียกใช้ก็จะไปเรียกตัวที่สองอีกต่อหนึ่ง เพียงแค่ให้ตัวที่ชี้ไปโนดถัดไปชี้ไปที่ null แทน n ที่ชี้ไปคือโนดอีกตัวนั่นเอง ตัว instance variable อยู่หน้าถัด ชี้ไป n theElement 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
11
// Friendly data; accessible by other package routines Object element;
ListNode next; } Instance variables 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
12
List iterator คือ object ที่ชี้ไปยังโนดที่เราสนใจในลิสต์
ทำไมต้องเขียนคลาสนี้แยกจากลิสต์ ในเมื่อเราก็เก็บตำแหน่งที่สนใจไว้ในลิสต์ได้เองนี่นา คำตอบคือ เราจะได้เก็บตำแหน่งที่สนใจได้หลายตำแหน่ง ถ้าเราเก็บบนลิสต์เลย ก็คงต้องมีการจำกัดจำนวน แต่เมื่อเราแยกเก็บ ก็ทำให้มีกี่ตำแหน่งที่สนใจก็ได้ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
13
โค้ดของ iterator public class LinkedListItr {
ListNode current; // ตำแหน่งปัจจุบันที่เราสนใจ /** theNode โนดไหนในลิสต์ก็ได้ */ LinkedListItr( ListNode theNode ) current = theNode; } หน้านี้เป็น constructor ของ iterator โดยเราสร้าง iterator ขึ้นมาให้ชี้ไปยังโนดที่เราสนใจทันที 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
14
/** ดูว่า current เลยท้ายลิสต์ไปหรือยัง
true ถ้า current เป็น null */ public boolean isPastEnd( ) { return current == null; } /** item ที่เก็บไว้ใน current หรือไม่ก็ null ถ้าตำแหน่งของ * current ไม่ได้อยู่ในลิสต์ public Object retrieve( ) return isPastEnd( ) ? null : current.element; 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
15
* เขยิบ current ไปยังตำแหน่งถัดไปในลิสต์
/** * เขยิบ current ไปยังตำแหน่งถัดไปในลิสต์ * ถ้า current เป็น null ก็ไม่ต้องทำอะไร */ public void advance( ) { if( !isPastEnd( ) ) current = current.next; } 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
16
โค้ดของ linked list public class LinkedList { private ListNode header;
public LinkedList( ) header = new ListNode( null ); } public boolean isEmpty( ) return header.next == null; ต้องบอกว่าโค้ดที่เราแสดง เป็น linked list ที่สร้างขึ้นเอง ในจาวาจะมี linked list กับ iterator อยู่แล้ว ซึ่งจะซับซ้อนกว่านี้ แต่ก็สร้างด้วยหลักการเดียวกัน ซึ่งเราจะได้เห็นในบทต่อๆไป ในโค้ดนี้เราจะใช้ dummy node 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
17
public void makeEmpty( ) { header.next = null; } /**
/** ทำลิสต์ให้ว่าง*/ public void makeEmpty( ) { header.next = null; } /** * รีเทิร์น iterator ที่ชี้ไป header node. */ public LinkedListItr zeroth( ) return new LinkedListItr( header ); 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
18
/* รีเทิร์น iterator ที่ชี้ไป node ถัดจาก header (ซึ่งเป็น null ได้
ถ้าลิสต์นี้ว่าง)*/ public LinkedListItr first( ) { return new LinkedListItr( header.next ); } /** ใส่โนดใหม่ตามหลังสมาชิกที่ชี้ด้วย p x item ที่จะเอาใส่โนดใหม่ p เป็น iterator ชี้ตำแหน่งที่อยู่ก่อนโนดที่จะลงใหม่ */ public void insert( Object x, LinkedListItr p ) if( p != null && p.current != null ) p.current.next = new ListNode( x, p.current.next ); ตอน insert ต้องเช็คว่า p มีตัวตน และชี้ไปที่โนดหนึ่งจริงหรือเปล่า ไม่งั้นจะทำไม่ได้ การเปลี่ยน pointer เพื่อการ insert จะเหมือนที่แสดงในรูปตอนแรก เพียงแต่เราเข้าถึงโนดที่อยู่ก่อนตัวที่จะเติมลงไป ได้โดยใช้ iterator 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
19
* @param x คือ ของข้างในของโนดที่เราต้องการหา.
/** x คือ ของข้างในของโนดที่เราต้องการหา. iterator ที่ชี้ไปที่โนดแรกที่มี x อยู่ข้างใน หรือชี้ไปที่ null ถ้า x ไม่อยู่ *ในลิสต์เลย */ public LinkedListItr find( Object x ) { /* 1*/ ListNode itr = header.next; /* 2*/ while( itr != null && !itr.element.equals( x ) ) /* 3*/ itr = itr.next; /* 4*/ return new LinkedListItr( itr ); } นี่เป็นการวนลูปเรื่อยๆ โดยเริ่มจากโนดแรกที่มีของ จนกว่าจะเจอ x หรือสิ้นสุดลิสต์ แล้วจึงสร้าง iterator ที่ชี้ไปยังที่ที่ x อยู่ (หรือชี้ไปที่ null ถ้า x ไม่อยู่ในลิสต์เลย) 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
20
* รีเทิร์น iterator ที่ชี้ไปที่โนดก่อนโนดแรกที่มี x
/** * รีเทิร์น iterator ที่ชี้ไปที่โนดก่อนโนดแรกที่มี x * ถ้าไม่มี x ในลิสต์เลยให้รีเทิร์น iterator ที่ชี้ไปที่โนดสุดท้ายของลิสต์ */ public LinkedListItr findPrevious( Object x ) { /* 1*/ ListNode itr = header; /* 2*/ while( itr.next != null && !itr.next.element.equals( x ) ) /* 3*/ itr = itr.next; /* 4*/ return new LinkedListItr( itr ); } การวนลูปจะเกือบเหมือนกับ find เพียงแต่ว่า ใช้ itr.next แทน itr เพื่อให้ตรวจสอบได้ยังโนดถัดไป เมื่อเจอ x ในโนดถัดไป จะได้ รีเทิร์น iterator ที่ชี้ไปโนดปัจจุบัน(ซึ่งอยู่ก่อน x พอดี) ส่วนถ้าไม่เจอ x เราก็ยังได้รู้ว่าโนดถัดไปจะเป็น null เราจะได้รีเทิร์น iterator ที่ชี้ไปโนดปัจจุบันซึ่งเป็นโนดสุดท้ายพอดี 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
21
* เอาโนดของ x ตัวแรกที่เจอออกจากลิสต์
/** * เอาโนดของ x ตัวแรกที่เจอออกจากลิสต์ x คือ item ในโนดที่ต้องการเอาออก */ public void remove( Object x ) { LinkedListItr p = findPrevious( x ); if( p.current.next != null ) // นี่หมายความว่าหา x เจอ เพราะไม่ใช่ตัวสุด // ท้ายของลิสต์ p.current.next = p.current.next.next; //เปลี่ยน reference //ข้ามตัวที่มี x ไป } 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
22
public static void printList( LinkedList theList ) {
if( theList.isEmpty( ) ) System.out.print( "Empty list" ); else LinkedListItr itr = theList.first( ); for( ; !itr.isPastEnd( ); itr.advance( ) ) System.out.print( itr.retrieve( ) + " " ); } System.out.println( ); นี่เป็นการพิมพ์เนื้อในของแต่ละโนดเรียงไปจนหมด โดยใช้การลูปที่ iterator ตั้งแต่สมาชิกตัวแรกของลิสต์จนถึงสมาชิกตัวสุดท้าย ใช้เมธอดของ iterator ทั้งหมด 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
23
Doubly linked list ในโนดจะมี instance variable เพิ่มมาอีกหนึ่งตัว คือ
Previous : ซึ่งจะชี้ไปยังโนดที่อยู่ก่อนหน้า ทำหน้าที่คล้าย next เพียงแต่ชี้ไปคนละทางเท่านั้น ถ้ามีลิสต์แบบนี้จะทำให้เรียงดูลิสต์ได้สองทิศทาง แต่อย่าลืมว่าต้องใช้เวลาเพิ่มในการเปลี่ยน pointer 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
24
Insert ใน doubly linked list
A1 A3 A2 X 1 newnode 3 2 4 P การ insert ใน doubly linked list เป็นขั้นตอนดังนี้ newnode.next = p.next; newnode.previous = p.next.previous; p.next.previous = newnode; p.next = newnode; 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
25
Remove ใน doubly linked list
A1 A3 A2 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
26
Circular Linked list ตัวท้ายสุดจะลิงค์กลับไปตัวแรก
อย่างนี้ก็ไม่ต้องมี dummy node ก็ได้ ทำให้เป็น doubly linked list ด้วยก็ยังได้ แล้วแต่จะประยุกต์ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
27
ตัวอย่างการใช้งาน linked list
สมมติเราต้องการเก็บข้อมูลที่อยู่ในรูปของ polynomial เราอาจใช้อาร์เรย์ โดย index i ใช้เก็บสัมประสิทธิ์ของ a0 a1 … a2 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
28
เมื่อใช้อาร์เรย์เก็บ polynomial
… a2 a0 a1 … a2 คำตอบจะเกิดจากการบวกกันช่องต่อช่อง 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
29
เมื่อใช้อาร์เรย์เก็บ polynomial (ต่อ)
… a2 จะต้องเอาข้างในของแต่ละช่องมาคูณกับทุกช่องของpolynomial อีกตัว แล้วเอาผลมาบวกกัน ถ้ามีเทอมที่มีสัมประสิทธิ์เป็น 0 อยู่เยอะๆล่ะ ก็จะเป็นการคูณ 0 ไปโดยเสียเวลา 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
30
เปลี่ยนมาใช้ linked list เก็บ
จะลดการใช้ 0 มากๆได้ ใช้เนื้อที่น้อยลงเยอะ เช่น 5x75+11x125+8 5 75 11 125 8 header สัมประสิทธิ์ กำลังของ x 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
31
Skip list ในโนดหนึ่ง เราให้มีพอยต์เตอร์แบบ next มากกว่าหนึ่งพอยต์เตอร์ได้ ซึ่งพอยต์เตอร์ที่มีเพิ่มมานั้นใช้ชี้ข้ามไปยังส่วนต่างๆของลิสต์ 2 6 8 5 122 15 ในตัวอย่างนี้ โนดทุกลำดับชี้ไปยังโนดถัดไป โนดลำดับที่หารสองลงตัวชี้ไปยังลำดับที่หารสองลงตัวตัวถัดไป และโนดลำดับที่หารสี่ลงตัวชี้ไปยังโนดลำดับที่หารสี่ลงตัวตัวถัดไป วิธีการหาของในลิสต์แบบนี้เริ่มจากการหาด้วยลิงค์ระดับสูงสุด (กระโดดข้ามมากที่สุด) ก่อน ถ้าไล่ตามลิงค์ระดับสูงสุดหาจนหมดลิสต์แล้วยังไม่เจอ ก็เริ่มหาใหม่โดยใช้ลิงค์ระดับที่ต่ำลงมา ทำเช่นนี้ไปเรื่อยๆ ถ้าระหว่างที่หา เกิดเจอโนดที่ต้องอยู่ข้างหลังโนดที่เราต้องการแน่ๆ ก็ให้ย้อนกลับมาหนึ่งโนด แล้วเริ่มการค้นหาจากโนดนั้นโดยใช้ลิงค์ระดับที่ต่ำลงระดับที่ต่ำลงมา จากรูป ถ้าเราต้องการหา 15 เริ่มแรกใช้ลิงค์ระดับสูงมาดูที่ 8 แล้วดูตัวถัดไป แต่ว่ามันเกิน 15 ดังนั้นเราจึงเริ่มที่ 8 แล้วหาด้วยลิงค์ระดับต่ำลงมา จึงเจอ 15 จะเห็นว่าใช้การตามลิงค์เป็นจำนวนน้อยครั้งกว่าการตามลิงค์ตั้งแต่สมาชิกตัวแรก ในกรณีที่หาของเจอ เวลาเฉลี่ยนั้นเป็น O(log n) ส่วนกรณีที่แย่ที่สุดนั้นเกิดจากการหาไม่เจอซึ่งต้องทำให้เริ่มหาใหม่จากลิงค์ระดับต่ำสุด ซึ่งกินเวลา O(n) 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
32
ปัญหาของ Skip list การใส่สิ่งของชิ้นใหม่ลงไปในลิสต์หรือการลบของชิ้นหนึ่งออกจากลิสต์จะทำให้เราต้องเปลี่ยนโครงสร้างลิงค์ของแต่ละโนดใหม่ทั้งหมด ซึ่งเป็นความยุ่งยาก ฉะนั้นในทางปฏิบัติ จึงบังคับแค่จำนวนของลิงค์ในแต่ละระดับ สมมติว่ามีจำนวนโนดตอนเริ่มต้นอยู่ 20 โนด ระดับที่ 0 –> 20 โนด เพราะระดับล่างสุดต้องเป็นลิงค์ลิสต์แบบปกติ ระดับที่ 1 – >10 โนด ระดับที่ 2 –> 5 โนด ระดับที่ 3 –> 2 โนด ถ้าเราจะให้มีลิงค์ 4 ระดับ โดยระดับ n เริ่มจากโนดลำดับที่ 2n และลิงค์ข้ามไปเป็นจำนวน 2n โนด สมมติว่ามีจำนวนโนดตอนเริ่มต้นอยู่ 20 โนด จะต้องมีโนดที่มีลิงค์ระดับต่างๆดังนี้ ระดับที่ 0 –> 20 โนด เพราะระดับล่างสุดต้องเป็นลิงค์ลิสต์แบบปกติ ระดับที่ 1 – >10 โนด ระดับที่ 2 –> 5 โนด ระดับที่ 3 –> 2 โนด ถ้าเราถือว่าไม่นับโนดซ้ำ จะมีโนด ระดับที่ 2 –> 5-2 = 3 โนด ระดับที่ 1 -> 10 – 2-3 = 5 โนด ระดับที่ 0 -> = 10 โนด ในการเติมโนดใหม่เข้าไป ให้สุ่มตัวเลขจาก 1 ถึง 20 มา ถ้าตัวเลขนั้นอยู่ระหว่าง 1 ถึง 10 ก็ให้ใส่โนดที่มีลิงค์ระดับที่ 0 ถ้าตัวเลขนั้นอยู่ระหว่าง 11 ถึง 15 ก็ให้ใส่โนดที่มีลิงค์ถึงระดับที่ 1 ถ้าเป็นเลข 16 ถึง 18 ก็ให้ใส่โนดที่มีลิงค์ถึงระดับที่ 2 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
33
ปัญหาของ Skip list (ต่อ)
ถ้าเราถือว่าไม่นับโนดซ้ำ จะมีโนด ระดับที่ 3 –> 2 โนด ระดับที่ 2 –> 5-2 = 3 โนด ระดับที่ 1 -> 10 – 2-3 = 5 โนด ระดับที่ 0 -> = 10 โนด ในการเติมโนดใหม่เข้าไป ให้สุ่มตัวเลขจาก 1 ถึง 20 มา ถ้าตัวเลขนั้นอยู่ระหว่าง 1 ถึง 10 ก็ให้ใส่โนดที่มีลิงค์ระดับที่ 0 ถ้าตัวเลขนั้นอยู่ระหว่าง 11 ถึง 15 ก็ให้ใส่โนดที่มีลิงค์ถึงระดับที่ 1 ถ้าเป็นเลข 16 ถึง 18 ก็ให้ใส่โนดที่มีลิงค์ถึงระดับที่ 2 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
34
Self-Organizing List จัดตัวที่เราพึ่งดูข้อมูลเป็นตัวล่าสุดไว้ที่หัวลิสต์ หรือ สลับที่โนดที่ดูข้อมูลเป็นตัวล่าสุดกับโนดที่อยู่ข้างหน้ามัน หรือ จัดลิสต์โดยให้โนดที่มีสมาชิกถูกดูข้อมูลบ่อยที่สุดไปอยู่หน้าลิสต์ เรียงกันมาตามลำดับหรือ กำหนดลักษณะของการเรียงไว้ล่วงหน้า เช่น เรียงข้อมูลตามตัวอักษร วิธีนี้ได้เปรียบตอนหาของในลิสต์ เพราะถ้าหาของ ณ ตำแหน่งหนึ่งไม่เจอ เราจะรู้ว่าของนั้นไม่อยู่ในส่วนที่เหลือของลิสต์แน่ๆ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
35
Self-Organizing List (ต่อ)
จากการทดลองที่แสดงในหนังสือของ อดัม ดรอสเด็ค สรุปได้ว่า การจัดตัวที่เราพึ่งดูข้อมูลเป็นตัวล่าสุดไว้ที่หัวลิสต์และการจัดลิสต์โดยให้โนดที่มีสมาชิกถูกดูข้อมูลบ่อยที่สุดไปอยู่หน้าลิสต์นั้นมีความเร็วในระดับเดียวกัน ซึ่งเร็วกว่าวิธีที่สลับโนดที่ดูข้อมูลเป็นตัวล่าสุดกับโนดที่อยู่ข้างหน้ามันและวิธีที่กำหนดลักษณะของการเรียงไว้ล่วงหน้า 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
36
Multilist หรือ Sparce Matrix
สมมติว่ามีข้อมูลนิสิตทั้งหมด และวิชาทั้งหมดของจุฬา ถ้ารู้นิสิต ต้องหาวิชาที่คนนี้ลงทะเบียนได้ ถ้ารู้วิชา ต้องหาได้ว่ามีใครลงวิชานี้บ้าง ใช้อาร์เรย์สองมิติสร้างตารางของนิสิตกับวิชาขึ้นมาได้ แต่จะมีช่องว่างเพียบ เปลืองที่เพราะนิสิตต่างสาขาไม่ลงของสาขาอื่น วิธีแก้คือ ทำลิสต์สองมิติ ดูรูปหน้าถัดไป 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
37
นิสิต 1 นิสิต 2 นิสิต 3 วิชา 1 วิชา 2 วิชา 3 2018/11/28
ให้มีโนดว่างอยู่ในลิสต์ เป็นตัวบอกการมีอยู่ของนิสิตและวิชา ถ้าเราต้องการหานิสิตที่เรียนวิชา 1 ทั้งหมดก็ให้ตามลิงค์ไปจากเฮดเดอร์ของวิชา 1 (ซ้ายไปขวาในรูป) พอเจอโนดก็เก็บพอยต์เตอร์ที่ชี้ไปโนดนี้ไว้ก่อน(สมมติว่าเก็บในตัวแปร t) แล้วจึงหาชื่อนิสิตโดยการตามลิงค์ในแนวดิ่งจนเจอเฮดเดอร์ที่บอกชื่อนิสิต จากนั้นก็ตามลิงค์จาก t ต่อไป วิธีนี้จะมีปัญหาตรงที่ต้องตามลิงค์แนวดิ่งไปเรื่อยๆ จนกว่าจะเห็นเฮดเดอร์ ซึ่งอาจจะนานได้(แต่จริงๆไม่น่าจะเกิดขึ้นเพราะนิสิตจะลงทะเบียนเรียนวิชาเป็นสัดส่วนน้อยมากเมื่อเทียบกับจำนวนวิชาทั้งหมดในจุฬา) ถ้าต้องการประหยัดเวลาก็ให้เก็บลิงค์ที่ชี้ไปยังเฮดเดอร์โดยตรงในทุกๆโนด หรือเก็บชื่อวิชาและนิสิตไว้ในทุกๆโนด ซึ่งจะทำให้เปลืองที่ในการเก็บมากขึ้น(แต่ก็ยังน้อยกว่าการใช้อาร์เรย์สองมิติ) 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
38
กราฟ Sparce table Node directory 2018/11/28 อ.ดร.วิษณุ โคตรจรัส B A C
วิธีที่ง่ายที่สุดคือการสร้างตารางขึ้นมาแล้วสร้างลิงค์ลิสต์แบบที่ทำกับตารางสปาร์ซ ในรูปนี้ ข้อมูลลิงค์ในแนวตั้งและแนวนอนนั้นเหมือนกัน เราจึงสามารถตัดลิงค์ในแนวหนึ่งทิ้งได้ ถ้าเป็นกราฟลูกศรมีทิศทางเดียว ตารางจะไม่สมมาตร ทำให้ต้องใช้ข้อมูลทั้งสองแกน นอกจากนี้ยังมีวิธีอื่นที่เป็นที่นิยมใช้ในการเก็บข้อมูลของกราฟ เช่น การใช้โนดไดเร็กทอรี่ หรือพูดง่ายๆคือ ลิงค์ลิสต์ที่ลิงค์ไปยังอาร์เรย์ของโนด C Node directory 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
39
Cursor implementation
คือการจำลองลิสต์โดยใช้โครงสร้างข้อมูลแบบอื่นจำลองการลิงค์ เนื่องจาก บางภาษาทำลิสต์แบบที่เราทำมาไม่ได้ ในบางภาษา การสร้างวัตถุนั้นเสียเวลามากเกินไป สิ่งที่ต้องจำลองคือ ต้องมีโนด และแต่ละโนดลิงค์ไปโนดต่อไป สร้างโนดได้ด้วยการ new และพอไม่มีลิงค์ไปยังโนดใด โนดนั้นก็จะถูกลบไปเองโดย garbage collection 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
40
Node เปลี่ยนไปใช้ตัวเลข index แทน pointer class CursorNode {
// Constructors CursorNode( Object theElement ) this( theElement, 0 ); } CursorNode( Object theElement, int n ) element = theElement; next = n; Node เปลี่ยนไปใช้ตัวเลข index แทน pointer 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
41
// Friendly data; accessible by other package routines Object element;
int next; } 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
42
Iterator ตัวเลขแทน pointer public class CursorListItr { /**
* สร้าง list iterator theNode โนดที่เราสนใจให้ iterator ชี้ไป */ CursorListItr( int theNode ) current = theNode; } Iterator ตัวเลขแทน pointer 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
43
/** true ถ้าตำแหน่งปัจจุบันเป็น null ซึ่งหมายความได้ว่าเราดู * ลิสต์จนเลยโนดสุดท้ายแล้ว */ public boolean isPastEnd( ) { return current == 0; } ตัวเลข 0 แทน null 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
44
ลิสต์ของเราคืออาร์เรย์นั่นเอง
/** * รีเทิร์น item stored in the current position. item ที่เก็บไว้ตรง current หรือไม่ก็รีเทิร์น null *ถ้า current ไม่อยู่ในลิสต์ */ public Object retrieve( ) { return isPastEnd( ) ? null : CursorList.cursorSpace[ current ].element; } ลิสต์ของเราคืออาร์เรย์นั่นเอง 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
45
* เขยิบ current ไปหนึ่งตำแหน่ง
/** * เขยิบ current ไปหนึ่งตำแหน่ง * ถ้า current นั้นเลยลิสต์ไปแล้วก็ไม่ต้องทำอะไร */ public void advance( ) { if( !isPastEnd( ) ) current = CursorList.cursorSpace[ current ].next; } int current; // Current position 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
46
Cursor List ให้มี free list (ลิสต์ที่เก็บโนดที่ยังไม่ใช้) และ use list (ลิสต์ที่เก็บโนดที่ใช้งานอยู่) เราสามารถให้สองลิสต์นี้อยู่บนโครงสร้างอาร์เรย์เดียวกัน ตอนเริ่ม ให้ลิสต์เป็น free list ทั้งหมดดังรูป ในตัวอย่างนี้เราจะใช้ free list เป็นโครงสร้างหลัก จะไม่มี use list ตรงๆ null 1 null 2 null 3 null 4 null 0 นั้นแทน null 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
47
private static int alloc( ) int p = cursorSpace[ 0 ].next;
public class CursorList { private static int alloc( ) int p = cursorSpace[ 0 ].next; cursorSpace[ 0 ].next = cursorSpace[ p ].next; if( p == 0 ) throw new OutOfMemoryError( ); return p; } ใช้แทนการ new จริงๆแล้วคือการ เลื่อนพอยต์เตอร์ลดช่องว่างของ free list ไปหนึ่งช่อง จากสถานะของหน้าที่แล้ว อันนี้จะทำงานคล้ายๆ new คือจะเตรียมที่ในการใช้งาน object จะเห็นว่าทุกๆเมธอดจำลองการทำงานมาจาก linked list หมดเลย เมธอดที่มีชื่อเหมือนกันก็จะมีหน้าที่เหมือนกัน ทำให้เราสามารถเลือกใช้ implementation ไหนก็ได้ตามสภาพงาน แต่การใช้อาร์เรย์ทำให้มีเมธอดเพิ่มในการ new และ free memory null 1 null 2 null 3 null 4 null 2 เดี๋ยวจะกลายเป็นส่วนของ use list 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
48
private static void free( int p ) { cursorSpace[ p ].element = null;
cursorSpace[ p ].next = cursorSpace[ 0 ].next; cursorSpace[ 0 ].next = p; } ใช้แทน garbage collection ทำหน้าที่เอาโนดตำแหน่ง p ใส่ไว้ข้างหน้า ของ free list (อย่าลืมว่า p ต้องเป็นส่วนหนึ่งของUse list อยู่แล้ว) ในรูปนี้ให้ use list ตอนแรกคือสมาชิกที่มี index 1 กับ 2 เราต้องการ free ตำแหน่ง 2 การใช้ constructor ก็จะเหมือนลิงค์ลิสต์ คือสร้างโนดที่เป็น header ขึ้นมา โดยให้ยังไม่มีการต่อไปโนดอื่น (ในที่นี้ให้ next เป็น 0 ซึ่งมีความหมายเดียวกับ null) null 3 x 2 y null 4 null 2 3 null 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
49
cursorSpace[ header ].next = 0; }
public CursorList( ) { header = alloc( ); cursorSpace[ header ].next = 0; } null 1 null 2 null 3 null 4 null 2 กลายเป็นส่วนของ use list เรียบร้อย 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
50
return cursorSpace[ header ].next == 0; } public void makeEmpty( )
public boolean isEmpty( ) { return cursorSpace[ header ].next == 0; } public void makeEmpty( ) while( !isEmpty( ) ) remove( first( ).retrieve( ) ); isEmpty ก็เช็คว่าลิสต์ว่างหรือไม่โดยดูจาก ว่าของในอาร์เรย์มีตัวที่ต่อจาก headerเป็นค่า 0 หรือไม่ (ถ้า 0 ก็คือเป็นเหมือน null) 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
51
public CursorListItr zeroth( ) { return new CursorListItr( header ); }
public CursorListItr first( ) return new CursorListItr( cursorSpace[ header ].next ); อันนี้ก็ไม่มีอะไร แค่รีเทิร์นตัว iterator ที่ชี้ไปที่ header หรือตัวที่อยู่ต่อจาก header ในอาร์เรย์ (ต้องหาตัวนั้นด้วย next นะ เพื่อให้การทำงานเหมือนกับตอนที่เป็นลิงค์ลิสต์) 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
52
ตรงนี้คือใส่ x ไปหลังที่ที่ p สนใจ โดย p ต้องอยู่ใน use list อยู่แล้ว
public void insert( Object x, CursorListItr p ) { if( p != null && p.current != 0 ) int pos = p.current; int tmp = alloc( ); cursorSpace[ tmp ].element = x; cursorSpace[ tmp ].next = cursorSpace[ pos ].next; cursorSpace[ pos ].next = tmp; } ตรงนี้คือใส่ x ไปหลังที่ที่ p สนใจ โดย p ต้องอยู่ใน use list อยู่แล้ว ส่วนการ insert ของลงหลังตำแหน่งที่ p สนใจนั้น ก็ใช้วิธี ให้ pos คือตำแหน่งในอาร์เรย์ที่ p สนใจ ให้โนดใหม่อยู่ที่ index tmp ของอาร์เรย์ เซ็ต element ในช่องของ tmp ให้เป็น x ภายในช่องของ tmp เซ็ตตัว next ของมันให้เป็นตัวเดียวกับตัวที่อยู่ถัดจาก pos ต่อจากนั้นก็เซ็ตตัวเลขบอกตัวที่อยู่ถัดจาก pos ให้เป็น tmp แบบนี้ก็จะได้ตัวที่ตำแหน่ง tmp มาแทรกหลังตัวที่ตำแหน่ง pos 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
53
สมมติจะเอา x2ใส่หลังตำแหน่งเบอร์2 pos == p.current == 2
null 2 x1 4 3 สมมติจะเอา x2ใส่หลังตำแหน่งเบอร์2 pos == p.current == 2 null 3 x1 4 tmp == alloc() ==2 null 3 x1 2 4 x2 Use list 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
54
/* 1*/ int itr = cursorSpace[ header ].next; /* 2*/ while( itr != 0 &&
public CursorListItr find( Object x ) { /* 1*/ int itr = cursorSpace[ header ].next; /* 2*/ while( itr != 0 && !cursorSpace[itr].element.equals( x ) ) /* 3*/ itr = cursorSpace[ itr ].next; /* 4*/ return new CursorListItr( itr ); } Find ทำงานโดยเริ่มหาจากตัวแรกที่ไม่ใช่ header แล้วลูปหา x เรื่อยๆ การทำงานจะเป็นแบบเดียวกับของลิงค์ลิสต์เลย สามารถเปรียบเทียบได้ชัดเจน 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
55
public CursorListItr findPrevious( Object x ) {
/* 1*/ int itr = header; /* 2*/ while( cursorSpace[ itr ].next != 0 && !cursorSpace[ cursorSpace[ itr ].next ].element.equals( x ) ) /* 3*/ itr = cursorSpace[ itr ].next; /* 4*/ return new CursorListItr( itr ); } การทำงานจะเป็นแบบ findPrevious ของลิงค์ลิสต์ แต่อาจดูสับสนนิดหน่อยเพราะมีการซ้อนอาร์เรย์เพื่อให้คำนวณ indexได้ถูกต้อง อย่าลืมว่าถ้าหา x ไม่เจอ ตัว iterator ที่ได้จะชี้จุดสนใจไปที่ตำแหน่งสุดท้ายของลิสต์ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
56
public void remove( Object x ) { CursorListItr p = findPrevious( x );
int pos = p.current; if( cursorSpace[ pos ].next != 0 ) // นี่หมายความว่าหา x เจอ //เพราะไม่ใช่ตัวสุดท้ายของลิสต์ int tmp = cursorSpace[ pos ].next; cursorSpace[ pos ].next = cursorSpace[ tmp ].next; free( tmp ); } มีปัญหาถ้าตัวแรกของ use list คือ x รูปแบบก็จะเป็นแบบเดียวกับของลิงค์ลิสต์ จะทำงานคล้ายการเปลี่ยน pointer ข้ามตรงที่มี x เพียงแต่เปลี่ยน index ของอาร์เรย์แทน 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
57
p= findPrevious() , pos =1
Remove x2 p= findPrevious() , pos =1 null 3 x1 2 4 x2 tmp = 2 null 3 x1 4 x2 นี่คือให้ use list ข้ามช่องที่มี x2 ไป free(tmp) null 3 x x2 null 4 null 2 null 3 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
58
variables initialization private int header;
static CursorNode[ ] cursorSpace; private static final int SPACE_SIZE = 100; static { cursorSpace = new CursorNode[ SPACE_SIZE ]; for( int i = 0; i < SPACE_SIZE; i++ ) cursorSpace[ i ] = new CursorNode( null, i + 1 ); cursorSpace[ SPACE_SIZE - 1 ].next = 0; } variables initialization ส่วนท้ายนี้เป็นส่วนที่เราสร้างตัวแปร และทำการ initialize อาร์เรย์ จะสังเกตว่ามีการทำ CursorNode ขึ้นมาทั้งหมดตั้งแต่ตอนนี้ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
59
stack มีเป็นชั้นๆ ใส่ของเข้าออกได้ทางเดียวเท่านั้น (LIFO = last in, first out) สิ่งที่เราทำกับข้อมูลบน stack ได้คือ Push -เอาของยัดใส่ Pop - เอาตัวบนสุดออก Top – บอกว่าตัวบนสุดคืออะไร แต่ไม่เอาออก A B C ฏ จริงๆ stack ก็คือลิสต์ทางเดียวนั่นเอง ดังนั้นเราสามารถทำ stack ขึ้นจากลิสต์ได้ หรือทำจากอาร์เรย์ก็ได้ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
60
ใช้ list ทำ stack Push = การ insert ของ เข้าที่ตัวถัดจาก header
Pop = การเอาของออกจากโนดที่อยู่ถัดจาก header ถ้าเป็นลิสต์ว่างก็ควรมีการ throw exception Top = แค่ดูของที่อยู่ในโนดถัดจาก header 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
61
โค้ดของ stack ประยุกต์จาก linked list
ในที่นี้ไม่ได้มี header เพื่อให้ดูง่าย แม้ไม่มี header เราก็ทำ stackได้ public class StackFromLinkedList { private ListNode top; public StackFromLinkedList( ){ top = null; } ให้มีตัวแปร top ที่เก็บโนดบนสุดของ stack ไว้ตลอดเวลา 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
62
* @return false ตลอดเพราะเราถือว่า stack ไม่มีวันเต็ม */
/** false ตลอดเพราะเราถือว่า stack ไม่มีวันเต็ม */ public boolean isFull( ) { return false; } true if stack นี้ว่าง มิฉะนั้นก็รีเทิร์นfalse public boolean isEmpty( ) return top == null; สังเกตว่า stack ที่ว่าง คือ stack ที่ไม่มีส่วน top 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
63
อย่าลืมว่า top() นั้นไม่เปลี่ยน stack
public void makeEmpty( ) { top = null; } /** ออบเจ็คต์ล่าสุดที่ใส่ stack แต่ถ้า stack ว่างให้รีเทิร์น null */ public Object top( ) if( isEmpty( ) ) return null; return top.element; อย่าลืมว่า top() นั้นไม่เปลี่ยน stack Throw exception ก็ได้ แล้วแต่จะทำ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
64
จริงๆก็คือเปลี่ยน pointer ให้ชี้ข้ามไปนั่นเอง
/** * เอาตัวบนสุดของ stack ทิ้งไป ฉะนั้นตัวบนสุดจะเปลี่ยนไป Underflow ถ้า stack นี้ไม่มีสมาชิก */ public void pop( ) throws Underflow { if( isEmpty( ) ) throw new Underflow( ); top = top.next; } จริงๆก็คือเปลี่ยน pointer ให้ชี้ข้ามไปนั่นเอง 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
65
* ทำทั้ง top() และ pop()
/** * ทำทั้ง top() และ pop() item ตัวที่ pop ออกไปด้วยเมธอดนี้ หรือไม่ก็รีเทิร์น null ถ้าว่าง */ public Object topAndPop( ) { if( isEmpty( ) ) return null; Object topItem = top.element; top = top.next; return topItem; } Throw exception ก็ได้ แล้วแต่จะทำ ตัวที่จะรีเทิร์น เลื่อน pointer ข้าม top ตัวเดิม 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
66
อันเก่า อันใหม่ /** * ใส่ออบเจ็คต์ใหม่ลง stack.
x ออบเจ็คต์ใหม่ที่ต้องการใส่ลงไป */ public void push( Object x ) { top = new ListNode( x, top ); } เวลาของทุกๆเมธอดเป็นค่าคงที่หมด เพราะไม่มีอะไรที่เกี่ยวกับขนาดของ stack เลย อันเก่า อันใหม่ โนด top ใหม่จะมีลิงค์ชี้ไปยัง top ตัวเก่า 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
67
ข้อเสียของ stack พอ pop อะไรออกไป ตัวนั้นก็จะหายไปเลย เอามาใช้ใหม่ไม่ได้ แก้โดยใช้ตัวแปรอื่น หรือ stack ที่สอง เก็บส่วนที่โยนทิ้งเอาไว้ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
68
ใช้อาร์เรย์ทำ stack แต่อย่าลืมว่าเมื่อใช้อาร์เรย์เราต้องกำหนดขนาดอาร์เรย์แต่เนิ่นๆ ทำให้เปลืองหน่วยความจำ ให้แต่ละ stack มี arrayBody topIndex – เป็น index (-1 หมายถึง stack นี้ว่าง) 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
69
ใช้อาร์เรย์ทำ stack (ต่อ)
การตรวจ error (เช่น pop() จาก stack ที่ว่างอยู่) จะใช้เวลานาน แต่ยังไงก็ควรทำ เพราะ error มันไม่เกิดบ่อยนักหรอก 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
70
โค้ด stack ที่ทำจากอาร์เรย์
public class StackFromArray { private Object [ ] arrayBody; private int topIndex; static final int DEFAULT_CAPACITY = 10; public StackFromArray( ) { this( DEFAULT_CAPACITY ); } 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
71
* สร้างสแตกขึ้นมา โดยกำหนดความจุได้ * @param capacity ความจุของสแตก.
/** * สร้างสแตกขึ้นมา โดยกำหนดความจุได้ capacity ความจุของสแตก. */ public StackFromArray( int capacity ) { arrayBody = new Object[ capacity ]; topIndex = -1; } การสร้างสแตกนั้นก็คือการสร้างอาร์เรย์แล้วใส่ค่าที่ถูกต้องให้กับ topIndex นั่นเอง โดยถือว่าค่า -1 หมายถึงสแตกว่าง 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
72
public boolean isEmpty( ) { return topIndex == -1; }
public boolean isFull( ) return topIndex == arrayBody.length - 1; public void makeEmpty( ) topIndex = -1; จะเห็นว่า isFull ในนี้ไม่เหมือนกับตอนใช้ linked list ตอนนี้เราใช้อาร์เรย์แล้ว ดังนั้นจึงมีเนื้อที่จำกัด 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
73
Throw exception ก็ได้ แล้วแต่จะทำ
public Object top( ) { if( isEmpty( ) ) return null; return arrayBody[ topIndex ]; } /** * เอาส่วน top ทิ้งออกไปจากสแตก โดยทำให้เป็น null. Underflow ถ้าสแตกนั้นว่างอยู่แล้ว. */ public void pop( ) throws Underflow throw new Underflow( ); arrayBody[ topIndex-- ] = null; Throw exception ก็ได้ แล้วแต่จะทำ Set ให้เป็น null แล้วเลื่อน index ต้องไม่ลืมถอยดัชนีลงมาเมื่อ pop ไปแล้ว 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
74
* เอาออบเจ็คต์ส่วนบนสุดออกจากสแตกและรีเทอร์นออบเจ็คต์นั้นออกมา
/** * เอาออบเจ็คต์ส่วนบนสุดออกจากสแตกและรีเทอร์นออบเจ็คต์นั้นออกมา ออบเจ็คต์ส่วนบนสุดที่พึ่งเอาออกจากสแตก หรือ null ถ้าสแตกว่าง */ public Object topAndPop( ) { if( isEmpty( ) ) return null; Object topItem = top( ); arrayBody[ topIndex- - ] = null; return topItem; } Throw exception ก็ได้ แล้วแต่จะทำ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
75
* @param x ของที่จะเอาใส่ * @exception Overflow ถ้าสแตกเต็มอยู่แล้ว */
/** * ถ้าสแตกยังไม่เต็ม ใส่ออบเจ็คต์ใหม่ตามที่กำหนดในพารามิเตอร์ลงไป x ของที่จะเอาใส่ Overflow ถ้าสแตกเต็มอยู่แล้ว */ public void push( Object x ) throws Overflow { if( isFull( ) ) throw new Overflow( ); arrayBody[ ++topIndex ] = x; } เลื่อน index แล้วค่อยเอาใส่อาร์เรย์ตรง index ใหม่ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
76
การใช้งาน stack (1) Balancing check
ตรวจวงเล็บภายในโค้ดว่ามีคู่ถูกต้องหรือไม่ อ่าน text ของโปรแกรมเข้ามา ถ้าเจอวงเล็บเปิดก็เอาเก็บใส่สแตก ถ้าเจอวงเล็บปิดก็ pop ตัวที่เราเก็บไว้ในสแตกทิ้ง แจ้ง error ถ้าตอนตรวจเจอวงเล็บปิดนั้น ไม่มีวงเล็บเปิดในสแตกเลย หรือมีวงเล็บแบบอื่นอยู่ ถ้าตอนตรวจเสร็จแล้ว สแตกไม่ว่าง ก็ถือว่าจับคู่ไม่ได้ ต้องแจ้ง error เช่นเดียวกัน 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
77
การใช้งาน stack (2) Postfix expression (reverse polish)
[7+(8*9)+5]*10 = * * เราสามารถใช้สแตก แก้เลขที่อยู่ในรูปแบบของ postfix ได้ เจอตัวเลข -> push ใส่สแตก เจอ operator -> pop ตัวเลขข้างในสแตกออกมาทำงานกับ operator 9 ตอนแรกอ่านตัวเลขก่อน 8 7 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
78
แล้วเอาผลกลับมาใส่สแตก
พออ่าน * Pop ไปคูณกัน แล้วเอาผลกลับมาใส่สแตก 9 8 72 7 7 ต่อจากนั้นก็อ่าน 5 เข้ามาตามปกติ พออ่าน + ก็ pop สองตัวบนไปบวกกัน 5 5 72 72 77 7 7 7 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
79
อ่าน + ตัวที่สองก็เช่นเดียวกัน
77 7 84 ต่อจากนั้นก็อ่าน 10 เข้ามาตามปกติ พออ่าน * ก็ pop สองตัวบนไปคูณกัน 10 10 84 84 840 ได้คำตอบ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
80
การใช้งาน stack (3) แปลงinfix ให้เป็น postfix Operand -> output เลย
operator -> เอาไว้บนสแตกก่อน เก็บวงเล็บเปิดไว้บนสแตกด้วย ถ้าเจอวงเล็บปิด -> pop และ output ไปเรื่อยๆจนกว่าจะเจอวงเล็บเปิด มีอีก แต่อธิบายยาก มาดูตัวอย่างกัน 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
81
a+b*c+(d*e+f)*g อ่าน a,+,b -> เอา a กับ b ไปใส่ output ส่วน + นั้นเอาใส่สแตก + a b อ่าน * กับ อ่าน c -> เอา * ไว้บนสแตก ส่วน c นั้นเอาออก output * + a b c อ่าน + -> ต้อง pop เครื่องหมายที่มี priority มากกว่า หรือเท่ากัน ออกไปที่ output จึงจะ สามารถ push + ตัวใหม่ลงสแตกได้ + a b c * + 2018/11/28 อ.ดร.วิษณุ โคตรจรัส ตัวใหม่
82
ต้อง pop เครื่องหมายที่มี priority มากกว่า + หรือเท่ากัน ออกไป *
อ่าน ( และ d -> เอาวงเล็บลงสแตกเพื่อรอวงเล็บปิด ส่วน d นั้นเอาออก output ไป ( + a b c * + d ต่อไปคือ * และ e * ( + a b c * + d e + ค่อยถูก push เข้ามา ต่อไปคือ + และ f ต้อง pop เครื่องหมายที่มี priority มากกว่า + หรือเท่ากัน ออกไป * ( + a b c * + d e * f 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
83
ต่อไปคือ ) -> เราต้อง pop ให้หมดจนถึงตรงวงเล็บ
+ Pop ทิ้ง ( + a b c * + d e * f + ต่อไปคือ * และ g -> เราก็เอา * ใส่สแตก และ g ใส่ output ตามธรรมดา * + a b c * + d e * f + g เมื่อไม่มี input แล้ว ก็ pop ของที่เหลือบนสแตกไปที่ output ให้หมด a b c * + d e * f + g * + 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
84
การใช้งาน stack (4) แปลงinfix ให้เป็น prefix ถ้าอ่านเจอตัวเลขจากอินพุต
เอาตัวเลขนั้นใส่สแตกของโอเปอรานด์ ถ้าเจอโอเปอเรเตอร์ ถ้าสแตกของโอเปอเรเตอร์ว่าง ก็เอาใส่สแตกนั้นซะ ถ้าโอเปอเรเตอร์ที่อ่านเจอคือเครื่องหมายวงเล็บเปิด เอาใส่โอเปอเรเตอร์สแตก ถ้าโอเปอเรเตอร์ที่อ่านเจอมีความสำคัญมากกว่าโอเปอเรเตอร์ที่อยู่บนสุดของโอเปอเรเตอร์สแตก ให้เอาใส่โอเปอเรเตอร์สแตก 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
85
ถ้าโอเปอเรเตอร์ที่อ่านเจอมีความสำคัญน้อยกว่าหรือเท่ากับโอเปอเรเตอร์ที่อยู่บนสุดของโอเปอเรเตอร์สแตก
ให้เอาโอเปอเรเตอร์ออกจากโอเปอเรเตอร์สแตก เอาโอเปอรานด์ที่โอเปอเรเตอร์นั้นใช้ออกจากโอเปอรานด์สแตก เอามาต่อกันแบบโดยให้โอเปอเรเตอร์อยู่ก่อน แล้วตามด้วยโอเปอรานด์ โดยเรียงเอาตัวที่ออกมาทีหลังสุดไว้ก่อน เอาผลลัพธ์ใส่โอเปอรานด์สแตก ถ้าโอเปอเรเตอร์ที่อ่านเจอคือเครื่องหมายวงเล็บปิด หรือเราอ่านจากอินพุตต่อไม่ได้แล้ว ให้ทำตามข้อย่อยของข้อ 4 จนกว่าส่วนบนสุดของโอเปอรานด์สแตกจะเป็นวงเล็บเปิด ต่อจากนั้นก็เอาวงเล็บเปิดนั้นออกทิ้งไปซะ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
86
เปลี่ยน –b+sqrt(b*x – d*a*c)/(e*a)
Operand stack Operator stack b - อ่าน + เข้ามา เนื่องจาก + มีความสำคัญเท่ากับเครื่องหมายที่อยู่บนโอเปอเรเตอร์สแตก ดังนั้นต้องเอาตัวที่อยู่บนสแตกออกมาจัดเรียงกับโอเปอรานด์ของมัน -b + 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
87
ต่อมาอ่านเครื่องหมาย sqrt กับ ( เข้ามา sqrt นี้เปรียบเสมือนเป็นเมธอด ดังนั้นจึงมีความสำคัญมากกว่าโอเปอเรเตอร์ทั่วไป เอาใส่สแตกไปเลย ส่วน ( ก็เอาใส่สแตกไปได้เลยเช่นกัน -b ( sqrt + ลำดับข้อมูลเข้าต่อมาคือ b, *, x เราเอาทั้งหมดใส่สแตกไปได้เลย x ( sqrt + b -b * 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
88
*bx ( sqrt + -b - a ( sqrt + d - *bx -b *
ตัวต่อมาที่อ่านจากอินพุตคือ - เนื่องจากมีความสำคัญน้อยกว่า * ดังนั้นต้องมีการป็อปสแตกเอา * ออกไปทำงาน *bx ( sqrt + -b - ต่อมาอ่าน d, *, a ซึ่ง * นั้นมีความสำคัญมากกว่า – ดังนั้นจึงเอาใส่สแตกได้เลย ส่วน d กับ a ก็เอาใส่สแตกได้เลยเช่นกัน a ( sqrt + d - *bx -b * 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
89
*da ( sqrt + - *bx -b * (ตัวใหม่)
ถัดมาก็อ่าน * เข้ามาอีกตัว คราวนี้เจอ * ตรงบนสุดของสแตกเหมือนกัน ความสำคัญเท่ากัน ดังนั้นต้องป็อปของจากสแตกมาทำงาน *da ( sqrt + - *bx -b * (ตัวใหม่) ต่อมาอ่าน c ซึ่งไม่มีอะไรพิเศษ เอาใส่สแตกเฉยๆ ไม่วาดนะ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
90
( sqrt + - *bx -b **dac sqrt + -b -*bx**dac / + -b sqrt - *bx**dac
จากนั้นอ่าน ) ตอนนี้ต้องป็อปสแตกเอาโอเปอเรเตอร์กับโอเปอรานด์มาเรียงจนกว่าจะถึงเครื่องหมาย ( แล้วก็ลบ ( นั้นออก ก่อนอื่นเอา * ออกไปทำงาน ( sqrt + - *bx -b **dac ต่อไปก็ป็อป – ออกไปทำงานบ้าง sqrt + -b -*bx**dac ต่อมาอ่านเครื่องหมาย / ซึ่งสำคัญน้อยกว่า sqrt (เราถือว่าเมธอดสำคัญกว่าเครื่องหมายทั่วไป) ดังนั้นต้องเอา sqrt ออกมาทำงาน / + -b sqrt - *bx**dac 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
91
/ + -b sqrt - *bx**dac e ( / + -b sqrt - *bx**dac e ( a *
2018/11/28 อ.ดร.วิษณุ โคตรจรัส
92
+ -b/ sqrt - *bx**dac*ea
ต่อมาอ่าน ) ตอนนี้ก็ป็อป * ออกไปทำงานจากนั้นก็ทิ้งตัววงเล็บไป / + -b sqrt - *bx**dac *ea ตอนนี้การอ่านจากอินพุตได้สิ้นสุดลงแล้ว ที่เหลือก็ป็อปโอเปอเรเตอร์ไปทำงานกับโอเปอรานด์เรื่อยๆ ก่อนอื่นป็อป / ซะก่อน + -b / sqrt - *bx**dac*ea สุดท้ายก็ป็อป + ไปทำงาน จะได้ผลลัพธ์อยู่บนโอเปอรานด์สแตก + -b/ sqrt - *bx**dac*ea 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
93
การใช้งาน stack (5) เก็บข้อมูลของการเรียกใช้เมธอด
การเรียกใช้เมธอดก็คล้ายกับการมีวงเล็บ Local variables ของเมธอดต้องถูกเก็บไว้แยกจากตัวแปรอื่นๆ เพื่อกันการสับสนชื่อ ต้องเก็บข้อมูลว่าทำเมธอดเสร็จจะรีเทิร์นไปที่ไหนด้วย ฉะนั้น ตอนเรียกใช้เมธอด ก็สร้างสแตกเก็บข้อมูลเหล่านี้ไว้เลยสิ สแตกสำหรับข้อมูลของแต่ละเมธอดนี้ เราเรียกว่า activation record หรือ stack frame 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
94
method1(){ method2(); } method2(){ method3(); top method3(){ …
main (){ method1(); top method3’s info method2’s info method1’s info 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
95
ข้อควรระวังเกี่ยวกับการเรียกใช้เมธอด
มี recursion แบบที่เปลืองสแตกโดยไม่ได้เก็บข้อมูลที่เป็นประโยชน์ด้วย นั่นคือ tail recursion ซึ่งเป็น recursive call ที่บรรทัดสุดท้ายของเมธอด เช่น myAlgo(ListNode p){ if (p == null) return; //do something myAlgo(p.next); } ในแต่ละการเรียกใช้ก็แค่เรียกเมธอดอื่น ลงไปเรื่อยๆ เพราะฉะนั้นจะมีสแตกจำนวนมากเกิดมา โดยเปล่าประโยชน์ๆๆ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
96
การแก้ข้อเสียของ tail recursion
ให้คอมไพเลอร์รู้จักมัน จะได้ไม่ต้องใช้สแตก หรือ เปลี่ยนเป็นเขียนลูปแทน จากตัวอย่างที่แล้ว เราเขียนได้ว่า myAlgo(ListNode p){ while(true){ if (p == null) return; //do something p = p.next; } 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
97
Queue (แถวคอย) คิวก็คือลิสต์นั่นแหละ
แต่การเอาของเข้าทำได้แค่ที่หัวคิว (เรียกว่า enqueue) ส่วนการเอาของออกจากคิวทำได้ที่ท้ายคิวเท่านั้น (เรียกว่า dequeue) เราสามารถเขียนคิวได้โดยใช้ ลิสต์ ดัดแปลงง่ายๆ ใช้อาร์เรย์ โดยมีตัวอาร์เรย์ ตำแหน่งหัวคิวท้ายคิว และจำนวนสมาชิกในคิว ในเวลาหนึ่งๆ คิวของเราอาจดูเป็นดังนี้ 8 4 3 6 7 back front 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
98
การ enqueue และ dequeue ของคิวที่ทำจากอาร์เรย์ (1)
enqueue(x) size++ back++ theArray[back] = x dequeue() size- - front++ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
99
การ enqueue และ dequeue ของคิวที่ทำจากอาร์เรย์ (2)
แต่ต้องระวัง แก้ได้โดยทำอาร์เรย์ให้วนได้เป็นวง 8 4 3 6 7 10 back front Enqueue ไม่ได้ทั้งๆที่เหลือที่ว่างด้านหน้า 9 8 4 3 6 7 10 back front 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
100
การ enqueue และ dequeue ของคิวที่ทำจากอาร์เรย์ (3)
เมื่อ back = front-1 คิวอาจจะว่างหรืออาจจะเต็มก็ได้ เราไม่รู้ ดังนั้นจึงต้องมี size กำกับ อย่าลืมจัดการกับ error เมื่อเราพยายามเติมสมาชิกให้กับคิวที่เต็ม และเมื่อเราพยายามเอาของออกจากคิวที่ว่างอยู่แล้ว 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
101
public class QueueArray{ private Object [ ] theArray;
private int size; private int front; private int back; static final int DEFAULT_CAPACITY = 10; public QueueArray( ) { this( DEFAULT_CAPACITY ); } public QueueArray( int capacity ) theArray = new Object[ capacity ]; makeEmpty( ); 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
102
public boolean isEmpty( ) { return size == 0; }
public boolean isFull( ) return size == theArray.length; public void makeEmpty( ) size = 0; front = 0; back = -1; 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
103
public Object getFront( ) { if( isEmpty( ) ) return null;
return theArray[ front ]; } /*รีเทอร์นของที่อยู่หัวคิว พร้อมกับลบของนั้นออกจากคิวด้วย ถ้าคิวว่าง ให้รีเทอร์น null */ public Object dequeue( ){ size--; Object frontItem = theArray[ front ]; theArray[ front ] = null; front = increment( front ); return frontItem; Throw exception ก็ได้ แล้วแต่จะทำ Throw exception ก็ได้ แล้วแต่จะทำ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
104
* @exception Overflow ถ้าคิวที่เต็มแล้ว */
/** * ใส่ของลงคิว x ของที่จะใส่ Overflow ถ้าคิวที่เต็มแล้ว */ public void enqueue( Object x ) throws Overflow { if( isFull( ) ) throw new Overflow( ); back = increment( back ); theArray[ back ] = x; size++; } 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
105
* เพิ่มค่าดัชนีอาร์เรย์ โดยให้วนไปหัวอาร์เรย์ได้
/** * เพิ่มค่าดัชนีอาร์เรย์ โดยให้วนไปหัวอาร์เรย์ได้ x ดัชนีของอาร์เรย์ ต้องอยู่ในขอบเขตของอาร์เรย์ x+1, หรือ 0, ถ้า x อยู่ท้ายอาร์เรย์แล้ว */ private int increment( int x ) { if( ++x == theArray.length ) x = 0; return x; } 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
106
คิวแบบสองหน้า (double-ended queue)
insertFirst(Object o): เอา o ใส่ข้างหน้าคิว insertLast(Object o): เอา o ใส่ข้างหลังคิว removeFirst(): เอาของออกจากหัวคิว รีเทิร์นของนั้นด้วย (แจ้งข้อผิดพลาดถ้าคิวว่าง) removeLast(): เอาของออกจากท้ายคิว รีเทิร์นของนั้นด้วย (แจ้งข้อผิดพลาดถ้าคิวว่าง) first(): รีเทิร์นของที่อยู่หัวคิว (แจ้งข้อผิดพลาดถ้าคิวว่าง) last(): รีเทิร์นของที่อยู่ท้ายคิว (แจ้งข้อผิดพลาดถ้าคิวว่าง) size(): รีเทิร์นจำนวนสมาชิกในคิว isEmpty(): ทดสอบว่าคิวว่างหรือไม่ 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
107
สแตก คิวสองหน้า size() isEmpty() top() last() push(x) insertLast(x)
pop() removeLast() 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
108
คิว คิวสองหน้า size() isEmpty() getFront() first() enqueue(x)
insertLast(x) dequeue() removeFirst() 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
109
การใช้งานคิว พริ้นเตอร์
งานต่างๆที่ถูกส่งไปที่พริ้นเตอร์จะมาต่อคิวกัน จริงๆมีการแซงคิวเพราะความสำคัญของงานไม่เท่ากัน นี่คือเรื่อง priority queue ต่อแถวซื้อของ เราสามารถทำสถานการณ์จำลองเพื่อวิเคราะห์ว่าควรเพิ่มจำนวนแถวหรือไม่ นี่คือเรื่อง simulation การขอดูไฟล์บนไฟล์เซอร์ฟเวอร์ ผู้ใช้งานขอดูได้ในแบบต่อคิว คิวโทรศัพท์ ตอนที่รอสาย 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
110
ใช้คิวในการจัดการเมโมรี่ฮีป
ถ้าเราสร้างคิวของเมโมรี่บล็อกที่ไม่ได้ใช้งานเอาไว้ เมื่อมีการสร้างออบเจ็กต์ใหม่เกิดขึ้น สิ่งที่ต้องทำก็คือ dequeue บล็อกหนึ่งออกมาใช้งาน และพอบล็อกนั้นใช้งานเสร็จสิ้นแล้วก็ enqueue กลับเข้าไป จำลองคิวการให้บริการของร้านค้า หาเวลารอเฉลี่ย คิว จ่ายเงิน 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
111
ให้ลูกค้ามาที่เคาน์เตอร์ ณ เวลา 30, 40, 60,110, 170
A11 A2 A3 A4 A5 D2 D3 เวลาที่ คนที่สามรอคิว คนแรกจ่ายเงิน คนที่สองจ่ายเงิน เวลาที่ คนที่สองรอคิว 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
112
static int SERVE_TIME = 60; //เวลาคิดเงินลูกค้าหนึ่งคน
class Simulation{ static int INFINITY = 1000; static int SERVE_TIME = 60; //เวลาคิดเงินลูกค้าหนึ่งคน int nextArrivalTime; //เวลาต่อไปที่จะมีลูกค้ามาเตรียมจ่ายเงิน int currentTime; int nextDepartureTime; //เวลาต่อไปที่จะมีลูกค้าที่จ่ายเงินเสร็จสิ้น int numberOfCustomers; int waitingTime; int sumOfWaitingTime; QueueList customerQueue; 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
113
public void processArrival(){ currentTime = nextArrivalTime;
numberOfCustomers++; if(nextDepartureTime == INFINITY){ nextDepartureTime = currentTime + SERVE_TIME; } else { customerQueue.enqueue(new Customer(nextArrivalTime)); } เมธอดที่สำคัญจริงๆคือ processArrival() และ processDeparture() เมธอด processArrival() นั้นถูกเรียกใช้เมื่อมีคนพร้อมจะจ่ายเงินที่เคาน์เตอร์ โดยถ้าไม่มีคนจ่ายเงินอยู่ที่เคาน์เตอร์ ก็ให้ไปจ่ายเงินได้เลย ซึ่งเหตุการณ์นี้จะเป็นการตั้งเวลาที่ลูกค้าคนใหม่จะจ่ายเงินเสร็จโดยปริยาย ส่วนในกรณีที่มีคนกำลังจ่ายเงินอยู่ที่เคาน์เตอร์ก็ให้เอาคนใหม่ใส่คิวรอไป ส่วนเมธอด processDeparture() นั้น จะถูกเรียกเมื่อลูกค้าคนหนึ่งจ่ายเงินที่เคาน์เตอร์เสร็จแล้ว โดยถ้าคิวที่รอยังมีคนก็ให้เอาคนที่รอออกจากคิว คำนวณเวลาที่คนที่ออกจากคิวรอคิว และคำนวณเวลาในการรอคิวของส่วนรวม รวมทั้งต้องคำนวณเวลาที่คนใหม่นี้จะจ่ายเงินเสร็จด้วย 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
114
public void processDeparture(){ currentTime = nextDepartureTime;
if(!customerQueue.isEmpty()){ Customer c = (Customer)customerQueue.dequeue(); waitingTime = currentTime - c.getArrivalTime(); sumOfWaitingTime += waitingTime; nextDepartureTime = currentTime + SERVE_TIME; } else{ // กรณีนี้คือไม่มีลูกค้าแล้ว waitingTime = 0; nextDepartureTime = INFINITY; } 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
115
public void process(int time){ nextArrivalTime = time;
while(nextDepartureTime <= nextArrivalTime) //ถ้ายังมีการออกจากคิวที่เกิดก่อนเวลาลูกค้าใหม่มา processDeparture(); processArrival(); } public void calculateMeanWaitingTime(){ double r = (double)sumOfWaitingTime / numberOfCustomers; System.out.println(r); 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
116
public static void main(String[] args){
Simulation a = new Simulation(); while(true){ if(ไม่มีข้อมูลของการมาที่เคาน์เตอร์แล้ว) break; a.process(ข้อมูลเวลาการมาที่เคาน์เตอร์อันที่อ่านมาได้ใหม่); } while(a.nextDepartureTime < INFINITY){ //เมื่อยังมีลูกค้าอยู่ในคิว a.processDeparture(); //ถึงตอนนี้ทุกอย่างได้ทำมาทั้งหมดแล้ว เหลือแต่คำนวณค่าเวลาในการรอโดยเฉลี่ย a.calculateMeanWaitingTime(); 2018/11/28 อ.ดร.วิษณุ โคตรจรัส
งานนำเสนอที่คล้ายกัน
© 2024 SlidePlayer.in.th Inc.
All rights reserved.