Mod angka negatif mencairkan otak saya

189

Saya mencoba mod integer untuk mendapatkan posisi array sehingga akan diulang. Melakukan i % arrayLengthberfungsi baik untuk angka positif tetapi untuk angka negatif semuanya salah.

 4 % 3 == 1
 3 % 3 == 0
 2 % 3 == 2
 1 % 3 == 1
 0 % 3 == 0
-1 % 3 == -1
-2 % 3 == -2
-3 % 3 == 0
-4 % 3 == -1

jadi saya butuh implementasi

int GetArrayIndex(int i, int arrayLength)

seperti yang

GetArrayIndex( 4, 3) == 1
GetArrayIndex( 3, 3) == 0
GetArrayIndex( 2, 3) == 2
GetArrayIndex( 1, 3) == 1
GetArrayIndex( 0, 3) == 0
GetArrayIndex(-1, 3) == 2
GetArrayIndex(-2, 3) == 1
GetArrayIndex(-3, 3) == 0
GetArrayIndex(-4, 3) == 2

Saya sudah melakukan ini sebelumnya tetapi untuk beberapa alasan itu melelehkan otak saya hari ini :(

Kolonel Panic
sumber
Lihat diskusi tentang modulus matematika di math.stackexchange.com/questions/519845/...
PPC
blogs.msdn.microsoft.com/ericlippert/2011/12/05/… adalah artikel yang bagus
Samra

Jawaban:

281

Saya selalu menggunakan modfungsi saya sendiri , didefinisikan sebagai

int mod(int x, int m) {
    return (x%m + m)%m;
}

Tentu saja, jika Anda merasa perlu dua panggilan ke operasi modulus, Anda dapat menuliskannya sebagai

int mod(int x, int m) {
    int r = x%m;
    return r<0 ? r+m : r;
}

atau varian daripadanya.

Alasannya adalah karena "x% m" selalu dalam kisaran [-m + 1, m-1]. Jadi, jika semuanya negatif, menambahkan m akan menempatkannya dalam kisaran positif tanpa mengubah nilainya modulo m.

ShreevatsaR
sumber
7
Catatan: untuk kelengkapan teoritik bilangan lengkap, Anda mungkin ingin menambahkan baris di bagian atas yang mengatakan "jika (m <0) m = -m;" meskipun dalam hal ini tidak masalah karena "arrayLength" mungkin selalu positif.
ShreevatsaR
4
Jika Anda akan memeriksa nilai m, Anda juga harus mengecualikan nol.
billpg
6
@RuudLenders: Tidak. Jika x = -5 dan m = 2, maka r = x%madalah -1, setelah r+mitu 1. Loop sementara tidak diperlukan. Intinya adalah bahwa (seperti yang saya tulis dalam jawaban), x%mselalu benar-benar lebih besar daripada -m, jadi Anda perlu menambahkan mpaling banyak satu kali untuk membuatnya positif.
ShreevatsaR
4
@dcastro: Saya tidak ingin -12 mod -10 menjadi 8. ini adalah konvensi yang paling umum dalam matematika, bahwa jika memilih seorang wakil runtuk amodulo b, maka itu adalah sedemikian sehingga 0 ≤ r <| b |.
ShreevatsaR
8
+1. Saya tidak peduli apa yang dilakukan setiap bahasa untuk modulus negatif - 'residu paling tidak negatif' menunjukkan keteraturan matematis dan menghilangkan ambiguitas.
Brett Hale
80

Harap dicatat bahwa% operator C # dan C ++ sebenarnya BUKAN modulo, itu tetap. Rumus untuk modulo yang Anda inginkan, dalam kasus Anda, adalah:

float nfmod(float a,float b)
{
    return a - b * floor(a / b);
}

Anda harus mengode ulang ini dalam C # (atau C ++) tetapi ini adalah cara Anda mendapatkan modulo dan bukan sisanya.

Петър Петров
sumber
21
"Harap dicatat bahwa% operator C ++ sebenarnya BUKAN modulo, itu tetap." Terima kasih, masuk akal sekarang, selalu bertanya-tanya mengapa tidak pernah bekerja dengan baik dengan angka negatif.
leetNightshade
2
"Harap dicatat bahwa% operator C ++ sebenarnya BUKAN modulo, itu sisa." Saya tidak berpikir ini akurat dan saya tidak melihat mengapa modulo berbeda dari yang lain. Itulah yang dikatakan di halaman Modulo Operation Wikipedia. Hanya saja bahasa pemrograman memperlakukan angka negatif secara berbeda. Operator modulo di C # jelas menghitung sisa "dari" nol (-9% 4 = -1, karena 4 * -2 adalah -8 dengan perbedaan -1) sedangkan definisi lain akan menganggap -9% 4 sebagai +3, karena -4 * 3 adalah -12, sisanya +3 (seperti pada fungsi pencarian Google, tidak yakin dengan bahasa back-end di sana).
Tyress
18
Tyress, ada perbedaan antara modulus dan sisanya. Misalnya: -21 mod 4 is 3 because -21 + 4 x 6 is 3. Tetapi -21 divided by 4 gives -5dengan a remainder of -1. Untuk nilai positif, tidak ada perbedaan. Jadi tolong informasikan diri Anda tentang perbedaan-perbedaan ini. Dan jangan percaya Wikipedia setiap saat :)
Петър Петров
2
Mengapa ada orang yang ingin menggunakan fungsi sisa alih-alih modulo? Mengapa mereka membuat %sisanya?
Aaron Franke
4
@ AaronFranke - ini adalah warisan dari CPU sebelumnya yang memiliki perangkat keras divisi untuk dengan cepat menghasilkan hasil bagi dan sisanya - dan inilah yang perangkat keras itu memberikan dividen negatif. Bahasa hanya mencerminkan perangkat keras. Sebagian besar programmer waktu bekerja dengan dividen positif, dan mengabaikan kekhasan ini. Kecepatan adalah yang terpenting.
ToolmakerSteve
16

