Class Stack<E>

Stack là một cấu trúc dữ liệu tuyến tính được sử dụng để lưu trữ một tập hợp các đối tượng theo nguyên tắc Last-In-First-Out (LIFO) nghĩa là "vào sau ra trước". Điều này có nghĩa là phần tử được thêm vào sau cùng sẽ được lấy ra đầu tiên.

Cách hoạt động của Stack

1. Thêm phần tử (Push):

Khi bạn thêm một phần tử vào Stack, phần tử này sẽ được đặt lên đỉnh của Stack. Trong Java, bạn có thể sử dụng phương thức push() để thêm phần tử.

Mô tả thêm một phần tử vào Stack - minh họa
Mô tả thêm một phần tử vào Stack.

2. Lấy phần tử (Pop):

Khi bạn lấy một phần tử ra khỏi Stack, phần tử trên đỉnh sẽ được lấy ra. Trong Java, phương thức pop() sẽ lấy và xóa phần tử trên đỉnh Stack.

Mô tả lấy một phần tử khỏi Stack - minh họa
Mô tả lấy một phần tử khỏi Stack.

Cấu trúc của một phần tử trong Stack

1. Phần tử (Element)

↳ Dữ liệu (Data): Mỗi phần tử trong Stack là một đối tượng chứa dữ liệu. Dữ liệu này có thể là bất kỳ kiểu dữ liệu nào, như số nguyên, chuỗi, đối tượng tùy chỉnh, v.v.

↳ Vị trí (Position): Phần tử trong Stack được tổ chức theo thứ tự, với phần tử được thêm vào sau cùng nằm ở đỉnh (top) của Stack.

2. Con trỏ đỉnh (Top Pointer)

↳ Mô tả: Top là một con trỏ hoặc chỉ số trong Stack trỏ đến phần tử hiện tại ở đỉnh của Stack.

↳ Vai trò: Khi một phần tử mới được thêm vào, top sẽ di chuyển đến vị trí mới của phần tử đó. Khi một phần tử bị loại bỏ, top sẽ quay lại vị trí của phần tử trước đó.

Cách tự triển khai một thư viện chuẩn cho cấu trúc dữ liệu Stack trong Java mà không sử dụng thư viện có sẵn:

Cách tự triển khai

// Tạo lớp Stack với kiểu dữ liệu tổng quát (generic)
public class Stack<T> {
    private Node<T> top; // Đỉnh của stack
    private int size;    // Kích thước của stack

    // Lớp Node đại diện cho mỗi phần tử trong stack
    private static class Node<T> {
        private T data;  // Dữ liệu của node
        private Node<T> next; // Liên kết tới node kế tiếp

        // Constructor
        public Node(T data) {
            this.data = data;
        }
    }

    // Constructor của Stack
    public Stack() {
        top = null;
        size = 0;
    }

    // Thêm phần tử vào stack (push)
    public void push(T data) {
        Node<T> newNode = new Node<>(data); // Tạo node mới
        newNode.next = top; // Liên kết node mới với đỉnh hiện tại
        top = newNode; // Đỉnh mới của stack là node mới
        size++; // Tăng kích thước stack
    }

    // Lấy và xóa phần tử khỏi stack (pop)
    public T pop() {
        if (isEmpty()) {
            throw new IllegalStateException("Stack rỗng");
        }
        T data = top.data; // Lấy dữ liệu của đỉnh hiện tại
        top = top.next; // Chuyển đỉnh stack xuống node kế tiếp
        size--; // Giảm kích thước stack
        return data; // Trả về dữ liệu của đỉnh trước
    }

    // Xem phần tử ở đỉnh mà không xóa nó (peek)
    public T peek() {
        if (isEmpty()) {
            throw new IllegalStateException("Stack rỗng");
        }
        return top.data; // Trả về dữ liệu của đỉnh
    }

    // Kiểm tra nếu stack rỗng
    public boolean isEmpty() {
        return top == null;
    }

    // Lấy kích thước của stack
    public int size() {
        return size;
    }

    // Duyệt qua các phần tử trong stack
    public void printStack() {
        Node<T> current = top;
        while (current != null) {
            System.out.println(current.data);
            current = current.next;
        }
    }
}

