← 返回作品

Leetcode Summary 01 — Cycle Detection

Cycle DetectionFloyd Cycle Algorithmalgorithm

前言

前段时间忙于找工作,因而有正大光明的托词和导师说,11月之后再去做课题相关的事情,也就有了一段较为自由的时间。 其中诸多准备,免不了在Leetcode上刷题,虽然对于最终的结果来说没有帮助,但刷题过程中遇到了一些解决思路相似的问题,就依次将这些共性问题阐述一遍罢了。 本系列的第一篇介绍的是环检测问题,对应的维基百科页面有Cycle detection。这类问题最常见的求解算法是Floyd Cycle Algorithm/Floyd's Tortoise and Hare,中文名通常为Floyd判圈算法/龟兔赛跑算法。

<!--more-->

1. 问题描述

在计算机科学中,有一类问题称之为环检测问题,即对于一个由迭代函数(iterated function)值组成的序列,判断该序列是否有环及环出现的位置。 关于什么是迭代函数(iterated function),严格定义可以查看这里,简单来说就是一个函数ff,其定义域和值域都是集合XX,那么对于某个自变量/输入aa,其因变量/输出bb也可以作为函数ff的自变量/输入,如此给定一个初始值xx,将ff的每一次输出作为下一次的输入,如此重复nn次,就称作函数ff的第nn次迭代。

对于上述函数ff,如果其集合XX是有限的,那么对于序列:

S={x0,x1=f(x0),x2=f(x1),⋯ ,xi=f(xi−1),⋯ }S = \{x_0, x_1 = f(x_0), x_2 = f(x_1), \cdots, x_i = f(x_{i-1}), \cdots\}

那么必然会有两个位置ii和jj,且i<ji < j,其xi=xjx_i = x_j,一旦出现这种状况,显然在jj位置之后,会重复从xix_i到xj−1x_{j-1}的序列。所谓环检测问题,就是给定ff和x0x_0,要求找到ii和jj。 当然对于无限集合XX,是有可能不存在环的,这取决于ff和x0x_0的共同作用。例如f=x2f=x^2,如果XX的范围是复数域,那么x0=0,1,−1,e2πminx_0=0,1,-1,e^{\frac{2\pi{}mi}{n}},则有环,否则没有。这里mm的取值范围是整数,nn的取值范围是1,2,3,4,6,7,8,12,14,15,16,24,28,30,31,32,⋯{1,2,3,4,6,7,8,12,14,15,16,24,28,30,31,32,\cdots},就不仔细推导了,有兴趣的可以自行推导。

2. 求解算法

对于上述问题,最常见的算法是Floyd's Tortoise and Hare,其次还有Brent's algorithm和Gosper's algorithm等。 最直接的想法是记录每次迭代的值,建立一个hash表,这样可以在重复出现时直接定位,但是这种方法空间复杂度太高,故弃。

2.1 Floyd's Tortoise and Hare

对于问题描述中迭代函数组成的序列,如果存在环,那么对于i≥μi\ge{}\mu,则xi=xi+kλx_i = x_{i+k\lambda},其中λ\lambda是环的长度,μ\mu是环的第一个元素出现的位置。 基于此,可以推出,∃i=kλ≥μ⟹xi=x2i\exists{}i = k\lambda\ge\mu\Longrightarrow{}x_i = x_{2i} 因此,只要设定两个指针P1P1和P2P2,其中P2P2的步长是2,P1P1的步长是1,那么就可以找到两个指针指向值相等的位置,反过来说,即只要存在相等值,就存在环,此时可以得出ν=P2−P1=P1=kλ\nu = P2 - P1 = P1 = k\lambda. 显然xμ+ν=xμx_{\mu{}+\nu} = x_{\mu},即xν+μ=xμx_{\nu{}+\mu} = x_{\mu},故找到上述位置之后,P1P1仍保持原来位置,而P2P2放回序列最初位置,此时两个指针的推进步长都设置为1,两指针指向的值再次相等时,就是环开始的位置。找到环开始的位置后,向后迭代找到下一次重复位置,就可以得到环的长度λ\lambda了。 img01

