Những loại thuật toán yêu cầu một bộ?


10

Trong các khóa học lập trình đầu tiên của tôi, tôi đã nói rằng tôi nên sử dụng một bộ bất cứ khi nào tôi cần làm những việc như loại bỏ các bản sao của một cái gì đó. Ví dụ: để xóa tất cả các mục trùng lặp khỏi một vectơ, lặp qua vectơ đã nói và thêm từng phần tử vào một tập hợp, sau đó bạn sẽ để lại các lần xuất hiện duy nhất. Tuy nhiên, tôi cũng có thể làm điều đó bằng cách thêm từng phần tử vào một vectơ khác và kiểm tra xem phần tử đó đã tồn tại chưa. Tôi cho rằng tùy thuộc vào ngôn ngữ được sử dụng, có thể có sự khác biệt về hiệu suất. Nhưng có một lý do để sử dụng một bộ khác hơn?

Về cơ bản: những loại thuật toán nào yêu cầu một bộ và không nên được thực hiện với bất kỳ loại container nào khác?


2
Bạn có thể nói cụ thể hơn về ý của bạn khi bạn sử dụng thuật ngữ "set?" Bạn đang đề cập đến một bộ C ++?
Robert Harvey

Đúng, thực ra, định nghĩa "tập hợp" dường như khá giống nhau trong hầu hết các ngôn ngữ: một thùng chứa chỉ chấp nhận các yếu tố duy nhất.
Floella

6
"Thêm từng phần tử vào một vectơ khác và kiểm tra xem phần tử đó đã tồn tại chưa" - đây chỉ là tự thực hiện một tập hợp. Vì vậy, bạn đang hỏi tại sao sử dụng một tính năng tích hợp khi bạn có thể tự viết một cái?
JacquesB

Câu trả lời:


8

Bạn đang hỏi về các bộ cụ thể nhưng tôi nghĩ câu hỏi của bạn là về một khái niệm lớn hơn: trừu tượng hóa. Bạn hoàn toàn chính xác rằng bạn có thể sử dụng Vector để làm điều này (nếu bạn đang sử dụng Java, thay vào đó hãy sử dụng ArrayList.) Nhưng tại sao dừng lại ở đó? Bạn cần Vector để làm gì? Bạn có thể làm tất cả điều này với mảng.

Khi bạn cần thêm một mục vào mảng, bạn có thể chỉ cần lặp qua mọi phần tử và nếu nó không ở đó, bạn sẽ thêm nó vào cuối. Nhưng, thực ra, trước tiên bạn cần kiểm tra xem có chỗ nào trong mảng không. Nếu không, bạn sẽ cần tạo một mảng mới lớn hơn và sao chép tất cả các phần tử hiện có từ mảng cũ sang mảng mới và sau đó bạn có thể thêm phần tử mới. Tất nhiên, bạn cũng cần cập nhật mọi tham chiếu đến mảng cũ để trỏ đến mảng mới. Có tất cả những gì đã làm? Tuyệt quá! Bây giờ những gì chúng ta đã cố gắng để thực hiện một lần nữa?

Hoặc, thay vào đó, bạn có thể sử dụng một thể hiện Set và chỉ cần gọi add(). Lý do mà các bộ tồn tại là vì chúng là một sự trừu tượng có ích cho nhiều vấn đề phổ biến. Ví dụ: giả sử bạn muốn theo dõi các mục và phản ứng khi một mục mới được thêm vào. Bạn gọi add()một bộ và nó trả về truehoặc falsedựa trên việc bộ đó đã được sửa đổi. Bạn có thể viết tất cả bằng tay bằng cách sử dụng nguyên thủy nhưng tại sao?

Thực sự có thể có trường hợp bạn có Danh sách và bạn muốn xóa các bản sao. Thuật toán bạn đề xuất về cơ bản là cách chậm nhất bạn có thể làm điều đó. Có một vài cách nhanh hơn phổ biến: xô chúng hoặc sắp xếp chúng. Hoặc, bạn có thể thêm chúng vào một tập hợp thực hiện một trong những thuật toán đó.

Ngay từ sớm trong sự nghiệp / giáo dục của bạn, trọng tâm là xây dựng các thuật toán này và hiểu chúng và điều quan trọng là phải làm điều đó. Nhưng đó không phải là những gì các nhà phát triển chuyên nghiệp làm trên cơ sở bình thường. Họ sử dụng các phương pháp này để xây dựng những điều thú vị hơn nhiều và sử dụng các triển khai được xây dựng sẵn và đáng tin cậy giúp tiết kiệm thời gian cho thuyền.


23

Tôi cho rằng tùy thuộc vào ngôn ngữ được sử dụng, có thể có sự khác biệt về hiệu suất. Nhưng có một lý do để sử dụng một bộ khác hơn?

Ồ vâng, (nhưng đó không phải là hiệu suất.)

Sử dụng một bộ khi bạn có thể sử dụng một vì không sử dụng nó có nghĩa là bạn phải viết thêm mã. Sử dụng một bộ làm cho những gì bạn làm dễ đọc. Tất cả những thử nghiệm cho logic duy nhất được ẩn giấu ở một nơi khác mà bạn không phải suy nghĩ về nó. Nó ở một nơi đã được thử nghiệm và bạn có thể tin tưởng rằng nó hoạt động.

Viết mã của riêng bạn để làm điều đó và bạn phải lo lắng về nó. Thôi nào. Ai muốn làm điều đó?

Về cơ bản: những loại thuật toán nào yêu cầu một bộ và không nên được thực hiện với bất kỳ loại container nào khác?

Không có thuật toán "không nên thực hiện với bất kỳ loại container nào khác". Có những thuật toán đơn giản có thể tận dụng các bộ. Thật tuyệt khi bạn không phải viết thêm mã.

Bây giờ không có gì đặc biệt về thiết lập về vấn đề này. Bạn nên luôn luôn sử dụng bộ sưu tập phù hợp nhất với nhu cầu của bạn. Trong java tôi đã tìm thấy hình ảnh này hữu ích trong việc đưa ra quyết định. Bạn sẽ nhận thấy rằng nó có ba loại bộ khác nhau.

nhập mô tả hình ảnh ở đây

Và như @germi đã chỉ ra một cách đúng đắn, nếu bạn sử dụng đúng bộ sưu tập cho công việc, mã của bạn sẽ trở nên dễ đọc hơn.


6
Bạn đã đề cập đến nó rồi, nhưng sử dụng một bộ cũng giúp người khác dễ dàng suy luận về mã hơn; họ không cần phải nhìn vào cách nó được phổ biến để biết rằng nó chỉ chứa các mặt hàng độc đáo.
mầm

14

Tuy nhiên, tôi cũng có thể làm điều đó bằng cách thêm từng phần tử vào một vectơ khác và kiểm tra xem phần tử đó đã tồn tại chưa.

Nếu bạn làm điều đó, thì bạn đang thực hiện ngữ nghĩa của một tập hợp trên cơ sở hạ tầng vector. Bạn đang viết thêm mã (có thể chứa lỗi) và kết quả sẽ cực kỳ chậm nếu bạn có nhiều mục nhập.

Tại sao bạn muốn làm điều đó hơn bằng cách sử dụng một triển khai thiết lập hiệu quả, đã được thử nghiệm?


6

Các thực thể phần mềm đại diện cho các thực thể trong thế giới thực thường là các tập hợp logic. Ví dụ, hãy xem xét một chiếc xe hơi. Ô tô có định danh duy nhất và nhóm xe tạo thành một bộ. Khái niệm tập hợp đóng vai trò là một ràng buộc đối với bộ sưu tập Ô tô mà một chương trình có thể biết và ràng buộc các giá trị dữ liệu là rất có giá trị.

Ngoài ra, các bộ có một đại số được xác định rất tốt. Nếu bạn có một bộ Ô tô thuộc sở hữu của George và một bộ thuộc sở hữu của Alice, thì liên minh rõ ràng là bộ thuộc sở hữu của cả George và Alice ngay cả khi cả George và Alice đều sở hữu cùng một chiếc xe. Vì vậy, các thuật toán nên sử dụng các tập hợp là những thuật toán trong đó logic của các thực thể liên quan đến các đặc điểm của bộ triển lãm. Điều đó hóa ra khá phổ biến.

Làm thế nào các bộ được thực hiện và làm thế nào ràng buộc tính duy nhất được đảm bảo là một vấn đề khác. Người ta hy vọng có thể tìm thấy một triển khai phù hợp cho logic tập hợp loại bỏ các trùng lặp được đưa ra cho các tập hợp đó rất cơ bản với logic, nhưng ngay cả khi bạn tự thực hiện, đảm bảo tính duy nhất là nội tại đối với việc chèn một mục trong một tập hợp và bạn không cần phải "kiểm tra xem phần tử đã tồn tại chưa".


"Kiểm tra nếu nó đã tồn tại" thường rất cần thiết cho sự trùng lặp. Thông thường các đối tượng được tạo ra từ dữ liệu. Và bạn chỉ muốn một đối tượng cho dữ liệu giống hệt nhau, được sử dụng lại bởi bất kỳ ai tạo ra một đối tượng từ cùng một dữ liệu. Vì vậy, bạn tạo một đối tượng mới, kiểm tra xem nó có trong tập hợp không, nếu nó ở đó bạn lấy đối tượng từ tập hợp, nếu không bạn chèn đối tượng của mình. Nếu bạn chỉ chèn đối tượng, bạn vẫn sẽ có rất nhiều đối tượng giống hệt nhau.
gnasher729

1
@ gnasher729 trách nhiệm của người triển khai Set bao gồm kiểm tra sự tồn tại, nhưng người dùng của Set có thể for 1..100: set.insert(10)và vẫn biết rằng chỉ có một số 10 trong bộ
Caleth

Người dùng có thể tạo một trăm đối tượng khác nhau trong mười nhóm đối tượng bằng nhau. Sau khi chèn có mười đối tượng trong tập hợp, nhưng 100 đối tượng vẫn nổi xung quanh. Không trùng lặp có nghĩa là có mười đối tượng trong tập hợp và mọi người đều sử dụng mười đối tượng đó. Rõ ràng bạn không chỉ cần một bài kiểm tra - bạn cần một hàm đưa ra một đối tượng, trả về đối tượng phù hợp trong tập hợp.
gnasher729

4

Ngoài các đặc điểm hiệu suất (rất quan trọng và không nên dễ dàng bị loại bỏ), Bộ rất quan trọng như một bộ sưu tập trừu tượng.

Bạn có thể mô phỏng Set behavior (bỏ qua hiệu năng) với Mảng không? Chắc chắn rồi! Mỗi lần bạn chèn, bạn có thể kiểm tra xem phần tử đã có trong mảng chưa, và sau đó chỉ thêm phần tử nếu nó chưa được tìm thấy. Nhưng đó là điều mà bạn có ý thức phải nhận thức và ghi nhớ mỗi khi bạn chèn vào Array-Psuedo-Set. Ồ, cái gì vậy, bạn đã chèn một lần trực tiếp, mà không kiểm tra trùng lặp trước? Welp, mảng của bạn đã phá vỡ tính bất biến của nó (rằng tất cả các phần tử là duy nhất và tương đương, không tồn tại trùng lặp).

Vì vậy, bạn sẽ làm gì để có được xung quanh đó? Bạn sẽ tạo một kiểu dữ liệu mới, gọi nó (giả sử PsuedoSet), bao bọc một mảng bên trong và hiển thị một inserthoạt động công khai, điều này sẽ thực thi tính duy nhất của các phần tử. Vì mảng được bao bọc chỉ có thể truy cập thông qua insertAPI công khai này , bạn đảm bảo rằng các bản sao không bao giờ có thể xuất hiện. Bây giờ thêm một số băm để cải thiện hiệu suất của containskiểm tra, và sớm hay muộn bạn sẽ nhận ra rằng bạn đã thực hiện đầy đủ Set.

Tôi cũng sẽ trả lời với một tuyên bố và theo dõi câu hỏi:

Trong các khóa học lập trình đầu tiên của tôi, tôi đã nói rằng tôi nên sử dụng một Array bất cứ khi nào tôi cần làm những việc như lưu trữ nhiều yếu tố được đặt hàng của một cái gì đó. Ví dụ: để lưu trữ một bộ sưu tập tên của đồng nghiệp. Tuy nhiên, tôi cũng có thể làm điều đó bằng cách phân bổ bộ nhớ thô và đặt giá trị của địa chỉ bộ nhớ được cung cấp bởi con trỏ bắt đầu + một số bù.

Bạn có thể sử dụng một con trỏ thô và offset cố định để bắt chước một Array không? Chắc chắn rồi! Mỗi lần bạn chèn, bạn có thể kiểm tra xem phần bù không đi ra khỏi phần cuối của bộ nhớ được phân bổ mà bạn đang làm việc. Nhưng đó là điều mà bạn có ý thức phải nhận thức và ghi nhớ mỗi khi bạn chèn vào Pseudo-Array của mình. Ồ, cái gì vậy, bạn đã chèn một lần trực tiếp, mà không kiểm tra phần bù trước? Welp, có lỗi Phân đoạn với tên của bạn trên đó!

Vì vậy, bạn sẽ làm gì để có được xung quanh đó? Bạn sẽ tạo một kiểu dữ liệu mới, gọi nó (giả sử PsuedoArray), bao bọc một con trỏ và kích thước, và hiển thị một inserthoạt động công khai, điều này sẽ thực thi rằng phần bù không vượt quá kích thước. Vì dữ liệu được bọc chỉ có thể truy cập thông qua insertAPI công khai này , bạn đảm bảo rằng không có lỗi tràn bộ đệm nào có thể xảy ra. Bây giờ thêm một số chức năng tiện lợi khác (Thay đổi kích thước mảng, xóa phần tử, v.v.) và sớm hay muộn bạn sẽ nhận ra rằng bạn đã triển khai toàn bộ Array.


3

Có tất cả các loại thuật toán dựa trên tập hợp, đặc biệt là nơi bạn cần thực hiện các giao điểm và liên hiệp của các tập hợp và có kết quả là một tập hợp.

