← Cấu trúc dữ liệu & Giải thuật← Data Structures & Algorithms
Cấu trúc dữ liệu & Giải thuậtData Structures & Algorithms7 Th8, 2026Aug 7, 202626 phút đọc21 min read

Cấu trúc dữ liệu là gì?What Are Data Structures?

Mục lục
  1. Tổng quan
  2. Kiến thức nền tảng
  3. Chính xác thì data structure là gì
  4. Abstract data type và implementation
  5. Hai cách bố trí memory nền tảng
  6. Vì sao data structures quan trọng — đo đạc chứ không khẳng định suông
  7. Khái niệm chính
  8. Phân loại
  9. Các data structure cơ bản
  10. Các data structure phức tạp
  11. Chọn cấu trúc: bắt đầu từ các thao tác
  12. Đây không phải chuyện hàn lâm — đây là thứ tạo nên công cụ bạn đang dùng
  13. Các built-in của Python thực chất là gì
  14. Best Practices
  15. Tài liệu tham khảo
Table of contents
  1. Overview
  2. Fundamentals
  3. What exactly is a data structure
  4. Abstract data type versus implementation
  5. The two fundamental layouts
  6. Why data structures matter — measured, not asserted
  7. Key Concepts
  8. Classification
  9. The basic data structures
  10. The complex data structures
  11. Choosing one: start from the operations
  12. These are not academic — they are what your tools are made of
  13. Python’s built-ins, and what they really are
  14. Best Practices
  15. References

Thuộc bộ kiến thức Data Structures & Algorithms Roadmap.

Tổng quan

Data structure là cách tổ chức và lưu trữ dữ liệu trong máy tính sao cho có thể sử dụng nó một cách hiệu quả. Định nghĩa đó chính xác nhưng khô khan. Có một cách nói hữu ích hơn: data structure là một thỏa thuận bạn ký với cái máy. Bạn đồng ý sắp xếp dữ liệu theo một kiểu nhất định, và đổi lại một số thao tác trở nên rẻ — đồng thời, không tránh khỏi, một số thao tác khác trở nên đắt. Không có cấu trúc nào làm mọi thứ đều nhanh. Chọn một cấu trúc chính là chọn xem bạn quan tâm đến những thao tác nào.

Hãy xét một trường hợp cụ thể. Bạn có một triệu bản ghi user và cần trả lời câu hỏi “user X có nằm trong tập này không?” một trăm nghìn lần. Lưu chúng trong một array thường thì mỗi câu trả lời tốn một lần quét lên tới một triệu phép so sánh — tổng cộng 1011 phép toán, mất vài phút CPU. Lưu chúng trong hash table thì mỗi câu trả lời tốn khoảng một lần tính hash và một lần chạm vào memory — tổng cộng 105 phép toán, vài mili giây. Cùng dữ liệu, cùng câu hỏi, cùng cái máy, cùng lập trình viên. Thứ duy nhất thay đổi là cách sắp xếp, và nó mua về tốc độ nhanh hơn năm bậc độ lớn. Đó là toàn bộ lý lẽ cho việc học data structures.

Đây cũng là lý do roadmap đặt data structures trước algorithms. Thuật toán là một chuỗi các bước, nhưng chi phí của mỗi bước phụ thuộc vào dữ liệu đang nằm trong cái gì. Thuật toán Dijkstra là O(V²) với array và O((V + E) log V) với binary heap — thuật toán không đổi, cấu trúc bên dưới nó mới đổi. Bạn không thể lập luận gì về chi phí thuật toán cho tới khi biết cấu trúc của mình tính giá bao nhiêu cho mỗi thao tác, và đó là lý do mọi ghi chú trong phần này đều đi kèm bảng complexity của cấu trúc.

Kiến thức nền tảng

Chính xác thì data structure là gì

Một data structure có ba phần, và bỏ qua bất kỳ phần nào cũng sẽ dẫn tới rối rắm về sau:

  1. Một cách bố trí (layout) — dữ liệu nằm vật lý trong memory như thế nào. Các ô liền kề? Các node rải rác trên heap được nối bằng pointer? Một bảng bucket kích thước cố định?
  2. Một tập thao tác — bạn được phép làm gì với nó: insert, delete, search, lấy phần tử nhỏ nhất, duyệt theo thứ tự sắp xếp.
  3. Invariant — những tính chất luôn đúng ở giữa các thao tác, và các thao tác phải bảo toàn chúng. “Node cha của heap luôn ≤ các node con.” “Cây con trái của BST chỉ chứa key nhỏ hơn.” “Load factor của hash table không bao giờ vượt 0.75.” Invariant chính là thứ khiến các thao tác nhanh; duy trì nó chính là cái giá của các thao tác.

Phần thứ ba là phần người ta hay bỏ qua, và nó lại là phần quan trọng. Mọi cấu trúc trong phần này thực chất đều là một invariant cộng với bộ máy giữ cho nó luôn đúng. Một khi đã nắm được invariant, bạn thường có thể tự suy lại ra code.

Abstract data type và implementation

Abstract data type (ADT) mô tả một cấu trúc làm cái gì — các thao tác của nó và ý nghĩa của chúng — mà không nói bằng cách nào. Implementation là một cấu trúc cụ thể cung cấp các thao tác đó. Sự phân biệt này quan trọng vì một ADT thường có nhiều implementation với hiệu năng khác nhau rất xa:

