470 likes | 725 Views
Hashing. อ ดร วิษณุ โคตรจรัส. เราต้องการทำอะไรกับข้อมูลที่ใช้ hash table. Insert Delete find (constant time) sort ไม่ได้ Findmin findmax ไม่ได้ นั่นก็คือทำได้ไม่เท่า binary search tree . Hash table. เรามี key กับ value แบบเดียวกับ Map ไง
E N D
Hashing อ ดร วิษณุ โคตรจรัส
เราต้องการทำอะไรกับข้อมูลที่ใช้ hash table • Insert • Delete • find (constant time) • sort ไม่ได้ • Findmin findmax ไม่ได้ • นั่นก็คือทำได้ไม่เท่า binary search tree
Hash table • เรามี key กับ value แบบเดียวกับ Map ไง • เราเอา key มาเข้า hash function เพื่อให้ได้ index ที่จะเก็บ value • ดังนั้นแฮชฟังก์ชั่นควรจะ • คำนวณง่าย • คีย์สองตัวต้องเข้าฟังก์ชั่นแล้วได้ index คนละตัว (อันนี้ทำได้ยากแต่เดี๋ยวดูไปเรื่อยๆก่อนนะ)
Hash function • พยายามกระจายคีย์ไปทั่วตาราง เราอาจใช้ • จำนวนคีย์ mod tableSize • แต่ถ้าคีย์เป็น 10, 20, 30, …ก็ใช้ฟังก์ชั่นนี้ไม่ดี • แล้วถ้าคีย์เป็น String ล่ะ เราลองมาดูตัวอย่าง hash function แบบต่างๆกัน
Hash function (1st example) • บวกค่า ASCII ของตัวอักษรต่างๆ public static int hash(String key, int tableSize){ int hashVal = 0; for(int i =0; i<key.length(); i++) hashVal += key.charAt(i); return hashVal%tableSize; }
แต่แบบนี้ไม่ดีถ้าตารางใหญ่แต่แบบนี้ไม่ดีถ้าตารางใหญ่ • เพราะถ้าคีย์ไม่ค่อยยาว เช่นมีแปดตัวอักษร • ASCII มีค่าได้มากที่สุดคือ 127 • ฉะนั้นผลบวกของทุกตัวอักษรจะได้ไม่เกิน 127*8 • ถ้าตารางใหญ่ จะไม่กระจาย ตัวที่หมื่น Index ที่ได้จะกองตอนต้นหมด
Hash function (2nd example) • สมมติว่าตารางใหญ่และตัวคีย์เป็นตัวอักษรสุ่มอย่างน้อยสามตัวอักษร • เราดูเฉพาะตัวอักษรสามตัวแรก public static int hash(String key, int tableSize){ return (key.charAt(0) +27*key.charAt(1) +729* key.charAT(2))%tableSize; } 27*27 จำนวนตัวอักษร รวมตัวว่าง กระจายในตารางขนาดหนึ่งหมื่นได้ดี (10007 เป็นค่าแรกสำหรับหนึ่งหมื่นที่เป็น prime เดี๋ยวมาดูว่าทำไมต้องเป็น)
แต่จริงๆคีย์ที่ใช้ก็ไม่สุ่มนี่แต่จริงๆคีย์ที่ใช้ก็ไม่สุ่มนี่ • ฉะนั้นมีการซ้ำมากแน่นอน • มาดูอีกฟังก์ชั่นในหน้าต่อไป
Hash function (3rd example) • คำนวณ polynomial function ของ 37 โดยใช้ Horner’s Rule • เราสามารถคำนวณ k0 + 37k1+ 37*37k2ได้โดยใช้ [(k2*37)+k1]*37 +k0 Horner rule คือการทำแบบนี้ไป n ตัว ซึ่งจริงๆแล้วคือการคำนวณ
public static int hash(String key, int tableSize){ int hashVal = 0; for(int i =0; i<key.length(); i++) hashVal= 37*hashVal+key.charAt(i); hashVal %= tableSize; if(hashVal<0) hashVal += tableSize; return hashVal; } เผื่อ overflow
อาจจะกระจายไม่ดีนัก แต่ค่อนข้างคำนวณง่าย • แต่ถ้าคีย์ยาวมาก ก็จะคำนวณนาน • แก้โดยไม่ใช้หมดทุกตัวอักษร อาจเลือกตัวอักษรมาบางตัวจากแต่ละส่วนที่สำคัญของคีย์นั้น • แต่ยังไง hash function ก็แยกของลงตารางไม่ได้100% • ต้องมีของที่ลงช่องเดียวกันเกิดขึ้น เราเรียกว่า collision • ต่อไปเราจะดูวิธีแก้ปัญหาเมื่อเกิด collision
วิธีแรก separate chaining • เก็บตัวที่ซ้ำใส่ linked list • ถ้าจะหาของ ก็ใช้ hash function แล้วต่อด้วยการหาในลิสต์ • ถ้าจะใส่ของลงไป ก็ ใช้ hash function เพื่อหาลิสต์ที่จะลง ต่อจากนั้นตรวจลิสต์ว่ามีของที่จะเติมลงไปหรือยัง ถ้ายังก็เติมลงไปหน้าลิสต์ (ตัวที่ถูกใส่ไปใหม่ๆมักถูกเรียกใช้ในเวลาไม่นานต่อมา)
มาดูสิ่งที่ต้องใช้ก่อนมาดูสิ่งที่ต้องใช้ก่อน • public interface Hashable • { • /** • * Compute a hash function for this object. • * @param tableSize the hash table size. • * @return (deterministically) a number between • * 0 and tableSize-1, distributed equitably. • */ • int hash( int tableSize ); • }
ตัวอย่างการใช้งาน Public class Student implements Hashable{ private String name; private double number; private int year; public int hash(int tableSize){ return SeparateChainingHashTable.hash(name, tableSize); } public boolean equals(Object rhs){ return name.equals(((Student)rhs).name); } } จริงๆแล้วคือ static method จากคลาส
public class SeparateChainingHashTable • { • /** • * Construct the hash table. • */ • public SeparateChainingHashTable( ) • { • this( DEFAULT_TABLE_SIZE ); • } • /** • * Construct the hash table. • * @param size approximate table size. • */ • public SeparateChainingHashTable( int size ) • { • theLists = new LinkedList[ nextPrime( size ) ]; • for( int i = 0; i < theLists.length; i++ ) • theLists[ i ] = new LinkedList( ); • }
ใช้ Student ตรงนี้ไง • /** • * Insert into the hash table. If the item is • * already present, then do nothing. • * @param x the item to insert. • */ • public void insert( Hashable x ) • { • LinkedList whichList = theLists[ x.hash( theLists.length ) ]; • LinkedListItr itr = whichList.find( x ); • if( itr.isPastEnd( ) ) • whichList.insert( x, whichList.zeroth( ) ); • } • /** • * Remove from the hash table. • * @param x the item to remove. • */ • public void remove( Hashable x ) • { • theLists[ x.hash( theLists.length ) ].remove( x ); • }
/** • * Find an item in the hash table. • * @param x the item to search for. • * @return the matching item, or null if not found. • */ • public Hashable find( Hashable x ) • { • return (Hashable)theLists[ x.hash( theLists.length ) ].find( x ).retrieve( ); • } • /** • * Make the hash table logically empty. • */ • public void makeEmpty( ) • { • for( int i = 0; i < theLists.length; i++ ) • theLists[ i ].makeEmpty( ); • }
/** • * A hash routine for String objects. • * @param key the String to hash. • * @param tableSize the size of the hash table. • * @return the hash value. • */ • public static int hash( String key, int tableSize ) • { • int hashVal = 0; • for( int i = 0; i < key.length( ); i++ ) • hashVal = 37 * hashVal + key.charAt( i ); • hashVal %= tableSize; • if( hashVal < 0 ) • hashVal += tableSize; • return hashVal; • }
private static final int DEFAULT_TABLE_SIZE = 101; • /** The array of Lists. */ • private LinkedList [ ] theLists; • /** • * Internal method to find a prime number at least as large as n. • * @param n the starting number (must be positive). • * @return a prime number larger than or equal to n. • */ • private static int nextPrime( int n ) • { • if( n % 2 == 0 ) • n++; • for( ; !isPrime( n ); n += 2 ) • ; • return n; • }
/** • * Internal method to test if a number is prime. • * Not an efficient algorithm. • * @param n the number to test. • * @return the result of the test. • */ • private static boolean isPrime( int n ) • { • if( n == 2 || n == 3 ) • return true; • if( n == 1 || n % 2 == 0 ) • return false; • for( int i = 3; i * i <= n; i += 2 ) • if( n % i == 0 ) • return false; • return true; • }
// Simple main • public static void main( String [ ] args ) • { • SeparateChainingHashTable H = new SeparateChainingHashTable( ); • final int NUMS = 4000; • final int GAP = 37; • System.out.println( "Checking... (no more output means success)" ); • for( int i = GAP; i != 0; i = ( i + GAP ) % NUMS ) • H.insert( new MyInteger( i ) ); • for( int i = 1; i < NUMS; i+= 2 ) • H.remove( new MyInteger( i ) ); • for( int i = 2; i < NUMS; i+=2 ) • if( ((MyInteger)(H.find( new MyInteger( i ) ))).intValue( ) != i ) • System.out.println( "Find fails " + i ); • for( int i = 1; i < NUMS; i+=2 ) • { • if( H.find( new MyInteger( i ) ) != null ) • System.out.println( "OOPS!!! " + i ); • } • } • }
นิยาม • Load factor • ซึ่งก็คือความยาวเฉลี่ยของลิงค์ลิสต์ Search time = time to do hashing + time to search list = constant + time to search list • Unsuccessful search Search time = ความยาวลิสต์ = ค่า load factor
Successful search • ที่ลิสต์ที่จะค้น มีหนึ่งโนดที่มีของ และก็โนดอื่นๆซึ่งมีตั้งแต่ 0 โนดขึ้นไป • ถ้าเราดูที่ตารางซึ่งมีจำนวนสมาชิก N ซึ่งถูกแบ่งไปเป็นลิสต์ M ลิสต์ • จะได้ว่า มีโนดที่ไม่ใช่โนดที่เราต้องการหาอยู่ N-1 โนด • ถ้าแบ่งไปเป็นลิสต์ละเท่าๆกันจะได้ว่าแต่ละลิสต์มี (N-1)/M โนด • = lambda- (1/M) • = lambda นั่นเอง เพราะว่า M มีค่ามาก • โดยเฉลี่ยแล้ว ครึ่งหนึ่งของโนดพวกนี้จะถูกค้น นั่นคือ lambda/2 • ฉะนั้น เวลาโดยเฉลี่ยที่ใช้ในการหาของเจอคือ 1 + (lambda/2) • นี่หมายความว่า ขนาดตารางไม่สำคัญ ที่สำคัญคือ load factor
วิธีที่สอง Open addressing • ถ้าไม่อยากใช้ลิสต์ ก็ต้องใช้นี่เลย • ถ้ามี collision ก็หาช่องใหม่เรื่อยๆจนกว่าจะเจอช่องว่าง • โดยลองช่องที่ h0(x), h1(x), … • hi(x)=[hash(x)+f(i)]%tableSize, f(0)=0 • ข้อมูลทุกตัวต้องลงตาราง ไม่มีลิสต์มาช่วยเก็บ ดังนั้นตารางต้องใหญ่ • Load factor <=0.5
Open addressing แบบ linear probing • F จะเป็นลิเนียร์ฟังก์ชั่นของ i • ส่วนใหญ่จะให้ f(i)=i • ซึ่งคือการหาช่องว่างในตารางโดยดูเรียงทีละช่องไปเรื่อยๆ • มีปัญหาคือ เวลาในการหาช่องว่างจะนานได้ จะเกิดการใช้ช่องติดกัน (primary clustering) ถ้ามีอันไหนมา collide ในช่วงนี้ กว่าจะหาช่องว่างเจอก็จะเป็นเวลานาน
Open addressing แบบ quadratic probing • ช่วยกำจัด primary clustering • ฟังก์ชั่นที่นิยมใช้กันคือ f(i)=i2 • อย่าลืมว่า hi(x)=[hash(x)+f(i)]%tableSize a ถ้า b ลงซ้ำที่เดียวกับ a เราบวกอีก 12 ก็จะเจอที่ว่าง ถ้า c ลงซ้ำที่เดียวกับ a เราบวกอีก 12 ก็จะเจอ b ดังนั้นลองบวก 22ก็จะเจอที่ว่าง
แต่ถ้าตารางใส่เกินครึ่งหนึ่งแล้ว หรือถ้าขนาดตารางไม่เป็นจำนวนเฉพาะ สูตรนี้ก็ไม่แน่ว่าจะหาช่องว่างได้ • แต่ถ้าใช้ตารางยังไม่ถึงครึ่งหนึ่งและขนาดตารางเป็นจำนวนเฉพาะแล้วละก็ ได้มีการพิสูจน์แล้วว่าหาช่องลงได้แน่นอน
พิสูจน์ • ให้ขนาดตารางเป็นจำนวนเฉพาะที่ใหญ่กว่าสาม • ให้ (h(x)+i2) mod tableSize • (h(x)+j2) mod tableSize • Prove by contradiction • สมมติให้ตำแหน่งทั้งสองเป็นตำแหน่งเดียวกันและ i ไม่เท่ากับ j เป็นสองตำแหน่งที่หาที่ลงได้
พิสูจน์(ต่อ) • i-j =0 นั้นเป็นไปไม่ได้ เพราะเราสมมติแล้วว่าสองตัวนี้ไม่เท่ากัน • i+j=0 ก็เป็นไปไม่ได้ เพราะสมมติว่า • ดังนั้นที่พึ่งสมมติให้ ตำแหน่งที่ลงได้เป็นตำแหน่งเดียวกัน จึงผิด • สรุปคือ ตำแหน่งที่ลงได้ เป็นคนละตำแหน่งเสมอ • เราจึงพูดได้ว่าถ้าตารางยังมีการใช้งานไม่ถึงครึ่ง จะต้องได้ช่องลงแน่นอน
ขนาดตารางต้องเป็นจำนวนเฉพาะขนาดตารางต้องเป็นจำนวนเฉพาะ • ถ้าไม่เป็น ตำแหน่งที่ควรจะลงได้จะลดลงมากทีเดียว • ตัวอย่าง ตารางขนาด 16 สมมติว่าการแฮชธรรมดาให้ลงช่องแรก • ใช้ quadratic probing 32 22 12 42 72 62 52 ซ้ำช่องที่เคยใช้หมดเลย
การลบทำแบบธรรมดาไม่ได้การลบทำแบบธรรมดาไม่ได้ • ถ้าเอาตัววงกลมเล็กออก พอทีหลังหาสมาชิก(ที่ตอน insert ต้องกระโดดหลบช่องที่วงกลมอยู่) จะกลายเป็นเจอช่องว่างไม่มีอะไร ซึ่งจะมีความหมายเหมือนการหาไม่เจอ 32 22 12 42 72 62 52 ดังนั้นต้องใช้ lazy deletion คือทำเครื่องหมายว่าลบแล้วหรือยัง
Open addressing implementation class HashEntry { Hashable element; // the element boolean isActive;// false is deleted public HashEntry( Hashable e ){ this( e, true ); } public HashEntry( Hashable e, boolean i ){ element = e; isActive = i; } }
public class QuadraticProbingHashTable{ • private static final int DEFAULT_TABLE_SIZE = 11; • /** The array of elements. */ • private HashEntry [ ] array; // The array of elements • private int currentSize; // The number of occupied cells • public QuadraticProbingHashTable( ){ • this( DEFAULT_TABLE_SIZE ); • } • /** • * Construct the hash table. • * @param size the approximate initial size. • */ • public QuadraticProbingHashTable( int size ){ • allocateArray( size ); • makeEmpty( ); • } nonactive null active
/** • * Internal method to allocate array. • * @param arraySize the size of the array. • */ • private void allocateArray( int arraySize ){ • array = new HashEntry[ arraySize ]; • } • /** • * Make the hash table logically empty. • */ • public void makeEmpty( ){ • currentSize = 0; • for( int i = 0; i < array.length; i++ ) • array[ i ] = null; • }
/** • * Return true if currentPos exists and is active. • * @param currentPos the result of a call to findPos. • * @return true if currentPos is active. • */ • private boolean isActive( int currentPos ){ • return array[ currentPos ] != null && array[ currentPos ].isActive; • }
/** • * Method that performs quadratic probing resolution. • * @param x the item to search for. • * @return the position where the search terminates. • */ • private int findPos( Hashable x ) { • /* 1*/ int collisionNum = 0; • /* 2*/ int currentPos = x.hash( array.length ); • /* 3*/ while( array[ currentPos ] != null && • !array[ currentPos ].element.equals( x ) ){ • /* 4*/ currentPos += 2 * ++collisionNum - 1; // Compute ith probe • /* 5*/ if( currentPos >= array.length ) // Implement the mod • /* 6*/ currentPos -= array.length; • } • /* 7*/ return currentPos; • } f(i)=i2=f(i-1)+2i-1
/** • * Find an item in the hash table. • * @param x the item to search for. • * @return the matching item. • */ • public Hashable find( Hashable x ){ • int currentPos = findPos( x ); • return isActive( currentPos ) ? array[ currentPos ].element : null; • }
/** • * Insert into the hash table. If the item is • * already present, do nothing. • * @param x the item to insert. • */ • public void insert( Hashable x ) • { • // Insert x as active • int currentPos = findPos( x ); • if( isActive( currentPos ) ) • return; //x is already inside, so do nothing • array[ currentPos ] = new HashEntry( x, true ); • // Rehash; see Section 5.5 • if( ++currentSize > array.length / 2 ) • rehash( ); • }
เวลา O(N) เพราะมี N สมาชิกให้ rehash และตารางตอนนี้ก็เป็น 2N แต่ทำไม่บ่อย ก่อนการ rehash ก็ต้องใส่ไปครึ่งตารางก่อนแล้ว ต้องคำนวณ index ใหม่เพราะนี่มันคนละตารางแล้ว • /** • * Expand the hash table. • */ • private void rehash( ) • { • HashEntry [ ] oldArray = array; • // Create a new double-sized, empty table • allocateArray( nextPrime( 2 * oldArray.length ) ); • currentSize = 0; • // Copy table over • for( int i = 0; i < oldArray.length; i++ ) • if( oldArray[ i ] != null && oldArray[ i ].isActive ) • insert( oldArray[ i ].element ); • return; • }
เพิ่มเติม rehash • Rehash ทำได้สามลักษณะคือ • ทำทันทีที่ตารางเต็มครึ่งหนึ่ง • ทำเมื่อ insert ไม่ได้แล้วเท่านั้น หรือ • ทำเมื่อถึงโหลดแฟกเตอร์ค่าหนึ่ง • อย่าลืมว่ายิ่งโหลดแฟกเตอร์มาก ก็จะ insert ได้ช้าลง • อันนี้น่าจะดีที่สุด เลือกโหลดแฟกเตอร์ให้ดีๆ
hash, nextPrime, isPrime เหมือนเดิม • /** • * Remove from the hash table. • * @param x the item to remove. • */ • public void remove( Hashable x ) • { • int currentPos = findPos( x ); • if( isActive( currentPos ) ) • array[ currentPos ].isActive = false; • }
ข้อเสียของ quadratic probing • Secondary clustering • แก้โดย double hashing นั่นคือ • f(i) = i*hash2(x) โดยเราจะดูช่องว่างที่ระยะ hash2(x), 2 *hash2(x), …ไปเรื่อยๆ • ดังนั้นต้องเลือกฟังก์ชั่นที่สองนี้ให้ดีๆ • ถ้ามีเก้าช่องและเอา hash2(x) เป็น x%9 ก็ไม่ได้อะไรเลยตอนเราใส่ 99 เพราะเป็น 0 ตลอด • ฟังก์ชั่นห้ามได้คำตอบเป็น 0 • ต้องให้แน่ใจว่าเข้าถึงทุกช่องในตารางได้
ตัวอย่าง hash2 • hash2(x)=R-(x%R) โดย R เป็นจำนวนเฉพาะที่น้อยกว่า tableSize • ลองกับตารางขนาด 16 เราใส่ 9, 25, 26, 41, 42, 58 สมมติว่า hash อันแรกเป็น x%tableSize 25 ชนดังนั้นต้องบวก 13-(25%13)=1 26 ชนดังนั้นต้องบวก 13-(26%13)=13
41 ชนดังนั้นต้องบวก 13-(41%13)=11 42 ชนดังนั้นต้องบวก 13-(42%13)=10 แต่ 42 มาชนนี่อีกที ต้องบวกจากตอนแรกเป็น 2*10
58 ชนนี่ ดังนั้นต้องบวก 13-(58%13)=7