2.2 Brent's algorithm

Floyd's Tortoise and Hare算法很精巧,但是判断环存在、找到环开始位置和确定环长度需要分为三步,Brent's algorithm效率更高,且只需要两步。 Brent's algorithm也是利用了快慢两个指针的想法,但是它的想法是动态增加步长,加快搜索速度。 其基本意图是设立两个指针,尽快让第一个指针到达环内,然后第二个指针以步长1前进,再次相等时走过的步数正好是环的长度λ\lambda,具体做法如下。 首先,设定两个指针P1P1和P2P2,P2P2第一次先前进20=12^0=1步,在前进的过程中若始终xP2≠xP1x_{P2}\ne{}x_{P1},则令P1=P2P1 = P2,然后P2P2再前进21=22^1=2,在前进的过程中若始终xP2≠xP1x_{P2}\ne{}x_{P1},则令P1=P2P1 = P2,然后P2P2再前进22=42^2=4...... 如此,直到出现相等的位置,在出现相等位置之前,最后一次P2P2前进的步数就是环的长度λ\lambda。 此时,将P1P1放于序列开始位置0,P2P2放于λ\lambda位置,由于xμ=xλ+μx_{\mu} = x_{\lambda{}+\mu},故只要让P1P1和P2P2依次向前前进,等到两者指向值相同时,P1P1的位置正好就是环的开始位置。 img02

Brent's algorithm的效率比Floyd's Tortoise and Hare要高,最差情况是Floyd's Tortoise and Hare算法。其主要原因是,步长动态增加,速度提高,以及首次找到相同值时,记录了环的长度,而Floyd's Tortoise and Hare算法首次找到相同值时,并不知道环的长度,只能知道环长度的整数倍。

2.3 Gosper's algorithm

Gosper算法可以参考此处,Kino还没有细看,就不细说了。

3. 相关练习

Leetcode上有以下数题与此相关。

3.1 Leetcode 141. Linked List Cycle

Link

Description: Given a linked list, determine if it has a cycle in it.

Floyd's Tortoise and Hare

class Solution {
public:
  bool hasCycle(ListNode *head) {
    if(!head || !(head->next)) {
      return false;
    }
    ListNode* tortoise = head->next;
    ListNode* hare = head->next->next;
    while(hare && hare->next) {
      if(tortoise == hare) {
        return true;
      }
      tortoise = tortoise->next;
      hare = hare->next->next;
    }
    
    return false;
  }
};

Brent's algorithm

class Solution {
public:
  bool hasCycle(ListNode *head) {
    if(!head || !(head->next)) {
      return false;
    }
    int current_step = 1;
    int limited_step = 1;
    ListNode* tortoise = head->next;
    ListNode* hare = head->next->next;
    while(hare && hare->next) {
      if(tortoise == hare) {
        return true;
      }
      if(current_step == limited_step) {
        current_step = 0;
        limited_step *= 2;
        tortoise = hare;
      }
      hare = hare->next;
      current_step++;
    }
    
    return false;
  }
};

3.2 Leetcode 142. Linked List Cycle II

Link

Description: Given a linked list, return the node where the cycle begins. If there is no cycle, return null.

Floyd's Tortoise and Hare

class Solution {
public:
  ListNode *detectCycle(ListNode *head) {
    if(!head || !(head->next)) {
      return NULL;
    }
    ListNode* tortoise = head->next;
    ListNode* hare = head->next->next;
    while(tortoise != hare) {
      if(!hare || !(hare->next)) {
        return NULL;
      }
      tortoise = tortoise->next;
      hare = hare->next->next;
    }
    
    tortoise = head;
    while(tortoise != hare) {
      tortoise = tortoise->next;
      hare = hare->next;
    }
    
    return tortoise;
  }
};

Brent's algorithm