Khai báo Class Stack<E> trong Java

Để sử dụng Class Stack<E>, bạn cần import gói java.util vào đầu file Java của mình. Gói này cung cấp các lớp và giao diện để làm việc với các collection trong Java.

Cú pháp câu lệnh import:

Cú pháp

import java.util.Stack;

Cú pháp khai báo Class Stack<E>:

Cú pháp

public class Stack<E>
extends Vector<E>

Dưới đây là giải thích chi tiết về cú pháp khai báo này:

↳ public: Là access modifier – cho phép lớp Stack được truy cập từ bất kỳ đâu trong chương trình.

↳ class: Từ khóa khai báo lớp trong Java.

↳ Stack<E>: Tên lớp là Stack và sử dụng generic E để đại diện cho kiểu dữ liệu phần tử bên trong ngăn xếp.

↳ extends Vector<E>: Lớp Stack kế thừa từ lớp Vector. Điều này có nghĩa là Stack là một loại Vector với tất cả các phương thức và thuộc tính của Vector. Ngoài ra, lớp Stack cũng có thể thêm vào hoặc ghi đè các phương thức của Vector để cung cấp thêm các chức năng đặc biệt, như các phương thức liên quan đến cấu trúc dữ liệu ngăn xếp (stack) như push, pop, peek, v.v.

Constructor của lớp Stack

Lớp Stack trong Java chỉ có một constructor duy nhất đó là:

↳ Stack(): Tạo một stack rỗng.

Constructor này sẽ tạo ra một stack rỗng, sẵn sàng để bạn thêm các phần tử vào.

Ví dụ

Stack<String> stack = new Stack<>();

Tại sao Stack chỉ có một constructor đơn giản như vậy?

↳ Tính chất LIFO: Stack tuân theo nguyên tắc LIFO (Last In First Out), tức là phần tử nào được thêm vào sau cùng sẽ được lấy ra trước. Việc chỉ cần một stack rỗng để bắt đầu là đủ để thực hiện các thao tác cơ bản của một stack.

↳ Kế thừa từ Vector: Lớp Stack trong Java thực chất là một lớp con của lớp Vector. Vì vậy, nó kế thừa tất cả các phương thức và thuộc tính của Vector, bao gồm cả việc tăng dung lượng tự động khi cần thiết.

Các phương thức chính trong lớp Stack

Lớp Stack trong Java cung cấp các phương thức cơ bản để thao tác với stack, bao gồm thêm, lấy và xóa phần tử đỉnh. Nó là một cấu trúc dữ liệu hữu ích cho việc lưu trữ và xử lý dữ liệu theo thứ tự LIFO (Last-In-First-Out). Dưới đây là danh sách tất cả các phương thức của Class Stack<E> trong Java:

↳ boolean empty(): Kiểm tra xem stack có rỗng hay không.

↳ E peek(): Lấy phần tử đỉnh của stack mà không xóa nó.

↳ E pop(): Lấy và xóa phần tử đỉnh của stack.

↳ E push(E item): Thêm phần tử vào đỉnh của stack.

↳ Int search(Object o): Tìm vị trí của phần tử o trong stack (đếm từ 1).

Ví dụ cách sử dụng phương thức empty()

↳ Phương thức empty() đơn giản trả về một giá trị boolean (true hoặc false) để cho biết stack hiện tại có rỗng hay không.

↳ Trả về true: Nếu stack không chứa bất kỳ phần tử nào.

↳ Trả về false: Nếu stack có ít nhất một phần tử.

Dưới đây là ví dụ về cách sử dụng phương thức empty() của lớp Stack để kiểm tra xem stack có rỗng hay không, với kiểu dữ liệu String:

Ví dụ: Example.java

import java.util.Stack;

