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::set
trườ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:
- Hai tập hợp chứa các chỉ số cho các phần tử.
- 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 đồ:
Đồ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ố.
... ý 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::vector
vớ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ũ int
hoặ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 T
có 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::sort
vớ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:
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>
:
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,