Tamsayıların bir bölümünün her zaman yuvarlanmasını nasıl sağlayabilirim?


242

Gerekirse bir tamsayı bölümünün her zaman yuvarlanmasını sağlamak istiyorum. Bundan daha iyi bir yol var mı? Çok fazla döküm var. :-)

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

48
"Daha iyi" olarak düşündüğünüzü daha açık bir şekilde tanımlayabilir misiniz? Daha hızlı? Daha kısa? Daha kesin? Daha sağlam? Daha açık bir şekilde doğru mu?
Eric Lippert

6
Her zaman C # 'da matematik ile çok fazla döküm var - bu yüzden bu tür şeyler için harika bir dil değil. Değerlerin sıfıra yuvarlatılmasını veya uzağa yuvarlanmasını ister misiniz - -3,1'e -3 (yukarı) veya -4 (sıfırdan uzağa) gitmeli
Keith

9
Eric: "Daha doğru mu? Daha sağlam mı? Daha açık doğru mu?" Aslında demek istediğim sadece "daha iyi" idi, okuyucunun daha iyi anlam vermesine izin verirdim. Birisi daha kısa bir kod parçasına sahip olsaydı, harika, bir başkası daha hızlı, aynı zamanda harika olsaydı :-) Ya sen herhangi bir önerin var mı?
Karsten

1
Başlığı okuduktan sonra, "Oh, bu bir çeşit C # toplaması mı?"
Matt Ball

6
Bu sorunun ne kadar zor olduğu ortaya çıktı ve tartışmanın ne kadar öğretici olduğu gerçekten şaşırtıcı.
Justin Morgan

Yanıtlar:


669

GÜNCELLEME: Bu soru Ocak 2013'teki blogumun konusuydu . Bu mükemmel soru için teşekkürler!


Tamsayı aritmetiğini doğru yapmak zordur. Şimdiye kadar çokça gösterildiği gibi, "akıllı" bir numara yapmaya çalıştığınız anda, hata yaptığınız ihtimaller iyidir. Ve bir kusur bulunduğunda, düzeltmenin başka bir şeyi bozup bozmadığını düşünmeden kusuru düzeltmek için kodu değiştirmek iyi bir problem çözme tekniği değildir. Şimdiye kadar, bu tamamen özellikle zor olmayan soruna beş farklı yanlış tamsayı aritmetik çözüm olduğunu düşünüyorum.

Tamsayı aritmetik problemlerine yaklaşmanın doğru yolu - yani, cevabı ilk seferde doğru yapma olasılığını artıran yol - soruna dikkatlice yaklaşmak, her seferinde bir adım çözmek ve iyi mühendislik prensiplerini kullanmaktır yani.

Değiştirmeye çalıştığınız şeyin özelliklerini okuyarak başlayın. Tamsayı bölme spesifikasyonu açıkça belirtmektedir:

  1. Bölme sonucu sıfıra yuvarlar

  2. İki işlenen aynı işarete sahip olduğunda sonuç sıfır veya pozitiftir ve iki işlenen ters işarete sahip olduğunda sıfır veya negatiftir.

  3. Sol işlenen temsil edilebilir en küçük int ise ve sağ işlenen –1 ise taşma oluşur. [...], [bir ArithmeticException] öğesinin atılıp atılmadığı veya taşma işleminin, sonuçta elde edilen değer sol işlenenin değeri ile bildirilmeyip bildirilmeyeceği olarak tanımlanır.

  4. Sağ işlenenin değeri sıfırsa, bir System.DivideByZeroException oluşturulur.

İstediğimiz, bölümü hesaplayan ancak sonucu her zaman sıfıra değil, daima yukarı doğru yuvarlayan bir tamsayı bölme işlevidir .

