You are on page 1of 34

‫‪Data Structures‬‬ ‫מבני נתונים‬

‫
‬
‫הרצאה שניה‪ :‬מערכים‪ ,‬מטריצות‬
‫דלילות‪ ,‬ורשימות מקושרות‬
‫אורי רוטנשטרייך‬
‫רועי אנגלברג‬
‫השקפים הוכנו ע״י פרופ׳ ארז פטרנק‬
‫)‪Chapter 11.2– Linked lists (204 – 213‬‬

‫‪1‬‬
‫מערך )כמבנה נתונים אבסטרקטי(‬
‫העיקרון‪ :‬אוסף איברים הנגישים לפי אינדקס‬

‫מערך מכיל אוסף של איברים וקבוצת אינדקסים ‪ ;I‬מערך תומך בפעולות הבאות‪:‬‬
‫‪ .1‬הפעולה )‪ create(type,I‬מחזירה מערך של איברים מטיפוס ‪ type‬עם‬
‫אינדקסים בקבוצה ‪.I‬‬
‫‪ .2‬הפעולה )‪ store(A,i,e‬מוסיפה את איבר ‪ e‬מטיפוס ‪ type‬עם אינדקס ‪ i‬לאברי‬
‫המערך ) ‪
 .( i 2 I‬‬
‫קיצור מקובל לפעולה זו‪. A[i] = e :‬‬
‫‪ .3‬הפעולה )‪ get(A,i‬מחזירה את האיבר שהוכנס למערך אחרון עם פקודת‬
‫‪ store‬שהשתמשה באינקס ‪
 .i‬‬
‫קיצור מקובל לפעולה זו‪. A[i] :‬‬

‫‪ ‬‬

‫‪© cs, Technion‬‬ ‫‪2‬‬


‫כללים למערך‬
‫• כללים‪:‬‬
‫כל הערכים במערך הם מאותו טיפוס‪ ,‬והוא נקבע בעת יצירתו‬ ‫•‬
‫בפעולה ‪.create‬‬
‫לא הוגדר‪:‬‬ ‫•‬
‫מה קורה אם מנסים להכניס איבר שאינו מטיפוס ‪type‬‬ ‫•‬
‫מה קורה אם קוראים מאינדקס שלא הוכנס לו כלום מעולם‬ ‫•‬

‫‪3‬‬
‫מימושים של מערך‬
‫)נניח אינדקסים שלמים ורצופים(‬

‫מימוש ראשון‪ :‬איזור זכרון רצוף‬ ‫•‬


‫סיבוכיות‪:‬‬ ‫•‬
‫זמן ביצוע ‪ store‬ו‪ get-‬הוא )‪.O(1‬‬ ‫•‬
‫זמן יצירת מערך חדש תלוי בזמן הקצאת מקום )תלוי‬ ‫•‬
‫במערכת( ובשאלה אם צריך למלא את כולו בערך‬
‫התחלתי בעת היצירה‪.‬‬

‫‪4‬‬
‫מימוש שני‬
‫מספר איזורים רצופים בזיכרון שלכל אחד גודל‬
‫קבוע ‪.n‬‬
‫‪ ‬‬
‫‪base‬‬ ‫‪ ‬‬

‫‪ ‬‬
‫‪ ‬‬

‫‪ ‬‬
‫למה זה מועיל?‬

‫‪5‬‬
‫מערך דו‪-‬מימדי‬
‫;]‪int A[m][n‬‬

‫‪ ‬‬

‫"‪!",‬‬ ‫&‪!",‬‬ ‫&‪!",$%‬‬

‫"‪!&,‬‬ ‫&‪!&,‬‬ ‫&‪!&,$%‬‬

‫⋮‬ ‫⋮‬ ‫⋮‬

‫&‪!'%&," !'%&,‬‬ ‫&‪!'%&,$%‬‬

‫‪  ‬ניתן לממש כאיזור זיכרון רציף‪ .‬נניח ש‪ base-‬היא הכתובת של ]‪ .A[0][0‬אז‪:‬‬

‫‪© cs, Technion‬‬ ‫‪6‬‬


‫מערך רב‪-‬מימדי‬
‫‪ ‬‬

‫במערך תלת מימדי ]‪ A[n3][n2][n1‬הכתובת של איבר ]‪ A[i3][i2][i1‬מחושבת ע״י הנוסחה‪:‬‬


‫‪base + i3 · n2 · n1 + i2 · n1 + i1‬‬

‫‪ ‬‬

‫‪ ‬‬
‫‪ ‬‬ ‫‪ ‬‬

‫‪ ‬‬ ‫‪ ‬‬ ‫‪ ‬‬

‫‪ ‬‬

‫‪© cs, Technion‬‬ ‫‪7‬‬


‫מימדי‬-‫חישוב כתובת יעיל עבור מערך רב‬
 

 
p=a[k];
for (j=k-1; j>=0; j--)
p = p⋅x + a[j]

addr=i[d];
for (j=d-1; j>=1; j--)
addr = addr*n[j] + i[j];
addr= base + addr;

.‫ כפלים‬d-‫ כפלים ל‬d2-‫יורדים מ‬


© cs, Technion 8
‫מימדי‬-‫סריקת מערך רב‬

‫אופציה ב‬ ‫אופציה א‬

for (i=0, i<n, i++) for (i=0, i<n, i++)


for (j=0, j<n, j++) for (j=0, j<n, j++)
A[i][j] = 0; A[j][i] = 0;

!"," !",& !",$%&

!&," !&,& !&,$%&

⋮ ⋮ ⋮

!'%&," !'%&,& !'%&,$%&

© cs, Technion 9
‫לוקליות של גישה לזכרון‬

‫חשוב לביצועים )ברמת‬ ‫לא בחומר של הקורס‪ ,‬אבל‪,‬‬


