5

I have a list of line segments (a std::vector<std::pair<int, int> > that I'd like to iterate through and subdivide. The algorithm would be, in psuedocode:

for segment in vectorOfSegments:
    firstPoint = segment.first;
    secondPoint = segment.second;
    newMidPoint = (firstPoint + secondPoint) / 2.0
    vectorOfSegments.remove(segment);
    vectorOfSegments.push_back(std::make_pair(firstPoint, newMidPoint));
    vectorOfSegments.push_back(std::make_pair(newMidPoint, secondPoint));

The issue that I'm running into is how I can push_back new elements (and remove the old elements) without iterating over this list forever.

It seems like the best approach may be to make a copy of this vector first, and use the copy as a reference, clear() the original vector, and then push_back the new elements to the recently emptied vector.

Is there a better approach to this?

nathan lachenmyer
  • 5,298
  • 8
  • 36
  • 57
  • What is your end goal? To split each pair just once? So if I have {<2,4>, <10,20>}, the result would be {<2,3>,<3,4>,<10,15>,<15,20>}? – Peter Uhnak Apr 11 '15 at 15:34
  • Dupe of this? http://stackoverflow.com/questions/5638323/modifying-a-data-structure-while-iterating-over-it Good links there nonetheless. – Andrew Henle Apr 11 '15 at 15:40
  • My goal would be to treat each pair as a point in space and find the midpoints. {<2,4>, <10,20>} would become {<2,4>, <6,12>, <10,20>}, for example. I'd like to be able to subdivide the list as many times as I'd like. – nathan lachenmyer Apr 11 '15 at 15:40
  • @AndrewHenle I don't think it's quite a dupe (that post doesn't give an alternative, for example) but it certainly answers my question about if I can do it with iterators! thanks! – nathan lachenmyer Apr 11 '15 at 15:43
  • Aha, because the `segment` refers to a pair <2,4>, thus firstPoint is 2, secondPoint is 4, and it would create pairs <2,3> and <3,4>. – Peter Uhnak Apr 11 '15 at 15:45

3 Answers3

2

Don't remove elements while you insert new segments. Then, when finished with inserting you could remove the originals:

int len=vectorOfSegments.size();
for (int i=0; i<len;i++)
{
    std::pair<int,int>& segment = vectorOfSegments[i];
    int firstPoint = segment.first;
    int secondPoint = segment.second;
    int newMidPoint = (firstPoint + secondPoint) / 2;
    vectorOfSegments.push_back(std::make_pair(firstPoint, newMidPoint));
    vectorOfSegments.push_back(std::make_pair(newMidPoint, secondPoint));
}
vectorOfSegments.erase(vectorOfSegments.begin(),vectorOfSegments.begin()+len);

Or, if you want to replace one segment by two new segments in one pass, you could use iterators like here:

for (auto it=vectorOfSegments.begin(); it != vectorOfSegments.end(); ++it)
{
    std::pair<int,int>& segment = *it;
    int firstPoint = segment.first;
    int secondPoint = segment.second;
    int newMidPoint = (firstPoint + secondPoint) / 2;
    it = vectorOfSegments.erase(it);
    it = vectorOfSegments.insert(it, std::make_pair(firstPoint, newMidPoint));
    it = vectorOfSegments.insert(it+1, std::make_pair(newMidPoint, secondPoint));
}

As Lightning Racis in Orbit pointed out, you should do a reserve before either of these approaches. In the first case do reserve(vectorOfSegmets.size()*3), in the latter reserve(vectorOfSegmets.size()*2+1)

2

It seems like the best approach may be to make a copy of this vector first, and use the copy as a reference, clear() the original vector, and then push_back the new elements to the recently emptied vector.

Almost. You don't need to copy-and-clear; move instead!

// Move data from `vectorOfSegments` into new vector `original`.
// This is an O(1) operation that more than likely just swaps
// two pointers.
std::vector<std::pair<int, int>> original{std::move(vectorOfSegments)};

// Original vector is now in "a valid but unspecified state".
// Let's run `clear()` to get it into a specified state, BUT
// all its elements have already been moved! So this should be
// extremely cheap if not a no-op.
vectorOfSegments.clear();

// We expect twice as many elements to be added to `vectorOfSegments`
// as it had before. Let's reserve some space for them to get
// optimal behaviour.
vectorOfSegments.reserve(original.size() * 2);

// Now iterate over `original`, adding to `vectorOfSegments`...
Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • Interesting! What's the advantage of using move? – nathan lachenmyer Apr 11 '15 at 15:55
  • @nathanlachenmyer: Not having to do a copy! You go from making a copy of every single element in your vector, to simply swapping two pointers in the `std::vector` itself. It's practically immediate. Move is very, very cheap for most standard containers. – Lightness Races in Orbit Apr 11 '15 at 15:55
0

This is easiest solved by using an explicit index variable like this:

for(size_t i = 0; i < segments.size(); i++) {
    ...    //other code
    if(/*condition when to split segments*/) {
        Point midpoint = ...;
        segments[i] = Segment(..., midpoint);    //replace the segment by the first subsegment
        segments.emplace_back(Segment(midpoint, ...));    //add the second subsegment to the end of the vector
        i--;    //reconsider the first subsegment
    }
}

Notes:

  • segments.size() is called in each iteration of the loop, so we really reconsider all appended segments.

  • The explicit index means that the std::vector<> is free to reallocate in the emplace_back() call, there are no iterators/pointers/references that can become invalid.

  • I assumed that you don't care about the order of your vector because you add the new segments to the end of the vector. If you do care, you might want to use a linked list to avoid quadratic complexity of your algorithm as insertion/deletion to/from an std::vector<> has linear complexity. In my code I avoid insertion/deletion by replacing the old segment.

    Another approach to retain order would be to ignore order at first and then reestablish order via sorting. Assuming a good sorting algorithm, that is O(n*log(n)) which is still better than the naive O(n^2) but worse than the O(n) of the linked list approach.

  • If you don't want to reconsider the new segments, just use a constant size and omit the counter decrement:

    size_t count = segments.size();
    for(size_t i = 0; i < count; i++) {
        ...    //other code
        if(/*condition when to split segments*/) {
            Point midpoint = ...;
            segments[i] = Segment(..., midpoint);    //replace the segment by the first subsegment
            segments.emplace_back(Segment(midpoint, ...));    //add the second subsegment to the end of the vector
        }
    }
    
cmaster - reinstate monica
  • 38,891
  • 9
  • 62
  • 106
  • Why not just move-and-build like in my answer? All this seems remarkably overcomplex. – Lightness Races in Orbit Apr 11 '15 at 17:05
  • _"Smaller memory footprint"_ How so? _"less non-sequential memory accesses"_ Even if you meant 'fewer' then this is obviously not true given that you jump to the end of the vector on every single iteration. (I concede that mine is precisely as bad in this regard.) _"No unnecessary move of the new segments (the erase() call in your code)"_ There is no erase() call in my code. – Lightness Races in Orbit Apr 11 '15 at 21:00
  • @LightningRacisinObrit My bad, I was looking at the wrong answer X-| Still, only point 4. (the stuff about `erase()`) was really wrong, the smaller memory footprint and the less memory accesses still stand: Smaller memory footprint, because I don't need more memory than for the resulting segments while your code needs memory for both the original and the resulting segments at the same time. Less memory accesses because of the reduced memory footprint. I agree, though, that this is not less sequential then yours. Sorry for mixing that up :-( – cmaster - reinstate monica Apr 12 '15 at 05:26
  • Well, sure, I could iteratively erase from the source as we go. I wouldn't do that unless using a `deque`, though. It's not clear whether we should optimise here for memory usage or for speed; certainly, we can't be optimizing for _both_ at the same time. It's a trade-off. I tend not to care too much about memory usage unless I'm in my day job (embedded firmware). – Lightness Races in Orbit Apr 12 '15 at 13:23