Issuu on Google+

!"#

แผนการสอนประจําบทเรียน รายชือ่ อาจารยผจู ดั ทํา สมชาย ประสิทธิจ์ ตู ระกูล หัวขอของเนื้อหา ตอนที่ 12.1 แนวคิดและฟงกชนั แฮช (1 คาบ) เรือ่ งที่ 12.1.1 แนวคิด เรือ่ งที่ 12.1.2 ฟงกชันแฮช ตอนที่ 12.2 การชนและการแกไขปญหาการชน (2 คาบ) เรือ่ งที่ 12.2.1 ปฏิทรรศนวนั เกิด เรือ่ งที่ 12.2.2 การแฮชแบบเปด เรือ่ งที่ 12.2.3 การแฮชแบบปด แนวคิด 1. ตารางแฮชเปนโครงสรางที่มีประสิทธิภาพสําหรับการจัดเก็บขอมูลเพื่อการคนหา การเพิม่ และการลบขอมูล โดยไมมีการดําเนินการที่เกี่ยวกับอันดับของขอมูล 2. ตารางแฮชอาศัยฟงกชันการคํานวณเลขที่อยูของขอมูล ที่เรียกวาฟงกชันแฮช ซึ่งมีหนาที่ แปลงคียของขอมูลไปเปนเลขที่อยูในชองในตารางแฮช 3. ฟงกชันแฮชที่ดีจะใชสามารถคํานวณเลขที่อยูไดรวดเร็ว มีพฤติกรรมกระจายอยางสม่ําเสมอ และแปลงคียไปเปนเลขที่อยูของตารางที่มีขนาดเล็ก 4. การชนคือปญหาที่คียสองคียที่ตางกัน เมื่อนําไปผานฟงกชันแฮชหนึ่งแลวไดเลขที่อยูเดียวกัน ในตารางแฮช 5. ปฏิทรรศนวนั เกิดเปนเหตุการณทแ่ี สดงใหเห็นวา ถึงแมฟงกชันจะกระจายอยางสม่ําเสมอ ก็มี โอกาสทีจ่ ะเกิดการชนของขอมูลได แมกับปริมาณขอมูลที่ไมมากนัก 6. การแกไขปญหาการชนมีแบบเปดและแบบปด 7. การแฮชแบบเปดอาศัยการจัดเก็บขอมูลที่ชนไวในรายการโยงเดียวกัน 8. การแฮชแบบปดอาศัยการหาชองวางในตารางชองใหมเพื่อจัดเก็บขอมูล เมื่อขอมูลถูกแฮชไป ชนกับขอมูลเดิมที่เก็บในตาราง


!"$ 9. วิธีการหาชองวางใหมในการแฮชแบบปดมีแบบ linear probing, quadratic probing, double hashing เปนตน วัตถุประสงค หลังจากศึกษาบทเรียนที่ 12 แลว นักศึกษาสามารถ 1. 2. 3. 4.

เขาใจแนวคิดการจัดเก็บขอมูลในตารางแฮช เขาใจจุดประสงคและสามารถออกแบบฟงกชันแฮชได เขาใจและเปรียบเทียบวิธีการแกไขปญหาการชนของขอมูลตางๆ เขียนโปรแกรมสรางและใชการดําเนินการตางๆ ของการแฮชได

กิจกรรมการเรียนการสอน กิจกรรมทีน่ กั ศึกษาตองทําสําหรับการเรียนการสอน ไดแก 1. ศึกษาเอกสารชุดวิชา/โฮมเพจชุดวิชา ตอนที่ 12.1 และตอนที่ 12.2 2. ทํากิจกรรมของบทเรียนที่ 12 3. ทําแบบประเมินผลของบทเรียนที่ 12 เอกสารประกอบการสอน 1. เอกสารชุดวิชา สื่อการสอน 1. โฮมเพจชุดวิชา 2. สไลดประกอบการบรรยาย (Powerpoint) 3. โปรแกรมคอมพิวเตอร ประเมินผล 1. ประเมินผลจากกิจกรรมที่ทํา 2. ประเมินผลจากคําถามทายบทเรียน


!"% ตอนที่ 12.1 แนวคิดและฟงกชนั แฮช หัวเรื่อง เรือ่ งที่ 12.1.1 แนวคิด เรือ่ งที่ 12.1.2 ฟงกชันแฮช แนวคิด 1. ตารางแฮชเปนโครงสรางที่มีประสิทธิภาพสําหรับการจัดเก็บขอมูลเพื่อการคนหา การเพิม่ และการลบขอมูล โดยไมมีการดําเนินการที่เกี่ยวกับอันดับของขอมูล 2. ตารางแฮชอาศัยฟงกชันการคํานวณเลขที่อยูของขอมูล ที่เรียกวาฟงกชันแฮช ซึ่งมีหนาที่ แปลงคียของขอมูลไปเปนเลขที่อยูในชองในตารางแฮช 3. ฟงกชันแฮชที่ดีจะใชสามารถคํานวณเลขที่อยูไดรวดเร็ว มีพฤติกรรมกระจายอยางสม่ําเสมอ และแปลงคียไปเปนเลขที่อยูของตารางที่มีขนาดเล็ก วัตถุประสงค หลังจากที่ศึกษาตอนที่ 12.1 แลว นักศึกษาสามารถ 1. เขาใจแนวคิดการจัดเก็บขอมูลในตารางแฮช 2. เขาใจจุดประสงคและสามารถออกแบบฟงกชันแฮชได เรื่องที่ 12.1.1 แนวคิด จะวาไปแลวตนไมคนหาแบบทวิภาคนั้น เปนโครงสรางขอมูลทีร่ องรับความตองการสารพัดรูปแบบที่ มีประสิทธิภาพดี ไมวาจะเปนการเพิ่ม ลบ คนหา คนหาตัวนอยสุด ตัวมากสุด ตัวถัดไป หรือแมกระทั่งไลเรียง ขอมูลจากนอยไปมาก ก็กระทําไดอยางมีประสิทธิภาพ แตถาเราจํากัดความตองการเหลือแคการเพิ่ม ลบ และ คนหา โดยไมมกี ารดําเนินการทีเ่ กีย่ วของกับอันดับของขอมูลเลย เราสามารถใชโครงสรางขอมูลอีกแบบหนึ่ง ที่เรียกวาตารางแฮช (hash table) ซึ่งมีประสิทธิภาพที่เหนือกวา แนวคิดของตารางแฮชนี้ถูกนําเสนอโดย Dumey ในป ค.ศ. 1956 ! สําหรับปญหาตารางสัญลักษณ (symbol table) ที่ใชเก็บชื่อตัวแปร และชื่อฟงกชัน ตางๆ ของโปรแกรมระหวางการแปล (compile) โปรแกรมคอมพิวเตอร สมมติวาเราตองการเก็บขอมูลของนักศึกษาจํานวน 700 คน โดยใชรหัสประจําตัวเปนคียในการคน หา (กําหนดใหรหัสประจําตัวเปนตัวเลข 10 หลัก) ถาเราใชตนไมคนหาแบบทวิภาคจัดเก็บก็จะไดตนไมซึ่งมี ความสูงอยางนอย log2 700 = 9 แสดงวากรณีดที ส่ี ดุ (นัน่ คือไดตน ไมทไ่ี ดดลุ สูง 9) การเพิม่ ลบ และคน หาในตนไมนี้มีการวิ่งผานโหนดตางๆ ในตนไมไมเกิน 9+1 = 10 โหนด ประสิทธิภาพการทํางานถูกกําหนด !

A. L. Dumey, Computers and Automation 5 (1956) 6–9.


