To me the biggest problem is:
- Increasing the size of a list node by an extra pointer (or references) for SLL, 3 for DLL. This might not affect performance that much if you're just sporadically allocating list nodes all over the memory space, since at that point it wouldn't matter much unless the node was just around 64 bytes, e.g., aligned to 64 byte boundaries, and the extra pointer made it over 64 bytes, in which case we would double up our cache misses. If you're using a careful allocation strategy, the smaller the list node, the better for sequential traversal, and one extra pointer might be a huge relative size increase just from the list POV.
- The biggest real-world benefit of linked lists even in performance-critical areas is often that ability to move nodes from one list to another, split lists into two lists and merge them in constant time, etc. You complexify this part in exchange for simplifying something that isn't that complex.
- Circular references were mentioned, and that might be a problem in some languages. If it's like C or C++, it would be a backpointer to the list which wouldn't affect collection in any way, the nodes would still be destroyed when requested.
What I recommend is to stop trying to put your linked list implementation into a node. Make a node raw data, a private implementation detail of a list that doesn't bother to provide much logic at all. Your life will generally become a lot easier if you stop trying to split functionality concerns too much between linked structures and their nodes: tree vs. tree node, linked list vs. list node, e.g. Put all the logic into the data structure where you have all the information you need to do everything (both head
pointer and node). moveToFront
should be a linked list method rather than list node method. With that, there's no more need for a backpointer.
class DataStructure
{
public:
...
private:
struct Node
{
// Don't put functionality here except maybe a ctor/dtor at
// most if you can avoid it. The distribution of concerns should
// this node have functionality will often make things more
// complex rather than simpler, and also have you reaching for
// backpointers quickly, potentially wanting to waste memory
// and increase the amount of state to maintain superfluously
// (more state usually means more potential for bugs) just
// because we want to put functions into this Node class.
};
...
Node* head_or_root;
};
Another minor thing worth noting is that there's not really much smarter work that moveToFront
can do besides erase
and push to front
, so it might not be worth having the extra function at all except as a convenience sort of thing.
That is, unless this is unavoidable, where the clients would only have a node that they access from a previously cached, persistent space, e.g., and no access to the linked list. If they do have access to the linked list, then the easiest way is just avoid putting logic into your nodes outright, and make them private, raw data C-style struct
type of things.
Even then, you will often make your life easier in this case to simply require clients to store a reference to both node and linked list, instead of always storing a backpointer in a node (which may not be needed by most operations, making us waste memory often in places that don't benefit from this).
Another thing is that since Node
models implementation details of a linked list, if you can avoid exposing Node
references directly and instead provide some indirect handle to it, like Iterator
, it'll reduce the risk of problems.