197

Why does the reverse function for the std::list class in the C++ standard library have linear runtime? I would think that for doubly-linked lists the reverse function should have been O(1).

Reversing a doubly-linked list should just involve switching the head and the tail pointers.

10
  • 19
    I do not understand why people are downvoting this question. It is a perfectly reasonable question to ask. Reversing a doubly linked list should take O(1) time. Commented Feb 24, 2016 at 20:23
  • 47
    unfortunately, some folks confuse the concepts of "the question is good" with "the question has a good idea". I love questions like this, where basically "my understanding seems different than a commonly accepted practice, please help resolve this conflict", because expanding how you think helps you solve a lot more problems down the road! It would appear others take the approach of "that's a waste of processing in 99.9999% of cases, don't even think about it". If it's any consolation, I've been downvoted for much, much less! Commented Feb 24, 2016 at 22:31
  • 4
    Yeah this question got an inordinate amount of downvotes for its quality. Probably it's the same as who upvoted Blindy's answer. In fairness, "reversing a doubly linked list should just involve switching the head and tail pointers" is generally not true for the standard linked list impl that everyone learns in highschool, or for many implementations that people use. A lot of times in SO people's immediate gut reaction to the question or answer drives upvote / downvote decision. If you had been more clear in that sentence or omitted it, I think you would have gotten less downvotes. Commented Feb 25, 2016 at 8:53
  • 3
    Or let me put the burden of proof with you, @Curious: I have whipped up a doubly linked list implementation here: ideone.com/c1HebO. Can you indicate how you would expect the Reverse function to be implemented in O(1)? Commented Feb 25, 2016 at 10:58
  • 2
    @CompuChip: Actually, depending on the implementation, it may not. You do not need an extra boolean to know which pointer to use: just use the one not pointing back to you... which could well be automatic with a XOR'ed linked list by the way. So yes, it depends on how the list is implemented, and the OP statement could be clarified. Commented Feb 25, 2016 at 13:33

7 Answers 7

193

Hypothetically, reverse could have been O(1). There (again hypothetically) could have been a boolean list member indicating whether the direction of the linked list is currently the same or opposite as the original one where the list was created.

Unfortunately, that would reduce the performance of basically any other operation (albeit without changing the asymptotic runtime). In each operation, a boolean would need to be consulted to consider whether to follow a "next" or "prev" pointer of a link.

Since this was presumably considered a relatively infrequent operation, the standard (which does not dictate implementations, only complexity), specified that the complexity could be linear. This allows "next" pointers to always mean the same direction unambiguously, speeding up common-case operations.

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

18 Comments

