Document 7551744
Download
Report
Transcript Document 7551744
Data Structures and
Algorithms
Abstract Data Types I:
Stacks and Queues
Abstract Data Type (ADT)
ופעולות בסיסיות לטיפול וניהול המידע,מנגנון אחסון למידע
Vector, Matrix
Queue (Buffer)
Random r/w access to any element by coordinates
First in, First out
Set (unordered), Ordered Lists (Dictionary)
Graphs (of nodes and vertices)
…..
ADTs are not implementations
We can use different implementations for ADTs
For instance: Stack ()מחסנית
Last in, first out
Basic mechanism for function calls, delimiter
checks in compilers, etc.
Operations: new, push, pop, peek, empty?
We will examine several implementations
Basic operations of a Stack
Push
New
?
Pop
Peek
Empty
?
C++ מימוש ב
const int MAX_SIZE = 100;
typedef int Type;
class Stack {
private:
Type data[MAX_SIZE];
int top;
public:
stack();
~stack();
void make_empty();
bool is_empty();
bool is_full();
void push(Type item);
Type pop();
Type top();
};
// next item is placed
// at data[top]
Stack Example:
delimiter check
Legal delimiters: {,[,(,),],}
Each opening delimiter must have a matching closing one.
Proper nesting:
a{bc[d]e}f(g)
OK
a{bc[d}e]f(g)
incorrect
We can perform this task easily using a stack!
בתודה למתרגל אלמוני באוניברסיטה העברית על השקופית
Stack example:
Delimiter check
1. for all characters c of a
2.
if (c) is either ‘(’,
push(c)
3.
if (c) is either ‘)’,
if (isEmpty())
error(…)
if (pop() does not
error(…)
4. if (not isEmpty())
error(…)
string
‘[’, or ‘{’:
‘]’, ‘}’:
match c)
בתודה למתרגל אלמוני באוניברסיטה העברית על השקופית
Stack implemented using array
Assume stack will not have more than K items
Create array S[ ] of size K: S[K]
Keep index of current head, h=0
New:
h=0
Push(x): S[h] = x ; h++
Pop(): h-; return S[h]
Peek(): return S[h-1]
Empty(): return (h==0)
Stack implemented in an array
Note that all operations need check array bounds
Pushing onto a full stack: Overflow
When h=K
Popping an empty stack: Underflow
When h<0
דוגמה
המשימה אותה עלינו להשלים היא לקרוא ביטוי אריתמטי (כדוגמת:
))(4+2)*(12/2
ולהציג את ערכו (בדוגמה שלנו)36 :
לשם ביצוע המשימה נידרש למבני הנתונים הבאים:
מחסנית של אופרטורים – המשתמשת אותנו בשלב חישוב הביטוי.
מחסנית של מספרים – המשתמשת אותנו בשלב תרגום הביטוי מצורת infix
לצורת ( postfixהסבר על שתי צורות אלה יבוא מייד).
האלגוריתם בכללותו יכלול שני שלבים:
תרגם את הביטוי הנקרא מ infix -ל.postfix-
הערך את הביטוי בצורת ה.postfix-
המשך
אם כותבים ביטוי (כהרגלנו ,ב )infix-יש להשתמש בסוגריים,
לדוגמה אם נשמיט את הסוגרים מהביטוי (4+2)*3נקבל את הביטוי השונה
לגמרי .4+2*3
,Polish postfix notationאו בקיצור ,postfixובה איננו נזקקים לסוגריים
וזאת משום שבמקום שהאופרטור יוצב בין שני האופרנדים ,הוא ניצב
אחריהם .ולכן אין ספק לגבי הסדר בו יש לבצע את הפעולות.
*63
>= 6 * 3
=> , 4 2 +
4+2
ולכן:
* 4 2 + 3ובניגוד לכך:
>= (4 + 2) * 3
423*+
=>
4+2* 3
וכן האלה:
*53+42-
=>
)(5 + 3) * (4 – 2
23*1+5*12–1-/
=>
)((2*3 + 1)*5)/(1-2-1
האלגוריתם:
חזור עד תום הקלט:
קרא אסימון ( )Tokenנוסף מהקלט.
אם אסימון זה הינו מספר אזי דחף אותו על-גבי
המחסנית.
אחרת (קראת אופרטור)
שלוף את שני המספרים (אופרנדים) שבראש המחסנית.
חשב את תוצאת הפעולה שנקראה עת היא מופעלת על
האופרנדים הנ"ל.
דחף את התוצאה ע"ג המחסנית.
החזר את הערך (היחיד) המצוי במחסנית .
דוגמה:
נניח כי קלט הוא בטוי ה postfix -הבא60 30 / 15 13 - :
*
60נקרא ,ונדחף ע"ג המחסנית.
30נקרא ,ונדחף ע"ג המחסנית.
/נקרא 60 ,30 :נשלפים מהמחסנית ,ו 60/30 =2 -נדחף
ע"ג המחסנית.
15נקרא ,ונדחף ע"ג המחסנית.
13נקרא ,ונדחף ע"ג המחסנית.
נקרא 15 ,13 :נשלפים מהמחסנית ,ו 15 -13 =2 -נדחףע"ג המחסנית.
* נקרא 2 ,2 :נשלפים מהמחסנית ,ו 2*2 = 4 -נדחף ע"ג
המחסנית.
הקלט תם 4 :המצוי בראש המחסנית נשלף ומוחזר בתור
ערכו של הביטוי.
4
2
13
15
2
30
60
האלגוריתם להפיכת ביטוי ל :postfix
חזור עד תום הקלט:
קרא נתון נוסף מהקלט
אם נתון זה הוא מספר שלח אותו לפלט
אחרת (נקרא אופרטור או סוגר)
כל עוד המחסנית אינה ריקה וכן קדימותו של האיבר שבראש
המחסנית ≥ מקדימותו של האסימון שנקרא:
שלוף את האיבר שבראש המחסנית.
ופלוט אותו לפלט.
אם האסימון שנקרא אינו סוגר ימני אזי דחוף אותו על המחסנית
אחרת שלוף את ראש המחסנית (שהינו סוגר).
(אחרי תום הקלט) שלוף מהמחסנית ושלח לפלט את כל מה
שמצוי בה .
][(2*3 + 1)*5
הכנס ]
הכנס )
הדפס - 2הכלל אותו נגזור :כל אופרנד\מספר שנקרא נשלח
מיידית לפלט
הכנס *
הדפס 3
קרא , +סדר קדימות של +קטן מ * לכן הוצא * הדפס והכנס
.+מצב פלט2 3 * :
הדפס 1מצב פלט2 3 * 1 :
קרא ( ,הוצא והדפס כל המחסנית עד ) ,פלט2 3 * 1 + :
הכנס *
הדפס 5
קרא [ ,הוצא הדפס כל המחסנית עד ] ,פלט2 3 * 1 + 5 * :
Implementation complexity
What is the run-time complexity of each
operation?
New:
h=0
Push(x): S[h] = x ; h++
Pop(): h-; return S[h]
Peek(): return S[h-1]
Empty(): return (h==0)
O(1)
O(1)
O(1)
O(1)
O(1)
The Catch: Fixed Memory
Array implementation always allocates same
amount of memory
S(n) = K where K largest possible stack
But if too many items (n>K), we’re in trouble
And if too few items (n<<K), we’re wasting space
Stack using dynamic memory
allocation, and pointers
Key idea: allocate memory as required
Keep pointer head, pointing to first item
Each stack item stored in a node
Node has two fields: item and next
X
Y
Z
Linked Lists
Each list element (node) holds a data item and a
reference (pointer) to the next node:
Class ListNode {
Object item;
ListNode next = null;
}
A list consists of a chain of zero or more nodes:
Class SimplestLinkedList {
ListNode head = null;
}
בתודה למתרגל אלמוני באוניברסיטה העברית על השקופית
Stack using linked-list
New: head = NULL
O(1)
Empty: return (head == NULL)
O(1)
Peek: if (!empty()), return head.item O(1)
Stack using pointers – Push(x)
Create new node T // allocate memory for it
T.item x
T.next head
head T
Let’s look at the blackboard
Stack using pointers – Pop()
If (empty(head)), return NULL
X head.item
T head // must save it to delete it later!
head head.next
delete T
// free its memory
Let’s look at the blackboard
Queue ADT
Queue stores data according to order of arrival
First In, First Out
Basic operations:
Empty?, new, ….
Enqueue(x)
Dequeue()
adds x to the back of the queue
removes returns top of queue
Basic operations of a queue
Enqueue
New
Empty?
Dequeue
Full?
?
Queue Application:
Communications Buffer
Two processes:
Writer: Puts information out
Reader: Gets information in
We want each process to be independent
Asynchronous: Work at their own pace
Reader
Writer
Queue Application:
Communications Buffer
Two processes:
Writer: Puts information out
Reader: Gets information in
We want each process to be independent
Asynchronous: Work at their own pace
Reader
Writer
Queue Application:
Communications Buffer
Two processes:
Writer: Puts information out
Reader: Gets information in
We want each process to be independent
Asynchronous: Work at their own pace
Reader
Writer
Queue Application:
Communications Buffer
Two processes:
Writer: Puts information out
Reader: Gets information in
We want each process to be independent
Asynchronous: Work at their own pace
Reader
Writer
Queue Application:
Communications Buffer
Two processes:
Writer: Puts information out
Reader: Gets information in
We want each process to be independent
Asynchronous: Work at their own pace
Reader
Writer
Queue Application:
Communications Buffer
Two processes:
Writer: Puts information out
Reader: Gets information in
We want each process to be independent
Asynchronous: Work at their own pace
Reader
Problem!
Writer
Buffering using a queue
Writer:
1.
2.
3.
While have information x to write:
if full?(), wait a bit
else enqueue(x)
Reader:
1.
2.
3.
While reading allowed:
if empty?(), wait a bit
else x = dequeue()
Queue Implementation 1:
Circular Array
Again we use an array of fixed size K
But now we keep two indexes: tail and head
K
head
tail
New Queue (Array)
Head = 0
Tail = 0
head
tail
Enqueue(x)
(Array)
Put x into array at tail, then move tail
X
head
tail
Dequeue(x)
(Array)
Return element x at head
Advance head
X
Y
head
Z
W
tail
Dequeue(x)
(Array)
Return element x at head
Advance head
X
Y
head
Z
W
tail
Enqueue(x) -- Cont’d
(Array)
But what happens when tail is at end of array
We can put X in, but then what?
How do we advance tail?
X
Y
head
Z
tail
Enqueue(x) -- Cont’d
(Array)
tail is now moved to the beginning of the array
tail (tail + 1) modulo K
In C, C++: tail = (tail+1) % K
Y
head
Z
tail
X
Dequeue(x) -- Cont’d
(Array)
And what happens when head reaches end?
Same thing: head (head + 1) modulo K
Y
Z
head
X
tail
Dequeue(x) -- Cont’d
(Array)
And what happens when head reaches end?
Same thing: head (head + 1) modulo K
Y
Z
head
X
tail
Empty?
(Array)
When head catches up with tail
head
tail
Empty?
(Array)
When head catches up with tail
head
tail
Empty?
(Array)
When head catches up with tail
head == tail
head
tail
Full?
Ooops! Looks exactly like empty?
(Array)
When tail catches up with head
head == tail
So how do we tell the difference?
head
tail
Full?
So how do we tell the difference?
Solution: Keep a counter num for # of items
(Array)
When we enqueue, num num + 1
When we dequeue, num num – 1
When we check empty or full, we look at num:
empty?
full?
head==tail
head==tail
AND
AND
num == 0
num == K
Circular Array Summary
Run-time complexity is appealing:
enqueue(), dequeue(), … are all O(1)
But storage complexity is a problem:
S(n) = K, where K is size of largest array
Can be wasteful (if K too large)
Can be a problem (if K too small)
Queue Implementation 2:
Doubly-linked list
Keep linked list, with pointers to head and tail
X
Y
head
tail
Keep nodes pointing in both directions
This allows us to move tail back ( )
And move head forward
()
Z
Queue as doubly-linked list
head = NULL,
head == NULL
tail = NULL
New:
Empty:
Enqueue(x) -- same as stack push(x)
1.
2.
3.
4.
5.
Create new node T // allocate memory
T.item x
T.next tail
tail.prev T
tail T
5.5 (Instead of 4: tail.next.prev T)
6.
T.prev NULL
Dequeue
1.
2.
3.
4.
5.
6.
7.
8.
If head == NULL, error(underflow)
X
head.item
Temp
head
head
head.prev
if head != NULL
head.next NULL // New head
else tail = NULL
Delete Temp, return X
Doubly-linked list summary
Enqueue(), Dequeue(), …. O(1)
But slightly less efficient in practice
Memory allocation overhead
Space:
No wasted space!