LoginSignup
5
1

More than 5 years have passed since last update.

プログラミングクイズ 循環リストの検出

Posted at

たまたま、遊びでプログラミング問題を解いてみました。一応解けたのですが、実行速度やメモリ使用量がよろしくなかったので、ちゃんと理解しようと思ってみました。自分のための理解したことのメモです。

問題の内容

循環リストの検出です。循環リストを含むかどうか?というのはどうやって検出するかです。

最初の回答

最初に考えたのは、結局のところ、LinkedList の next に対して、終端(この問題では、null) が存在したら、それは、循環じゃないと判断できます。ただし、循環だった場合は、永遠にループしちゃうので、要素を調べて、すでに出てきたものだったら、循環と判断すれば良いです。要素を、List にぶち込んで、Contains で調べたらええやろと思ってこんな感じでコード書きました。

public class Solution {
    private List<ListNode> list = new List<ListNode>();
    public bool HasCycle(ListNode head) {
        if (head == null) {
            return false;
        }
        if (head.next == null) {
            return false;
        }
        if(list.Contains(head)) {
            return true;
        }
        list.Add(head);
        return HasCycle(head.next);
    }
}

うーん。香ばしい。でも、一応動く。ただ、結果は当然のことながらよくなく。遅いし、メモリは使っとるし散々です。ソリューションの欄をみて、模範解答を勉強してみます。

Screen Shot 2019-04-28 at 7.11.47 PM.png

模範解答1(HashSet)

模範解答その1は、考え方は私と全く同じですが、再帰を使っていないのと、Listではなく、HashTableを使ってることです。そらそっちの方が効率がいいですね。O(1) のオーダーで検索してくれるので早いに決まってます。再帰は、やっぱちょい遅いのかなぁ。影響あるのかなと思いつつ、まずは、Hashに。速さはアップですね。まぁいい感じ。でもやっぱメモリがいまいち。そら、ライブラリのHashSet使ってるからそらそうか。じゃあ、再帰はどうなるんだろう。

Screen Shot 2019-04-28 at 8.30.05 PM.png

模範解答1(再帰をやめる)

public class Solution {
    private ISet<ListNode> set = new HashSet<ListNode>();
    public bool HasCycle(ListNode head) {
        while(head != null) {
            if (head == null || head.next == null) {
                return false;
            }
            if (set.Contains(head)) return true;
            set.Add(head);
            head = head.next;
        }
        return false;
    }
}

結果を見ると、速度的には変わらず、メモリ使用量が若干ましのようです。ループと再帰を比べると、スタックに積まれることを考えると、当然の結果もします。ただ、そこまで大きな影響があるわけじゃない様子です。
Screen Shot 2019-04-28 at 8.36.48 PM.png

模範解答2

模範解答その2は、有名なアルゴリズムらしく、ListNode のオブジェクトを2つ作って、最初のものは、Fast と呼んで、速度の係数2に、2つめは、速度の係数を1にすると、最大 循環したループの長さで、2つのポインタが出会うというアルゴリズムです。

何故こうなるか?というと、循環のリストは次のようなものです。

1-> 2 -> 3 -> 4 -> 2

この場合、2 -> 3 -> 4 -> 2 -> 3.... といった具合に循環します。循環のループの要素数は3でそれが繰り返されます。さて、1つの要素の速度を2、2つめを1にするとどういうことが起こるか?というと、最初の1をのぞいて、2つめの要素(遅い方)がループに入った時点で、早い方は、すでにループに入っています。その時点で、速いのと遅いのの間にどれぐらい距離があるかわかりません。(ループの長さや、ループに入るまでの距離によります。そのループに入った時点での、遅いのと、速いのの距離がNとすると、1回ロジックが繰り返されるたびに、距離が開いていきます。N,N+1,N+2といった具合です。ただ循環ということは、円になっていますので、循環のループの数をMとすると、N+i がMになった時点で、遅いのと速いのが出会うというわけです。

よくわからないと思うのでコードをみてみましょう。

public class Solution {
    public bool HasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false;
        }
        ListNode slow = head;
        ListNode fast = head.next;
        while(slow != fast) {
            if (fast == null || fast.next == null) {
                return false;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }
}

なんだこれは、凄い効率とスピードじゃないか、、、ライブラリは多くのコードを含むので、メモリはそうだろうなと思ったけど、速度もクソ速い。O(N)のオーダーなのは、前のアルゴリズムと変わらないけど、メモリは、O(1) のオーダーになったはず。同じO(N) のオーダーなのに速い理由はなんだろう。最短で、List size で終了するはずで、ループでも、ポジションP+ 循環要素数で終わるはず。

Screen Shot 2019-04-28 at 8.36.48 PM.png

ちなみに、このアルゴリズムは最初なかなか理解でき戦でしたが、このページで理解できました。

まとめ

アルゴリズムのイディオムすげぇ。こんなの絶対思いつかないから勉強するしかないなぁ。でも理解できて気持ちが良いです。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1