簡體   English   中英

在不復制 C++ 中的數據的情況下從類似向量的數據結構中刪除索引的最佳方法

[英]Most optimal way to remove an index from an vector-like data structure without copying data in C++

問題:

我需要創建一個具有可索引訪問權限的簡單向量或類似向量的數據結構,例如:

arr[0] = 'a';
arr[1] = 'b';
//...
arr[25] = 'z';

從這個結構中,我想刪除一些索引,例如索引[5] 不需要從 memory 中刪除索引處的實際值,並且不應將值復制到任何地方,我只需要數據的索引之后重新安排的結構,以便:

arr[0] = 'a';
//...
arr[4] = 'e';
arr[5] = 'g';
//...
arr[24] = 'z';

std::vector 是在這種情況下使用的最佳數據結構嗎?我應該如何在不復制數據的情況下正確刪除索引? 請提供代碼。

或者我可以使用更優化的數據結構嗎?

請注意,除了通過索引之外,我不打算以任何其他方式訪問數據,並且我不需要在任何時候將其連續存儲在 memory 中。

您想要的可能包含在以下其中一項中:

  1. std::hive提出了什么

Hive 是對游戲編程界通常稱為“桶數組”容器的形式化、擴展和優化; 類似的結構存在於高性能計算、高性能交易、3D 模擬、物理模擬、機器人、服務器/客戶端應用程序和粒子模擬領域的各種化身中。

  1. C++23 中的std::flat_map

flat_map 是一種關聯容器,它支持唯一鍵(每個鍵值最多包含一個)並提供基於鍵的另一種類型 T 的值的快速檢索。

由於您希望更新索引,因此您需要一個順序容器:vector、list、deque 或類似容器。 vector 和 deque 會四處復制值,但 list 對於幾乎所有用途來說也很慢,所以這些在一開始都不適合。

因此,最好的解決方案是std::vector<std::unique_ptr<Item>> 然后你會得到非常快的訪問,但是當通過索引刪除元素時,實際的項目本身並沒有移動,只有指針被重新排列。

另一個基於范圍的解決方案包括:

  • enumerate v的每個元素,
  • remove (每個元素) _if it is_contained_in those toBeRemoved based on the key
  • 最后transform結果只val值。
auto w = enumerate(v)
       | remove_if(is_contained_in(toBeRemoved), key)
       | transform(val);

Compilier Explorer上的完整示例。


我還使用了BOOST_HOF_LIFT中的BOOST_HOF_LIFTstd::get轉換為我可以傳遞的 object,基於它我定義了keyval

矢量數據結構的要求是它的數據在 memory 中是連續的,因此如果不移動 memory 來填充間隙(除了最后一個元素),就不可能刪除一個元素。

向量是序列容器之一。 具有最小 O(1) 元素移除成本的序列容器是雙鏈表(由std::list實現)。 列表可以按順序高效訪問,但與向量不同的是隨機訪問的 O(n)。

有關對各種容器類的不同操作的時間復雜度的討論,請參見例如https://dev.to/pratikparvati/c-stl-containers-choose-your-containers-wisely-4lc4

每個容器針對不同的操作具有不同的性能特征。 您需要選擇最適合您最常執行的操作的一種。 如果順序訪問和元素插入和刪除是關鍵,那么列表是合適的。 如果隨機訪問更為關鍵,並且插入/刪除不頻繁,那么使用向量可能是值得的。 可能在您的應用程序中兩者都不是最佳選擇,但對於您問題中詳述的特定情況,鏈表很適合。

使用基於視圖的方法怎么樣?

在這里,我展示了一個使用ranges::any_view的解決方案。 這個答案的底部是完整的工作代碼,它表明我們組成的類似向量的實體實際上指向原始std::vector的元素。

當心,我不是在以任何方式解決性能問題。 總的來說,我並沒有聲稱對它了解很多,而且我對它的了解甚至更少,因為它與我在下面使用的抽象成本有關。

解決方案的核心是這個 function 只刪除一個元素,索引為i的元素,形成輸入range

constexpr auto shoot =
    [](std::size_t i, auto&& range)
        -> any_view<char const&, category::random_access> {
    return concat(range | take(i), range | drop(i + 1));
};

