Tại sao chúng ta sử dụng mảng thay vì các cấu trúc dữ liệu khác?


195

Khi tôi đang lập trình, tôi chưa thấy một trường hợp nào trong đó một mảng tốt hơn để lưu trữ thông tin hơn một dạng khác. Tôi thực sự đã tìm ra các "tính năng" được thêm vào trong các ngôn ngữ lập trình đã được cải thiện nhờ điều này và bằng cách thay thế chúng. Tôi thấy bây giờ họ không thay thế mà thay vào đó là cuộc sống mới.

Vì vậy, về cơ bản, quan điểm của việc sử dụng mảng là gì?

Đây không phải là quá nhiều lý do tại sao chúng ta sử dụng mảng từ quan điểm máy tính, nhưng tại sao chúng ta sẽ sử dụng mảng từ quan điểm lập trình (một sự khác biệt tinh tế). Những gì máy tính làm với mảng không phải là điểm của câu hỏi.


2
Tại sao không xem xét những gì máy tính làm với mảng? Chúng tôi có một hệ thống đánh số nhà vì chúng tôi có những con đường STRAIGHT . Vì vậy, nó là cho mảng.
lcn

" Cấu trúc dữ liệu khác " hoặc " hình thức khác " nghĩa là gì? Và vì mục đích gì?
tevemadar

Câu trả lời:


771

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.


1
@Jonathan: Bạn đã cập nhật sơ đồ để trỏ đến phần tử thứ 5 nhưng bạn cũng đã thay đổi MyArray [4] thành MyArray [5] để nó vẫn không chính xác, thay đổi chỉ số trở lại thành 4 và giữ nguyên sơ đồ và bạn sẽ ổn .
Robert Gamble

54
Đây là điều khiến tôi băn khoăn về "wiki cộng đồng" bài đăng này có giá trị "đúng" đại diện
Quibbledome

8
Câu trả lời tốt đẹp. Nhưng cây bạn mô tả là cây tìm kiếm nhị phân - cây nhị phân chỉ là cây mà mỗi nút có nhiều nhất là hai con. Bạn có thể có một cây nhị phân với các phần tử theo thứ tự bất kỳ. Cây tìm kiếm nhị phân được tổ chức như bạn mô tả.
gnud

1
Giải thích tốt, nhưng tôi không thể giúp nitpick ... nếu bạn được phép sắp xếp lại các mục vào cây tìm kiếm nhị phân, tại sao bạn không thể sắp xếp lại các phần tử trong mảng để tìm kiếm nhị phân cũng hoạt động trong đó? Bạn có thể đi vào chi tiết hơn về O (n) chèn / xóa cho một cây, nhưng O (n) cho một mảng.
thị trường

2
Không phải là cây nhị phân đại diện cho một O (log n) vì thời gian truy cập tăng logarit liên quan đến kích thước của tập dữ liệu?
Evan Plaice

73

Đối với truy cập ngẫu nhiên O (1), không thể đánh bại.


6
Về điểm nào? O (1) là gì? Truy cập ngẫu nhiên là gì? Tại sao nó không thể bị đánh? Điểm khác?
jason

3
O (1) có nghĩa là thời gian không đổi, ví dụ nếu bạn muốn lấy phần tử n-esim của một mảng, bạn chỉ cần truy cập trực tiếp qua bộ chỉ mục của nó (mảng [n-1]), với danh sách được liên kết chẳng hạn, bạn có để tìm cái đầu, rồi đi đến nút tiếp theo tuần tự n-1 lần đó là O (n), thời gian tuyến tính.
CMS

8
Ký hiệu Big-O mô tả tốc độ của thuật toán thay đổi như thế nào dựa trên kích thước của đầu vào. Một thuật toán O (n) sẽ mất gấp đôi thời gian để chạy với số lượng vật phẩm gấp đôi và gấp 8 lần thời gian để chạy với số lượng vật phẩm gấp 8 lần. Nói cách khác, tốc độ của thuật toán O (n) thay đổi theo [tiếp ...]
Gareth

8
kích thước của đầu vào của nó. O (1) ngụ ý rằng kích thước của đầu vào ('n') không ảnh hưởng đến tốc độ của thuật toán, đó là tốc độ không đổi bất kể kích thước đầu vào
Gareth

9
Tôi thấy O (1) của bạn và nâng bạn O (0).
Chris Conway

23

Không phải tất cả các chương trình làm điều tương tự hoặc chạy trên cùng một phần cứng.

Đây thường là câu trả lời tại sao các tính năng ngôn ngữ khác nhau tồn tại. Mảng là một khái niệm khoa học máy tính cốt lõi. Việc thay thế các mảng bằng danh sách / ma trận / vectơ / bất kỳ cấu trúc dữ liệu nâng cao nào sẽ ảnh hưởng nghiêm trọng đến hiệu suất và hoàn toàn không thể thực hiện được trong một số hệ thống. Có bất kỳ số lượng các trường hợp sử dụng một trong các đối tượng thu thập dữ liệu "nâng cao" này vì chương trình được đề cập.

Trong lập trình kinh doanh (điều mà hầu hết chúng ta làm), chúng ta có thể nhắm mục tiêu phần cứng tương đối mạnh. Sử dụng Danh sách trong C # hoặc Vector trong Java là lựa chọn phù hợp để thực hiện trong các tình huống này vì các cấu trúc này cho phép nhà phát triển hoàn thành các mục tiêu nhanh hơn, do đó cho phép loại phần mềm này nổi bật hơn.

Khi viết phần mềm nhúng hoặc hệ điều hành, một mảng thường có thể là lựa chọn tốt hơn. Mặc dù một mảng cung cấp ít chức năng hơn, nó chiếm ít RAM hơn và trình biên dịch có thể tối ưu hóa mã hiệu quả hơn để tra cứu thành mảng.

Tôi chắc chắn rằng tôi đang bỏ qua một số lợi ích cho những trường hợp này, nhưng tôi hy vọng bạn có được điểm.


4
Trớ trêu thay, trong Java, bạn nên sử dụng ArrayList (hoặc LinkedList) thay vì Vector. Điều này là để làm với một vectơ được đồng bộ hóa thường là chi phí không cần thiết.
ashirley

0

Một cách để xem xét các lợi thế của mảng là xem khả năng truy cập O (1) của mảng là bắt buộc và do đó được viết hoa:

  1. Trong các bảng tra cứu ứng dụng của bạn (một mảng tĩnh để truy cập các phản hồi phân loại nhất định)

  2. Ghi nhớ (đã tính kết quả hàm phức tạp, do đó bạn không tính lại giá trị hàm, giả sử log x)

  3. Các ứng dụng thị giác máy tính tốc độ cao yêu cầu xử lý hình ảnh ( https://en.wikipedia.org/wiki/Lookup_table#Lookup_tables_in_image_ Processing )

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.