מבנה נתונים תמציתי

במדעי המחשב מבנה נתונים תמציתי הוא מבנה נתונים, המשתמש בכמות זיכרון הקרובה לכמות הזיכרון המינמלית הדרושה על פי תורת האינפורמציה, אך (בניגוד לייצוגים דחוסים אחרים), עדיין תומך בביצוע שאילתות בזמן יעיל. הרעיון הוצג במקור על ידי ג'ייקובסון כדי לקודד מערך-סיביות, עצים, וגרפים מישוריים. בניגוד לאלגוריתמים כלליים לדחיסת נתונים, מבני נתונים תמציתיים משמרים את היכולת להשתמש בהם במקום, מבלי לחלץ אותם קודם. תחום קרוב למבני נתונים תמציתיים הוא תחום מבני נתונים דחוסים. בשניהם גודל מבנה הנתונים תלוי במידע המסוים המיוצג במבנה. עבור מידע מסוים, אם על פי תורת האינפורמציה דרושים Z {\displaystyle Z} ביטים לייצוג של המידע, אזי ייצוג של המידע יקרא: מרומז אם הוא צורך  Z + O ( 1 ) {\displaystyle Z+O(1)} ביטים של זיכרון. תמציתי אם הוא צורך Z + o ( Z ) {\displaystyle Z+o(Z)} ביטים של זיכרון, קומפקטי אם הוא צורך O ( Z ) {\displaystyle O(Z)} ביטים של זיכרון. לדוגמה, מבנה נתונים המשתמש ב 2 Z {\displaystyle 2Z} ביטים של אחסון הוא קומפקטי, Z + Z {\displaystyle Z+{\sqrt {Z}}} ביטים הוא תמציתי, Z + lg ⁡ Z {\displaystyle Z+\lg Z} ביטים גם הוא תמציתי ו- Z + 3 {\displaystyle Z+3} ביטים הוא מרומז. מבנים מרומזים בדרך כלל מצומצמים לאחסון מידע באמצעות תמורה כלשהי של נתוני הקלט; הדוגמה הידועה ביותר לכך היא הערימה. מילונים תמציתיים הניתנים לאינדוקס, או בשמם הנוסף מילוני דרגה/בחירה, מהווים בסיס למספר טכניקות של ייצוג תמציתי, כגון עצים בינאריים, עצים  k {\displaystyle k} -ארים, מולטי קבוצות, עצי סיפות ומערכים. הבעיה הבסיסית היא לאחסן תת-קבוצה S {\displaystyle S} של העולם U = [ 0 … n ) = { 0 , 1 , … , n − 1 } {\displaystyle U=[0\dots n)=\{0,1,\dots ,n-1\}} המיוצגת בדרך כלל על ידי מערך-סיביות  B [ 0 … n ) {\displaystyle B[0\dots n)} כאשר  B [ i ] = 1 {\displaystyle B[i]=1} אם ורק אם  i ∈ S {\displaystyle i\in S} . מילון הניתן לאינדוקס תומך בפעולות הרגילות על מילונים (שאילתות, ובמקרה הדינמי גם הוספות ומחיקות) כמו גם בפעולות הבאות: דרגה (rank) - שאילתה המקבלת אינדקס i של איבר ואיבר q ( q ∈ { 0 , 1 } {\displaystyle q\in \{0,1\}} ) ומחזירה את מספר האיברים ב-B עד האינדקס i שהערך שלהם הוא q. פורמלית: r a n k q ( i ) = | { k ∈ [ 0 … i ] : B [ k ] = q } | {\displaystyle \mathbf {rank} _{q}(i)=|\{k\in [0\dots i]:B[k]=q\}|} . בחר (select) - שאילתה המקבלת אינדקס i ואיבר q ( q ∈ { 0 , 1 } {\displaystyle q\in \{0,1\}} ) ומחזירה את המיקום של האיבר ה-i שהערך שלו הוא q. פורמלית: s e l e c t q ( i ) = min { k ∈ [ 0 … n ) : r a n k q ( k ) = i } {\displaystyle \mathbf {select} _{q}(i)=\min\{k\in [0\dots n):\mathbf {rank} _{q}(k)=i\}} . ישנו ייצוג פשוט המשתמש ב  n + o ( n ) {\displaystyle n+o(n)} ביטים של זיכרון (מערך הביטים המקורי ומבנה עזר בגודל o ( n ) {\displaystyle o(n)} ) התומך בפעולות דרגה (rank) ובחירה (select) בזמן קבוע. הוא משתמש ברעיון דומה לזה של שאילתות מינימום-טווח; ישנו מספר קבוע של רקורסיות לפני שעוצרים בתת-בעיה בגודל מוגבל. מערך הביטים   B {\displaystyle B} מחולק לגושים גדולים בגודל  l = lg 2 ⁡ n {\displaystyle l=\lg ^{2}n}  ביטים וגושים קטנים בגודל  s = lg ⁡ n / 2 {\displaystyle s=\lg n/2}  ביטים. לכל גוש גדול מאחסנים את הדרגה של הביט הראשון בטבלה נפרדת  R l [ 0 … n / l ) {\displaystyle R_{l}[0\dots n/l)} ; כל כניסה כזאת דורשת  lg ⁡ n {\displaystyle \lg n} ביטים וסך הכל של  ( n / l ) lg ⁡ n = n / lg ⁡ n {\displaystyle (n/l)\lg n=n/\lg n} ביטים של זיכרון. בגוש גדול מאוחסנת טבלה נוספת  R s [ 0 … l / s ) {\displaystyle R_{s}[0\dots l/s)} , השומרת את הדרגה של כל אחד מ- l / s = 2 lg ⁡ n {\displaystyle l/s=2\lg n}   הבלוקים הקטנים שהיא מכילה. ההבדל כאן הוא שדרושים רק  lg ⁡ l = lg ⁡ lg 2 ⁡ n = 2 lg ⁡ lg ⁡ n {\displaystyle \lg l=\lg \lg ^{2}n=2\lg \lg n}  ביטים לכל כניסה, כיוון שמספיק לשמור רק את ההפרשים מהדרגה של הביט הראשון בגוש הגדול המכיל. לכן, הטבלה דורשת סך הכל   ( n / s ) lg ⁡ l = 4 n lg ⁡ lg ⁡ n / lg ⁡ n {\displaystyle (n/s)\lg l=4n\lg \lg n/\lg n} ביטים. ניתן להשתמש בטבלת חיפוש  R p {\displaystyle R_{p}}  כדי לשמור את התשובה לכל שאילתת דרגה אפשרית במחרוזת ביטים באורך  s {\displaystyle s} לכל  i ∈ [ 0 , s ) {\displaystyle i\in [0,s)} , בעלות של 2 s s lg ⁡ s = O ( n lg ⁡ n lg ⁡ lg ⁡ n ) {\displaystyle 2^{s}s\lg s=O({\sqrt {n}}\lg n\lg \lg n)} ביטים של זיכרון. כיוון שכל טבלאות העזר הללו צורכות  o ( n ) {\displaystyle o(n)} זיכרון, מבנה הנתונים הזה תומך בשאילתות דרגה בזמן  O ( 1 ) {\displaystyle O(1)} ו n + o ( n ) {\displaystyle n+o(n)}  ביטים של זיכרון. כדי לענות על שאילתה עבור r a n k 1 ( i ) {\displaystyle \mathbf {rank} _{1}(i)} בזמן קבוע, האלגוריתם מחשב בזמן קבוע: r a n k 1 ( i ) = R l [ ⌊ i / l ⌋ ] + R s [ ⌊ i / s ⌋ ] + R p [ i ⌊ i / s ⌋ , i  mod  s ] {\displaystyle \mathbf {rank} _{1}(i)=R_{l}[\lfloor i/l\rfloor ]+R_{s}[\lfloor i/s\rfloor ]+R_{p}[i\lfloor i/s\rfloor ,i{\text{ mod }}s]} בפועל, ניתן להחליף את טבלת החיפוש  R p {\displaystyle R_{p}}  בפעולות על ביטים ובטבלאות קטנות יותר כדי למצוא את מספר הביטים הדולקים בבלוקים הקטנים. זה לעיתים קרובות מועיל, שכן מבני נתונים תמציתיים משמשים למערכות נתונים גדולות, שבהן החטאות מטמון נעשות הרבה יותר תכופות, והסיכויים שטבלת החיפוש תפונה מהמטמון גדלים. ניתן לתמוך בקלות בשאילתות בחירה על ידי ביצוע חיפוש בינארי על אותו מבנה עזר המשמש לפעולות דרגה; עם זאת, דבר זה דורש O ( lg ⁡ n ) {\displaystyle O(\lg n)} זמן במקרה הגרוע ביותר. ניתן להשתמש במבנה מסובך יותר המשתמש ב- 3 n / lg ⁡ lg ⁡ n + O ( n lg ⁡ n lg ⁡ lg ⁡ n ) = o ( n ) {\displaystyle 3n/\lg \lg n+O({\sqrt {n}}\lg n\lg \lg n)=o(n)} ביטים של זיכרון נוסף, כדי לתמוך בפעולות בחירה בזמן קבוע. למעשה, ברבים מהפתרונות האלה מוסתרים קבועים בסימון  O ( ⋅ ) {\displaystyle O(\cdot )}  אשר שולטים בבעיה לפני שניכר יתרון אסימפטוטי כל שהוא; יישומים באמצעות פעולות נרחבות על מילים ובלוקים של מילים-מיושרות לעיתים קרובות מראים ביצועים טובים יותר בפועל. גישת ה- n + o ( n ) {\displaystyle n+o(n)}  זיכרון ניתנת לשיפור בזכות ההבחנה כי ישנם  ( n m ) {\displaystyle \textstyle {\binom {n}{m}}}  תתי קבוצות של [ n ) {\displaystyle [n)}  באורך m {\displaystyle m} (או מחרוזות בינאריות באורך n {\displaystyle n}   עם בדיוק m {\displaystyle m} 1ים), ולכן B ( m , n ) = ⌈ lg ⁡ ( n m ) ⌉ {\displaystyle \textstyle {\mathcal {B}}(m,n)=\lceil \lg {\binom {n}{m}}\rceil } הוא החסם התחתון של תורת האינפורמציה על מספר הביטים הדרושים לאחסון B {\displaystyle B} . ישנו מילון (סטטי) תמציתי המשיג את החסם הזה, דהיינו משתמש ב- B ( m , n ) + o ( B ( m , n ) ) {\displaystyle {\mathcal {B}}(m,n)+o({\mathcal {B}}(m,n))} זיכרון. ניתן להרחיב את המבנה הזה כדי לתמוך בשאילתות דרגה ובחירה, תוך שימוש ב-  B ( m , n ) + O ( m + n lg ⁡ lg ⁡ n / lg ⁡ n ) {\displaystyle {\mathcal {B}}(m,n)+O(m+n\lg \lg n/\lg n)} זיכרון. ניתן לצמצם את החסם הזה לתחלופת זיכרון/זמן על ידי הקטנת מקום האחסון של המילון ל- B ( m , n ) + O ( n t t / lg t ⁡ n + n 3 / 4 ) {\displaystyle {\mathcal {B}}(m,n)+O(nt^{t}/\lg ^{t}n+n^{3/4})}  וזמן שאילתה  O ( t ) {\displaystyle O(t)}  . כאשר צריך לקודד רצף של פריטים בעלי אורך משתנה (כגון מחרוזות), ישנן מספר אפשרויות: גישה ישירה היא לשמור את האורך ואת הפריט באותה רשומה- והללו יכולות להיות ממוקמות אחת אחרי השנייה. זה מאפשר מעבר יעיל לעוקב, אבל לא מוצא את הפריט הkי. אופציה נוספת היא למקם את הפריטים לפי הסדר עם תו מפריד (למשל, מחרוזת עם סיומת אפס). אפשרות זו משתמשת במפריד במקום באורך, והיא איטית יותר באופן משמעותי, מפני שיש לסרוק את כל הרצף כדי למצוא תווים מפרידים. שתי האפשרויות יעילות מבחינת זיכרון. גישה אחרת היא הפרדה מאוגדת: ניתן למקם את הפריטים זה אחר זה, ללא מפרידים. ניתן לשמור את גבולות הפריט יכולים כרצף של אורכים, או באופן יעיל יותר, כהיסטים בתוך רצף זה. לחלופין, מקודדים בנוסף מחרוזת בינארית נפרדת המורכבת מ1ים במקומות בהם פריט מתחיל, ו0ים בכל מקום אחר. בהינתן המחרוזת זאת, פונקציית ה s e l e c t {\displaystyle select}  יכולה לקבוע במהירות היכן כל פריט מתחיל, בהינתן האינדקס שלו. מבנה זה קומפקטי אך לא תמציתי, היות שהוא צורך 2Z זיכרון, דהיינו (O(Z. דוגמה נוספת היא ייצוג של עץ בינארי: עץ בינארי שרירותי על n {\displaystyle n} צמתים ניתן לייצג ב- 2 n + o ( n ) {\displaystyle 2n+o(n)} ביטים, תוך תמיכה במגוון רחב של פעולות על כל צומת, אשר כוללות את מציאת האב שלו, הילד הימני והשמאלי שלו, והחזרת גודל של תת-עץ, כל אחת בזמן קבוע. מספר העצים הבינאריים השונים על n {\displaystyle n} קודקודים הוא ( 2 n n ) / ( n + 1 ) {\displaystyle {\tbinom {2n}{n}}/(n+1)} . עבור n {\displaystyle n} גדול, ערך זה הוא בערך 4 n {\displaystyle 4^{n}} ; ולכן ידרשו לכל הפחות כ- log 2 ⁡ ( 4 n ) = 2 n {\displaystyle \log _{2}(4^{n})=2n} ביטים כדי לקודד אותו. עץ בינארי תמציתי לפיכך יתפוס רק 2 {\displaystyle 2} ביטים לכל צומת. פונקציית גיבוב מינימלית מושלמת Jacobson, G. J (1988). Succinct static data structures (Ph.D.). Pittsburgh, PA: Carnegie Mellon University. Raman, R.; V. Raman; S. S Rao (2002). "Succinct indexable dictionaries with applications to encoding k-ary trees and multisets". Proceedings of the thirteenth annual ACM-SIAM symposium on Discrete algorithms (PDF). pp. 233–242. ISBN 0-89871-513-X. Sadakane, K.; R. Grossi (2006). "Squeezing succinct data structures into entropy bounds". Proceedings of the seventeenth annual ACM-SIAM symposium on Discrete algorithm (PDF). pp. 1230–1239. ISBN 0-89871-605-5. אורכב מ-המקור (PDF) ב-2011-09-29. נבדק ב-2017-01-02. Jacobson, G. (1989). "Space-efficient static trees and graphs" (PDF). González, R.; S. Grabowski; V. Mäkinen; G. Navarro (2005). "Practical implementation of rank and select queries". Poster Proceedings Volume of 4th Workshop on Efficient and Experimental Algorithms (WEA) (PDF). pp. 27–38. Clark, D. (1998). "Compact pat trees" (PDF). Vigna, S. (2008). "Broadword implementation of rank/select queries" (PDF). Experimental Algorithms. Lecture Notes in Computer Science. pp. 154–168. doi:10.1007/978-3-540-68552-4_12. ISBN 978-3-540-68548-7. אורכב מ-המקור (PDF) ב-2016-06-11. נבדק ב-2017-01-02. Brodnik, A.; J. I Munro (1999). "Membership in constant time and almost-minimum space" (PDF). SIAM J. Comput. pp. 1627–1640. doi:10.1137/S0097539795294165. Pătraşcu, M. (2008). "Succincter". Foundations of Computer Science, 2008. FOCS'08. IEEE 49th Annual IEEE Symposium on (PDF). pp. 305–313. Belazzougui, Djamal. "Hash, displace, and compress" (PDF).