Uji Daftar Tertaut untuk Siklus

1. Perkenalan

Daftar tertaut tunggal adalah urutan node terhubung yang diakhiri dengan referensi nol . Namun, dalam beberapa skenario, node terakhir mungkin menunjuk ke node sebelumnya - secara efektif membuat siklus.

Dalam kebanyakan kasus, kami ingin dapat mendeteksi dan menyadari siklus ini; artikel ini akan fokus pada hal itu - mendeteksi dan berpotensi menghapus siklus.

2. Mendeteksi Siklus

Sekarang mari kita jelajahi beberapa algoritme untuk mendeteksi siklus dalam daftar tertaut.

2.1. Kekuatan Brute - O (n ^ 2) Kompleksitas Waktu

Dengan algoritme ini, kami melintasi daftar menggunakan dua loop bersarang. Di loop luar, kami melintasi satu per satu. Di loop dalam, kita mulai dari head dan melintasi sebanyak mungkin node yang dilintasi oleh loop luar pada saat itu.

Jika node yang dikunjungi oleh loop luar dikunjungi dua kali oleh loop dalam, maka sebuah siklus telah terdeteksi. Sebaliknya, jika loop luar mencapai akhir daftar, ini berarti tidak ada siklus:

public static  boolean detectCycle(Node head) { if (head == null) { return false; } Node it1 = head; int nodesTraversedByOuter = 0; while (it1 != null && it1.next != null) { it1 = it1.next; nodesTraversedByOuter++; int x = nodesTraversedByOuter; Node it2 = head; int noOfTimesCurrentNodeVisited = 0; while (x > 0) { it2 = it2.next; if (it2 == it1) { noOfTimesCurrentNodeVisited++; } if (noOfTimesCurrentNodeVisited == 2) { return true; } x--; } } return false; }

Keuntungan dari pendekatan ini adalah membutuhkan jumlah memori yang konstan. Kerugiannya adalah kinerjanya sangat lambat ketika daftar besar disediakan sebagai masukan.

2.2. Hashing - O (n) Kompleksitas Ruang

Dengan algoritma ini, kami mempertahankan satu set node yang sudah dikunjungi. Untuk setiap node, kami memeriksa apakah itu ada di set. Jika tidak, maka kami menambahkannya ke set. Adanya node dalam himpunan berarti kita telah mengunjungi node tersebut dan menampilkan sebuah siklus dalam daftar.

Ketika kami menemukan node yang sudah ada di himpunan, kami akan menemukan awal siklus. Setelah menemukan ini, kita dapat dengan mudah memutus siklus dengan menyetel bidang berikutnya dari node sebelumnya ke null , seperti yang ditunjukkan di bawah ini:

public static  boolean detectCycle(Node head) { if (head == null) { return false; } Set
    
      set = new HashSet(); Node node = head; while (node != null) { if (set.contains(node)) { return true; } set.add(node); node = node.next; } return false; }
    

Dalam solusi ini, kami mengunjungi dan menyimpan setiap node satu kali. Jumlah ini menjadi kompleksitas waktu O (n) dan kompleksitas ruang O (n), yang, secara rata-rata, tidak optimal untuk daftar besar.

2.3. Pointer Cepat dan Lambat

Algoritme berikut untuk menemukan siklus paling baik dijelaskan menggunakan metafora .

Pertimbangkan trek balap tempat dua orang berlomba. Mengingat kecepatan orang kedua dua kali lipat kecepatan orang pertama, orang kedua akan mengitari trek dua kali lebih cepat dari orang pertama dan akan bertemu orang pertama lagi di awal putaran.

Di sini kami menggunakan pendekatan serupa dengan melakukan iterasi melalui daftar secara bersamaan dengan iterator lambat dan iterator cepat (kecepatan 2x). Setelah kedua iterator memasuki satu lingkaran, mereka akhirnya akan bertemu di suatu titik.

Oleh karena itu, jika dua iterator bertemu pada titik mana pun, maka kita dapat menyimpulkan bahwa kita telah menemukan sebuah siklus:

public static  CycleDetectionResult detectCycle(Node head) { if (head == null) { return new CycleDetectionResult(false, null); } Node slow = head; Node fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { return new CycleDetectionResult(true, fast); } } return new CycleDetectionResult(false, null); }

Di mana CycleDetectionResult adalah kelas praktis untuk menampung hasil: variabel boolean yang mengatakan apakah siklus ada atau tidak dan jika ada, maka ini juga berisi referensi ke titik pertemuan di dalam siklus:

public class CycleDetectionResult { boolean cycleExists; Node node; }

Metode ini juga dikenal sebagai 'The Tortoise and The Hare Algorithm' atau 'Flyods Cycle-Finding Algorithm'.

3. Penghapusan Siklus dari Daftar

Mari kita lihat beberapa metode untuk menghapus siklus. Semua metode ini mengasumsikan bahwa 'Algoritme Pencarian Siklus Flyod' digunakan untuk deteksi siklus dan dibuat di atasnya.

3.1. Paksaan

Setelah iterator cepat dan lambat bertemu pada suatu titik dalam siklus, kita ambil satu lagi iterator (katakanlah ptr ) dan arahkan ke bagian atas daftar. Kami mulai mengulang daftar dengan ptr. Di setiap langkah, kami memeriksa apakah ptr dapat dijangkau dari titik pertemuan.

This terminates when ptr reaches the beginning of the loop because that is the first point when it enters the loop and becomes reachable from the meeting point.

Once the beginning of the loop (bg) is discovered, then it is trivial to find the end of the cycle (node whose next field points to bg). The next pointer of this end node is then set to null to remove the cycle:

public class CycleRemovalBruteForce { private static  void removeCycle( Node loopNodeParam, Node head) { Node it = head; while (it != null) { if (isNodeReachableFromLoopNode(it, loopNodeParam)) { Node loopStart = it; findEndNodeAndBreakCycle(loopStart); break; } it = it.next; } } private static  boolean isNodeReachableFromLoopNode( Node it, Node loopNodeParam) { Node loopNode = loopNodeParam; do { if (it == loopNode) { return true; } loopNode = loopNode.next; } while (loopNode.next != loopNodeParam); return false; } private static  void findEndNodeAndBreakCycle( Node loopStartParam) { Node loopStart = loopStartParam; while (loopStart.next != loopStartParam) { loopStart = loopStart.next; } loopStart.next = null; } }

Unfortunately, this algorithm also performs poorly in case of large lists and large cycles, because we've to traverse the cycle multiple times.

3.2. Optimized Solution – Counting the Loop Nodes

Let's define a few variables first:

  • n = the size of the list
  • k = the distance from the head of the list to the start of the cycle
  • l = the size of the cycle

We have the following relationship between these variables:

k + l = n

We utilize this relationship in this approach. More particularly, when an iterator that begins from the start of the list, has already traveled l nodes, then it has to travel k more nodes to reach the end of the list.

Here's the algorithm's outline:

  1. Once fast and the slow iterators meet, find the length of the cycle. This can be done by keeping one of the iterators in place while continuing the other iterator (iterating at normal speed, one-by-one) till it reaches the first pointer, keeping the count of nodes visited. This counts as l
  2. Take two iterators (ptr1 and ptr2) at the beginning of the list. Move one of the iterator (ptr2) l steps
  3. Now iterate both the iterators until they meet at the start of the loop, subsequently, find the end of the cycle and point it to null

This works because ptr1 is k steps away from the loop, and ptr2, which is advanced by l steps, also needs k steps to reach the end of the loop (n – l = k).

And here's a simple, potential implementation:

public class CycleRemovalByCountingLoopNodes { private static  void removeCycle( Node loopNodeParam, Node head) { int cycleLength = calculateCycleLength(loopNodeParam); Node cycleLengthAdvancedIterator = head; Node it = head; for (int i = 0; i < cycleLength; i++) { cycleLengthAdvancedIterator = cycleLengthAdvancedIterator.next; } while (it.next != cycleLengthAdvancedIterator.next) { it = it.next; cycleLengthAdvancedIterator = cycleLengthAdvancedIterator.next; } cycleLengthAdvancedIterator.next = null; } private static  int calculateCycleLength( Node loopNodeParam) { Node loopNode = loopNodeParam; int length = 1; while (loopNode.next != loopNodeParam) { length++; loopNode = loopNode.next; } return length; } }

Next, let's focus on a method in which we can even eliminate the step of calculating the loop length.

3.3. Optimized Solution – Without Counting the Loop Nodes

Let's compare the distances traveled by the fast and slow pointers mathematically.

For that, we need a few more variables:

  • y = distance of the point where the two iterators meet, as seen from the beginning of the cycle
  • z = distance of the point where the two iterators meet, as seen from the end of the cycle (this is also equal to l – y)
  • m = number of times the fast iterator completed the cycle before the slow iterator enters the cycle

Keeping the other variables same as defined in the previous section, the distance equations will be defined as:

  • Distance traveled by slow pointer = k (distance of cycle from head) + y (meeting point inside cycle)
  • Distance traveled by fast pointer = k (distance of cycle from head) + m (no of times fast pointer completed the cycle before slow pointer enters) * l (cycle length) + y (meeting point inside cycle)

We know that distance traveled by the fast pointer is twice that of the slow pointer, hence:

k + m * l + y = 2 * (k + y)

which evaluates to:

y = m * l – k

Subtracting both sides from l gives:

l – y = l – m * l + k

or equivalently:

k = (m – 1) * l + z (where, l – y is z as defined above)

This leads to:

k = (m – 1) Full loop runs + An extra distance z

In other words, if we keep one iterator at the head of the list and one iterator at the meeting point, and move them at the same speed, then, the second iterator will complete m – 1 cycles around the loop and meet the first pointer at the beginning of the cycle. Using this insight we can formulate the algorithm:

  1. Use ‘Flyods Cycle-Finding Algorithm' to detect the loop. If loop exists, this algorithm would end at a point inside the loop (call this the meeting point)
  2. Take two iterators, one at the head of the list (it1) and one at the meeting point (it2)
  3. Traverse both iterators at the same speed
  4. Since the distance of the loop from head is k (as defined above), the iterator started from head would reach the cycle after k steps
  5. In k steps, iterator it2 would traverse m – 1 cycles of the loop and an extra distance z. Since this pointer was already at a distance of z from the beginning of the cycle, traversing this extra distance z, would bring it also at the beginning of the cycle
  6. Both the iterators meet at the beginning of the cycle, subsequently, we can find the end of the cycle and point it to null

This can be implemented:

public class CycleRemovalWithoutCountingLoopNodes { private static  void removeCycle( Node meetingPointParam, Node head) { Node loopNode = meetingPointParam; Node it = head; while (loopNode.next != it.next) { it = it.next; loopNode = loopNode.next; } loopNode.next = null; } }

This is the most optimized approach for detection and removal of cycles from a linked list.

4. Conclusion

In this article, we described various algorithms for detecting a cycle in a list. We looked into algorithms with different computing time and memory space requirements.

Terakhir, kami juga menunjukkan tiga metode untuk menghapus siklus, setelah itu terdeteksi menggunakan 'Algoritma Pencarian Siklus Flyod'.

Contoh kode lengkap tersedia di Github.