ADTHứa hẹn điều gìImplementation phổ biếnĐánh đổi
ListDãy có thứ tự, truy cập theo vị tríDynamic array, doubly linked listArray: index O(1), chèn giữa O(n). List: index O(n), chèn O(1) nếu đã có node
StackLIFO: push, pop, peekDynamic array, linked listArray nhanh hơn (cache); list không bao giờ phải resize
QueueFIFO: enqueue, dequeueCircular buffer, linked list, hai stackCircular buffer là O(1) và thân thiện cache nhưng dung lượng cố định
Map / DictionaryTra cứu key → valueHash table, balanced BST, skip listHash: O(1) trung bình, không có thứ tự. BST: O(log n) worst case, nhưng duyệt sorted và range query được
SetKiểm tra thành viênHash set, tree set, bitsetĐánh đổi giống Map; dùng bitset nếu key là số nguyên nhỏ và dày
Priority QueueLấy/xóa phần tử cực trịBinary heap, pairing heap, sorted arrayHeap: insert và extract O(log n), peek O(1)
GraphĐỉnh và cạnhAdjacency list, adjacency matrixList: bộ nhớ O(V + E). Matrix: bộ nhớ O(V²) nhưng kiểm tra cạnh O(1)

Lợi ích thực tế: viết thuật toán dựa trên ADT thì sau này bạn có thể thay implementation mà không phải viết lại thuật toán. Một routine tìm đường đi ngắn nhất chỉ gọi push, pop_minneighbours sẽ chạy với bất kỳ priority queue nào và bất kỳ cách biểu diễn graph nào. Đây chính là nguyên lý abstraction từ ./01-programming-fundamentals-and-pseudocode.md, được áp dụng.

# Một ADT (Stack), hai implementation, interface giống hệt nhau.

class ArrayStack:
    """Stack dựa trên dynamic array. Thân thiện cache, push amortized O(1)."""
    def __init__(self):
        self._items = []

    def push(self, x):
        self._items.append(x)          # amortized O(1); O(n) ở bước resize

    def pop(self):
        if not self._items:
            raise IndexError("pop from empty stack")
        return self._items.pop()       # O(1) — xóa ở cuối

    def peek(self):
        return self._items[-1]

    def __len__(self):
        return len(self._items)


class LinkedStack:
    """Stack dựa trên singly linked list. Push O(1) thật sự, không bao giờ resize,
    nhưng mỗi node là một lần cấp phát heap riêng."""
    class _Node:
        __slots__ = ("value", "next")
        def __init__(self, value, next):
            self.value = value
            self.next = next

    def __init__(self):
        self._head = None
        self._size = 0

    def push(self, x):
        self._head = self._Node(x, self._head)   # O(1), luôn luôn
        self._size += 1

    def pop(self):
        if self._head is None:
            raise IndexError("pop from empty stack")
        node = self._head
        self._head = node.next
        self._size -= 1
        return node.value

    def peek(self):
        return self._head.value

    def __len__(self):
        return self._size


def check_balanced(text, stack):
    """Chạy được với cả hai implementation — nó chỉ dùng ADT."""
    pairs = {")": "(", "]": "[", "}": "{"}
    for ch in text:
        if ch in "([{":
            stack.push(ch)
        elif ch in pairs:
            if len(stack) == 0 or stack.pop() != pairs[ch]:
                return False
    return len(stack) == 0


assert check_balanced("([]{})", ArrayStack()) is True
assert check_balanced("([)]", LinkedStack()) is False

Hai cách bố trí memory nền tảng

Gần như mọi cấu trúc đều được xây trên một trong hai cách sắp xếp memory, và hầu hết khác biệt hiệu năng giữa chúng đều truy ngược về lựa chọn này.

Liền kề (contiguous) — một khối memory duy nhất, các phần tử nằm ở những offset cố định. Địa chỉ của phần tử ibase + i * item_size, tính được bằng một phép nhân và một phép cộng, nên indexing là O(1). Vì các phần tử nằm sát nhau, một lần nạp cache line kéo về được vài phần tử cùng lúc, khiến việc duyệt tuần tự cực nhanh trong thực tế — thường nhanh gấp 5–10 lần so với cùng số lần truy cập rải rác, dù cả hai đều là O(n) dưới mô hình RAM.

Liên kết (linked) — các phần tử là những lần cấp phát riêng biệt, mỗi phần tử giữ một pointer trỏ tới phần tử kế tiếp. Không có phép tính số học nào tìm ra phần tử i; bạn phải đi bộ từ head, nên indexing là O(n). Nhưng chèn giữa hai node chỉ là hai lần ghi pointer — không dời dữ liệu, không cấp phát lại — nên nó là O(1) khi bạn đã nắm sẵn tham chiếu tới vị trí đó.

Liền kề (array):  một lần cấp phát, chỉ số tính được
  index:      0     1     2     3     4
            +-----+-----+-----+-----+-----+
            |  10 |  20 |  30 |  40 |  50 |
            +-----+-----+-----+-----+-----+
  address:  1000  1008  1016  1024  1032        addr(i) = 1000 + 8*i

Liên kết (singly linked list): nhiều lần cấp phát, nằm rải rác
  head
   |
   v
 +------+----+     +------+----+     +------+----+
 |  10  | *--|---->|  20  | *--|---->|  30  | \  |
 +------+----+     +------+----+     +------+----+
  @0x7f2a            @0x91c4           @0x4e08     (không tính được địa chỉ)

Mọi thứ còn lại đều là biến thể hoặc tổ hợp của hai cái này. Hash table là một array liền kề gồm các bucket, mà mỗi bucket có thể là một chuỗi liên kết. B-tree là một cây gồm các node liền kề, có kích thước khớp một page trên đĩa. deque là một linked list gồm các block liền kề. Biết một cấu trúc thuộc về nửa nào của sơ đồ trên là bạn đã đoán được phần lớn bảng complexity của nó trước khi đọc.

