Tuesday 16 March 2010

Custom Doubly Linked List in C#

Of course you can use the generic LinkedList(T) class instead of this custom implementation for a specific project of mine and I thought I would like to blog it for future references.

The custom IDoubleLinkedNode<T> Interface:

/// <summary>

/// Node interface to support the DoubleLinkedList class

/// </summary>

/// <typeparam name="T"></typeparam>

public interface IDoubleLinkedNode<T>

: IComparable<T>, IEquatable<T>

{

T Next { get; set; }

T Previous { get; set; }

}

The custom DoubleLinkedList<T> class:

/// <summary>

/// Custom doubly linked list.

/// This is created to replace the Generic LinkedList(T) so that

/// the target object implements the node interface

/// (Next, Previous etc...) IDoubleLinkedNode

/// instead of being wrapped in a LinkedListNode object.

/// This also supports a custom implementation of Merged Sort

/// Algorithm for sorting the list

/// which is unavailable in the Generic LinkedList(T),

/// in that case, will have to be extended.

/// </summary>

/// <typeparam name="T">IDoubleLinkedNode type</typeparam>

public class DoubleLinkedList<T> : ICollection<T>

where T : IDoubleLinkedNode<T>

{

// Holds the first item.

private T head;

// Holds the last item to allow us add fast.

private T tail;

private int count;

#region Sort

/// <summary>

/// Sort the linked list using merge sort based

/// on the IComparable implementation

/// </summary>

public void Sort()

{

head = Merge_Sort(head);

}

#region Private Merge Sort

/// <summary>

/// Initiate a merge sort on the doublylinkedlist node item

/// Based on the IComparable ordering rule

/// </summary>

/// <param name="first"></param>

/// <returns></returns>

private T Merge_Sort(T first)

{

// If node is empty or single, then we don't need to sort

if (first == null || first.Next == null)

return first;

// Get the next item

T second = Split(first);

first = Merge_Sort(first);

second = Merge_Sort(second);

return Merge(first, second);

}

/// <summary>

/// Split the Linked Node into 2 parts, mainly

/// alternating through the pointers into 2 seperate

/// link nodes. Eg. 1 -> 2 -> 3 -> 4 -> 5 will be splitted

/// into 1 -> 3 -> 5 and 2 -> 4 respectively

/// </summary>

/// <param name="item"></param>

/// <returns></returns>

private T Split(T item)

{

// If it is the last item, we will return nothing

if (item == null || item.Next == null)

return default(T);

// Assign alternate items to the second item

T second = item.Next;

// Assign alternate items pointers to the current item

item.Next = second.Next;

// Continue assigning the alternate pointers

second.Next = Split(second.Next);

// Return the second concatenated list

return second;

}

/// <summary>

/// Perform merge of the first and second item based on the

/// </summary>

/// <param name="first"></param>

/// <param name="second"></param>

/// <returns></returns>

private T Merge(T first, T second)

{

if (first == null)

return second;

if (second == null)

return first;

// If both are not null, we will run the comparer

// If first is less than second

if (first.CompareTo(second) < 0)

{

first.Next = Merge(first.Next, second);

first.Next.Previous = first;

first.Previous = default(T);

return first;

}

else

{

second.Next = Merge(first, second.Next);

second.Next.Previous = second;

second.Previous = default(T);

return second;

}

}

#endregion

#endregion

#region [] accessor

/// <summary>

/// This is for convenience

/// </summary>

/// <param name="index"></param>

/// <returns></returns>

public T this[int index]

{

get

{

// Throw an exception if the index is not valid.

if (index < 0)

throw new ArgumentOutOfRangeException();

// Get the first item.

var item = head;

// Loop until we reach the item we want.

for (int i = 0; i < index; i++)

{

// If there is no next item throw an exception.

if (item.Next == null)

throw new ArgumentOutOfRangeException();

// Go to the next item.

item = item.Next;

}

// Return the value of the item.

return item;

}

}

#endregion

#region ICollection<T> Members

public void Add(T value)

{

if (value == null)

throw new ArgumentNullException("value");

var item = value;

// Check if we need to update the first and last

// fields.

if (head == null)

head = item;

if (tail != null)

{

tail.Next = item;

item.Previous = tail;

}

tail = item;

count++;

}

public void Clear()

{

head = default(T);

tail = default(T);

count = 0;

}

public bool Contains(T value)

{

var item = head;

// Loop until we reach the end of the list.

while (item != null)

{

// Check if we match.

if (item.Equals(value))

return true;

// Otherwise go on.

item = item.Next;

}

return false;

}

public void CopyTo(T[] array, int arrayIndex)

{

throw new NotImplementedException();

}

public int Count

{

get { return count; }

}

public bool IsReadOnly

{

get { return false; }

}

public bool Remove(T value)

{

if (head == null)

return false;

// Get the first item.

var item = head;

// Check if the first item matches.

if (item.Equals(value))

{

// If so move the next item to the first one.

head = item.Next;

head.Previous = default(T);

count--;

// If first is null the list has been cleared.

// Last is also to set null.

if (head == null)

tail = default(T);

return true;

}

else

{

// Loop until we reach the end.

while (item.Next != null)

{

// Check the values.

if (item.Next.Equals(value))

{

// If we match store the one after the next

// as the next one.

item.Next = item.Next.Next;

// If this is not the last item,

// then we set the previous value

if (item.Next != null)

item.Next.Previous = item;

count--;

// If the next item is null, set this item

// as the last one.

if (item.Next == null)

tail = item;

return true;

}

// Move to the next item.

item = item.Next;

}

}

return false;

}

#endregion

#region IEnumerable<T> Members

public IEnumerator<T> GetEnumerator()

{

// Get the first item.

var item = head;

// Loop until we reach the end.

while (item != null)

{

// Return the item.

yield return item;

// Move to the next one.

item = item.Next;

}

}

#endregion

#region IEnumerable Members

IEnumerator IEnumerable.GetEnumerator()

{

return GetEnumerator();

}

#endregion

}

4 comments:

Vozzie said...
This comment has been removed by the author.
Vozzie said...
This comment has been removed by the author.
Vozzie said...

Hi, first of all, great work. It's the first Merge i examine and it' doesn't look bad but a question raises,...

In the Sort method you call "Merge_Sort" and adjust the "head" item of the doubly linked list. Didn't you forget to adjust the tail too? (not that the tail is used in a way this causes a bug, it could later...)

But anyway, great work! I want to implement this in another "language" and it's hard... :) Thx...

Anonymous said...

I ran into the same problem where the tail is not properly set after the merge sort. Still trying to find a way to keep track of it during the sorting process (not sure if that's even more efficient) but in the meantime, I'm just iterating through the list from the beginning after the sort until "next" is not null.