@MooseBoys: I don't agree with your analogy. The difference is that, in the case of a list,the implementation could provide reverse with O(1) complexity without affecting the big-o of any other operation, by using this boolean flag trick. But, in practice an extra branch in every operation is costly, even if it is technically O(1). By contrast, you cannot make a list structure in which sort is O(1) and all the other operations are the same cost. The point of the question is that, seemingly, you can get O(1) reverse for free if you only care about big O, so why didn't they do that.
If you used an XOR-linked-list, reversing would become constant time. An iterator would be bigger though, and incrementing/decrementing it would be slightly more computationally expensive. That might be dwarfed by the unavoidable memory-accesses though for any kind of linked list.
@IlyaPopov: Does every node actually need this? The user never asks any questions of the list node itself, only the main list body. So accessing the boolean is easy for any method that the user calls. You could make the rule that the iterators are invalidated if the list is reversed, and/or store a copy of the boolean with the iterator, for example. So I think it would not affect the big O, necessarily. I'll admit, I didn't go line-by-line through the spec. :)
@Kevin: Hm, what? You cannot xor two pointers directly anyway, you need to convert them to integers first (obviously of type std::uintptr_t. Then you can xor them.
@Kevin, you definitely could make an XOR linked list in C++, it's practically the poster-child language for this kind of thing. Nothing says you have to use std::uintptr_t, you could cast to a char array and then XOR the components. It would be slower but 100% portable. Likely you could make it select between these two implementations and only use the second as a fallback if uintptr_t is missing. Some if it is described in this answer: stackoverflow.com/questions/14243971/…
|
61

It could be O(1) if the list would store a flag that allows swapping the meaning of the “prev” and “next” pointers each node has. If reversing the list would be a frequent operation, such an addition might be in fact useful and I don't know of any reason why implementing it would be prohibited by the current standard. However, having such a flag would make ordinary traversal of the list more expensive (if only by a constant factor) because instead of

current = current->next;

in the operator++ of the list iterator, you would get

if (reversed)
  current = current->prev;
else
  current = current->next;

which is not something you'd decide to add easily. Given that lists are usually traversed much more often than they are reversed, it would be very unwise for the standard to mandate this technique. Therefore, the reverse operation is allowed to have linear complexity. Do note, however, that tO(1) ⇒ tO(n) so, as mentioned earlier, implementing your “optimization” technically would be permitted.

If you come from a Java or similar background, you might wonder why the iterator has to check the flag each time. Couldn't we instead have two distinct iterator types, both derived from a common base type, and have std::list::begin and std::list::rbegin polymorphically return the appropriate iterator? While possible, this would make the whole thing even worse because advancing the iterator would be an indirect (hard to inline) function call now. In Java, you're paying this price routinely anyway, but then again, this is one of the reasons many people reach for C++ when performance is critical.

As pointed out by Benjamin Lindley in the comments, since reverse is not allowed to invalidate iterators, the only approach permitted by the standard seems to be to store a pointer back to the list inside the iterator which causes a double-indirect memory access.

10 Comments

@galinette: std::list::reverse does not invalidate iterators.
@galinette Sorry, I mis-read your earlier comment as “flag per iterator” as opposed to “flag per node” as you wrote it. Of course, a flag per node would be counter-productive as you would, again, have to do a linear traversal to flip them all.
@5gon12eder: You could eliminate the branching at very lost cost: store the next and prev pointers in an array, and store the direction as a 0 or 1. To iterate forward, you'd follow pointers[direction] and to iterate in reverse pointers[1-direction] (or vice versa). This would still add a tiny bit of overhead, but probably less than a branch.
You probably can't store a pointer to the list inside the iterators. swap() is specified to be constant time and not invalidate any iterators.
@TavianBarnes Damn it! Well, triple indirection then… (I mean, not actually triple. You'd have to store the flag in a dynamically allocated object but the pointer in the iterator can of course point directly to that object instead of indirecting over the list.)
|
37

Surely since all containers that support bidirectional iterators have the concept of rbegin() and rend(), this question is moot?

It's trivial to build a proxy that reverses the iterators and access the container through that.

This non-operation is indeed O(1).

such as:

#include <iostream>
#include <list>
#include <string>
#include <iterator>

template<class Container>
struct reverse_proxy
{
    reverse_proxy(Container& c)
    : _c(c)
    {}

    auto begin() { return std::make_reverse_iterator(std::end(_c)); }
    auto end() { return std::make_reverse_iterator(std::begin(_c)); }

    auto begin() const { return std::make_reverse_iterator(std::end(_c)); }
    auto end() const { return std::make_reverse_iterator(std::begin(_c)); }

    Container& _c;
};

template<class Container>
auto reversed(Container& c)
{
    return reverse_proxy<Container>(c);
}

int main()
{
    using namespace std;
    list<string> l { "the", "cat", "sat", "on", "the", "mat" };

    auto r = reversed(l);
    copy(begin(r), end(r), ostream_iterator<string>(cout, "\n"));

    return 0;
}

expected output:

mat
the
on
sat
cat
the

Given this, it seems to me that the standards committee have not taken time to mandate O(1) reverse-ordering of the container because it's not necessary, and the standard library is largely built on the principle of mandating only what is strictly necessary while avoiding duplication.

Just my 2c.

Comments

18

Because it has to traverse every node (n total) and update their data (the update step is indeed O(1)). This makes the whole operation O(n*1) = O(n).

9 Comments

Because you need to update the links between every item too. Take out a piece of paper and draw it out instead of downvoting.
Why ask if you're so sure then? You're wasting both our time.
@Curious Doubly linked list's nodes have a sense of direction. Reason from there.
@Blindy A good answer should be complete. "Take out a piece of paper and draw it out" thus should not be a requisite component of a good answer. Answers which are not good answers are subject to downvotes.
@Shoe: Do they have to? Please research XOR-linked-list and the like.
|
2

It also swaps previous and next pointer for every node. Thats why it takes Linear. Although it can be done in O(1) if the function using this LL also takes information about LL as input like whether it is accessing normally or reverse.

Comments

1

Only an algorithm explanation. Imagine you have an array with elements, then you need to inverted it. The basic idea is to iterate on each element changing the element on the first position to the last position, the element on second position to penultimate position, and so on. When you reach at the middle of the array you'll have all elements changed, thus in (n/2) iterations, which is considered O(n).

Comments

1

It is O(n) simply because it needs to copy the list in reverse order. Each individual item operation is O(1) but there are n of them in the entire list.

Of course there are some constant-time operations involved in setting up the space for the new list, and changing pointers afterwards, etc. The O notation doesn't consider individual constants once you include a first-order n factor.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.