!!& ดวยความสูงของตนไม โครงสรางของตนไมนั้นจํากัดใหตนไมตองสูงอยางนอย log2 n จะทําใหเตี้ยกวานี้ก็ คงทําไมได 2 ถาเรามีความทะเยอทะยานตองการใหประสิทธิภาพของการเพิม่ ลบ และคนหาดีกกวานี้ จะทําอยาง ไร เชนตองการใชเวลาคงตัวเสมอในการเพิม่ ลบ และคนหาขอมูล โดยไมขึ้นกับจํานวนขอมูล (จะ ทะเยอทะยานเกินไปหรือเปลา ขอใหลองคิดดูเลนๆ ตอนนีก้ อ น) ใหสังเกตวาโครงสรางของตนไมนั้นบังคับ การเขาถึงขอมูลวาจะตองกระทําผานเปนดานๆ จากรากวิง่ เลือ้ ยลงมาเรือ่ ยๆ จึงเปนสาเหตุใหใชเวลาไมคงตัว ถาเราเปลี่ยนมาใชตาราง (ซึง่ ก็คอื แถวลําดับหรืออะเรยนน่ั เอง) เก็บขอมูล แลวอาศัยการคํานวณหาเลขทีอ่ ยู ของขอมูลในตารางเอาเลย คือใชสูตรหรือฟงกชันพิเศษ h(x) ทีส่ ามารถเปลีย่ น x ซึ่งคือคียของขอมูลไปเปน เลขที่อยูของขอมูลในตาราง ปญหาที่ทาทายก็คือจะหาฟงกชันนี้ไดอยางไร ซึง่ สามารถคํานวณหาเลขทีอ่ ยูไ ด ในเวลาคงตัว โดยฟงกชันนี้ตองเปนแบบหนึ่งตอหนึ่งทั่วถึง (bijective function) นัน่ คือถาคียต า งกัน เลขทีอ่ ยู ที่ไดจากฟงกชันนี้ตองตางกันดวย จะวาไปแลวฟงกชนั คํานวณเลขทีอ่ ยูท เ่ี ราตองการนัน้ หาไดไมยากเลย เชนฟงกชัน h(x) = x เปน ฟงกชันที่หาเลขที่อยูไดตามที่ตองการ h(x) = x ใหความหมายวาใหใชคาของคียมาเปนตัวระบุเลขที่อยูของ คียนั้น เชนขอมูลของนักศึกษารหัส 4230010121 ก็ใหจัดเก็บไวที่ชองที่ 4230010121 ของตาราง สูตรนีง้ า ย แตสรางปญหามากมายตามมา เพราะเราตองจองตารางขนาดมหึมาเพื่อเก็บขอมูลของนักศึกษาเพียง 700 คน (รหัสประจําตัวเปนเลขฐานสิบขนาดสิบหลัก ก็ตองจองตารางขนาด 1010 ชอง !!!) เพื่อใหแนวคิดการใชฟงกชันคํานวณเลขที่อยูของขอมูลเปนไปไดในทางปฏิบัติ จึงตองจํากัดใหตา รางที่เก็บขอมูลนั้นตองมีขนาด “ไมใหญมาก” ความจริงนาจะเขียนวา “ตารางมีขนาดเทากับจํานวนขอมูล” แตไมกลา เพราะจะบีบกันขนาดนี้อาจหาฟงกชันที่ตองการยาก ลองมาดูตัวอยางรหัสประจําตัวของนักเรียน สัก 10 คนที่เราสุมตัวอยางมา (จาก 700 คน) ขางลางนี้ 3933324521, 3930852021, 3931458621, 3932799821, 3930127721 3931053021, 3930883421, 3931835521, 3931569621, 3931960221 เห็นไดชัดวามีตัวเลขหลายๆ หลักในรหัสประจําตัวที่เหมือนกัน (ความจริงนักศึกษาทั้ง 150 คนนี้ เปนนักศึกษาคณะวิศวะฯ ซึง่ เขาเรียนในป 2539 ทุกคนมีตัวเลข 5 หลักที่ถูกตัดออกนี้เหมือนกันหมด) เพียง แคตัดทิ้งไปไมนํามาพิจารณา ก็จะไดฟงกชัน h(x) ซึง่ ตองการตารางขนาดเล็กกวา เชนเราตัดสองหลักขวาที่ เปน 21 และ 3 หลักซายทีเ่ ปน 393 ออก ไดเปนฟงกชนั ขางลางนี้

h(x) = x / 100 mod 105 ก็จะเหลือรหัสเพียง 5 หลัก จึงตองใชตารางขนาดหนึ่งแสนชอง ถึงแมวาจะมีขนาดเล็กกวา 1010 ชองมากมาย แตกย็ งั ถือวาสิน้ เปลืองเหลือเกิน จองไวแสน ใชแคเจ็ดรอย แลวเราจะหาฟงกชันอื่นที่ตองการขนาดตารางที่

2

อาจทําใหเตี้ยลงไดถาเปลี่ยนเปนตนไมคนหาแบบ m ภาค นั่นคือหนึ่งโหนดมีลูกไดไมเกิน m ลูก แตอยางไรก็ตามตนไมกย็ งั สูง เปน O( log m ) อยูด ี


!!" นอยกวาไดหรือไม ถาดูจากนักศึกษา 10 คนที่เราสุมตัวอยางมาจะเห็นไดวาหลักจากตัดเลขออก 5 ตัวแลว จะไดรหัสตางๆ ดังนี้ 33245, 08520, 14586, 27998, 01277, 10530, 08834, 18355, 15696, 19602 ถาเราสนใจเลข 3 หลักทางขวาก็จะเห็นไดวาไมเหมือนกันเลย แตเราคงดวนสรุปวาใหตดั ออกอีก 2 หลักทาง ซาย แลวใชสามหลักที่เหลือเปนเลขที่อยู ก็หายจะเร็วเกินไป เพราะนีเ่ ปนเพียง 10 คนจาก 700 เราจะไป ทราบไดอยางไรวาจะไดคาที่ไมซ้ํากัน (ถาไดก็ดีเพราะจะใชตารางแคหันชองเพื่อเก็บขอมูล 700 คน ซึ่งก็ถือ วาคุมทีเดียว กับการที่เราสามารถหาตําแหนงของขอมูลไดในเวลาคงตัว) ถาเราทราบรหัสของนักศึกษาทัง้ 700 คน ก็อาจนํามาพิจารณาหาสูตรที่คํานวณเลขที่อยูซึ่งใชขนาด ของตารางที่ไมใหญมากได (แตคงเหนื่อยมากๆ ถาดูดวยตา เขียนโปรแกรมชวยก็อาจเปนไปได) แตประเด็น ของการเก็บขอมูลอยูตรงที่วาเรารูเพียงชวงที่เปนไปไดของคีย แตเราไมทราบคาของคียตางๆ ในชุดขอมูลที่ จะมาจัดเก็บ เชนเราเพียงแตรวู า รหัสประจําตัวเปนเลข 10 หลัก เมือ่ ตัดเลขซ้าํ ออกแลวเหลือ 5 หลัก มีคาเปน ไปไดตั้งแต 00000 ถึง 99999 แตเราไมทราบหรอกวา หมายเลขใดในชวงนี้จะถูกจัดเก็บ ปญหาจึงมีอยูวาจะ ออกแบบฟงกชันคํานวณเลขที่อยูไดอยางไร แฮชชิง (hashing) ใชแนวคิดของการใชฟงกชันคํานวณเลขที่อยูที่ไดเสนอมา เพียงแตคราวนี้ อนุญาตใหฟงกชันคํานวณเลขที่อยูนี้เปนแบบ many-to-one ได นั่นคือคียสองคียที่ตางๆ กันมีเลขที่อยูเหมือน กันได (เรียกวาเกิด “ชน” กันของคีย) อานแลวก็คงเอะใจวา การอนุญาตเชนนีย้ อ มสรางปญหาแนนอนเพราะ หนึ่งชองในตารางนั้นเก็บขอมูลไดแคตัวเดียว แลวจะทําอยางไร แฮชชิงอาศัยความจริงที่วาถากําหนดใหตา รางเก็บขอมูลไมใหญมาก การออกแบบฟงกชนั คํานวณเลขทีอ่ ยูเ พือ่ ไมใหเกิดปญหาการชนดังกลาวเลยเปน เรือ่ งยาก (ถาเราไมรชู ดุ ขอมูลกอน) และคงเปนไปไมไดดวย (จะไดนาํ เสนอในรายละเอียดตอไป) จึงควรให ความสนใจวา จะออกแบบฟงกชันคํานวณเลขที่อยูนี้อยางไรจึงจะบรรเทาหรือลดจํานวนการชนใหนอยๆ และ เมือ่ เกิดการชนจะแกปญ  หาอยางไร ซึง่ จะไดนาํ เสนอตอไป จุดเดนของแฮชชิงอยูตรงที่วา เราสามารถควบ คุมเวลาการทํางานของการเพิม่ ลบ และคนหา ใหอยูภายในขอบเขตที่ตองการได โดยใชเนื้อที่หนวยความจํา แลกกับเวลาการทํางาน ซึง่ เปนปรากฏการณทไ่ี มเกิดขึน้ กับการจัดเก็บขอมูลแบบรายการหรือตนไม เรื่องที่ 12.1.2 ฟงกชันแฮช แฮชชิงใชแนวคิดของการใชฟงกชันคํานวณเลขที่อยูที่เรียกวาฟงกชันแฮช (hash function) ซึ่งมี พฤติกรรมที่แลดู “มั่วๆ” “สุมๆ” “เละๆ” หมายความวาเมื่อดูจากคาของ h(x) แลวแทบจะดูไมเห็นความ สัมพันธอะไรเลยกับ x ซึ่งเปนคีย (พฤติกรรมเชนนี้เองซึ่งเปนที่มาของคําวา “hash” นักศึกษาที่ยังไมรูวาคํา ภาษาอังกฤษสัน้ ๆ นีแ้ ปลวาอะไรก็อยากใหเปดพจนานุกรมตอนนีเ้ ลย) มาดูความหมายอยางเปนทางการกันสักหนอย กําหนดใหคียตางๆ เปนจํานวนเต็มในชวง [0, U–1] และ m คือขนาดของตารางซึ่งเรียกวาตารางแฮช (hash table) ฟงกชันแฮชมีหนาที่แปลงจํานวนเต็มในชวง [0, U–1] เปนจํานวนเต็มในชวง [0, m–1] ฟงกชันแฮชที่เราตองการจะตองมีคุณสมบัติดังนี้ 1. คํานวณเลขทีอ่ ยูไ ดรวดเร็ว ในเวลาคงตัว (โดยทั่วไปใชเวลาแปรตามขนาดของคีย ไมใชจํานวน คีย)


