Menu

Stack trong C#: Push, Pop, Peek, hoàn tác và kiểm tra dấu ngoặc

Stack<T> là collection vào sau, ra trước: phần tử được thêm gần nhất sẽ ra đầu tiên. Tìm hiểu Push, Pop và Peek, ngoại lệ khi stack rỗng và TryPop, vì sao stack được duyệt theo thứ tự ngược, cùng hai ứng dụng kinh điển: lịch sử hoàn tác và kiểm tra dấu ngoặc cân bằng.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Stack<T> là một chồng: bạn đặt phần tử lên trên bằng Push và lấy chúng ra từ trên cùng bằng Pop, nên phần tử vào sau cùng sẽ ra đầu tiên (LIFO). Chỉ có đỉnh là truy cập được, và mọi thao tác trên đỉnh đều mất thời gian hằng số.

Push, Pop và Peek

Đầu ra:

On top: green
Count: 3
Took green
On top: red
Took red
Took blue
Count: 0

green được push sau cùng nên được lấy ra đầu tiên. Peek trả về đỉnh mà không thay đổi stack, đó là cách bạn xem Pop sẽ trả về gì trước khi quyết định lấy nó.

Ngoại lệ khi stack rỗng và TryPop

Pop hoặc peek một stack rỗng sẽ ném InvalidOperationException. Lỗi này hay xuất hiện nhất trong các parser và thuật toán nhận đầu vào có nhiều phần tử đóng hơn phần tử mở.

Đầu ra:

Caught InvalidOperationException
True 10
False 0

TryPop và TryPeek (từ .NET Core 2.0) trả về false khi stack rỗng và đặt biến out về giá trị mặc định, ở đây là 0. Trên .NET Framework, hãy kiểm tra Count > 0 trước.

Thứ tự duyệt: đỉnh trước

Duyệt một stack không lấy phần tử nào ra, và nó đi từ đỉnh xuống, theo đúng thứ tự mà Pop sẽ trả về các phần tử:

Đầu ra:

checkout products home 
checkout > products > home
True
home
checkout

Bản sao bị đảo ngược hay khiến người ta bất ngờ: hàm khởi tạo nhận bất kỳ IEnumerable<T> nào và push các phần tử theo thứ tự, mà stack lại được duyệt từ đỉnh, nên đỉnh cũ rơi xuống đáy của bản sao. Đảo ngược dãy trước (Reverse() của LINQ trả về các phần tử từ đáy lên) sẽ cho bản sao có cùng đỉnh.

Push một danh sách phần tử vào stack mới cũng đảo ngược chúng, đây là cách nhanh để đảo một dãy: new Stack<char>("hello") pop ra lần lượt o, l, l, e, h.

Ví dụ: lịch sử hoàn tác

Trình soạn thảo lưu mọi thay đổi trên một stack. Hoàn tác (undo) pop thay đổi gần nhất và khôi phục lại; làm lại (redo) giữ một stack thứ hai chứa các thay đổi đã hoàn tác.

Đầu ra:

Hello, world!
Hello, world
Hello
Hello, world

Lưu toàn bộ bản chụp là phiên bản đơn giản nhất. Các trình soạn thảo thật push những đối tượng lệnh nhỏ (đã chèn gì, ở đâu), mỗi đối tượng có một phương thức để tự đảo ngược, nhưng hai stack vẫn hoạt động theo cùng một cách.

Ví dụ: dấu ngoặc cân bằng

Kiểm tra (, [ và { có được đóng đúng thứ tự hay không là bài tập stack tiêu chuẩn, và logic tương tự nằm bên trong mọi trình biên dịch và parser JSON.

Đầu ra:

"f(a[i], {x: 1})" -> True
"(]" -> False
"((a)" -> False
"a)b(" -> False
"" -> True

Ba phép kiểm tra lỗi tương ứng với ba cách dấu ngoặc có thể sai: một dấu đóng khi không có gì đang mở (a)b(, được bắt bằng Count == 0 thay vì ngoại lệ từ Pop), một dấu đóng sai loại ((]), và dấu mở không bao giờ được đóng (((a), được bắt bằng phép kiểm tra cuối cùng).

Các ứng dụng khác

  • Tìm kiếm theo chiều sâu. Thay queue trong tìm kiếm theo chiều rộng bằng một stack và việc duyệt sẽ đi sâu trước khi đi rộng. Một stack tường minh cũng thay được đệ quy khi đầu vào đủ sâu để có nguy cơ gây StackOverflowException, ngoại lệ không thể bắt được.
  • Tính giá trị biểu thức. Ký pháp hậu tố (3 4 + 2 *) được tính bằng cách push các số và pop hai số cho mỗi toán tử.
  • Quay lui (backtracking). Lịch sử điều hướng, giải mê cung và trạng thái parser đều push một vị trí rồi pop để quay lại khi gặp ngõ cụt.

Xem Queue để biết cấu trúc vào trước, ra trước tương ứng.

Stack, Queue và List

Stack<T>Queue<T>List<T>
Thứ tự lấy raMới nhất trướcCũ nhất trướcBất kỳ, theo chỉ số
ThêmPushEnqueueAdd, Insert
XóaPop (đỉnh)Dequeue (đầu)Remove, RemoveAt
XemPeekPeeklist[i]
Phiên bản an toànTryPop, TryPeekTryDequeue, TryPeekkhông cần

Với nhiều luồng, ConcurrentStack<T> trong System.Collections.Concurrent cung cấp Push, TryPop và TryPeek mà không cần khóa.

Lỗi thường gặp

  • Pop mà không kiểm tra. Stack rỗng sẽ ném InvalidOperationException; hãy kiểm tra Count hoặc dùng TryPop.
  • Tưởng foreach bắt đầu từ phần tử được push đầu tiên. Nó bắt đầu từ đỉnh.
  • Sao chép bằng new Stack<T>(stack). Bản sao bị đảo ngược.
  • Push bên trong foreach trên chính stack đó. Sẽ ném ngoại lệ; hãy dùng vòng lặp while (stack.Count > 0).

Câu hỏi thường gặp

Stack trong C# là gì?

Stack<T> trong System.Collections.Generic là collection vào sau, ra trước (LIFO). Push đặt một phần tử lên đỉnh, Pop lấy ra và trả về phần tử trên đỉnh, còn Peek trả về phần tử trên đỉnh mà không lấy nó ra. Cả ba đều chạy trong thời gian hằng số.

Điều gì xảy ra khi Pop một stack rỗng trong C#?

Pop và Peek ném InvalidOperationException khi stack rỗng. Hãy kiểm tra stack.Count > 0 trước, hoặc dùng TryPop(out var item) và TryPeek(out var item), hai phương thức này trả về false thay vì ném ngoại lệ (từ .NET Core 2.0).

foreach duyệt Stack theo thứ tự nào?

Từ đỉnh xuống: phần tử được push gần nhất ra đầu tiên, đúng thứ tự mà Pop sẽ trả về. ToArray() cũng dùng thứ tự đó. Hệ quả là new Stack<T>(otherStack) tạo ra một bản sao bị đảo ngược, vì hàm khởi tạo push các phần tử theo đúng thứ tự nó duyệt chúng.

Stack và Queue trong C# khác nhau thế nào?

Stack<T> trả về phần tử mới nhất trước (vào sau, ra trước), còn Queue<T> trả về phần tử cũ nhất trước (vào trước, ra trước). Dùng stack cho lịch sử hoàn tác, cấu trúc lồng nhau và tìm kiếm theo chiều sâu; dùng queue để xử lý công việc theo thứ tự đến và tìm kiếm theo chiều rộng.

Làm sao kiểm tra dấu ngoặc cân bằng trong C#?

Duyệt chuỗi một lần. Push mọi dấu ngoặc mở vào một Stack<char>. Với mỗi dấu ngoặc đóng, stack phải không rỗng và phần tử trên đỉnh phải là dấu ngoặc mở tương ứng, khi đó bạn pop nó ra. Chuỗi cân bằng khi duyệt xong mà stack rỗng.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu