LIFO กับ FIFO
LIFO ย่อมาจาก Last In, First Out เช่นเดียวกับรายการสุดท้ายที่ใส่เข้าไปในสแต็กคือรายการแรกที่นำออกจากสแต็ก
สิ่งที่คุณอธิบายด้วยความคล้ายคลึงกับอาหารของคุณ (ในการแก้ไขครั้งแรก ) คือคิวหรือ FIFO, เข้าก่อน, ออกก่อน
ความแตกต่างที่สำคัญระหว่างทั้งสองคือ LIFO / สแต็กผลัก (แทรก) และ pops (ลบ) จากปลายเดียวกันและ FIFO / คิวทำจากปลายตรงข้าม
// Both:
Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]
// Stack // Queue
Pop() Pop()
-> [a, b] -> [b, c]
ตัวชี้สแต็ก
ลองมาดูสิ่งที่เกิดขึ้นภายใต้ฝากระโปรงของกอง นี่คือหน่วยความจำบางส่วนแต่ละกล่องมีที่อยู่:
...[ ][ ][ ][ ]... char* sp;
^- Stack Pointer (SP)
และมีตัวชี้สแต็คชี้ไปที่ด้านล่างของสแต็กที่ว่างเปล่าในปัจจุบัน (ไม่ว่าสแต็คจะโตขึ้นหรือโตขึ้นไม่เกี่ยวข้องโดยเฉพาะที่นี่ดังนั้นเราจะไม่สนใจสิ่งนั้น แต่แน่นอนในโลกแห่งความเป็นจริง และสิ่งที่ลบออกจาก SP)
งั้นลองผลักa, b, and c
อีกครั้ง กราฟิกด้านซ้ายการทำงาน "ระดับสูง" ที่อยู่ตรงกลางโค้ดหลอก C-ish ด้านขวา:
...[a][ ][ ][ ]... Push('a') *sp = 'a';
^- SP
...[a][ ][ ][ ]... ++sp;
^- SP
...[a][b][ ][ ]... Push('b') *sp = 'b';
^- SP
...[a][b][ ][ ]... ++sp;
^- SP
...[a][b][c][ ]... Push('c') *sp = 'c';
^- SP
...[a][b][c][ ]... ++sp;
^- SP
อย่างที่คุณเห็นแต่ละครั้งที่เราpush
ใส่อาร์กิวเมนต์ในตำแหน่งที่ตัวชี้สแต็คกำลังชี้และปรับตัวชี้สแต็กให้ชี้ไปที่ตำแหน่งถัดไป
ตอนนี้ขอป๊อป:
...[a][b][c][ ]... Pop() --sp;
^- SP
...[a][b][c][ ]... return *sp; // returns 'c'
^- SP
...[a][b][c][ ]... Pop() --sp;
^- SP
...[a][b][c][ ]... return *sp; // returns 'b'
^- SP
Pop
อยู่ฝั่งตรงข้ามของpush
มันปรับตัวชี้สแต็คไปยังจุดที่สถานที่ก่อนหน้านี้และลบรายการที่อยู่ที่นั่น (โดยปกติจะส่งกลับมาให้ใครก็ตามที่เรียกว่าpop
)
คุณอาจสังเกตเห็นว่าb
และc
ยังคงอยู่ในหน่วยความจำ ฉันแค่ต้องการรับรองว่าสิ่งเหล่านั้นไม่ใช่ตัวพิมพ์ผิด เราจะกลับไปที่ในไม่ช้า
ชีวิตที่ไม่มีตัวชี้สแต็ค
มาดูกันว่าจะเกิดอะไรขึ้นถ้าเราไม่มีตัวชี้สแต็ค เริ่มต้นด้วยการกดอีกครั้ง:
...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]... Push(a) ? = 'a';
เอ่ออืม ... ถ้าเราไม่มีตัวชี้สแต็คเราก็ไม่สามารถย้ายบางสิ่งไปยังที่อยู่ที่ชี้ไป บางทีเราสามารถใช้ตัวชี้ที่ชี้ไปที่ฐานแทนด้านบน
...[ ][ ][ ][ ]... char* bp; // "base pointer"
^- bp bp = malloc(...);
...[a][ ][ ][ ]... Push(a) *bp = 'a';
^- bp
// No stack pointer, so no need to update it.
...[b][ ][ ][ ]... Push(b) *bp = 'b';
^- bp
เอ่อโอ้. เนื่องจากเราไม่สามารถเปลี่ยนค่าคงที่ของฐานสแต็กเราจึงเขียนทับa
โดยการกดb
ไปยังตำแหน่งเดียวกัน
ทำไมเราไม่ติดตามจำนวนครั้งที่เราผลัก และเราจะต้องติดตามเวลาที่เราตอกหมุดด้วย
...[ ][ ][ ][ ]... char* bp; // "base pointer"
^- bp bp = malloc(...);
int count = 0;
...[a][ ][ ][ ]... Push(a) bp[count] = 'a';
^- bp
...[a][ ][ ][ ]... ++count;
^- bp
...[a][b][ ][ ]... Push(a) bp[count] = 'b';
^- bp
...[a][b][ ][ ]... ++count;
^- bp
...[a][b][ ][ ]... Pop() --count;
^- bp
...[a][b][ ][ ]... return bp[count]; //returns b
^- bp
มันใช้งานได้ แต่จริง ๆ แล้วมันค่อนข้างคล้ายกับก่อนยกเว้น*pointer
มีราคาถูกกว่าpointer[offset]
(ไม่มีเลขคณิตพิเศษ) ไม่พูดถึงมันพิมพ์น้อย ดูเหมือนว่าฉันจะสูญเสีย
ลองอีกครั้ง แทนที่จะใช้รูปแบบสตริง Pascal ในการค้นหาจุดสิ้นสุดของคอลเลกชันที่ใช้อาร์เรย์ (ติดตามจำนวนรายการที่อยู่ในคอลเลกชัน) ให้ลองใช้รูปแบบสตริง C (สแกนตั้งแต่ต้นจนจบ):
...[ ][ ][ ][ ]... char* bp; // "base pointer"
^- bp bp = malloc(...);
...[ ][ ][ ][ ]... Push(a) char* top = bp;
^- bp, top
while(*top != 0) { ++top; }
...[ ][ ][ ][a]... *top = 'a';
^- bp ^- top
...[ ][ ][ ][ ]... Pop() char* top = bp;
^- bp, top
while(*top != 0) { ++top; }
...[ ][ ][ ][a]... --top;
^- bp ^- top return *top; // returns '('
คุณอาจเดาปัญหาได้แล้วที่นี่ หน่วยความจำที่ไม่ได้กำหนดค่าเริ่มต้นนั้นไม่ได้รับประกันว่าจะเป็น 0 ดังนั้นเมื่อเราค้นหาตำแหน่งบนสุดa
เราจะข้ามสถานที่ตั้งของหน่วยความจำที่ไม่ได้ใช้ซึ่งมีขยะสุ่มอยู่ในนั้น ในทำนองเดียวกันเมื่อเราสแกนไปที่ด้านบนสุดเราจะกระโดดได้ดีกว่าที่a
เราผลักไปจนในที่สุดเราก็พบตำแหน่งหน่วยความจำอื่นที่เพิ่งเกิดขึ้น0
และย้ายกลับไปและกลับขยะแบบสุ่มก่อนหน้านั้น
มันง่ายพอที่จะแก้ไขเราเพียงแค่ต้องเพิ่มการดำเนินการPush
และPop
เพื่อให้แน่ใจว่าด้านบนสุดของสแต็กนั้นได้รับการอัพเดตเสมอเพื่อทำเครื่องหมายด้วย a 0
และเราต้องเริ่มต้นสแต็กด้วยตัวยุติดังกล่าว แน่นอนซึ่งหมายความว่าเราไม่สามารถมี0
(หรือค่าใดก็ตามที่เราเลือกเป็นเทอร์มิเนเตอร์) เป็นค่าจริงในสแต็ก
ยิ่งไปกว่านั้นเรายังได้เปลี่ยนการดำเนินงานของ O (1) ไปเป็นการดำเนินการ O (n)
TL; DR
ตัวชี้สแต็ติดตามด้านบนของสแต็คที่ทั้งหมดของการกระทำที่เกิดขึ้น มีวิธีการกำจัดมัน ( bp[count]
และtop
ยังคงเป็นตัวชี้สแต็ก) แต่ทั้งคู่จบลงด้วยความซับซ้อนและช้ากว่าการมีตัวชี้สแต็ก และไม่รู้ว่าส่วนบนสุดของสแต็คหมายความว่าคุณไม่สามารถใช้สแต็กได้
หมายเหตุ: ตัวชี้สแต็กชี้ไปที่ "ด้านล่าง" ของ runtime stack ใน x86 อาจเป็นความเข้าใจผิดที่เกี่ยวข้องกับ runtime stack ทั้งหมดที่คว่ำ กล่าวอีกนัยหนึ่งฐานของสแต็คจะถูกวางไว้ที่ที่อยู่หน่วยความจำสูงและส่วนปลายของสแต็กจะขยายลงไปยังที่อยู่หน่วยความจำที่ต่ำกว่า ตัวชี้สแต็กจะชี้ไปที่ปลายของสแต็กที่การกระทำทั้งหมดเกิดขึ้นเพียงปลายนั้นอยู่ที่หน่วยความจำต่ำกว่าฐานของสแต็ก