LIFO vs FIFO
LIFO là viết tắt của Last In, First Out. Như trong, mục cuối cùng được đưa vào ngăn xếp là mục đầu tiên được lấy ra khỏi ngăn xếp.
Những gì bạn mô tả với sự tương tự các món ăn của bạn (trong phiên bản đầu tiên ), là một hàng đợi hoặc FIFO, First In, First Out.
Sự khác biệt chính giữa hai loại, đó là LIFO / stack đẩy (chèn) và bật (loại bỏ) từ cùng một đầu, và một hàng đợi / hàng đợi làm như vậy từ các đầu đối diện.
// Both:
Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]
// Stack // Queue
Pop() Pop()
-> [a, b] -> [b, c]
Con trỏ ngăn xếp
Chúng ta hãy xem những gì đang xảy ra dưới mui xe của ngăn xếp. Đây là một số bộ nhớ, mỗi hộp là một địa chỉ:
...[ ][ ][ ][ ]... char* sp;
^- Stack Pointer (SP)
Và có một con trỏ ngăn xếp chỉ xuống dưới cùng của ngăn xếp hiện tại trống rỗng (việc ngăn xếp tăng lên hay phát triển xuống không đặc biệt liên quan ở đây vì vậy chúng tôi sẽ bỏ qua điều đó, nhưng tất nhiên trong thế giới thực, điều đó xác định thao tác nào thêm vào và trừ đi SP).
Vì vậy, hãy đẩy a, b, and c
một lần nữa. Đồ họa ở bên trái, hoạt động "mức cao" ở giữa, mã giả C-ish ở bên phải:
...[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
Như bạn có thể thấy, mỗi lần chúng ta push
, nó sẽ chèn đối số vào vị trí con trỏ ngăn xếp hiện đang trỏ và điều chỉnh con trỏ ngăn xếp để trỏ đến vị trí tiếp theo.
Bây giờ hãy bật:
...[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
ngược lại push
, nó điều chỉnh con trỏ ngăn xếp để trỏ đến vị trí trước đó và xóa mục đã có (thường để trả lại cho bất kỳ ai được gọi pop
).
Bạn có thể nhận thấy rằng b
và c
vẫn còn trong bộ nhớ. Tôi chỉ muốn đảm bảo với bạn rằng đó không phải là lỗi chính tả. Chúng tôi sẽ trở lại ngay.
Cuộc sống không có con trỏ ngăn xếp
Hãy xem điều gì xảy ra nếu chúng ta không có con trỏ ngăn xếp. Bắt đầu với việc đẩy lại:
...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]... Push(a) ? = 'a';
Er, hmm ... nếu chúng ta không có con trỏ ngăn xếp, thì chúng ta không thể di chuyển thứ gì đó đến địa chỉ mà nó trỏ đến. Có lẽ chúng ta có thể sử dụng một con trỏ trỏ đến cơ sở thay vì trên cùng.
...[ ][ ][ ][ ]... 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
À ồ. Vì chúng tôi không thể thay đổi giá trị cố định của cơ sở của ngăn xếp, chúng tôi chỉ ghi đè lên a
bằng cách đẩy b
đến cùng một vị trí.
Chà, tại sao chúng ta không theo dõi số lần chúng ta đã đẩy. Và chúng ta cũng cần theo dõi thời gian chúng ta xuất hiện.
...[ ][ ][ ][ ]... 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
Chà nó hoạt động, nhưng nó thực sự khá giống với trước đây, ngoại trừ *pointer
rẻ hơn pointer[offset]
(không có số học thêm), chưa kể nó ít để gõ. Điều này có vẻ như là một mất mát đối với tôi.
Hãy thử lại lần nữa. Thay vì sử dụng kiểu chuỗi Pascal để tìm phần cuối của bộ sưu tập dựa trên mảng (theo dõi có bao nhiêu mục trong bộ sưu tập), hãy thử kiểu chuỗi C (quét từ đầu đến cuối):
...[ ][ ][ ][ ]... 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 '('
Bạn có thể đã đoán được vấn đề ở đây. Bộ nhớ chưa được khởi tạo không được đảm bảo là 0. Vì vậy, khi chúng ta tìm kiếm vị trí hàng đầu a
, chúng ta sẽ bỏ qua một loạt các vị trí bộ nhớ không sử dụng có rác ngẫu nhiên trong đó. Tương tự như vậy, khi chúng ta quét lên trên cùng, cuối cùng chúng ta sẽ bỏ qua vượt quá mức a
chúng ta vừa đẩy cho đến khi cuối cùng chúng ta tìm thấy một vị trí bộ nhớ khác xảy ra 0
, và di chuyển trở lại và trả lại rác ngẫu nhiên ngay trước đó.
Điều đó đủ dễ để khắc phục, chúng ta chỉ cần thêm các thao tác vào Push
và Pop
để đảm bảo đỉnh của ngăn xếp luôn được cập nhật để được đánh dấu bằng a 0
và chúng ta phải khởi tạo ngăn xếp với một đầu cuối như vậy. Tất nhiên, điều đó cũng có nghĩa là chúng ta không thể có 0
(hoặc bất kỳ giá trị nào chúng ta chọn làm dấu kết thúc) làm giá trị thực trong ngăn xếp.
Trên hết, chúng tôi cũng đã thay đổi các hoạt động O (1) thành các hoạt động O (n).
TL; DR
Con trỏ ngăn xếp theo dõi đỉnh của ngăn xếp, nơi tất cả các hành động xảy ra. Có nhiều cách để loại bỏ nó ( bp[count]
và top
về cơ bản vẫn là con trỏ ngăn xếp), nhưng cả hai đều phức tạp và chậm hơn là chỉ có con trỏ ngăn xếp. Và không biết vị trí trên cùng của ngăn xếp có nghĩa là bạn không thể sử dụng ngăn xếp.
Lưu ý: Con trỏ ngăn xếp chỉ vào "đáy" của ngăn xếp thời gian chạy trong x86 có thể là một quan niệm sai lầm liên quan đến toàn bộ ngăn xếp thời gian chạy bị lộn ngược. Nói cách khác, cơ sở của ngăn xếp được đặt tại một địa chỉ bộ nhớ cao và đầu của ngăn xếp phát triển thành các địa chỉ bộ nhớ thấp hơn. Con trỏ ngăn xếp không trỏ đến đỉnh của ngăn xếp nơi tất cả các hành động xảy ra, chỉ là mũi là tại một địa chỉ bộ nhớ thấp hơn so với các cơ sở của ngăn xếp.