0

My goal is to write a class that works like a unordered_map but keeps the insertion order of the elements while still allowing O(1) lookup by key.

My approach is as follows:

// like unordered_map but keeps the order of the elements for iteration
// implemented as a vector with a unordered_map for constant time lookups
// consider if element removal is needed, since it is O(N) because of the use of a vector of elements
template <typename KEY, typename VAL>
struct ordered_map {
    struct KeyValue {
        KEY key;
        VAL value;
    };

    std::vector<KeyValue> elements; // KeyValue pairs in order of insertion to allow iteration
    std::unordered_map<KEY, int> indexmap; // key -> index map to allow O(1) lookup, is there a way to avoid the redundant key?
    
    //...
}

But I have the problem that in my approach I want to lookup into the indexmap using the keys which are stored 'externally' to it (in the elements vector based on the index value in the map).

std::sort for example allows passing in a comparator, but unordered_sap does not seem to have anything similar.

I could not really find anything online about how to accomplish this, but I might be searching with the wrong terms.

I this approach at all supported by the stl?

Or do I need to resort to storing the Key twice, which I would like to avoid as keys can be heap objects like std::strings for example.

EDIT: unordered_map instead of unordered_set which does not work

12
  • I'm guessing that indexmap holds the hashes of the keys since it's using std::size_t. So what's the problem with using keys stored externally? You would just hash the key and check if it exists in the indexmap? Commented Jan 19, 2021 at 11:25
  • Perhaps boost::multi_index can help? Example Commented Jan 19, 2021 at 11:25
  • 1
    KeyValue is small so having std::set<KeyValue> and std::unordered_set<KeyValue> at the same time should not be a problem. On other hand I smell XY problem so please explain why you have this requirement. Commented Jan 19, 2021 at 11:27
  • I would probably switch your approach around. Use unordered_map to hold the values and a vector with pointers to keep track of the order. Commented Jan 19, 2021 at 11:27
  • If you want the unordered_set<size_t> to actually be an unordered_map<SomeKey, size_t>, it would have been less confusing to write that. Then SomeKey can just be a std::reference_wrapper<KEY> - although I agree it's more sensible to keep the KEY in the map and manage an external sequence of iterators. Commented Jan 19, 2021 at 11:44

1 Answer 1

0

My goal is to write a class that works like a unordered_map but keeps the insertion order of the elements while still allowing O(1) lookup by key

... and without duplicating the key, in cases it's large and/or expensive.

So, you want two things:

  1. a constant-time associative lookup with the same semantics as std::unordered_map (no duplicate keys, or you would/should have asked for std::unordered_multimap)
  2. a sequential index tracking insertion order

You have chosen to implement the associative lookup using a sequential container, and the sequential index using an associative container. I don't know why, but let's try the more natural alternative:

  1. the associative lookup should just be std::unordered_map<Key, SomeValueType>
  2. the sequential index can just be std::vector<SomeValueType*>

The remaining blank is SomeValueType: it could just be VAL, but then we need to do extra work to fix the sequential index whenever we erase something. We could make it std::pair<VAL, size_t> instead, so it can store the index i of the iterator we'll need to remove on erasure. The down side is that on top of moving all i+1..N vector elements down one, we also need to update the index value for each of those map elements.

If you want to preserve constant-time erasure, you probably need the sequential index to be a std::list<SomeValueType*>, and to keep a std::list::iterator in your map element. Actual linear iteration will be slower on a linked list than a vector, but you get the same complexity when erasing elements.


NB. The sequential index does need to store pointers rather than iterators - I originally forgot the invalidation behaviour for unordered maps. However, if you want access to the key, they can obviously be std::unordered_map<key, SomeValueType>::value_type pointers ... I just wrote out the shorter alternative.

Sign up to request clarification or add additional context in comments.

2 Comments

Iterators can get invalidated on rehashing, but a pointer to the value will not.
Erasure is rarely needed but the approach with a std::list<SomeValueType*> is a good tip, thanks. The reason why I had my approach backwards is probably because I think of the hashmap as an acceleration structure and the vector as the 'real' data structure I actually want. Actually thinking about the fact that the map already used linked lists internally (i think) the better solution would be to simply add pointers to the already existing elements to keep track of the order.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.