!!! 2. ลดจํานวนการชน (ไมมีการชนเลยไดยิ่งดี) แตเนื่องจากจํานวนการชนจะมีมากมีนอยก็ขึ้นกับ เซตของคียตางๆ ทีถ่ กู เลือกมาเก็บ จึงตีความแบบรวมๆ วาโอกาสที่แตละคียจะไปตกอยูใน ชองใดในตารางมีเทาๆ กัน (เรียกคุณสมบัตเิ ชนนีว้ า simple uniform hashing) ขอขยายความในเรื่องของ simple uniform hashing ถา P(x) คือความนาจะเปนที่คีย x จะถูกเลือก มาจากเซตของคียที่เปนไปไดทั้งหมด {0,1,2,...,U -1} และ s! คือเซตของคียทั้งหมดซึ่งเมื่อถูกแฮชแลวจะไป ตกที่ชอง j นัน่ คือ s! = { x | h(x) = j } ฟงกชันแฮช h(x) จะเปน simple uniform hashing เมื่อ $ # #" = # %!

! "

สําหรับ j = 0, 1, ..., m –1

คิดกรณีงา ยๆ และใชตัวอยางประกอบก็แลวกัน สมมติวาใชรหัสประจําตัว 10 หลักเปนคีย คาของ คียท เ่ี ปนไปไดกค็ อื 0000000000 ถึง 9999999999 (นัน่ คือ U = 1010) กําหนดใหทุกๆ คาของคียเหลานี้มีสิทธิ์ ถูกเลือกมาเก็บไดเทาๆ กัน หมายความวา P(x) มีคาเทากัน (ซึง่ ก็คอื เทากับ 1/1010) ถาเรานําคียท เ่ี ปนไปได ทุกๆ คีย (คือลุยตั้งแตคีย 0000000000 ถึง 9999999999) มาผาน h(x) ไดผลซึง่ ระบุเลขทีอ่ ยูข องตาราง จะได วาแตละชองในตารางจะมีคียที่อยากมาอยูเปนจํานวนเทาๆ กัน หมายความวาการ “โปรย” คียตางๆ ลงตา รางดวย h(x) นัน้ กระจายอยางสม่าํ เสมอดีมาก สมมติตอวาถาชุดของคียที่มาเก็บมี n ตัว โดยที่ n < m และ n << U (อานเครือ่ งหมาย << นี้วานอยกวามากๆ) เชน m = 1000, n = 700 คีย 700 ตัวที่ถูกเลือกมาจาก 1010 คาทีเ่ ปนไปไดนน้ั เมื่อนํามาโปรยลงตารางแฮชโดยใช h(x) ก็นา จะชนกันนอย (หรือวาไมชนกันเลย ?) ขาวรายก็คือวาในทางปฏิบัติเรามักไมรูคา P(x) ทําใหการออกแบบฟงกชันแฮชที่ใหไดผลกระจาย อยางสม่าํ เสนอตามทีต่ อ งการนัน้ เห็นจะเปนไปไดยาก อยางไรก็ตามมีกลวิธีตางๆที่ใชสําหรับออกแบบ ฟงกชันแฮชที่ไดดีในทางปฏิบัติ ดังนี้ 3 1. การวิเคราะหเลขโดด (digit analysis) : กลวิธนี ก้ี ค็ อื การคัดเลือกเลขโดดบางหลักของคียม า พิจารณา ดังตัวอยางที่ไดนําเสนอมาในตอนตน (กรณีตดั สองหลักขวาทีเ่ ปน 21 และ 3 หลัก ซายทีเ่ ปน 393 ออกจากรหสประจําตัวนักศึกษาทีน่ าํ เสนอในหนาที่ 220 ) โดยเราตองมีความ มั่นใจวาสิ่งที่ตัดไปนั้นไมมีผลทําใหเกิดความเอนเอียงในการกระจายของคีย 2. การพับ (folding) : กลวิธนี บ้ี างครัง้ ก็เรียกกันวาเปนการบีบอัดคีย โดยใชแนวคิดในการแบงคีย ออกเปนสวนๆ แลวนําสวนตางๆ เหลานีม้ า "รวม" กัน การรวมในที่นี้อาจหมายถึงการนําสวน ตางๆ ที่ไดจากการแบงมาบวกกัน หรือ exclusive or กันเปนตน ลักษณะการแบงแลวรวมเขน กันแลดูคลายการนําคียยาวๆ มาพับหลายๆ ทบใหมีขนาดเล็กลง ตัวอยางเชนเราจะแบงคีย ออกเปนสวนๆ สวนละ 3 หลัก แลวนําทุกๆสวนมาบวกกัน ถา x = 2217365 ก็จะไดเปน 221+736+5 = 962

3

ตองบอกตรงนี้เลยวากลวิธีการออกแบบฟงกชันใหแลดู "เละๆ" "มัว่ ๆ" เหลานี้ใชไดดีในทางปฏิบัติ มีการวิเคราะหเชิง คณิตศาสตรกนั มากมายแตกต็ ง้ั อยูบ นสมมติฐานตางๆ นาๆ ทีจ่ ะไมขอกลาวในรายละเอียดในวิชานี้


!!' 3. กลางกําลังสอง (midsquare) : กลวิธนี ส้ี รางความ "มั่ว" ใหกับคียอยางเห็นไดชัด นัน่ คือเรานํา คีย x มายกกําลังสอง แลวตัด k ทางซายและทางขวาทิ้ง เหลือแตตรงกลางนํามาใชเปนผลลัพธ เช���น x = 2217365 ยกกําลังสองได 4916707543225 ให k = 4 ก็คอื ตัดสีห่ ลักขวาและสีห่ ลัก ซายทิ้งไป นัน่ คือ 4916707543225 ไดเปน 70754 ดังนั้นสามารถบรรยายฟงกชันไดในรูปแบบ h(x) = (x2 div 10k) mod 10k 4. การหาร (division) : กลวิธีนี้ใชการหารคียดวยคาคงตัว p แลวเอาเศษเปนผลลัพธนน่ั คือ h(x) = x mod p ถาจองตารางใหมีขนาดเปน p ดวย ก็จะสบายใจไดวา x mod p ตองไดผลตกอยูใน ชวงของตารางที่จองไวแนนอน (เพราะ x mod p ตองไดผลในชวง [0, p-1] ) โดยทั่วไปมักหลีก เลี่ยงไมใช p ทีเ่ ปนจํานวนในรูปแบบ 2k เพราะนัน่ หมายความเศษของการหารก็คอื k บิตทาง ขวาของคีย หรือในกรณีที่คียมีความหมายเปนเลขฐานสิบ ก็มกั หลีกเลีย่ ง p ในรูปแบบ 10k ซึง่ ผลการหารคือ k หลักทางขวาเชนกัน ทั้งนี้เพราะวาเรานาจะใชทุกๆ หลักของคียมาพิจารณา (ตรงนี้อยูใตสมมติฐานวาทุกหลักมีความหมายที่เราไมตัดทิ้ง) คา p ที่ดีที่มักใชกันก็คือ p ทีเ่ ปน จํานวนเฉพาะซึ่งมีคาไมใกลกับจํานวนที่อยูในรูปแบบ 2k 5. การคูณ (multiplication) : กลวิธีนี้อาศัยการคูณคียดวยจํานวนจริง z ที่มีคาระหวาง [0,1) จาก นัน้ จึงนําไปคูณกับจํานวนเต็ม m ซึ่งแทนขนาดของตารางแฮช บรรยายไดดงั นี้ h(x) = trunc( m * fract( x * z ) ) ผลลัพธที่ไดจะตกอยูในชวง 0 ถึง m -1 ไดมกี ารศึกษากันวาคา z ที่ใหผลดีก็คือ ( 5 -1)/2 ≈ 0.618 ผูออกแบบฟงกชันแฮชสามารถใชกลวิธีตางๆ ขางตนหลายๆ รูปแบบผสมผสานกันก็ได แตอยาลืม วาการคํานวณของฟงกชันแฮชจะตองกระทําไดรวดเร็ว และผลที่ไดตองอยูในชวง 0 ถึง m -1 โดยที่ m คือ ขนาดของตาราง ไดมีการศึกษาวิเคราะหรูปแบบของฟงกชันแฮชกันอยางมากมาย ถึงแมเราจะเลือกใช ฟงกชันแฮชแบบใดก็ตาม ก็ยอมตองมีชุดของขอมูลที่ทําใหเกิดการชนมากมายได (เพราะถาเรารูว า ฟงกชนั แฮชเปนอยางไร และตองการแกลงฟงกชันแฮชนั้น ก็ยอมหาขอมูลที่แกลงใหเกิดการชนไดแนๆ) วิธีหนึ่งที่ หลีกเลี่ยงปญหาดังกลาวก็คือการปองกันการใชฟงกชันที่มีรูปแบบไมตายตัว โดยที่ใชกันไดดีในทางปฏิบัติ เปนดังนี้ h(x) = ((ax + b) mod p ) mod m k เปนจํานวนเต็มในชวง [0, U–1] m เปนขนาดของตารางแฮช p เปนจํานวนเฉพาะที่มีคาในชวง [U, 2U ) a และ b เปนคาคงตัวโดยที่ 0 < a < p และ 0 ≤ b < p จุดที่อนุญาตใหเราปองกันไมใหมีใครมา เดาฟงกชันแฮชไดก็คือคาของ a และ b โดยเรากําหนดให a และ b มีคา เปนจํานวนสุม โดยตอนเริ่มตนการ ใชงานตารางแฮชก็สุมคาทั้งสอง จากนั้นก็ใชคานั้นไปตลอดการทํางานของตารางแฮชนั้น ดวยกลวิธีแบบนี้ทํา ใหใครก็ตามที่ตองการใสขอมูลขาเขาที่แกลงใหการชนบอยๆ จะไดทํางานชาๆ นัน้ กระทําไมได เพราะคา a