Đặt thuật toán dựa trên được sử dụng nhiều trong các thuật toán tìm đường khác nhau, v.v.

Để biết sơ lược về lý thuyết tập hợp, hãy xem liên kết này: http://people.umass.edu/partee/NZ_2006/set%20Theory%20Basics.pdf

Nếu bạn cần thiết lập ngữ nghĩa, sử dụng một bộ. Điều này sẽ tránh được các lỗi do trùng lặp giả vì bạn quên cắt xén vectơ / danh sách ở một giai đoạn nào đó và nó sẽ nhanh hơn bạn có thể làm bằng cách liên tục cắt xén vectơ / danh sách của bạn.


1

Tôi thực sự tìm thấy các bộ chứa tiêu chuẩn hầu như là vô dụng và tôi chỉ thích sử dụng mảng nhưng tôi làm theo cách khác.

Để tính toán các giao điểm, tôi lặp qua mảng đầu tiên và đánh dấu các phần tử bằng một bit đơn. Sau đó, tôi lặp qua mảng thứ hai và tìm kiếm các phần tử được đánh dấu. Voila, thiết lập giao điểm trong thời gian tuyến tính với công việc và bộ nhớ ít hơn nhiều so với bảng băm, ví dụ: Liên minh và khác biệt cũng đơn giản như nhau để áp dụng phương pháp này. Nó giúp ích cho cơ sở mã của tôi xoay quanh các phần tử lập chỉ mục thay vì sao chép chúng (tôi sao chép các chỉ mục thành các phần tử, không phải dữ liệu của chính các phần tử) và hiếm khi cần sắp xếp mọi thứ, nhưng tôi đã sử dụng cấu trúc dữ liệu được thiết lập trong nhiều năm như một kết quả.

Tôi cũng có một số mã C khó hiểu mà tôi sử dụng ngay cả khi các phần tử không cung cấp trường dữ liệu cho các mục đích như vậy. Nó liên quan đến việc sử dụng bộ nhớ của các phần tử bằng cách đặt bit quan trọng nhất (mà tôi không bao giờ sử dụng) cho mục đích đánh dấu các phần tử đi qua. Điều đó khá thô thiển, đừng làm điều đó trừ khi bạn thực sự làm việc ở cấp độ lắp ráp gần, nhưng chỉ muốn đề cập đến cách áp dụng nó ngay cả trong trường hợp khi các yếu tố không cung cấp một số trường cụ thể cho giao dịch nếu bạn có thể đảm bảo rằng một số bit nhất định sẽ không bao giờ được sử dụng. Nó có thể tính toán một giao điểm được thiết lập giữa 200 triệu phần tử (bout 2,4 gigs dữ liệu) trong chưa đầy một giây trên i7 dinky của tôi. Hãy thử thực hiện một giao điểm được thiết lập giữa hai std::settrường hợp chứa một trăm triệu phần tử mỗi phần; thậm chí không đến gần.

Qua bên đó...

Tuy nhiên, tôi cũng có thể làm điều đó bằng cách thêm từng phần tử vào một vectơ khác và kiểm tra xem phần tử đó đã tồn tại chưa.

Việc kiểm tra xem liệu một phần tử đã tồn tại trong vectơ mới nói chung sẽ là một hoạt động thời gian tuyến tính, điều này sẽ làm cho giao điểm tập hợp trở thành một phép toán bậc hai (khối lượng công việc bùng nổ kích thước đầu vào càng lớn). Tôi khuyên bạn nên sử dụng kỹ thuật trên nếu bạn chỉ muốn sử dụng các vectơ hoặc mảng cũ đơn giản và thực hiện theo cách có tỷ lệ tuyệt vời.

Về cơ bản: những loại thuật toán nào yêu cầu một bộ và không nên được thực hiện với bất kỳ loại container nào khác?

Không có gì nếu bạn hỏi ý kiến ​​thiên vị của tôi nếu bạn đang nói về nó ở cấp độ chứa (như trong cấu trúc dữ liệu được triển khai cụ thể để cung cấp các hoạt động tập hợp một cách hiệu quả), nhưng có rất nhiều yêu cầu thiết lập logic ở cấp độ khái niệm. Ví dụ: giả sử bạn muốn tìm các sinh vật trong một thế giới trò chơi có khả năng vừa bay vừa bơi, và bạn có các sinh vật bay trong một bộ (cho dù bạn có thực sự sử dụng một bộ chứa) hay không và có thể bơi trong một bộ khác . Trong trường hợp đó, bạn muốn một giao lộ được thiết lập. Nếu bạn muốn những sinh vật có thể bay hoặc là ma thuật, thì bạn sử dụng một tập hợp. Tất nhiên, bạn thực sự không cần một bộ chứa để thực hiện điều này và việc triển khai tối ưu nhất thường không cần hoặc không muốn một bộ chứa được thiết kế đặc biệt là một bộ.

Tiếp tục đi

Được rồi, tôi đã nhận được một số câu hỏi hay từ JimmyJames liên quan đến phương pháp giao lộ được thiết lập này. Đó là loại bỏ chủ đề nhưng ồ, tôi rất thích thấy nhiều người sử dụng phương pháp xâm nhập cơ bản này để đặt giao lộ để họ không xây dựng toàn bộ cấu trúc phụ trợ như cây nhị phân cân bằng và bảng băm chỉ nhằm mục đích thiết lập các hoạt động. Như đã đề cập, yêu cầu cơ bản là các danh sách các phần tử sao chép nông để chúng được lập chỉ mục hoặc trỏ đến một phần tử được chia sẻ có thể được "đánh dấu" khi đi qua danh sách hoặc mảng chưa được sắp xếp đầu tiên hoặc bất cứ thứ gì để chọn vào phần thứ hai vượt qua danh sách thứ hai.

Tuy nhiên, điều này có thể được thực hiện thực tế ngay cả trong bối cảnh đa luồng mà không cần chạm vào các yếu tố với điều kiện:

  1. Hai tập hợp chứa các chỉ số cho các phần tử.
  2. Phạm vi của các chỉ số không quá lớn (giả sử [0, 2 ^ 26), không phải hàng tỷ hoặc nhiều hơn) và được chiếm giữ một cách hợp lý.

Điều này cho phép chúng ta sử dụng một mảng song song (chỉ một bit cho mỗi phần tử) cho mục đích thiết lập các hoạt động. Biểu đồ:

nhập mô tả hình ảnh ở đây