Bu işlev için bir şartname yazın. İşlevimizin int DivRoundUp(int dividend, int divisor)olası her girdi için tanımlanmış davranışı olmalıdır. Bu tanımsız davranış derinden endişe verici, bu yüzden onu ortadan kaldıralım. Operasyonumuzun bu spesifikasyona sahip olduğunu söyleyeceğiz:

  1. bölen sıfırsa işlem atar

  2. temettü int. minval ve bölen -1 ise işlem atar

  3. kalan yoksa - bölünme 'çift' ise - dönüş değeri integral katsayısıdır

  4. Aksi takdirde , bölümden daha büyük olan en küçük tamsayıyı döndürür , yani her zaman yukarı yuvarlar.

Şimdi bir spesifikasyonumuz var, bu yüzden test edilebilir bir tasarım geliştirebileceğimizi biliyoruz . "Çifte" çözüm, sorun ifadesinde açıkça reddedildiği için, sorunun bir çift olarak hesaplanması yerine, yalnızca tamsayı aritmetiği ile çözülebileceği ek bir tasarım kriteri eklediğimizi varsayalım.

Peki ne hesaplamalıyız? Açıkçası, sadece tamsayı aritmetiğinde kalırken spesifikasyonumuzu karşılamak için üç gerçeği bilmemiz gerekir. İlk olarak, tamsayı bölümü neydi? İkincisi, bölünmenin geri kalanı yok muydu? Ve üçüncüsü, değilse, tamsayı bölümü yukarı veya aşağı yuvarlanarak hesaplandı mı?

Artık bir şartnamemiz ve bir tasarımımız olduğuna göre, kod yazmaya başlayabiliriz.

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;
}

Bu akıllı mı? Güzel değil? Hayır. Kısa mı? Hayır. Spesifikasyona göre doğru mu? Buna inanıyorum, ama tam olarak test etmedim. Yine de oldukça iyi görünüyor.

Biz burada profesyoneliz; iyi mühendislik uygulamalarını kullanır. Araçlarınızı araştırın, istediğiniz davranışı belirtin, önce hata durumlarını göz önünde bulundurun ve açık doğruluğunu vurgulamak için kodu yazın. Bir hata bulduğunuzda, rastgele karşılaştırmaların yönlerini değiştirmeye ve zaten çalışan şeyleri kırmaya başlamadan önce algoritmanızın başlamak için derinden kusurlu olup olmadığını düşünün.


44
Mükemmel örnek cevap
Gavin Miller

61
Önemsediğim şey davranış değil; her iki davranış da haklı görünüyor. Önem verdiğim şey, belirtilmediğidir , yani kolayca test edilemez. Bu durumda, kendi operatörünüzü tanımlarız, böylece hangi davranışı sevdiğimizi belirtebiliriz. ben bu davranış "atmak" ya da "atmayın" umurumda değil, ama ben belirtilmesi umurumda değil.
Eric Lippert