Implementasi single-line %hanya menggunakan sekali:

int mod(int k, int n) {  return ((k %= n) < 0) ? k+n : k;  }
Evgeni Sergeev
sumber
1
Apakah ini benar? karena saya tidak melihatnya diterima oleh siapa pun, atau komentar untuk itu. Sebagai contoh. mod (-10,6) akan kembali 6. Apakah itu benar? bukankah seharusnya mengembalikan 4?
John Demetriou
3
@JohnDemetriou Nomor Anda sama-sama salah: (A) harus mengembalikan 2 dan (B) mengembalikan 2; coba jalankan kodenya. Butir (A): untuk menemukan mod(-10, 6)dengan tangan, Anda bisa menambah atau mengurangi 6 berulang-ulang sampai jawabannya ada dalam kisaran [0, 6). Notasi ini berarti "inklusif di sebelah kiri, dan eksklusif di sebelah kanan". Dalam kasus kami, kami menambahkan 6 dua kali, memberi 2. Kode ini cukup sederhana, dan mudah untuk melihat bahwa itu benar: pertama, itu sama dengan menambahkan / mengurangi nseperti di atas, kecuali bahwa itu berhenti satu npendek, jika mendekati dari sisi negatifnya. Dalam hal ini kami memperbaikinya. Ada: komentar :)
Evgeni Sergeev
1
Ngomong-ngomong, inilah alasan mengapa menggunakan satu %mungkin ide yang bagus. Lihat tabel Biaya apa saja dalam kode terkelola di artikel Menulis Kode Terkelola Lebih Cepat: Mengetahui Apa Harganya . Penggunaannya %sama mahal dengan yang int divtercantum dalam tabel: sekitar 36 kali lebih mahal daripada menambah atau mengurangi, dan sekitar 13 kali lebih mahal daripada mengalikan. Tentu saja, bukan masalah besar kecuali ini adalah inti dari apa yang dilakukan kode Anda.
Evgeni Sergeev
2
Tetapi apakah satu %lebih mahal daripada tes dan lompatan, terutama jika itu tidak mudah diprediksi?
Medinoc
6

