Mặc dù máy tính đã có thể xử lý hàng triệu phép tính mỗi giây, nhưng khi một bài toán trở phức tạp, cách tổ chức dữ liệu vẫn vô cùng quan trọng.
Để minh họa điểm này, hãy tham khảo ví dụ sau: bạn đi đến thư viện, thử tìm kiếm một quyển sách với chủ đề nào đó. Các cuốn sách được xếp theo lĩnh vực. Trong mỗi chủ đề, sách lại được xếp theo tên tác giả, nhờ vậy mà việc lấy và cất sách từ giá trở nên khá dễ dàng và đơn giản.
Bây giờ, hãy thử tưởng tượng thay vì tổ chức thành từng giá sách cụ thể, sách được chất thành từng đống ở khắp thư viện. Để tìm được quyển sách của mình, bạn sẽ phải mất hàng giờ, thậm chí rất nhiều ngày.
Tương tự, một phần mềm không thể vận hành hiệu quả khi dữ liệu không được lưu trữ một cách phù hợp với ứng dụng.
Trong bài viết này, chúng ta sẽ cùng nhau điểm qua các loại cấu trúc dữ liệu từ cơ bản đến nâng cao. Để tìm hiểu chi tiết về một cấu trúc dữ liệu, các bạn có thể đọc ở link tương ứng. Trong bài viết này, mình tạm chia các CTDL được chia thành các loại sau:
Mảng và danh sách liên kết là 2 cấu trúc dữ liệu nền tảng cho tất cả các loại cấu trúc dữ liệu khác. Mảng và danh sách liên kết đều được dùng khi bạn muốn lưu nhiều dữ liệu (thường có cùng kiểu dữ liệu). Bảng dưới đây so sánh các thao tác về mảng và danh sách liên kết:
Mảng | Danh sách liên kết | |
---|---|---|
Bộ nhớ | Cố định (cần biết trước số phần tử) | Có thể tăng giảm tùy ý |
Thêm/Xóa 1 phần tử | , giả sử biết con trỏ tới phần tử đó | |
Tìm kiếm 1 phần tử | ||
Truy cập phần tử | ||
Khác | - Ít bộ nhớ hơn - Cache locality: các phần tử ở vị trí gần nhau trên bộ nhớ máy tính, nên khi truy cập các phần tử liên tiếp sẽ nhanh hơn |
Bạn có thể đọc thêm về mảng và danh sách liên kết ở đây
Stack là CTDL cho phép thực hiện các thao tác:
Cả 2 thao tác trên đều có độ phức tạp . Chú ý ta chỉ có thể xóa phần tử ở cuối CTDL, nói cách khác là phần tử mà mới được thêm vào gần nhất. Vì vậy, Stack còn được gọi là FIFO (First In First Out).
Stack có cài đặt đơn giản và được sử dụng trong nhiều thuật toán như DFS, tìm chu trình Euler, tìm khớp của đồ thị.
Trong C++ STL, có sẵn kiểu dữ liệu stack
.
Queue là CTDL cho phép thực hiện các thao tác:
Cả 2 thao tác đều có độ phức tạp . Chú ý ta chỉ có thể xóa phần tử ở đầu CTDL, nói cách khác là phần tử mà đã được thêm vào lâu nhất. Vì vậy, Stack còn được gọi là LIFO (Last In First Out).
Queue có cài đặt đơn giản và được sử dụng trong BFS.
Trong C++ STL, có sẵn kiểu dữ liệu queue
.
Deque (Double Ended Queue), là CTDL tổng quát hơn của Stack và Queue. Nó cho phép:
Cả 2 thao tác đều có độ phức tạp .
Deque được sử dụng trong một số thuật toán như:
Trong C++ STL, có sẵn kiểu dữ liệu deque
.
Heap là một cấu trúc dữ liệu cho phép thực hiện các thao tác:
Bạn có thể đọc thêm về Heap ở đây
Fibonacci Heap là một dạng heap có độ phức tạp bé hơn. Trong C++, CTDL priority_queue được cài đặt bằng Fibonacci Heap.
Cây Tìm Kiếm Nhị Phân (BST Binary Search Tree) là một cây nhị phân có tính chất: Với mỗi giá trị trên đỉnh đang xét, giá trị của mọi đỉnh trên cây con trái luôn nhỏ hơn đỉnh đang xét và giá trị của mọi đỉnh trên cây con phải luôn lớn hơn đỉnh đang xét.
Cây tìm kiếm nhị phân cho phép thực hiện các thao tác:
Trong trường hợp dữ liệu ngẫu nhiên, các thao tác trên có độ phức tạp trung bình là . Tuy nhiên trong trường hợp xấu nhất, cây tìm kiếm nhị phân bị suy biến (thành 1 "đường thẳng"), thì độ phức tạp mỗi thao tác là .
Để khắc phục điều này, có rất nhiều CTDL cải tiến từ cây tìm kiếm nhị phân, thường được gọi là các cây nhị phân cân bằng. Khi đó, các thao tác trên có thể được thực hiện với độ phức tạp . Ví dụ:
Bảng băm là một CTDL thường được sử dụng như một từ điển: mỗi phần tử trong bảng băm là một cặp (khóa, giá trị). Nếu so sánh với mảng, khóa được xem như chỉ số của mảng, còn giá trị giống như giá trị mà ta lưu tại chỉ số tương ứng. Bảng băm không như các loại từ điển thông thường - ta có thể tìm được giá trị thông qua khóa của nó.
Bảng băm hoạt động dựa trên hàm Hash: Hash là quá trình khởi tạo một giá trị khóa (thường là 32 bit hoặc 64 bit) từ một phần dữ liệu. Nó có thể là bit đầu tiên của dữ liệu, bit cuối cùng, giá trị mod cho một số nguyên tố nào đó. Dựa theo giá trị hash, dữ liệu được chia vào các bucket:
Trong trường hợp hàm Hash hoạt động tốt, mỗi bucket có rất ít phần tử, độ phức tạp của các thao tác trên Hash table như sau:
Bạn có thể đọc thêm về Hash table ở đây
Mảng cộng dồn là một cách áp dụng khéo léo mảng. Có 2 dạng bài cơ bản có thể giải được bằng cách áp dụng Prefix Sum.
Ví dụ
Cách làm
Ví dụ
Cách làm
Trên bảng 2 chiều , ta đặt là tổng các ô trong hình chữ nhật có 2 đỉnh đối diện là và .
Khi đó, ta có: .
Giải thích công thức trên:
đỏ = xanh da trời + vàng - tím + xanh lá
Disjoint Sets là cấu trúc dữ liệu được sử dụng trong thuật toán Kruskal và thuật toán Prim - 2 thuật toán tìm cây khung nhỏ nhất của đồ thị. Như tên gọi của nó, Disjoint Set được dùng để quản lý các tập hợp không giao nhau.
Bài toán
Cho đồ thị có đỉnh. Ta cần thực hiện 2 loại truy vấn:
Disjoint set cho phép ta thực hiện 2 thao tác trên với độ phức tạp .
Bạn có thể đọc thêm về Disjoint Set ở bài viết này.
Sparse Table là cấu trúc dữ liệu được sử dụng trong bài toán LCA & RMQ.
Với cả 2 bài toán, Sparse Table cho phép:
Segment Tree, còn được gọi là Interval Tree trong nhiều tài liệu tiếng Việt, là cấu trúc dữ liệu cho phép thực hiện các truy vấn trên một dãy số. Segment Tree rất linh động và có thể áp dụng với nhiều loại truy vấn khác nhau, nên nó xuất hiện rất nhiều trong các kỳ thi.
Với dãy số độ dài , Segment Tree cho phép thực hiện các thao tác với độ phức tạp .
Bạn có thể đọc thêm về Segment Tree ở đây.
Segment Tree cũng có một mở rộng với nhiều ứng dụng quan trọng là Segment Tree trên tập đoạn thẳng.
Cũng giống như Segment Tree, Fenwick tree (còn được gọi là Binary Indexed Tree) là cấu trúc dữ liệu cho phép thực hiện các truy vấn trên một dãy số:
Bạn có thể đọc thêm về Fenwick Tree ở đây.
Heavy Light Decomposition là một thuật toán được áp dụng nhiều trong những bài cần xử lý các truy vấn trên cây. Heavy-light decomposition là kĩ thuật phân tách một cây thành nhiều chuỗi đỉnh (chain) rời nhau. Sau đó, chúng ta có thể áp dụng các cấu trúc dữ liệu như Interval Tree hay Binary-Indexed Tree lên những chuỗi này để có thể cập nhật dữ liệu hoặc trả lời các truy vấn trên một đường đi giữa 2 đỉnh trong cây.
Bạn có thể đọc thêm ở: Thuật toán phân tách cây
Persistent Data Structures là những cấu trúc dữ liệu được dùng khi chúng ta cần có toàn bộ lịch sử của các thay đổi trên 1 cấu trúc dữ liệu.
Bạn có thể đọc thêm ở: Persistent Data Structures
Trie là một cấu trúc dữ liệu dùng để quản lý một tập hợp các xâu. Trie cho phép:
Ngoài ra trên thực tế, trie cũng rất tiết kiệm bộ nhớ khi áp dụng để lưu các từ có nghĩa, vì vậy nó là một CTDL có ứng dụng rất lớn.
Bạn có thể đọc thêm bài viết về trie.
Bài viết sẽ được cập nhật sau
Suffix Array là một CTDL giúp sắp xếp các hậu tố của một xâu theo thứ tự từ điển. CTDL này thường được sử dụng trong các bài toán xử lý xâu.
Bạn có thể đọc thêm về Suffix Array ở đây.
Bài viết sẽ được cập nhật sau.
Palindrome tree (còn được gọi là Eertree), là một CTDL mới được phổ biến vào năm 2014 nhờ bài thuyết trình của Mikhail Rubinchik.
Như tên gọi của nó, Palindrome tree là một CTDL giúp giải quyết các bài toán về Palindrome. Bạn có thể đọc thêm ở đây