Click here to Skip to main content
Click here to Skip to main content

Doubly-Linked List Implementation

By , 3 Sep 2002
 

Introduction

I was writing some code in C# the other day and I realized I needed a linked list to make something I was doing easier. I wanted a list that would collapse around a removed object, and that was not be backed by an array. I naturally went to the .NET Framework and the System.Collections namespace to look at the .NET linked list that I just assumed would be there. To my surprise there was not one. I could not believe it. I then searched MSDN looking for one to see if it was placed in another namespace. It was not; it just did not exist in the .NET Framework. Finally, I searched the internet and still could not find one implemented in C#.

That inspired me to write my own linked list. Not just a standard linked list, but a doubly-linked list. A list where each element has a reference to the next element and the previous element. A standard linked list would just have a reference to the next element. With doubly-linked list, the list can easily be traversed forward and backwards and delete an element in constant time.

Break It Down

My doubly-linked list, which we will call just LinkedList from now on, starts by implementing the following interfaces.

  • IList (which is a ICollection and IEnumerable)
  • ICloneable

Then under this public facade we have a lot of very important private members that do the bulk of the work.

  • class Node
  • class LinkedListEnumerator
  • FindNodeAt(int)
  • Remove(Node)
  • Node headerNode
  • int modifications

Some Details

The Node class is a very simple class, yet it is a very key part of the LinkedList. It wraps an object and keeps a reference to the next node and the previous node. The Node class is hidden from the user of the LinkedList, so that it works like any other collection in the .NET Framework.

The headerNode member variable of type Node has an important role as the starting point in the list. This Node contains a null reference and can never be accessed by the user or removed. It is not considered in the count of total objects in the list. This Node is important in a doubly-linked list, as it is technically the beginning and ending of the list.

The FindNodeAt(int index) method contains the search algorithm for accessing the list by index. At the moment it divides the list in half and searches from the beginning or the end depending on which is closest to the requested index. This method is used by all the other methods directly or indirectly that require access to an object by index. This helps to make the searches much faster. There is potential for improvement for large lists by further dividing before searching, however, at a cost for small lists. Right now this seems like the best compromise for most usages. The current algorithm used to find a Node is below.

    Node node = headerNode;

    if (index < (count / 2))
    {
      for (int i = 0; i <= index; i++)
        node = node.NextNode;
    }
    else
    {
      for (int i = count; i > index; i--)
        node = node.PreviousNode;
    }

The Remove(Node value) is important because it adjusts the remaining Nodes by compressing the list. This is done simply by taking the Node that needs to be removed and changing its previous Node's next Node reference to its next Node, and changing its next Node's previous Node reference to its previous Node then leaving itself for the garbage collector. This may be easier to understand by viewing the algorithm used in this method below.

    if (value != headerNode)
    {
      value.PreviousNode.NextNode = value.NextNode;
      value.NextNode.PreviousNode = value.PreviousNode;

      count--;
      modifications++;
    }

The modifications member variable of type int is incremented every time there is a modification to the structure of the list. The variable is then used by the LinkedListEnumerator to guard against concurrent modifications to the list while enumerating.

The LinkedList class is not thread safe by design. If thread safety is required, the class can be extended to provide it.

The LinkedListEnumerator class is fail-fast. This means it uses the modifications variable it is passed when it is created to know if any modifications have been made while enumerating. The check is made in its MoveNext() method before it increments to the next value. If a modification has been detected then it will throw a SystemException that can then be caught and handled accordingly. Below is the source for LinkedListEnumerator class.

private class LinkedListEnumerator : IEnumerator
{
  private LinkedList  linkedList;
  private int         validModificationCount;
  private Node        currentNode;
  
  public LinkedListEnumerator(LinkedList linkedList)
  {
    this.linkedList         = linkedList;
    validModificationCount  = linkedList.modifications;
    currentNode             = linkedList.headerNode;
  }
  
  public object Current
  {
    get
    {
      return currentNode.CurrentNode;
    }
  }
  
  public void Reset()
  {
    currentNode = linkedList.headerNode;
  }

  public bool MoveNext()
  {
    bool moveSuccessful = false;

    if (validModificationCount != linkedList.modifications)
      throw new SystemException(
          "A concurrent modification occured to the LinkedList " + 
          "while accessing it through it's enumerator.");

    currentNode = currentNode.NextNode;

    if (currentNode != linkedList.headerNode)
      moveSuccessful = true;

    return moveSuccessful;
  }
}

The LinkedList(ICollection) constructor and the AddAll(ICollection) and InsertAll(int, ICollection) are there for convenience to the user of the list. This constructor calls AddAll(ICollection) which in turn calls InsertAll(int, ICollection). Below is the the code for this method.

public virtual void InsertAll(int index, ICollection collection)
{
  if (collection != null)
  {
    if (0 < collection.Count)
    {
      modifications++;

      Node startingNode = (index == count ? 
                  headerNode : FindNodeAt(index));
      Node previousNode = startingNode.PreviousNode;

      foreach (object obj in collection)
      {
        Node node             = new Node(obj, 
                  startingNode, previousNode);
        previousNode.NextNode = node;
        previousNode          = node;
      }

      startingNode.PreviousNode = previousNode;

      count += collection.Count;            
    }
    else
      throw new ArgumentOutOfRangeException("index", 
                   index, "less than zero");
  }
  else
    throw new ArgumentNullException("collection");
}

The LinkedList provides two methods for cloning. The first is the ICloneable interface Clone() method. It provides a shallow copy of the LinkedList. The second is Clone(bool attemptDeepCopy). It attempts to make a deep copy of the list if passed true, if false it will make a shallow copy. If an object in the list is not an ICloneable then it will throw a SystemException. The returned attempted deep copied list is not guaranteed to be a true deep copy. It defers the cloning to the objects own Clone() method. Here is the source for these two methods.

public virtual object Clone()
{
  LinkedList listClone = new LinkedList();

  for (Node node = headerNode.NextNode; node != headerNode; 
          node = node.NextNode)
    listClone.Add(node.CurrentNode);

  return listClone;
}

public virtual LinkedList Clone(bool attemptDeepCopy)
{
  LinkedList listClone;

  if (attemptDeepCopy)
  {
    listClone = new LinkedList();

    object currentObject;

    for (Node node = headerNode.NextNode; node != headerNode; 
           node = node.NextNode)
    {
      currentObject = node.CurrentNode;

      if (currentObject == null)
        listClone.Add(null);
      else if (currentObject is ICloneable)
        listClone.Add(((ICloneable)currentObject).Clone());
      else
        throw new SystemException("The object of type [" + 
                     currentObject.GetType() + 
                     "] in the list is not an ICloneable, cannot attempt " + 
                     "a deep copy.");
    } 
  }
  else
    listClone = (LinkedList)this.Clone();

  return listClone;
}

Conclusion

I believe this is a great class to learn how to implement your own custom collection. It is also very useful as it is, and ready to be included in your next project. The rest of the class is fairly straight-forward for a list collection. The class is fairly well commented using XML comment tags. Experienced and intermediate developers should have no trouble following the class.

This LinkedList is part of a set of .NET utilities I am developing, and have released as an open source project under the GNU General Public License (GPL). My complete project can be found at Source Forge.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Rodney S. Foley
Software Developer
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
AnswerOptimized doubly linked listmemberMember 94077375 Sep '12 - 3:34 
Doubly linked list can also be implemented where each node has only one pointer, Unlike the usual 2 pointers (prev-Next).. the single pointer doubly linked list should have the pointer which is XOR of the address
 
http://www.ritambhara.in/memory-efficient-doubly-linked-list/[^]
GeneralRe: Optimized doubly linked listmemberRodney S. Foley6 Sep '12 - 20:30 
This is a C# article (and old one at that) and you cannot use pointers directly to do what you are suggesting. Your link is purely for a C++ implementation so it is not applicable to post here about it.
 
Also you claim "a lot of memory savings", because you only use one pointer instead of two. A typical pointer is 4 bytes on 32 bit and 8 bytes on 64 bit (not talking about function pointers which are a little different).   So lets say you have a ridiculously large doubly linked list with 100,000 nodes. You only save only 390 KB on a 32 bit machine and 781 KB on a 64 bit machine. However with a more realistic size of lets say 100 you are only saving 0.39 KB on a 32 bit machine and 0.78 KB on a 64 bit machine.  
 
So even if you are on a embedded system the complexity added by using XOR logic to use only a single pointer is not worth it. Its a great research project or assignment in college but in the real world where you want maintainable code and the KISS principle is king.
GeneralMy vote of 5memberVistaaR22 Feb '11 - 0:55 
Very detailed , and crisp code. Not to mention one of its kind.
Thanks
GeneralMy vote of 1memberBen_Desjardins22 Jan '09 - 9:00 
think STL instead
GeneralThere is something in the code that is totally unnecessary.memberECDundy7 Jul '08 - 22:45 
This is totally unnecessary...
    Node node = headerNode;
 
    if (index < (count / 2))
    {
      for (int i = 0; i <= index; i++)
        node = node.NextNode;
    }
    else
    {
      for (int i = count; i > index; i--)
        node = node.PreviousNode;
    }
Try to imagine a more straightforward way to do the same.
if you cant...
just write me ECDundy@rediffmail.com...
GeneralThis might help!memberMember 335201122 Feb '08 - 17:40 
Go to wintellect.com. These guys have written some free libraries for collections and threading. Try using them. I hope they will help. Smile | :)
 
By the way they have been assigned this task by microsoft.
 
"Be peaceful, be courteous, obey the law, respect everyone; but if someone puts his hand on you, send him to the cemetery" - Malcolm X

QuestionLinked Controls?memberCoderJ22 Aug '07 - 12:25 
Is there any way of making linked controls in C#?
 

GeneralNot as fas as I expectedmemberphm379 Mar '07 - 3:41 
Every time I use ArrayList.Remove() I think about writing a doubly-linked list collection. On one such occasion a few days ago, I finally Googled and found your LinkedList.
 
Yours looks like a pretty classical implementation. I would think that moving a few pointers about should be faster than copying all or part of the list. Yet in my tests, LinkedList.Remove() seems to perform a lot worse than ArrayList.Remove() on a set of 10,000 objects.
 
My suspicion is that an Array.Copy() really isn’t all that expensive. Block copying is probably supported all the way down to the PC processor, and maybe further. Also, all that is being copied are object references.
 
Most of the cost in both Remove()s seems to be in searching for the object to be removed. In my tests, ArrayList.IndexOf() performs about twice as fast as LinkedList.IndexOf(). Both LinkedList and ArrayList do a linear search from top to bottom. ArrayList needs to perform one dereference and one comparison per iteration. LinkedList needs to perform two dereferences and two comparisons per iteration. Perhaps the difference lies therein.

GeneralJoint collection articlememberJonathan de Halleux30 Mar '04 - 1:33 
Hi,
 
This is a message I'm posting to the "collection" writers on CodeProject. Would you be interrested in making a joint article that would merge the different colleciton available on CP: there are already a bunch that we could collect in a single project/assembly, your linklist has definitely a place in there.
 
I know you have a sf site for this but I would like to keep this on CP since it would be a merge of different articles... Cheers,Jonathan.
 
Jonathan de Halleux - www.dotnetwiki.org -
MbUnit - QuickGraph

Generalsome optimizationsmemberTweety21 Feb '04 - 18:21 
1. why don't you make Node a struct? that way you overcome the overhead of an object-derived class and you avoid an additional pointer-dereference. You win nothing by wanting to only copy 1 pointer (4B) instead of 3 pointers (12B -- the next and previous + the actual held object) because you lose it at treversals and references. In a tight loop (i.e. game), it saves a few miliseconds
 
2. the code:
if (currentObject == null)
listClone.Add(null);
else if (currentObject is ICloneable)
listClone.Add(((ICloneable)currentObject).Clone());
else
throw new SystemException("The object of type [" +
currentObject.GetType() +
"] in the list is not an ICloneable, cannot attempt " +
"a deep copy.");

in the Clone(bool) can be written like this for a slight speed boost:
if (currentObject == null)
listClone.Add(null);
else
{
IClonable clonable=currentObject as IClonable;
if (clonable!=null)
listClone.Add(clonable.Clone());
else
throw new SystemException("The object of type [" +
currentObject.GetType() +
"] in the list is not an ICloneable, cannot attempt " +
"a deep copy.");
}

 
you'll avoid adding to the assembly a redundant as.
is is implemented as an as (sinonimous to the paranthesis notation of casting) and an if. so, in the original code, you'd have in the assembly 2 ass (Big Grin | :-D ). again, slight speed boost for tight loops.
 
Otherwise, great code, man!;)
 
/* Peace and love,
 *     Tweety
 */

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 4 Sep 2002
Article Copyright 2002 by Rodney S. Foley
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid