Without a doubt, there are many ways to implement a doubly-linked list. But all of them are filled with corner cases and branches, except for the circular implementation with sentinel.
In this approach. we add a sentinel node, which is linked both to the start and to the end of the list, making the list circular. I firmly believe all the other approaches are a big waste of brainpower and CPU branch predictor =)
The issue is that the current testing framework penalizes this approach by assuming that LinkedList can be initialized using “default field initialization” only. The implementation with sentinel needs to assign a pointer to itself during its initialization, which is not possible even with the commonly used “return new object by value from init method” style of constructors.
I’m not sure what to suggest here, since apparently this implementation is a bit against the standard zig OOP style. I had to wrap the proper linked list into a lazily-initialized PImpl wrapper, I also had to move “len” field there, since testing code assumes it can access it directly.
The link you shared is an invite for a private request, which, I think, can be accepted exactly once by one person to view your code. People can view your published solution here: stgatilov's solution for Linked List in Zig on Exercism
Why do you need a circular list vs having the last node point to null as the next?
The basic use for a circular linked list with sentinel node would be efficient implementation of a sorted linked list. You would put a impossible value into sentinel node that is guaranteed to break the comparison loop. Now, as you iterate when you add stuff into linked list there is no need to test head/tail at all.
Why would making it circular vs non-circular make it more efficient? The only difference is if the head/tail node contains a sentinel value which tells you to stop vs using a null at the tail telling you to stop.
Probably in this context it won’t make a difference. It’s more efficient when implementing a sorted linked list as you don’t need to test for head/tail at all. With a really big linked list one comparison less for each node can make a difference.
# without sentinel
if to_be_deleted.prev:
to_be_deleted.prev.next = to_be_deleted.next
else:
list.first = to_be_deleted.next
if to_be_deleted.next:
to_be_deleted.next.prev = to_be_deleted.prev
else:
list.last = to_be_deleted.prev
# with sentinel
to_be_deleted.prev.next = to_be_deleted.next
to_be_deleted.next.prev = to_be_deleted.prev
By using the sentinel you have to handle fewer special cases.
(I’m not convinced by the argument about branchless being faster in this case because working with a linked list usually involves lots of cache misses and that is magnitudes slower than a conditional instruction.)
Yes, the main profit is that you don’t need to check for head/tail/empty cases everywhere. Indeed, for a large list these checks are well-predicted and don’t slow things down much. But having to write all these checks is pretty annoying.