Đồng bộ hóa luồng chỉ cần ở đó khi có được một mảng bit song song từ nhóm và giải phóng nó trở lại nhóm (được thực hiện ngầm khi đi ra khỏi phạm vi). Hai vòng lặp thực tế để thực hiện thao tác thiết lập không cần bất kỳ đồng bộ hóa luồng nào. Chúng ta thậm chí không cần sử dụng nhóm bit song song nếu luồng chỉ có thể phân bổ và giải phóng các bit cục bộ, nhưng nhóm bit có thể thuận tiện để khái quát mẫu trong các cơ sở mã phù hợp với kiểu biểu diễn dữ liệu này trong đó các phần tử trung tâm thường được tham chiếu theo chỉ mục để mỗi luồng không phải bận tâm đến việc quản lý bộ nhớ hiệu quả. Các ví dụ cơ bản cho khu vực của tôi là các hệ thống thành phần thực thể và các biểu diễn lưới được lập chỉ mục. Cả hai thường xuyên cần thiết lập các giao điểm và có xu hướng đề cập đến mọi thứ được lưu trữ tập trung (các thành phần và thực thể trong ECS ​​và các đỉnh, cạnh,

Nếu các chỉ mục không bị chiếm đóng dày đặc và rải rác rải rác, thì điều này vẫn có thể áp dụng với việc triển khai thưa thớt hợp lý của mảng bit / boolean song song, giống như một chỉ lưu trữ bộ nhớ trong các đoạn 512 bit (64 byte cho mỗi nút không được kiểm soát đại diện cho 512 chỉ mục liền kề ) và bỏ qua phân bổ các khối liền kề hoàn toàn bỏ trống. Có thể bạn đang sử dụng một cái gì đó như thế này nếu cấu trúc dữ liệu trung tâm của bạn bị chiếm giữ bởi các yếu tố.

nhập mô tả hình ảnh ở đây

... ý tưởng tương tự cho một bitet thưa thớt để phục vụ như một mảng bit song song. Các cấu trúc này cũng cho vay theo hướng bất biến vì dễ dàng sao chép các khối chunky nông mà không cần phải sao chép sâu để tạo ra một bản sao bất biến mới.

Một lần nữa thiết lập các giao điểm giữa hàng trăm triệu phần tử có thể được thực hiện trong một giây bằng cách sử dụng phương pháp này trên một máy rất trung bình và đó là trong một luồng.

Nó cũng có thể được thực hiện dưới một nửa thời gian nếu khách hàng không cần một danh sách các yếu tố cho giao điểm kết quả, giống như nếu họ chỉ muốn áp dụng một số logic cho các yếu tố được tìm thấy trong cả hai danh sách, tại đó họ chỉ có thể vượt qua một con trỏ hàm hoặc hàm functor hoặc ủy nhiệm hoặc bất cứ thứ gì được gọi trở lại để xử lý các phạm vi của các phần tử giao nhau. Một cái gì đó cho hiệu ứng này:

// 'func' receives a range of indices to
// process.
set_intersection(func):
{
    parallel_bits = bit_pool.acquire()

    // Mark the indices found in the first list.
    for each index in list1:
        parallel_bits[index] = 1

    // Look for the first element in the second list 
    // that intersects.
    first = -1
    for each index in list2:
    {
         if parallel_bits[index] == 1:
         {
              first = index
              break
         }
    }

    // Look for elements that don't intersect in the second
    // list to call func for each range of elements that do
    // intersect.
    for each index in list2 starting from first:
    {
        if parallel_bits[index] != 1:
        {
             func(first, index)
             first = index
        }
    }
    If first != list2.num-1:
        func(first, list2.num)
}

... Hoặc một cái gì đó cho hiệu ứng này. Phần đắt nhất của mã giả trong sơ đồ đầu tiên là intersection.append(index)trong vòng lặp thứ hai, và điều đó áp dụng ngay cả đối std::vectorvới kích thước của danh sách nhỏ hơn trước.

Nếu tôi sao chép sâu mọi thứ thì sao?

Thôi, dừng lại đi! Nếu bạn cần thiết lập các giao lộ, điều đó có nghĩa là bạn đang sao chép dữ liệu để giao nhau. Rất có thể là ngay cả những vật nhỏ nhất của bạn cũng không nhỏ hơn chỉ số 32 bit. Rất có thể giảm phạm vi địa chỉ của các phần tử của bạn xuống 2 ^ 32 (2 ^ 32 phần tử, không phải 2 ^ 32 byte) trừ khi bạn thực sự cần nhiều hơn ~ 4,3 tỷ phần tử ngay lập tức, tại đó cần một giải pháp hoàn toàn khác ( và điều đó chắc chắn không sử dụng bộ chứa trong bộ nhớ).

Các trận đấu chính

Còn các trường hợp chúng ta cần thực hiện các thao tác trong đó các phần tử không giống nhau nhưng có thể có các khóa khớp nhau thì sao? Trong trường hợp đó, ý tưởng tương tự như trên. Chúng ta chỉ cần ánh xạ mỗi khóa duy nhất vào một chỉ mục. Nếu khóa là một chuỗi, ví dụ, thì các chuỗi được thực hiện có thể làm điều đó. Trong các trường hợp đó, một cấu trúc dữ liệu đẹp như bảng ba hoặc bảng băm được gọi để ánh xạ các khóa chuỗi thành các chỉ mục 32 bit, nhưng chúng ta không cần các cấu trúc như vậy để thực hiện các thao tác được đặt trên các chỉ mục 32 bit kết quả.

Toàn bộ rất nhiều giải pháp thuật toán và cấu trúc dữ liệu rất rẻ và đơn giản mở ra như thế này khi chúng ta có thể làm việc với các chỉ số cho các phần tử trong một phạm vi rất hợp lý, không phải là phạm vi địa chỉ đầy đủ của máy, và vì vậy nó thường đáng giá hơn có thể có được một chỉ mục duy nhất cho mỗi khóa duy nhất.

Tôi yêu các chỉ số!

Tôi yêu các chỉ số nhiều như pizza và bia. Khi tôi ở độ tuổi 20, tôi đã thực sự thích C ++ và bắt đầu thiết kế tất cả các loại cấu trúc dữ liệu tuân thủ tiêu chuẩn hoàn chỉnh (bao gồm các thủ thuật liên quan để phân tán một ctor điền từ một ctor phạm vi vào thời gian biên dịch). Nhìn lại đó là một sự lãng phí lớn thời gian.

Nếu bạn xoay vòng cơ sở dữ liệu của mình xung quanh việc lưu trữ các phần tử tập trung trong mảng và lập chỉ mục cho chúng thay vì lưu trữ chúng theo cách phân mảnh và có khả năng trên toàn bộ phạm vi địa chỉ của máy, thì bạn có thể khám phá một thế giới về khả năng cấu trúc dữ liệu và thuật toán chỉ bằng cách thiết kế các thùng chứa và thuật toán xoay quanh đồng bằng cũ inthoặc int32_t. Và tôi thấy kết quả cuối cùng sẽ hiệu quả và dễ bảo trì hơn rất nhiều khi tôi không liên tục chuyển các phần tử từ cấu trúc dữ liệu này sang cấu trúc khác sang cấu trúc khác.

Một số ví dụ sử dụng các trường hợp khi bạn chỉ có thể giả sử rằng bất kỳ giá trị duy nhất nào Tcó một chỉ mục duy nhất và sẽ có các thể hiện nằm trong một mảng trung tâm:

Các loại cơ số đa luồng hoạt động tốt với các số nguyên không dấu cho các chỉ mục . Tôi thực sự có một loại cơ số đa luồng, mất khoảng 1/10 thời gian để sắp xếp hàng trăm triệu phần tử như loại sắp xếp song song của Intel và Intel đã nhanh hơn 4 lần so std::sortvới các đầu vào lớn như vậy. Tất nhiên, Intel linh hoạt hơn nhiều vì đây là một loại dựa trên so sánh và có thể sắp xếp mọi thứ theo từ vựng, do đó, nó so sánh táo với cam. Nhưng ở đây tôi thường chỉ cần cam, như tôi có thể thực hiện một loại sắp xếp cơ số chỉ để đạt được các mẫu truy cập bộ nhớ thân thiện với bộ nhớ cache hoặc lọc nhanh các bản sao.

Khả năng xây dựng các cấu trúc được liên kết như danh sách được liên kết, cây, biểu đồ, các bảng băm chuỗi riêng biệt, v.v. mà không cần phân bổ heap cho mỗi nút . Chúng ta chỉ có thể phân bổ các nút hàng loạt, song song với các phần tử và liên kết chúng với nhau bằng các chỉ mục. Bản thân các nút chỉ trở thành một chỉ mục 32 bit cho nút tiếp theo và được lưu trữ trong một mảng lớn, như vậy:

nhập mô tả hình ảnh ở đây

Thân thiện để xử lý song song. Các cấu trúc được liên kết thường không thân thiện để xử lý song song, vì rất ít khi cố gắng đạt được sự song song trong cây hoặc danh sách được liên kết ngang ngược với, chỉ nói, thực hiện song song cho vòng lặp qua một mảng. Với biểu diễn mảng chỉ số / trung tâm, chúng ta luôn có thể đi đến mảng trung tâm đó và xử lý mọi thứ trong các vòng lặp song song chunky. Chúng tôi luôn có mảng trung tâm của tất cả các phần tử mà chúng tôi có thể xử lý theo cách này, ngay cả khi chúng tôi chỉ muốn xử lý một số phần tử (tại thời điểm đó bạn có thể xử lý các phần tử được lập chỉ mục bởi danh sách được sắp xếp theo cơ chế để truy cập thân thiện với bộ đệm thông qua mảng trung tâm).

Có thể liên kết dữ liệu với từng thành phần một cách nhanh chóng . Như với trường hợp của các bit song song ở trên, chúng ta có thể dễ dàng và cực kỳ rẻ liên kết dữ liệu song song với các phần tử để xử lý tạm thời. Điều này đã sử dụng các trường hợp vượt quá dữ liệu tạm thời. Ví dụ, một hệ thống lưới có thể muốn cho phép người dùng gắn bao nhiêu bản đồ UV vào lưới theo ý muốn. Trong trường hợp như vậy, chúng ta không thể mã hóa số lượng bản đồ UV sẽ có trong mỗi đỉnh và mặt bằng cách sử dụng phương pháp AoS. Chúng ta cần có khả năng liên kết các dữ liệu đó một cách nhanh chóng và các mảng song song có ích ở đó và rẻ hơn nhiều so với bất kỳ loại container liên kết tinh vi nào, thậm chí cả bảng băm.

Tất nhiên các mảng song song được tán thành do tính chất dễ bị lỗi của chúng là giữ các mảng song song đồng bộ với nhau. Ví dụ, bất cứ khi nào chúng tôi xóa một phần tử tại chỉ mục 7 khỏi mảng "gốc", chúng tôi cũng phải làm điều tương tự cho "trẻ em". Tuy nhiên, hầu hết các ngôn ngữ đều có thể khái quát khái niệm này thành một thùng chứa có mục đích chung sao cho logic phức tạp để giữ các mảng song song đồng bộ với nhau chỉ cần tồn tại ở một nơi trong toàn bộ cơ sở mã và một bộ chứa mảng song song như vậy có thể sử dụng triển khai mảng thưa thớt ở trên để tránh lãng phí nhiều bộ nhớ cho các không gian trống liền kề trong mảng được thu hồi sau các lần chèn tiếp theo.

Xây dựng thêm: Cây Bitset thưa thớt

Được rồi, tôi có một yêu cầu để giải thích thêm một số điều mà tôi nghĩ là mỉa mai, nhưng dù sao tôi cũng sẽ làm như vậy vì nó rất vui! Nếu mọi người muốn đưa ý tưởng này lên các cấp độ hoàn toàn mới, thì có thể thực hiện các giao điểm được thiết lập mà không cần lặp tuyến tính qua các phần tử N + M. Đây là cấu trúc dữ liệu cuối cùng của tôi mà tôi đã sử dụng từ lâu và về cơ bản là các mô hình set<int>:

nhập mô tả hình ảnh ở đây

Lý do nó có thể thực hiện các giao điểm được thiết lập mà không cần kiểm tra từng phần tử trong cả hai danh sách là bởi vì một bit tập hợp ở gốc của cấu trúc phân cấp có thể chỉ ra rằng, một triệu phần tử liền kề bị chiếm giữ trong tập hợp. Chỉ cần kiểm tra một bit, chúng ta có thể biết rằng N chỉ số trong phạm vi, [first,first+N)nằm trong tập hợp, trong đó N có thể là một số rất lớn.

Tôi thực sự sử dụng điều này như một trình tối ưu hóa vòng lặp khi duyệt qua các chỉ số bị chiếm dụng, bởi vì giả sử có 8 triệu chỉ số bị chiếm trong tập hợp. Chà, thông thường chúng ta sẽ phải truy cập 8 triệu số nguyên trong bộ nhớ trong trường hợp đó. Với cái này, nó có khả năng có thể chỉ cần kiểm tra một vài bit và đưa ra các phạm vi chỉ mục của các chỉ số chiếm dụng để lặp qua. Hơn nữa, các phạm vi của các chỉ mục mà nó đưa ra được sắp xếp theo thứ tự giúp truy cập tuần tự rất thân thiện với bộ đệm, trái ngược với lặp lại thông qua một loạt các chỉ mục chưa được sử dụng để truy cập dữ liệu phần tử gốc. Tất nhiên, kỹ thuật này tệ hơn đối với các trường hợp cực kỳ thưa thớt, với trường hợp xấu nhất là mọi chỉ số đều là số chẵn (hoặc mỗi số lẻ), trong trường hợp đó không có vùng tiếp giáp nào. Nhưng trong trường hợp sử dụng của tôi ít nhất,


2
"Để tính toán các giao điểm, tôi lặp qua mảng đầu tiên và đánh dấu các phần tử bằng một bit. Sau đó, tôi lặp qua mảng thứ hai và tìm kiếm các phần tử được đánh dấu." Bạn đánh dấu chúng ở đâu, trên mảng thứ hai?
JimmyJames

1
Ồ tôi hiểu rồi, bạn đang 'thực hiện' dữ liệu một đối tượng duy nhất đại diện cho từng giá trị. Đây là một kỹ thuật thú vị cho một tập hợp các trường hợp sử dụng cho các bộ. Tôi thấy không có lý do tại sao không thực hiện phương pháp này như là một hoạt động trên lớp thiết lập của riêng bạn.
JimmyJames

2
"Đó là một giải pháp xâm nhập vi phạm đóng gói trong một số trường hợp ..." Một khi tôi đã hiểu ý của bạn, điều đó xảy ra với tôi nhưng sau đó tôi nghĩ rằng nó không cần thiết. Nếu bạn có một lớp quản lý hành vi này, các đối tượng chỉ mục có thể độc lập với tất cả dữ liệu phần tử và được chia sẻ trên tất cả các phiên bản của loại bộ sưu tập của bạn. tức là sẽ có một tập hợp dữ liệu chủ và sau đó mỗi trường hợp sẽ quay lại tập chủ. Đa luồng sẽ cần phức tạp hơn nhưng tôi nghĩ nếu có thể quản lý được.
JimmyJames

1
Có vẻ như điều này sẽ có khả năng hữu ích trong một giải pháp cơ sở dữ liệu nhưng tôi không biết liệu có cách nào được thực hiện theo cách này không. Cảm ơn vì đã đưa nó ra đây. Bạn có tâm trí của tôi làm việc.
JimmyJames

1
Bạn có thể giải thích thêm một chút? ;) Tôi sẽ kiểm tra xem khi nào tôi có nhiều thời gian.
JimmyJames

-1

Để kiểm tra xem một tập hợp chứa n phần tử có chứa phần tử X khác thường mất thời gian không đổi. Để kiểm tra xem một mảng chứa n phần tử có chứa phần tử X khác thường mất thời gian O (n) hay không. Điều đó thật tệ, nhưng nếu bạn muốn loại bỏ các bản sao khỏi n mục, đột nhiên phải mất O (n) kịp thời thay vì O (n ^ 2); 100.000 mặt hàng sẽ đưa máy tính của bạn đến đầu gối của nó.

Và bạn đang hỏi thêm lý do? "Ngoài buổi chụp hình, bạn có thích buổi tối không, thưa bà Lincoln?"


2
Tôi nghĩ rằng bạn có thể muốn đọc lại nó. Dành thời gian O (n) thay vì thời gian O (n²) thường được coi là một điều tốt.
JimmyJames

Có lẽ bạn đứng trên đầu của bạn trong khi đọc này? OP đã hỏi "tại sao không chỉ lấy một mảng".
gnasher729

2
Tại sao đi từ O (n²) đến O (n) sẽ mang 'máy tính đến đầu gối'? Tôi đã phải bỏ lỡ điều đó trong lớp học của tôi.
JimmyJames
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.