!!( และ b ที่ใชเปลี่ยนทุกครั้งที่โปรแกรมเริ่มทํางาน โปรแกรม) 4

(แตพอตั้งคาแลวก็เปนคาคงตัวตลอดการทํางานของ

สําหรับกรณีที่คียของขอมูลไมใชตัวเลข เชนชื่อคน ชือ่ เครือ่ งคอมพิวเตอร ชือ่ โดเมนในอินเตอรเนต เปนตน ก็สามารถแปลงเปนตัวเลขไดโดยการมองสตริงของตัวอักษรเสมือนตัวเลขฐานสูงๆ เชนฐาน 26 (กรณี เปนสตริงของตัวอักษรภาษาอังกฤษ) ตัวอยางเชน ‘BARNEY’ เมือ่ มองเปนฐาน 26 ก็จะไดเปน 5 ord(‘B’)*265 + ord(‘A’)*264 + ord(‘R’)*263 + ord(‘N’)*262 + ord(‘E’)*261 + ord(‘Y’)*260 = 66*265 + 65*264 + 82*263 + 78*262 + 69*261 + 89*260 = 815370099 นักศึกษาเห็นวิธีแปลงขางบนนี้คงรูสึกวายุงยาก ตองมีการยกกําลังมากมายซ้ําซาก เสียเวลา อีกทัง้ ถาสตริงมีความยาวมาก คงไดผลลัพธที่มีขนาดใหญเกินขนาดของจํานวนเต็มที่รับไดในภาษาปาสกาล เรา สามารถจัดรูปแบบการคํานวณใหกระทัดรัดขึน้ ไดดงั นี้ 1. เลือกพิจารณาฐานในรูปแบบทีง่ า ยตอการคํานวณเชนฐาน 32 (เนือ่ งจากการคูณจํานวนใดดวย 32 ก็คอื การเลือ่ นบิตไปทางซาย 5 บิต) 2. ใช Horner’s rule ในการจัดรูปแบบใหม ซึ่งอาศัยการแยกตัวประกอบตัวคูณตางๆ (ซึง่ ก็คอื เลข ฐาน) ออกเปนวงเล็บซอนวงเล็บ (ซึ่งงายตอการเขียนโปรแกรมดวยวงวน) ตัวอยางเชนสตริง ‘BABY’ ถาใช Horner’s rule แปลงเปนจํานวนเต็มโดยมองเปนฐาน 32 จะไดเปน ( ( ord(‘B’) * 26 + ord(‘A’) ) * 26 + ord(‘B’) ) * 26 + ord(‘Y’) 3. ใชกลวิธีการหารเอาแตเศษ ( mod ) ดวยขนาดของตารางแฮชภายในแตละวงเล็บระหวางการ คํานวณดวย Horner’s rule เพื่อปองกันมิใหผลลัพธมีคาใหญเกินไป และก็ไมไดทําใหผลลัพธ ของการคํานวณแตกตางไปจากการไมใช Horner’s rule แลวคอยมา mod ตอนสุดทายแต อยางไร (จากความรูในเรื่อง congruence ที่ขอจะไมอธิบายในที่นี้) ดวยวิธีทั้งสามขางตนเขียนไดเปนฟงกชันการแปลงสตริงเปนตัวเลขดังนี้ FUNCTION Hash( Key : String ) : integer; VAR H, I, Len : integer; BEGIN Len := Length( Key ); H:= ord( key[1] ); FOR I := 2 to len DO H := ( H * 32 + ord( x[i] ) ) MOD TableSize; Hash := H; END;

4

5

รูปแบบของฟงกชนั แฮชดังกลาว ซึง่ อนุญาตใหเราเปลีย่ นคาของ a และ b ไดมากมายนีเ้ รียกวาการแฮชเชิงจักรภพ (universal hashing) จึงจะไมขออธิบายและวิเคราะหในที่นี้ ในภาษาปาสกาล ord จะคืนคา ASCII ของตัวอักษรทีไ่ ดรบั เชนรหัส ASCII ของ ‘B’ คือ 42H = 6510


!!) ตอนที่ 12.2 การชนและการแกไขปญหาการชน หัวเรื่อง เรือ่ งที่ 12.2.1 ปฏิทรรศนวนั เกิด เรือ่ งที่ 12.2.2 การแฮชแบบเปด เรือ่ งที่ 12.2.3 การแฮชแบบปด แนวคิด 1. การชนคือปญหาที่คียสองคียที่ตางกัน เมื่อนําไปผานฟงกชันแฮชหนึ่งแลวไดเลขที่อยูเดียวกัน ในตารางแฮช 2. ปฏิทรรศนวนั เกิดเปนเหตุการณทแ่ี สดงใหเห็นวา ถึงแมฟงกชันจะกระจายอยางสม่ําเสมอ ก็มี โอกาสทีจ่ ะเกิดการชนของขอมูลได แมกับปริมาณขอมูลที่ไมมากนัก 3. การแกไขปญหาการชนมีแบบเปดและแบบปด 4. การแฮชแบบเปดอาศัยการจัดเก็บขอมูลที่ชนไวในรายการโยงเดียวกัน 5. การแฮชแบบปดอาศัยการหาชองวางในตารางชองใหมเพื่อจัดเก็บขอมูล เมื่อขอมูลถูกแฮชไป ชนกับขอมูลเดิมที่เก็บในตาราง 6. วิธีการหาชองวางใหมในการแฮชแบบปดมีแบบ linear probing, quadratic probing, double hashing เปนตน วัตถุประสงค หลังจากที่ศึกษาตอนที่ 12.2 แลว นักศึกษาสามารถ 1. เขาใจและเปรียบเทียบวิธีการแกไขปญหาการชนของขอมูลตางๆ 2. เขียนโปรแกรมสรางและใชการดําเนินการตางๆ ของการแฮชได เรื่องที่ 12.2.1 ปฏิทรรศนวันเกิด ตัวอยางที่ผานมาในเรื่องที่ 12.1.1 เราใชรหัสประจําตัวเปนคียของนักศึกษา แลวพยายามหาฟงกชัน คํานวณเลขที่อยูจากรหัสประจําตัว คราวนี้จะขอใชวิธีใหม คือจะขอใชวนั /เดือนเกิดของนักศึกษาเปนตัวระบุ เลขที่อยูของขอมูลนักศึกษาในตาราง นัน่ คือใครเกิดวันที่ 1 มกราคม ก็จะถูกเก็บไวในตารางชองที่ 0 เกิดวันที่ 2 มกราคม ก็จะถูกเก็บไวในตารางชองที่ 1 ... เกิดวันที่ 31 ธันวาคม ก็จะถูกเก็บไวในตารางชองที่ 365 ถาทํา เชนนี้ยอมแสดงวาตองจองตารางขนาด 366 ชอง สมมติวาเราตองการเก็บขอมูลของนักศึกษาจํานวน 40 คน ก็เรียกไดวาจองไวเกินตั้งเกือบสิบเทาของจํานวนขอมูล ซึ่งอาจรูสึกไมคอยคุม แตถา เราสามารถเพิม่ ลบ และ คนหาขอมูลไดรวดเร็วมาก ก็นับวาคุมทีเดียว


!!* เหตุผลทีเ่ ลือกวันเดือนเกิดใหเปนตัวเลขระบุเลขทีอ่ ยูใ นตาราง ก็เพราะรูส กึ วาวันเดือนของปนา จะมี ลักษณะกระจายอยางสม่าํ เสมอ มีพฤติกรรมสุมพอสมควร นาจะใชเปนฟงกชันแฮชของ "คน" ได คือเราแทบ ประกันอะไรไมไดเลยวาคนสองคนทีม่ วี นั เดือนเกิดเหมือนกัน จะมีลกั ษณะอะไรเหมือนกันบางจากขอมูล ประจํานักศึกษา ฟงกชนั คํานวณเลขทีอ่ ยูซ ง่ึ มีลกั ษณะคลายกับตัวเลขสุม เปนคุณสม���ัตทิ เ่ี ราตองการ ถาเราตองการจัดเก็บขอมูลนักศึกษาเกิน 366 คน ยอมตองเกิดปญหาการชนแนๆ แตในที่นี้สมมติ วาเราตองการเก็บขอมูลเพียง 40 คนจาก 366 ชองที่มีใหจัดเก็บ ก็นา จะรูส กึ ปลอดภัยมากๆ เลยวาไมชน แนๆ มาดูตัวอยางกันดีกวา ภาพประกอบ 1 แสดงผังการกระจายวันเดือนเกิดของคน 40 คน 6 พบวามีคน สองคูท เ่ี กิดวันเดียวกัน (นัน่ คือเกิดการชนกันทีส่ องตําแหนงในตาราง) ตรงนี้เห็นผลแลวคงขัดกับความรูสึก ครั้งแรกที่บอกวาการที่คน 40 คนจะมีวนั เกิดซ้าํ กันสักสองคนนัน้ มีโอกาสนอยมาก

#Hits

2

1

0 Day of Year

