【问题标题】:Algorithm to check a linked-list for loops检查链表中是否存在循环的算法
【发布时间】:2016-09-07 22:19:49
【问题描述】:

我想找到checks a linked-list with n elements for consistency 的算法。链表使用dummy head(也称为哨兵节点)。该算法需要run in O(n) time 并且允许use O(1) extra space 除了遍历列表所需的空间。列表is unknownsize。另外它是forbidden to modify the list

如果存在指向前一个列表项的列表项,则列表计为不一致。 首先,我考虑存储第一个元素,然后在将当前元素与第一个元素进行比较时遍历列表。

【问题讨论】:

  • 你知道n吗?
  • 否,因为列表的大小未知。所以算法应该能够检查任何大小(或 n 个元素)的一致性
  • 好吧,在这种情况下,弗洛伊德的算法是你能得到的最好的算法(不使用额外的存储空间)
  • 同意乔普。弗洛伊德的龟兔赛跑可以满足您的需求。 en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare

标签: algorithm performance linked-list singly-linked-list sentinel


【解决方案1】:

列表是否提供 Size 属性,告诉您它包含多少个元素 (n) 而无需遍历它?

如果确实如此,那么满足您所有 big-O 要求的简单解决方案是尝试遍历列表,同时计算元素。如果计数超过列表中预期的元素数,则它有一个循环。

伪代码看起来像这样:

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;
}

这在 O(n) 内完成并使用 O(1) 存储。

【讨论】:

  • 我只是在想同样的想法。如果 Count 可用,那么这是正确的答案。您可以利用列表的循环性并在超过 Count 值时中断
  • 好吧@joop,如果没有可用的大小/计数,那么我认为弗洛伊德的龟兔赛跑算法可能是正确/预期的解决方案是对的,但 OP 似乎不这么认为。
【解决方案2】:

Floyd's "Tortoise and Hare" algorithm 可以满足您的需要,只需稍作修改即可使用您的虚拟头/尾(哨兵)节点。

下面是我将如何编写伪代码:

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;
}

【讨论】:

  • 我想找出不一致发生在列表中的哪个位置。例如:如果没有不一致我们返回 -1 否则我们返回特定的索引。我知道这样的算法需要更多的时间,可能是 O(n^2) 或 o(n^2)。因此,为了简化这一点,我们假设列表的大小是已知的。
  • 查看我链接的 Wiki 页面。 Tortoise and Hare 算法的其余部分描述了如何找到循环的索引 (μ) 和循环的长度 (λ)。
  • 我为这个问题添加了第二个答案。你能看一下吗?
【解决方案3】:

如果您在链接列表中的每个项目上都没有 Visited 属性,您将需要一些 RAM。 如果您有一个已访问的属性,您首先需要在运行算法之前清除它。这可能不符合您的 big-O 要求。

不清楚“指向上一个列表项”是什么意思。通过引用(对象)或相同的值/属性值集(结构)是否相等?我假设参考。下面的代码也可以很容易地修改为处理结构。

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;
}

使用已使用存储空间(无需额外存储)的现有数字 VisitCounter 的 O(n) 解决方案:

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;
}

这会更改列表中项目的内容,但不会更改列表本身。如果您不允许更改列表中某个项目的内容,那么这当然也不是解决方案。好吧,再想一想,这根本不是一个可能的解决方案。当不一致时,您的列表是循环的,最后一个算法永远不会完成:)

然后,您必须从列表中的每个访问项目向后遍历列表,这将打破您的 O(n+1) 要求。

结论:如果 Count 可用,则不是那么不可能的任务。查看 GrahamS 的回答

【讨论】:

  • 有一个访问过的属性,但不允许使用它,因为这会涉及到列表的修改......也考虑过那个方法:-/
  • 如果你被允许在你的列表之外构建一个访问过的项目的数组,那么这个代码应该可以工作。低于或在 O(n)
  • 访问项目的数组(或列表)是否违反了 O(1) 存储要求?大概数组会随着列表的大小而缩放,所以它会是 O(n)。如果这是一项任务,那么我认为您可能只是打算使用 Visited 属性。
  • 与“指向上一个列表项”我的意思如下:众所周知,链表的每个元素都指向列表的下一个元素。假设列表中有一个索引为“a”的元素,如果该元素指向另一个索引为“b”较低的元素(a
  • 这毫无意义@Snelfie。创建第二个访问元素列表需要随列表大小增加的存储空间,因此不是 O(1)。
【解决方案4】:

这是我对第二个问题的解决方案。

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

【讨论】:

  • 你应该只在第一个while循环找到循环时才执行找到循环位置的第二个while循环,否则它永远不会终止,因为没有循环slow永远不会等于@987654323 @.
  • 看起来更好。您实际上并不需要 else 子句,因为如果 isConsistent 它会提前返回,但可以保留它。我可能会将函数重命名为 FindCyclePosition 并将返回类型更改为 N。
【解决方案5】:

基本上,指向前一个项目意味着在列表中有一个循环。在这种情况下,循环检查似乎是合适的。

【讨论】:

  • 很明显我必须检查一个循环。没有循环就没有不一致。问题是在带有虚拟头的链表中总是有一个循环。那么如何确定是否只有虚拟头创建的循环或是否有另一个循环造成不一致?
  • 虚拟头指向链表的第一个元素,链表的最后一个元素指向虚拟头(哨兵节点)
【解决方案6】:

一开始我并没有考虑使用 list.Sentinel。现在我有了一个新想法。

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

【讨论】:

  • 这或多或少是龟兔算法的开始,但 OP 似乎在问题中拒绝了这一点。 en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare
  • 这也是微不足道的。 (丑陋的!)头节点/哨兵只是一个小细节。 @GrahamS:这 OP ...
  • 为什么要检查slow 是否为空?那什么时候会发生?哨兵节点的使用应该避免空值,所以slow 应该从Sentinel.next 开始。为什么要检查循环中的其他条件,你可以这样做while (slow != list.Sentinel &amp;&amp; fast != list.Sentinel)
  • @GrahamS 你说使用哨兵可以避免空值。想象一下 list 是空的,而你初始化了 slow = list.Sentinel.next。这样会不会导致slow被初始化为list.Sentinel的情况?如果我没记错的话:当列表为空时,哨兵指向自己...
  • 是的,没错——如果列表为空,那么slowfast 都将被初始化为list.Sentinel,因此while 循环不会执行并且函数将返回true。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-06-18
  • 2011-11-15
  • 1970-01-01
  • 2012-09-21
  • 2010-11-09
  • 1970-01-01
相关资源
最近更新 更多