class Solution {
public:
  ListNode *detectCycle(ListNode *head) {
    if(!head || !(head->next)) {
      return NULL;
    }
    int current_step = 1;
    int limited_step = 1;
    ListNode* tortoise = head->next;
    ListNode* hare = head->next->next;
    while(tortoise != hare) {
      if(!hare || !(hare->next)) {
        return NULL;
      }
      if(current_step == limited_step) {
        current_step = 0;
        limited_step *= 2;
        tortoise = hare;
      }
      hare = hare->next;
      current_step++;
    }
    
    tortoise = head;
    hare = head;
    while(current_step--) {
      tortoise = tortoise->next;
    }
    
    while(tortoise != hare) {
      tortoise = tortoise->next;
      hare = hare->next;
    }
    
    return tortoise;
  }
};

3.3 Leetcode 202. Happy Number

Link

Description: Write an algorithm to determine if a number is "happy". A happy number is a number defined by the following process: Starting with any positive integer, replace the number by the sum of the squares of its digits, and repeat the process until the number equals 1 (where it will stay), or it loops endlessly in a cycle which does not include 1. Those numbers for which this process ends in 1 are happy numbers.

Floyd's Tortoise and Hare

class Solution {
public:
  bool isHappy(int n) {
    int tortoise = nextNum(n);
    int hare = nextNum(nextNum(n));
    while(tortoise != hare) {
      tortoise = nextNum(tortoise);
      hare = nextNum(nextNum(hare));
    }
    
    return tortoise == 1;
  }
  
  int nextNum(int n) {
    int nxt = 0;
    while(n) {
      int digit = n%10;
      nxt += digit*digit;
      n /= 10;
    }
    
    return nxt;
  }
};

Brent's algorithm

class Solution {
public:
  bool isHappy(int n) {
    int current_step = 1;
    int limited_step = 1;
    int tortoise = nextNum(n);
    int hare = nextNum(nextNum(n));
    while(tortoise != hare) {
      if(current_step == limited_step) {
        current_step = 0;
        limited_step *= 2;
        tortoise = hare;
      }
      hare = nextNum(hare);
      current_step++;
    }
    
    return tortoise == 1;
  }
  
  int nextNum(int n) {
    int nxt = 0;
    while(n) {
      int digit = n%10;
      nxt += digit*digit;
      n /= 10;
    }
    
    return nxt;
  }
};

3.4 Leetcode 287. Find the Duplicate Number

Link

Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.

Floyd's Tortoise and Hare

class Solution {
private:
  vector<int> nums;
public:
  int findDuplicate(vector<int>& nums) {
    if(nums.empty()) {
      return -1;
    }
    this->nums = nums;
    
    int tortoise = f(nums[0]);
    int hare = f(f(nums[0]));
    while(tortoise != hare) {
      tortoise = f(tortoise);
      hare = f(f(hare));
    }
    
    tortoise = nums[0];
    while(tortoise != hare) {
      tortoise = f(tortoise);
      hare = f(hare);
    }
    
    return tortoise;
  }
  
  int f(int n) {
    return nums[n];
  }
};

Brent's algorithm

class Solution {
private:
  vector<int> nums;
public:
  int findDuplicate(vector<int>& nums) {
    if(nums.empty()) {
      return -1;
    }
    this->nums = nums;
    
    int current_step = 1;
    int limited_step = 1;
    int tortoise = f(nums[0]);
    int hare = f(f(nums[0]));
    while(tortoise != hare) {
      if(current_step == limited_step) {
        current_step = 0;
        limited_step *= 2;
        tortoise = hare;
      }
      hare = f(hare);
      current_step++;
    }
    
    tortoise = nums[0];
    hare = nums[0];
    while(current_step--) {
      tortoise = f(tortoise);
    }
    
    while(tortoise != hare) {
      tortoise = f(tortoise);
      hare = f(hare);
    }
    
    return tortoise;
  }
  
  int f(int n) {
    return nums[n];
  }
};

4. 实际应用

环检测(cycle detection)问题的实际应用有 伪随机数生成器(pseudorandom number generators)强度的度量,比如 线性同余生成器(linear congruential generator),加密哈希函数的冲突检测,细胞自动机(cellular automaton)的振荡周期配置等