public class Example {
    public static void main(String[] args) {
        // Tạo một Stack với kiểu dữ liệu String
        Stack<String> stack = new Stack<>();

        // Kiểm tra nếu Stack rỗng
        System.out.println("Stack có rỗng không? " + stack.empty()); // True

        // Thêm một số phần tử vào Stack
        stack.push("Apple");
        stack.push("Banana");
        stack.push("Cherry");

        // Kiểm tra lại nếu Stack rỗng sau khi thêm phần tử
        System.out.println("Stack có rỗng không? " + stack.empty()); // False

        // Xóa tất cả các phần tử khỏi Stack
        stack.pop(); // Xóa "Cherry"
        stack.pop(); // Xóa "Banana"
        stack.pop(); // Xóa "Apple"

        // Kiểm tra nếu Stack rỗng sau khi xóa tất cả các phần tử
        System.out.println("Stack có rỗng không? " + stack.empty()); // True
    }
}

Kết quả của chương trình là:

Stack có rỗng không? true
Stack có rỗng không? false
Stack có rỗng không? true

Ví dụ về cách sử dụng phương thức peek()

Dưới đây là ví dụ về cách sử dụng phương thức peek() của lớp Stack để lấy phần tử đỉnh của stack mà không xóa nó, với kiểu dữ liệu Integer:

Ví dụ: Example.java

import java.util.Stack;

public class Example {
    public static void main(String[] args) {
        // Tạo một Stack với kiểu dữ liệu Integer
        Stack<Integer> stack = new Stack<>();

        // Thêm một số phần tử vào Stack
        stack.push(10);
        stack.push(20);
        stack.push(30);

        // Lấy và in phần tử đỉnh của Stack mà không xóa nó
        System.out.println("Phần tử đỉnh hiện tại của Stack: " + stack.peek()); // 30

        // In toàn bộ Stack để xem cấu trúc
        System.out.println("Toàn bộ Stack: " + stack);

        // Xóa phần tử đỉnh của Stack
        stack.pop();

        // Lấy và in phần tử đỉnh mới của Stack sau khi xóa phần tử đỉnh trước đó
        System.out.println("Phần tử đỉnh mới của Stack sau khi xóa: " + stack.peek()); // 20
    }
}

Kết quả của chương trình là:

Phần tử đỉnh hiện tại của Stack: 30
Toàn bộ Stack: [10, 20, 30]
Phần tử đỉnh mới của Stack sau khi xóa: 20

Ví dụ về cách sử dụng phương thức pop()

Dưới đây là ví dụ về cách sử dụng phương thức pop() của lớp Stack để lấy và xóa phần tử đỉnh của stack, với kiểu dữ liệu Double:

Ví dụ: Example.java

import java.util.Stack;

public class Example {
    public static void main(String[] args) {
        // Tạo một Stack với kiểu dữ liệu Double
        Stack<Double> stack = new Stack<>();

        // Thêm một số phần tử vào Stack
        stack.push(1.1);
        stack.push(2.2);
        stack.push(3.3);

        // In Stack trước khi gọi pop()
        System.out.println("Stack trước khi gọi pop(): " + stack);

        // Lấy và xóa phần tử đỉnh của Stack
        Double topElement = stack.pop();
        System.out.println("Phần tử đỉnh đã bị xóa: " + topElement);

        // In Stack sau khi gọi pop()
        System.out.println("Stack sau khi gọi pop(): " + stack);

        // Lấy phần tử đỉnh mới của Stack
        Double newTopElement = stack.peek();
        System.out.println("Phần tử đỉnh mới sau khi gọi pop(): " + newTopElement);
    }
}

Kết quả của chương trình là:

Stack trước khi gọi pop(): [1.1, 2.2, 3.3]
Phần tử đỉnh đã bị xóa: 3.3
Stack sau khi gọi pop(): [1.1, 2.2]
Phần tử đỉnh mới sau khi gọi pop(): 2.2

Ví dụ về cách sử dụng phương thức push(E item)

Dưới đây là ví dụ về cách sử dụng phương thức push(E item) của lớp Stack để thêm phần tử vào đỉnh của stack, với kiểu dữ liệu String:

Ví dụ: Example.java

import java.util.Stack;

public class Example {
    public static void main(String[] args) {
        // Tạo một Stack với kiểu dữ liệu String
        Stack<String> stack = new Stack<>();

        // Thêm các phần tử vào đỉnh của Stack
        stack.push("Apple");
        stack.push("Banana");
        stack.push("Cherry");

        // In Stack sau khi thêm các phần tử
        System.out.println("Stack sau khi thêm các phần tử:");
        for (String item : stack) {
            System.out.println(item);
        }
    }
}

Kết quả của chương trình là:

get(ChronoField.DAY_OF_WEEK): 3
Stack sau khi thêm các phần tử:
Apple
Banana
Cherry

Sau khi thêm các phần tử, in toàn bộ stack để thấy cấu trúc của nó. Phần tử "Cherry" sẽ là phần tử đỉnh vì nó là phần tử cuối cùng được thêm vào stack.

Ví dụ minh họa cách sử dụng search(Object o)

↳ Phương thức search(Object o) của lớp Stack tìm vị trí của phần tử o trong stack, đếm từ 1 bắt đầu từ đỉnh của stack. Nếu phần tử không tồn tại trong stack, phương thức sẽ trả về -1.

Dưới đây là ví dụ minh họa cách sử dụng search(Object o) với kiểu dữ liệu String:

Ví dụ: Example.java

import java.util.Stack;

public class Example {
    public static void main(String[] args) {
        // Tạo một Stack với kiểu dữ liệu String
        Stack<String> stack = new Stack<>();

        // Thêm các phần tử vào đỉnh của Stack
        stack.push("Apple");
        stack.push("Banana");
        stack.push("Cherry");
        stack.push("Date");
        stack.push("Elderberry");

        // Tìm vị trí của các phần tử trong Stack
        int position1 = stack.search("Cherry"); // Tìm vị trí của "Cherry"
        int position2 = stack.search("Banana"); // Tìm vị trí của "Banana"
        int position3 = stack.search("Fig");    // Tìm vị trí của "Fig", không tồn tại trong stack

        // In kết quả tìm kiếm
        System.out.println("Vị trí của 'Cherry': " + position1); // 3
        System.out.println("Vị trí của 'Banana': " + position2); // 4
        System.out.println("Vị trí của 'Fig': " + position3);    // -1
    }
}

Kết quả của chương trình là:

Vị trí của 'Cherry': 3
Vị trí của 'Banana': 4
Vị trí của 'Fig': -1

Ví dụ duyệt qua phần tử của Stack

Bạn có thể duyệt qua các phần tử của stack bằng cách sử dụng vòng lặp for-each hoặc bằng cách sử dụng phương thức iterator():

Ví dụ: Example.java

import java.util.Stack;

public class Example {
    public static void main(String[] args) {
        // Tạo một Stack với kiểu dữ liệu String
        Stack<String> stack = new Stack<>();
        stack.push("Apple");
        stack.push("Banana");
        stack.push("Cherry");

        // Duyệt qua Stack bằng vòng lặp for-each
        System.out.println("Duyệt qua Stack bằng vòng lặp for-each:");
        for (String item : stack) {
            System.out.println(item);
        }

         // Duyệt qua Stack bằng iterator
         System.out.println("Duyệt qua Stack bằng iterator:");
         java.util.Iterator<String> iterator = stack.iterator();
         while (iterator.hasNext()) {
             System.out.println(iterator.next());
         }
    }
}

Kết quả của chương trình là:

get(ChronoField.DAY_OF_WEEK): 3
Duyệt qua Stack bằng vòng lặp for-each:
Apple
Banana
Cherry
Duyệt qua Stack bằng iterator:
Apple
Banana
Cherry

Ví dụ so sánh các phần tử với Stack

Bạn có thể so sánh hai Stack bằng cách sử dụng phương thức equals(Object o). Nếu hai stack có cùng số lượng phần tử và các phần tử tương ứng giống nhau, equals sẽ trả về true.

Ví dụ: Example.java

import java.util.Stack;

