
Xử lý một danh sách dữ liệu chứa quá nhiều giá trị lặp lại là một trong những tác vụ phổ biến nhất mà lập trình viên gặp phải. Bài viết này sẽ hướng dẫn bạn 2 cách chuẩn xác và tối ưu nhất bằng Python để loại bỏ các phần tử trùng lặp trong list, đồng thời đi sâu vào phân tích hiệu năng cũng như cách bảo toàn thứ tự của dữ liệu ban đầu.
💡 Trả lời nhanh: Để xóa phần tử lặp nhưng không quan tâm đến thứ tự, hãy dùng list(set(your_list)). Nếu bắt buộc phải giữ nguyên thứ tự xuất hiện ban đầu của các phần tử, giải pháp tối ưu nhất là list(dict.fromkeys(your_list)).
Đề bài
Input: Một danh sách (list) chứa các phần tử (có thể là số nguyên, chuỗi văn bản), trong đó có nhiều phần tử xuất hiện nhiều lần. Ví dụ: [1, 2, 2, 3, 1, 4].
Output: Một danh sách mới chỉ chứa các phần tử duy nhất, các giá trị lặp lại đã bị loại bỏ. Tùy thuộc vào yêu cầu cụ thể, danh sách đầu ra có thể là [1, 2, 3, 4] (giữ thứ tự) hoặc một thứ tự ngẫu nhiên.
Ràng buộc: Cố gắng tối ưu thuật toán với độ phức tạp thời gian là O(N), tránh sử dụng các vòng lặp lồng nhau gây lãng phí tài nguyên.
Phân tích bài toán xóa phần tử trùng lặp
Bài toán loại bỏ phần tử trùng lặp trong Python yêu cầu trích xuất các giá trị duy nhất từ một danh sách (list), thường được giải quyết bằng cách sử dụng cấu trúc dữ liệu Set hoặc Dictionary để tối ưu cả thời gian thực thi (O(N)).
Bản chất của việc lọc trùng lặp là tìm kiếm xem một phần tử đã từng xuất hiện trước đó hay chưa. Nếu sử dụng list thông thường để lưu kết quả và toán tử in để kiểm tra, chương trình sẽ phải duyệt lại từ đầu đến cuối list kết quả cho mỗi phần tử mới.
Điều này dẫn đến độ phức tạp O(N²), cực kỳ chậm chạp khi danh sách đầu vào lên tới hàng triệu phần tử. Thay vào đó, Python cung cấp các cấu trúc dữ liệu dựa trên bảng băm (Hash Table) giúp việc kiểm tra tồn tại chỉ mất O(1) thời gian.
📌 Góc nhìn thực tế: Trong thực tế, sinh viên năm 1 hay nhầm lẫn giữa việc “chỉ cần lấy giá trị duy nhất” và việc “phải giữ nguyên thứ tự xuất hiện ban đầu của chúng”. Nhầm lẫn này dẫn đến việc sử dụng sai cấu trúc dữ liệu, làm hỏng logic của các bài toán yêu cầu tính tuần tự như xử lý file log hay danh sách xếp hạng.
Giả định
Chúng ta giả định rằng tất cả các phần tử bên trong list đầu vào đều là các kiểu dữ liệu có thể băm (hashable types) như số nguyên (int), số thực (float), chuỗi (str), hoặc tuple. Các kiểu dữ liệu unhashable như list hay dict con sẽ không thể áp dụng trực tiếp hai cách giải tối ưu dưới đây mà cần kỹ thuật xử lý phức tạp hơn.
Chính vì sự khác biệt về yêu cầu bảo toàn thứ tự dữ liệu, bài viết sẽ trình bày hai hướng tiếp cận riêng biệt: một cách đánh đổi thứ tự lấy tốc độ tuyệt đối, và một cách dung hòa cả hai yếu tố.
Cách giải 1: Dùng hàm set()
Sử dụng set() là cách nhanh nhất để xóa phần tử trùng lặp trong Python nhưng không bảo toàn thứ tự xuất hiện ban đầu của danh sách.
Ý tưởng cốt lõi
Trong toán học cũng như trong Python, “Tập hợp” (Set) là một cấu trúc dữ liệu đặc biệt không cho phép bất kỳ phần tử nào xuất hiện quá một lần. Bằng cách ép kiểu list đầu vào thành một set, Python sẽ tự động loại bỏ toàn bộ các giá trị thừa ở cấp độ C cốt lõi (rất nhanh). Sau khi đã lọc xong, chúng ta chỉ cần ép kiểu ngược lại thành list để nhận kết quả.
Các bước thực hiện
-
Đọc list đầu vào
lst. -
Truyền
lstvào hàmset(). Hệ thống sẽ tạo ra một tập hợp chỉ chứa các giá trị duy nhất. -
Truyền tập hợp vừa tạo vào hàm
list()để chuyển đổi nó trở lại thành danh sách. -
Trả về list mới.
Minh họa tay
Giả sử input: lst = [1, 2, 2, 3, 1, 4]
-
Bước 1:
unique_set = set([1, 2, 2, 3, 1, 4])Quá trình nội bộ:
-
Thêm 1 ->
{1} -
Thêm 2 ->
{1, 2} -
Thêm 2 -> Đã có, bỏ qua
-
Thêm 3 ->
{1, 2, 3} -
Thêm 1 -> Đã có, bỏ qua
-
Thêm 4 ->
{1, 2, 3, 4}(Lưu ý: set không lưu thứ tự chèn, kết quả hiển thị có thể xáo trộn)
-
-
Bước 2:
result = list({1, 2, 3, 4}) -
Output:
[1, 2, 3, 4](hoặc[2, 1, 4, 3], v.v.)
Đánh giá
-
Phù hợp người mới vì: Cú pháp cực kỳ ngắn gọn, thường chỉ tốn đúng 1 dòng code.
-
Ưu điểm: Hiệu năng xuất sắc, chạy cực kỳ nhanh chóng trên các tập dữ liệu khổng lồ nhờ cơ chế bảng băm được tối ưu hóa bằng C.
-
Nhược điểm: Làm mất thứ tự gốc của dữ liệu. Nếu bạn cần biết phần tử nào đứng trước, phần tử nào đứng sau như lúc mới nhập, cách này sẽ phá hỏng điều đó.
-
Độ phức tạp:
O(N) thời gian/O(N) bộ nhớ(với N là số lượng phần tử của list).
Cách giải 2: Dùng dict.fromkeys()
Khác với Cách 1, để giữ nguyên thứ tự ban đầu của list, từ Python 3.7 trở đi, dict.fromkeys() là giải pháp tối ưu và chuẩn Pythonic nhất.
Ý tưởng cốt lõi
Dictionary trong Python sử dụng các “khóa” (keys) mang tính chất duy nhất, giống hệt như set. Điểm đột phá là bắt đầu từ phiên bản Python 3.7, dictionary được thiết kế lại để luôn ghi nhớ và duy trì đúng thứ tự mà các khóa được chèn vào (insertion order). Bằng cách dùng list đầu vào để tạo ra các khóa cho một dictionary giả, chúng ta vừa lọc được phần tử trùng lặp, vừa giữ nguyên được vị trí của chúng.
Các bước thực hiện
-
Đọc list đầu vào
lst. -
Gọi phương thức
dict.fromkeys(lst). Phương thức này sẽ lấy từng phần tử tronglstlàm khóa (key) cho một dictionary mới. Vì khóa của dict là duy nhất, các giá trị trùng lặp tự động bị ghi đè/bỏ qua. -
Đưa dictionary vừa tạo vào hàm
list(). Khi ép kiểu dict sang list, Python sẽ mặc định chỉ lấy danh sách các khóa theo đúng thứ tự chúng được tạo. -
Trả về kết quả.
Minh họa tay
Giả sử input: lst = [1, 2, 2, 3, 1, 4]
-
Bước 1:
temp_dict = dict.fromkeys([1, 2, 2, 3, 1, 4])Quá trình nội bộ:
-
Khóa 1 ->
{1: None} -
Khóa 2 ->
{1: None, 2: None} -
Khóa 2 -> Đã có khóa 2, bỏ qua (không tạo thêm, không làm thay đổi thứ tự khóa 2 ban đầu)
-
Khóa 3 ->
{1: None, 2: None, 3: None} -
Khóa 1 -> Đã có khóa 1, bỏ qua
-
Khóa 4 ->
{1: None, 2: None, 3: None, 4: None}
-
-
Bước 2:
result = list(temp_dict)(chỉ lấy phần key) -
Output:
[1, 2, 3, 4](Chắc chắn đúng thứ tự xuất hiện ban đầu).
Khi nào nên dùng Cách 2?
Cách này bắt buộc phải dùng khi bạn đang xử lý các dữ liệu nhạy cảm về thứ tự. Ví dụ: bạn có một mảng lưu trữ lịch sử người dùng truy cập trang web (ai truy cập trước đứng trước) và bạn muốn lọc ra danh sách những người truy cập duy nhất nhưng vẫn phải biết ai đến sớm nhất.
Đánh giá
-
Ưu điểm: Vừa lọc trùng lặp O(1) cho mỗi phần tử, vừa đảm bảo tính toàn vẹn của cấu trúc mảng ban đầu. Viết gọn trong 1 dòng code.
-
Nhược điểm: Tốn thêm một chút overhead bộ nhớ ngầm định để khởi tạo dictionary và lưu các giá trị
Noneđi kèm với key, tuy nhiên sự khác biệt này là không đáng kể trên các máy tính hiện đại. -
Độ phức tạp:
O(N) thời gian/O(N) bộ nhớ.
So sánh nhanh 2 cách
Bảng này giúp bạn quyết định nên dùng cách nào để xử lý phần tử lặp mà không cần đọc lại toàn bài.
| Tiêu chí | Cách 1: Dùng hàm set() | Cách 2: Dùng dict.fromkeys() |
| Ý tưởng cốt lõi | Ép kiểu mảng thành tập hợp | Lấy các phần tử làm khóa cho từ điển |
| Độ phức tạp | O(N) thời gian, O(N) bộ nhớ | O(N) thời gian, O(N) bộ nhớ |
| Dễ đọc / dễ hiểu | ★★★★★ | ★★★★☆ |
| Tốc độ thực thi | ★★★★★ (Nhanh nhất) | ★★★★☆ (Rất nhanh) |
| Bảo toàn thứ tự | Không | Có (Từ Python 3.7+) |
| Phù hợp khi | Cần tốc độ tối đa, chỉ lấy giá trị | Cần giữ nguyên cấu trúc lịch sử, thứ hạng |
| Không phù hợp khi | Thứ tự các phần tử có ý nghĩa logic | Phiên bản Python quá cũ (< 3.6) |
Code Python phần tử trùng lặp đầy đủ
Cách 1 — Dùng hàm set() (Không giữ thứ tự):
# Tên biến nhất quán với phần minh họa tay
def remove_duplicates_with_set(lst):
"""
Hàm loại bỏ các phần tử lặp lại dùng set.
Nhận vào một list, trả về một list chứa các giá trị duy nhất.
Lưu ý: Kết quả trả về có thể không giữ nguyên thứ tự ban đầu.
"""
# Ép kiểu list thành set để lọc trùng, sau đó ép lại thành list
unique_set = set(lst)
result = list(unique_set)
return result
# --- TEST NHANH ---
# assert sorted(remove_duplicates_with_set([1, 2, 2, 3, 1, 4])) == [1, 2, 3, 4]
# --- Nhập liệu giả định từ người dùng ---
# Lưu ý: Giả định input đầu vào là danh sách số nguyên hợp lệ
raw_input = input("Nhập danh sách các số, cách nhau bởi dấu phẩy (vd: 1,2,2,3): ")
if raw_input:
lst = [int(x.strip()) for x in raw_input.split(",")]
print("Danh sách duy nhất (dùng set):", remove_duplicates_with_set(lst))
Cách 2 — Dùng dict.fromkeys() (Giữ nguyên thứ tự):
# Điểm khác với Cách 1: Sử dụng dictionary để duy trì insertion order (thứ tự chèn).
def remove_duplicates_preserve_order(lst):
"""
Hàm lọc phần tử trùng lặp nhưng vẫn giữ nguyên thứ tự xuất hiện ban đầu.
"""
# Lấy các phần tử làm keys cho dict, các keys trùng nhau sẽ bị bỏ qua
temp_dict = dict.fromkeys(lst)
# Lấy danh sách keys từ dict
result = list(temp_dict)
return result
# --- TEST NHANH ---
# assert remove_duplicates_preserve_order([1, 2, 2, 3, 1, 4]) == [1, 2, 3, 4]
# --- Chạy thử code ---
print("Danh sách duy nhất (giữ thứ tự):", remove_duplicates_preserve_order([1, 2, 2, 3, 1, 4]))
Ví dụ chạy thử
| STT | Input | Output | Giải thích |
| 1 | [10, 20, 20, 30, 10] |
[10, 20, 30] |
Dữ liệu thông thường, các số 20 và 10 ở sau bị loại bỏ, giữ nguyên vị trí xuất hiện đầu tiên. |
| 2 | ["apple", "banana", "apple"] |
["apple", "banana"] |
Hoạt động tốt với chuỗi văn bản vì chuỗi là kiểu dữ liệu băm được (hashable). |
| 3 | [] (List rỗng) |
[] |
Cả set() và dict.fromkeys() đều xử lý list rỗng an toàn mà không sinh lỗi. |
| 4 | [5, 5, 5, 5, 5] |
[5] |
Edge case: Toàn bộ phần tử giống hệt nhau. Code chỉ giữ lại đúng 1 bản ghi đầu tiên. |
Lỗi thường gặp khi viết hàm phần tử trùng lặp
Lỗi 1: TypeError: unhashable type: ‘list’
Code của bạn sẽ văng lỗi TypeError: unhashable type: 'list' khi bạn cố gắng truyền một mảng có chứa các mảng con (ví dụ mảng 2 chiều) vào hàm set() hoặc dict.fromkeys().
Nguyên nhân: Vì set và dictionary key yêu cầu các phần tử bên trong phải là kiểu dữ liệu cố định, không thể thay đổi (immutable) để tính toán mã băm (hash). List là kiểu dữ liệu có thể thay đổi (mutable), do đó Python từ chối băm nó để tránh rủi ro mất dữ liệu.
Code sai:
# output sai là: TypeError: unhashable type: 'list'
matrix = [[1, 2], [1, 2], [3, 4]]
unique = list(set(matrix))
Code đúng:
# output đúng là: [[1, 2], [3, 4]]
matrix = [[1, 2], [1, 2], [3, 4]]
# Chuyển list con thành tuple (có thể hash) trước khi ném vào dict
unique = [list(t) for t in dict.fromkeys(tuple(x) for x in matrix)]
Lỗi 2: Bỏ sót phần tử do xóa trực tiếp bằng vòng lặp for
Nhiều sinh viên gặp lỗi bỏ sót phần tử trùng lặp ngay cạnh nhau, ví dụ mảng in ra vẫn còn phần tử thừa.
Nguyên nhân: Vì bạn đang dùng vòng lặp for item in lst: kết hợp với phương thức lst.remove(item). Khi bạn xóa một phần tử, chiều dài của mảng bị co lại, nhưng chỉ số (index) của vòng lặp vẫn tiếp tục tiến lên, khiến nó nhảy cóc qua phần tử đứng ngay sau phần tử vừa bị xóa.
Code sai:
# output sai là: [1, 2, 2, 3] (bị sót số 2)
lst = [1, 2, 2, 2, 3]
for item in lst:
if lst.count(item) > 1:
lst.remove(item)
Code đúng:
# output đúng là: [1, 2, 3]
lst = [1, 2, 2, 2, 3]
# Không thay đổi list đang duyệt. Hãy tạo list mới hoặc dùng dict.fromkeys
lst = list(dict.fromkeys(lst))
Lỗi 3: Code chạy chậm đột biến (Time Limit Exceeded)
Chương trình của bạn bị chấm điểm Time Limit Exceeded trên các hệ thống LeetCode hay HackerRank khi mảng có hàng triệu phần tử.
Nguyên nhân: Vì bạn đã dùng một list rỗng để lưu kết quả và dùng biểu thức if item not in new_list: bên trong vòng lặp for. Phép toán in đối với list tốn O(K) thời gian (K là kích thước list mới). Kết hợp với vòng lặp ngoài O(N), tổng thời gian lên tới O(N²), vô cùng chậm chạp.
Code sai:
# output đúng về mặt logic, nhưng sai về hiệu năng (Quá chậm)
lst = [1, 2, 3] * 100000
new_list = []
for item in lst:
if item not in new_list: # Đoạn này gây thắt nút cổ chai
new_list.append(item)
Code đúng:
# output đúng và chạy trong nháy mắt (O(N))
lst = [1, 2, 3] * 100000
new_list = list(dict.fromkeys(lst))
Lỗi 4: TypeError: unhashable type: ‘dict’
Khi xử lý dữ liệu JSON hoặc API trả về, bạn áp dụng hàm xóa trùng lặp và gặp lỗi TypeError: unhashable type: 'dict'.
Nguyên nhân: Vì dữ liệu bạn truyền vào là một list chứa các dictionary bên trong. Tương tự như list con ở Lỗi 1, dictionary trong Python là kiểu mutable và không thể được băm để đưa vào set() hay làm khóa cho một dict khác.
Code sai:
# output sai là: TypeError: unhashable type: 'dict'
users = [{"id": 1}, {"id": 1}, {"id": 2}]
unique_users = list(set(users))
Code đúng:
# output đúng là: [{'id': 1}, {'id': 2}]
users = [{"id": 1}, {"id": 1}, {"id": 2}]
# Cách dễ nhất là dùng list comprehension kết hợp với một set trung gian lưu chuỗi stringified
seen = set()
unique_users = []
for u in users:
# Biến dict thành string hoặc tuple các items để hash
t = tuple(u.items())
if t not in seen:
seen.add(t)
unique_users.append(u)
Lỗi 5: Hiểu sai rằng set() luôn sắp xếp dữ liệu tăng dần
Bạn dùng Cách 1, thấy kết quả in ra là [1, 2, 3] và mặc định cho rằng set() có tính năng tự động sắp xếp danh sách từ bé đến lớn. Khi nộp bài lên hệ thống test ẩn thì bị báo sai mảng.
Nguyên nhân: Vì thuật toán băm của các số nguyên nhỏ trong Python vô tình trả về chính giá trị của số đó, khiến vị trí của chúng trong bảng băm trông có vẻ theo thứ tự. Trên thực tế, Set là tập hợp không có thứ tự. Đối với số lớn, chuỗi, hoặc chạy trên bản Python khác, kết quả trả về sẽ lộn xộn một cách khó đoán.
Code sai:
# output sai là: Nghĩ rằng lst luôn được sắp xếp
lst = [3, 1, 2, 1]
print(list(set(lst))) # Có thể in [1, 2, 3] làm bạn nhầm lẫn
Code đúng:
# output đúng là: Rõ ràng về mục đích
lst = [3, 1, 2, 1]
# Nếu bạn thực sự cần sắp xếp sau khi xóa trùng, hãy bọc trong hàm sorted()
print(sorted(set(lst))) # Kết quả luôn chuẩn xác là [1, 2, 3]
Câu hỏi thường gặp
Hàm loại bỏ phần tử trùng lặp chuẩn nhất là gì?
Hàm chuẩn và hiệu quả nhất trong Python để loại bỏ các giá trị lặp lại là set(). Bằng cách chạy câu lệnh list(set(your_list)), Python sử dụng sức mạnh của bảng băm (hash table) cấp thấp để thanh lọc toàn bộ dữ liệu chỉ trong O(N) thời gian, cực kỳ nhanh chóng so với vòng lặp.
Tại sao cấu trúc Set lại xóa được phần tử trùng lặp?
Tập hợp (Set) trong Python được định nghĩa dựa trên khái niệm tập hợp toán học, nơi một giá trị chỉ tồn tại duy nhất một lần. Khi bạn cố nhét một phần tử đã tồn tại vào Set, thuật toán băm (hash) sẽ nhận diện ra mã băm trùng lặp và quyết định bỏ qua lệnh chèn đó, giúp mảng tự động lọc sạch.
Làm thế nào để loại bỏ phần tử trùng lặp Python mà không phân biệt chữ hoa chữ thường?
Để xóa giá trị lặp không phân biệt hoa thường (case-insensitive), bạn phải đồng nhất dữ liệu trước. Hãy dùng list comprehension để chuyển tất cả về chữ thường, ví dụ: list(set([x.lower() for x in your_list])). Cách này đảm bảo “Apple” và “apple” được tính là một giá trị duy nhất.
Có cách nào dùng list comprehension để lọc trùng lặp phần tử trong Python không?
Bạn hoàn toàn có thể dùng list comprehension để lọc trùng lặp bằng cách kết hợp nó với một set bên ngoài để theo dõi. Cú pháp như sau: seen = set(); result = [x for x in your_list if not (x in seen or seen.add(x))]. Tuy nhiên, cách này hơi khó đọc và ít được khuyến khích hơn dict.fromkeys().
Nên dùng set() hay dict.fromkeys() để xử lý phần tử trùng lặp?
Nên dùng set() khi bạn có một tệp dữ liệu khổng lồ, cần tốc độ tối ưu nhất và không quan tâm đến vị trí ban đầu của dữ liệu. Trái lại, hãy chọn dict.fromkeys() khi thứ tự xuất hiện của phần tử mang ý nghĩa quan trọng đối với logic nghiệp vụ (ví dụ: dòng thời gian sự kiện).
Sự khác biệt giữa việc dùng set() và vòng lặp for truyền thống để xóa mảng trùng lặp là gì?
Sự khác biệt cốt lõi nằm ở hiệu năng (Big O). Vòng lặp for kết hợp với toán tử in list tốn thời gian O(N²) vì cứ mỗi phần tử lại phải duyệt mảng một lần nữa. Trong khi đó, set() dựa trên bảng băm (hash map), tra cứu tồn tại chỉ mất O(1), giúp tổng thời gian rút gọn xuống còn O(N).
Tại sao code báo lỗi TypeError khi tôi cố xóa phần tử trùng lặp trong list chứa list con?
Lỗi này xảy ra vì cấu trúc set hoặc dict yêu cầu các phần tử bên trong phải là kiểu dữ liệu bất biến (immutable) để băm được. List con (nested list) có thể bị sửa đổi nội dung, do đó Python từ chối băm nó. Giải pháp là ép kiểu các list con thành tuple trước khi thực hiện việc lọc.
Kết luận
Bài toán loại bỏ phần tử trùng lặp trong list nhìn bề ngoài khá đơn giản, nhưng lại là cơ hội tuyệt vời để lập trình viên hiểu sâu hơn về cấu trúc dữ liệu của Python. Nếu bạn không quan trọng thứ tự, hãy dùng set() để mã nguồn ngắn gọn và thực thi nhanh nhất. Nếu bạn đang xử lý logs hay danh sách cần bảo lưu trình tự thời gian, dict.fromkeys() sẽ là người bạn đồng hành đáng tin cậy. Hãy luôn ghi nhớ giới hạn về kiểu dữ liệu có thể băm (hashable) để tránh những lỗi TypeError không đáng có trong quá trình làm việc với mảng phức tạp.
Các khóa học liên quan:
Một số sản phẩm từ Python:
Một số sách lập trình Python bạn hãy tham khảo