Bagaimana saya bisa memastikan bahwa pembagian bilangan bulat selalu dikumpulkan?

242

Saya ingin memastikan bahwa pembagian bilangan bulat selalu dikumpulkan jika perlu. Apakah ada cara yang lebih baik dari ini? Ada banyak casting yang terjadi. :-)

(int)Math.Ceiling((double)myInt1 / myInt2)
Karsten
sumber
48
Bisakah Anda lebih jelas mendefinisikan apa yang Anda anggap "lebih baik"? Lebih cepat? Singkat? Lebih tepat? Lebih kuat? Lebih jelas benar?
Eric Lippert
6
Anda selalu memiliki banyak casting dengan matematika di C # - itu sebabnya itu bukan bahasa yang bagus untuk hal semacam ini. Apakah Anda ingin nilai dibulatkan atau menjauh dari nol - harus -3.1 pergi ke -3 (naik) atau -4 (menjauh dari nol)
Keith
9
Eric: Apa yang Anda maksud dengan "Lebih akurat? Lebih kuat? Lebih jelas benar?" Sebenarnya yang saya maksudkan hanyalah "lebih baik", saya akan membiarkan pembaca memasukkan makna menjadi lebih baik. Jadi, jika seseorang memiliki kode yang lebih pendek, bagus, jika yang lain memiliki kode yang lebih cepat, juga bagus :-) Bagaimana dengan Anda, Anda punya saran?
Karsten
1
Apakah saya satu-satunya yang, setelah membaca judul, meskipun, "Oh, itu semacam pengumpulan C #?"
Matt Ball
6
Sungguh menakjubkan betapa sulitnya pertanyaan ini ternyata, dan seberapa instruktif diskusi itu.
Justin Morgan

Jawaban:

669

UPDATE: Pertanyaan ini adalah topik blog saya pada Januari 2013 . Terima kasih atas pertanyaannya!


Mendapatkan aritmatika integer benar sulit. Seperti yang telah ditunjukkan sejauh ini, saat Anda mencoba melakukan trik "pintar", kemungkinan besar Anda telah melakukan kesalahan. Dan ketika cacat ditemukan, mengubah kode untuk memperbaiki cacat tanpa mempertimbangkan apakah perbaikan merusak sesuatu yang lain bukanlah teknik pemecahan masalah yang baik. Sejauh ini kami sudah memikirkan lima solusi aritmatika integer salah yang berbeda untuk masalah yang tidak terlalu sulit ini.

Cara yang tepat untuk mendekati masalah bilangan bulat aritmatika - yaitu, cara yang meningkatkan kemungkinan mendapatkan jawaban yang benar pertama kali - adalah dengan mendekati masalah dengan hati-hati, menyelesaikannya satu langkah pada satu waktu, dan menggunakan prinsip-prinsip teknik yang baik dalam melakukan begitu.

Mulailah dengan membaca spesifikasi untuk apa yang ingin Anda ganti. Spesifikasi untuk divisi integer dengan jelas menyatakan:

  1. Divisi ini membulatkan hasil ke nol

  2. Hasilnya nol atau positif ketika kedua operan memiliki tanda yang sama dan nol atau negatif ketika kedua operan memiliki tanda yang berlawanan

  3. Jika operan kiri adalah int representable terkecil dan operan kanan adalah -1, terjadi overflow. [...] itu adalah implementasi yang ditentukan apakah [sebuah ArithmeticException] dilemparkan atau overflow tidak dilaporkan dengan nilai yang dihasilkan adalah dari operan kiri.

  4. Jika nilai operan yang tepat adalah nol, sebuah System.DivideByZeroException dilemparkan.

Apa yang kita inginkan adalah fungsi pembagian bilangan bulat yang menghitung hasil bagi tetapi putaran hasilnya selalu ke atas , tidak selalu menuju nol .

Jadi tulis spesifikasi untuk fungsi itu. Fungsi kami int DivRoundUp(int dividend, int divisor)harus memiliki perilaku yang ditentukan untuk setiap input yang mungkin. Perilaku tak terdefinisi itu sangat mengkhawatirkan, jadi mari kita hilangkan itu. Kami akan mengatakan bahwa operasi kami memiliki spesifikasi ini:

  1. Operasi melempar jika pembagi adalah nol

  2. operasi melempar jika dividen adalah int.minval dan pembagi -1

  3. jika tidak ada sisa - pembagian adalah 'genap' - maka nilai kembali adalah hasil bagi yang tidak terpisahkan

  4. Kalau tidak, ia mengembalikan bilangan bulat terkecil yang lebih besar dari hasil bagi, yaitu selalu bulat .

Sekarang kami memiliki spesifikasi, jadi kami tahu kami dapat membuat desain yang dapat diuji . Misalkan kita menambahkan kriteria desain tambahan bahwa masalah diselesaikan hanya dengan aritmatika bilangan bulat, daripada menghitung hasil bagi sebagai ganda, karena solusi "ganda" telah secara eksplisit ditolak dalam pernyataan masalah.

Jadi apa yang harus kita hitung? Jelas, untuk memenuhi spesifikasi kami sambil tetap hanya dalam hitung bilangan bulat, kita perlu mengetahui tiga fakta. Pertama, apa hasil bagi integer? Kedua, apakah pembagian itu bebas dari sisa? Dan ketiga, jika tidak, apakah hasil bagi bilangan bulat dihitung dengan membulatkan ke atas atau ke bawah?

Sekarang kita memiliki spesifikasi dan desain, kita dapat mulai menulis kode.

public static int DivRoundUp(int dividend, int divisor)
{
  if (divisor == 0 ) throw ...
  if (divisor == -1 && dividend == Int32.MinValue) throw ...
  int roundedTowardsZeroQuotient = dividend / divisor;
  bool dividedEvenly = (dividend % divisor) == 0;
  if (dividedEvenly) 
    return roundedTowardsZeroQuotient;

  // At this point we know that divisor was not zero 
  // (because we would have thrown) and we know that 
  // dividend was not zero (because there would have been no remainder)
  // Therefore both are non-zero.  Either they are of the same sign, 
  // or opposite signs. If they're of opposite sign then we rounded 
  // UP towards zero so we're done. If they're of the same sign then 
  // we rounded DOWN towards zero, so we need to add one.

  bool wasRoundedDown = ((divisor > 0) == (dividend > 0));
  if (wasRoundedDown) 
    return roundedTowardsZeroQuotient + 1;
  else
    return roundedTowardsZeroQuotient;
}

Apakah ini pintar? Tidak, cantik? Tidak, pendek? Tidak. Sesuai dengan spesifikasinya? Saya percaya begitu, tetapi saya belum sepenuhnya mengujinya. Ini terlihat cukup bagus.

Kami profesional di sini; gunakan praktik rekayasa yang baik. Teliti alat Anda, tentukan perilaku yang diinginkan, pertimbangkan kasus kesalahan terlebih dahulu, dan tulis kode untuk menekankan kebenarannya. Dan ketika Anda menemukan bug, pertimbangkan apakah algoritma Anda sangat cacat untuk memulai sebelum Anda secara acak mulai menukar arah perbandingan di sekitar dan memecah hal-hal yang sudah berfungsi.

Eric Lippert
sumber
44
Jawaban teladan yang sangat baik
Gavin Miller
61
Yang saya pedulikan bukanlah perilaku; salah satu perilaku tampaknya dapat dibenarkan. Yang saya pedulikan adalah bahwa itu tidak ditentukan , yang berarti tidak mudah diuji. Dalam hal ini, kami mendefinisikan operator kami sendiri, sehingga kami dapat menentukan perilaku apa pun yang kami suka. Saya tidak peduli apakah perilaku itu "melempar" atau "tidak melempar", tetapi saya peduli bahwa itu dinyatakan.
Eric Lippert
68
Sial, gagal gagal :(
Jon Skeet
32
Man - Bisakah Anda menulis buku tentang itu, tolong?
xtofl
76
@finnw: Apakah saya sudah mengujinya atau tidak tidak relevan. Memecahkan masalah aritmatika integer ini bukan masalah bisnis saya ; jika ya, maka saya akan mengujinya. Jika seseorang ingin mengambil kode dari orang asing dari internet untuk menyelesaikan masalah bisnis mereka maka tanggung jawab ada pada mereka untuk mengujinya secara menyeluruh.
Eric Lippert
49

Semua jawaban di sini sejauh ini agak rumit.

Di C # dan Java, untuk dividen dan pembagi positif, Anda hanya perlu melakukan:

( dividend + divisor - 1 ) / divisor 

Sumber: Konversi Angka, Roland Backhouse, 2001

Ian Nelson
sumber
Luar biasa. Meskipun, Anda harus menambahkan tanda kurung, untuk menghilangkan ambiguitas. Buktinya agak panjang, tapi Anda bisa merasakannya di usus, bahwa itu benar, hanya dengan melihatnya.
Jörgen Sigvardsson
1
Hmmm ... bagaimana dengan dividend = 4, divisor = (- 2) ??? 4 / (-2) = (-2) = (-2) setelah dibulatkan ke atas. tetapi algoritma yang Anda berikan (4 + (-2) - 1) / (-2) = 1 / (-2) = (-0.5) = 0 setelah dibulatkan.
Scott
1
@ Esc - maaf, saya dihilangkan untuk menyebutkan bahwa solusi ini hanya berlaku untuk dividen dan pembagi positif. Saya telah memperbarui jawaban saya untuk menyebutkan klarifikasi ini.
Ian Nelson
1
Saya suka itu, tentu saja Anda dapat memiliki limpahan buatan dalam pembilang sebagai produk sampingan dari pendekatan ini ...
TCC
2
@ Tagtag: Idenya bagus, tetapi penggunaan modulo salah. Ambil 13 dan 3. Hasil yang diharapkan 5, tetapi ((13-1)%3)+1)berikan 1 sebagai hasilnya. Mengambil pembagian yang tepat, 1+(dividend - 1)/divisormemberikan hasil yang sama dengan jawaban untuk dividen dan pembagi positif. Juga, tidak ada masalah melimpah, betapapun buatan mereka.
Lutz Lehmann
48

Jawaban akhir berbasis int

Untuk bilangan bulat yang ditandatangani:

int div = a / b;
if (((a ^ b) >= 0) && (a % b != 0))
    div++;

Untuk bilangan bulat yang tidak ditandatangani:

int div = a / b;
if (a % b != 0)
    div++;

Alasan untuk jawaban ini

Divisi integer ' /' didefinisikan untuk membulatkan ke nol (7.7.2 dari spesifikasi), tetapi kami ingin mengumpulkan. Ini berarti bahwa jawaban negatif sudah dibulatkan dengan benar, tetapi jawaban positif perlu disesuaikan.

Jawaban positif non-nol mudah dideteksi, tetapi jawaban nol sedikit lebih rumit, karena itu bisa berupa pembulatan dari nilai negatif atau pembulatan dari yang positif.

Taruhan paling aman adalah mendeteksi kapan jawabannya harus positif dengan memeriksa bahwa tanda-tanda kedua bilangan bulat itu identik. Integer xor operator ' ^' pada kedua nilai akan menghasilkan 0 sign-bit ketika hal ini terjadi, yang berarti hasil non-negatif, sehingga pemeriksaan (a ^ b) >= 0menentukan bahwa hasilnya seharusnya positif sebelum pembulatan. Perhatikan juga bahwa untuk bilangan bulat tak bertanda, setiap jawaban jelas positif, sehingga pemeriksaan ini dapat dihilangkan.

Cek yang tersisa hanyalah apakah pembulatan telah terjadi, yang a % b != 0akan melakukan pekerjaan.

Pelajaran yang dipetik

Aritmatika (bilangan bulat atau sebaliknya) tidak semudah kelihatannya. Diperlukan pemikiran yang cermat setiap saat.

Juga, walaupun jawaban akhir saya mungkin tidak sesederhana atau sejelas atau bahkan secepat puasa jawaban floating point, ia memiliki satu kualitas penebusan yang sangat kuat bagi saya; Saya sekarang telah beralasan melalui jawaban, jadi saya benar-benar yakin itu benar (sampai seseorang yang lebih cerdas mengatakan sebaliknya - pandangan sekilas ke arah Eric -).

Untuk mendapatkan perasaan kepastian yang sama tentang jawaban floating point, saya harus melakukan lebih banyak (dan mungkin lebih rumit) memikirkan apakah ada kondisi di mana presisi floating-point menghalangi, dan apakah Math.Ceilingmungkin sesuatu yang tidak diinginkan pada input 'tepat'.

Jalan itu dilalui

Ganti (perhatikan saya ganti dengan yang kedua , myInt1dengan myInt2asumsi itulah yang Anda maksud):

(int)Math.Ceiling((double)myInt1 / myInt2)

dengan:

(myInt1 - 1 + myInt2) / myInt2

Satu-satunya peringatan adalah bahwa jika myInt1 - 1 + myInt2meluap tipe integer yang Anda gunakan, Anda mungkin tidak mendapatkan apa yang Anda harapkan.

Alasan ini salah : -1000000 dan 3999 harus memberi -250, ini memberi -249

EDIT:
Mengingat ini memiliki kesalahan yang sama dengan solusi integer lainnya untuk myInt1nilai negatif , mungkin lebih mudah untuk melakukan sesuatu seperti:

int rem;
int div = Math.DivRem(myInt1, myInt2, out rem);
if (rem > 0)
  div++;