Vì sao data structures quan trọng — đo đạc chứ không khẳng định suông

Roadmap nói data structures “tăng hiệu năng” và “giảm độ phức tạp của code”. Cả hai đều đúng, nhưng ý thứ hai bị đánh giá thấp. Đây là cùng một nhiệm vụ được viết hai lần:

# Nhiệm vụ: từ một luồng event, báo cáo ID event đầu tiên bị lặp lại.

# Phiên bản 1 — dùng list làm tập "đã thấy". Đúng, và là bậc hai.
def first_repeat_list(events):
    seen = []
    for e in events:
        if e in seen:          # quét tuyến tính O(len(seen)) ở mọi vòng lặp
            return e
        seen.append(e)
    return None                # tổng cộng: O(n^2)

# Phiên bản 2 — dùng set làm tập "đã thấy". Cùng hình dạng, nhưng tuyến tính.
def first_repeat_set(events):
    seen = set()
    for e in events:
        if e in seen:          # O(1) trung bình — một lần hash, một lần chạm bucket
            return e
        seen.add(e)
    return None                # tổng cộng: O(n)

Hai function dài bằng nhau và dễ đọc như nhau. Với n = 100_000 event phân biệt, cái đầu tốn hàng chục giây còn cái sau tốn vài phần trăm giây. Bài học ở đây là quyết định về hiệu năng được đưa ra bởi đúng một từ — [] so với set() — và không có lượng vi tối ưu nào bên trong thân vòng lặp có thể lấy lại được khoảng chênh đó. Chọn cấu trúc chính là việc tối ưu; phần còn lại là nhiễu.

Ý về sự đơn giản của code cũng là thật. Thử viết “tìm k phần tử nhỏ nhất của một luồng” mà không có heap, hoặc “kiểm tra xem thêm cạnh này có tạo ra chu trình không” mà không có disjoint set. Làm được, nhưng code dài gấp ba và mọc thêm vài bug lệch một đơn vị. Cấu trúc đúng không chỉ chạy nhanh hơn; nó làm cho thuật toán đúng trở nên hiển nhiên.

Khái niệm chính

Phân loại

Vài trục phân loại đáng gọi tên vì chúng xuất hiện liên tục:

Các data structure cơ bản

Năm cái này là vốn từ vựng. Mọi thứ phức tạp hơn đều được xây từ chúng.

Array (./04-arrays.md) — một khối liền kề chứa các phần tử cùng kiểu, định vị bằng chỉ số nguyên. Truy cập ngẫu nhiên trong thời gian hằng số là điểm mạnh định danh của nó; cái giá là chèn hoặc xóa ở bất kỳ đâu ngoài vị trí cuối đều phải dời phần đuôi. Dynamic array (list của Python, vector của C++, slice của Go) bổ sung khả năng tự lớn lên bằng cách cấp phát lại sang khối lớn hơn, thao tác này là O(n) ở bước resize nhưng amortized O(1) cho mỗi lần append vì mỗi lần resize đều nhân đôi dung lượng nên rất hiếm khi xảy ra.

Linked list (./05-linked-lists.md) — các node nằm rải rác trong memory, mỗi node giữ một giá trị và một pointer trỏ tới node kế tiếp (và trỏ tới node trước nữa, trong doubly linked list). Chèn và xóa ở một vị trí đã biết là O(1) mà không phải dời gì. Cái giá là có thật và thường bị xem nhẹ: không truy cập ngẫu nhiên được, mỗi phần tử tốn thêm một lần cấp phát và một pointer, và hành vi cache rất tệ. Trong thực tế, dynamic array thắng linked list ở phần lớn workload; linked list chỉ thắng khi bạn phải nối/cắt liên tục tại những vị trí đã nắm sẵn, hoặc khi bạn cần tham chiếu ổn định tới phần tử qua các lần thay đổi.

Stack (./06-stacks-and-queues.md) — LIFO (vào sau, ra trước). Chỉ phần tử mới nhất là chạm tới được. Instance quan trọng nhất của nó lại không phải cái bạn viết ra: đó là call stack mà mỗi lời gọi function đẩy một frame lên, và đó là lý do đệ quy không có điểm dừng sinh ra stack overflow. Stack cũng là động cơ của việc tính biểu thức, backtracking (./21-backtracking.md), DFS phiên bản lặp, và lịch sử undo.

Queue (./06-stacks-and-queues.md) — FIFO (vào trước, ra trước). Phần tử rời đi theo thứ tự đến. Queue mô hình hóa mọi thứ phải xếp hàng: lập lịch tác vụ, đệm request, hàng đợi in ấn, breadth-first search. Cài đặt đúng thì nó là một circular buffer trên array; cài đặt sai thì nó là một list Python với pop(0), tốn O(n) mỗi lần dequeue và âm thầm biến thuật toán tuyến tính thành bậc hai. Hãy dùng collections.deque.

Hash table (./07-hash-tables.md) — lưu các cặp key-value trong một array các bucket, dùng hash function để biến key thành chỉ số bucket. Insert, delete và lookup O(1) ở average case khiến nó là cấu trúc hữu dụng phổ quát nhất từng tồn tại, và là câu trả lời mặc định cho “tôi cần tra cứu theo tên”. Các lưu ý: worst case tụt xuống O(n) khi nhiều key va chạm, nó ngốn nhiều memory hơn bản thân dữ liệu (bảng được cố tình để trống một phần), và nó không cho bạn thứ tự nào cả.

Cấu trúcTruy cập theo chỉ sốSearchInsertDeleteBộ nhớWorst case có khác?
Static arrayO(1)O(n), O(log n) nếu đã sortedkhông có (cố định)không cóO(n)không
Dynamic arrayO(1)O(n)O(1) amortized ở cuối, O(n) ở chỗ khácO(n) (O(1) ở cuối)O(n)có — một lần append là O(n) khi nó kích hoạt resize
Singly linked listO(n)O(n)O(1) ở head hoặc tại node đã nắmO(1) tại node đã nắm, ngược lại O(n)O(n) + 1 pointer/phần tửkhông
Doubly linked listO(n)O(n)O(1) tại node đã nắmO(1) tại node đã nắmO(n) + 2 pointer/phần tửkhông
Stackkhông cóO(n)O(1) pushO(1) popO(n)dựa trên array: O(n) khi resize
Queue (circular buffer)không cóO(n)O(1) enqueueO(1) dequeueO(n)dựa trên array: O(n) khi resize
Hash tablekhông cóO(1) trung bìnhO(1) trung bìnhO(1) trung bìnhO(n) (có phần dư)có — O(n) nếu mọi key đều va chạm

Các data structure phức tạp

Nhóm này giải quyết những bài toán mà năm cấu trúc cơ bản không kham nổi, nhìn chung bằng cách áp đặt thêm cấu trúc và trả giá lúc sửa đổi.

Tree (./10-tree-data-structures.md) — một hệ phân cấp: một root, mỗi node có không hoặc nhiều con, không có chu trình. Tree mô hình hóa mọi thứ lồng nhau — file system, DOM document, sơ đồ tổ chức, cú pháp đã parse. Binary search tree bổ sung một invariant về thứ tự (mọi thứ bên trái nhỏ hơn, mọi thứ bên phải lớn hơn) khiến search, insert và delete là O(h) với h là chiều cao.

Balanced tree (./11-balanced-and-multiway-trees.md) — vấn đề của BST thường là h có thể suy biến thành n: đổ dữ liệu đã sorted vào và bạn nhận được một linked list khoác áo cây, với thao tác O(n). AVL tree, red-black tree và B-tree bổ sung cơ chế cân bằng lại để đảm bảo h = O(log n). Riêng B-tree dùng node rộng, cỡ đúng bằng một page trên đĩa, và đó chính xác là lý do nó là cấu trúc đứng sau gần như mọi index của database — xem ../../postgresql-dba/vi/08-indexing-strategies.md.

Heap (./12-heaps-and-priority-queues.md) — một cây nhị phân với invariant yếu hơn BST: mọi node cha đều ≤ (hoặc ≥) các con của nó, không có ràng buộc thứ tự giữa các anh em. Chính sự yếu ớt đó là điểm mấu chốt — nó rẻ đến mức duy trì được insert và extract-min trong O(log n) còn peek-min là O(1), và toàn bộ cây có thể nhồi vào một array liền kề mà không cần pointer nào. Nó là implementation tiêu chuẩn của priority queue và là động cơ bên trong Dijkstra, A*, heapsort và k-way merge.

Graph (./13-graph-data-structures.md) — các đỉnh nối với nhau bằng cạnh, không ràng buộc hình dạng: cho phép chu trình, cho phép nhiều đường đi, cho phép các mảnh rời rạc. Tree là trường hợp đặc biệt của một graph liên thông không có chu trình. Graph mô hình hóa mọi loại mạng lưới — đường sá, quan hệ xã hội, phụ thuộc package, máy trạng thái — và mang theo họ thuật toán phong phú nhất trong roadmap này (./14-shortest-path-algorithms.md, ./15-minimum-spanning-trees.md).

Trie (./17-advanced-tree-structures.md) — một cây được đánh key theo tiền tố chuỗi, trong đó đường đi từ root ghép lại thành key. Lookup tốn O(m) theo độ dài key thay vì O(log n) theo số lượng key, và truy vấn theo tiền tố (“mọi từ bắt đầu bằng pre”) thì miễn phí. Đây là thứ đứng sau autocomplete và bảng định tuyến IP.

Disjoint set / union-find (./16-disjoint-set-union-find.md) — theo dõi một phân hoạch các phần tử thành các nhóm và trả lời “hai phần tử này có cùng nhóm không?” trong thời gian gần như hằng số. Interface nhỏ, đòn bẩy khổng lồ: nó là thứ khiến thuật toán MST Kruskal và bài toán connectivity động trở nên khả thi.

Cấu trúcSearchInsertDeleteBảo đảm thêmBộ nhớ
BST (cân bằng)O(log n)O(log n)O(log n)duyệt sorted, range query, predecessor/successorO(n)
BST (mất cân bằng, worst)O(n)O(n)O(n)vẫn có, nhưng không đảm bảo thời gianO(n)
B-tree (bậc b)O(log_b n)O(log_b n)O(log_b n)ít lần đọc đĩa — node khớp một pageO(n)
Binary heapO(n) (key bất kỳ)O(log n)O(log n) extract-minpeek-min O(1), build từ array O(n)O(n), không pointer
Trie (bảng chữ cái cỡ s, key dài m)O(m)O(m)O(m)tìm theo tiền tố, thứ tự từ điểnO(n · m · s) worst
Graph — adjacency listO(deg v) kiểm tra cạnhO(1) thêm cạnhO(deg v)duyệt hàng xóm hiệu quảO(V + E)
Graph — adjacency matrixO(1) kiểm tra cạnhO(1)O(1)graph dày, thuật toán ma trậnO(V²)
Disjoint setO(α(n)) findO(α(n)) unionkhông cóα(n) ≤ 4 với mọi n thực tếO(n)

Chọn cấu trúc: bắt đầu từ các thao tác

Phương pháp đáng tin cậy không phải là học thuộc các bảng trên, mà là viết ra những thao tác mà workload của bạn thật sự thực hiện, kèm tần suất ước lượng, rồi đọc bảng để tìm cấu trúc làm cho các thao tác thường xuyên trở nên rẻ.

Bạn cần gìDùng gìVì sao
Truy cập theo chỉ số vào dãy kích thước cố định, duyệt nhiềuArraytruy cập O(1), hành vi cache tốt nhất
Tra cứu theo key, không quan tâm thứ tựHash tableO(1) trung bình
Tra cứu theo key duyệt theo thứ tự sắp xếp, hoặc range queryBalanced BST / skip listhash table không làm được range
Liên tục lấy phần tử nhỏ nhất/lớn nhất, xen kẽ với insertHeappeek O(1), cập nhật O(log n)
Thêm/xóa ở cả hai đầuDeque (collections.deque)O(1) cả hai đầu
Xử lý theo thứ tự đếnQueueFIFO
Undo, backtracking, lồng nhauStackLIFO
So khớp tiền tố trên chuỗiTrieO(m), tiền tố là bản chất
Thành viên nhóm / tính liên thôngDisjoint setunion và find gần như hằng số
Quan hệ giữa các thực thểGraphmọi thứ khác đều là trường hợp đặc biệt

Hai quy tắc kinh nghiệm tiết kiệm thời gian. Thứ nhất, nếu dữ liệu vừa memory và bạn không có yêu cầu về thứ tự, hãy thử hash table trước — nó đúng một cách đáng ngạc nhiên, và cũng là thứ rẻ nhất để thay đổi về sau. Thứ hai, nếu dữ liệu không vừa memory thì câu trả lời thường là B-tree hoặc LSM tree, vì mô hình chi phí đổi từ “số phép toán CPU” sang “số lần đọc đĩa”, và những cấu trúc có node cao và mảnh sẽ thua đậm (../../backend/vi/10-database-design-and-scaling.md).

Đây không phải chuyện hàn lâm — đây là thứ tạo nên công cụ bạn đang dùng

Mọi cấu trúc trong phần này đều đang chịu lực trong phần mềm bạn dùng hằng ngày:

Ở đâuCấu trúcMục đích
Database indexB-tree / B+ treelookup O(log n) với ít lần đọc đĩa (../../postgresql-dba/vi/08-indexing-strategies.md)
Redis / MemcachedHash table (+ skip list cho sorted set)tra key O(1) (../../backend/vi/11-caching.md)
LRU cacheHash table + doubly linked listlookup O(1) cập nhật độ mới O(1)
Thư mục trong filesystemB-tree, hash tablephân giải tên nhanh
GitDAG các commit, Merkle tree các objectlịch sử và địa chỉ hóa theo nội dung
CompilerAST (tree), symbol table (hash), CFG (graph)parsing, xác định scope, tối ưu
OS schedulerHeap hoặc red-black tree các task đang chờ chạychọn task kế tiếp theo độ ưu tiên
Router mạngTrie trên các tiền tố IPlongest-prefix match ở tốc độ đường truyền
Kiểm tra chính tả, autocompleteTrie, DAWGtìm theo tiền tố và tìm mờ
Undo trong editorStack (hoặc hai stack nếu có redo)lịch sử LIFO

Các built-in của Python thực chất là gì

Python giấu implementation sau những cái tên thân thiện, và sự lệch pha đó gây ra lỗi complexity thật sự. Hãy nắm bảng ánh xạ:

Kiểu PythonCấu trúc thật sựCần cẩn thận
listDynamic array chứa pointerinsert(0, x)pop(0)O(n); inO(n)
dictHash table open addressing, giữ thứ tự chèn từ 3.7worst case O(n); key phải hashable
set / frozensetHash table không có phần valuecác lưu ý giống dict
tupleArray kích thước cố địnhbất biến nên hashable — dùng làm key của dict được
collections.dequeDoubly linked list gồm các block cố địnhO(1) ở cả hai đầu; truy cập giữa là O(n)
heapq (trên một list)Binary min-heap trong arraykhông có max-heap sẵn — hãy đảo dấu key
bisect (trên list đã sorted)Binary searchsearch là O(log n) nhưng insort chèn vẫn O(n)
strArray bất biến các code pointdựng chuỗi bằng += trong vòng lặp là O(n²); dùng "".join()

Standard library không có sẵn balanced tree, linked list, trie hay graph. sortedcontainers là câu trả lời bên thứ ba thường dùng cho ordered map.

Best Practices

Tài liệu tham khảo

Part of the Data Structures & Algorithms Roadmap knowledge base.

Overview

A data structure is a way of organizing and storing data in a computer so that it can be used efficiently. That definition is accurate but bloodless. A more useful way to say it: a data structure is a deal you make with the machine. You agree to arrange your data a particular way, and in exchange certain operations become cheap — and, inevitably, others become expensive. There is no structure that makes everything fast. Choosing one is choosing which operations you care about.

Consider a concrete case. You have one million user records and you need to answer “is user X in this set?” a hundred thousand times. Store them in a plain array and each answer costs a scan of up to a million comparisons — 1011 operations total, several minutes of CPU. Store them in a hash table and each answer costs roughly one hash computation and one memory probe — 105 operations total, a few milliseconds. Same data, same question, same machine, same programmer. The only thing that changed is the arrangement, and it bought a speedup of five orders of magnitude. That is the entire argument for studying data structures.

This is also why the roadmap places data structures before algorithms. An algorithm is a sequence of steps, but the cost of each step depends on what the data is sitting in. Dijkstra’s algorithm is O(V²) with an array and O((V + E) log V) with a binary heap — the algorithm did not change, the structure underneath it did. You cannot reason about algorithmic cost at all until you know what your structure charges for each operation, which is why every note in this section pairs the structure with its complexity table.

Fundamentals

What exactly is a data structure

A data structure has three parts, and skipping any one of them leads to confusion later:

  1. A layout — how the data physically sits in memory. Contiguous cells? Nodes scattered on the heap linked by pointers? A fixed-size table of buckets?
  2. A set of operations — what you are allowed to do to it: insert, delete, search, get the minimum, iterate in sorted order.
  3. Invariants — properties that are always true between operations, and which the operations must preserve. “A heap’s parent is always ≤ its children.” “A BST’s left subtree holds only smaller keys.” “A hash table’s load factor never exceeds 0.75.” The invariant is what makes the operations fast; maintaining it is what the operations cost.

That third item is the one people skip, and it is the important one. Every structure in this section is really an invariant plus the machinery to keep it true. Once you know the invariant, you can usually re-derive the code.

Abstract data type versus implementation

An abstract data type (ADT) describes what a structure does — its operations and their meaning — without saying how. An implementation is a concrete structure that provides those operations. The distinction matters because a single ADT usually has several implementations with wildly different performance profiles:

ADTWhat it promisesCommon implementationsTrade-off
ListOrdered sequence, access by positionDynamic array, doubly linked listArray: O(1) index, O(n) middle insert. List: O(n) index, O(1) insert given a node
StackLIFO: push, pop, peekDynamic array, linked listArray is faster (cache); list never needs to resize
QueueFIFO: enqueue, dequeueCircular buffer, linked list, two stacksCircular buffer is O(1) and cache-friendly but fixed capacity
Map / DictionaryKey → value lookupHash table, balanced BST, skip listHash: O(1) average, unordered. BST: O(log n) worst, but sorted iteration and range queries
SetMembership testingHash set, tree set, bitsetSame trade-off as Map; bitset if keys are small dense integers
Priority QueueGet/remove the extreme elementBinary heap, pairing heap, sorted arrayHeap: O(log n) insert and extract, O(1) peek
GraphVertices and edgesAdjacency list, adjacency matrixList: O(V + E) space. Matrix: O(V²) space but O(1) edge test

The practical payoff: write your algorithm against the ADT, and you can swap implementations later without rewriting the algorithm. A shortest-path routine that only calls push, pop_min, and neighbours works with any priority queue and any graph representation. This is the abstraction principle from ./01-programming-fundamentals-and-pseudocode.md, applied.

# One ADT (Stack), two implementations, identical interface.

class ArrayStack:
    """Stack backed by a dynamic array. Cache-friendly, amortized O(1) push."""
    def __init__(self):
        self._items = []

    def push(self, x):
        self._items.append(x)          # amortized O(1); O(n) on the resize step

    def pop(self):
        if not self._items:
            raise IndexError("pop from empty stack")
        return self._items.pop()       # O(1) — removes from the end

    def peek(self):
        return self._items[-1]

    def __len__(self):
        return len(self._items)


class LinkedStack:
    """Stack backed by a singly linked list. True O(1) push, never resizes,
    but every node is a separate heap allocation."""
    class _Node:
        __slots__ = ("value", "next")
        def __init__(self, value, next):
            self.value = value
            self.next = next

    def __init__(self):
        self._head = None
        self._size = 0

    def push(self, x):
        self._head = self._Node(x, self._head)   # O(1), always
        self._size += 1

    def pop(self):
        if self._head is None:
            raise IndexError("pop from empty stack")
        node = self._head
        self._head = node.next
        self._size -= 1
        return node.value

    def peek(self):
        return self._head.value

    def __len__(self):
        return self._size


def check_balanced(text, stack):
    """Works with either implementation — it only uses the ADT."""
    pairs = {")": "(", "]": "[", "}": "{"}
    for ch in text:
        if ch in "([{":
            stack.push(ch)
        elif ch in pairs:
            if len(stack) == 0 or stack.pop() != pairs[ch]:
                return False
    return len(stack) == 0


assert check_balanced("([]{})", ArrayStack()) is True
assert check_balanced("([)]", LinkedStack()) is False

The two fundamental layouts

Almost every structure is built on one of two memory arrangements, and nearly all of their performance differences trace back to this choice.

Contiguous — one block of memory, elements at fixed offsets. The address of element i is base + i * item_size, computable with one multiply and one add, so indexing is O(1). Because elements are adjacent, a single cache line fetch brings in several of them, which makes sequential traversal extremely fast in practice — often 5–10× faster than the same number of scattered accesses, even though both are O(n) under the RAM model.

Linked — elements are separate allocations, each holding a pointer to the next. There is no arithmetic that finds element i; you must walk from the head, so indexing is O(n). But inserting between two nodes is just two pointer writes — no shifting, no reallocation — so it is O(1) when you already hold a reference to the position.

Contiguous (array):  one allocation, indices computable
  index:      0     1     2     3     4
            +-----+-----+-----+-----+-----+
            |  10 |  20 |  30 |  40 |  50 |
            +-----+-----+-----+-----+-----+
  address:  1000  1008  1016  1024  1032        addr(i) = 1000 + 8*i

Linked (singly linked list): many allocations, scattered
  head
   |
   v
 +------+----+     +------+----+     +------+----+
 |  10  | *--|---->|  20  | *--|---->|  30  | \  |
 +------+----+     +------+----+     +------+----+
  @0x7f2a            @0x91c4           @0x4e08     (no address arithmetic possible)

Everything else is a variation or a combination. A hash table is a contiguous array of buckets, where each bucket may be a linked chain. A B-tree is a tree of contiguous nodes, sized to a disk page. A deque is a linked list of contiguous blocks. Knowing which half of this diagram a structure lives in tells you most of its complexity table before you read it.

Why data structures matter — measured, not asserted

The roadmap says data structures “enhance performance” and “reduce complexity of code.” Both are true, but the second is underrated. Here is the same task written twice:

# Task: from a stream of events, report the first event ID that repeats.

# Version 1 — list as a "seen" collection. Correct, and quadratic.
def first_repeat_list(events):
    seen = []
    for e in events:
        if e in seen:          # O(len(seen)) linear scan on every iteration
            return e
        seen.append(e)
    return None                # total: O(n^2)

# Version 2 — set as a "seen" collection. Same shape, linear.
def first_repeat_set(events):
    seen = set()
    for e in events:
        if e in seen:          # O(1) average — one hash, one probe
            return e
        seen.add(e)
    return None                # total: O(n)

The two functions are the same length and equally readable. On n = 100_000 distinct events the first takes tens of seconds and the second takes hundredths of one. The lesson is that the performance decision was made by a single word — [] versus set() — and no amount of micro-optimizing the loop body could recover the difference. Picking the structure is the optimization; the rest is noise.

The code-simplicity claim is real too. Try writing “find the k smallest of a stream” without a heap, or “detect whether adding this edge creates a cycle” without a disjoint set. It can be done, but the code triples in length and grows several off-by-one bugs. The right structure does not just run faster; it makes the correct algorithm obvious.

Key Concepts

Classification

A few axes are worth naming because they come up constantly:

The basic data structures

These five are the vocabulary. Everything more complicated is built out of them.

Array (./04-arrays.md) — a contiguous block holding elements of the same type, addressed by integer index. Constant-time random access is its defining strength; the cost is that inserting or deleting anywhere but the end requires shifting the tail. Dynamic arrays (Python list, C++ vector, Go slices) add automatic growth by reallocating to a larger block, which is O(n) on the resize but amortized O(1) per append because resizes double the capacity and are therefore rare.

Linked list (./05-linked-lists.md) — nodes scattered in memory, each holding a value and a pointer to the next (and, in a doubly linked list, the previous). Insertion and deletion at a known position are O(1) with no shifting. The costs are real and often underestimated: no random access, one allocation and one pointer of overhead per element, and terrible cache behaviour. In practice a dynamic array beats a linked list for most workloads; the linked list wins when you splice frequently at positions you already hold, or when you need stable references to elements across mutations.

Stack (./06-stacks-and-queues.md) — LIFO (last in, first out). Only the most recent element is reachable. Its most important instance is not one you write: the call stack that every function invocation pushes a frame onto, which is why unbounded recursion produces a stack overflow. Stacks also drive expression evaluation, backtracking (./21-backtracking.md), iterative DFS, and undo history.

Queue (./06-stacks-and-queues.md) — FIFO (first in, first out). Elements leave in arrival order. Queues model anything that waits its turn: task scheduling, request buffering, print spooling, breadth-first search. Implemented well it is a circular buffer over an array; implemented badly it is a Python list with pop(0), which is O(n) per dequeue and silently turns a linear algorithm quadratic. Use collections.deque.

Hash table (./07-hash-tables.md) — stores key-value pairs in an array of buckets, using a hash function to convert a key into a bucket index. Average-case O(1) insert, delete, and lookup makes it the most generally useful structure in existence, and it is the default answer to “I need to look things up by name.” The caveats: worst case degrades to O(n) when many keys collide, it consumes more memory than the data alone (the table is deliberately kept partly empty), and it gives you no ordering.

StructureAccess by indexSearchInsertDeleteSpaceWorst case differs?
Static arrayO(1)O(n), O(log n) if sortedn/a (fixed)n/aO(n)no
Dynamic arrayO(1)O(n)O(1) amortized at end, O(n) elsewhereO(n) (O(1) at end)O(n)yes — a single append is O(n) when it triggers a resize
Singly linked listO(n)O(n)O(1) at head or at a held nodeO(1) at a held node, else O(n)O(n) + 1 pointer/elementno
Doubly linked listO(n)O(n)O(1) at a held nodeO(1) at a held nodeO(n) + 2 pointers/elementno
Stackn/aO(n)O(1) pushO(1) popO(n)array-backed: O(n) on resize
Queue (circular buffer)n/aO(n)O(1) enqueueO(1) dequeueO(n)array-backed: O(n) on resize
Hash tablen/aO(1) avgO(1) avgO(1) avgO(n) (with slack)yes — O(n) if all keys collide

The complex data structures

These solve problems the basic five cannot, generally by imposing more structure and paying for it during modification.

Tree (./10-tree-data-structures.md) — a hierarchy: one root, each node with zero or more children, no cycles. Trees model anything nested — file systems, DOM documents, org charts, parsed syntax. The binary search tree adds an ordering invariant (everything left is smaller, everything right is larger) that makes search, insert, and delete O(h) where h is the height.

Balanced tree (./11-balanced-and-multiway-trees.md) — the catch with a plain BST is that h can degrade to n: insert sorted data and you get a linked list wearing a tree costume, with O(n) operations. AVL trees, red-black trees, and B-trees add rebalancing to guarantee h = O(log n). B-trees in particular use wide, disk-page-sized nodes, which is exactly why they are the structure behind nearly every database index — see ../../postgresql-dba/en/08-indexing-strategies.md.

Heap (./12-heaps-and-priority-queues.md) — a binary tree with a weaker invariant than a BST: every parent is ≤ (or ≥) its children, with no ordering between siblings. That weakness is the point — it is cheap enough to maintain that insert and extract-min are O(log n) while peek-min is O(1), and the whole tree can be packed into a contiguous array with no pointers at all. It is the standard implementation of a priority queue and the engine inside Dijkstra, A*, heapsort, and k-way merges.

Graph (./13-graph-data-structures.md) — vertices connected by edges, with no restriction on shape: cycles allowed, multiple paths allowed, disconnected pieces allowed. Trees are the special case of a connected acyclic graph. Graphs model networks of every kind — roads, social connections, package dependencies, state machines — and carry the richest algorithm family in this roadmap (./14-shortest-path-algorithms.md, ./15-minimum-spanning-trees.md).

Trie (./17-advanced-tree-structures.md) — a tree keyed by string prefixes, where the path from the root spells the key. Lookup costs O(m) in the key length rather than O(log n) in the number of keys, and prefix queries (“all words starting with pre”) come free. This is what backs autocomplete and IP routing tables.

Disjoint set / union-find (./16-disjoint-set-union-find.md) — tracks a partition of elements into groups and answers “are these two in the same group?” in effectively constant time. Small interface, enormous leverage: it is what makes Kruskal’s MST algorithm and dynamic connectivity practical.

StructureSearchInsertDeleteExtra guaranteesSpace
BST (balanced)O(log n)O(log n)O(log n)sorted iteration, range queries, predecessor/successorO(n)
BST (unbalanced, worst)O(n)O(n)O(n)same, but no time guaranteeO(n)
B-tree (order b)O(log_b n)O(log_b n)O(log_b n)few disk reads — nodes match a pageO(n)
Binary heapO(n) (arbitrary key)O(log n)O(log n) extract-minO(1) peek-min, O(n) build from arrayO(n), no pointers
Trie (alphabet size s, key length m)O(m)O(m)O(m)prefix search, lexicographic orderO(n · m · s) worst
Graph — adjacency listO(deg v) edge testO(1) add edgeO(deg v)efficient neighbour iterationO(V + E)
Graph — adjacency matrixO(1) edge testO(1)O(1)dense graphs, matrix algorithmsO(V²)
Disjoint setO(α(n)) findO(α(n)) unionn/aα(n) ≤ 4 for any real nO(n)

Choosing one: start from the operations

The reliable method is not to memorize the tables but to write down the operations your workload actually performs, with rough frequencies, and then read the tables to find a structure that makes the frequent ones cheap.

What you needUseWhy
Index into a fixed-size sequence, iterate a lotArrayO(1) access, best cache behaviour
Look up by key, order irrelevantHash tableO(1) average
Look up by key and iterate in sorted order, or do range queriesBalanced BST / skip listhash tables cannot do ranges
Repeatedly get the smallest/largest, with inserts in betweenHeapO(1) peek, O(log n) update
Insert/remove at both endsDeque (collections.deque)O(1) both ends
Process in arrival orderQueueFIFO
Undo, backtracking, nestingStackLIFO
Prefix matching over stringsTrieO(m), prefix-native
Group membership / connectivityDisjoint setnear-constant union and find
Relationships between entitiesGrapheverything else is a special case

Two rules of thumb that save time. First, if the data fits in memory and you have no ordering requirement, try a hash table first — it is right surprisingly often, and it is the cheapest thing to change later. Second, if the data does not fit in memory, the answer is usually a B-tree or an LSM tree, because the cost model changes from “CPU operations” to “disk reads” and structures with tall, thin nodes lose badly (../../backend/en/10-database-design-and-scaling.md).

These are not academic — they are what your tools are made of

Every structure in this section is load-bearing in software you already use:

WhereStructurePurpose
Database indexB-tree / B+ treeO(log n) lookups with few disk reads (../../postgresql-dba/en/08-indexing-strategies.md)
Redis / MemcachedHash table (+ skip list for sorted sets)O(1) key lookup (../../backend/en/11-caching.md)
LRU cacheHash table + doubly linked listO(1) lookup and O(1) recency update
Filesystem directoriesB-tree, hash tablefast name resolution
GitDAG of commits, Merkle tree of objectshistory and content addressing
CompilersAST (tree), symbol table (hash), CFG (graph)parsing, scoping, optimization
OS schedulerHeap or red-black tree of runnable taskspick the next task by priority
Network routersTrie over IP prefixeslongest-prefix match at line rate
Spell checkers, autocompleteTrie, DAWGprefix and fuzzy search
Undo in your editorStack (or two stacks for redo)LIFO history

Python’s built-ins, and what they really are

Python hides implementations behind friendly names, and the mismatch causes real complexity mistakes. Know the mapping:

Python typeActual structureWatch out for
listDynamic array of pointersinsert(0, x) and pop(0) are O(n); in is O(n)
dictOpen-addressed hash table, insertion-ordered since 3.7worst case O(n); keys must be hashable
set / frozensetHash table without valuessame caveats as dict
tupleFixed-size arrayimmutable, so hashable — usable as a dict key
collections.dequeDoubly linked list of fixed-size blocksO(1) at both ends; indexing the middle is O(n)
heapq (on a list)Binary min-heap in an arrayno built-in max-heap — negate the keys
bisect (on a sorted list)Binary searchsearch is O(log n) but the insort insert is still O(n)
strImmutable array of code pointsbuilding with += in a loop is O(n²); use "".join()

There is no built-in balanced tree, linked list, trie, or graph in the standard library. sortedcontainers is the usual third-party answer for ordered maps.

Best Practices

References