Làm thế nào để thực hiện một hàng đợi bằng cách sử dụng hai ngăn xếp?


394

Giả sử chúng ta có hai ngăn xếp và không có biến tạm thời khác.

Có thể "xây dựng" cấu trúc dữ liệu hàng đợi chỉ bằng hai ngăn xếp không?

Câu trả lời:


701

Giữ 2 ngăn xếp, hãy gọi cho họ inboxoutbox.

Enqueue :

  • Đẩy phần tử mới lên inbox

Dequeue :

  • Nếu outboxtrống, đổ đầy nó bằng cách bật từng phần tử từ inboxvà đẩy nó lênoutbox

  • Pop và trả lại phần tử hàng đầu từ outbox

Sử dụng phương pháp này, mỗi phần tử sẽ nằm trong mỗi ngăn xếp chính xác một lần - có nghĩa là mỗi phần tử sẽ được đẩy hai lần và bật hai lần, cho các hoạt động thời gian không đổi được khấu hao.

Đây là một triển khai trong Java:

public class Queue<E>
{

    private Stack<E> inbox = new Stack<E>();
    private Stack<E> outbox = new Stack<E>();

    public void queue(E item) {
        inbox.push(item);
    }

    public E dequeue() {
        if (outbox.isEmpty()) {
            while (!inbox.isEmpty()) {
               outbox.push(inbox.pop());
            }
        }
        return outbox.pop();
    }

}

13
Độ phức tạp thời gian trong trường hợp xấu nhất vẫn là O (n). Tôi kiên trì nói điều này bởi vì tôi hy vọng không có học sinh nào ở ngoài đó (điều này nghe giống như một câu hỏi về bài tập về nhà / giáo dục) nghĩ rằng đây là một cách chấp nhận được để thực hiện một hàng đợi.
Tyler

26
Đúng là thời gian trong trường hợp xấu nhất cho một thao tác pop duy nhất là O (n) (trong đó n là kích thước hiện tại của hàng đợi). Tuy nhiên, thời gian trường hợp xấu nhất cho một chuỗi các hoạt động xếp hàng n cũng là O (n), cho chúng ta thời gian không đổi được khấu hao. Tôi sẽ không thực hiện một hàng đợi theo cách này, nhưng nó không phải là xấu.
Dave L.

1
@Tyler Nếu ngăn xếp của bạn dựa trên mảng, như hầu hết là, bạn sẽ luôn gặp trường hợp xấu nhất O (n) cho một thao tác.
Thomas Ahle

2
@Tyler : Kiểm tra sgi.com/tech/stl/Deque.html . Deque "hỗ trợ truy cập ngẫu nhiên vào các yếu tố". Do đó cả deque và stack đều dựa trên mảng. Điều này là do nó cung cấp cho bạn địa phương tốt hơn của tài liệu tham khảo và do đó nhanh hơn trong thực tế.
Thomas Ahle

13
@Newtang a) hàng đợi 1,2,3 => Hộp thư đến [3,2,1] / Hộp thư đi [] . b) dequeue. hộp thư đi trống, vì vậy hãy nạp lại => Hộp thư đến [] / Hộp thư đi [1,2,3] . Pop từ hộp thư đi, trả về 1 => Hộp thư đến [] / Hộp thư đi [2,3] . c) hàng đợi 4,5 => Hộp thư đến [5,4] / Hộp thư đi [2,3] . d) dequeue. hộp thư đi không trống, vì vậy hãy bật từ hộp thư đi, trả về 2 => Hộp thư đến [5,4] / Hộp thư đi [3] . Điều đó có làm cho nó ý nghĩa hơn không?
Dave L.

226

A - Làm thế nào để đảo ngược một ngăn xếp

Để hiểu cách xây dựng một hàng đợi bằng hai ngăn xếp, bạn nên hiểu làm thế nào để đảo ngược một ngăn xếp rõ ràng. Hãy nhớ cách stack hoạt động, nó rất giống với stack stack trên bếp của bạn. Đĩa rửa cuối cùng sẽ nằm trên đỉnh của ngăn xếp sạch, được gọi là L ast I n F irst O ut (LIFO) trong khoa học máy tính.

Hãy tưởng tượng ngăn xếp của chúng tôi như một cái chai như dưới đây;

nhập mô tả hình ảnh ở đây

Nếu chúng ta đẩy các số nguyên tương ứng 1,2,3, thì 3 sẽ nằm trên đỉnh của ngăn xếp. Bởi vì 1 sẽ được đẩy lên trước, sau đó 2 sẽ được đặt lên trên cùng 1. Cuối cùng, 3 sẽ được đặt lên trên cùng của ngăn xếp và trạng thái mới nhất của ngăn xếp của chúng tôi được biểu thị dưới dạng chai sẽ như dưới đây;

nhập mô tả hình ảnh ở đây

Bây giờ chúng ta có ngăn xếp của chúng ta được biểu diễn dưới dạng một chai được điền với các giá trị 3,2,1. Và chúng tôi muốn đảo ngược ngăn xếp để phần tử trên cùng của ngăn xếp sẽ là 1 và phần tử dưới cùng của ngăn xếp sẽ là 3. Chúng ta có thể làm gì? Chúng ta có thể lấy cái chai và giữ nó lộn ngược để tất cả các giá trị sẽ đảo ngược theo thứ tự?

nhập mô tả hình ảnh ở đây

Vâng, chúng tôi có thể làm điều đó, nhưng đó là một chai. Để thực hiện cùng một quy trình, chúng ta cần có một ngăn xếp thứ hai sẽ lưu trữ các phần tử ngăn xếp thứ nhất theo thứ tự ngược lại. Hãy đặt ngăn xếp dân cư của chúng tôi sang trái và ngăn xếp trống mới của chúng tôi ở bên phải. Để đảo ngược thứ tự của các phần tử, chúng ta sẽ bật từng phần tử từ ngăn xếp bên trái và đẩy chúng sang ngăn xếp bên phải. Bạn có thể thấy những gì xảy ra khi chúng tôi làm như vậy trên hình ảnh dưới đây;