Itu harus memberikan hasil yang benar dalam divmenggunakan operasi integer saja.

Alasan ini salah : -1 dan -5 harus memberi 1, ini memberi 0

EDIT (sekali lagi, dengan perasaan):
Operator divisi berputar ke nol; untuk hasil negatif ini tepat, sehingga hanya hasil non-negatif yang perlu penyesuaian. Juga mempertimbangkan bahwa DivRemhanya melakukan a /dan a %, mari kita lewati panggilan (dan mulai dengan perbandingan mudah untuk menghindari perhitungan modulo ketika tidak diperlukan):

int div = myInt1 / myInt2;
if ((div >= 0) && (myInt1 % myInt2 != 0))
    div++;

Alasan ini salah : -1 dan 5 harus memberi 0, ini memberi 1

(Dalam pembelaan saya sendiri terhadap upaya terakhir saya seharusnya tidak pernah mencoba jawaban yang beralasan saat pikiran saya memberi tahu saya bahwa saya terlambat 2 jam untuk tidur)

jerigen
sumber
19

Kesempatan sempurna untuk menggunakan metode ekstensi:

public static class Int32Methods
{
    public static int DivideByAndRoundUp(this int number, int divideBy)
    {                        
        return (int)Math.Ceiling((float)number / (float)divideBy);
    }
}

Ini juga membuat kode Anda dapat dibaca:

int result = myInt.DivideByAndRoundUp(4);
Joshcomley
sumber
1
Erm, apa? Kode Anda akan disebut myInt.DivideByAndRoundUp () dan akan selalu kembali 1 kecuali untuk input 0 yang akan menyebabkan Exception ...
configurator
5
Kegagalan epik. (-2) .DivideByAndRoundUp (2) mengembalikan 0.
Timwi
3
Saya benar-benar terlambat ke pesta, tetapi apakah kode ini dikompilasi? Kelas Matematika saya tidak mengandung metode Plafon yang membutuhkan dua argumen.
R. Martinho Fernandes
17

Anda bisa menulis pembantu.

static int DivideRoundUp(int p1, int p2) {
  return (int)Math.Ceiling((double)p1 / p2);
}
JaredPar
sumber
1
Masih jumlah casting yang sama terjadi
ChrisF
29
@Outlaw, anggap semua yang Anda inginkan. Tetapi bagi saya jika mereka tidak memasukkannya ke dalam pertanyaan, saya biasanya menganggap mereka tidak mempertimbangkannya.
JaredPar
1
Menulis penolong tidak berguna jika hanya itu. Sebagai gantinya, tulislah pembantu dengan test suite yang komprehensif.
dolmen
3
@dolmen Apakah Anda terbiasa dengan konsep penggunaan kembali kode ? oO
Rushyo
4

Anda dapat menggunakan sesuatu seperti berikut ini.

a / b + ((Math.Sign(a) * Math.Sign(b) > 0) && (a % b != 0)) ? 1 : 0)
Daniel Brückner
sumber
12
Kode ini jelas salah dalam dua cara. Pertama, ada kesalahan kecil dalam sintaksis; Anda membutuhkan lebih banyak tanda kurung. Tetapi yang lebih penting, itu tidak menghitung hasil yang diinginkan. Misalnya, coba pengujian dengan = -1000000 dan b = 3999. Hasil pembagian integer reguler adalah -250. Divisi ganda adalah -250,0625 ... Perilaku yang diinginkan adalah untuk mengumpulkan. Jelas pembulatan yang benar dari -250.0625 adalah pembulatan ke -250, tetapi kode Anda membulatkan ke -249.
Eric Lippert
36
Maaf karena harus terus mengatakan ini, tetapi kode Anda MASIH SALAH. 1/2 seharusnya membulatkan HINGGA 1, tetapi kode Anda membaliknya BAWAH ke 0. Setiap kali saya menemukan bug Anda "memperbaikinya" dengan memperkenalkan bug lain. Saran saya: berhentilah melakukan itu. Ketika seseorang menemukan bug dalam kode Anda, jangan hanya menampar perbaikan tanpa memikirkan dengan jelas apa yang menyebabkan bug itu terjadi. Gunakan praktik rekayasa yang baik; temukan kekurangan dalam algoritme dan perbaiki. Kelemahan dalam ketiga versi yang salah dari algoritma Anda adalah bahwa Anda tidak menentukan dengan benar kapan pembulatan "turun".
Eric Lippert
8
Sulit dipercaya berapa banyak bug yang bisa ada dalam kode kecil ini. Saya tidak pernah punya banyak waktu untuk memikirkannya - hasilnya memanifestasikan dalam komentar. (1) a * b> 0 akan benar jika tidak meluap. Ada 9 kombinasi untuk tanda a dan b - [-1, 0, +1] x [-1, 0, +1]. Kita dapat mengabaikan case b == 0 meninggalkan 6 case [-1, 0, +1] x [-1, +1]. a / b putaran menuju nol, yaitu pembulatan untuk hasil negatif dan pembulatan untuk resuls positif. Oleh karena itu penyesuaian harus dilakukan jika a dan b memiliki tanda yang sama dan keduanya bukan nol.
Daniel Brückner
5
Jawaban ini mungkin adalah hal terburuk yang saya tulis di SO ... dan sekarang terhubung oleh blog Eric ... Yah, maksud saya bukan untuk memberikan solusi yang mudah dibaca; Saya benar-benar mengunci hack pendek dan cepat. Dan untuk mempertahankan solusi saya lagi, saya mendapatkan ide yang benar pertama kali, tetapi tidak memikirkan meluap. Jelas kesalahan saya untuk memposting kode tanpa menulis dan mengujinya di VisualStudio. "Perbaikan" bahkan lebih buruk - saya tidak menyadari bahwa itu adalah masalah meluap dan saya pikir saya membuat kesalahan logis. Karena itu "perbaikan" pertama tidak mengubah apa pun; Saya baru saja membalikkan
Daniel Brückner
10
logika dan mendorong bug di sekitar. Di sini saya membuat kesalahan berikutnya; seperti Eric sudah sebutkan saya tidak benar-benar menganalisis bug dan hanya melakukan hal pertama yang kelihatannya benar. Dan saya masih tidak menggunakan VisualStudio. Oke, saya sedang terburu-buru dan tidak menghabiskan lebih dari lima menit untuk "memperbaiki", tetapi ini seharusnya tidak menjadi alasan. Setelah saya Eric berulang kali menunjukkan bug, saya menyalakan VisualStudio dan menemukan masalah sebenarnya. Perbaikan menggunakan Sign () menjadikannya lebih tidak dapat dibaca dan mengubahnya menjadi kode yang tidak ingin Anda pertahankan. Saya belajar pelajaran saya dan tidak akan lagi meremehkan betapa rumitnya
Daniel Brückner
-2

Beberapa jawaban di atas menggunakan pelampung, ini tidak efisien dan benar-benar tidak perlu. Untuk int unsigned, ini adalah jawaban yang efisien untuk int1 / int2:

(int1 == 0) ? 0 : (int1 - 1) / int2 + 1;

Untuk int yang ditandatangani ini tidak akan benar

Tom Mensink
sumber
Bukan apa yang diminta OP sejak awal, tidak benar-benar menambah jawaban lain.
santamanno
-4

Masalah dengan semua solusi di sini adalah apakah mereka membutuhkan pemain atau mereka memiliki masalah numerik. Melakukan casting ke float atau double selalu menjadi pilihan, tetapi kita bisa melakukan yang lebih baik.

Ketika Anda menggunakan kode jawaban dari @jerryjvl

int div = myInt1 / myInt2;
if ((div >= 0) && (myInt1 % myInt2 != 0))
    div++;

ada kesalahan pembulatan. 1/5 akan membulatkan, karena 1% 5! = 0. Tapi ini salah, karena pembulatan hanya akan terjadi jika Anda mengganti 1 dengan 3, sehingga hasilnya adalah 0,6. Kita perlu menemukan cara untuk mengumpulkan ketika perhitungan memberi kita nilai lebih dari atau sama dengan 0,5. Hasil dari operator modulo pada contoh atas memiliki rentang dari 0 hingga myInt2-1. Pembulatan hanya akan terjadi jika sisanya lebih besar dari 50% pembagi. Jadi kode yang disesuaikan terlihat seperti ini:

int div = myInt1 / myInt2;
if (myInt1 % myInt2 >= myInt2 / 2)
    div++;

Tentu saja kami memiliki masalah pembulatan di myInt2 / 2 juga, tetapi hasil ini akan memberi Anda solusi pembulatan yang lebih baik daripada yang lain di situs ini.

raboni
sumber
"Kita perlu menemukan cara untuk mengumpulkan ketika perhitungan memberi kita nilai lebih dari atau sama dengan 0,5" - Anda telah melewatkan titik pertanyaan ini - atau selalu mengumpulkan yaitu OP ingin mengumpulkan 0,001 ke 1.
Grhm