Remove Nth Node From End of List
Bạn được cung cấp một danh sách liên kết đơn được lưu trong hai mảng có cùng độ dài. Nút i chứa giá trị values[i] và liên kết đến nút next[i], -1 đánh dấu điểm kết thúc danh sách, còn nút đầu là nút 0. Các nút không được lưu theo thứ tự trong danh sách, vì vậy hãy lần theo các liên kết.
Hãy xóa nút thứ n tính từ cuối danh sách, trong đó nút cuối cùng là nút thứ 1 tính từ cuối. Trả về các giá trị của những nút còn lại theo thứ tự trong danh sách.
Hàm
- valuesinteger-array
- giá trị mà mỗi nút lưu giữ
- nextinteger-array
- chỉ số của nút mà mỗi nút liên kết đến, hoặc -1 nếu đó là nút cuối cùng
- ninteger
- nút cần xóa, tính từ cuối danh sách, trong đó 1 là nút cuối cùng
- Trả vềinteger-array
- các giá trị còn lại theo thứ tự trong danh sách, rỗng khi nút duy nhất bị xóa
Ràng buộc
1 ≤ L ≤ 5000, trong đóLlà độ dài củavaluesvànext.-100 ≤ values[i] ≤ 1001 ≤ n ≤ L- Mỗi
next[i]là-1hoặc chỉ số nút từ0đếnL-1. - Bắt đầu từ nút
0, danh sách đi qua mỗi nút đúng một lần rồi đến-1. Không có chu trình.
Ví dụ
- Đầu vào
- values = [5, 9, 2, 7, 6]next = [2, 3, 4, -1, 1]n = 2
- Đầu ra
- [5, 2, 6, 7]
- Giải thích
- Lần theo các liên kết từ nút
0, ta đọc được danh sách5, 2, 6, 9, 7. Phần tử thứ 2 tính từ cuối là9, ở nút1; xóa phần tử này thì còn lại5, 2, 6, 7.
- Đầu vào
- values = [10, 20, 30, 40]next = [1, 2, 3, -1]n = 4
- Đầu ra
- [20, 30, 40]
- Giải thích
- Bốn nút và
n = 4: nút thứ 4 tính từ cuối là nút đầu. Danh sách hiện bắt đầu tại nút1và có các giá trị20, 30, 40.
- Đầu vào
- values = [42]next = [-1]n = 1
- Đầu ra
- []
- Giải thích
- Nút duy nhất vừa là nút đầu vừa là nút cuối. Xóa nút này sẽ để lại một danh sách rỗng, nên đáp án là
[].
Câu hỏi mở rộng
Bạn có thể tìm và bỏ liên kết nút đó chỉ trong một lượt mà không cần đếm độ dài trước không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Danh sách chỉ duyệt theo chiều tiến, còn nút được xác định bằng khoảng cách từ cuối danh sách. Nếu biết độ dài
L, nút đó sẽ nằm ở vị trí nào tính từ đầu danh sách? Và bạn cần thay đổi liên kết của nút nào để loại bỏ nó?Bạn có thể đo khoảng cách đến cuối mà không cần biết độ dài. Đặt một con trỏ đi trước con trỏ còn lại
nliên kết rồi di chuyển cả hai cùng lúc. Khi con trỏ đi trước đứng ở nút cuối cùng, con trỏ theo sau sẽ đứng ngay trước nút cần xóa.Di chuyển
fastvề phía trướcnlần. Nếu lúc này nó là-1, thì nút đầu là nút cần xóa, vì vậy danh sách bắt đầu tạinext[0]. Nếu không, di chuyểnslowvàfastcùng nhau khinext[fast] != -1, rồi đặtnext[slow] = next[next[slow]]. Duyệt danh sách từ nút đầu và thu thập các giá trị.
Lời giải
Ta tính nút cần tìm từ cuối danh sách, nhưng danh sách liên kết đơn (singly linked list) chỉ đi tới, và để gỡ liên kết một nút, bạn phải đứng ở nút ngay trước nó. Giữ hai con trỏ (pointer) cách nhau n liên kết: khi con trỏ phía trước đến nút cuối cùng, con trỏ phía sau sẽ đứng ngay trước nút cần tìm. Ở đây, L là số nút.
Sao chép các giá trị vào một mảng
Ý tưởng
Con trỏ ở đây là chỉ số nút, còn di chuyển về phía trước là node = next[node]. Duyệt một lần và thêm từng giá trị vào mảng: [5, 2, 6, 9, 7] trong ví dụ đầu tiên. Phần tử thứ n tính từ cuối nằm ở chỉ số L-n, ở đây là 5-2 = 3, tức 9. Xóa phần tử đó rồi trả về phần còn lại.
Thuật toán chạy trong thời gian O(L) và dùng thêm O(L) bộ nhớ, đồng thời không chỉnh sửa bất kỳ liên kết nào.
Thuật toán
- Bắt đầu với một mảng (array) rỗng và
node = 0. - Khi
nodekhác-1, thêmvalues[node]vào mảng rồi chuyển đếnnext[node]. - Xóa phần tử ở chỉ số (index)
length - n. - Trả về mảng.
def removeNthFromEnd(values, next, n):
# Write the values out in list order.
order = []
node = 0
while node != -1:
order.append(values[node])
node = next[node]
# Counting from the end, the n-th value sits at index len(order) - n.
del order[len(order) - n]
return orderĐếm các nút, sau đó hủy liên kết
Ý tưởng
Để xóa một nút, hãy nối liên kết của nút đứng trước nó với nút phía sau nó: next[prev] = next[next[prev]]. Đếm số nút trong lần duyệt đầu tiên. Với nút đầu ở vị trí 0, nút cần xóa ở vị trí L-n, vì vậy prev cách nút đầu L-n-1 bước. Trong ví dụ đầu tiên, đi hai bước 0 → 2 → 4 sẽ đến nút 4, và next[4] = next[1] = 3 loại bỏ nút 9.
Khi n = L, nút cần xóa là nút đầu, không có nút nào đứng trước nó, vì vậy danh sách bắt đầu từ next[0] như trong ví dụ thứ hai. Hai lần duyệt tốn khoảng 2L bước và một ít bộ nhớ để lưu các số nguyên.
Thuật toán
- Đi từ nút
0đến-1và đếm số nút, gọi số đó làL. - Nếu
n == L, nút đầu mới lànext[0]. - Nếu không, bắt đầu với
prevở nút0và di chuyển nóL-n-1lần, sau đó đặtnext[prev] = next[next[prev]]. - Đi từ nút đầu và thu thập
values[node]theo thứ tự.
def removeNthFromEnd(values, next, n):
# First pass: count the nodes.
length = 0
node = 0
while node != -1:
length += 1
node = next[node]
head = 0
if n == length:
# The node to remove is the head, so the list starts at its second node.
head = next[0]
else:
# Second pass: stop on the node just before position length - n.
prev = 0
for _ in range(length - n - 1):
prev = next[prev]
next[prev] = next[next[prev]] # skip over the removed node
result = []
node = head
while node != -1:
result.append(values[node])
node = next[node]
return resultHai con trỏ cách nhau n nút
Ý tưởng
Di chuyển fast đi trước n nút, trong khi slow chờ ở đầu danh sách, sau đó di chuyển cả hai mỗi lần một nút. Khoảng cách vẫn là n, nên khi fast ở nút cuối cùng (vị trí L-1), slow ở vị trí L-1-n, ngay trước nút cần tìm. Bạn không cần L.
Trong ví dụ đầu tiên, fast đi qua các vị trí 0 → 2 → 4. Sau đó, slow di chuyển đến 2 và 4, trong khi fast di chuyển đến 1 và 3, nút cuối cùng, rồi next[4] = next[1] = 3 xóa nút 9.
Vì n ≤ L, fast chỉ đến -1 trong lúc đi trước khi n = L, đúng lúc nút đầu tiên là nút cần tìm. Phép kiểm tra đó đảm nhiệm vai trò của nút giả khi làm việc với các đối tượng nút.
Thuật toán
- Đặt
fast = 0rồi di chuyển nónlần bằngfast = next[fast]. - Nếu
fast == -1, nút đầu là mục tiêu: nút đầu mới lànext[0]. - Nếu không, đặt
slow = 0rồi di chuyển cả hai trong khinext[fast] != -1. - Đặt
next[slow] = next[next[slow]]. - Duyệt từ nút đầu và thu thập
values[node]theo thứ tự.
def removeNthFromEnd(values, next, n):
# Send fast n links ahead of slow.
fast = 0
for _ in range(n):
fast = next[fast]
head = 0
if fast == -1:
# Fast fell off the end, so the list has exactly n nodes: remove the head.
head = next[0]
else:
# Move both, keeping the gap. When fast stands on the last node,
# slow stands just before the node to remove.
slow = 0
while next[fast] != -1:
slow = next[slow]
fast = next[fast]
next[slow] = next[next[slow]] # skip over the removed node
result = []
node = head
while node != -1:
result.append(values[node])
node = next[node]
return result
Lỗi thường gặp và trường hợp biên
- Xóa mục tại chỉ số mảng
L-n. Các nút không được lưu theo thứ tự trong danh sách: trong ví dụ đầu tiên,values[3] = 7là nút cuối cùng, không phải9. - Di chuyển cả hai con trỏ cho đến khi
fast == -1thay vìnext[fast] == -1. Khi đó,slownằm ngay tại nút cần xóa, và một nút không thể tự bỏ liên kết với chính nó. - Quên trường hợp nút đầu. Khi
n = L,fastlà-1sau khi bắt đầu từ nút đầu, và việc đọcnext[fast]gây lỗi. Python âm thầm đọcnext[-1]và trả về một danh sách sai. - Lấy kết quả từ nút
0sau khi nút đầu đã bị xóa. Bắt đầu lượt duyệt cuối cùng từ nút đầu mới. - Trong Lua và R, mảng bắt đầu từ 1. Giữ chỉ số nút bắt đầu từ 0 và đọc
next[node + 1].
Câu hỏi thường gặp3
Tại sao lời giải cho bài toán này lại dùng một nút giả?
Xóa một nút sẽ thay đổi liên kết của nút đứng trước nó, còn nút đầu danh sách thì không có nút nào đứng trước. Thêm một nút giả trước nút đầu danh sách sẽ giúp mọi nút đều có nút đứng trước, nhờ đó chỉ cần một dòng lệnh để ngắt liên kết trong mọi trường hợp, và đáp án bắt đầu từ nút kế tiếp của nút giả. Kiểm tra xem con trỏ đứng đầu đã đi ra khỏi danh sách chưa cũng giải quyết được vấn đề tương tự mà không cần dùng nút giả.
Độ phức tạp về thời gian và không gian khi xóa nút thứ n tính từ cuối là bao nhiêu?
Cần thời gian O(L) cho danh sách gồm L nút, vì bạn phải đi đến cuối để biết vị trí của mục tiêu. Cả cách đếm trước và phương pháp hai con trỏ (two pointer method) đều chỉ dùng thêm O(1) bộ nhớ. Sao chép các giá trị vào một mảng sẽ dùng O(L) bộ nhớ.
Giải pháp hai con trỏ có nhanh hơn cách đếm độ dài trước không?
Không chênh lệch nhiều. Cả hai đều có độ phức tạp O(L), và hai con trỏ di chuyển gần như số lần tương đương với hai lượt duyệt. Điểm lợi là bạn không cần biết trước độ dài, nên phương pháp này cũng dùng được với danh sách chỉ có thể đọc một lần.
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 removeNthFromEnd(values, next, n):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
values = [5, 9, 2, 7, 6] next = [2, 3, 4, -1, 1] n = 2
Đầu ra mong đợi
[5, 2, 6, 7]