public class Example {
    public static void main(String[] args) {
        // Tạo hai Stack với kiểu dữ liệu String
        Stack<String> stack1 = new Stack<>();
        Stack<String> stack2 = new Stack<>();

        // Thêm các phần tử vào stack1
        stack1.push("Apple");
        stack1.push("Banana");
        stack1.push("Cherry");

        // Thêm các phần tử vào stack2
        stack2.push("Apple");
        stack2.push("Banana");
        stack2.push("Cherry");

        // So sánh hai Stack
        System.out.println("Stack1 và Stack2 có bằng nhau không? " + stack1.equals(stack2)); // True

        // Thay đổi stack2
        stack2.pop(); // Xóa phần tử "Cherry"
        stack2.push("Date");

        // So sánh lại
        System.out.println("Stack1 và Stack2 có bằng nhau không sau khi thay đổi? " + stack1.equals(stack2)); // False
    }
}

Kết quả của chương trình là:

Stack1 và Stack2 có bằng nhau không? true
Stack1 và Stack2 có bằng nhau không sau khi thay đổi? false

Ví dụ stack với đối tượng do người dùng định nghĩa

Dưới đây là ví dụ về cách sử dụng lớp Stack với một đối tượng do người dùng định nghĩa. Trong ví dụ này, chúng ta sẽ định nghĩa một lớp Person với các thuộc tính name và age, và sau đó sử dụng lớp Stack để lưu trữ các đối tượng Person.

Ví dụ: Example.java

import java.util.Stack;

class Person {
    private String name;
    private int age;

    // Constructor
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter và Setter
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    // Phương thức toString() để in thông tin đối tượng
    @Override
    public String toString() {
        return "Person{Tên='" + name + "', Tuổi=" + age + "}";
    }
}

public class Example {
    public static void main(String[] args) {
        // Tạo một Stack với kiểu dữ liệu Person
        Stack<Person> stack = new Stack<>();

        // Thêm các đối tượng Person vào đỉnh của Stack
        stack.push(new Person("Vuong", 30));
        stack.push(new Person("Truong", 25));
        stack.push(new Person("Duong", 35));

        // Duyệt qua Stack và in các đối tượng Person
        System.out.println("Duyệt qua Stack bằng vòng lặp for-each:");
        for (Person person : stack) {
            System.out.println(person);
        }

        // Sử dụng phương thức peek() để lấy phần tử đỉnh mà không xóa nó
        System.out.println("\nPhần tử đỉnh hiện tại của Stack: " + stack.peek());

        // Xóa phần tử đỉnh của Stack bằng phương thức pop()
        System.out.println("\nXóa phần tử đỉnh của Stack: " + stack.pop());

        // Duyệt qua Stack sau khi xóa phần tử đỉnh
        System.out.println("\nDuyệt qua Stack sau khi xóa phần tử đỉnh:");
        for (Person person : stack) {
            System.out.println(person);
        }
    }
}

Kết quả của chương trình là:

Duyệt qua Stack bằng vòng lặp for-each:
Person{Tên='Vuong', Tuổi=30}
Person{Tên='Truong', Tuổi=25}
Person{Tên='Duong', Tuổi=35}
Phần tử đỉnh hiện tại của Stack: Person{Tên='Duong', Tuổi=35}
Xóa phần tử đỉnh của Stack: Person{Tên='Duong', Tuổi=35}
Duyệt qua Stack sau khi xóa phần tử đỉnh:
Person{Tên='Vuong', Tuổi=30}
Person{Tên='Truong', Tuổi=25}

Như vậy, chúng ta đã tìm hiểu các phương thức chính của lớp Stack trong Java. Với nguyên tắc hoạt động LIFO (Last-In-First-Out), Stack là một cấu trúc dữ liệu cơ bản nhưng cực kỳ quan trọng cho nhiều tác vụ như quản lý lời gọi hàm, duyệt cây, hoặc giải các bài toán đệ quy. Nắm vững cách sử dụng các phương thức thêm (push), lấy (pop), và kiểm tra phần tử đỉnh (peek) sẽ giúp bạn áp dụng Stack hiệu quả vào các bài toán lập trình của mình.

Câu Nói Truyền Cảm Hứng

“Bắt đầu ở đâu không quan trọng, quan trọng là bạn sẵn sàng bắt đầu.” – W. Clement Stone

Không Gian Tích Cực

“Chúc bạn luôn giữ vững niềm tin và sức mạnh để vượt qua mọi thử thách trong cuộc sống.”