Mengapa loop bersarang dianggap praktik buruk?

33

Dosen saya menyebutkan hari ini bahwa mungkin untuk "memberi label" loop di Jawa sehingga Anda dapat merujuk mereka ketika berhadapan dengan loop bersarang. Jadi saya mencari fitur karena saya tidak tahu tentang hal itu dan banyak tempat di mana fitur ini dijelaskan diikuti oleh peringatan, mengecilkan loop bersarang.

Saya tidak begitu mengerti mengapa? Apakah karena hal itu memengaruhi keterbacaan kode? Atau itu sesuatu yang lebih "teknis"?

DSF
sumber
4
Jika saya ingat kursus CS3 dengan benar, itu karena sering mengarah ke waktu eksponensial yang berarti jika Anda mendapatkan kumpulan data yang besar, aplikasi Anda akan menjadi tidak dapat digunakan.
Travis Pessetto
21
Satu hal yang harus Anda pelajari tentang dosen MS adalah bahwa tidak semua yang mereka katakan berlaku 100% di dunia nyata. Saya akan mencegah loop bersarang lebih dari beberapa kedalaman, tetapi jika Anda harus memproses elemen m x n untuk menyelesaikan masalah Anda, Anda akan melakukan banyak iterasi.
Blrfl
8
@ TravisPessetto Sebenarnya masih kompleksitas polinomial - O (n ^ k), k menjadi jumlah bersarang, bukan eksponensial O (k ^ n), di mana k adalah konstanta.
m3th0dman
1
@ m3th0dman Terima kasih telah mengoreksi saya. Guru saya bukan yang terbaik dalam hal ini. Dia memperlakukan O (n ^ 2) dan O (k ^ n) sebagai sama.
Travis Pessetto
2
Loop bersarang meningkatkan kompleksitas siklomatik (lihat di sini ), yang mengurangi kemampuan pemeliharaan suatu program, menurut beberapa orang.
Marco

Jawaban:

62

Loop bersarang baik-baik saja selama mereka menggambarkan algoritma yang benar.

Loop bersarang memiliki pertimbangan kinerja (lihat jawaban @ Travis-Pesetto), tetapi kadang-kadang algoritma yang tepat, misalnya ketika Anda perlu mengakses setiap nilai dalam matriks.

Pelabelan loop di Jawa memungkinkan untuk keluar secara prematur dari beberapa loop bersarang ketika cara lain untuk melakukan ini akan rumit. Misalnya beberapa gim mungkin memiliki kode seperti ini:

Player chosen_one = null;
...
outer: // this is a label
for (Player player : party.getPlayers()) {
  for (Cell cell : player.getVisibleMapCells()) {
    for (Item artefact : cell.getItemsOnTheFloor())
      if (artefact == HOLY_GRAIL) {
        chosen_one = player;
        break outer; // everyone stop looking, we found it
      }
  }
}

Walaupun kode seperti contoh di atas terkadang merupakan cara optimal untuk mengekspresikan algoritma tertentu, biasanya lebih baik memecah kode ini menjadi fungsi yang lebih kecil, dan mungkin menggunakan returnalih-alih break. Jadi breakdengan label adalah bau kode yang samar ; berikan perhatian ekstra saat Anda melihatnya.

9000
sumber
2
Sama seperti grafik sisi catatan menggunakan algoritma yang harus mengakses setiap bagian dari matriks. Namun, GPU khusus untuk menangani ini dengan cara yang efisien waktu.
Travis Pessetto
Ya, GPU melakukan ini secara paralel-besar-besaran; pertanyaannya adalah tentang satu utas eksekusi, saya kira.
9000
2
Salah satu alasan label cenderung dicurigai adalah karena sering ada alternatif. Dalam hal ini Anda dapat kembali sebagai gantinya.
jgmjgm
22

Loop bersarang sering (tetapi tidak selalu) merupakan praktik yang buruk, karena mereka sering (tetapi tidak selalu) berlebihan untuk apa yang Anda coba lakukan. Dalam banyak kasus, ada cara yang jauh lebih cepat dan tidak boros untuk mencapai tujuan yang ingin Anda capai.

Misalnya, jika Anda memiliki 100 item dalam daftar A, dan 100 item dalam daftar B, dan Anda tahu bahwa untuk setiap item dalam daftar A ada satu item dalam daftar B yang cocok dengan itu, (dengan definisi "kecocokan" kiri sengaja sengaja dikaburkan di sini), dan Anda ingin membuat daftar pasangan, cara sederhana untuk melakukannya adalah seperti ini:

for each item X in list A:
  for each item Y in list B:
    if X matches Y then
      add (X, Y) to results
      break

Dengan 100 item dalam setiap daftar, ini akan membutuhkan rata-rata 100 * 100/2 (5.000) matchesoperasi. Dengan lebih banyak item, atau jika korelasi 1: 1 tidak terjamin, itu menjadi lebih mahal.

Di sisi lain, ada cara yang jauh lebih cepat untuk melakukan operasi seperti ini:

sort list A
sort list B (according to the same sort order)
I = 0
J = 0
repeat
  X = A[I]
  Y = B[J]
  if X matches Y then
    add (X, Y) to results
    increment I
    increment J
  else if X < Y then
    increment I
  else increment J
until either index reaches the end of its list

Jika Anda melakukannya dengan cara ini, alih-alih berdasarkan jumlah matchesoperasi length(A) * length(B), itu sekarang didasarkan pada length(A) + length(B), yang berarti kode Anda akan berjalan lebih cepat.

Mason Wheeler
sumber
10
Dengan peringatan bahwa pengaturan penyortiran membutuhkan waktu non-sepele, O(n log n)dua kali, jika Quicksort digunakan.
Robert Harvey
@RobertHarvey: Tentu saja. Tapi itu masih jauh lebih kecil daripada O(n^2)nilai-nilai non-kecil dari N.
Mason Wheeler
10
Versi kedua dari algoritma ini umumnya salah. Pertama, diasumsikan bahwa X dan Y dapat dibandingkan melalui <operator, yang umumnya tidak dapat diturunkan dari matchesoperator. Kedua, bahkan jika kedua X dan Y adalah numerik, 2 algoritma mungkin masih menghasilkan hasil yang salah, misalnya ketika X matches Yadalah X + Y == 100.
Pasha
9
@ user958624: Jelas ini adalah gambaran umum dari algoritma umum. Seperti operator "cocok", "<" harus didefinisikan dengan cara yang benar dalam konteks data yang dibandingkan. Jika ini dilakukan dengan benar, hasilnya akan benar.
Mason Wheeler
PHP melakukan sesuatu seperti yang terakhir dengan array mereka yang unik, saya pikir dan / atau sebaliknya. Orang alih-alih hanya menggunakan array PHP yang berbasis hash dan itu akan jauh lebih cepat.
jgmjgm
11

Salah satu alasan untuk menghindari loop sarang adalah karena itu adalah ide yang buruk untuk bersarang struktur blok terlalu dalam, terlepas dari apakah mereka loop atau tidak.

Setiap fungsi atau metode harus mudah dipahami, baik tujuannya (nama harus mengungkapkan apa fungsinya) dan untuk pengelola (harus mudah untuk memahami internal). Jika suatu fungsi terlalu rumit untuk dipahami dengan mudah, itu biasanya berarti bahwa beberapa internal harus difaktorkan ke dalam fungsi yang terpisah sehingga mereka dapat disebut dalam fungsi utama (sekarang lebih kecil) dengan nama.

Nested loop dapat menjadi sulit untuk dipahami secara relatif cepat, meskipun beberapa nesting of loop baik-baik saja - memberikan, seperti yang ditunjukkan orang lain, itu tidak berarti Anda membuat masalah kinerja dengan menggunakan algoritma lambat yang sangat (dan tidak perlu).

Sebenarnya, Anda tidak perlu loop bersarang untuk mendapatkan batas kinerja yang sangat lambat. Pertimbangkan, misalnya, satu loop yang dalam setiap iterasi mengambil satu item dari antrian, lalu mungkin mengembalikan beberapa item - misalnya pencarian labirin pertama kali. Performa tidak diputuskan oleh kedalaman bersarang loop (yang hanya 1) tetapi dengan jumlah item yang dimasukkan ke dalam antrian sebelum akhirnya habis ( jika pernah habis) - seberapa besar bagian yang dapat dijangkau dari maze adalah.

Steve314
sumber
1
Anda bisa sangat sering hanya meratakan loop bersarang dan masih membutuhkan waktu yang sama. ambil untuk 0 ke lebar, untuk 0 ke tinggi; Anda bisa menggunakan 0 untuk lebar kali tinggi saja.
jgmjgm
@ jgmjgm - ya, dengan risiko menyulitkan kode di dalam loop. Meratakan juga dapat menyederhanakan hal itu kadang-kadang, tetapi lebih sering Anda setidaknya menambahkan kompleksitas memulihkan indeks yang sebenarnya Anda inginkan. Salah satu trik untuk itu adalah menggunakan tipe indeks yang faktor semua loop bersarang ke dalam logika untuk menambah indeks senyawa khusus - Anda tidak mungkin melakukan itu hanya untuk satu loop, tetapi mungkin Anda memiliki beberapa loop dengan struktur yang sama atau mungkin Anda dapat menulis versi generik yang lebih fleksibel. Overhead untuk menggunakan tipe itu (jika ada) dapat sia-sia untuk kejelasan.
Steve314
Saya tidak menyarankan itu sebagai hal yang baik untuk dilakukan, tetapi betapa sederhananya sederhana untuk mengubah dua loop menjadi satu tetapi masih tidak berdampak pada kompleksitas waktu.
jgmjgm
7

Mengingat kasus banyak loop bersarang Anda berakhir dengan waktu polinomial. Misalnya diberi kode semu ini:

set i equal to 1
while i is not equal to 100
  increment i
  set j equal to 1
  while j is not equal to i
    increment j
  end
 end

Ini akan dianggap sebagai O (n ^ 2) waktu yang akan menjadi grafik yang mirip dengan: masukkan deskripsi gambar di sini

Di mana sumbu y adalah jumlah waktu yang diperlukan program Anda untuk berhenti dan sumbu x adalah jumlah data.

Jika Anda mendapatkan terlalu banyak data, program Anda akan sangat lambat sehingga tidak ada yang menunggu. dan tidak sebanyak 1.000 entri data yang saya percaya akan memakan waktu terlalu lama.

Travis Pessetto
sumber
10
Anda mungkin ingin memilih ulang grafik Anda: itu adalah kurva eksponensial bukan kurva kuadratik, juga rekursi tidak menghasilkan barang O (n log n) dari O (n ^ 2)
ratchet freak
5
Untuk rekursi, saya memiliki dua kata "stack" dan "overflow"
Mateusz
10
Pernyataan Anda bahwa rekursi dapat mengurangi operasi O (n ^ 2) menjadi O (n log n) sebagian besar tidak akurat. Algoritma yang sama diimplementasikan secara rekursif vs iteratif harus memiliki kompleksitas waktu O-besar yang sama persis. Lebih jauh, rekursi sering kali lebih lambat (tergantung pada implementasi bahasa), karena setiap panggilan rekursif membutuhkan pembuatan bingkai stack baru, sementara iterasi hanya membutuhkan cabang / perbandingan. Panggilan fungsi biasanya murah, tetapi tidak gratis.
dckrooney
2
@ TravisPessetto Saya telah melihat 3 stack overflows dalam 6 bulan terakhir saat mengembangkan aplikasi C # karena rekursi atau referensi objek siklik. Apa yang lucu tentang itu bahwa itu akan crash dan Anda tidak tahu apa yang menimpa Anda. Ketika Anda melihat loop bersarang Anda tahu bahwa sesuatu yang buruk dapat terjadi, dan pengecualian tentang indeks yang salah untuk sesuatu mudah dilihat.
Mateusz
2
@Mususz juga, bahasa seperti Java memungkinkan Anda menangkap kesalahan stack overflow. Ini dengan jejak tumpukan harus membiarkan Anda melihat apa yang terjadi. Saya tidak punya banyak pengalaman, tetapi satu-satunya waktu saya melihat kesalahan stack overflow adalah dalam PHP yang memiliki bug yang menyebabkan rekursi tak terbatas dan PHP kehabisan memori 512MB yang ditugaskan untuk itu. Rekursi harus memiliki nilai terminasi akhir yang tidak baik untuk loop tak terbatas. Seperti segala sesuatu di CS, ada waktu dan tempat untuk semua hal.
Travis Pessetto
0

Mengendarai truk tiga puluh ton dan bukannya kendaraan penumpang kecil adalah praktik yang buruk. Kecuali ketika Anda perlu mengangkut 20 atau 30 ton barang.

Saat Anda menggunakan nested loop, itu bukan praktik yang buruk. Entah itu benar-benar bodoh, atau justru itulah yang dibutuhkan. Kamu putuskan.

Namun, seseorang mengeluh tentang label loop. Jawaban untuk itu: jika Anda harus mengajukan pertanyaan, maka jangan gunakan label. Jika Anda cukup tahu untuk memutuskan sendiri, maka Anda memutuskan sendiri.

gnasher729
sumber
Jika Anda cukup tahu untuk memutuskan sendiri, Anda cukup tahu untuk tidak menggunakan loop berlabel. :-)
user949300
Tidak. Ketika Anda sudah cukup diajar, Anda telah diajarkan untuk tidak menggunakan penggunaan berlabel. Ketika Anda cukup tahu Anda melampaui dogma dan melakukan apa yang benar.
gnasher729
1
Masalah dengan label adalah bahwa itu jarang diperlukan dan banyak orang menggunakannya sejak dini untuk mengatasi kesalahan kontrol aliran. Agak seperti bagaimana orang menempatkan keluar di semua tempat ketika mereka mendapatkan kontrol aliran yang salah. Fungsinya juga membuatnya sangat berlebihan.
jgmjgm
-1

Tidak ada yang salah secara inheren atau bahkan buruk tentang loop bersarang. Namun mereka memiliki pertimbangan dan jebakan tertentu.

Artikel yang Anda pimpin, kemungkinan atas nama singkatnya atau karena proses psikologis yang dikenal sebagai dibakar, melompati spesifik.

Terbakar adalah ketika Anda memiliki pengalaman negatif dengan sesuatu yang melibatkan diri Anda, maka hindarilah. Misalnya saya mungkin memotong sayuran dengan pisau tajam dan memotong sendiri. Saya kemudian bisa mengatakan pisau tajam itu buruk, jangan menggunakannya untuk memotong sayuran untuk mencoba membuat pengalaman buruk itu tidak mungkin terjadi lagi. Itu jelas sangat tidak praktis. Pada kenyataannya Anda hanya perlu berhati-hati. Jika Anda menyuruh orang lain untuk memotong sayuran maka Anda akan merasakan hal ini lebih kuat. Jika saya menyuruh anak-anak untuk memotong sayuran, saya akan merasa sangat kuat untuk memberitahu mereka untuk tidak menggunakan pisau tajam terutama jika saya tidak bisa mengawasi mereka dengan cermat.

Masalahnya dalam pemrograman adalah bahwa Anda tidak akan memenuhi efisiensi puncak jika Anda selalu lebih memilih keselamatan terlebih dahulu. Dalam hal ini anak-anak hanya dapat memotong sayuran lunak. Dihadapkan dengan hal lain dan mereka hanya akan membuatnya berantakan menggunakan pisau tumpul. Sangat penting untuk mempelajari penggunaan loop yang tepat termasuk loop bersarang dan Anda tidak dapat melakukannya jika mereka dianggap buruk dan Anda tidak pernah mencoba menggunakannya.

Seperti banyak jawaban di sini menunjukkan nested for loop adalah indikasi karakteristik kinerja program Anda yang mungkin secara eksponensial lebih buruk setiap bersarang. Yaitu, O (n), O (n ^ 2), O (n ^ 3) dan seterusnya yang terdiri dari O (n ^ kedalaman) di mana kedalaman mewakili berapa banyak loop yang Anda buat bersarang. Saat sarang Anda tumbuh, waktu yang dibutuhkan tumbuh secara eksponensial. Masalahnya dengan ini adalah bahwa itu bukan kepastian waktu atau kompleksitas ruang Anda adalah (cukup sering a * b * c tetapi tidak semua loop sarang dapat berjalan sepanjang waktu juga) juga bukan kepastian bahwa Anda akan memiliki masalah kinerja bahkan jika itu.

Bagi banyak orang, terutama mahasiswa, penulis dan dosen yang jujur, jarang memprogram untuk hidup atau sehari-hari untuk loop juga bisa menjadi sesuatu yang tidak biasa dan yang menyebabkan terlalu banyak beban kognitif pada pertemuan awal. Ini adalah aspek yang bermasalah karena selalu ada kurva belajar dan menghindarinya tidak akan efektif dalam mengubah siswa menjadi programmer.

Loop bersarang bisa menjadi liar, yaitu mereka dapat berakhir bersarang sangat dalam. Jika saya melewati setiap benua, lalu melalui setiap negara, lalu melalui setiap kota, lalu melalui setiap toko, lalu melalui setiap rak, lalu melalui setiap produk jika itu adalah kaleng kacang melalui masing-masing kacang dan mengukur ukurannya untuk mendapatkan rata-rata maka Anda dapat melihat bahwa sarang akan sangat dalam. Anda akan memiliki piramida dan banyak ruang terbuang dari margin kiri. Anda bahkan mungkin keluar dari halaman.

Ini adalah masalah yang akan lebih signifikan secara historis di mana layar kecil dan resolusi rendah. Dalam kasus-kasus itu, bahkan beberapa tingkat sarang benar-benar dapat memakan banyak ruang. Ini adalah masalah yang lebih kecil hari ini di mana ambang batas lebih tinggi meskipun masih bisa menimbulkan masalah jika ada cukup sarang.

Terkait adalah argumen estetika. Banyak orang tidak menemukan sarang untuk loop secara estetika berbeda dengan tata letak dengan keselarasan yang lebih konsisten ini mungkin atau mungkin tidak terkait dengan apa yang digunakan orang, pelacakan mata dan masalah lainnya. Namun bermasalah karena cenderung memperkuat diri dan pada akhirnya dapat membuat kode lebih sulit dibaca karena memecah blok kode dan merangkum loop di belakang abstraksi seperti fungsi juga berisiko memecah pemetaan kode untuk aliran eksekusi.

Ada kecenderungan alami terhadap apa yang digunakan orang. Jika Anda memprogram sesuatu dengan cara paling sederhana, probabilitas tidak perlu bersarang adalah yang tertinggi, probabilitas kebutuhan satu tingkat turun dengan urutan besarnya, probabilitas untuk tingkat lain turun lagi. Frekuensi menurun dan pada dasarnya berarti semakin dalam sarang semakin kurang terlatih indra manusia untuk mengantisipasinya.

Terkait dengan itu adalah bahwa dalam konstruksi kompleks mana pun, yang dapat dianggap sebagai loop bersarang, maka Anda harus selalu bertanya apakah solusi yang paling sederhana karena ada potensi untuk solusi yang terlewatkan yang membutuhkan lebih sedikit loop. Ironisnya, solusi bersarang seringkali merupakan cara paling sederhana untuk menghasilkan sesuatu yang bekerja dengan jumlah minimum usaha, kompleksitas, dan beban kognitif. Biasanya sarang untuk loop. Jika Anda mempertimbangkan misalnya salah satu jawaban di atas di mana cara yang jauh lebih cepat daripada nested for loop juga jauh lebih kompleks dan terdiri dari kode yang jauh lebih banyak.

Diperlukan banyak perawatan karena sering kali memungkinkan untuk melakukan lilitan secara abstrak atau meratakannya dengan hasil akhirnya yang pada akhirnya menjadi obat yang lebih buruk daripada penyakitnya, terutama jika Anda tidak misalnya menerima peningkatan kinerja yang terukur dan signifikan dari upaya tersebut.

Sangat umum bagi orang untuk sering mengalami masalah kinerja dalam kaitannya dengan loop yang memberi tahu komputer untuk mengulangi suatu tindakan berkali-kali dan secara inheren akan sering terlibat dalam kemacetan kinerja. Sayangnya tanggapan terhadap hal ini bisa sangat dangkal. Sudah menjadi hal biasa bagi orang-orang untuk melihat loop dan melihat masalah kinerja di mana tidak ada dan kemudian menyembunyikan loop dari penglihatan tanpa efek nyata. Kode "terlihat" cepat tetapi letakkan di jalan, kunci kontak, injak akselerator dan lihat speedometer dan Anda mungkin menemukan ini masih secepat wanita tua berjalan di bingkai zimmer-nya.

Persembunyian semacam ini mirip dengan jika Anda memiliki sepuluh perampok di rute Anda. Jika alih-alih memiliki rute lurus ke tempat Anda ingin pergi, Anda mengaturnya sehingga ada perampok di belakang setiap sudut maka memberikan ilusi saat Anda memulai perjalanan Anda bahwa tidak ada perampok. Keluar dari akal pikiran. Anda masih akan dirampok sepuluh kali tetapi sekarang Anda tidak akan melihatnya datang.

Jawaban atas pertanyaan Anda adalah keduanya, tetapi tidak ada kekhawatiran yang mutlak. Mereka sepenuhnya subjektif atau hanya obyektif kontekstual. Sayangnya kadang-kadang pendapat yang sepenuhnya subjektif atau lebih tepatnya mengambil preseden dan mendominasi.

Sebagai aturan praktis, jika perlu loop bersarang atau yang tampaknya seperti langkah jelas berikutnya, yang terbaik adalah tidak sengaja dan cukup melakukannya. Namun jika ada keraguan berlama-lama maka harus ditinjau kembali nanti.

Aturan praktis lainnya adalah Anda harus selalu memeriksa kardinalitas dan bertanya pada diri sendiri apakah loop ini akan menjadi masalah. Dalam contoh saya sebelumnya, saya pergi ke kota-kota. Untuk pengujian saya mungkin hanya melewati sepuluh kota tapi berapa jumlah maksimum kota yang bisa diharapkan dalam penggunaan di dunia nyata? Saya kemudian dapat mengalikannya dengan yang sama untuk benua. Ini adalah aturan praktis untuk selalu mempertimbangkan dengan loop terutama yang mengulang jumlah dinamis (variabel) kali apa yang mungkin diterjemahkan ke bawah baris.

Apapun selalu lakukan apa yang berhasil dulu. Cara ketika Anda melihat peluang untuk pengoptimalan, Anda dapat membandingkan solusi yang dioptimalkan dengan yang paling mudah untuk bekerja dan mengonfirmasi bahwa itu menghasilkan manfaat yang diharapkan. Anda juga dapat menghabiskan waktu terlalu lama untuk mengoptimalkan sebelum pengukuran dilakukan dan itu mengarah ke YAGNI atau banyak waktu terbuang dan tenggat waktu yang terlewat.

jgmjgm
sumber
Contoh pisau tumpul <-> yang tajam tidak bagus, karena pisau tumpul umumnya lebih berbahaya untuk dipotong. Dan O (n) -> O (n ^ 2) -> O (n ^ 3) tidak eksponensial dalam n, itu geometris atau polinomial .
Caleth
Pisau tumpul lebih buruk adalah sedikit mitos urban. Dalam praktiknya bervariasi dan biasanya khusus untuk kasus di mana pisau tumpul sangat tidak cocok biasanya membutuhkan banyak kekuatan dan melibatkan banyak selip. Anda benar, ada batasan yang tersembunyi tetapi melekat di sana, Anda hanya bisa memotong sayuran lunak. Saya akan mempertimbangkan kedalaman eksponensial tetapi Anda benar contoh itu sendiri tidak.
jgmjgm
@ jgmjgm: n ^ kedalaman adalah polinomial, kedalaman ^ n adalah eksponensial. Ini bukan masalah interpretasi.
Roel Schroeven
x ^ y berbeda dengan y ^ x? Kesalahan yang Anda buat adalah Anda tidak pernah membaca jawabannya. Anda telah menerima untuk eksponensial dan kemudian menerima persamaan yang sendiri bukan eksponensial. Jika Anda membaca Anda akan melihat saya katakan itu meningkat secara eksponensial untuk setiap lapisan bersarang dan jika Anda menguji sendiri, waktu untuk (a = 0; a <n; a ++); untuk (b = 0; b <n ; b ++); untuk (c = 0; c <n; c ++); saat menambahkan atau menghapus loop maka Anda akan melihat bahwa itu memang eksponensial. Anda akan menemukan satu loop berkinerja di n ^ 1, dua berkinerja di n ^ 2 dan tiga berkinerja di n ^ 3. Anda gagal memahami bersarang: D. Subset gemoetrik eksponensial.
jgmjgm
Saya kira ini membuktikan poin bahwa orang benar-benar berjuang dengan konstruksi bersarang.
jgmjgm