3

I want to find an algorithm which checks a linked-list with n elements for consistency. The linked-list uses a dummy head (also known as sentinel-node). The algorithm needs to run in O(n) time and is allowed to use O(1) extra space apart from the space needed to iterate through the list. The size of the list is unknown. In addition it's forbidden to modify the list.

A list counts as inconsistent if there is a list item which points at a previous list item. First I thought about storing the first element and then iterate through the list while comparing the current element with the first one.

GrahamS
  • 9,980
  • 9
  • 49
  • 63
Elfie
  • 374
  • 3
  • 15
  • Do you know `n` in advance ? – joop May 12 '16 at 08:33
  • no because the size of the list is unknown. So the algorithm should be capable to check for consistency for any size (or n elements) – Elfie May 12 '16 at 08:38
  • 1
    Well, in that case Floyd's algorithm is the best you can get (without using additional storage) – joop May 12 '16 at 08:42
  • Agree with joop. Floyd's Tortoise and Hare does what you want. https://en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare – GrahamS May 12 '16 at 10:46

6 Answers6

3

Does the list provide a Size property that tells you how many elements it contains (n) without traversing it?

If it does then a simple solution that meets all your big-O requirements is to attempt to traverse the list, counting the elements as you go. If the count exceeds the expected number of elements in the list then it has a loop.

Pseudo-code would look something like this:

bool isConsistent (List list)
{
    bool consistent = true;
    Node node = list.Sentinel.Next;
    int count = 0;

    while (node != list.Sentinel && consistent)
    {
         count++;

         if (count > list.Size)
             consistent = false;

         node = node.Next;
    }

    return consistent;
}

That completes in O(n) and uses O(1) storage.

GrahamS
  • 9,980
  • 9
  • 49
  • 63
  • I was just thinking about the same idea. If the Count is available, then this is the correct answer. You can take advantage of the circularity of the list and break when you exceed the Count value – Frode May 12 '16 at 09:37
  • Okay @joop if no size/count is available then I think you are right that Floyd's Tortoise and Hare algorithm is probably the correct/expected solution, but the OP doesn't seem to think so. – GrahamS May 12 '16 at 10:23
2

Floyd's "Tortoise and Hare" algorithm does what you need and only needs a small modification to work with your dummy head/tail (sentinel) node.

Here is how I would write the pseudo-code:

bool IsConsistent (List list)
{
    Node tortoise = list.Sentinel.Next;
    Node hare = tortoise.Next;

    while (tortoise != list.Sentinel && hare != list.Sentinel)
    {
        if (tortoise == hare)
            return false;

        tortoise = tortoise.Next;
        hare = hare.Next.Next;
    }

    return true;
}
GrahamS
  • 9,980
  • 9
  • 49
  • 63
  • I thought about finding out at which position in the list the inconsistency occurs. For example: if there is no inconsistency we return -1 else we return the specific index. I know that such an algorithm needs more time, maybe O(n^2) or o(n^2). So just to simplify this one, let's assume that the size of the list is known. – Elfie May 12 '16 at 12:26
  • 1
    Look at the Wiki page I linked. The rest of the Tortoise and Hare algorithm describes how to find the index of the loop (μ) and the length of the loop (λ). – GrahamS May 12 '16 at 12:29
  • I added a second answer for this question. Could you please have a look at it? – Elfie May 12 '16 at 13:14
1

You will need some RAM for that if you don't have a Visited property on each item in the linked list. If you have a Visited property you will first need to clear it before running the algorithm. This will probably not fit your big-O requirements.

It's not clear what you mean with "points at previous list item". Is equal by reference (object) or same value/set of property values (struct)? I assume reference. The code below can easily be modified to handle structs as well.

static void Main(string[] args)
{
    var list = BuildALinkedListFromSomeData();
    var isConsitent = IsConsistent(list);
}

static bool IsConsistent(LinkedList<Item> list)
{ 
    var visited = new List<LinkedListNode<Item>>()
    var runner = list.First;
    while(runner != null)
    {
        if (visited.Contains(runner))
            return false;
        visited.Add(runner);
        runner = runner.Next;
    }
    return true;
}

A O(n) solution that uses an existing numeric VisitCounter that already uses storage space (no additional storage needed):

static bool IsConsistent(LinkedList<Item> list)
{
    var runner = list.First;
    if (runner == null)
        return false;  // Assume consistent if empty

    var consistent = true; 
    var runId = runner.Value.VisitCount;
    while (runner != null)
    {
        // Does the traversed item match the current run id?
        if(runner.Value.VisitCount > runId)
        {
            // No, Flag list as inconsistent. It must have been visited previously during this run
            consistent = false;
            // Reset the visit count (so that list is ok for next run)
            runner.Value.VisitCount = runId; 
        }
        // Increase visit count
        runner.Value.VisitCount++;
        // Visit next item in list
        runner = runner.Next;
    }
    return consistent;
}

This makes changes to the content of an item in the list, but not the list itself. If you're not allowed to change the content of an item in the list, then of course this is not a solution either. Well, second-thought, this is not a possible solution at all. When inconsistent, your list is circular and the last algorithm will never finish :)

You will then have to traverse the list backwards from each visited item in your list and this will break your O(n+1) requirement.

Conclusion: Not so Mission Impossible if Count is available. See GrahamS' answer

Frode
  • 3,325
  • 1
  • 22
  • 32
  • there is a visited property, but it's not allowed to be used because this would implicate a modification of the list.... thought about that method too :-/ – Elfie May 12 '16 at 08:40
  • 1
    If you are allowed to build an array of visited items outside your list, then this code should work. Below or at O(n) – Frode May 12 '16 at 08:42
  • with "points at previous list item" I mean the following: It is known that every element of a linked-list points at the next element of the list. Imagine that there is an element inside the list with the index "a" and if this element points at another element with a lower index "b" (a < b), then the list is inconsistent. – Elfie May 12 '16 at 08:50
  • Doesn't an array (or list) of visited items violate the the O(1) storage requirement? Presumably the array would scale with the size of the list, so it would be O(n). If this is an assignment then I think you are probably just meant to use the Visited property. – GrahamS May 12 '16 at 08:50
  • @GrahamS About the storage requirement, you have O(n) space for the list you iterate through and additionally you get O(1) of extra space to write this algorithm. Am I right with the assumption, that by storing a second list of visited nodes, we have a bigger storage requirement as allowed? – Elfie May 12 '16 at 09:02
  • GrahamS: I agree. But then, if you are not allowed to cache visited items, you need to traverse the list backwards at each item, which would break the O(n+1) requirement. – Frode May 12 '16 at 09:05
  • There is maybe one way to solve it. If you have a Visited property that is an integer, you could increase the value by one when you traverse the list. Then of course it will be O(n) because you need to set the Visited-counter correctly for all nodes even if you find any inconsistencies – Frode May 12 '16 at 09:10
  • @Frode you are allowed to cache the visited items but you are not allowed to modify the list in any way. So it's allowed to create a second list with all the visited elements. Every item has a visited property but you are not allowed to modify it. There wouldn't be any problem at all for me to solve this if I was allowed to use the visited property. – Elfie May 12 '16 at 09:11
  • 1
    That makes no sense @Snelfie. Creating a second list of visited elements requires storage that increases with the size of the list, so it is not O(1). – GrahamS May 12 '16 at 09:14
1

Here is my solution for the second question.

IsConsistent(LinkedList<Item> list) :N
    slow = List.Sentinel.next :Element
    fast = slow.next :Element
    isConsistent = true :boolean
    while(fast != list.Sentinel && fast.next != list.Sentinel && isConsistent) do
        if(slow == fast)
            isConsistent = false
        else 
            slow:= slow.next
            fast:= fast.next.next 
    od
    if(isConsistent)
        return 0
    else
        position = 0 : N
        slow:= list.Sentinel
        while(slow != fast) do
            slow:= slow.next
            fast:= fast.next
            position:= position + 1
        od
        return position
Elfie
  • 374
  • 3
  • 15
  • 1
    You should only execute the second while loop that finds the position of the cycle when the first while loop has found a cycle, otherwise it will never terminate because without a cycle `slow` can never equal `fast`. – GrahamS May 12 '16 at 13:47
  • 1
    That looks better. You don't really need the `else` clause since it returns early if isConsistent, but it is okay to leave it in. I would probably rename the function to something like `FindCyclePosition` and change the return type to N. – GrahamS May 12 '16 at 14:16
0

Basically, pointing a previous item means having a loop inside the list. In this case, loop check is seem appropriate.

fcat
  • 1,251
  • 8
  • 18
  • It is obvious that I have to check for a loop. Without loop there is no inconsistency. The thing is that there is always a loop inside a linked-list with dummy head. So how to I find out if there is only the loop created by the dummy head or if there is another loop creating inconsistency? – Elfie May 12 '16 at 08:24
  • the dummy head points at the first element of the linked-list and the last element of the list points back at the dummy head (sentinel-node) – Elfie May 12 '16 at 08:26
0

At first I didn't thought about using list.Sentinel. Now I got a new idea.

IsConsistent(LinkedList<Item> list) :boolean
    slow = List.Sentinel.next :Element
    fast = slow.next :Element
    while(slow != list.Sentinel && fast != list.Sentinel) do
        if(slow == fast) return false
        else 
              slow:= slow.next
              fast:= fast.next.next 
    od
    return true
Elfie
  • 374
  • 3
  • 15
  • That's more or less the start of the Tortoise and Hare algorithm, but the OP seems to reject that in the question. https://en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare – GrahamS May 12 '16 at 10:26
  • 2
    It is also trivial. The (ugly!) headnode/sentinel is only a minor detail. @GrahamS : this **is** the OP ... – joop May 12 '16 at 10:27
  • Why check `slow` for null? When would that happen? The use of a sentinel node should avoid nulls so `slow` should just start at `Sentinel.next`. And why check other conditions in the loop, you could just do `while (slow != list.Sentinel && fast != list.Sentinel)` – GrahamS May 12 '16 at 10:38
  • @GrahamS you say that the use of a sentinel avoids nulls. Imagine the **list is empty** and you initialize **slow = list.Sentinel.next**. Would this lead to the case that slow is initialized as list.Sentinel? If I'm not mistaking: **the Sentinel points at himself while the list is empty**... – Elfie May 12 '16 at 11:13
  • 1
    Yes that's right - if the list was empty then `slow` and `fast` would both be initialised to `list.Sentinel`, so the `while` loop would not execute and the function would return true. – GrahamS May 12 '16 at 11:17
  • I've added an answer that shows how I would write that out as it is easier than explaining it in the comments. – GrahamS May 12 '16 at 11:37