Sort List
Bạn có 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 kết thúc danh sách và nút đầu là nút 0. Các nút không được lưu theo thứ tự trong danh sách, và các mảng cũng có thể chứa những nút còn sót lại mà quá trình duyệt từ nút 0 không bao giờ đến được. Những nút đó không thuộc danh sách, ngay cả khi liên kết của chúng trỏ vào danh sách.
Sắp xếp danh sách theo thứ tự giá trị không giảm và trả về các giá trị của các nút theo thứ tự đó.
Hàm
- nextinteger-array
- chỉ số của nút mà mỗi nút liên kết đến, hoặc -1 đối với nút cuối cùng
- valuesinteger-array
- giá trị mà mỗi nút lưu giữ
- Trả vềinteger-array
- các giá trị của các nút trong danh sách, bắt đầu từ giá trị nhỏ nhất
Ràng buộc
1 ≤ n ≤ 4000, trong đónlà độ dài củanextvàvalues.-1000 ≤ values[i] ≤ 1000- Mỗi
next[i]là-1hoặc chỉ số của một nút từ0đếnn-1. - Bắt đầu từ nút
0, danh sách đến-1mà không đi qua một nút nào hai lần. - Các nút mà quá trình duyệt bắt đầu từ nút
0không tới được thì không thuộc danh sách. Các mụcnextcủa chúng có thể trỏ đến bất cứ đâu.
Ví dụ
- Đầu vào
- next = [3, -1, 4, 2, 1]values = [4, 1, 3, 2, 5]
- Đầu ra
- [1, 2, 3, 4, 5]
- Giải thích
- Đường đi từ nút
0ghé thăm các nút0, 3, 2, 4, 1, nên danh sách có các giá trị4, 2, 3, 5, 1. Sau khi sắp xếp, ta có1, 2, 3, 4, 5.
- Đầu vào
- next = [2, 0, -1, -1]values = [6, 9, -2, 5]
- Đầu ra
- [-2, 6]
- Giải thích
- Danh sách là
0 → 2, tương ứng với6, -2. Không thể đến các nút1và3từ nút đầu, nên các giá trị9và5của chúng bị bỏ qua, dù nút1liên kết với nút0.
- Đầu vào
- next = [1, 2, 3, -1]values = [3, -1, 3, -1]
- Đầu ra
- [-1, -1, 3, 3]
- Giải thích
- Danh sách có các giá trị
3, -1, 3, -1. Các giá trị bằng nhau vẫn xuất hiện trong đáp án với số lần bằng số lần chúng xuất hiện:-1, -1, 3, 3.
Câu hỏi mở rộng
Bạn có thể sắp xếp bằng cách liên kết lại các nút trong thời gian O(n log n) mà không sao chép các giá trị ra ngoài không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Những thuật toán sắp xếp nào cần nhảy đến vị trí bất kỳ, và những thuật toán nào chỉ đi về phía trước trong dữ liệu đầu vào? Danh sách liên kết phù hợp với kiểu thứ hai.
Gộp hai danh sách đã sắp xếp thành một danh sách không cần thêm bộ nhớ: bạn liên kết lại các nút, luôn lấy giá trị nhỏ hơn trong hai giá trị ở đầu. Nếu bạn có thể chia danh sách thành hai nửa đã sắp xếp, chỉ cần gộp một lần là xong.
Tìm điểm giữa bằng một con trỏ chậm và một con trỏ nhanh, rồi tách danh sách tại đó. Sắp xếp mỗi nửa theo cùng cách, dừng lại khi chỉ còn một nút, vì nút đó đã được sắp xếp. Sau đó, trộn hai nửa đã sắp xếp và trả về nút đầu mới.
Lời giải
Quicksort và heapsort dựa vào khả năng truy cập ngẫu nhiên, còn danh sách liên kết đơn chỉ di chuyển về phía trước. Merge sort là lựa chọn phù hợp: việc chia danh sách ở giữa và trộn hai danh sách đã sắp xếp đều chỉ duyệt theo chiều tiến, nên thuật toán sắp xếp chính các liên kết trong O(n log n). Ở đây, n là độ dài của các mảng.
Sao chép các giá trị vào một mảng rồi sắp xếp mảng đó
Ý tưởng
Danh sách là những gì lần duyệt node = next[node] ghé qua, bắt đầu từ nút 0 cho đến -1. Thu thập các giá trị đó vào một mảng mới, sắp xếp bằng hàm sắp xếp tích hợp sẵn rồi trả về mảng. Bạn không thể sắp xếp trực tiếp values: trong ví dụ thứ hai, nó chứa 9 và 5, vốn không nằm trong danh sách.
Lần duyệt có độ phức tạp O(n) và việc sắp xếp có độ phức tạp O(n log n), với bộ nhớ bổ sung O(n). Bản thân danh sách không bao giờ bị thay đổi, đó là lý do người phỏng vấn hỏi về cách tiếp cận tiếp theo.
Thuật toán
- Bắt đầu với một mảng (array) rỗng và
node = 0. - Trong khi
nodekhác-1, hãy thêmvalues[node]vào mảng và chuyển đếnnext[node]. - Sắp xếp mảng theo thứ tự tăng dần rồi trả về mảng đó.
def sortList(next, values):
# Collect the values of the nodes on the list, then sort them.
on_list = []
node = 0
while node != -1:
on_list.append(values[node])
node = next[node]
on_list.sort()
return on_listSắp xếp trộn trên các liên kết
Ý tưởng
Một con trỏ chậm di chuyển một liên kết và một con trỏ nhanh di chuyển hai liên kết sẽ tìm thấy điểm giữa chỉ trong một lượt duyệt. Để hợp nhất hai danh sách đã sắp xếp, bạn chỉ cần các nút đầu: lấy nút nhỏ hơn rồi tiến đến nút tiếp theo. Cả hai bước đều không cần mảng thứ hai.
Trong ví dụ đầu tiên, slow dừng ở 3, và việc cắt liên kết của nó chia 4, 2, 3, 5, 1 thành 4, 2, 3 và 5, 1. Hai danh sách này được sắp xếp thành 2, 3, 4 và 1, 5, rồi thao tác hợp nhất lấy 1, 2, 3, 4, sau đó nối phần còn lại là 5.
Mỗi cấp độ chia tách chạm đến từng nút một số lần không đổi, và có khoảng log n cấp độ, nên thời gian là O(n log n). Bộ nhớ bổ sung duy nhất là ngăn xếp đệ quy, với độ sâu khoảng 12 lời gọi cho 4000 nút.
Thuật toán
- Nếu danh sách chỉ có một nút (
next[head] == -1), trả vềhead. - Đặt
slow = headvàfast = next[head]; di chuyểnslowmột liên kết vàfasthai liên kết khifastvànext[fast]đều không phải-1. - Đặt
second = next[slow]rồi cắt danh sách bằngnext[slow] = -1. - Sắp xếp đệ quy cả hai nửa. Trộn chúng: nối nút đầu có giá trị nhỏ hơn vào sau
tailcho đến khi một nửa rỗng, rồi nối phần còn lại của nửa kia. - Duyệt từ đầu danh sách được trả về và thu thập các giá trị.
def sortList(next, values):
def merge(a, b):
# Splice two sorted lists into one; on a tie the node from a goes first.
head = tail = -1
while a != -1 and b != -1:
if values[b] < values[a]:
node, b = b, next[b]
else:
node, a = a, next[a]
if tail == -1:
head = node
else:
next[tail] = node
tail = node
next[tail] = a if a != -1 else b # attach whatever is left
return head
def sort(head):
if next[head] == -1: # one node is already sorted
return head
# slow stops on the last node of the first half
slow, fast = head, next[head]
while fast != -1 and next[fast] != -1:
slow = next[slow]
fast = next[next[fast]]
second = next[slow]
next[slow] = -1 # cut the list in two
return merge(sort(head), sort(second))
result = []
node = sort(0)
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
- Sắp xếp toàn bộ mảng
values. Ở ví dụ thứ hai, cách này đưa9và5vào, dù chúng không có trong danh sách. - Đặt
fastở nút đầu. Với hai nút,slowsẽ đến nút thứ hai, thao tác cắt tạo ra nửa thứ hai rỗng và đệ quy không bao giờ thu nhỏ danh sách. - Quên thao tác cắt
next[slow] = -1. Khi đó, nửa thứ nhất nối tiếp sang nửa thứ hai, và cả hai lần gọi đều sắp xếp các danh sách chồng lấn. - Bỏ phần còn lại của một nửa khi nửa kia đã hết. Những nút đó sẽ biến mất khỏi kết quả.
- Trong Lua và R, mảng bắt đầu từ 1: hãy giữ chỉ số nút bắt đầu từ 0 và đọc
next[node + 1]. Phiên bản R sao chép mảng liên kết một lần, nên dùng thêm O(n) bộ nhớ.
Câu hỏi thường gặp3
Cách tốt nhất để sắp xếp danh sách liên kết là gì?
Sắp xếp trộn. Việc chia đôi ở giữa, sắp xếp hai nửa rồi gộp chúng đều duyệt danh sách theo chiều tiến, đây là chiều duy nhất mà danh sách liên kết đơn hỗ trợ. Thuật toán chạy trong O(n log n) và nối lại các nút hiện có.
Vì sao merge sort được ưu tiên hơn quicksort khi sắp xếp danh sách liên kết?
Quicksort có thể phân hoạch danh sách bằng cách liên kết lại, nhưng để chọn chốt tốt cần truy cập ngẫu nhiên, còn nếu chọn phần tử đầu làm chốt thì danh sách đã sắp xếp sẽ có độ phức tạp O(n²). Merge sort luôn chia đôi, nên vẫn có độ phức tạp O(n log n) với mọi đầu vào.
Bạn có thể sắp xếp danh sách liên kết trong thời gian O(n log n) và dùng bộ nhớ O(1) không?
Có, với merge sort từ dưới lên. Thuật toán gộp các đoạn liền kề có độ dài 1, rồi 2, rồi 4, cho đến khi một đoạn bao phủ toàn bộ danh sách. Có log n lượt duyệt tuyến tính, và bộ nhớ sử dụng chỉ gồm một vài chỉ số nút.
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 sortList(next, values):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
next = [3, -1, 4, 2, 1] values = [4, 1, 3, 2, 5]
Đầu ra mong đợi
[1, 2, 3, 4, 5]