Javaにリンクリスト構造があるとしましょう。それはノードで構成されています。
class Node {
Node next;
// some user data
}
そして各ノードは、最後のノードを除いて次のノードを指します。リストにループが含まれている可能性があるとします。つまり、最後のノードは、nullを持たずに、リスト内の前のノードの1つを参照しています。
書くのに最適な方法は何ですか
boolean hasLoop(Node first)
与えられたNodeがループを含むリストの最初のものであればtrue
を返し、そうでなければfalse
を返します。一定量のスペースと妥当な時間がかかるようにするには、どうすればよいでしょうか。
これが、ループを含むリストの外観図です。
Floydのサイクル検出アルゴリズム を使用することもできます。これは亀とも呼ばれます。野ウサギのアルゴリズム。
この考えは、リストへの2つの参照を持ち、それらを異なる速度で移動することです。一方を1
ノード、もう一方を2
ノードで進めます。
next
)のどちらかがnull
になります。アルゴリズムを実装するJava関数
boolean hasLoop(Node first) {
if(first == null) // list does not exist..so no loop either
return false;
Node slow, fast; // create two references.
slow = fast = first; // make both refer to the start of the list
while(true) {
slow = slow.next; // 1 hop
if(fast.next != null)
fast = fast.next.next; // 2 Hops
else
return false; // next node null => no loop
if(slow == null || fast == null) // if either hits null..no loop
return false;
if(slow == fast) // if the two ever meet...we must have a loop
return true;
}
}
これは、奇数長のリストを正しく処理し、明瞭さを向上させるFast/Slowソリューションの改良版です。
boolean hasLoop(Node first) {
Node slow = first;
Node fast = first;
while(fast != null && fast.next != null) {
slow = slow.next; // 1 hop
fast = fast.next.next; // 2 Hops
if(slow == fast) // fast caught up to slow, so there is a loop
return true;
}
return false; // fast reached null, so the list terminates
}
私が一時的にリストを変更しているので、TurtleとRabbitの代わりの解決策。
考えはリストを歩き、あなたが行くと同時にそれを逆にすることである。それから、あなたが最初に訪れたことのあるノードに最初に到達したとき、その次のポインタは "後方"を指し、反復が再びfirst
に向かって進み、そこで終了します。
Node prev = null;
Node cur = first;
while (cur != null) {
Node next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
boolean hasCycle = prev == first && first != null && first.next != null;
// reconstruct the list
cur = prev;
prev = null;
while (cur != null) {
Node next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return hasCycle;
テストコード:
static void assertSameOrder(Node[] nodes) {
for (int i = 0; i < nodes.length - 1; i++) {
assert nodes[i].next == nodes[i + 1];
}
}
public static void main(String[] args) {
Node[] nodes = new Node[100];
for (int i = 0; i < nodes.length; i++) {
nodes[i] = new Node();
}
for (int i = 0; i < nodes.length - 1; i++) {
nodes[i].next = nodes[i + 1];
}
Node first = nodes[0];
Node max = nodes[nodes.length - 1];
max.next = null;
assert !hasCycle(first);
assertSameOrder(nodes);
max.next = first;
assert hasCycle(first);
assertSameOrder(nodes);
max.next = max;
assert hasCycle(first);
assertSameOrder(nodes);
max.next = nodes[50];
assert hasCycle(first);
assertSameOrder(nodes);
}
フロイドのアルゴリズムより良い
Richard Brentは 代替サイクル検出アルゴリズム を説明しました。これは、ここでは遅いノードが動かず、後で「テレポート」されることを除いて、ウサギと亀[Floyd's cycle]とほとんど同じです。固定ノードの高速ノードの位置.
説明はこちらから入手できます。 http://www.siafoo.net/algorithm/11 Brentは、彼のアルゴリズムはフロイドのサイクルアルゴリズムよりも24〜36%速いと主張しています。 O(n)時間複雑度、O(1)空間複雑度。
public static boolean hasLoop(Node root){
if(root == null) return false;
Node slow = root, fast = root;
int taken = 0, limit = 2;
while (fast.next != null) {
fast = fast.next;
taken++;
if(slow == fast) return true;
if(taken == limit){
taken = 0;
limit <<= 1; // equivalent to limit *= 2;
slow = fast; // teleporting the turtle (to the hare's position)
}
}
return false;
}
Pollard's rho algorithm を見てください。これはまったく同じ問題ではありませんが、その論理を理解してリンクリストに適用することもできます。
(あなたが怠惰な場合は、あなただけのチェックアウトすることができます 周期検出 - カメやウサギについての部分を確認してください。)
これには線形時間と2つの追加ポインタが必要です。
Javaの場合:
boolean hasLoop( Node first ) {
if ( first == null ) return false;
Node turtle = first;
Node hare = first;
while ( hare.next != null && hare.next.next != null ) {
turtle = turtle.next;
hare = hare.next.next;
if ( turtle == hare ) return true;
}
return false;
}
(ほとんどの解決策はnext
とnext.next
の両方がnullかどうかをチェックしません。また、カメは常に遅れているので、nullをチェックする必要はありません。うさぎはすでに行っています。)
ユーザー nicornaddict には上記のNiceアルゴリズムがありますが、残念なことに、長さが3以上の非ループリストのバグが含まれています。問題は、fast
が直前に「スタック」する可能性があることですリストの終わり、slow
がそれに追いつき、ループが(間違って)検出されます。
これが修正されたアルゴリズムです。
static boolean hasLoop(Node first) {
if(first == null) // list does not exist..so no loop either.
return false;
Node slow, fast; // create two references.
slow = fast = first; // make both refer to the start of the list.
while(true) {
slow = slow.next; // 1 hop.
if(fast.next == null)
fast = null;
else
fast = fast.next.next; // 2 Hops.
if(fast == null) // if fast hits null..no loop.
return false;
if(slow == fast) // if the two ever meet...we must have a loop.
return true;
}
}
アルゴリズム
public static boolean hasCycle (LinkedList<Node> list)
{
HashSet<Node> visited = new HashSet<Node>();
for (Node n : list)
{
visited.add(n);
if (visited.contains(n.next))
{
return true;
}
}
return false;
}
複雑さ
Time ~ O(n)
Space ~ O(n)
以下は最善の方法ではないかもしれません - それはO(n ^ 2)です。しかし、それは(最終的には)仕事を終わらせるのに役立つはずです。
count_of_elements_so_far = 0;
for (each element in linked list)
{
search for current element in first <count_of_elements_so_far>
if found, then you have a loop
else,count_of_elements_so_far++;
}
public boolean hasLoop(Node start){
TreeSet<Node> set = new TreeSet<Node>();
Node lookingAt = start;
while (lookingAt.peek() != null){
lookingAt = lookingAt.next;
if (set.contains(lookingAt){
return false;
} else {
set.put(lookingAt);
}
return true;
}
// Inside our Node class:
public Node peek(){
return this.next;
}
私の無知を許してください(私はまだJavaとプログラミングにかなり新しいです)、しかしなぜ上記の仕事はしないでしょうか?
これで一定のスペースの問題は解決されないと思いますが、少なくとも妥当な時間内にそこにたどり着くことができるでしょうか。リンクリストのスペースと、n個の要素を含むセットのスペースの合計(nは、リンクリストの要素数、またはループに達するまでの要素数)だけを取ります。そして、時間がたてば、最悪の場合の分析はO(nlog(n))を提案すると私は思います。 contains()に対するSortedSetの検索はlog(n)です(javadocを確認してください)。ただし、TreeSetの基本構造はTreeMapで、その逆も赤黒のツリーです)。または最後にループすると、n回の検索が必要になります。
クラスNode
を埋め込むことが許可されているのであれば、以下で実装したように問題を解決します。 hasLoop()
はO(n)時間内に実行され、counter
のスペースのみを取ります。これは適切な解決策のようですか?それともNode
を埋め込むことなくそれを行う方法はありますか? (明らかに、実際の実装ではRemoveNode(Node n)
などのようなより多くのメソッドがあるでしょう)
public class LinkedNodeList {
Node first;
Int count;
LinkedNodeList(){
first = null;
count = 0;
}
LinkedNodeList(Node n){
if (n.next != null){
throw new error("must start with single node!");
} else {
first = n;
count = 1;
}
}
public void addNode(Node n){
Node lookingAt = first;
while(lookingAt.next != null){
lookingAt = lookingAt.next;
}
lookingAt.next = n;
count++;
}
public boolean hasLoop(){
int counter = 0;
Node lookingAt = first;
while(lookingAt.next != null){
counter++;
if (count < counter){
return false;
} else {
lookingAt = lookingAt.next;
}
}
return true;
}
private class Node{
Node next;
....
}
}
この文脈では、どこにでもテキスト素材への負荷があります。私は、概念を把握するのに本当に役立った図表表現を投稿したいだけでした。
速い点と遅い点が点pで出会うと、
高速移動距離= a + b + c + b = a + 2b + c
Slow = a + bの移動距離
速いので遅いよりも2倍速いです。だからa + 2b + c = 2(a + b)、そしてa = c.
そのため、別の遅いポインタがheadからqへ再び走るとき、同時に、速いポインタがpから走りますqになるので、それらはqの点で集まります。
public ListNode detectCycle(ListNode head) {
if(head == null || head.next==null)
return null;
ListNode slow = head;
ListNode fast = head;
while (fast!=null && fast.next!=null){
fast = fast.next.next;
slow = slow.next;
/*
if the 2 pointers meet, then the
dist from the meeting pt to start of loop
equals
dist from head to start of loop
*/
if (fast == slow){ //loop found
slow = head;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null;
}
リンクリスト内のループを検出することは、最も簡単な方法の1つで行うことができ、その結果O(N)の複雑さが生じます。
頭から始めてリストを移動するときに、アドレスのソートされたリストを作成します。新しいアドレスを挿入するときには、そのアドレスが既にソート済みリストにあるかどうかを確認してください。これはO(logN)の複雑さを意味します。
boolean hasCycle(Node head) {
boolean dec = false;
Node first = head;
Node sec = head;
while(first != null && sec != null)
{
first = first.next;
sec = sec.next.next;
if(first == sec )
{
dec = true;
break;
}
}
return dec;
}
上記の関数を使用して、Javaのリンクリストでループを検出します。
// To detect whether a circular loop exists in a linked list
public boolean findCircularLoop() {
Node slower, faster;
slower = head;
faster = head.next; // start faster one node ahead
while (true) {
// if the faster pointer encounters a NULL element
if (faster == null || faster.next == null)
return false;
// if faster pointer ever equals slower or faster's next
// pointer is ever equal to slower then it's a circular list
else if (slower == faster || slower == faster.next)
return true;
else {
// advance the pointers
slower = slower.next;
faster = faster.next.next;
}
}
}
あなたはそれを一定のO(1)時間でさえ行うことができます(それほど速くも効率的でもないでしょうが)。 Nを超えるレコードをトラバースすると、ループが発生します。
上記の答えで提案されているようにフロイドのカメアルゴリズムを使用することもできます。
このアルゴリズムは、単一リンクリストに閉サイクルがあるかどうかを確認できます。これは、異なる速度で移動する2つのポインタでリストを繰り返すことによって実現できます。このように、サイクルがあれば、2つのポインタは将来のある時点で出会うでしょう。
リンクリストのデータ構造については、私の blog post を気軽にチェックしてください。ここに、上記のアルゴリズムをJava言語で実装したコードスニペットも含まれています。
よろしく、
アンドレアス(@xnorcode)
私はこのスレッドを扱うのがすごく遅くて新しいかもしれません。それでも..
ノードのアドレスとポイントされた「次の」ノードをテーブルに格納できない理由
このように表にできれば
node present: (present node addr) (next node address)
node 1: addr1: 0x100 addr2: 0x200 ( no present node address till this point had 0x200)
node 2: addr2: 0x200 addr3: 0x300 ( no present node address till this point had 0x300)
node 3: addr3: 0x300 addr4: 0x400 ( no present node address till this point had 0x400)
node 4: addr4: 0x400 addr5: 0x500 ( no present node address till this point had 0x500)
node 5: addr5: 0x500 addr6: 0x600 ( no present node address till this point had 0x600)
node 6: addr6: 0x600 addr4: 0x400 ( ONE present node address till this point had 0x400)
したがって、形成されたサイクルがあります。
これは周期を検出するための解決策です。
public boolean hasCycle(ListNode head) {
ListNode slow =head;
ListNode fast =head;
while(fast!=null && fast.next!=null){
slow = slow.next; // slow pointer only one hop
fast = fast.next.next; // fast pointer two Hops
if(slow == fast) return true; // retrun true if fast meet slow pointer
}
return false; // return false if fast pointer stop at end
}
これが私の実行可能なコードです。
私がしたことは、リンクを追跡する3つの一時的なノード(スペースの複雑さO(1)
)を使ってリンクリストを明らかにすることです。
これを行うことに関する興味深い事実は、リンクリスト内のサイクルを検出するのを手助けすることです。それがルートノードを指すことを意味するサイクルがあります。
このアルゴリズムの時間の複雑さはO(n)
、空間の複雑さはO(1)
です。
リンクリストのクラスノードは次のとおりです。
public class LinkedNode{
public LinkedNode next;
}
これは、最後のノードが2番目のノードを指しているという3つのノードの単純なテストケースを含むメインコードです。
public static boolean checkLoopInLinkedList(LinkedNode root){
if (root == null || root.next == null) return false;
LinkedNode current1 = root, current2 = root.next, current3 = root.next.next;
root.next = null;
current2.next = current1;
while(current3 != null){
if(current3 == root) return true;
current1 = current2;
current2 = current3;
current3 = current3.next;
current2.next = current1;
}
return false;
}
これは、最後のノードが2番目のノードを指している3つのノードの単純なテストケースです。
public class questions{
public static void main(String [] args){
LinkedNode n1 = new LinkedNode();
LinkedNode n2 = new LinkedNode();
LinkedNode n3 = new LinkedNode();
n1.next = n2;
n2.next = n3;
n3.next = n2;
System.out.print(checkLoopInLinkedList(n1));
}
}
これがJavaでの私の解決策です
boolean detectLoop(Node head){
Node fastRunner = head;
Node slowRunner = head;
while(fastRunner != null && slowRunner !=null && fastRunner.next != null){
fastRunner = fastRunner.next.next;
slowRunner = slowRunner.next;
if(fastRunner == slowRunner){
return true;
}
}
return false;
}
このコードは最適化されており、ベストアンサーとして選択されたものよりも早く結果が生成されます。このコードは、順方向および逆方向ノードポインタを追いかけるという非常に長いプロセスに入るのを防ぎます。以下のドライランを見て、あなたが私が言おうとしていることを理解するでしょう。そして、下記の与えられた方法を通して問題を見てそしていいえを測定する。答えを見つけるためにとられたステップの。
1 - > 2 - > 9 - > 3 ^ -------- ^
これがコードです:
boolean loop(node *head)
{
node *back=head;
node *front=head;
while(front && front->next)
{
front=front->next->next;
if(back==front)
return true;
else
back=back->next;
}
return false
}
これを固定量の時間またはスペースを取るようにする方法を見ることはできません。両方ともリストのサイズとともに増加します。
私は(まだIdentityHashSetがないと仮定して)IdentityHashMapを利用し、各ノードをマップに格納します。ノードが保存される前に、containsKeyを呼び出します。ノードが既に存在する場合は、サイクルがあります。
ItentityHashMapは、.equalsの代わりに==を使用するため、オブジェクトの内容が同じではなくメモリ内のどこにあるかを確認できます。
//リンクリスト検索ループ関数
int findLoop(struct Node* head)
{
struct Node* slow = head, *fast = head;
while(slow && fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
return 1;
}
return 0;
}