ภาพประกอบ 12.1 ตัวอยางผังการกระจายวันเดือนเกิดของนักศึกษาจํานวน 41 คน เราจะมาหาคําตอบวาตองมีคนอยางนอยสักกี่คนในหองๆ หนึ่ง จึงจะทําใหโอกาสที่มีคนสองคนใน หองนัน้ มีวนั เดือนเกิดเดียวกันมีเกิน 50% กําหนดใหวนั เดือนเกิดของคนนัน้ นับวาเปนฟงกชนั ทีก่ ระจายดี มากๆ นั่นหมายความวาคนๆ หนึ่งมีโอกาสที่จะมีวันเดือนเกิดตามที่กําหนดใหเปน 1/366 ถากําหนดใหมีคน m คนในหอง 1. พิจารณาคนที่ 1 ความนาจะเปนทีไ่ มมวี นั เดือนเกิดซ้าํ กันยอมเปน 1 2. พิจารณาคนที่ 1, 2 ความนาจะเปนทีไ่ มมวี นั เดือนเกิดซ้าํ กันยอมเปน (365/366) 3. พิจารณาคนที่ 1, 2, 3 ความนาจะเปนทีไ่ มมวี นั เดือนเกิดซ้าํ กันยอมเปน (365/366)(364/366) 4. ... 5. พิจารณาคนที่ 1, 2, 3, ..., m ความนาจะเปนทีไ่ มมวี นั เดือนเกิดซ้าํ กันยอมเปน -'**.!/"0 '** '*) '*( '**+⋅+'**+⋅+'**+⋅+,,,++⋅+ '** +

6

ผูเขียนไดขอมูลนี้จากการไลถามวันเกิดนักศึกษาในหองขณะสอนหัวขอ hashing ในป 2543 (รวมวันเดือนเกิดของผูเ ขียนดวย)


!!# คําถามก็คือวา m ตองมีคาอยางนอยเทาไร จึงทําใหคาขางบบนี้นอยกวา 0.5 เขียนโปรแกรม คอมพิวเตอรลองแปรคา m ดูก็จะพบวา m ตองมีคาอยางนอย 23 หมายความวาถาหองๆ หนึ่งมีคนอยางนอย 24 คน ก็มโี อกาสเกินครึง่ ทีจ่ ะมีสองคนในหองนีท้ ม่ี วี นั เดือนเกิดเหมือนกัน เหตุการณเชนนีเ้ รียกวาปฏิทรรศน วันเกิด (Birthday Paradox) ซึ่งออกจะขัดกับความรูสึกของคนทั่วไปที่คิดวานาจะตองมีจํานวนคนมากกวานี้ ปฏิทรรศนวันเกิดนั้นบอกเราวาถึงแมฟงกชันแฮชที่ใชจะดี (เทียบกับวันเดือนเกิดของคน ซึง่ คาดวา นาจะกระจายดี) และขอมูลมีไมมาก ก็มีโอกาสที่จะเกิดการชน (นั่นคือเพียงแค 24 คน ก็มโี อกาสเกินครึง่ วาจะ มีวนั เดือนเกิดซ้าํ กัน) ดังนั้นจึงจําเปนตองหาวิธีการแกไขปญหาการชนของขอมูลวาจะตองทําอยางไร วิธีแก ไขแบงไดเปนสองวิธีหลักคือ การแฮชแบบเปด (open hashing) และการแฮชแบบปด (closed hashing) เรื่องที่ 12.2.3 การแฮชแบบปด การแฮชแบบปด (closed hashing) บางทีเรียกกันวา open addressing มีแนวคิดในการจัดเก็บขอ มูลทั้งหมดในตารางแฮช เมื่อมีการชนเกิดขึ้น ก็จะแกไขปญหาดวยการหาชองวางชองอื่นในตาราง เมือ่ เปรียบ เทียบกับการแฮชแบบเปดนั้นก็จะพบวาไมมีการจองเนื้อที่หนวยความจําใหม (ดวยคําสัง่ new) และไมตอ งเสีย เนื้อที่ที่เก็บตัวชี้ในรายการโยง (เพราะไมไดใช) แตตองเสียเวลาหาชองวางใหมในตาราง เมื่อขอมูลถูกแฮชครั้งแรกจะได h(x) เปนเลขที่อยูของตารางที่ตองการเก็บขอมูล ถาไมวาง (เกิดการ ชน) ก็จะไปดูชอ ง h1(x) ถาไมวางอีกก็ไปดูชอง h2(x) กระทําเชนนีไ้ ปเรือ่ ยๆ จนกวาจะพบชองวาง จะขอเรียก ชองแรกที่ไปดูวา h0(x) นัน่ คือ h0(x) = h(x) ดังนัน้ hi(x) คือเลขที่อยูของตารางที่สนใจสําหรับคีย x หลังการ ชนครั้งที่ i นิยามให hi(x) = ( h(x) + S(x, i) ) mod m โดยที่ S(x, i) คือฟงกชันในการคํานวณระยะหางจากชองแรกที่ถูกแฮช h(x) ที่สนใจดู 7 ของคีย x หลังการชน ครั้งที่ i โดยทั่วไปก็หวังวาจะไดลําดับของการดูชองตางๆ h0(x), h1(x), h2(x),... ที่มีจํานวนไมมากนัก จะได เร็วๆ จํานวน probe จะนอยก็เมือ่ ตารางไม “แนน” หมายความวาคา load factor λ$ซึง่ คือสัดสวนของจํานวน ขอมูลที่เก็บในตารางกับขนาดของตารางมีคาไมสูง สําหรับการแฮชแบบปดนั้น ขอมูลทั้งหมดถูกเก็บตามชอง ตางๆ ในตารางแฮช ดังนัน้ %$ ≤$ λ$ ≤$ ! โดยทั่วไปจะรักษาใหตารางแฮชมี λ$ ≤$ %&' จะไดไมแนน สงผลให ทํางานไดรวดเร็ว แตแนนอนวาก็สน้ิ เปลืองเนือ้ ที่ (จอง 100 คาดวาใชแค 50) แตนเ่ี ปนคุณสมบัตเิ ดนของตา รางแฮชที่ใชเนื้อที่แลกกับเวลาการทํางาน เราจะนําเสนอฟงกชนั S(x, i) สามรูปแบบพื้นฐานที่ใชกันทั่วไป Linear Probing วิธีนี้กําหนดให S(x, i) = i เปนฟงกชันงายๆ บอกวาหลังการชนครั้งที่ i ก็ใหพิจารณาดูชองที่หางจาก ชองแรกที่แฮชมา ไปอีก i ชอง (ตามดวยการมอดุโลขนาดของตารางดวย) เขียนไดเปน hi(x) = ( h(x) + i ) mod m 7

การพิจารณาดูวา ชองหนึง่ ๆ ในตารางนัน้ เรียวาการ probe ชองนัน้


!!$ หรือตีความไดอีกแบบหนึ่งวาเปนการดูชองถัดไป (จากชองที่แลว) นัน่ คือ hi(x) = ( hi -1(x) + 1 ) mod m ตัวอยางเชน กําหนดใหตารางแฮชมี 11 ชอง ใช h(x) = x mod 11 เปนฟงกชันแฮช เราตองการเพิ่มขอ มูล 58, 70, 17, 36, และ 4 ตามลําดับ จะไดการเปลี่ยนแปลงของตารางแฮชดังภาพประกอบ 12.2 การเพิ่มขอมูลในตารางแฮชที่ใช linear probing


!!% 0

1

2

4

เพิม่ 58 (1 probe)

3 58

5

6

เพิม่ 70 (1 probe)

58

70

เพิม่ 17 (1 probe)

58

70

เพิม่ 36 (3 probes)

58

70

36

17

เพิม่ 4 (3 probes)

58

70

36

17

7

8

9

10

17

4

ภาพประกอบ 12.2 การเพิ่มขอมูลในตารางแฮชที่ใช linear probing การเพิม่ 58, 70, และ 17 ใชเพียงหนึ่ง probe ตอหนึ่งขอมูล เนื่องจากหลังการแฮชครั้งแรกแลวพบชองวางทัน ที แตพอเพิ่ม 36 ซึ่งถูกแฮชไปที่ชองหมายเลข 3 พบวาชนก็ตอง probe ตอที่ ชอง 4 ซึง่ ก็ชนอีกก็ probe ตอ ที่ชอง 5 จึงจะพบวาวาง (สําหรับการเพิม่ 4 นัน้ ขอใหนักศึกษาลองไลดูวาไดตามที่แสดงไวหรือไม) ขอดีของ linear probing ก็อยูตรงที่งายดี มีชองวางในตารางเหลืออยูก็ตอง probe พบแนๆ 8 แตขอเสีย ก็คือเกิดสภาวะที่เรียกวาขอมูลเกาะกลุมกัน ยิ่งเกาะกลุมกันมาก ยิ่งทําใหมีพฤติกรรมการ probe ที่ เปนแบบลําดับนั้นยาวขึ้นเรื่อยๆ ลองดูภาพประกอบ 12.3 ตารางแฮชที่ใช linear probing จะเขาใจมากขึ้น รูปนี้แสดงตารางแฮชที่มีการเพิ่มขอมูลไปจํานวนหนึ่ง ขอมูลตางๆ ในตารางถูก แสดงดวยเสนสีดาํ เสนสีดาํ ทีเ่ กาะกลุม กันจนเปนสีเ่ หลีย่ มสีดาํ ขนาดความกวางแตกตางกัน แสดงใหเห็นถึง จํานวนขอมูลที่อยูติดกันในตาราง ชองสีขาวก็คือสวนของตารางที่วาง ถาจะถามตรงนี้วาการเพิ่มขอมูลตัวตอ ไป จะมีโอกาสไปถูกเก็บที่ที่ใดมากที่สุด ถาฟงกชันแฮชกระจายดี ขอมูลตัวถัดไปนั้นจะมีโอกาสถูกแฮชไปที่ ชองใดๆ ดวยโอกาสเทาๆ กัน ขอใหดูกลุมขอมูลที่เกาะกลุมกันใหญสุดในตาราง (ชองสีดาํ ชุดทีส่ องจากทาง ซาย) ไมวาขอมูลใหมจะถูกแฮชมาชนกับขอมูลตัวใดในกลุมนี้ ก็จะตองถูกจับใหไปเก็บที่ชองถัดไปทางขวาที่ วางของกลุมนี้ (ดวยพฤติกรรมของ linear probing) สงผลใหกลุมนี้มีขนาดโตขึ้น เนื่องจากกลุมนี้มีขนาดใหญ สุดในตาราง ก็ยอมเปนกลุมที่มีโอกาสโตขึ้นมากที่สุด เราเรียกลักษณะการเกาะกลุม และการโตของกลุม เชนนี้ วา primary clustering (บางที่เรียกวา cookie monster effect ขอใหนักศึกษาลองคิดดูวาทําไมถึงเรียกเชนนี้)

ภาพประกอบ 12.3 ตารางแฮชที่ใช linear probing

8

อีกทัง้ เนือ่ งจากลําดับการ probe เปนการดูชอ งถัดไปเรือ่ ยๆ สงผลใหการเขาถึงขอมูลในหนวยความจํานัน้ รวดเร็วมาก ดวย สถาปตยกรรมการจัดการหนวยความจําเปน cache หลายๆ ระดับของคอมพิวเตอรในปจจุบนั


!'& Quadratic Probing เพื���อขจัดปญหาการเกาะกลุมที่พบใน linear probing วิธีนี้กําหนดให S(x, i) = i2 บอกวาหลังการชน ครั้งที่ i ก็ใหพิจารณาดูชองที่หางจากชองแรกที่แฮชมา ไปอีก i2 ชอง (ตามดวยการมอดุโลขนาดของตาราง ดวย) เขียนไดเปน hi(x) = ( h(x) + i2 ) mod m หรือตีความในกรณีที่คิดจากชองที่ probe ชองที่แลว ไดเปน 9 hi(x) = ( hi -1(x) + (2i – 1) ) mod m ตัวอยางเชน กําหนดใหตารางแฮชมี 11 ชอง ใช h(x) = x mod 11 เปนฟงกชันแฮช เราตองการเพิ่มขอมูล 58, 70, 17, 36, และ 4 ตามลําดับ จะไดการเปลี่ยนแปลงของตารางแฮชดัง ภาพประกอบ 5 0 1 2 3 4 5 6 7 8 9 10 เพิม่ 58 (1 probe) 58 เพิม่ 70 (1 probe)

58

70

เพิม่ 17 (1 probe)

58

70

17

เพิม่ 36 (3 probes)

58

70

17

36

เพิม่ 4 (2 probes)

58

70

17

36

4

ภาพประกอบ 12.4 การเพิ่มขอมูลในตารางแฮชที่ใช quadratic probing การเพิม่ 58, 70, และ 17 ใชเพียงหนึ่ง probe ตอหนึ่งขอมูล เนื่องจากหลังการแฮชครั้งแรกแลวพบ ชองวางทันที แตพอเพิ่ม 36 ซึ่งถูกแฮชไปที่ชองหมายเลข 3 พบวาชนก็ตอง probe ตอที่ ชอง 3+12 = 4 ซึง่ ก็ ชนอีกก็ probe ตอที่ชอง 3+22 = 7 จึงจะพบวาวาง การเพิม่ 4 ก็อาศัยอีกสอง probe ก็พบชองวางดังภาพ ประกอบ หลายคนอาจจะรูสึกวา quadratic probing ไมเห็นมีอะไรดีเลย ภาพประกอบ 12.2 การเพิ่มขอมูลในตา รางแฮชที่ใช linear probing และภาพประกอบ 12.4 การเพิ่มขอมูลในตารางแฮชที่ใช quadratic probing

9

จากลําดับการ probe ของ quadratic probing h(x), h(x) + 1, h(x) + 4, h(x) + 9, h(x) + 16, ... ดูดีๆ จะพบวาชองใหมท่ี probe หาง จากชองที่แลวเปน +1, +3, +5, +7, ... ขอใหนักศึกษาพิสูจนใหเห็นจริงเชิงคณิตศาสตรกันเอง


!'" นั้นเพิ่มขอมูลชุดเดียวกัน ผลสุดทายก็ไดขอ มูลทีเ่ กาะกลุม เปนกอนเดียวกันเหมือนกัน ขอใหเขาใจ ดวยวาการเกาะกลุม กันของกรณีการใช quadratic probing นั้นไมเปนปญหาใหญเทาของกรณี liner probing เพราะวาการ probe นัน้ เปนแบบ “กระโดดๆ” ซึง่ probe หลุดออกจากกลุม ไดเร็วกวาแบบ linear probing มาก จะขอเพิ่มขอมูลตอดวยการเพิ่ม 11 และ 24 ซึง่ เพิม่ ไดสบายๆ ดังแสดงในภาพประกอบ 12.5 การ probe หาชองวางไมพบดวย quadratic probing ตามดวยการเพิ่ม 35 ซึ่งจะมีลําดับการ probe ดังนี้ 2, 3, 6, 0, 7, 5, 5, 7, 0, 6, 3, 2, 3, 6, 0, 7, 5, ... หมายความวามีการ probe เพียงแค 6 ชอง (ซึ่งแรงเงาสีทึบใหเห็นในภาพประกอบ 6) ซ้าํ ๆ กันในตาราง และก็เผอิญเปนชองที่มีขอมูลแลวทั้งสิ้น แสดงใหเห็นวา quadratic probing นัน้ ไมสามารถ probe หาชองวาง ไดทั้งๆ ที่ตารางแฮชยังไมเต็ม 0 1 2 3 4 5 6 7 8 9 10 เพิม่ 11 (1 probe)

11

24

58

70

4

17

36

เพิม่ 23 (1 probe)

11

24

58

70

4

17

36

24 58 70

4

17

36

เพิม่ 35 (? probes) 11

ภาพประกอบ 12.5 การ probe หาชองวางไมพบดวย quadratic probing เหตุการณที่แสดงใหเห็นขางบนนี้อาจทําใหเราดวนสรุปวาไมควรใช quadratic probing เลย แตตอง รีบบอกตอนนีเ้ ลยวา เราสามารถปองกันไมใหเกิดปญหานี้ไดโดยการควบคุมให load factor ของตารางมีคา ต่ํากวา 0.5 และกําหนดใหตารางมีขนาดเปนจํานวนเฉพาะ ดวยขอกําหนดสองขอนี้ เราสามารถพิสจู นไดวา ถามีชองวางในตาราง quadratic probing ตองหาพบแนๆ ดังนี้ กําหนดให m คือขนาดของตาราง a และ b เปนครัง้ ที่ probe โดยที่ 0 ≤ b < a ≤ m/2 เราจะแสดงใหเห็น จริงวา ha(x) ไมเทากับ hb(x) เลย หมายความวาการ probe ตั้งแตครั้งที่ 0 จนถึงครั้งที่ m/2 นั้นไมซ้ําชองกัน เลยในตาราง แสดงวาถามีชองวางเกิน m/2 ชอง ก็ยอมตอง probe พบหนึ่งในชองวางนั้นแนๆ เราอาศัย การพิสูจนดวยขอขัดแยง โดยสมมติใหมี ha(x) = hb(x) นัน่ คือมี ( h(x) + a2 ) mod m = ( h(x) + b2 ) mod m โดยที่ 0 ≤ b < a ≤ m/2 เขียนใหมในรูปของสมภาค (congruence) เปน h(x) + a2 ≡ h(x) + b2 (mod m) ตัด h(x) ออกทัง้ สองขาง ได a( ≡ b( (mod m) เขียนใหมได (a–b)(a+b) ≡ 0 (mod m) ซึง่ สมภาคนีจ้ ะเปนจริงไดมไี ดสามกรณีคอื m หาร (a–b) ลงตัว หรือ m หาร (a+b) ลงตัว หรือ m หาร (a–b)(a+b) ลงตัว จากเงือ่ นไข 0 ≤ b < a ≤  m/2 แสดงวา (a–b) และ (a+b) มีคามากกวา 0 แตนอยกวา m ดังนัน้ m หาร (a–b) และ (a+b) ไมลงตัวแนๆ


!'! สําหรับกรณีสดุ ทายนัน้ ก็ไมมที างเปนจริงได เนื่องจากเรากําหนดให m เปนจํานวนเฉพาะ จึงแยกตัวประกอบ เปน (a–b) กับ (a+b) ไมได สรุปไดวาไมมื ha(x) = hb(x) สําหรับ 0 ≤ b < a ≤ m/2 quadratic probing มีพฤติกรรมการ probe ที่ใหผลดีมากในทางปฏิบัติ ขจัดปญหา primary clustering ที่เกิดใน linear probing แตก็ยังมีปญหาในตัวเองอยูบางตรงที่วาฟงกชัน S(x, i) = i2 นั้นมีระยะ กระโดดซึ่งขึ้นกับหมายเลขขงครั้งที่ชน ไมไดขน้ึ กับคาของคียเ ลย ดังนั้นชุดขอมูลที่มี h(x) เหมือนกันจะชนกัน และกันไปเรือ่ ยๆ ตลอดการ probe เสมือนกับวาชุดขอมูลทีแ่ ฮชไปทีท่ เ่ี ดียวก็เกาะกลุม กันนัน่ เอง แตเปนการ เกาะกลุมที่เห็นไดยากดวยตา เนื่องจากไมจําเปนตองอยูติดกันในตาราง แตเปนชุดขอมูลที่มีลําดับการ probe เหมือนกัน เรียกการเกาะกลุม ในลักษณะเชนนีว้ า secondary clustering Double Hashing ฟงกชัน S(x, i) ซึ่งใชคํานวณหาระยะกระโดดในการ probe ทีไ่ ดนาํ เสนอมาของ linear probing และ quadratic probing นั้นมีคาขึ้นกับหมายเลขครั้งที่ชนเทานั้น จึงเกิดปญหา secondary clustering ตามที่ได นําเสนอมา เพื่อขจัดปญหาดังกลาว เราสามารถออกแบบให S(x, i) มีคาแปรตามคาของคียดวย double hashing กําหนดให S(x, i) = i * เขียนไดเปน hi(x) = ( h(x) + i * g(x) ) mod m หรือตีความในกรณีที่คิดจากชองที่ probe ชองที่แลว ไดเปน hi(x) = ( hi -1(x) + g(x) ) mod m หมายความวาการ probe ครั้งตอไปจะหางจากตําแหนงปจจุบันไป g(x) ชอง โดยพฤติกรรมของ g(x) ก็คลายฟงกชันแฮช กลาวคือถึงแมคียสองคียที่ตางกันจะมีเลขที่อยูที่แฮชไปเหมือนกัน แตเมือ่ ผาน g(x) ซึ่งคํานวณระยะกระโดด ก็มีโอกาสสูงที่ไดระยะที่ไมเหมือนกัน จึงขจัดปญหา secondary clustering ได ตัวอยางเชน กําหนดใหตารางแฮชมี 11 ชอง ใช h(x) = x mod 11 เปนฟงกชันแฮช และ g(x) = 7 - (x mod 7) เราตองการเพิ่มขอมูล 58, 70, 17, 36, และ 4 ตามลําดับ จะไดการเปลีย่ นแปลง ของตารางแฮชดังภาพประกอบ 12.6 การเพิ่มขอมูลในตารางแฮชที่ใช double hashing

0

1

2

4

เพิม่ 58 (1 probe)

3 58

5

6

เพิม่ 70 (1 probe)

58

70

เพิม่ 17 (1 probe)

58

70

17

เพิม่ 36 (2 probes)

58

70

17

เพิม่ 4 (2 probes)

58

70

17

7

8

9

36 4

36

10


!'' ภาพประกอบ 12.6 การเพิ่มขอมูลในตารางแฮชที่ใช double hashing การเพิม่ 58, 70, และ 17 ใชเพียงหนึ่ง probe ตอหนึ่งขอมูล เนื่องจากหลังการแฮชครั้งแรกแลวพบ ชองวางทันที แตพอเพิ่ม 36 ซึ่งถูกแฮชไปที่ชองหมายเลข 3 พบวาชนก็ตอง probe ตอโดยกระโดดไปอีก 7 (36 mod 7) = 6 ชองไดที่ชอง 9 พบวาวางก็ใสขอมูลได สําหรับการเพิม่ 4 ที่ถูกแฮชไปชอง 4 พบวาไมวาง คาของ g(4) = 7 - (4 mod 7) = 3 แสดงวากระโดดไปอีก 3 ชองไดชอง 7 วางก็ใสขอมูลได ประเด็นทีต่ อ งระวังก็คอื เราจะมั่นใจไดอยางไรวา ถามีชองวางในตารางแลว g(x) จะใหคาซึ่งเมื่อ กระโดดในระยะเทากับ g(x) แลวจะ probe พบชองวางในตาราง ตัวอยางเชนถาตารางมีขนาด 8 ชอง (m=8) และคํานวณไดคาของ g(x) เปน 4 จะพบวาการ probe ดวยการกระโดดขามทีละ 4 ชอง จะ probe อยูแค 2 ชองในตารางเทานั้น (ทั้งนี้เพราะวา 8/4 = 2) เพือ่ หลีกเลีย่ งปญหาดังกลาว และสรางความมั่นใจสามารถ probe ไดครบทุกชองแนๆ จะตองกําหนดใหขนาดของตารางเปนจํานวนเฉพาะ เพราะวาคาของ g(x) ในชวง ตั้งแต 1 ถึง m-1 นั้นไมมีตัวใดหาร m ลงตัวแนๆ ถา m เปนจํานวนเฉพาะ การเพิ่มและการคนหาขอมูล การเพิม่ และการคนหาขอมูลนัน้ มีขน้ั ตอนการทํางานคลายๆ กัน กลาวคือเริม่ ดวยการหาเลขทีอ่ ยูเ ริม่ ตนที่จะ probe ดวยฟงกชันแฮช ถาเปนการเพิม่ ขอมูลก็ตอ ง probe จนพบชองที่วางไวใสขอมูล ถาเปนการ คนหาก็ตอง probe จนพบขอมูลที่ตองการ โดยเมื่อใด probe พบชองวางก็แสดงวาขอมูลที่ตองการคนหาไมมี อยูในตาราง ดังนั้นขอมูลแตละชองในตารางแฮชนั้นจะตองมีสถานะกํากับวาเปนชองที่วางหรือไมดวย การลบขอมูล การลบขอมูลในตารางของการแฮชแบบปดนั้นจะมีประเด็นที่ตองระวังเปนพิเศษวาจะลบอยางไร เรา ลบขอมูลออกจากตารางดวยการตั้งสภาวะของชองที่ถูกลบใหเปนชองวางไมไดตารางแถวบนในภาพ ประกอบ 12.7 การลบขอมูลในตารางแฮช เปนผลมาจากการเพิ่มขอมูลในตารางแฮชในภาพประกอบ 12.2 การเพิ่มขอมูลในตารางแฮชที่ใช linear probing ซึง่ ใน linear probing โดยมี h(x) = x mod 11 ถาเราตองการลบ 17 ออกจากตารางก็ตองคนหา 17 กอน โดย h(17) = 17 mod 11 = 6 ก็พบ 17 ในชองที่ 6 พอดี ถาเราลบ 17 ดวยการลางชองที่ 6 ใหมี สภาวะวาง (การลบแบบที่ 1 แถวกลางในภาพประกอบ 12.7 การลบขอมูลในตารางแฮช ) จะทําใหการคนหา 4 ไดผลเปนคนหาไมพบ เนื่องจาก h(4) = 4 mod 11 = 4 พบวา probe ชองที่ 4, 5 ก็ไมใช 4 probe ตอที่ชองที่ 6 ก็พบวาวาง ก็เลยสรุปวาหาไมพบในตาราง ดังนั้นการลบจะใชวิธีการ ระบุใหชองในตารางที่จะถูกลบเปนสภาวะวางไมได จะตองเพิ่มสภาวะของชองในตารางเปนอีกหนึ่ง


!'( แบบคือ "เคยเปนขอมูล แตถกู ลบไปแลว" ดงแสดงในแถวลางของภาพประกอบ 12.7 การลบขอมูล ในตารางแฮช ดังนัน้ ขบวนการ probe จะตองดําเนินการตอถึงแมวาจะพบชองที่มีสภาวะถูกลบ 0 1 2 3 4 5 6 7 8 9 58 70 36 17 4 ลบ 17 (แบบที่ 1)

58

70

36

ลบ 17 (แบบที่ 2)

58

70

36

10

4 17

4

ภาพประกอบ 12.7 การลบขอมูลในตารางแฮช โปรแกรมสําหรับการแฮชแบบ double hashing ก็ไดเวลามาดูตวั โปรแกรมกันอยางละเอียดกัน หลังจากที่ไดเขาใจแนวคิดการทํางานของการแฮช แบบปด จะขอนําเสนอเฉพาะแบบ double hashing เทานัน้ เราเริม่ ดวยการประกาศประเภทขอมูลของตา รางแฮชในภาษาปาสกาลดังนี้ TYPE CellStatus = ( Active, Empty, Deleted ); HashCell = RECORD element : ElementType; status : CellStatus; END; Position = integer; HashTable = RECORD CurrentSize : integer; Table : ARRAY[ 0..TableSize-1 ] OF HashCell; END;

เปน record ซึ่งประกอบดวยสมาชิกตัวแรกคือ currentSize ซึ่งเก็บจํานวนขอมูลที่ ไดถูกจัดเก็บไวในตาราง (currentSize นี้มีไวคํานวณ load factor ของตารางแฮช) และสมาชิกอีกตัวหนึ่ง คือ table ซึง่ เปนแถวลําดับมีขนาดเทากับ TableSize เริ่มตั้งแตชองที่ 0 ถึง TableSize-1 แตละชองเก็บ ขอมูลประเภท HashCell ซึง่ เปน record ประกอบดวยขอมูลที่จะจัดเก็บ (element) สภาวะของชอง (status) ซึ่งมีไดสามสภาวะ (CellStatus) คือ Active แทนกรณีมีขอมูลเก็บอยู Empty แทนกรณีเเปน ชองวาง และ Deleted แทนกรณีเปนชองที่เคยมีขอมูลแตถูกลบไปแลว การสรางตารางแฮชตอนเริม่ จึงตองมีการลางตารางใหทกุ ๆ ชองในตารางมีสภาวะเปนชองวางทั้ง หมดดังนี้ HashTable

PROCEDURE InitializeTable( VAR H : HashTable ); VAR I : integer; BEGIN FOR I:=0 TO TableSize-1 DO


!') H.table[i].status := Empty; END;

การคนหาขอมูลรับขอมูลที่ตองการคนมาผานฟงกชันแฮช แลวเริม่ ดําเนินการ probe แบบ quadratic probing ไปเรือ่ ยๆ จนกวาจะพบขอมูลที่ตองการ หรือจนกวาจะพบชองวางซึ่งสรุปไดวาหาไมพบ ชองถัดไปที่ตอง probe อาศัยนิยาม hi(x) = ( hi-1(x) + g(x) ) mod TableSize เปนสูตรในการคํานวณ เขียนได เปนฟงกชนั ดังนี้ FUNCTION Find( VAR H : HashTable; Key : ElementType ) : Position; VAR P : Position; BEGIN p := findPos( H, Key ); IF (H.table[p].status = Active) AND (H.table[p].element = key) THEN Find := p ELSE Find := -1; END; FUNCTION FindPos( VAR H : HashTable; Key : ElementType ) : Position; VAR I, P, Step : integer; BEGIN I := 0; P := Hash( Key ); Step := Hash2( Key ); { Hash2(x) is g(x) } WHILE (I < TableSize) AND (H.table[p].status <> Empty) AND (H.table[p].element <> Key) DO BEGIN I := I + 1; P := ( P + Step ) MOD TableSize; END; FindPos := p; END;

จะคืนคา -1 เมื่อไมพบขอมูลที่ตองการในตาราง โดยมีการเรียกใชฟงกชัน findPos ซึง่ เปน ฟงกชันหลักในการคน findPos ซึ่งคืนตําแหนงในตารางที่ทําใหหยุด probe (findPos จะหยุดการ probe เมือ่ ได probe มาครบทั้งตารางแลว หรือ probe พบชองวาง หรือ probe พบชองที่มีขอมูลที่ตองการคน) จึง เปนหนาทีข่ อง find ที่หลังจากเรียก findPos แลวตองตรวจสอบกอนวาพบขอมูลในชองที่เปนขอมูลหรือไม หรือวาคนไมพบ การเพิ่มก็อาศัยการหาขอมูล ซึ่งคาดหวังวาจะตองหาไมพบ แลวนําขอมูลใหมใสเขาไปในชองนั้น ตามดวยการเพิ่มคาของ currentSize อีกหนึง่ เขียนเปนฟงกชันดวยการเรียกใช findPos (ซึง่ เขียนกอน หนานี้) ไดดงั นี้ find

PROCEDURE Insert( VAR H : HashTable; Key : ElementType ); VAR P : Position; BEGIN P := FindPos( H, Key ); IF H.table[p].status <> Active THEN BEGIN H.table[p].element := Key;


!'* H.table[p].status := Active; H.currentSize := H.currentSize + 1; END; END;

การลบก็อาศัยการหาขอมูลดวย เปน Deleted ดังนี้

findPos

อีกเชนกัน เมื่อหาพบก็เพียงแตตั้งคาของ

status

ให

PROCEDURE Remove( VAR H : HashTable; Key : ElementType ); VAR P : Position; BEGIN P := findPos( H, key ); IF (H.table[p].status = Active) AND (H.table[p].element = key) THEN BEGIN H.table[p].status := Deleted; END; END;

Rehashing ตัวโปรแกรมที่นําเสนอในหัวขอที่แลวนั้น เราไมไดควบคุมขนาดของ λ (load factor) ของตารางแต อยางใด แตในทางปฏิบัตินั้นมักคุมให λ < 0.5 ตารางจะไดไมแนนมาก การเพิม่ ลบ และคนหาก็จะทํางานได รวดเร็ว โดยเราสามารถคุมคาของ λ ไดในฟงกชันการเพิ่มขอมูล โดยถาเมื่อใดการเพิ่มขอมูลทําให λ มีคาเกิน ก็ตอ งจัดการอะไรบางอยาง เราอาจจะทําแคแสดงขอความเตือนผูใชถึงสภาพที่เกิดขึ้น หรือถาจะใหดี ก็ควร จัดการขยายตาราง (โดยทั่วไปมักขยายเปนสองเทาของตารางเดิม) และยายขอมูลจากตารางเกามาแฮชลงตา รางใหม เพือ่ เปนการลดคาของ λ ลง ภาพประกอบ 12.8 การขยายตารางจาก 11 ชองเปน 23 ชอง แลวแฮช ขอมูลทั้งหมดใหม แสดงสถานะการณทต่ี อ จากภาพประกอบ 12.6 การเพิ่มขอมูลในตารางแฮชที่ใช double hashing เมื่อมีการเพิ่มขอมูลใหมคือ 64 จะทําใหมีจํานวนขอมูลเปน 6 ตัว λ = 0.55 จึงทําการจองตาราง ใหมมีขนาดเปนสองเทาของตารางเดิมคือ 2*11 = 22 แตเนื่องจากเราตองการใหขนาดของตารางเปนจํานวน เฉพาะ จึงตองหาจํานวนเฉพาะตัวที่มีคามากกวา 22 ซึง่ ก็คอื 23 จึงจองตารางใหมขนาด 23 ชอง จากนั้นวิ่งไล ในตารางเกาทีละชองเริ่มอจากชอง 0 ไปเรือ่ ยๆ ชองใดเปนขอมูล ก็นําไปเพิ่มลงในตารางใหม โดยใชฟงกชัน แฮชใหม (ซึ่งโดยทั่วไปก็จะใชฟงกชันแฮชที่คลายของเดิม เพียงแตปดทายดวยการ mod ดวยขนาดของตา รางใหม ในทีน่ ค้ี อื ก็ h(x) = x mod 23) ดังนั้นลําดับการเพิ่มขอมูลในตารางใหมก็คือ 58, 70, 36, 17, 4 และ 64 จะไดขอมูลในตารางแฮชใหมดังแสดงในแถวลางของภาพประกอบ 12.8 การขยายตารางจาก 11 ชอง เปน 23 ชอง แลวแฮชขอมูลทั้งหมดใหม ซึ่งมี λ = 0.26 0 1 2 3 4 5 6 7 8 9 10 58 70 17 4 36 64


!'# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 70 4 58 36 17 64 ภาพประกอบ 12.8 การขยายตารางจาก 11 ชองเปน 23 ชอง แลวแฮชขอมูลทั้งหมดใหม ขั้นตอนการสรางตารางแฮชใหมนั้นกระทําไดไมยุงยากเลยสําหรับภาษาคอมพิวเตอรที่มีคุณสมบัติที่ รองรับการขยายขนาดของแถวลําดับให (เชน C หรือ Java) แตสําหรับภาษาปาสกาลนั้นจะไมมีคุณสมบัตินี้ เนื่องจากขนาดของแถวลําดับจะตองระบุไวลวงหนากอนการแปลตัวโปรแกรม (แตถาใช Turbo Pascal ก็ สามารถใชฟงกชัน getmem ชวยในการจองขนาดตารางใหมได และตองปรับเปลี่ยนโปรแกรมที่ไดเขียนมา เล็กนอย ซึง่ ขอจะไมนาํ เสนอในทีน่ ้ี) กิจกรรม 12.1 ปญหาการหารสามคูณสอง : วากันวาจํานวนเต็มใดๆ สามารถหาไดดว ยการเริม่ จากเลข 1 แลวหาร สาม (ปดเศษทิ้ง) และ/หรือคูณสอง ไปเรือ่ ยๆ เชน 10 = 1×2×2×2×2/3×2 เปนตน โปรแกรมขางลางนี้วิธีหา คําตอบของปญหานี้ อยากใหนักศีกษาทําความเขาใจกับการทํางานหลักของโปรแกรมนี้กอน จากนัน้ เติม ฟงกชัน hash, insert และ find ที่ใชจัดเก็บขอมูลดวยตารางแฮชเพื่อใหโปรแกรมใชงานไดจริง


!'$ PROGRAM M2d3; CONST MaxDepth = 100; TYPE Solution = ARRAY[1..MaxDepth] of String; VAR N X I H

: : : :

integer; Solution; Integer; HashTable;

FUNCTION M2D3_DFS( VAR H : HashTable; VAR X : Solution; K, N, S : integer ) : boolean; VAR S1 : integer; Found : boolean; BEGIN IF K = MaxDepth THEN Found := false ELSE BEGIN Insert( H, S ); IF ( S = N ) THEN found := true ELSE BEGIN found := false; S1 := S div 3; IF find( H, S1 ) = -1 THEN BEGIN X[K+1] := '/3'; found := M2D3_DFS( T, X, K+1, N, S1 ); END; S1 := 2*S; IF (NOT found) AND (find( H, S1 ) = -1) THEN BEGIN X[K+1] := 'x2'; found := M2D3_DFS( T, X, K+1, N, S1 ); END; END; END; M2D3_DFS := found; END; BEGIN {M2d3} write('Enter a number : '); readln( N ); FOR I := 1 TO Maxdepth DO x[I] := ''; t := nil; IF M2D3_DFS( T, X, 0, N, 1 ) THEN BEGIN I := 1; WHILE X[I] <> '' DO BEGIN write(X[I]); I := I+1; END; writeln; END ELSE writeln('fail');

END.


data structure lesson12