Jawaban ShreevatsaR tidak akan berfungsi untuk semua kasus, bahkan jika Anda menambahkan "jika (m <0) m = -m;", jika Anda memperhitungkan dividen / pembagi negatif.

Sebagai contoh, -12 mod -10 akan menjadi 8, dan seharusnya -2.

Implementasi berikut akan bekerja untuk dividen / pembagi positif dan negatif dan sesuai dengan implementasi lain (yaitu, Java, Python, Ruby, Scala, Skema, Javascript dan Kalkulator Google):

internal static class IntExtensions
{
    internal static int Mod(this int a, int n)
    {
        if (n == 0)
            throw new ArgumentOutOfRangeException("n", "(a mod 0) is undefined.");

        //puts a in the [-n+1, n-1] range using the remainder operator
        int remainder = a%n;

        //if the remainder is less than zero, add n to put it in the [0, n-1] range if n is positive
        //if the remainder is greater than zero, add n to put it in the [n-1, 0] range if n is negative
        if ((n > 0 && remainder < 0) ||
            (n < 0 && remainder > 0))
            return remainder + n;
        return remainder;
    }
}

Test suite menggunakan xUnit:

    [Theory]
    [PropertyData("GetTestData")]
    public void Mod_ReturnsCorrectModulo(int dividend, int divisor, int expectedMod)
    {
        Assert.Equal(expectedMod, dividend.Mod(divisor));
    }

    [Fact]
    public void Mod_ThrowsException_IfDivisorIsZero()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() => 1.Mod(0));
    }

    public static IEnumerable<object[]> GetTestData
    {
        get
        {
            yield return new object[] {1, 1, 0};
            yield return new object[] {0, 1, 0};
            yield return new object[] {2, 10, 2};
            yield return new object[] {12, 10, 2};
            yield return new object[] {22, 10, 2};
            yield return new object[] {-2, 10, 8};
            yield return new object[] {-12, 10, 8};
            yield return new object[] {-22, 10, 8};
            yield return new object[] { 2, -10, -8 };
            yield return new object[] { 12, -10, -8 };
            yield return new object[] { 22, -10, -8 };
            yield return new object[] { -2, -10, -2 };
            yield return new object[] { -12, -10, -2 };
            yield return new object[] { -22, -10, -2 };
        }
    }
dcastro
sumber
Pertama, suatu modfungsi biasanya disebut dengan modulus positif (perhatikan variabel arrayLengthdalam pertanyaan asli yang dijawab di sini, yang mungkin tidak pernah negatif), sehingga fungsi tersebut tidak perlu dibuat untuk bekerja untuk modulus negatif. (Itulah sebabnya saya menyebut perlakuan modulus negatif dalam komentar atas jawaban saya, bukan dalam jawaban itu sendiri.) (Lanjutan ...)
ShreevatsaR
3
(... lanjutan) Kedua, apa yang harus dilakukan untuk modulus negatif adalah masalah konvensi. Lihat misalnya Wikipedia . "Biasanya, dalam teori bilangan, sisa positif selalu dipilih", dan inilah bagaimana saya mempelajarinya juga (dalam Teori Angka Dasar Burton ). Knuth juga mendefinisikannya demikian (khususnya, r = a - b floor(a/b)selalu positif). Bahkan di antara sistem komputer, Pascal dan Maple misalnya, mendefinisikannya selalu positif.
ShreevatsaR
@ShreevatsaR Saya tahu bahwa definisi Euclidian menyatakan bahwa hasilnya akan selalu positif - tetapi saya mendapat kesan bahwa sebagian besar implementasi mod modern akan mengembalikan nilai dalam rentang [n + 1, 0] untuk pembagi negatif "n", yang berarti bahwa -12 mod -10 = -2. Saya sudah melihat ke dalam Google Kalkulator , Python , Ruby dan Scala , dan mereka semua mengikuti konvensi ini.
dcastro
Juga, untuk menambah daftar: Skema dan Javascript
dcastro
1
Sekali lagi, ini masih merupakan bacaan yang bagus. Definisi "selalu positif" (jawaban saya) konsisten dengan ALGOL, Dart, Maple, Pascal, Z3, dll. "Tanda pembagi" (jawaban ini) konsisten dengan: APL, COBOL, J, Lua, Mathematica, MS Excel, Perl, Python, R, Ruby, Tcl, dll. Keduanya tidak konsisten dengan "tanda dividen" seperti pada: AWK, bash, bc, C99, C ++ 11, C #, D, Eiffel, Erlang, Go, Java , OCaml, PHP, Rust, Scala, Swift, VB, x86 assembly, dll. Saya benar-benar tidak melihat bagaimana Anda dapat mengklaim satu konvensi itu "benar" dan yang lain "salah".
ShreevatsaR
6