68
Lanet olsun, bilgiçlik başarısız :(
Jon Skeet

32
Adam - Bununla ilgili bir kitap yazabilir misiniz lütfen?
xtofl

76
@finnw: Test edip etmediğim önemli değil. Bu tamsayı aritmetik problemini çözmek benim iş problemim değil ; öyleyse, test ederdim. Birisi iş sorununu çözmek için internetten yabancılardan kod almak istiyorsa, o zaman iyice test etmek için üzerlerinde .
Eric Lippert

49

Şimdiye kadar tüm cevaplar oldukça karmaşık görünüyor.

C # ve Java'da, pozitif temettü ve bölen için yapmanız gerekenler:

( dividend + divisor - 1 ) / divisor 

Kaynak: Sayı Dönüşümü, Roland Backhouse, 2001


Muhteşem. Rağmen, belirsizlikleri kaldırmak için parantez eklemeliyiz.Onun kanıtı biraz uzundu, ama bağırsakta, sadece ona bakarak doğru olduğunu hissedebilirsiniz.
Jörgen Sigvardsson

1
Hmmm ... ya temettü = 4, bölen = (- 2) ??? Yuvarlandıktan sonra 4 / (-2) = (-2) = (-2). ancak sağladığınız algoritma yuvarlandıktan sonra (4 + (-2) - 1) / (-2) = 1 / (-2) = (-0.5) = 0 olur.
Scott

1
@Scott - üzgünüm, bu çözümün sadece pozitif temettü ve bölen için geçerli olduğunu belirtmem gerekir. Bunu açıklığa kavuşturmak için cevabımı güncelledim.
Ian Nelson

1
hoşuma gitti, elbette bu yaklaşımın bir yan ürünü olarak payda biraz yapay bir taşma olabilir ...
TCC

2
@PIntag: Fikir iyi, ama modulo kullanımı yanlış. 13 ve 3'ü alın. Beklenen sonuç 5, ancak ((13-1)%3)+1)sonuç olarak 1 verir. Doğru türdeki bölünmeyi almak, 1+(dividend - 1)/divisorpozitif temettü ve bölen cevabı ile aynı sonucu verir. Ayrıca, taşma problemi yoktur, ancak yapay olabilirler.
Lutz Lehmann

48

Son int tabanlı cevap

İşaretli tamsayılar için:

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

İmzasız tamsayılar için:

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

Bu cevabın nedeni

Tamsayı bölümü ' /' sıfıra (spesifikasyonun 7.7.2'si) yuvarlanması olarak tanımlanır, ancak yuvarlamak istiyoruz. Bu, olumsuz cevapların zaten doğru yuvarlandığı anlamına gelir, ancak olumlu cevapların ayarlanması gerekir.

Sıfır olmayan pozitif cevapları tespit etmek kolaydır, ancak sıfır cevabı biraz daha zordur, çünkü bu negatif bir değerin yuvarlanması veya pozitif bir yuvarlamanın yuvarlanması olabilir.

En güvenli bahis, her iki tamsayı işaretinin aynı olduğunu kontrol ederek cevabın ne zaman pozitif olması gerektiğini tespit etmektir. Tamsayı xveya operatörü ' ^' iki değerde, bu durumda 0 işaret bitiyle sonuçlanır, bu da negatif olmayan bir sonuç anlamına gelir, bu nedenle kontrol (a ^ b) >= 0yuvarlamadan önce sonucun pozitif olması gerektiğini belirler. Ayrıca imzasız tamsayılar için her cevabın açıkça olumlu olduğuna dikkat edin, bu nedenle bu kontrol atlanabilir.

Geriye kalan tek kontrol a % b != 0, işi yapacak yuvarlama olup olmadığıdır .

Dersler öğrenildi

Aritmetik (tamsayı veya başka türlü) göründüğü kadar basit değildir. Her zaman dikkatli düşünmek gerekir.

Ayrıca, son cevabım belki de kayan nokta cevapları kadar 'basit' veya 'açık' veya belki de 'hızlı' olmasa da, benim için çok güçlü bir kullanım kalitesi var; Şimdi cevabı düşündüm, bu yüzden bunun doğru olduğundan eminim (biri daha akıllıca bir şey söyleyene kadar - Eric'in yönüne gizlice bakış -).

Kayan nokta cevabı hakkında aynı kesinlik hissi elde etmek için, kayan nokta hassasiyetinin yoluna girebileceği herhangi bir koşul olup olmadığını ve Math.Ceilingbelki de 'sadece doğru' girdilerde istenmeyen bir şey.

Gidilen yol

(Ben ikincisini yerini not değiştirme myInt1ile myInt2bu ne anlama geldiğini varsayarak,):

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

ile:

(myInt1 - 1 + myInt2) / myInt2

Tek uyarı, kullandığınız myInt1 - 1 + myInt2tamsayı türünü aşarsa, beklediğinizi alamayabilirsiniz.

Nedeni yanlış : -1000000 ve 3999 -250 vermeli, bu -249 verir

DÜZENLEME:
Bunun negatif myInt1değerler için diğer tamsayı çözümü ile aynı hataya sahip olduğu düşünüldüğünde, aşağıdaki gibi bir şey yapmak daha kolay olabilir:

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

Bu divyalnızca tamsayı işlemlerinin kullanımında doğru sonucu vermelidir .

Nedeni yanlış : -1 ve -5 1 vermeli, bu 0 verir

EDIT (bir kez daha, duygu ile):
Bölme operatörü sıfıra yuvarlar; negatif sonuçlar için bu kesinlikle doğrudur, bu nedenle sadece negatif olmayan sonuçların ayarlanması gerekir. Ayrıca DivRema /ve her %neyse, çağrıyı atlayalım (ve gerekli olmadığında modulo hesaplamasını önlemek için kolay karşılaştırma ile başlayalım):

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

Bunun nedeni yanlış : -1 ve 5 0 vermeli, bu 1 veriyor

(Son girişimimi kendi savunmamda, aklım bana uyku için 2 saat geç kaldığımı söylerken asla mantıklı bir cevap vermeye kalkışmamalıydım)


19

Bir uzatma yöntemi kullanmak için mükemmel şans:

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

Bu, kod uber'inizi de okunabilir hale getirir:

int result = myInt.DivideByAndRoundUp(4);

1
Ne? Kodunuz myInt.DivideByAndRoundUp () olarak adlandırılır ve bir İstisnaya neden olacak 0 girişi dışında her zaman 1 döndürür ...
yapılandırıcı

5
Destansı başarısızlık. DivideByAndRoundUp (2) 0 döndürür.
Timwi

3
Partiye gerçekten geç kaldım, ama bu kod derleniyor mu? Math sınıfım iki argüman alan bir Tavan yöntemi içermiyor.
R. Martinho Fernandes

17

Bir yardımcı yazabilirsiniz.

static int DivideRoundUp(int p1, int p2) {
  return (int)Math.Ceiling((double)p1 / p2);
}

1
Yine de aynı miktarda döküm devam ediyor
ChrisF

29
@Outlaw, istediğinizi varsayalım. Ama benim için soruyu sormazlarsa, genellikle bunu düşünmediklerini varsayıyorum.
JaredPar

1
Bir yardımcı yazmak, sadece bu ise işe yaramaz. Bunun yerine, kapsamlı bir test paketiyle bir yardımcı yazın.
dolmen

3
@dolmen Kodun yeniden kullanımı kavramını biliyor musunuz ? oO
Rushyo

4

Aşağıdaki gibi bir şey kullanabilirsiniz.

a / b + ((Math.Sign(a) * Math.Sign(b) > 0) && (a % b != 0)) ? 1 : 0)

12
Bu kod iki şekilde yanlıştır. Öncelikle, sözdiziminde küçük bir hata var; daha fazla parantez lazım. Ancak daha da önemlisi, istenen sonucu hesaplamaz. Örneğin, bir = -1000000 ve b = 3999 ile test etmeyi deneyin. Normal tamsayı bölümü sonucu -250'dir. Çift bölüm -250.0625 ... İstenen davranış yuvarlamaktır. Açıkça -250.0625'ten doğru yuvarlama -250'ye yuvarlamaktır, ancak kodunuz -249'a yuvarlanır.
Eric Lippert

36
Bunu söylemeye devam ettiğim için üzgünüm ama kodun hala YANLIŞ Daniel. 1/2, 1'e yuvarlanmalıdır, ancak kodunuz AŞAĞI 0'a yuvarlar. Her hata bulduğumda, başka bir hata vererek onu düzeltirsiniz. Benim tavsiyem: bunu yapmayı kes. Birisi kodunuzda bir hata bulduğunda, ilk etapta hataya neden olan şeyi net bir şekilde düşünmeden bir düzeltme yapmayın. İyi mühendislik uygulamaları kullanın; algoritmada kusur bulmak ve düzeltmek. Algoritmanızın her üç yanlış versiyonundaki kusur, yuvarlamanın ne zaman "aşağı" olduğunu doğru bir şekilde belirlememenizdir.
Eric Lippert

8
Bu küçük kod parçasında kaç hata olabileceği inanılmaz. Bunu düşünmek için hiç zamanım olmadı - sonuç yorumlarda ortaya çıkıyor. (1) a * b> 0 taşmadığı takdirde doğru olur. A ve b - [-1, 0, +1] x [-1, 0, +1] 'nin işareti için 9 kombinasyon vardır. 6 vakayı [-1, 0, +1] x [-1, +1] bırakarak b == 0 vakasını görmezden gelebiliriz. a / b sıfıra doğru yuvarlar, yani negatif sonuçlar için yuvarlanır ve pozitif sonuçlar için yuvarlanır. Dolayısıyla, a ve b aynı işarete sahipse ve her ikisi de sıfır değilse, ayarlama yapılmalıdır.
Daniel Brückner

5
Bu cevap muhtemelen SO üzerine yazdığım en kötü şey ... ve şimdi Eric'in blogu ile bağlantılı ... Niyetim okunabilir bir çözüm vermek değildi; Gerçekten kısa ve hızlı bir hack için kilitliyordum. Ve çözümümü tekrar savunmak için, fikri ilk kez doğru anladım, ancak taşmaları düşünmedim. Açıkçası, kodu VisualStudio'da yazmadan ve test etmeden göndermek benim hatamdı. "Düzeltmeler" daha da kötü - bir taşma sorunu olduğunu fark etmedi ve mantıklı bir hata yaptım düşündüm. Sonuç olarak ilk "düzeltmeler" hiçbir şeyi değiştirmedi; Ben sadece ters
Daniel Brückner

10
mantığı ve böcek etrafında itti. Burada sonraki hataları yaptım; Eric'in daha önce bahsettiği gibi, hatayı gerçekten analiz etmedim ve doğru görünen ilk şeyi yaptım. Ve hala VisualStudio kullanmadım. Tamam, acelem vardı ve "düzeltme" ye beş dakikadan fazla zaman harcamadım, ama bu bir bahane olmamalı. Eric defalarca hatayı işaret ettikten sonra VisualStudio'yu ateşledim ve gerçek problemi buldum. Sign () kullanan düzeltme, işi daha da okunmaz hale getirir ve gerçekten korumak istemediğiniz koda dönüştürür. Dersimi öğrendim ve artık ne kadar zor olduğunu hafife almayacağım
Daniel Brückner

-2

Yukarıdaki cevaplardan bazıları float kullanıyor, bu verimsiz ve gerçekten gerekli değil. İmzasız ints için bu int1 / int2 için etkili bir cevaptır:

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

İmzalı girişler için bu doğru olmayacak


OP'nin ilk etapta sorduğu şey, diğer cevaplara gerçekten eklemiyor.
santamanno

-4

Buradaki tüm çözümlerle ilgili sorun, ya bir oyuncuya ihtiyaç duymaları ya da sayısal bir problemleri olması. Şamandıra veya iki katına çıkarmak her zaman bir seçenektir, ancak daha iyisini yapabiliriz.

@Jerryjvl tarafından verilen yanıtın kodunu kullandığınızda

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

bir yuvarlama hatası var. 1/5 yuvarlanır, çünkü% 1 5! = 0. Ancak bu yanlıştır, çünkü yuvarlama yalnızca 1'i 3 ile değiştirirseniz gerçekleşir, bu nedenle sonuç 0,6 olur. Hesaplama bize 0,5 veya daha büyük bir değer verdiğinde yuvarlanmanın bir yolunu bulmalıyız. Üst örnekteki modulo operatörünün sonucu 0 ila myInt2-1 arasındadır. Yuvarlama, yalnızca geri kalan bölenin% 50'sinden fazla olduğunda gerçekleşir. Ayarlanan kod şöyle görünür:

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

Tabii ki myInt2 / 2'de de yuvarlama problemimiz var, ancak bu sonuç size bu sitedeki diğerlerinden daha iyi bir yuvarlama çözümü verecektir.


"Hesaplama bize 0,5 veya daha büyük bir değer verdiğinde yuvarlanmanın bir yolunu bulmalıyız" - bu sorunun noktasını kaçırdınız - veya her zaman yuvarlayın, yani OP 0.001 ile 1 arasında yuvarlamak istiyor.
Grhm
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.