‫הקבועים(‪.‬‬
‫תפגשו את זה במערכות הפעלה ובמבנה מחשבים ספרתיי 
‬
‫ם‬

‫עובדה‪ :‬קריאה מהזכרון הרבה יותר יקרה מביצוע פעולה במעבד‪.‬‬

‫אופטימיזציות הממומשות בכל מחשב‪:‬‬


‫‪ (1‬המעבד ״זוכר״ את המידע האחרון שקרא‪ ,‬ולא צריך לקרוא אותו שוב‪.‬‬
‫‪ (2‬כשצריך לקרוא מילה‪ ,‬המעבד קורא עוד כמה מילים קדימה‪.‬‬

‫שיקולים כאלה לא יטרידו אותנו בקורס הזה יותר‪ ,‬ובוודאי‬


‫לא ברמה היותר תאורטית של סיבוכיות אסימפטוטית‪.‬‬

‫‪© cs, Technion‬‬ ‫‪10‬‬


‫איתחול מערך בזמן )‪O(1‬‬
‫בייצוג הרגיל‪ ,‬איתחול מערך בגודל ‪ n‬דורש זמן )‪.O(n‬‬ ‫•‬
‫נרצה לאתחל בזמן קבוע )‪ .O(1‬האם ניתן?‬ ‫•‬
‫בתמורה נרשה שימוש ביותר זיכרון‪.‬‬ ‫•‬
‫הבחנה ראשונה‪ :‬אי אפשר באמת לאתחל כל כך מהר‪ ,‬אבל אנחנו‬ ‫•‬
‫שולטים ב‪ get-‬וב‪ .store-‬אז מה כדאי לעשות?‬
‫פיתרון‪ :‬נשמור מידע על אילו אינדקסים כבר נכתבו ואם ניגשים לאינדקס‬ ‫•‬
‫שלא נכתב עליו כלום‪ ,‬נחזיר את הערך ההתחלתי‪.‬‬
‫נסיון ראשון‪ :‬נשמור רשימה של האינקסים שנכתבו במערך שני‪.‬‬ ‫•‬

‫‪© cs, Technion‬‬ ‫‪11‬‬


‫‪ ‬‬
‫האינדקסים‬
‫המערך‬
‫הרלוונטיים‬

‫‪5 333‬‬ ‫‪453 5‬‬ ‫זבל‬


‫‪4 7‬‬ ‫‪643 4‬‬
‫‪222‬‬
‫‪3 222‬‬ ‫‪123 3‬‬ ‫‪top‬‬

‫‪2 111‬‬ ‫‪101‬‬


‫‪0 2‬‬ ‫‪top‬‬
‫‪3‬‬
‫‪1 131‬‬ ‫‪1 1‬‬
‫‪177‬‬ ‫‪top‬‬
‫‪212‬‬
‫‪5‬‬
‫‪0 212‬‬ ‫‪4‬‬ ‫‪0‬‬

‫בעיה‪ :‬הסיבוכיות של‬


‫אחת הפעולות נפגעת‪.‬‬

‫• איזו פעולה?‬
‫• איך נפתור את זה?‬
‫בדוגמא זו‪V[0]=5, V[1]=3, V[4]=7 :‬‬

‫‪© cs, Technion‬‬ ‫‪12‬‬


‫‪ ‬‬

‫קיצורי דרך‬ ‫‪  ‬האינדקסים‬


‫הרלוונטיים‬

‫‪5 333‬‬ ‫‪5 234‬‬ ‫‪453 5‬‬ ‫זבל‬


‫‪4 7‬‬‫‪7‬‬ ‫‪4 0‬‬‫‪0‬‬ ‫‪643 4‬‬
‫‪3 222‬‬
‫‪222‬‬ ‫‪675‬‬
‫‪3 675‬‬ ‫‪123 3‬‬ ‫‪top‬‬
‫‪2 111‬‬
‫‪111‬‬ ‫‪554‬‬
‫‪2 554‬‬ ‫‪101‬‬
‫‪0 2‬‬ ‫‪top‬‬
‫‪3‬‬
‫‪1 131‬‬ ‫‪1‬‬
‫‪1 224‬‬ ‫‪1 1‬‬
‫‪177‬‬ ‫‪top‬‬

‫‪0 212‬‬
‫‪5‬‬
‫‪212‬‬ ‫‪131‬‬
‫‪2‬‬
‫‪0 131‬‬ ‫‪4 0‬‬
‫‪ ‬‬

‫למה לא חייבים לאתחל את המערך‬ ‫בדוגמא זו‪V[0]=5, V[1]=3, V[4]=7 :‬‬


‫של המצביעים לאזור הבטוח?‬
‫‪© cs, Technion‬‬ ‫‪13‬‬
‫הקוד לאיתור זבל‬
‫{ )‪int is_initialized(int i‬‬
‫;) ‪return ( B[i] < top && B[i] >= 0 && C[B[i]] = = i‬‬
‫}‬
‫דוגמא לאיבר שהוכנס למבנה‪C[B[4]] = = 4 :‬‬
‫דוגמאות לזבל‪ B[5] > top :‬וכן ‪C[B[3]]= = 0‬‬

‫קיצורי דרך‬ ‫‪  ‬האינדקסים‬


‫הרלוונטיים‬

‫‪5 333‬‬ ‫‪5 234‬‬ ‫‪453 5‬‬


‫‪4‬‬ ‫‪7‬‬ ‫‪4 0‬‬ ‫‪643 4‬‬ ‫זבל‬

‫‪3 222‬‬ ‫‪3 675‬‬


‫‪2‬‬
‫‪675‬‬ ‫‪123 3‬‬ ‫‪top‬‬
‫‪2 111‬‬ ‫‪2 554‬‬
‫‪554‬‬ ‫‪0 2‬‬
‫‪1‬‬ ‫‪3‬‬ ‫‪1‬‬
‫‪1 224‬‬ ‫‪1‬‬ ‫‪1‬‬
‫‪0‬‬ ‫‪5‬‬ ‫‪0 131‬‬
‫‪2‬‬
‫‪131‬‬ ‫‪4‬‬ ‫‪0‬‬
‫‪ ‬‬

‫‪© cs, Technion‬‬ ‫‪14‬‬


‫הקוד לפעולות המערך‬
top = 0; ‫אתחול‬
constant = const; init(V,const):

if (is_initialized(i)) ‫אחזר‬
return A[i]; get(V,i):
else
return constant;

if (!is_initialized(i)) { ‫שמור‬
C[top] = i ; store(V,I,e):
B[i] = top ;
top = top +1;}
A[i]= e;

A ‫ניתן להשתמש במערך‬  (top > n) ‫ ברגע שהמערך מתמלא‬ :‫הערה‬


.A-‫ זבל ב‬,‫ ויותר לא יהיה‬,‫ כיוון שאין‬,‫בלבד‬
© cs, Technion 15
‫תכונות‬
‫דורש ממשק מוקפד בין המשתמש לבין המערך‪.‬‬ ‫•‬
‫• זו תכונה שמומלץ להקפיד עליה תמיד‬
‫יתרונות‬ ‫•‬
‫• אסימפטוטית‪ :‬פיתרון מצויין‪.‬‬
‫• מעשית‪ :‬נמנעת הפסקה ארוכה בריצה לצורך איתחול )בייחוד למערך‬
‫גדול(‬
‫חסרונות‬ ‫•‬
‫• פי שלוש מקום‬
‫• יותר זמן‬
‫• זמן איתחול ״נבלע״ בזמן פעילות )‪(amortized‬‬
‫• קוד פחות פשוט‬
‫בעולם האמיתי‬ ‫•‬
‫• לא ראיתי כזה‬
‫• יש שימוש בזכרון לא רצוף )שיכול גם לקצר הפסקות(‬
‫• בחלק מהשפות )‪ (Java‬מנהל הזיכרון מאפס‪ ,‬בשפות אחרות המשתמש‬
‫מאתחל )‪(C‬‬
‫‪16‬‬
‫ייצוג מטריצות סימטריות‬
‫‪ ‬‬
‫‪0‬‬ ‫‪1‬‬ ‫‪2‬‬ ‫…‬ ‫!‬
‫‪0‬‬ ‫‪1‬‬
‫‪ 1‬‬ ‫‪2‬‬ ‫‪3‬‬
‫‪2‬‬ ‫‪4‬‬ ‫‪5‬‬ ‫‪6‬‬
‫⋮‬ ‫‪7‬‬ ‫‪8‬‬ ‫‪9 10‬‬
‫…‬ ‫…‬ ‫…‬
‫!‬

‫הייצוג‪ :‬אוסף וקטורים המאוחסנים במערך רציף יחיד‪:‬‬


‫‪1 2 3 4 5 6 7 8 9 10 ….‬‬

‫‪ ‬‬

‫תחתון‪:‬‬
‫‪ ‬‬ ‫חצי מערך‬

‫חצי מערך עליון‪:‬‬


‫‪© cs, Technion‬‬ ‫‪17‬‬
‫ייצוג מטריצות דלילות‬
‫‪ ‬‬

‫‪0‬‬ ‫‪1‬‬ ‫‪2‬‬ ‫‪3‬‬ ‫‪4‬‬ ‫‪5‬‬ ‫‪6‬‬ ‫‪7‬‬


‫‪0‬‬ ‫‪3‬‬ ‫‪1‬‬
‫בדוגמא זו ‪ 7‬מתוך ‪40‬‬ ‫‪1‬‬ ‫‪5‬‬
‫איברים שונים מהקבוע‪.‬‬
‫‪2‬‬ ‫‪4‬‬
‫‪3‬‬
‫‪4‬‬ ‫‪2‬‬ ‫‪7‬‬ ‫‪8‬‬

‫‪ ‬‬

‫‪© cs, Technion‬‬ ‫‪18‬‬


‫מטריצות דלילות )המשך(‬
‫‪ ‬‬

‫‪ ‬‬

‫‪ ‬‬

‫‪ ‬‬

‫‪© cs, Technion‬‬ ‫‪19‬‬


‫רשימות מקושרת‬

‫‪20‬‬
‫רשימות מקושרות‬

‫נזכיר כעת כיצד מתבצע חיפוש‪ ,‬הכנסה‪ ,‬והוצאה‪find, insert, delete :‬‬
‫ברשימות מקושרות ונגדיר וריאציות עליהן‪.‬‬

‫‪head‬‬ ‫‪7‬‬ ‫‪2‬‬ ‫‪6‬‬ ‫‪5‬‬

‫חסרונות בהשוואה למערך‪:‬‬ ‫יתרונות בהשוואה למערך‪:‬‬

‫‪ .1‬אין גישה לפי אינדקס בזמן )‪.O(1‬‬ ‫‪ .1‬מאפשר הקצאת זיכרון‬


‫‪ .2‬דרושה הקצאה )קטנה( לכל‬ ‫דינמית‪ :‬אין צורך להקצות‬
‫הוספה של איבר‪.‬‬ ‫מקום זיכרון גדול מראש ואין‬
‫מגבלה על מספר האיברים‪.‬‬
‫‪ .2‬סריקה של האברים בזמן‬
‫לינארי; אין ״חורים״ בין‬
‫איברים כמו שיש במערך‬
‫כשמוציאים איברים מאמצע‬
‫הרשימה‪.‬‬
‫‪© cs, Technion‬‬ ‫‪21‬‬
‫חיפוש ברשימה מקושרת‬
void init (NODE *head){ ‫פעולת איתחול‬
* head = NULL; :init(head)
}

NODE * find (DATA_TYPE x, NODE *head){ ‫פעולת חיפוש‬


NODE *t; find(x,head):
t = head;
while (t!=NULL && t → info != x)
t = t → next ;
return t;  
}

head 7 2 5

t
© cs, Technion 22
‫הכנסת איבר לרשימה מקושרת‬
int insert ( NODE *t, DATA_TYPE x){ ‫פעולת הכנסה‬
NODE *p; insert(t,x):
if (t == NULL) return 0;
p = (NODE *) malloc (sizeof (NODE)); ‫ מצביע לצומת‬t ‫הפרמטר‬
p → info = x; ‫שאחריו מוסיפים צומת‬
p → next = t → next ; .‫חדש‬
t → next = p ;
return 1;
}
 

head 7 2 5

t 6
© cs, Technion 23
‫הוצאת איבר מרשימה מקושרת‬
‫{)‪delete ( NODE *t‬‬ ‫פעולת הסרה‬
‫; ‪NODE * temp‬‬ ‫‪delete(t):‬‬
‫‪ */‬מצביע לצומת שמורידים*‪temp = t → next; /‬‬
‫הפרמטר ‪ t‬מצביע‬
‫; ‪t → next = temp → next‬‬
‫לצומת שלפני‬
‫; )‪free (temp‬‬
‫הצומת‬
‫}‬
‫שמוציאים‪.‬‬
‫‪ ‬‬

‫‪head‬‬ ‫‪7‬‬ ‫‪2‬‬ ‫‪5‬‬

‫‪t‬‬ ‫‪6‬‬

‫‪temp‬‬
‫‪© cs, Technion‬‬ ‫‪24‬‬
‫הוצאת איבר מראש רשימה מקושרת‬
int delete_first ( NODE **p_head){ ‫פעולת מחיקת צומת ראשון‬
NODE * temp ; delete_first(r):
if (*p_head == NULL) return 0 ;
temp = *p_head;
*p_head = temp → next ;
free (temp) ;
return 1; /* success code
*/
}

head 7 2 5

p_head
?‫ איך נמנעים מכך‬.‫קוד מיוחד עבור האיבר הראשון זה מסורבל‬

© cs, Technion 25
‫רשימות עם כותרת‬
‫מוסיפים איבר "ריק" בתחילת הרשימה )נקרא לרוב ”‪.(“dummy node‬‬
‫תוספת זו חוסכת‪:‬‬
‫• קוד מיוחד לרשימות ריקות‪.‬‬
‫• קוד מיוחד להסרת הצומת הראשון‪.‬‬
‫• קוד מיוחד להכנסה לפני הצומת הראשון‪.‬‬
‫בייצוג זה רשימה בת שתי איברים מיוצגת כך‪:‬‬
‫‪head‬‬ ‫‪2‬‬ ‫‪5‬‬

‫‪head‬‬ ‫ורשימה ריקה מיוצגת כך‪:‬‬

‫הכנסה לפני איבר ראשון זהה‬


‫להכנסה לפני איבר כלשהו‪:‬‬

‫‪head‬‬ ‫‪2‬‬ ‫‪5‬‬

‫‪1‬‬
‫‪© cs, Technion‬‬ ‫‪26‬‬
‫רשימות מעגליות‬
‫‪2‬‬ ‫‪5‬‬

‫‪rear‬‬ ‫‪9‬‬ ‫‪6‬‬

‫‪7‬‬
‫יתרונות‪:‬‬
‫‪ .1‬אפשר להגיע מכל איבר לכל איבר‪.‬‬
‫‪element1‬‬ ‫‪ .2‬ניתן לשרשר רשימות מעגליות בזמן קבוע‪,‬‬
‫בהינתן מצביעים לאיבר אחד בכל רשימה‪.‬‬

‫‪3‬‬ ‫‪4‬‬ ‫‪2‬‬ ‫‪5‬‬

‫‪temp‬‬

‫‪6‬‬ ‫‪8‬‬ ‫‪9‬‬ ‫‪7‬‬

‫‪© cs, Technion‬‬ ‫‪element2‬‬ ‫‪27‬‬


‫רשימה מקושרת דו‪-‬כיוונית‬

‫‪2‬‬ ‫‪5‬‬ ‫‪8‬‬ ‫‪9‬‬

‫‪t‬‬

‫יתרונות‪:‬‬
‫• מאפשר להגיע מכל איבר לכל איבר‬
‫• מאפשר להוציא איבר בהינתן מצביע ‪ t‬אליו )ולא רק את האיבר‬
‫שאחריו(‪.‬‬
‫; ‪t → next → prev = t → prev‬‬
‫; ‪t → prev → next = t → next‬‬

‫‪2‬‬ ‫‪5‬‬ ‫‪8‬‬ ‫‪9‬‬

‫‪t‬‬
‫‪© cs, Technion‬‬ ‫‪28‬‬
‫הוצאה “טריקית" ברשימה רגילה‬
‫האם ניתן להוציא איבר אליו מצביע ‪ t‬גם מרשימה חד‪-‬כיוונית ?‬

‫‪head‬‬ ‫‪2‬‬ ‫‪5‬‬ ‫‪8‬‬ ‫‪9‬‬

‫‪t‬‬
‫פשוט נעתיק את האינפורמציה בתא העוקב ל‪ t-‬לתא ש‪ t-‬מצביע אליו‬
‫ונוציא את התא העוקב‪.‬‬

‫‪head‬‬ ‫‪2‬‬ ‫‪8‬‬ ‫‪8‬‬ ‫‪9‬‬

‫‪t‬‬
‫חסרונות‪:‬‬
‫• לא עובד עבור האיבר האחרון‬
‫• יתכן ויש מצביעים נוספים לתא שהוצא )‪ (8‬ואין אפשרות לעדכן מצביעים אלה‬
‫• יתכן שיש מצביעים נוספים לתא ששינינו )‪ (5‬והם מצפים עדיין לראות שם את ‪5‬‬
‫• צומת יכול להכיל הרבה אינפורמציה ולכן העתקה עלולה להיות ארוכה‬
‫‪© cs, Technion‬‬ ‫‪29‬‬
‫דוגמאות לשימושים ברשימות‬

‫‪30‬‬
‫ייצוג פולינומים דלילים‬
‫‪0.3x1000 + 4.5x2 + 0.1‬‬ ‫נעלם אחד‪ .‬דוגמא‪:‬‬

‫‪0.3 1000‬‬ ‫‪4.5‬‬ ‫‪2‬‬ ‫‪0.1‬‬ ‫‪0‬‬

‫מקדם‬ ‫חזקה‬

‫מימוש זה יעיל כאשר ההפרש בין החזקה הגבוהה והנמוכה גדול וכן הרבה‬
‫מקדמים של חזקות הביניים מתאפסים‪.‬‬

‫‪ ‬‬

‫‪9‬‬ ‫‪2‬‬ ‫‪0‬‬

‫‪2‬‬ ‫‪1‬‬ ‫‪5‬‬ ‫‪0‬‬ ‫‪14‬‬ ‫‪1‬‬ ‫‪7‬‬ ‫‪0‬‬

‫‪© cs, Technion‬‬


‫‪3‬‬ ‫‪2‬‬ ‫‪1‬‬ ‫‪0‬‬ ‫‪31‬‬
‫ייצוג מטריצות דלילות‬
:‫מבנה צומת‬

4
column

0
row value next
1
2
down
3
4

typedef struct node {


float value ;
struct node*next, *down ;
int row, column ;
} NODE ;

© cs, Technion 32
‫כפל מטריצות דלילות‬
#define N_row 20  
#define N_col 20
NODE *row_header[N_row],
*col_header[N_col];
/* Compute the value of c[i][j] */
float mult_row_col(int i, int j){
float c = 0 ;
NODE *r = row_header[i],
*k = col_header[j];

while (r != NULL && k != NULL){


if (r → column < k → row) r = r → next ;
else if (r → column > k → row) k = k →
down ;
else /* r->column = k->row */
c + = r → value * k → value ;
r = r → next ; k = k → down ; }
return c;
© cs, Technion } 33
‫סיום מערכים ורשימות מקושרות‬

‫‪© cs, Technion‬‬ ‫‪34‬‬

You might also like