这个检测循环链表的函数的时间复杂度是多少?

Posted

技术标签:

【中文标题】这个检测循环链表的函数的时间复杂度是多少?【英文标题】:What is the time complexity for this function for detecting cyclic linked lists? 【发布时间】:2017-09-03 08:12:58 【问题描述】:

我刚刚尝试使用 unordered_map 来检测循环链表(我知道这是个坏主意)。

在我的名为linked_list 的类中,我的isCyclic() 函数的代码sn-p 是这样的:

    bool linked_list::isCyclic(Node * head)

  Node * trav = head;
  unordered_map<Node *, int > visited;

  while(trav != NULL)
  
    if(visited[trav->next_ptr] == 3)
        return true;
    visited[trav] = 3;
    trav = trav->next_ptr;
  
  return false;

对于每个新节点,我检查 next_ptr 是否指向已经访问过的节点,如果是,则返回 true 否则我只是将该节点设置为已访问并将 trav 更新为下一个要访问的节点,直到出现循环情况或者直到遍历所有节点。

我不知道这个算法的大 O 表示法是什么,因为根据我的计算,它的 O(n*n) 我猜是错误的,因为我并不总是那么准确。

【问题讨论】:

你是通过什么方式到达 O(n^2) 的?或者这是一个猜测,类似于你的结论是错误的? 因为我在一些论坛上读到的所有节点的 while 循环和 unordered_map 的时间复杂度是 O(n) 。这就是我认为它是 O(n^2) 的原因,但正如我之前提到的,我是这个主题的绝对初学者,所以我有 99% 的机会是错的。 【参考方案1】:

所提供算法的复杂度为O(n)。原因很简单,因为 unordered_map 和 unordered_set 都根据标准库的要求提供了恒定的插入、搜索和删除时间。因此,给定一个平均常数时间k,复杂度变为O(k*n),相当于k*O(n),最终具有O(n)的基本复杂度,因为常数k变得无关紧要。

为了证明这一点,以下示例将您的循环搜索简化为使用std::unordered_set 的最基本情况,这只不过是std::unordered_map,其中键映射到self。

bool check_for_cycle_short(const Node *p)

    std::unordered_set<const Node*> mm;
    for (; p && mm.insert(p).second; p = p->next);
    return p != nullptr;

请注意,std::unordered_set&lt;T&gt;::insert 返回 iterator,boolstd::pair,后者指示插入是否实际发生,因为在插入之前未找到密钥。

现在考虑这个例子,它是O(n^2)

bool check_for_cycle_long(const Node *p)

    for (; p; p = p->next)
    
        for (const Node *q = p->next; q; q = q->next)
        
            if (q == p)
                return true;
        
    
    return false;

这会用每个节点彻底搜索列表的其余部分,从而执行(n-1) + (n-2) + (n-3).... + (n-(n-1)) 比较。

示例

要查看这些操作,请考虑以下短程序,它加载一个包含 100000 个节点的链表,然后在最坏情况下检查两个循环(没有):

#include <iostream>
#include <iomanip>
#include <chrono>
#include <unordered_set>

struct Node

    Node *next;
;

bool check_for_cycle_short(const Node *p)

    std::unordered_set<const Node*> mm;
    for (; p && mm.insert(p).second; p = p->next);
    return p != nullptr;


bool check_for_cycle_long(const Node *p)

    for (; p; p = p->next)
    
        for (const Node *q = p->next; q; q = q->next)
        
            if (q == p)
                return true;
        
    
    return false;


int main()

    using namespace std::chrono;

    Node *p = nullptr, **pp = &p;
    for (int i=0; i<100000; ++i)
    
        *pp = new Node();
        pp = &(*pp)->next;
    
    *pp = nullptr;

    auto tp0 = steady_clock::now();
    std::cout << std::boolalpha << check_for_cycle_short(p) << '\n';
    auto tp1 = steady_clock::now();
    std::cout << std::boolalpha << check_for_cycle_long(p) << '\n';
    auto tp2 = steady_clock::now();

    std::cout << "check_for_cycle_short : " <<
        duration_cast<milliseconds>(tp1-tp0).count() << "ms\n";
    std::cout << "check_for_cycle_long  : " <<
        duration_cast<milliseconds>(tp2-tp1).count() << "ms\n";

输出(MacBook Air 双核 i7 @ 2.2GHz)

false
false
check_for_cycle_short : 36ms
check_for_cycle_long  : 7239ms

正如预期的那样,结果反映了我们的怀疑。

【讨论】:

感谢您的出色解释。我提供的代码不正确吗?我的意思是它没有为不同的测试用例提供正确的输出吗?

以上是关于这个检测循环链表的函数的时间复杂度是多少?的主要内容,如果未能解决你的问题,请参考以下文章

单链表循环链表双向链表的比较

初识链表(无头单向非循环链表的增删查改)

看动画理解「链表」实现LRU缓存淘汰算法

(java实现)双向循环链表

数据结构——双向链表循环链表

Python数据结构与算法(2.5)——循环链表