Implement Stack using Queues
Tạo một ngăn xếp vào sau, ra trước mà chỉ dùng hàng đợi để lưu trữ. Hàng đợi chỉ có thể thêm một phần tử vào cuối, xóa hoặc đọc phần tử ở đầu và cho biết kích thước. Ngăn xếp hỗ trợ push x (đặt x lên trên cùng), pop (xóa và trả về phần tử trên cùng), top (trả về phần tử trên cùng mà vẫn giữ nguyên phần tử đó) và empty (ngăn xếp có rỗng không?).
Các thao tác lần lượt nằm trong ops, trong đó args[i] chứa giá trị cần đẩy vào và 0 trong các trường hợp khác. Thực hiện chúng trên một ngăn xếp rỗng và trả về một chuỗi cho mỗi thao tác: "null" cho thao tác đẩy vào, số được chuyển thành văn bản cho thao tác lấy ra hoặc xem phần tử trên cùng, và "true" hoặc "false" cho thao tác kiểm tra rỗng.
Hàm
- opsstring-array
- các phép toán theo thứ tự chúng được thực hiện
- argsinteger-array
- giá trị cho mỗi thao tác push, 0 cho mọi thao tác khác
- Trả vềstring-array
- một câu trả lời cho mỗi phép toán, dưới dạng văn bản
Ràng buộc
1 ≤ ops.length ≤ 1000args.length == ops.length- Mỗi
ops[i]làpush,pop,tophoặcempty. -109 ≤ args[i] ≤ 109đối với thao tác push, vàargs[i] == 0đối với mọi thao tác khác.popvàtopchỉ được gọi khi ngăn xếp có ít nhất một phần tử.
Ví dụ
- Đầu vào
- ops = ["push", "push", "top", "pop", "empty"]args = [1, 2, 0, 0, 0]
- Đầu ra
- ["null", "null", "2", "2", "false"]
- Giải thích
- 2 được đưa vào sau cùng nên nằm trên cùng:
topvàpopđều trả về"2". 1 vẫn còn bên trong nênemptytrả về"false".
- Đầu vào
- ops = ["push", "push", "pop", "push", "pop", "pop", "empty"]args = [4, 7, 0, 9, 0, 0, 0]
- Đầu ra
- ["null", "null", "7", "null", "9", "4", "true"]
- Giải thích
- Lần pop đầu tiên trả về 7, phần tử mới nhất. 9 được đặt lên trên 4 và rời khỏi ngăn xếp trước, vì vậy các lần pop tiếp theo trả về
"9"và"4", rồi ngăn xếp trống.
- Đầu vào
- ops = ["empty", "push", "top", "pop", "empty"]args = [0, -3, 0, 0, 0]
- Đầu ra
- ["true", "null", "-3", "-3", "true"]
- Giải thích
- Ngăn xếp ban đầu rỗng, nên đáp án đầu tiên là
"true". Giá trị âm được lưu như mọi giá trị khác: top và pop đều trả về"-3", và ngăn xếp lại rỗng.
Câu hỏi mở rộng
Bạn có thể giữ một hàng đợi duy nhất nhưng chuyển phần xử lý từ thao tác push sang pop, để push có độ phức tạp O(1) không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Hàng đợi trả về phần tử cũ nhất trước, còn ngăn xếp trả về phần tử mới nhất trước. Phần tử mới nhất phải nằm ở đâu trong hàng đợi để được lấy ra đầu tiên?
Lấy phần tử ở đầu ra rồi thêm vào cuối sẽ xoay hàng đợi một vị trí. Một vòng đầy đủ sẽ giữ nguyên hàng đợi; một vòng ngắn hơn sẽ chuyển các phần tử từ đầu ra phía sau các phần tử khác.
Sau khi thêm
xvào hàng đợi, hãy lần lượt chuyển các phần tử còn lạisize-1từ đầu hàng đợi ra cuối hàng đợi. Giờ đây,xnằm ở đầu hàng đợi và các phần tử cũ hơn theo sau theo thứ tự từ mới nhất đến cũ nhất, vì vậy thao tác lấy ra và xem phần tử trên cùng chỉ tác động đến đầu hàng đợi.
Lời giải
Hàng đợi lấy phần tử cũ nhất ra trước, còn ngăn xếp lấy phần tử mới nhất ra trước, và hàng đợi không bao giờ cho bạn xem phần tử cuối. Công cụ duy nhất bạn có là lấy phần tử ở đầu hàng đợi ra và thêm phần tử vào cuối, thao tác này xoay vòng hàng đợi. Hãy xoay hàng đợi để đưa phần tử mới nhất lên đầu, khi bạn cần đọc phần tử đó hoặc ngay sau khi nó được thêm vào.
Hai hàng đợi, lấy ra phần tử mới nhất khi pop
Ý tưởng
Giữ các phần tử trong hàng đợi main theo thứ tự đến, để phần tử mới nhất nằm ở cuối và mỗi lần đẩy chỉ cần thêm một phần tử vào hàng đợi. Thao tác lấy ra chuyển từng phần tử còn lại sang hàng đợi thứ hai, spare, cho đến khi chỉ còn một phần tử: phần tử mới nhất. Lấy phần tử đó ra khỏi hàng đợi rồi đổi chỗ hai hàng đợi, vì lúc này spare chứa các phần tử còn lại theo thứ tự ban đầu. Thao tác xem phần tử trên cùng thực hiện tương tự, nhưng cũng thêm phần tử mới nhất vào hàng đợi spare.
Mỗi thao tác lấy ra hoặc xem phần tử trên cùng đều di chuyển từng phần tử đã lưu đúng một lần: 500 lần đẩy, mỗi lần tiếp theo là một thao tác xem phần tử trên cùng, tốn 1 + 2 + ... + 500, tức khoảng 125.000 lượt di chuyển.
Thuật toán
- Giữ hai hàng đợi:
mainchứa các phần tử theo thứ tự đến, cònsparetrống. - Với
push x: thêmxvàomain. - Với
popvàtop: chuyển các phần tử từmainsangsparecho đến khi chỉ còn một phần tử, rồi lấy phần tử đó ra làm câu trả lời. Vớitop, cũng thêm phần tử đó vàospare. Hoán đổi hai hàng đợi. - Với
empty: cho biếtmaincó trống hay không.
from collections import deque
class QueueStack:
def __init__(self):
self.main = deque() # the oldest item at the front, the newest at the back
self.spare = deque() # empty between operations
def push(self, x):
self.main.append(x)
def _move_all_but_newest(self):
# Pass every item except the newest to the spare queue, in order.
while len(self.main) > 1:
self.spare.append(self.main.popleft())
def pop(self):
self._move_all_but_newest()
newest = self.main.popleft()
self.main, self.spare = self.spare, self.main
return newest
def top(self):
self._move_all_but_newest()
newest = self.main.popleft()
self.spare.append(newest) # top keeps the item
self.main, self.spare = self.spare, self.main
return newest
def empty(self):
return not self.main
def stackOps(ops, args):
stack = QueueStack()
result = []
for op, arg in zip(ops, args):
if op == "push":
stack.push(arg)
result.append("null")
elif op == "pop":
result.append(str(stack.pop()))
elif op == "top":
result.append(str(stack.top()))
else:
result.append("true" if stack.empty() else "false")
return resultMột hàng đợi, xoay sau mỗi lần thêm
Ý tưởng
Luôn giữ hàng đợi theo thứ tự của ngăn xếp, phần tử mới nhất ở đầu. Khi đó, thao tác lấy ra tương đương với dequeue, top đọc phần tử ở đầu và không cần hàng đợi thứ hai. Một phần tử mới x được thêm vào cuối, phía sau các phần tử cũ hơn, vốn đã được sắp xếp theo thứ tự mới nhất trước. Lấy phần tử đầu ra rồi thêm nó vào cuối size-1 lần: mỗi bước đưa một phần tử cũ hơn ra phía sau x, giữ nguyên thứ tự của chúng, để x kết thúc ở đầu.
Trong ví dụ đầu tiên, thêm 2 vào [1] cho kết quả [1, 2], rồi xoay một lần cho kết quả [2, 1]. Thêm một phần tử vào k phần tử cần k lần xoay, nên thao tác thêm có độ phức tạp O(n), còn các thao tác khác có độ phức tạp O(1).
Thuật toán
- Duy trì một hàng đợi luôn có phần tử đầu tiên là phần tử trên cùng của ngăn xếp.
- Với
push x: thêmxvào hàng đợi, sau đó lặp lạisize-1lần: lấy phần tử đầu hàng đợi ra rồi thêm lại vào hàng đợi. - Với
pop: lấy phần tử đầu hàng đợi ra và trả về phần tử đó. - Với
top: trả về phần tử đầu hàng đợi mà không xóa nó. - Với
empty: cho biết hàng đợi có rỗng hay không.
from collections import deque
class QueueStack:
def __init__(self):
self.queue = deque() # the newest item is always at the front
def push(self, x):
self.queue.append(x)
# Rotate the older items behind x, so x comes out first.
for _ in range(len(self.queue) - 1):
self.queue.append(self.queue.popleft())
def pop(self):
return self.queue.popleft()
def top(self):
return self.queue[0]
def empty(self):
return not self.queue
def stackOps(ops, args):
stack = QueueStack()
result = []
for op, arg in zip(ops, args):
if op == "push":
stack.push(arg)
result.append("null")
elif op == "pop":
result.append(str(stack.pop()))
elif op == "top":
result.append(str(stack.top()))
else:
result.append("true" if stack.empty() else "false")
return result
Lỗi thường gặp và trường hợp biên
- Quay
sizelần thay vìsize-1. Quay đủ một vòng sẽ đưaxra sau cùng, nên lần lấy tiếp theo sẽ trả về phần tử cũ nhất. - Quay trước khi thêm
xvào hàng đợi (queue). Các phần tử cũ quay vòng, cònxvẫn nằm ở sau cùng. - Với hai hàng đợi, không đưa phần tử trở lại
top. Khi đó, lần lấy tiếp theo sẽ trả về phần tử bên dưới. - Với hai hàng đợi, không hoán đổi chúng. Các phần tử nằm trong
spare, cònemptytrả lời"true"quá sớm. - Trả về số hoặc giá trị logic (boolean). Mọi câu trả lời đều là chuỗi (string), kể cả
"null"khi thêm phần tử.
Câu hỏi thường gặp3
Độ phức tạp thời gian của ngăn xếp được xây dựng từ hàng đợi là bao nhiêu?
Với một hàng đợi xoay vòng sau mỗi lần thêm phần tử, push có độ phức tạp O(n), còn pop, top và empty có độ phức tạp O(1). Với hai hàng đợi di chuyển các phần tử sau mỗi lần lấy phần tử ra, push có độ phức tạp O(1), còn pop và top có độ phức tạp O(n). Dù dùng cách nào, mỗi phần tử chỉ được lưu một lần, nên độ phức tạp không gian là O(n).
Tại sao ngăn xếp tạo từ hàng đợi lại chậm hơn hàng đợi tạo từ ngăn xếp?
Đổ một ngăn xếp vào ngăn xếp khác sẽ đảo ngược thứ tự của nó, vì vậy một lần đổ sẽ biến thứ tự của ngăn xếp thành thứ tự của hàng đợi và mỗi phần tử chỉ di chuyển một lần trong suốt vòng đời. Chuyển các phần tử giữa các hàng đợi vẫn giữ nguyên thứ tự của chúng, nên không có lần chuyển nào đảo ngược thứ tự. Vì vậy, một thao tác ở đây luôn tốn O(n).
Nên để thao tác push hay pop tốn kém?
Hãy tối ưu thao tác ít xảy ra hơn. Nếu bạn đọc ngăn xếp bằng top và pop thường xuyên hơn nhiều so với lúc thêm phần tử, hãy xoay ngăn xếp khi push để mỗi lần đọc có độ phức tạp O(1). Nếu có nhiều lần push trước một vài lần pop, hãy giữ push ở mức O(1) và chuyển các phần tử khi pop.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def stackOps(ops, args):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
ops = ["push", "push", "top", "pop", "empty"] args = [1, 2, 0, 0, 0]
Đầu ra mong đợi
["null", "null", "2", "2", "false"]