nhập mô tả hình ảnh ở đây

Vì vậy, chúng tôi biết làm thế nào để đảo ngược một ngăn xếp.

B - Sử dụng hai ngăn xếp như một hàng đợi

Ở phần trước, tôi đã giải thích làm thế nào chúng ta có thể đảo ngược thứ tự các phần tử ngăn xếp. Điều này rất quan trọng, bởi vì nếu chúng ta đẩy và bật các phần tử pop vào ngăn xếp, đầu ra sẽ chính xác theo thứ tự ngược lại của hàng đợi. Suy nghĩ về một ví dụ, hãy đẩy mảng số nguyên {1, 2, 3, 4, 5}lên một ngăn xếp. Nếu chúng ta bật các phần tử và in chúng cho đến khi ngăn xếp trống, chúng ta sẽ nhận được mảng theo thứ tự ngược lại, sẽ {5, 4, 3, 2, 1}nhớ rằng với cùng một đầu vào, nếu chúng ta hủy hàng đợi cho đến khi hàng đợi trống, đầu ra sẽ {1, 2, 3, 4, 5}. Vì vậy, rõ ràng là với cùng một thứ tự đầu vào của các phần tử, đầu ra của hàng đợi hoàn toàn ngược lại với đầu ra của ngăn xếp. Vì chúng ta biết cách đảo ngược một ngăn xếp bằng cách sử dụng một ngăn xếp bổ sung, chúng ta có thể xây dựng một hàng đợi bằng hai ngăn xếp.

Mô hình hàng đợi của chúng tôi sẽ bao gồm hai ngăn xếp. Một ngăn xếp sẽ được sử dụng cho enqueuehoạt động (ngăn xếp số 1 ở bên trái, sẽ được gọi là Ngăn xếp đầu vào), một ngăn xếp khác sẽ được sử dụng cho dequeuehoạt động (ngăn xếp số 2 ở bên phải, sẽ được gọi là Ngăn xếp đầu ra). Kiểm tra hình ảnh dưới đây;

nhập mô tả hình ảnh ở đây

Mã giả của chúng tôi là như dưới đây;


Hoạt động Enqueue

Push every input element to the Input Stack

Hoạt động Dequeue

If ( Output Stack is Empty)
    pop every element in the Input Stack
    and push them to the Output Stack until Input Stack is Empty

pop from Output Stack

Hãy liệt kê các số nguyên {1, 2, 3}tương ứng. Các số nguyên sẽ được đẩy vào Ngăn xếp đầu vào ( Ngăn xếp số 1 ) nằm ở bên trái;

nhập mô tả hình ảnh ở đây

Sau đó, điều gì sẽ xảy ra nếu chúng ta thực hiện một hoạt động dequeue? Bất cứ khi nào một thao tác dequeue được thực thi, hàng đợi sẽ kiểm tra xem Stack đầu ra có trống hay không (xem mã giả ở trên) Nếu Stack đầu ra trống, thì Stack đầu vào sẽ được trích xuất trên đầu ra để các phần tử của Stack đầu vào sẽ được đảo ngược. Trước khi trả về một giá trị, trạng thái của hàng đợi sẽ như dưới đây;

nhập mô tả hình ảnh ở đây

Kiểm tra thứ tự của các phần tử trong Ngăn xếp đầu ra (Ngăn xếp số 2). Rõ ràng là chúng ta có thể bật các phần tử từ Ngăn xếp đầu ra để đầu ra sẽ giống như khi chúng ta thoát khỏi hàng đợi. Do đó, nếu chúng ta thực hiện hai thao tác dequeue, đầu tiên chúng ta sẽ nhận được {1, 2}tương ứng. Sau đó, phần tử 3 sẽ là phần tử duy nhất của Ngăn xếp đầu ra và Ngăn xếp đầu vào sẽ trống. Nếu chúng ta liệt kê các phần tử 4 và 5, thì trạng thái của hàng đợi sẽ như sau;

nhập mô tả hình ảnh ở đây

Bây giờ Ngăn xếp đầu ra không trống và nếu chúng tôi thực hiện thao tác dequeue, chỉ có 3 sẽ được bật ra từ Ngăn xếp đầu ra. Sau đó, nhà nước sẽ được nhìn thấy như dưới đây;

nhập mô tả hình ảnh ở đây

Một lần nữa, nếu chúng ta thực hiện thêm hai thao tác dequeue, trên thao tác dequeue đầu tiên, hàng đợi sẽ kiểm tra xem Stack đầu ra có trống không, điều đó có đúng không. Sau đó bật ra các phần tử của Ngăn xếp đầu vào và đẩy chúng vào Ngăn xếp đầu ra cho đến khi Ngăn xếp đầu vào trống, sau đó trạng thái của Hàng đợi sẽ như dưới đây;

nhập mô tả hình ảnh ở đây

Dễ thấy, đầu ra của hai thao tác dequeue sẽ là {4, 5}

C - Thực hiện xếp hàng được xây dựng với hai ngăn xếp

Đây là một triển khai trong Java. Tôi sẽ không sử dụng triển khai Stack hiện có nên ví dụ ở đây sẽ phát minh lại bánh xe;

C - 1) Lớp MyStack: Thực hiện ngăn xếp đơn giản

public class MyStack<T> {

    // inner generic Node class
    private class Node<T> {
        T data;
        Node<T> next;

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

    private Node<T> head;
    private int size;

    public void push(T e) {
        Node<T> newElem = new Node(e);

        if(head == null) {
            head = newElem;
        } else {
            newElem.next = head;
            head = newElem;     // new elem on the top of the stack
        }

        size++;
    }

    public T pop() {
        if(head == null)
            return null;

        T elem = head.data;
        head = head.next;   // top of the stack is head.next

        size--;

        return elem;
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public void printStack() {
        System.out.print("Stack: ");

        if(size == 0)
            System.out.print("Empty !");
        else
            for(Node<T> temp = head; temp != null; temp = temp.next)
                System.out.printf("%s ", temp.data);

        System.out.printf("\n");
    }
}

C - 2) Lớp MyQueue: Thực hiện xếp hàng bằng cách sử dụng hai ngăn xếp

public class MyQueue<T> {

    private MyStack<T> inputStack;      // for enqueue
    private MyStack<T> outputStack;     // for dequeue
    private int size;

    public MyQueue() {
        inputStack = new MyStack<>();
        outputStack = new MyStack<>();
    }

    public void enqueue(T e) {
        inputStack.push(e);
        size++;
    }

    public T dequeue() {
        // fill out all the Input if output stack is empty
        if(outputStack.isEmpty())
            while(!inputStack.isEmpty())
                outputStack.push(inputStack.pop());

        T temp = null;
        if(!outputStack.isEmpty()) {
            temp = outputStack.pop();
            size--;
        }

        return temp;
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

}

C - 3) Mã trình diễn

public class TestMyQueue {

    public static void main(String[] args) {
        MyQueue<Integer> queue = new MyQueue<>();

        // enqueue integers 1..3
        for(int i = 1; i <= 3; i++)
            queue.enqueue(i);

        // execute 2 dequeue operations 
        for(int i = 0; i < 2; i++)
            System.out.println("Dequeued: " + queue.dequeue());

        // enqueue integers 4..5
        for(int i = 4; i <= 5; i++)
            queue.enqueue(i);

        // dequeue the rest
        while(!queue.isEmpty())
            System.out.println("Dequeued: " + queue.dequeue());
    }

}

C - 4) Đầu ra mẫu

Dequeued: 1
Dequeued: 2
Dequeued: 3
Dequeued: 4
Dequeued: 5

18
Tôi sẽ +1 cái này cả ngày nếu tôi có thể. Tôi không thể hiểu làm thế nào nó được khấu hao theo thời gian liên tục. Hình minh họa của bạn thực sự đã xóa mọi thứ, đặc biệt là phần để lại các yếu tố còn lại trên ngăn xếp đầu ra và chỉ nạp lại khi nó trống.
Shane McQuillan

1
Điều này thực sự giúp ngăn chặn các lỗi hết thời gian tôi gặp phải trong quá trình bật. Tôi đã đặt các phần tử trở lại trong ngăn xếp ban đầu nhưng không cần phải làm điều đó. Thanh danh!
Ngân hàng Pranit

2
Tất cả các ý kiến ​​nên được mô hình hóa sau cái này.
lolololol ol

4
Tôi thực sự không cần một giải pháp cho việc này, chỉ cần duyệt ... Nhưng khi tôi thấy một câu trả lời như thế này, tôi chỉ đơn giản là yêu .. Câu trả lời tuyệt vời !!!
Maverick

80

Bạn thậm chí có thể mô phỏng một hàng đợi chỉ bằng một ngăn xếp. Ngăn xếp thứ hai (tạm thời) có thể được mô phỏng bằng ngăn xếp cuộc gọi của các cuộc gọi đệ quy đến phương thức chèn.

Nguyên tắc giữ nguyên khi chèn một phần tử mới vào hàng đợi:

  • Bạn cần chuyển các phần tử từ ngăn xếp này sang ngăn xếp tạm thời khác, để đảo ngược thứ tự của chúng.
  • Sau đó đẩy phần tử mới được chèn, vào ngăn xếp tạm thời
  • Sau đó chuyển các phần tử trở lại ngăn xếp ban đầu
  • Phần tử mới sẽ ở dưới cùng của ngăn xếp và phần tử cũ nhất nằm trên cùng (đầu tiên được bật lên)

Một lớp Queue chỉ sử dụng một Stack, sẽ như sau:

public class SimulatedQueue<E> {
    private java.util.Stack<E> stack = new java.util.Stack<E>();

    public void insert(E elem) {
        if (!stack.empty()) {
            E topElem = stack.pop();
            insert(elem);
            stack.push(topElem);
        }
        else
            stack.push(elem);
    }

    public E remove() {
        return stack.pop();
    }
}

51
Có thể mã trông thanh lịch nhưng nó rất kém hiệu quả (O (n ** 2)) và nó vẫn có hai ngăn xếp, một trong heap và một trong ngăn xếp cuộc gọi, như @pythonquick chỉ ra. Đối với thuật toán không đệ quy, bạn luôn có thể lấy một ngăn xếp "phụ" từ ngăn xếp cuộc gọi bằng các ngôn ngữ hỗ trợ đệ quy.
Antti Huima

1
@ antti.huima Và bạn có thể giải thích làm thế nào đây có thể là một phép chèn bậc hai không?! Theo những gì tôi hiểu, chèn không hoạt động đẩy pop và n, do đó, đây là thuật toán O (n) tuyến tính hoàn hảo.
LP_

1
@LP_ phải mất thời gian bậc hai O (n ^ 2) để chèn n itemsvào hàng đợi bằng cấu trúc dữ liệu trên. tổng (1 + 2 + 4 + 8 + .... + 2(n-1))kết quả trong ~O(n^2). Tôi hy vọng bạn có được điểm.
Ankit Kumar

1
@ antti.huima Bạn đã nói về độ phức tạp của hàm chèn (bạn đã nói "O (n 2) insert" - bạn có thể có nghĩa là "O (n 2) điền"). Theo quy ước , "chèn phức tạp" là thời gian một lần chèn, ở đây là tuyến tính trong số lượng phần tử đã có. Nếu chúng ta nói trong thời gian cần thiết để chèn n mục, chúng ta sẽ nói rằng một hashtable có chèn tuyến tính. Đó không phải là trường hợp.
LP_

2
Về cơ bản bạn đang sử dụng ngăn xếp, như một ngăn xếp. Điều này có nghĩa là nếu một số lượng lớn các mục trong ngăn xếp, bạn có thể bị tràn ngăn xếp - nó gần giống như giải pháp được thiết kế cho trang web này!
UKMonkey

11

Sự phức tạp thời gian sẽ tồi tệ hơn, mặc dù. Một thực hiện hàng đợi tốt làm mọi thứ trong thời gian liên tục.

Biên tập

Không chắc chắn tại sao câu trả lời của tôi đã bị đánh giá thấp ở đây. Nếu chúng tôi lập trình, chúng tôi quan tâm đến sự phức tạp về thời gian và sử dụng hai ngăn xếp tiêu chuẩn để thực hiện một hàng đợi là không hiệu quả. Đó là một điểm rất hợp lệ và có liên quan. Nếu ai đó cảm thấy cần phải hạ thấp điều này nhiều hơn, tôi sẽ quan tâm để biết tại sao.

Chi tiết hơn một chút : về lý do tại sao sử dụng hai ngăn xếp lại tệ hơn chỉ là một hàng đợi: nếu bạn sử dụng hai ngăn xếp và ai đó gọi dequeue trong khi hộp thư đi trống, bạn cần thời gian tuyến tính để đến cuối hộp thư đến (như bạn có thể thấy trong mã của Dave).

Bạn có thể triển khai hàng đợi dưới dạng danh sách liên kết đơn (mỗi phần tử trỏ đến phần tử được chèn tiếp theo), giữ một con trỏ bổ sung cho phần tử được chèn cuối cùng để đẩy (hoặc biến nó thành danh sách tuần hoàn). Việc thực hiện hàng đợi và dequeue trên cấu trúc dữ liệu này rất dễ thực hiện trong thời gian liên tục. Đó là trường hợp xấu nhất trong thời gian không đổi, không được khấu hao. Và, như các ý kiến ​​dường như yêu cầu làm rõ điều này, thời gian liên tục trong trường hợp xấu nhất hoàn toàn tốt hơn thời gian không đổi được khấu hao.


Không phải trong trường hợp trung bình. Câu trả lời của Brian mô tả một hàng đợi sẽ được khấu hao các hoạt động enqueue dequeue liên tục .
Daniel Spiewak

Đúng. Bạn có trường hợp trung bình & thời gian khấu hao phức tạp như nhau. Nhưng mặc định thường là trường hợp xấu nhất cho mỗi hoạt động và đây là O (n) trong đó n là kích thước hiện tại của cấu trúc.
Tyler

1
Trường hợp xấu nhất cũng có thể được khấu hao. Ví dụ, các mảng động có thể thay đổi (vectơ) thường được coi là có thời gian chèn không đổi, mặc dù thường phải thực hiện thao tác thay đổi kích thước và sao chép đắt tiền.
Daniel Spiewak

1
"Trường hợp xấu nhất" và "khấu hao" là hai loại phức tạp thời gian khác nhau. Thật vô nghĩa khi nói rằng "trường hợp xấu nhất có thể được khấu hao" - nếu bạn có thể thực hiện trường hợp xấu nhất = được khấu hao, thì đây sẽ là một cải tiến đáng kể; bạn sẽ chỉ nói về trường hợp xấu nhất, không có trung bình.
Tyler

Tôi không chắc ý của bạn về trường hợp xấu nhất của O (1) là "hoàn toàn tốt hơn" so với sự kết hợp giữa trường hợp trung bình O (1) và trường hợp xấu nhất O (n). Các yếu tố tỷ lệ liên tục quan trọng. Một cấu trúc dữ liệu, nếu nó chứa N mục, có thể cần phải được đóng gói lại sau N hoạt động tại thời điểm N micro giây và nếu không thì mất một micrô giây cho mỗi thao tác, có thể hữu ích hơn nhiều so với việc mất một mili giây cho mỗi thao tác, thậm chí nếu kích thước dữ liệu sẽ mở rộng lên hàng triệu mục (do đó ngụ ý rằng một số thao tác riêng lẻ sẽ mất nhiều giây).
supercat

8

Đặt hàng đợi được thực hiện là q và ngăn xếp được sử dụng để thực hiện q là stack1 và stack2.

q có thể được thực hiện theo hai cách:

Phương pháp 1 (Bằng cách thực hiện thao tác enQueue tốn kém)

Phương thức này đảm bảo rằng phần tử mới được nhập luôn nằm ở đầu ngăn xếp 1, do đó thao tác deQueue chỉ bật từ stack1. Để đặt phần tử ở đầu stack1, stack2 được sử dụng.

enQueue(q, x)
1) While stack1 is not empty, push everything from stack1 to stack2.
2) Push x to stack1 (assuming size of stacks is unlimited).
3) Push everything back to stack1.
deQueue(q)
1) If stack1 is empty then error
2) Pop an item from stack1 and return it.

Phương pháp 2 (Bằng cách làm cho hoạt động deQueue tốn kém)

Trong phương thức này, trong hoạt động en-queue, phần tử mới được nhập ở đầu stack1. Trong hoạt động hủy hàng đợi, nếu stack2 trống thì tất cả các phần tử được chuyển sang stack2 và cuối cùng đỉnh của stack2 được trả về.

enQueue(q,  x)
 1) Push x to stack1 (assuming size of stacks is unlimited).

deQueue(q)
 1) If both stacks are empty then error.
 2) If stack2 is empty
   While stack1 is not empty, push everything from stack1 to stack2.
 3) Pop the element from stack2 and return it.

Phương pháp 2 chắc chắn tốt hơn phương pháp 1. Phương pháp 1 di chuyển tất cả các phần tử hai lần trong hoạt động enQueue, trong khi phương thức 2 (trong hoạt động deQueue) di chuyển các phần tử một lần và chỉ di chuyển các phần tử nếu stack2 trống.


Không có giải pháp nào tôi hiểu ngoại trừ phương pháp của bạn 2. Tôi thích cách bạn giải thích nó bằng phương pháp enqueue và dequeue với các bước.
theGreenCabbage


3

Một giải pháp trong c #

public class Queue<T> where T : class
{
    private Stack<T> input = new Stack<T>();
    private Stack<T> output = new Stack<T>();
    public void Enqueue(T t)
    {
        input.Push(t);
    }

    public T Dequeue()
    {
        if (output.Count == 0)
        {
            while (input.Count != 0)
            {
                output.Push(input.Pop());
            }
        }

        return output.Pop();
    }
}

2

Hai ngăn xếp trong hàng đợi được định nghĩa là stack1stack2 .

Enqueue: Các yếu tố euqueued luôn được đẩy vào stack1

Dequeue: Phần đầu của stack2 có thể được bật ra vì đây là phần tử đầu tiên được chèn vào hàng đợi khi stack2 không trống. Khi stack2 trống, chúng ta bật tất cả các phần tử từ stack1 và đẩy chúng vào stack2 từng cái một. Phần tử đầu tiên trong hàng đợi được đẩy xuống dưới cùng của stack1 . Nó có thể được bật ra trực tiếp sau khi hoạt động popping và đẩy vì nó nằm trên đỉnh stack2 .

Dưới đây là mã mẫu C ++ giống nhau:

template <typename T> class CQueue
{
public:
    CQueue(void);
    ~CQueue(void);

    void appendTail(const T& node); 
    T deleteHead();                 

private:
    stack<T> stack1;
    stack<T> stack2;
};

template<typename T> void CQueue<T>::appendTail(const T& element) {
    stack1.push(element);
} 

template<typename T> T CQueue<T>::deleteHead() {
    if(stack2.size()<= 0) {
        while(stack1.size()>0) {
            T& data = stack1.top();
            stack1.pop();
            stack2.push(data);
        }
    }


    if(stack2.size() == 0)
        throw new exception("queue is empty");


    T head = stack2.top();
    stack2.pop();


    return head;
}

Giải pháp này được mượn từ blog của tôi . Phân tích chi tiết hơn với các mô phỏng hoạt động từng bước có sẵn trong trang web blog của tôi.


2

Bạn sẽ phải bật mọi thứ ra khỏi ngăn xếp đầu tiên để có được phần tử dưới cùng. Sau đó đặt tất cả chúng trở lại vào ngăn xếp thứ hai cho mọi hoạt động "dequeue".


3
Vâng, bạn đúng. Tôi tự hỏi, làm thế nào bạn có nhiều phiếu giảm giá. Tôi đã nâng cao câu trả lời của bạn
Binita Bharati

Thật đáng sợ khi thấy rằng đây là câu trả lời cuối cùng của anh ấy và đã một thập kỷ kể từ đó.
Shanu Gupta

2

Đối với nhà phát triển c # ở đây là chương trình hoàn chỉnh:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace QueueImplimentationUsingStack
{
    class Program
    {
        public class Stack<T>
        {
            public int size;
            public Node<T> head;
            public void Push(T data)
            {
                Node<T> node = new Node<T>();
                node.data = data;
                if (head == null)
                    head = node;
                else
                {
                    node.link = head;
                    head = node;
                }
                size++;
                Display();
            }
            public Node<T> Pop()
            {
                if (head == null)
                    return null;
                else
                {
                    Node<T> temp = head;
                    //temp.link = null;
                    head = head.link;
                    size--;
                    Display();
                    return temp;
                }
            }
            public void Display()
            {
                if (size == 0)
                    Console.WriteLine("Empty");
                else
                {
                    Console.Clear();
                    Node<T> temp = head;
                    while (temp!= null)
                    {
                        Console.WriteLine(temp.data);
                        temp = temp.link;
                    }
                }
            }
        }

        public class Queue<T>
        {
            public int size;
            public Stack<T> inbox;
            public Stack<T> outbox;
            public Queue()
            {
                inbox = new Stack<T>();
                outbox = new Stack<T>();
            }
            public void EnQueue(T data)
            {
                inbox.Push(data);
                size++;
            }
            public Node<T> DeQueue()
            {
                if (outbox.size == 0)
                {
                    while (inbox.size != 0)
                    {
                        outbox.Push(inbox.Pop().data);
                    }
                }
                Node<T> temp = new Node<T>();
                if (outbox.size != 0)
                {
                    temp = outbox.Pop();
                    size--;
                }
                return temp;
            }

        }
        public class Node<T>
        {
            public T data;
            public Node<T> link;
        }

        static void Main(string[] args)
        {
            Queue<int> q = new Queue<int>();
            for (int i = 1; i <= 3; i++)
                q.EnQueue(i);
           // q.Display();
            for (int i = 1; i < 3; i++)
                q.DeQueue();
            //q.Display();
            Console.ReadKey();
        }
    }
}

2

Thực hiện các hoạt động sau đây của một hàng đợi bằng cách sử dụng ngăn xếp.

đẩy (x) - Đẩy phần tử x vào phía sau hàng đợi.

pop () - Loại bỏ phần tử từ phía trước hàng đợi.

peek () - Lấy phần tử phía trước.

blank () - Trả về cho dù hàng đợi trống.

nhập mô tả hình ảnh ở đây

class MyQueue {

  Stack<Integer> input;
  Stack<Integer> output;

  /** Initialize your data structure here. */
  public MyQueue() {
    input = new Stack<Integer>();
    output = new Stack<Integer>();
  }

  /** Push element x to the back of queue. */
  public void push(int x) {
    input.push(x);
  }

  /** Removes the element from in front of queue and returns that element. */
  public int pop() {
    peek();
    return output.pop();
  }

  /** Get the front element. */
  public int peek() {
    if(output.isEmpty()) {
        while(!input.isEmpty()) {
            output.push(input.pop());
        }
    }
    return output.peek();
  }

  /** Returns whether the queue is empty. */
  public boolean empty() {
    return input.isEmpty() && output.isEmpty();
  }
}

1
// Two stacks s1 Original and s2 as Temp one
    private Stack<Integer> s1 = new Stack<Integer>();
    private Stack<Integer> s2 = new Stack<Integer>();

    /*
     * Here we insert the data into the stack and if data all ready exist on
     * stack than we copy the entire stack s1 to s2 recursively and push the new
     * element data onto s1 and than again recursively call the s2 to pop on s1.
     * 
     * Note here we can use either way ie We can keep pushing on s1 and than
     * while popping we can remove the first element from s2 by copying
     * recursively the data and removing the first index element.
     */
    public void insert( int data )
    {
        if( s1.size() == 0 )
        {
            s1.push( data );
        }
        else
        {
            while( !s1.isEmpty() )
            {
                s2.push( s1.pop() );
            }
            s1.push( data );
            while( !s2.isEmpty() )
            {
                s1.push( s2.pop() );
            }
        }
    }

    public void remove()
    {
        if( s1.isEmpty() )
        {
            System.out.println( "Empty" );
        }
        else
        {
            s1.pop();

        }
    }

1

Việc triển khai hàng đợi bằng hai ngăn xếp trong Swift:

struct Stack<Element> {
    var items = [Element]()

    var count : Int {
        return items.count
    }

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        return items.removeLast()
    }

    func peek() -> Element? {
        return items.last
    }
}

struct Queue<Element> {
    var inStack = Stack<Element>()
    var outStack = Stack<Element>()

    mutating func enqueue(_ item: Element) {
        inStack.push(item)
    }

    mutating func dequeue() -> Element? {
        fillOutStack() 
        return outStack.pop()
    }

    mutating func peek() -> Element? {
        fillOutStack()
        return outStack.peek()
    }

    private mutating func fillOutStack() {
        if outStack.count == 0 {
            while inStack.count != 0 {
                outStack.push(inStack.pop()!)
            }
        }
    }
}

1

Mặc dù bạn sẽ nhận được rất nhiều bài đăng liên quan đến việc thực hiện hàng đợi với hai ngăn xếp: 1. Hoặc bằng cách làm cho quá trình enQueue tốn kém hơn rất nhiều 2. Hoặc bằng cách làm cho quá trình deQueue tốn kém hơn rất nhiều

https://www.geekforgeek.org/queue-USE-stacks/

Một cách quan trọng tôi đã tìm ra từ bài viết trên là xây dựng hàng đợi chỉ với cấu trúc dữ liệu ngăn xếp và ngăn xếp cuộc gọi đệ quy.

Mặc dù người ta có thể lập luận rằng theo nghĩa đen, điều này vẫn đang sử dụng hai ngăn xếp, nhưng lý tưởng nhất là việc này chỉ sử dụng một cấu trúc dữ liệu ngăn xếp.

Dưới đây là lời giải thích của vấn đề:

  1. Khai báo một ngăn xếp để enQueue và khử dữ liệu và đẩy dữ liệu vào ngăn xếp.

  2. trong khi deQueueing có một điều kiện cơ bản trong đó phần tử của ngăn xếp được bật khi kích thước của ngăn xếp là 1. Điều này sẽ đảm bảo rằng không có tràn ngăn xếp trong quá trình đệ quy deQueue.

  3. Trong khi deQueueing đầu tiên bật dữ liệu từ đầu ngăn xếp. Lý tưởng nhất là phần tử này sẽ là phần tử có mặt ở đầu ngăn xếp. Bây giờ một khi điều này được thực hiện, hãy gọi đệ quy hàm deQueue và sau đó đẩy phần tử bật ở trên trở lại vào ngăn xếp.

Mã sẽ trông như dưới đây:

if (s1.isEmpty())
System.out.println("The Queue is empty");
        else if (s1.size() == 1)
            return s1.pop();
        else {
            int x = s1.pop();
            int result = deQueue();
            s1.push(x);
            return result;

Bằng cách này, bạn có thể tạo một hàng đợi bằng cách sử dụng cấu trúc dữ liệu ngăn xếp đơn và ngăn xếp cuộc gọi đệ quy.


1

Dưới đây là giải pháp trong ngôn ngữ javascript sử dụng cú pháp ES6.

Stack.js

//stack using array
class Stack {
  constructor() {
    this.data = [];
  }

  push(data) {
    this.data.push(data);
  }

  pop() {
    return this.data.pop();
  }

  peek() {
    return this.data[this.data.length - 1];
  }

  size(){
    return this.data.length;
  }
}

export { Stack };

QueueUsingTwoStacks.js

import { Stack } from "./Stack";

class QueueUsingTwoStacks {
  constructor() {
    this.stack1 = new Stack();
    this.stack2 = new Stack();
  }

  enqueue(data) {
    this.stack1.push(data);
  }

  dequeue() {
    //if both stacks are empty, return undefined
    if (this.stack1.size() === 0 && this.stack2.size() === 0)
      return undefined;

    //if stack2 is empty, pop all elements from stack1 to stack2 till stack1 is empty
    if (this.stack2.size() === 0) {
      while (this.stack1.size() !== 0) {
        this.stack2.push(this.stack1.pop());
      }
    }

    //pop and return the element from stack 2
    return this.stack2.pop();
  }
}

export { QueueUsingTwoStacks };

Dưới đây là cách sử dụng:

index.js

import { StackUsingTwoQueues } from './StackUsingTwoQueues';

let que = new QueueUsingTwoStacks();
que.enqueue("A");
que.enqueue("B");
que.enqueue("C");

console.log(que.dequeue());  //output: "A"

Đây là lỗi. Nếu bạn tranh thủ nhiều yếu tố hơn sau khi giải quyết, bạn sẽ đưa chúng vào stack1. Khi bạn đi đến dequeuemột lần nữa, bạn sẽ chuyển chúng vào các mục stack2, đặt chúng trước những gì đã có.
Alexander - Tái lập Monica

0

Tôi sẽ trả lời câu hỏi này trong Go vì Go không có nhiều bộ sưu tập phong phú trong thư viện tiêu chuẩn của nó.

Vì một ngăn xếp thực sự dễ thực hiện, tôi nghĩ tôi nên thử và sử dụng hai ngăn xếp để thực hiện một hàng đợi kết thúc kép. Để hiểu rõ hơn về cách tôi đi đến câu trả lời của mình, tôi đã chia phần thực hiện thành hai phần, phần đầu hy vọng dễ hiểu hơn nhưng chưa hoàn chỉnh.

type IntQueue struct {
    front       []int
    back        []int
}

func (q *IntQueue) PushFront(v int) {
    q.front = append(q.front, v)
}

func (q *IntQueue) Front() int {
    if len(q.front) > 0 {
        return q.front[len(q.front)-1]
    } else {
        return q.back[0]
    }
}

func (q *IntQueue) PopFront() {
    if len(q.front) > 0 {
        q.front = q.front[:len(q.front)-1]
    } else {
        q.back = q.back[1:]
    }
}

func (q *IntQueue) PushBack(v int) {
    q.back = append(q.back, v)
}

func (q *IntQueue) Back() int {
    if len(q.back) > 0 {
        return q.back[len(q.back)-1]
    } else {
        return q.front[0]
    }
}

func (q *IntQueue) PopBack() {
    if len(q.back) > 0 {
        q.back = q.back[:len(q.back)-1]
    } else {
        q.front = q.front[1:]
    }
}

Về cơ bản, đó là hai ngăn xếp mà chúng ta cho phép phần dưới của các ngăn xếp được thao tác với nhau. Tôi cũng đã sử dụng các quy ước đặt tên STL, trong đó các thao tác đẩy, bật, nhìn truyền thống của ngăn xếp có tiền tố trước / sau cho dù chúng tham chiếu đến mặt trước hay mặt sau của hàng đợi.

Vấn đề với đoạn mã trên là nó không sử dụng bộ nhớ rất hiệu quả. Trên thực tế, nó phát triển vô tận cho đến khi bạn hết không gian. Nó rất là tệ. Cách khắc phục cho việc này chỉ đơn giản là sử dụng lại phần dưới của không gian ngăn xếp bất cứ khi nào có thể. Chúng tôi phải giới thiệu một phần bù để theo dõi điều này vì một lát cắt trong Go không thể phát triển ở phía trước một khi được thu nhỏ.

type IntQueue struct {
    front       []int
    frontOffset int
    back        []int
    backOffset  int
}

func (q *IntQueue) PushFront(v int) {
    if q.backOffset > 0 {
        i := q.backOffset - 1
        q.back[i] = v
        q.backOffset = i
    } else {
        q.front = append(q.front, v)
    }
}

func (q *IntQueue) Front() int {
    if len(q.front) > 0 {
        return q.front[len(q.front)-1]
    } else {
        return q.back[q.backOffset]
    }
}

func (q *IntQueue) PopFront() {
    if len(q.front) > 0 {
        q.front = q.front[:len(q.front)-1]
    } else {
        if len(q.back) > 0 {
            q.backOffset++
        } else {
            panic("Cannot pop front of empty queue.")
        }
    }
}

func (q *IntQueue) PushBack(v int) {
    if q.frontOffset > 0 {
        i := q.frontOffset - 1
        q.front[i] = v
        q.frontOffset = i
    } else {
        q.back = append(q.back, v)
    }
}

func (q *IntQueue) Back() int {
    if len(q.back) > 0 {
        return q.back[len(q.back)-1]
    } else {
        return q.front[q.frontOffset]
    }
}

func (q *IntQueue) PopBack() {
    if len(q.back) > 0 {
        q.back = q.back[:len(q.back)-1]
    } else {
        if len(q.front) > 0 {
            q.frontOffset++
        } else {
            panic("Cannot pop back of empty queue.")
        }
    }
}

Đó là rất nhiều chức năng nhỏ nhưng trong số 6 chức năng thì 3 chức năng này chỉ là gương của người khác.


Bạn đang sử dụng mảng ở đây. Tôi không thấy ngăn xếp của bạn ở đâu.
melpomene

@melpomene OK, nếu bạn xem xét kỹ hơn, bạn sẽ nhận thấy rằng các thao tác duy nhất mà tôi đang thực hiện là thêm / xóa phần tử cuối cùng trong mảng. Nói cách khác, đẩy và bật lên. Đối với tất cả các mục đích và mục đích, đây là các ngăn xếp nhưng được thực hiện bằng cách sử dụng các mảng.
John Leidegren

@melpomene Thật ra, điều đó chỉ đúng một nửa, tôi cho rằng các ngăn xếp đã kết thúc gấp đôi. Tôi đang cho phép ngăn xếp được sửa đổi theo cách không chuẩn từ dưới lên trong các điều kiện nhất định.
John Leidegren

0

đây là giải pháp của tôi trong java bằng cách sử dụng danh sách liên kết.

class queue<T>{
static class Node<T>{
    private T data;
    private Node<T> next;
    Node(T data){
        this.data = data;
        next = null;
    }
}
Node firstTop;
Node secondTop;

void push(T data){
    Node temp = new Node(data);
    temp.next = firstTop;
    firstTop = temp;
}

void pop(){
    if(firstTop == null){
        return;
    }
    Node temp = firstTop;
    while(temp != null){
        Node temp1 = new Node(temp.data);
        temp1.next = secondTop;
        secondTop = temp1;
        temp = temp.next;
    }
    secondTop = secondTop.next;
    firstTop = null;
    while(secondTop != null){
        Node temp3 = new Node(secondTop.data);
        temp3.next = firstTop;
        firstTop = temp3;
        secondTop = secondTop.next;
    }
}

}

Lưu ý: Trong trường hợp này, hoạt động pop rất tốn thời gian. Vì vậy, tôi sẽ không đề xuất tạo một hàng đợi bằng hai ngăn xếp.


0

Với O(1) dequeue(), giống như câu trả lời của pythonquick :

// time: O(n), space: O(n)
enqueue(x):
    if stack.isEmpty():
        stack.push(x)
        return
    temp = stack.pop()
    enqueue(x)
    stack.push(temp)

// time: O(1)
x dequeue():
    return stack.pop()

Với O(1) enqueue()(điều này không được đề cập trong bài đăng này vì vậy câu trả lời này), cũng sử dụng quay lui để bong bóng và trả lại các mục cuối cùng.

// O(1)
enqueue(x):
    stack.push(x)

// time: O(n), space: O(n)
x dequeue():
    temp = stack.pop()
    if stack.isEmpty():
        x = temp
    else:
        x = dequeue()
        stack.push(temp)
    return x

Rõ ràng, đó là một bài tập mã hóa tốt vì nó không hiệu quả nhưng vẫn thanh lịch.


0

** Giải pháp JS dễ dàng **

  • Lưu ý: Tôi lấy ý kiến ​​từ người khác nhận xét

/*

enQueue(q,  x)
 1) Push x to stack1 (assuming size of stacks is unlimited).

deQueue(q)
 1) If both stacks are empty then error.
 2) If stack2 is empty
   While stack1 is not empty, push everything from stack1 to stack2.
 3) Pop the element from stack2 and return it.

*/
class myQueue {
    constructor() {
        this.stack1 = [];
        this.stack2 = [];
    }

    push(item) {
        this.stack1.push(item)
    }

    remove() {
        if (this.stack1.length == 0 && this.stack2.length == 0) {
            return "Stack are empty"
        }

        if (this.stack2.length == 0) {

            while (this.stack1.length != 0) {
                this.stack2.push(this.stack1.pop())
            }
        }
        return this.stack2.pop()
    }


    peek() {
        if (this.stack2.length == 0 && this.stack1.length == 0) {
            return 'Empty list'
        }

        if (this.stack2.length == 0) {
            while (this.stack1.length != 0) {
                this.stack2.push(this.stack1.pop())
            }
        }

        return this.stack2[0]
    }

    isEmpty() {
        return this.stack2.length === 0 && this.stack1.length === 0;
    }

}

const q = new myQueue();
q.push(1);
q.push(2);
q.push(3);
q.remove()

console.log(q)


-1
public class QueueUsingStacks<T>
{
    private LinkedListStack<T> stack1;
    private LinkedListStack<T> stack2;

    public QueueUsingStacks()
    {
        stack1=new LinkedListStack<T>();
        stack2 = new LinkedListStack<T>();

    }
    public void Copy(LinkedListStack<T> source,LinkedListStack<T> dest )
    {
        while(source.Head!=null)
        {
            dest.Push(source.Head.Data);
            source.Head = source.Head.Next;
        }
    }
    public void Enqueue(T entry)
    {

       stack1.Push(entry);
    }
    public T Dequeue()
    {
        T obj;
        if (stack2 != null)
        {
            Copy(stack1, stack2);
             obj = stack2.Pop();
            Copy(stack2, stack1);
        }
        else
        {
            throw new Exception("Stack is empty");
        }
        return obj;
    }

    public void Display()
    {
        stack1.Display();
    }


}

Đối với mọi hoạt động enqueue, chúng tôi thêm vào đầu stack1. Đối với mỗi dequeue, chúng ta làm trống nội dung của stack1 vào stack2 và loại bỏ phần tử ở đầu stack. Độ phức tạp của thời gian là O (n) cho dequeue, vì chúng ta phải sao chép stack1 sang stack2. độ phức tạp thời gian của enqueue giống như một ngăn xếp thông thường


Mã này không hiệu quả (sao chép không cần thiết) và bị hỏng: if (stack2 != null)luôn luôn đúng vì stack2được khởi tạo trong hàm tạo.
melpomene

-2

Triển khai hàng đợi bằng hai đối tượng java.util.Stack:

public final class QueueUsingStacks<E> {

        private final Stack<E> iStack = new Stack<>();
        private final Stack<E> oStack = new Stack<>();

        public void enqueue(E e) {
            iStack.push(e);
        }

        public E dequeue() {
            if (oStack.isEmpty()) {
                if (iStack.isEmpty()) {
                    throw new NoSuchElementException("No elements present in Queue");
                }
                while (!iStack.isEmpty()) {
                    oStack.push(iStack.pop());
                }
            }
            return oStack.pop();
        }

        public boolean isEmpty() {
            if (oStack.isEmpty() && iStack.isEmpty()) {
                return true;
            }
            return false;
        }

        public int size() {
            return iStack.size() + oStack.size();
        }

}

3
Mã này có chức năng giống hệt với câu trả lời của Dave L. Nó không thêm gì mới, thậm chí không có lời giải thích.
melpomene

Nó thêm các phương thức isEmpty () và size () cùng với xử lý ngoại lệ cơ bản. Tôi sẽ chỉnh sửa để thêm giải thích.
realPK

1
Không ai yêu cầu các phương thức bổ sung đó và chúng là tầm thường (mỗi dòng một dòng): return inbox.isEmpty() && outbox.isEmpty()return inbox.size() + outbox.size(), tương ứng. Mã của Dave L. đã ném một ngoại lệ khi bạn thoát khỏi hàng đợi trống. Câu hỏi ban đầu thậm chí không phải là về Java; đó là về cấu trúc dữ liệu / thuật toán nói chung. Việc triển khai Java chỉ là một minh họa bổ sung.
melpomene

1
Đây là một nguồn tuyệt vời cho những người muốn tìm hiểu cách xây dựng hàng đợi từ hai ngăn xếp, các sơ đồ chắc chắn đã giúp tôi nhiều hơn là đọc câu trả lời của Dave.
Kemal Tezer Dilsiz

@melpomene: Không phải là về phương pháp tầm thường mà là cần thiết. Giao diện hàng đợi trong Java mở rộng các phương thức đó từ giao diện Bộ sưu tập vì chúng cần thiết.
realPK
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.