詳細,

  • 給定要從輸入range中刪除的項目的索引i
  • 它通過從范圍中take第一個i元素來創建一個range (這些是索引i元素之前的元素),
  • 它通過從范圍中drop第一個i + 1元素來創建一個range (因此保留索引i元素之后的元素),
  • 最后它concat了這兩個范圍
  • 將結果范圍作為any_view<char const&, category::random_access> ,以避免為每次重復應用shoot嵌套越來越多的視圖;
  • category::random_access允許對元素進行基於[]的訪問。

鑒於上述情況,從范圍中刪除一些元素就像這樣簡單:

auto w = shoot(3, shoot(9, shoot(10, v)));

但是,如果您要調用shoot(9, shoot(3, v)) ,您將首先刪除第 3 個元素,然后是結果范圍的第 9 個元素,這意味着您已經刪除了第 3 個和第 10 個元素關於原始向量; 這與基於范圍的方法無關,而只是提供 function 以僅刪除一個元素。

顯然,您可以在此基礎上構建一個 function 來消除另一個范圍內的所有索引:

  • 對要刪除的元素的indices進行sort
  • for - reverse循環它們(出於上述原因),
  • 並一張一張地shoot它們(不使用any_view我們不能做view = shoot(n, view);因為每次shoot都會改變view的類型):
constexpr auto shoot_many = [](auto&& indices, auto&& range){
    any_view<char const&, category::random_access> view{range};
    sort(indices);
    for (auto const& idx : reverse(indices)) {
        view = shoot(idx, view);
    }
    return view;
};

我已經為shoot_many嘗試了另一種解決方案,我基本上會為range的所有元素編制索引, filter掉那些索引包含在indices中的元素,最后transform ing 刪除索引。 這是它的草圖:

constexpr auto shoot_many = [](auto&& indices, auto&& range){
    std::set<std::size_t> indices_(indices.begin(), indices.end()); // for easier lookup
                                                                    // (assuming they're not sorted)
    auto indexedRange = zip(range, iota(0)); // I pretty sure there's a view doing this already
    using RandomAccessViewOfChars
        = any_view<char const&, category::random_access>;
    return RandomAccessViewOfChars{
        indexedRange | filter([indices_](auto&& pair){ return indices_.contains(pair.second); })
                     | transform([](auto&& pair){ return pair.first; })};
};

但是,這不起作用,因為在 pipe 中使用filter意味着在我們真正遍歷它之前我們不知道結果范圍的長度,這反過來意味着我返回的 output 不滿足category::random_access any_view的編譯時要求。 傷心。

無論如何,這是解決方案

#include <assert.h>
#include <cstddef>
#include <functional>
#include <iostream>
#include <memory>
#include <range/v3/algorithm/sort.hpp>
#include <range/v3/range/conversion.hpp>
#include <range/v3/view/any_view.hpp>
#include <range/v3/view/concat.hpp>
#include <range/v3/view/drop.hpp>
#include <range/v3/view/iota.hpp>
#include <range/v3/view/reverse.hpp>
#include <range/v3/view/take.hpp>
#include <set>
#include <vector>

using namespace ranges;
using namespace ranges::views;

// utility to drop one element from a range and give you back a view
constexpr auto shoot =
    [](std::size_t i, auto&& range)
        -> any_view<char const&, category::random_access> {
    return concat(range | take(i), range | drop(i + 1));
};

constexpr auto shoot_many = [](auto&& indices, auto&& range){
    any_view<char const&, category::random_access> view{range};
    sort(indices);
    for (auto const& idx : reverse(indices)) {
        view = shoot(idx, view);
    }
    return view;
};


int main() {
    // this is the input
    std::vector<char> v = iota('a') | take(26) | to_vector;
    // alternavively,   = {'a', 'b', ...)

    // remove a few elements by index
    auto w = shoot_many(std::vector<int>{3, 10, 9}, v);

    for (std::size_t i = 0, j = 0; i != v.size(); ++i, ++j) {
        if (i == 10 || i == 9 || i == 3) {
            --j;
            std::cout << v[i] << ',' << '-' << std::endl;
        } else {
            std::cout << v[i] << ',' << w[j] << std::endl;

            assert( v[i] ==  w[j]);
            assert(&v[i] == &w[j]);
        }
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM