Thời gian để quay ngược thời gian cho một bài học. Mặc dù chúng ta không nghĩ về những điều này nhiều trong các ngôn ngữ được quản lý ưa thích của chúng ta ngày nay, chúng được xây dựng trên cùng một nền tảng, vì vậy hãy xem cách quản lý bộ nhớ trong C.
Trước khi tôi đi sâu vào, một lời giải thích nhanh về thuật ngữ " con trỏ " nghĩa là gì. Một con trỏ chỉ đơn giản là một biến "trỏ" đến một vị trí trong bộ nhớ. Nó không chứa giá trị thực tại vùng nhớ này, nó chứa địa chỉ bộ nhớ cho nó. Hãy nghĩ về một khối bộ nhớ như một hộp thư. Con trỏ sẽ là địa chỉ của hộp thư đó.
Trong C, một mảng chỉ đơn giản là một con trỏ có phần bù, phần bù chỉ định khoảng cách trong bộ nhớ. Điều này cung cấp thời gian truy cập O (1) .
MyArray [5]
^ ^
Pointer Offset
Tất cả các cấu trúc dữ liệu khác đều dựa trên điều này hoặc không sử dụng bộ nhớ liền kề để lưu trữ, dẫn đến thời gian truy cập ngẫu nhiên kém (Mặc dù có những lợi ích khác khi không sử dụng bộ nhớ tuần tự).
Ví dụ: giả sử chúng ta có một mảng có 6 số (6,4,2,3,1,5) trong đó, trong bộ nhớ sẽ trông như thế này:
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
Trong một mảng, chúng ta biết rằng mỗi phần tử nằm cạnh nhau trong bộ nhớ. Mảng AC (Được gọi MyArray
ở đây) chỉ đơn giản là một con trỏ đến phần tử đầu tiên:
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
^
MyArray
Nếu chúng tôi muốn tìm kiếm MyArray[4]
, bên trong nó sẽ được truy cập như thế này:
0 1 2 3 4
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
^
MyArray + 4 ---------------/
(Pointer + Offset)
Vì chúng ta có thể truy cập trực tiếp vào bất kỳ phần tử nào trong mảng bằng cách thêm phần bù vào con trỏ, chúng ta có thể tra cứu bất kỳ phần tử nào trong cùng một khoảng thời gian, bất kể kích thước của mảng. Điều này có nghĩa là việc nhận được MyArray[1000]
sẽ mất cùng thời gian như nhận được MyArray[5]
.
Một cấu trúc dữ liệu thay thế là một danh sách liên kết. Đây là danh sách tuyến tính của các con trỏ, mỗi trỏ tới nút tiếp theo
======== ======== ======== ======== ========
| Data | | Data | | Data | | Data | | Data |
| | -> | | -> | | -> | | -> | |
| P1 | | P2 | | P3 | | P4 | | P5 |
======== ======== ======== ======== ========
P(X) stands for Pointer to next node.
Lưu ý rằng tôi đã tạo mỗi "nút" thành một khối riêng. Điều này là do chúng không được đảm bảo (và rất có thể sẽ không) liền kề trong bộ nhớ.
Nếu tôi muốn truy cập P3, tôi không thể truy cập trực tiếp vì tôi không biết nó nằm ở đâu trong bộ nhớ. Tất cả những gì tôi biết là gốc (P1) nằm ở đâu, vì vậy thay vào đó tôi phải bắt đầu ở P1 và theo từng con trỏ đến nút mong muốn.
Đây là thời gian tra cứu O (N) (Chi phí tra cứu tăng khi mỗi yếu tố được thêm vào). Nó đắt hơn nhiều để đến P1000 so với P4.
Các cấu trúc dữ liệu cấp cao hơn, chẳng hạn như hashtables, ngăn xếp và hàng đợi, tất cả có thể sử dụng một mảng (hoặc nhiều mảng) trong nội bộ, trong khi Danh sách liên kết và Cây nhị phân thường sử dụng các nút và con trỏ.
Bạn có thể tự hỏi tại sao mọi người sẽ sử dụng cấu trúc dữ liệu yêu cầu truyền tải tuyến tính để tìm kiếm một giá trị thay vì chỉ sử dụng một mảng, nhưng chúng có công dụng của chúng.
Lấy mảng của chúng tôi một lần nữa. Lần này, tôi muốn tìm phần tử mảng chứa giá trị '5'.
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
^ ^ ^ ^ ^ FOUND!
Trong tình huống này, tôi không biết nên bù vào con trỏ nào để tìm nó, vì vậy tôi phải bắt đầu từ 0 và tiếp tục tìm đến khi tìm thấy nó. Điều này có nghĩa là tôi phải thực hiện 6 kiểm tra.
Do đó, việc tìm kiếm một giá trị trong một mảng được coi là O (N). Chi phí tìm kiếm tăng lên khi mảng trở nên lớn hơn.
Hãy nhớ ở trên, nơi tôi đã nói rằng đôi khi sử dụng cấu trúc dữ liệu không tuần tự có thể có lợi thế? Tìm kiếm dữ liệu là một trong những lợi thế này và một trong những ví dụ tốt nhất là Cây nhị phân.
Cây nhị phân là một cấu trúc dữ liệu tương tự như một danh sách được liên kết, tuy nhiên thay vì liên kết đến một nút, mỗi nút có thể liên kết với hai nút con.
==========
| Root |
==========
/ \
========= =========
| Child | | Child |
========= =========
/ \
========= =========
| Child | | Child |
========= =========
Assume that each connector is really a Pointer
Khi dữ liệu được chèn vào cây nhị phân, nó sử dụng một số quy tắc để quyết định nơi đặt nút mới. Khái niệm cơ bản là nếu giá trị mới lớn hơn cha mẹ, nó sẽ chèn nó sang bên trái, nếu nó thấp hơn, nó sẽ chèn nó sang bên phải.
Điều này có nghĩa là các giá trị trong cây nhị phân có thể trông như thế này:
==========
| 100 |
==========
/ \
========= =========
| 200 | | 50 |
========= =========
/ \
========= =========
| 75 | | 25 |
========= =========
Khi tìm kiếm cây nhị phân cho giá trị 75, chúng ta chỉ cần truy cập 3 nút (O (log N)) vì cấu trúc này:
- Là 75 dưới 100? Nhìn vào nút phải
- Là 75 lớn hơn 50? Nhìn vào nút trái
- Có 75!
Mặc dù có 5 nút trong cây của chúng tôi, chúng tôi không cần nhìn vào hai nút còn lại, vì chúng tôi biết rằng chúng (và con của chúng) không thể chứa giá trị mà chúng tôi đang tìm kiếm. Điều này cho chúng ta thời gian tìm kiếm rằng trong trường hợp xấu nhất có nghĩa là chúng ta phải truy cập mọi nút, nhưng trong trường hợp tốt nhất, chúng ta chỉ phải truy cập một phần nhỏ của các nút.
Đó là nơi các mảng được đánh bại, chúng cung cấp thời gian tìm kiếm O (N) tuyến tính, mặc dù thời gian truy cập O (1).
Đây là một tổng quan cực kỳ cao về cấu trúc dữ liệu trong bộ nhớ, bỏ qua rất nhiều chi tiết, nhưng hy vọng nó minh họa điểm mạnh và điểm yếu của một mảng so với các cấu trúc dữ liệu khác.