Menambah pengertian.

Menurut definisi Euclidean hasil mod harus selalu positif.

Ex:

 int n = 5;
 int x = -3;

 int mod(int n, int x)
 {
     return ((n%x)+x)%x;
 }

Keluaran:

 -1
Abin
sumber
15
Saya bingung ... Anda mengatakan bahwa hasilnya harus selalu positif, tetapi kemudian daftar output sebagai -1?
Jeff B
@JeffBridgman Saya telah menyatakan itu berdasarkan definisi Euclidean. `ada dua pilihan yang mungkin untuk sisanya, satu negatif dan positif lainnya, dan ada juga dua pilihan yang mungkin untuk hasil bagi. Biasanya, dalam teori bilangan the positive remainder is always chosen,, tetapi bahasa pemrograman memilih tergantung pada bahasa dan tanda-tanda a dan / atau n. [5] Standard Pascal dan Algol68 memberikan sisa positif (atau 0) bahkan untuk pembagi negatif, dan beberapa bahasa pemrograman, seperti C90, serahkan saja pada implementasi ketika salah satu dari atau negatif adalah '.
Abin
5

Membandingkan dua jawaban utama

(x%m + m)%m;

dan

int r = x%m;
return r<0 ? r+m : r;

Tidak ada yang benar-benar menyebutkan fakta bahwa yang pertama mungkin melempar OverflowExceptionsementara yang kedua tidak. Lebih buruk lagi, dengan konteks tidak dicentang default, jawaban pertama dapat mengembalikan jawaban yang salah (lihat mod(int.MaxValue - 1, int.MaxValue)misalnya). Jadi jawaban kedua sepertinya tidak hanya lebih cepat, tetapi juga lebih tepat.

lilo0
sumber
4

Cukup tambahkan modulus Anda (arrayLength) ke hasil negatif dari% dan Anda akan baik-baik saja.

starblue
sumber
4

Untuk para pengembang yang lebih sadar kinerja

uint wrap(int k, int n) ((uint)k)%n

Perbandingan kinerja kecil

Modulo: 00:00:07.2661827 ((n%x)+x)%x)
Cast:   00:00:03.2202334 ((uint)k)%n
If:     00:00:13.5378989 ((k %= n) < 0) ? k+n : k

Adapun biaya kinerja pemain untuk Anda lihat di sini

Markus Cozowicz
sumber
3
Methink yang -3 % 10seharusnya -3 atau 7. Karena hasil yang tidak negatif diinginkan, 7 akan menjadi jawabannya. Implementasi Anda mengembalikan 3. Anda harus mengubah kedua parameter ke uintdan menghapus gips.
Saya menyukai Stack Overflow
5
Aritmatika tak bertanda hanya setara jika nmerupakan kekuatan dua, dalam hal ini Anda cukup menggunakan logika dan ( (uint)k & (n - 1)) sebagai gantinya, jika kompiler belum melakukannya untuk Anda (kompiler sering kali cukup pintar untuk mengetahui hal ini).
j_schultz
2

Saya suka trik yang disajikan oleh Peter N Lewis di utas ini : "Jika n memiliki rentang terbatas, maka Anda bisa mendapatkan hasil yang Anda inginkan hanya dengan menambahkan kelipatan [pembagi] konstan yang diketahui yang lebih besar daripada nilai absolut dari minimum."

Jadi jika saya memiliki nilai d yaitu dalam derajat dan saya ingin mengambil

d % 180f

dan saya ingin menghindari masalah jika d negatif, maka saya hanya melakukan ini:

(d + 720f) % 180f

Ini mengasumsikan bahwa meskipun d mungkin negatif, diketahui bahwa ia tidak akan pernah lebih negatif dari -720.

RenniePet
sumber
2
-1: tidak cukup umum, (dan sangat mudah untuk memberikan solusi yang lebih umum).
Evgeni Sergeev
4
Ini sebenarnya sangat membantu. ketika Anda memiliki rentang yang berarti, ini dapat menyederhanakan perhitungan. dalam case saya math.stackexchange.com/questions/2279751/…
M.kazem Akhgary
Tepatnya, hanya menggunakan ini untuk perhitungan dayOfWeek (rentang -6 hingga +6) dan disimpan dengan dua %.
NetMage
@EvgeniSergeev +0 untuk saya: tidak menjawab pertanyaan OP tetapi dapat membantu dalam konteks yang lebih spesifik (tapi masih dalam konteks pertanyaan)
Erdal G.
1

Anda mengharapkan perilaku yang bertentangan dengan perilaku yang didokumentasikan dari operator% di c # - mungkin karena Anda mengharapkannya bekerja dengan cara yang berfungsi dalam bahasa lain yang Anda lebih terbiasa. The dokumentasi pada negara-negara c # (penekanan):

Untuk operan tipe integer, hasil dari% b adalah nilai yang dihasilkan oleh a - (a / b) * b. Tanda sisa yang tidak nol sama dengan tanda dari operan sebelah kiri

Nilai yang Anda inginkan dapat dihitung dengan satu langkah ekstra:

int GetArrayIndex(int i, int arrayLength){
    int mod = i % arrayLength;
    return (mod>=0) : mod ? mod + arrayLength;
}
Andrew
sumber
1

Penerapan satu baris jawaban dcastro (paling sesuai dengan bahasa lain):

int Mod(int a, int n)
{
    return (((a %= n) < 0) && n > 0) || (a > 0 && n < 0) ? a + n : a;
}

Jika Anda ingin tetap menggunakan %operator (Anda tidak dapat membebani operator asli di C #):

public class IntM
{
    private int _value;

    private IntM(int value)
    {
        _value = value;
    }

    private static int Mod(int a, int n)
    {
        return (((a %= n) < 0) && n > 0) || (a > 0 && n < 0) ? a + n : a;
    }

    public static implicit operator int(IntM i) => i._value;
    public static implicit operator IntM(int i) => new IntM(i);
    public static int operator %(IntM a, int n) => Mod(a, n);
    public static int operator %(int a, IntM n) => Mod(a, n);
}

Gunakan kasing, keduanya berfungsi:

int r = (IntM)a % n;

// Or
int r = a % n(IntM);
Erdal G.
sumber
0

Semua jawaban di sini berfungsi dengan baik jika pembagi Anda positif, tetapi tidak cukup lengkap. Berikut ini adalah implementasi saya yang selalu mengembalikan pada kisaran[0, b) , sehingga tanda keluaran sama dengan tanda pembagi, memungkinkan pembagi negatif sebagai titik akhir untuk rentang keluaran.

PosMod(5, 3)pengembalian 2
PosMod(-5, 3)pengembalian 1
PosMod(5, -3)pengembalian -1
PosMod(-5, -3)pengembalian-2

    /// <summary>
    /// Performs a canonical Modulus operation, where the output is on the range [0, b).
    /// </summary>
    public static real_t PosMod(real_t a, real_t b)
    {
        real_t c = a % b;
        if ((c < 0 && b > 0) || (c > 0 && b < 0)) 
        {
            c += b;
        }
        return c;
    }

(di mana real_tbisa menjadi jenis nomor apa saja)

Aaron Franke
sumber