טבלת גיבוב

במדעי המחשב, טבלת גִּבּוּב או טבלת ערבול (באנגלית: Hash table), היא מבנה נתונים מילוני, אשר נותן גישה לרשומה באמצעות המפתח המתאים לה. המבנה הזה עובד באמצעות הפיכת המפתח על ידי פונקציית הגיבוב, למספר המייצג מיקום במערך שמפנה אל הרשומה המבוקשת. הפעולה העיקרית שבה היא תומכת ביעילות היא אחזור המידע מתוך מבנה הנתונים: בהינתן מפתח נתון (למשל שם של אדם), מצא את הרשומה המתאימה (למשל מספר הטלפון של אותו אדם). הרעיון לטבלת הגיבוב הופיע כבר ב-1953 במזכר פנימי בחברת IBM שפורסם על ידי ה.פ. לון (H. P. Luhn) ובמקביל פותחה על ידי ג'ין אמדל (Gene amdahl), ה.מ בוהם (E. M. Boehme), נתניאל רוצ'סטר (Nathaniel Rochester) וארתור סמואל (Arthur Samuel) תוכנית שמשתמשת בגיבוב. כאשר למדען המחשב הרוסי אנדריי ארשוב (Andrey Ershov), היה את אותו רעיון כמו לאמדל. טבלת הגיבוב מורכבת ממערך, והתאים שבו מפנים לרשומה הרצויה. כדי להגיע אל התא הרצוי נעשה שימוש בפונקציה שנקראת פונקציית גיבוב או פונקציית ערבול. הפונקציה מקבלת את המפתח של הרשומה ומחזירה את מספר התא שמפנה אל הרשומה אם היא קיימת. הפונקציה משמשת לחיפוש רשומה רצויה, מציאת מקום מתאים עבור הכנסה של רשומה חדשה, ומציאת המקום של רשומה שצריך למחוק. כדוגמה לטבלת גיבוב פשוטה נוכל לקחת מפעל קטן בעל 1000 עובדים שבו רוצים לשמור רשומות המכילות את פרטי העובדים והגישה לרשומות תהיה לפי מספר עובד בן 3 ספרות (000-999). כדי לבנות טבלת גיבוב מתאימה, נוכל להשתמש במערך בעל אלף תאים ונבחר פונקציית גיבוב פשוטה שעבור כל מספר עובד, מחזירה את המספר עצמו (פונקציית הזהות) ולמעשה מצביעה על כך שאת המידע על העובד שמספרו X נוכל להשיג בתא מספר X. בדוגמה הזאת ביצענו מיעון ישיר - השתמשנו ב"פונקציה חד-חד-ערכית", כלומר, לא קיים יותר ממספר עובד אחד שיופנה לתא מסוים. פעמים רבות לא ניתן להשתמש ב"פונקציה חד-חד-ערכית", למשל, אם היינו רוצים לגשת לרשומות עובדים בעזרת מספר תעודת הזהות שלהם ושימוש בפונקציית הזהות, אז היינו צריכים להקצות מערך של מיליארד(!) תאים כדי שיתאים לטווח של הפונקציה. הבעיה נוצרת כאשר מספר המפתחות האפשריים, גדול בהרבה מאשר מספר הרשומות שיהיו בטבלת הגיבוב. כדי להתמודד עם הבעיה משתמשים גם בפונקציות שאינן חד-חד-ערכיות, מה שיאפשר שימוש במערך קטן יותר אך יגרור בעיה נוספת של התנגשויות, כלומר, הפונקציה מחזירה עבור הרשומה, מספר תא שמתאים גם לרשומה אחרת. עבור טבלת גיבוב המכילה מערך עם B תאים. קיימים שני סוגים עיקריים לפתרון בעיית ההתנגשות שנקראים טבלה פתוחה וטבלה סגורה: טבלה פתוחה או "מיעון פתוח", פותרת את בעיית ההתנגשות על ידי הקביעה שיש יותר מתא אחד במערך שיכול להפנות לרשומה. למשל, ניתן להשתמש בפונקציית גיבוב שמקבלת בנוסף למפתח, את מספר הניסיונות הכושלים למצוא תא מתאים בטבלה, כך שעבור מספר ניסיון שונה, הפונקציה תחזיר מיקום שונה במערך. טבלת הגיבוב תשתמש בפונקציה כאשר עבור כל ניסיון כושל היא תחשב את הפונקציה מחדש עם הנתון המעודכן עד שימצא מקום פנוי בטבלה. היתרון בשיטה הוא בניצול מקסימלי של המקום, כאשר מספר האיברים המקסימלי הוא B. טבלה סגורה פותרת את בעיית ההתנגשות על ידי הפניה מכל תא במערך אל מבנה נתונים שמספק את אותן תכונות כמו טבלת הגיבוב (חיפוש, מחיקה, הוספה וכו'). טבלת הגיבוב משתמשת בפונקציה כדי למצוא את התא המתאים במערך והוא מפנה אל מבנה הנתונים שנותן גישה אל כל הרשומות שנשלחו לתא על ידי הפונקציה. השימוש במבני הנתונים הנוספים, משפיע על מאפייני הטבלה כמו למשל על סיבוכיות הזמן של פעולות הטבלה, המקום בזיכרון שהטבלה תצרוך ומספר הרשומות שיהיה ניתן להכניס לטבלה. דוגמה לכך היא שימוש ברשימות מקושרות שכל תא במערך מפנה לאחת מהן. השימוש ברשימה מקושרת, מאפשר להכניס אל טבלת הגיבוב מספר רשומות גדול מ-B אבל אם מספר הרשומות (n) יהיה גדול משמעותית מ-B, אז לפי עקרון שובך היונים הרשימות המקושרות תהיינה יותר ארוכות וסיבוכיות הזמן של פעולות על הטבלה תושפע מהרשימות ותהיה   O ( n ) {\displaystyle \ O(n)} . השימוש במבנה הנתונים הנוסף גם יצרוך יותר מקום בזיכרון בגלל דרישות מבנה הנתונים ובגלל המערך שסביר ויהיו בו יותר תאים ריקים בניגוד לטבלה הפתוחה. קיימת שיטה נוספת לפתרון התנגשויות הנקראת גיבוב קוקייה. כאשר הרשומות שיהיו בטבלה, ידועות מראש, ניתן להשתמש בגיבוב מושלם, כלומר, שימוש בפונקציה שידוע מראש שהיא מתאימה עבור כל מפתח את האינדקס המתאים לו בלי שום התנגשות. ולכן מאפשרת השגת נתון ב-(O(1 גם במקרה הגרוע ביותר. טבלה זו קלה לתחזוק, אך נדרש זמן גדול לבנייתה הראשונית והיא דורשת מקום מורחב יחסית לטבלת גיבוב רגילה. ערך מורחב – פונקציית גיבוב פונקציית הגיבוב מתאימה עבור כל מפתח, אינדקס במערך שיוכל להתאים עבורו. כדי שפעולות הטבלה יהיו מהירות, הפונקציה צריכה להתבצע בסיבוכיות נמוכה ביותר ביחס לערכו של המפתח עד כדי שלא יהיו תלויות בו. דרישה נוספת היא שהפונקציה תחזיר ערכים באופן קרוב עד כמה שאפשר להתפלגות אחידה בדידה. אם הפונקציה תחזיר ערכים מסוימים בתדירות גבוהה, זמן ביצוע פעולות הטבלה יגדל ואף יכול להגיע לסיבוכיות שתלויה במספר הרשומות בטבלה. החזרת ערכים זהים במערך, יכולה להוביל להעמסה על מבנה הנתונים אליו מפנה המערך בטבלה סגורה מה שיכול להוביל לסיבוכיות גבוהה בהתאם למבנה הנתונים. בטבלה פתוחה כדאי להימנע ממצב בו יצטברו רשומות בגושים. כלומר, בגלל שיטת הגיבוב, מספר ההתנגשויות עבור שימוש בפונקציה יהיה גבוה. למשל, אם פונקציית הגיבוב מטפלת בהתנגשות על ידי בחירה של התא הבא במערך, אז עבור רצף של תאים, ניתקל בהתנגשות נוספת עבור כל תא בהמשך הרצף שלא מוביל לרשומה הרצויה. אם מדובר בפעולת הכנסה לטבלה אז הרשומה תוכנס לתא הבא אחרי הרצף והיא תגדיל את הרצף באחד. כדי לחפש ערך בטבלה משתמשים בפונקציית הגיבוב שמקבלת כמשתנה את המפתח של הרשומה המבוקשת כאשר המטרה היא שהערך שהפונקציה מחזירה יוביל אותנו אל הרשומה. כאשר מדובר על טבלה פתוחה, פעולת החיפוש צריכה לטפל בבעיית ההתנגשות. כלומר, המפתח הוביל אותנו אל רשומה שלא מתאימה אליו. בעיה זו נפתרת לרוב על ידי משתנה נוסף שהפונקציה מקבלת והוא מקבל ערך חדש עבור כל התנגשות נוספת בחיפוש הנוכחי. פונקציית הגיבוב מתייחסת אל המשתנה הזה כאשר היא מחזירה את הערך ותחזיר ערך חדש אחרי כל התנגשות. החיפוש נגמר כאשר מצאנו את הרשומה המתאימה או כשמגיעים לתא שלא הפנה לשום ערך. בשימוש בשיטה הזאת יכולה להתקיים בעיה כאשר מוחקים מהטבלה את אחת הרשומות שהיא גם אחת מההתנגשויות בחיפוש אחר רשומה נוספת. בפעולת החיפוש נקבל את הרושם שהגענו לתא שלא הפנה לשום רשומה ולכן נפסיק את החיפוש. הפתרון לבעיה הוא באמצעות משתנה שישמור עבור כל תא בטבלה אם הוא הפנה אל רשומה ובזמן החיפוש נבדוק באמצעות המשתנה אם התא "אוכלס" בעבר. עבור פונקציית גיבוב טובה, סיבוכיות הזמן תהיה תלויה במספר ההתנגשויות בזמן חיפוש ערך ותהיה גבוהה יותר ככל שמספר הרשומות בטבלה, מתקרב לגודל המערך (מסומן "B<<n" כאשר B - גודל המערך, n - מספר הרשומות בפועל במערך). הסיבוכיות תהיה תלויה גם בכמות הרשומות שהוכנסו לטבלה אפילו אם הן כבר לא נמצאות בה כיוון שהרבה תאים יסומנו כתאים ש"אוכלסו". כאשר כמות הרשומות אינה גבוהה, מספר ההתנגשויות צפוי להיות בודד ולכן סיבוכיות החיפוש תהיה במקרה הממוצע   O ( 1 ) {\displaystyle \ O(1)} . במקרה הגרוע של הרבה התנגשויות הסיבוכיות תהיה   O ( n ) {\displaystyle \ O(n)} . בטבלה סגורה בעיית ההתנגשות נפתרת על ידי מבנה נתונים נוסף שאליו התא בטבלה מפנה. החיפוש נעשה על ידי שימוש בפונקציית הגיבוב שמחזירה אינדקס במערך ולאחר מכן בפונקציית החיפוש של מבנה הנתונים שאליו מפנה האינדקס. עבור פונקציית גיבוב טובה, כאשר כמות הרשומות באותו סדר גודל של מספר התאים במערך (מסומן "B~n"), סיבוכיות החיפוש תהיה במקרה הממוצע   O ( 1 ) {\displaystyle \ O(1)} כיוון שבכל מבנה נתונים יהיה בממוצע מספר בודד של רשומות. במקרה הגרוע, הסיבוכיות תהיה כמו הסיבוכיות במקרה הגרוע של מבנה הנתונים בו משתמשים. הכנסת רשומה לטבלה מתבצעת בעזרת מציאת מקום מתאים לרשומה באופן דומה לחיפוש רגיל בטבלה והכנסת הרשומה במקום שנמצא. עבור הכנסה בטבלה פתוחה, גם כאשר מוצאים תא שהיה בעבר מאוכלס, מכניסים לתוכו את הרשומה. סיבוכיות ההכנסה עבור פונקציה טובה וטבלה שמספר הרשומות בה קטן באופן משמעותי ממספר התאים, תהיה במקרה הממוצע   O ( 1 ) {\displaystyle \ O(1)} . במקרה הגרוע של הרבה התנגשויות הסיבוכיות תהיה   O ( n ) {\displaystyle \ O(n)} . בטבלה סגורה ההכנסה מתבצעת באמצעות מציאת המקום המתאים בטבלה על ידי פונקציית הגיבוב ואז שימוש בפונקציית ההכנסה של מבנה הנתונים הנוסף איתו עובדים. סיבוכיות ההכנסה תהיה כמו בחיפוש. גם הוצאת רשומה מטבלת הגיבוב דומה לחיפוש. בטבלה פתוחה, מבצעים חיפוש של הרשומה וכאשר מוצאים אותה, מוחקים אותה ומסמנים את התא כתא ש"אוכלס". סיבוכיות המחיקה תהיה כמו סיבוכיות החיפוש. בטבלה סגורה המחיקה מתבצעת באמצעות מציאת המקום המתאים בטבלה על ידי פונקציית הגיבוב ואז שימוש בפונקציית ההוצאה של מבנה הנתונים הנוסף איתו עובדים. סיבוכיות המחיקה תהיה כמו סיבוכיות החיפוש. עבור טבלה סגורה, ניתן להשתמש במבנה הנתונים רשימה מקושרת ואף על פי שבמקרה הגרוע, היעילות תהיה   O ( n ) {\displaystyle \ O(n)} עבור כל אחת משלוש הפעולות, קלות התפיסה של פעולת הרשימות המקושרות והתכנות שלהם, יכולים לפצות על כך. ניתן להשתמש גם בעץ חיפוש מאוזן כמו עץ AVL ועץ B+ שיקטינו את החיפוש במקרה הגרוע ל   O ( log ⁡ n ) {\displaystyle \ O(\log n)} . במערכות זמן אמת בהם זמן ביצוע פעולה הוא קריטי גם במקרה הגרוע, בחירה במבני נתונים אלו יכולה לשפר את ביצועי המערכת. בטבלה הפתוחה קיימות מספר שיטות לפתירת בעיית ההתנגשות: בדיקה ליניארית (אנ') - עבור כל התנגשות נבדוק סדרתית האם האינדקס הבא במערך פנוי. שיטה זו קלה לחישוב ולהבנה ושומרת על מקומיות בזיכרון דבר שחוסך גישה לדיסק הקשיח אך יכולה ליצור בעיה של הצטברות רשומות בסדרת אינדקסים ובעקבות כך יהיה מספר גדול של התנגשויות עבור הרשומות באותם באינדקסים. ההצטברות תתהווה משום שעבור כל ניסיון הכנסה של רשומה לצביר רשומות, ההצטברות תגדל בעוד אחד וכך גם מספר האינדקסים שיגדילו את הצביר יגדל באחד. בדיקה ריבועית (אנ') - עבור כל התנגשות נבדק מקום מתאים אחר תוך שימוש בהעלאה בריבוע של מספר ההתנגשויות שהיו עד כה עבור אותו מפתח. הפונקציה עבור הבדיקה הריבועית היא: h ( k , i ) = ( h ( k ) + c 1 i + c 2 i 2 ) mod m {\displaystyle h(k,i)=(h(k)+c_{1}i+c_{2}i^{2})\mod m} כאשר: h {\displaystyle h} - פונקציית הגיבוב, k {\displaystyle k} - המפתח המבוקש, i {\displaystyle i} - מספר ההתנגשויות, m {\displaystyle m} - מספר התאים בטבלה ו- c 1 , c 2 {\displaystyle c_{1},c_{2}} - מספרים קבועים. בשיטה זו אמנם תיתכן הצטברות שתבוא לידי ביטוי בסדרות של תאים מאוכלסים שמתחילות מ- h ( k ) {\displaystyle h(k)} וממשיכות בתאים שבאופן רצוף האינדקס עבורם הוא h ( k , i ) {\displaystyle h(k,i)} והם מאוכלסים. ההצטברות מתהווה משום שכל המפתחות שמחזירים h ( k ) {\displaystyle h(k)} תמיד ינותבו אל אותה סדרת תאים. ההצטברות עבור הבדיקה הריבועית פחות משמעותית מההצטברות המתרחשת עבור הבדיקה הליניארית, אך הבדיקה הריבועית שומרת על מקומיות בזיכרון ברמה פחותה. גיבוב כפול (אנ') - בגיבוב כפול משתמשים בפונקציית גיבוב נוספת כדי לטפל במקרה של התנגשות. הנוסחה עבור גיבוב כפול היא: h ( k , i ) = ( h 1 ( k ) + i ⋅ h 2 ( k ) ) mod m {\displaystyle h(k,i)=(h_{1}(k)+i\cdot h_{2}(k))\mod m} כאשר k {\displaystyle k} - המפתח המבוקש, h 1 , h 2 {\displaystyle h_{1},h_{2}} - פונקציות גיבוב, i {\displaystyle i} - מספר ההתנגשויות ו- m {\displaystyle m} - מספר התאים בטבלה. ההצטברות בגיבוב כפול מתרחשת רק עבור מפתחות שבאופן נדיר שתי הפונקציות מחזירות עבורם את אותו הערך, ובכך עדיפה על הבדיקה הריבועית אבל לא נשמרת המקומיות בכלל. טבלאות גיבוב מועילות כאשר יש לאחסן מספר גדול של רשומות ובמיוחד כאשר גודל קבוצת הנתונים ניתן לחיזוי. טבלאות גיבוב משמשות לעיתים למימוש מערכים אסוציאטיביים, קבוצות, ומטמון (אזור אחסון של נתונים שלהם יזדקק המחשב בתדירות גבוהה). בשחמט ממוחשב, טבלת גיבוב משמשת בדרך כלל למימוש טבלת מעברים, ובמסדי נתונים שונים ניתן להשתמש בטבלת גיבוב להחזקת טבלאות תוכן. פונקציית גיבוב גיבוב קוקייה מדיה וקבצים בנושא טבלת גיבוב בוויקישיתוף טבלת גיבוב, באתר MathWorld (באנגלית) Hans Peter Luhn and the Birth of the Hashing Algorithm - IEEE Spectrum, spectrum.ieee.org (באנגלית)