Keep a binary search tree (BST) of intervals with the key being the starting point of the interval.
For any new interval to be inserted:
Find the biggest value in the BST smaller than the starting point of the new interval (can be done in O(log n)).
Either that interval or the next interval will overlap with the new interval, or there will be no overlap (in which case we just do an insert).
There can be more intervals overlapping with the new interval, so from here we need to iterate through the rest of the BST (starting from the point found above) and merge the interval with any overlapping intervals.
While any given insert can take O(n log n) in the worst case (in case that interval overlaps with e.g. every other interval), the amortised time will be O(log n) per insert (since we can count the work done to delete an element as part of the work done to insert it, which is O(log n) work in total).
Some lightly-tested C++ code doing this:
// set<pair<int, int> > can also be used, but this way seems conceptually simpler
map<int, pair<int, int> > intervals;
void insert(int left, int right)
{
if (!intervals.empty())
{
// get the next bigger element
auto current = intervals.upper_bound(left);
// checking if not found is not needed because of decrement and how C++ iterators work
// decrement to get next smaller one instead, but only if we're not that the beginning
if (current != intervals.begin())
--current;
// skip current if it's entirely to the left of the new interval
if (current->second.second < left)
++current;
// update new starting point if there's an overlap and current is more to the left
if (current != intervals.end() && current->first <= right)
left = min(left, current->first);
// iterate through while there's an overlap (deleting as we go)
for (; current != intervals.end() && current->first <= right;
current = intervals.erase(current))
// update the end point of new interval
right = max(right, current->second.second);
}
// insert our updated interval
intervals[left] = make_pair(left, right);
}
// FYI: current->first is the start, current->second.second is the end
Live demo.