מדריך ל- BitSet בג'אווה

1. סקירה כללית

במדריך זה, נראה כיצד נוכל להשתמש BitSets לייצג וקטור של ביטים.

ראשית, נתחיל ברציונל שמאחורי אי שימוש ב- בוליאני []. ואז לאחר היכרות עם BitSet פנימיים, נבחן מקרוב את ה- API שלו.

2. מערך ביטים

כדי לאחסן ולתפעל מערכים של ביטים, אפשר לטעון שעלינו להשתמש בוליאני [] כמבנה הנתונים שלנו. במבט ראשון, זו עשויה להיראות הצעה סבירה.

עם זאת, כל אחד בוליאני חבר בא בוליאני [] בדרך כלל צורכת בת אחד במקום סיבית אחת בלבד. אז כשיש לנו דרישות זיכרון הדוקות, או שאנחנו רק מכוונים לטביעת רגל זיכרון מופחתת, בוליאני [] רחוקים מלהיות אידיאליים.

כדי להפוך את העניין לקונקרטי יותר, בואו נראה כמה מקום א בוליאני [] עם 1024 אלמנטים צורכת:

בוליאני [] ביטים = בוליאני חדש [1024]; System.out.println (ClassLayout.parseInstance (bits) .toPrintable ());

באופן אידיאלי, אנו מצפים לטביעת רגל זיכרון של 1024 סיביות ממערך זה. עם זאת, פריסת ה- Java Object (JOL) חושפת מציאות שונה לחלוטין:

[פנימי של אובייקט Z: קיזוז גודל סוג תיאור ערך 0 4 (כותרת עצם) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (כותרת אובייקט) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (כותרת עצם) 7b 12 07 00 (01111011 00010010 00000111 00000000) (463483) 12 4 (כותרת עצם) 00 04 00 00 (00000000 00000100 00000000 00000000) (1024) 16 1024 בוליאני [Z. גודל מופע לא מתאים: 1040 בתים

אם נתעלם מהתקורה של כותרת האובייקט, רכיבי המערך צורכים 1024 בתים, במקום 1024 סיביות הצפויות. זה 700% יותר זיכרון ממה שציפינו.

הבעיות כתיביות וקריעת מילים הן הסיבות העיקריות לכך בוליאניs הם יותר מסיבית אחת בלבד.

כדי לפתור בעיה זו, נוכל להשתמש בשילוב של סוגי נתונים מספריים (כגון ארוך) ופעולות קצת חכמות. זה המקום בו BitSet מגיע ב.

3. איך BitSet עובד

כפי שהזכרנו קודם, כדי להשיג את הסיבית האחת לכל שימוש בזיכרון הדגל, ה- BitSet API משתמש בשילוב של סוגי נתונים מספריים בסיסיים ופעולות סיביות.

למען הפשטות, נניח שנציג שמונה דגלים עם אחד בתים. בהתחלה, אנו מאותחלים את כל החלקים של הסינגל הזה בתים עם אפס:

עכשיו אם אנחנו רוצים להגדיר את הסיבוב במיקום שלוש ל נָכוֹן, ראשית עלינו לשנות את המספר 1 בשלושה:

ואז אוֹ התוצאה שלה עם הזרם בתים ערך:

אותו תהליך יקרה אם תחליט לקבוע את הסיביות באינדקס שבע:

כפי שמוצג לעיל, אנו מבצעים משמרת שמאלה בשבעה ביטים ומשלבים את התוצאה עם הקודמת בתים ערך באמצעות אוֹ מַפעִיל.

3.1. קבלת אינדקס ביט

כדי לבדוק אם מוגדר אינדקס סיביות מסוים נָכוֹן או לא, נשתמש ב- ו מַפעִיל. למשל, כך אנו בודקים אם מוגדר אינדקס שלוש:

  1. ביצוע משמרת שמאלה בשלושה ביטים בערך אחד
  2. אנדינג את התוצאה עם הזרם בתים ערך
  3. אם התוצאה גדולה מאפס, אז מצאנו התאמה, ומדד הסיביות נקבע בפועל. אחרת, האינדקס המבוקש ברור או שווה ל- שֶׁקֶר

התרשים שלעיל מציג את שלבי הפעולה לקבלת אינדקס שלוש. אם אנו מבררים על אינדקס ברור, התוצאה תהיה שונה:

מאז ו התוצאה שווה לאפס, מדד ארבע ברור.

3.2. גידול האחסון

נכון לעכשיו, אנו יכולים לאחסן רק וקטור של 8 ביטים. כדי לעבור את המגבלה הזו, אנחנו רק צריכים להשתמש במערך של בתיםבמקום במקום יחיד בתים, זהו זה!

כעת, בכל פעם שאנחנו צריכים להגדיר, לקבל או לנקות אינדקס ספציפי, עלינו למצוא קודם את אלמנט המערך המתאים. למשל, נניח שנגדיר את אינדקס 14:

כפי שמוצג בתרשים לעיל, לאחר שמצאנו את אלמנט המערך הנכון, הגדרנו את האינדקס המתאים.

כמו כן, אם אנו רוצים לקבוע כאן אינדקס מעבר ל 15, ה- BitSet תרחיב את המערך הפנימי שלה, תחילה. רק לאחר הרחבת המערך והעתקת האלמנטים הוא יגדיר את הסיבית המבוקשת. זה קצת דומה לאיך רשימת מערך עובד באופן פנימי.

עד כה השתמשנו ב- בתים סוג נתונים לשם פשטות. ה BitSet API, לעומת זאת, משתמש במערך של ארוך ערכים פנימיים.

4. ה BitSet ממשק API

עכשיו שאנחנו יודעים מספיק על התיאוריה, הגיע הזמן לראות מה BitSet ממשק API נראה כמו.

בתור התחלה, בואו נשווה את טביעת הרגל של הזיכרון של a BitSet מופע עם 1024 ביטים עם בוליאני [] ראינו קודם:

BitSet bitSet = BitSet חדש (1024); System.out.println (GraphLayout.parseInstance (bitSet) .toPrintable ());

פעולה זו תדפיס את הגודל הרדוד של BitSet מופע וגודל המערך הפנימי שלו:

[מוגן בדוא"ל] חיצוני של אובייקטים: ADDRESS SIZE TYPE PATH VALUE 70f97d208 24 java.util.BitSet (object) 70f97d220 144 [J .words [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 , 0, 0, 0, 0, 0]

כפי שמוצג לעיל, הוא משתמש ב- ארוך[] עם 16 אלמנטים (16 * 64 סיביות = 1024 סיביות) פנימית. בכל מקרה, במקרה זה משתמשים בסך 168 בתים, בעוד ש- בוליאני [] השתמשו ב -1024 בתים.

ככל שיש לנו יותר סיביות, כך גדל ההבדל בטביעת הרגל. לדוגמא, כדי לאחסן 1024 * 1024 סיביות, ה- בוליאני [] צורכת 1 מגהבייט, ואת BitSet מופע צורך כ -130 KB.

4.1. בונה BitSetס

הדרך הפשוטה ביותר ליצור BitSet למשל הוא להשתמש בבנאי ללא ארג:

BitSet bitSet = BitSet חדש ();

זה ייצור BitSet מופע עם א ארוך[] בגודל אחד. כמובן שהוא יכול לגדול באופן אוטומטי מערך זה במידת הצורך.

אפשר גם ליצור BitSet עם מספר ביטים ראשוני:

BitSet bitSet = BitSet חדש (100_000);

כאן, למערך הפנימי יהיו מספיק אלמנטים להכיל 100,000 ביטים. קונסטרוקטור זה יועיל כאשר יש לנו כבר אומדן סביר למספר הביטים לאחסון. במקרי שימוש כאלה, זה יכול למנוע או להקטין את ההעתקה המיותרת של אלמנטים במערך בזמן הגדלתו.

אפשר אפילו ליצור BitSet מקיים ארוך[], בתים [], לונג באפר, ו ByteBuffer. למשל, כאן אנו יוצרים BitSet מופע מנתון ארוך[]:

BitSet bitSet = BitSet.valueOf (חדש ארוך [] {42, 12});

ישנן שלוש גרסאות עמוסות יותר של ה- ערך של() שיטת מפעל סטטית לתמיכה בסוגים האחרים שהוזכרו.

4.2. הגדרת ביטים

אנו יכולים להגדיר את הערך של אינדקס מסוים ל- נָכוֹן משתמש ב סט (אינדקס) שיטה:

BitSet bitSet = BitSet חדש (); bitSet.set (10); assertThat (bitSet.get (10)). isTrue ();

כרגיל, המדדים מבוססים על אפס. אפשר אפילו להגדיר טווח ביטים ל נָכוֹן משתמש ב סט (מ- Inclusive, toExclusive) שיטה:

bitSet.set (20, 30); עבור (int i = 20; i <= 29; i ++) {assertThat (bitSet.get (i)). isTrue (); } assertThat (bitSet.get (30)). isFalse ();

כפי שעולה מחתימת השיטה, אינדקס ההתחלה כולל, והסיום הוא בלעדי.

כשאומרים הגדרת אינדקס, בדרך כלל מתכוונים להגדיר אותו ל נָכוֹן. למרות המינוח הזה, אנו יכולים להגדיר אינדקס סיביות מסוים ל שֶׁקֶר משתמש ב סט (אינדקס, בוליאני) שיטה:

bitSet.set (10, false); assertThat (bitSet.get (10)). isFalse ();

גרסה זו תומכת גם בקביעת טווח ערכים:

bitSet.set (20, 30, false); עבור (int i = 20; i <= 29; i ++) {assertThat (bitSet.get (i)). isFalse (); }

4.3. ניקוי ביטים

במקום להגדיר אינדקס ביט ספציפי ל שֶׁקֶראנחנו יכולים פשוט לנקות את זה באמצעות נקה (אינדקס) שיטה:

bitSet.set (42); assertThat (bitSet.get (42)). isTrue (); bitSet.clear (42); assertThat (bitSet.get (42)). isFalse ();

יתר על כן, אנו יכולים גם לנקות טווח ביטים באמצעות ה- ברור (מ- Inclusive, toExclusive) גרסה עמוסה:

bitSet.set (10, 20); עבור (int i = 10; i <20; i ++) {assertThat (bitSet.get (i)). isTrue (); } bitSet.clear (10, 20); עבור (int i = 10; i <20; i ++) {assertThat (bitSet.get (i)). isFalse (); }

מעניין, אם אנו קוראים לשיטה זו מבלי להעביר טיעונים, היא תנקה את כל הביטים:

bitSet.set (10, 20); bitSet.clear (); עבור (int i = 0; i <100; i ++) {assertThat (bitSet.get (i)). isFalse (); }

כפי שמוצג לעיל, לאחר קריאה ל- ברור() שיטה, כל הביטים מוגדרים לאפס.

4.4. מקבל ביטים

עד כה השתמשנו ב- קבל (אינדקס) שיטה די נרחבת. כאשר מוגדר אינדקס הסיביות המבוקש, שיטה זו תחזור נָכוֹן. אחרת זה יחזור שֶׁקֶר:

bitSet.set (42); assertThat (bitSet.get (42)). isTrue (); assertThat (bitSet.get (43)). isFalse ();

דומה ל מַעֲרֶכֶת ו ברור, אנו יכולים לקבל מגוון של מדדי סיביות באמצעות ה- קבל (מ- Inclusive, toExclusive) שיטה:

bitSet.set (10, 20); BitSet newBitSet = bitSet.get (10, 20); עבור (int i = 0; i <10; i ++) {assertThat (newBitSet.get (i)). isTrue (); }

כפי שמוצג לעיל, שיטה זו מחזירה שיטה אחרת BitSet בטווח [20, 30) של הנוכחי. כלומר מדד 20 של bitSet משתנה שווה ערך לאפס אינדקס של newBitSet מִשְׁתַנֶה.

4.5. ביטים מרפרפים

כדי לשלול את ערך אינדקס הסיביות הנוכחי, אנו יכולים להשתמש ב- הפוך (אינדקס) שיטה. כלומר, זה יהפוך נָכוֹן ערכים ל שֶׁקֶר ולהיפך:

bitSet.set (42); bitSet.flip (42); assertThat (bitSet.get (42)). isFalse (); bitSet.flip (12); assertThat (bitSet.get (12)). isTrue ();

באופן דומה, אנו יכולים להשיג את אותו הדבר עבור מגוון ערכים באמצעות ה- הפוך (מ- Inclusive, toExclusive) שיטה:

bitSet.flip (30, 40); עבור (int i = 30; i <40; i ++) {assertThat (bitSet.get (i)). isTrue (); }

4.6. אורך

ישנן שלוש שיטות דומות לאורך BitSet. ה גודל() השיטה מחזירה את מספר הביטים שהמערך הפנימי יכול לייצג. למשל, מכיוון שהבנאי ללא ארגון מקצה א ארוך[] מערך עם אלמנט אחד, ואז ה- גודל() יחזיר 64 עבור זה:

BitSet defaultBitSet = BitSet חדש (); assertThat (defaultBitSet.size ()). isEqualTo (64);

עם מספר 64 סיביות אחד, אנו יכולים לייצג 64 סיביות בלבד. כמובן שזה ישתנה אם נעביר את מספר הביטים במפורש:

BitSet bitSet = BitSet חדש (1024); assertThat (bitSet.size ()). isEqualTo (1024);

יתר על כן, ה מספר איברים בקבוצה() השיטה מייצגת את מספר הביטים בקבוצת a BitSet:

assertThat (bitSet.cardinality ()). isEqualTo (0); bitSet.set (10, 30); assertThat (bitSet.cardinality ()). isEqualTo (30 - 10);

בהתחלה, שיטה זו מחזירה אפס כמו כל הביטים שֶׁקֶר. לאחר הגדרת טווח [10, 30) ל- נָכוֹן, אז ה מספר איברים בקבוצה() שיחת שיטה מחזירה 20.

וגם ה אורך() השיטה מחזירה את האינדקס האחד אחרי האינדקס של הביט הקבוע האחרון:

assertThat (bitSet.length ()). isEqualTo (30); bitSet.set (100); assertThat (bitSet.length ()). isEqualTo (101);

בהתחלה, מדד הסט האחרון הוא 29, ולכן שיטה זו מחזירה 30. כאשר אנו מגדירים את המדד 100 לאמיתי, אז ה- אורך() שיטה מחזירה 101. ראוי גם להזכיר כי שיטה זו תחזיר אפס אם כל הביטים ברורים.

סוף - סוף, ה זה ריק() השיטה מחזירה שֶׁקֶר כשיש לפחות סט קבוצה אחד ב BitSet. אחרת זה יחזור נָכוֹן:

assertThat (bitSet.isEmpty ()). isFalse (); bitSet.clear (); assertThat (bitSet.isEmpty ()). isTrue ();

4.7. שילוב עם אחר BitSetס

ה מצטלב (BitSet) שיטה לוקח אחרת BitSet וחוזר נָכוֹן כששניים BitSetיש משהו במשותף. כלומר, יש להם לפחות קבוצה אחת באותו אינדקס:

BitSet ראשון = BitSet חדש (); first.set (5, 10); BitSet שנייה = BitSet חדש (); second.set (7, 15); assertThat (first.intersects (second)). isTrue ();

טווח [7, 9] מוגדר בשניהם BitSets, כך ששיטה זו חוזרת נָכוֹן.

אפשר גם לבצע את ההגיוני ו פעולה על שתיים BitSetס:

ראשון.ו (שני); assertThat (first.get (7)). isTrue (); assertThat (first.get (8)). isTrue (); assertThat (first.get (9)). isTrue (); assertThat (first.get (10)). isFalse ();

זה יבצע הגיוני ו בין השניים BitSets ומשנה את ראשון משתנה עם התוצאה. באופן דומה, אנו יכולים לבצע הגיון xor בשתיים BitSetגם זה:

first.clear (); first.set (5, 10); first.xor (שני); עבור (int i = 5; i <7; i ++) {assertThat (first.get (i)). isTrue (); } עבור (int i = 10; i <15; i ++) {assertThat (first.get (i)). isTrue (); }

ישנן שיטות אחרות כגון andNot (BitSet) או ה או (BitSet),שיכולים לבצע פעולות לוגיות אחרות בשניים BitSetס.

4.8. שונות

נכון ל- Java 8, יש זרם() שיטה להזרים את כל החלקים המוגדרים של BitSet. לדוגמה:

BitSet bitSet = BitSet חדש (); bitSet.set (15, 25); bitSet.stream (). forEach (System.out :: println);

פעולה זו תדפיס את כל הביטים המוגדרים למסוף. מכיוון שזה יחזיר IntStream, אנו יכולים לבצע פעולות מספריות נפוצות כגון סיכום, ממוצע, ספירה וכן הלאה. לדוגמה, כאן אנו סופרים את מספר הקבוצות:

assertThat (bitSet.stream (). count ()). isEqualTo (10);

גַם, ה nextSetBit (fromIndex) השיטה תחזיר את אינדקס הסיביות הבא שנקבע החל מ- מ- Index:

assertThat (bitSet.nextSetBit (13)). isEqualTo (15);

ה מ- Index עצמה כלולה בחישוב זה. כשאין כאלה נָכוֹן קצת נשאר ב BitSet, זה יחזור -1:

assertThat (bitSet.nextSetBit (25)). isEqualTo (-1);

בדומה לכך, ה nextClearBit (מאתIndex) מחזיר את המדד הברור הבא החל מ- מ- Index:

assertThat (bitSet.nextClearBit (23)). isEqualTo (25);

מצד שני, ה previousClearBit (fromIndex) מחזיר את המדד של המדד הברור הקרוב ביותר בכיוון ההפוך:

assertThat (bitSet.previousClearBit (24)). isEqualTo (14);

הדבר נכון גם לגבי previousSetBit (fromIndex):

assertThat (bitSet.previousSetBit (29)). isEqualTo (24); assertThat (bitSet.previousSetBit (14)). isEqualTo (-1);

יתר על כן, אנו יכולים להמיר א BitSet אל א בתים [] או א ארוך[] משתמש ב toByteArray () אוֹ toLongArray () שיטות, בהתאמה:

בתים [] בתים = bitSet.toByteArray (); long [] longs = bitSet.toLongArray ();

5. מסקנה

במדריך זה ראינו כיצד אנו יכולים להשתמש BitSets לייצג וקטור של ביטים.

בהתחלה הכרנו את הרציונל מאחורי אי שימוש בוליאני [] לייצג וקטור של ביטים. ואז ראינו איך א BitSet עובד באופן פנימי ואיך נראה ה- API שלו.

כרגיל, כל הדוגמאות זמינות ב- GitHub.