Tại sao rand () + rand () tạo ra số âm?


304

Tôi quan sát thấy rand()chức năng thư viện khi nó được gọi chỉ một lần trong một vòng lặp, nó hầu như luôn tạo ra các số dương.

for (i = 0; i < 100; i++) {
    printf("%d\n", rand());
}

Nhưng khi tôi thêm hai rand()cuộc gọi, các số được tạo bây giờ có nhiều số âm hơn.

for (i = 0; i < 100; i++) {
    printf("%d = %d\n", rand(), (rand() + rand()));
}

Ai đó có thể giải thích tại sao tôi thấy số âm trong trường hợp thứ hai?

PS: Tôi khởi tạo hạt giống trước vòng lặp là srand(time(NULL)).


11
rand()không thể phủ định ...
twentylemon

293
rand () + rand () có thể owerflow
maskacovnik

13
Là gì RAND_MAXcho trình biên dịch của bạn? Bạn thường có thể tìm thấy nó trong stdlib.h. (Hài hước: kiểm tra man 3 rand, nó mang mô tả một dòng "trình tạo số ngẫu nhiên xấu".)
usr2564301

6
làm những gì mọi lập trình viên lành mạnh sẽ làm abs(rand()+rand()). Tôi muốn có một UB tích cực hơn là một tiêu cực! ;)
Vinicius Kamakura

11
@hexa: đó không phải là sự phân bổ cho UB, vì đã xảy ra sự bổ sung. Bạn không thể làm cho UB trở thành hành vi được xác định . Một progrtammer lành mạnh sẽ tránh UB như địa ngục.
quá trung thực cho trang web này

Câu trả lời:


542

rand()được định nghĩa để trả về một số nguyên giữa 0RAND_MAX.

rand() + rand()

có thể tràn. Những gì bạn quan sát có thể là kết quả của hành vi không xác định gây ra bởi tràn số nguyên.


4
@JakubArnold: Làm thế nào mà hành vi tràn được chỉ định bởi mỗi ngôn ngữ khác nhau? Ví dụ Python không có (tốt, tối đa bộ nhớ khả dụng), khi int chỉ phát triển.
quá trung thực cho trang web này

2
@Olaf Nó phụ thuộc vào cách một ngôn ngữ quyết định đại diện cho số nguyên đã ký. Java không có cơ chế để phát hiện tràn số nguyên (cho đến java 8) và định nghĩa nó để bọc xung quanh và Go chỉ sử dụng biểu diễn bổ sung của 2 và định nghĩa nó hợp pháp cho các tràn số nguyên đã ký. C rõ ràng hỗ trợ bổ sung nhiều hơn 2.
PP

2
@EvanCarslake Không, đó không phải là một hành vi phổ quát. Những gì bạn nói là về đại diện bổ sung của 2. Nhưng ngôn ngữ C cũng cho phép các đại diện khác. Đặc tả ngôn ngữ C cho biết tràn số nguyên đã ký là không xác định . Vì vậy, nói chung, không có chương trình nào nên dựa vào hành vi như vậy và cần mã hóa cẩn thận để không gây ra tràn số nguyên đã ký. Nhưng điều này không áp dụng cho các số nguyên không dấu vì chúng sẽ "bao bọc" theo cách được xác định rõ (modulo 2). [tiếp tục] ...
PP

12
Đây là trích dẫn từ tiêu chuẩn C liên quan đến tràn số nguyên đã ký: Nếu một điều kiện ngoại lệ xảy ra trong quá trình đánh giá biểu thức (nghĩa là, nếu kết quả không được xác định theo toán học hoặc không nằm trong phạm vi giá trị đại diện cho loại của nó), thì hành vi không định nghĩa được.
PP

3
@EvanCarslake di chuyển một chút khỏi câu hỏi trình biên dịch C sử dụng tiêu chuẩn và cho các số nguyên đã ký, họ có thể cho rằng a + b > anếu họ biết điều đó b > 0. Họ cũng có thể giả định rằng nếu có câu lệnh được thực thi a + 5sau đó thì giá trị hiện tại sẽ thấp hơn INT_MAX - 5. Vì vậy, ngay cả trên bộ xử lý / trình thông dịch bổ sung của 2 chương trình không có bẫy cũng có thể không hoạt động như thể ints là phần bù 2 mà không có bẫy.
Maciej Piechotka

90

Vấn đề là sự bổ sung. rand()trả về một intgiá trị của 0...RAND_MAX. Vì vậy, nếu bạn thêm hai trong số họ, bạn sẽ nhận được RAND_MAX * 2. Nếu vượt quá INT_MAX, kết quả của bổ sung sẽ vượt quá phạm vi hợp lệ mà an intcó thể giữ. Tràn đầy các giá trị đã ký là hành vi không xác định và có thể dẫn đến bàn phím của bạn nói chuyện với bạn bằng tiếng nước ngoài.

Vì không có lợi ở đây khi thêm hai kết quả ngẫu nhiên, ý tưởng đơn giản là không làm điều đó. Ngoài ra, bạn có thể truyền từng kết quả unsigned inttrước khi bổ sung nếu điều đó có thể giữ tổng. Hoặc sử dụng một loại lớn hơn. Lưu ý rằng longkhông nhất thiết phải rộng hơn int, tương tự áp dụng cho long longnếu intít nhất là 64 bit!

Kết luận: Chỉ cần tránh bổ sung. Nó không cung cấp thêm "tính ngẫu nhiên". Nếu bạn cần nhiều bit hơn, bạn có thể nối các giá trị sum = a + b * (RAND_MAX + 1), nhưng điều đó cũng có thể yêu cầu loại dữ liệu lớn hơn int.

Vì lý do đã nêu của bạn là để tránh kết quả bằng 0: Điều đó không thể tránh được bằng cách thêm kết quả của hai rand()cuộc gọi, vì cả hai đều có thể bằng không. Thay vào đó, bạn chỉ có thể tăng lên. Nếu RAND_MAX == INT_MAX, điều này không thể được thực hiện trong int. Tuy nhiên, (unsigned int)rand() + 1sẽ làm rất, rất có thể. Có khả năng (không dứt khoát), vì nó yêu cầu UINT_MAX > INT_MAX, điều này đúng với tất cả các triển khai mà tôi biết (bao gồm khá nhiều kiến ​​trúc nhúng, DSP và tất cả các nền tảng máy tính để bàn, thiết bị di động và máy chủ trong 30 năm qua).

Cảnh báo:

Mặc dù đã được bình luận ở đây, xin lưu ý rằng việc thêm hai giá trị ngẫu nhiên sẽ không có phân phối đồng đều, nhưng phân phối hình tam giác như lăn hai con xúc xắc: để có được 12(hai con xúc xắc) cả hai con xúc xắc phải hiển thị 6. vì 11đã có hai biến thể có thể: 6 + 5hoặc 5 + 6, v.v.

Vì vậy, việc bổ sung cũng là xấu từ khía cạnh này.

Cũng lưu ý rằng các kết quả rand()tạo ra không độc lập với nhau, vì chúng được tạo bởi một trình tạo số giả ngẫu nhiên . Cũng lưu ý rằng tiêu chuẩn không chỉ định phân phối chất lượng hoặc thống nhất của các giá trị được tính toán.


14
@badmad: Vậy nếu cả hai cuộc gọi trả về 0 thì sao?
quá trung thực cho trang web này

3
@badmad: Tôi chỉ tự hỏi nếu UINT_MAX > INT_MAX != falseđược bảo đảm theo tiêu chuẩn. (Âm thanh có khả năng, nhưng không chắc chắn nếu được yêu cầu). Nếu vậy, bạn chỉ có thể bỏ một kết quả và tăng (theo thứ tự đó!).
quá trung thực cho trang web này

3
Có nhiều lợi ích khi thêm nhiều số ngẫu nhiên khi bạn muốn phân phối không đồng đều: stackoverflow.com/questions/30492259/
Kẻ

6
để tránh 0, đơn giản là "trong khi kết quả là 0, cuộn lại"?
Olivier Dulac

2
Không chỉ là thêm chúng một cách xấu để tránh 0, mà còn dẫn đến phân phối không đồng đều. Bạn nhận được phân phối như kết quả của xúc xắc lăn: 7 gấp 6 lần so với 2 hoặc 12.
Barmar

36

Đây là một câu trả lời để làm rõ câu hỏi được đưa ra trong nhận xét cho câu trả lời này ,

lý do tôi đã thêm là để tránh '0' là số ngẫu nhiên trong mã của tôi. rand () + rand () là giải pháp bẩn nhanh chóng xuất hiện trong tâm trí tôi.

Vấn đề là tránh 0. Có (ít nhất) hai vấn đề với giải pháp đề xuất. Một là, như các câu trả lời khác chỉ ra, rand()+rand()có thể gọi hành vi không xác định. Lời khuyên tốt nhất là không bao giờ gọi hành vi không xác định. Một vấn đề khác là không có gì đảm bảo rằng rand()sẽ không sản xuất 0 lần liên tiếp.

Việc sau đây từ chối số không, tránh hành vi không xác định và trong phần lớn các trường hợp sẽ nhanh hơn hai cuộc gọi đến rand():

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) {}
// or do rnum = rand(); while (rnum == 0);

9
Thế còn rand() + 1?
hỏi

3
@askvictor Điều đó có thể tràn (mặc dù không thể).
gerrit

3
@gerrit - phụ thuộc vào MAX_INT và RAND_MAX
hỏi

3
@gerrit, tôi sẽ ngạc nhiên nếu chúng không giống nhau, nhưng tôi cho rằng đây là nơi dành cho trẻ em :)
hỏi

10
Nếu RAND_MAX == MAX_INT, rand () + 1 sẽ tràn với xác suất chính xác giống như giá trị của rand () là 0, điều này làm cho giải pháp này hoàn toàn vô nghĩa. Nếu bạn sẵn sàng mạo hiểm và bỏ qua khả năng tràn, bạn cũng có thể sử dụng rand () và bỏ qua khả năng trả lại 0.
Emil Jeřábek

3

Về cơ bản rand()sản xuất số giữa 0RAND_MAX, và 2 RAND_MAX > INT_MAXtrong trường hợp của bạn.

Bạn có thể mô đun với giá trị tối đa của kiểu dữ liệu của bạn để ngăn chặn tràn. Chương trình này sẽ phá vỡ sự phân phối của các số ngẫu nhiên, nhưng randchỉ là một cách để có được các số ngẫu nhiên nhanh chóng.

#include <stdio.h>
#include <limits.h>

int main(void)
{
    int i=0;

    for (i=0; i<100; i++)
        printf(" %d : %d \n", rand(), ((rand() % (INT_MAX/2))+(rand() % (INT_MAX/2))));

    for (i=0; i<100; i++)
        printf(" %d : %ld \n", rand(), ((rand() % (LONG_MAX/2))+(rand() % (LONG_MAX/2))));

    return 0;
}

2

Có thể bạn có thể thử một cách tiếp cận phức tạp hơn bằng cách đảm bảo rằng giá trị được trả về bằng tổng 2 rand () không bao giờ vượt quá giá trị của RAND_MAX. Một cách tiếp cận có thể có thể là sum = rand () / 2 + rand () / 2; Điều này sẽ đảm bảo rằng đối với trình biên dịch 16 bit có giá trị RAND_MAX là 32767 ngay cả khi cả hai rand xảy ra trả về 32767, thậm chí sau đó (32767/2 = 16383) 16383 + 16383 = 32766, do đó sẽ không dẫn đến tổng âm.


1
OP muốn loại trừ 0 khỏi kết quả. Việc bổ sung cũng không cung cấp phân phối thống nhất các giá trị ngẫu nhiên.
quá trung thực cho trang web này

@Olaf: Không có gì đảm bảo rằng hai cuộc gọi liên tiếp rand()sẽ không mang lại kết quả bằng 0, vì vậy mong muốn tránh số 0 không phải là lý do chính đáng để thêm hai giá trị. Mặt khác, mong muốn có một phân phối không đồng đều sẽ là một lý do chính đáng để thêm hai giá trị ngẫu nhiên nếu một đảm bảo rằng tràn không xảy ra.
supercat

1

lý do tôi đã thêm là để tránh '0' là số ngẫu nhiên trong mã của tôi. rand () + rand () là giải pháp bẩn nhanh chóng xuất hiện trong tâm trí tôi.

Một giải pháp đơn giản (được thôi, gọi nó là "Hack") không bao giờ tạo ra kết quả bằng 0 và sẽ không bao giờ tràn ra là:

x=(rand()/2)+1    // using divide  -or-
x=(rand()>>1)+1   // using shift which may be faster
                  // compiler optimization may use shift in both cases

Điều này sẽ giới hạn giá trị tối đa của bạn, nhưng nếu bạn không quan tâm đến điều đó, thì điều này sẽ hoạt động tốt với bạn.


1
Sidenote: Cẩn thận với sự thay đổi bên phải của các biến đã ký. Nó chỉ được xác định rõ cho các giá trị không âm, đối với phủ định, nó được thực hiện được xác định. (May mắn thay, rand()luôn trả về một giá trị không âm). Tuy nhiên, tôi sẽ để tối ưu hóa cho trình biên dịch ở đây.
quá trung thực cho trang web này

@Olaf: Nói chung, việc chia hai chữ ký sẽ kém hiệu quả hơn một ca. Trừ khi một nhà văn biên dịch đã đầu tư công sức trong việc kể trình biên dịch rằng randsẽ không âm, sự thay đổi sẽ hiệu quả hơn so với sự phân chia bởi một ký số nguyên 2. Division bởi 2ucó thể làm việc, nhưng nếu xlà một intcó thể dẫn đến cảnh báo về chuyển đổi ngầm từ unsigned ký kết.
supercat

@supercat: Vui lòng đọc lại bình luận của tôi. Bạn nên biết rõ bất kỳ trình biên dịch hợp lý nào cũng sẽ sử dụng một ca làm việc / 2(tôi đã thấy điều này ngay cả đối với một cái gì đó như -O0, tức là không có tối ưu hóa được yêu cầu rõ ràng). Nó có thể là tối ưu hóa tầm thường nhất và thành lập nhất của mã C. Điểm là sự phân chia được xác định rõ bởi tiêu chuẩn cho toàn bộ phạm vi số nguyên, không chỉ các giá trị không âm. Một lần nữa: để lại các lựa chọn cho trình biên dịch, viết mã chính xác và rõ ràng ở vị trí đầu tiên. Điều này thậm chí còn quan trọng hơn cho người mới bắt đầu.
quá trung thực cho trang web này

@Olaf: Mỗi trình biên dịch tôi đã kiểm tra sẽ tạo mã hiệu quả hơn khi dịch chuyển sang rand()phải một hoặc chia cho 2uhơn so với khi chia cho 2, ngay cả khi sử dụng -O3. Một cách hợp lý có thể nói tối ưu hóa như vậy là không có vấn đề, nhưng nói rằng "để lại tối ưu hóa như vậy cho trình biên dịch" sẽ ngụ ý rằng trình biên dịch sẽ có khả năng thực hiện chúng. Bạn có biết bất kỳ trình biên dịch nào thực sự sẽ?
supercat

@supercat: Bạn nên sử dụng trình biên dịch hiện đại hơn sau đó. gcc vừa tạo mã tốt vào lần cuối cùng tôi kiểm tra Trình biên dịch được tạo. Tuy nhiên, như tôi đã chấp nhận để có một Groopie, tôi không muốn bị quấy rối đến phần mở rộng mà bạn trình bày lần trước. Những bài viết này đã cũ, ý kiến ​​của tôi là hoàn toàn hợp lệ. Cảm ơn bạn.
quá trung thực cho trang web này

1

Để tránh 0, hãy thử điều này:

int rnumb = rand()%(INT_MAX-1)+1;

Bạn cần bao gồm limits.h.


4
Điều đó sẽ tăng gấp đôi xác suất để có được 1. Về cơ bản là giống nhau (nhưng chậm hơn một cách có điều kiện) khi thêm 1 điều kiện nếu rand()mang lại 0.
quá trung thực cho trang web này vào

Vâng, bạn đúng Olaf. Nếu rand () = 0 hoặc INT_MAX -1 thì số rnumb sẽ là 1.
Doni

Thậm chí tệ hơn, khi tôi nghĩ về nó. Nó thực sự sẽ tăng gấp đôi khả năng cho 12(tất cả giả định RAND_MAX == INT_MAX). Tôi đã quên mất - 1.
quá trung thực cho trang web này

1
-1đây phục vụ không có giá trị. rand()%INT_MAX+1; vẫn sẽ chỉ tạo các giá trị trong phạm vi [1 ... INT_MAX].
chux - Phục hồi lại

-2

Trong khi những gì mọi người khác đã nói về khả năng tràn rất có thể là nguyên nhân của tiêu cực, ngay cả khi bạn sử dụng số nguyên không dấu. Vấn đề thực sự là sử dụng chức năng thời gian / ngày làm hạt giống. Nếu bạn đã thực sự quen thuộc với chức năng này, bạn sẽ biết chính xác lý do tại sao tôi nói điều này. Như những gì nó thực sự làm là đưa ra một khoảng cách (thời gian trôi qua) kể từ một ngày / thời gian nhất định. Mặc dù việc sử dụng chức năng ngày / giờ làm hạt giống cho rand (), là một cách rất phổ biến, nhưng nó thực sự không phải là lựa chọn tốt nhất. Bạn nên tìm kiếm các lựa chọn thay thế tốt hơn, vì có nhiều giả thuyết về chủ đề này và tôi không thể đi sâu vào tất cả chúng. Bạn thêm vào phương trình này khả năng tràn và cách tiếp cận này đã bị tiêu diệt ngay từ đầu.

Những người đã đăng rand () + 1 đang sử dụng giải pháp được sử dụng nhiều nhất để đảm bảo rằng họ không nhận được số âm. Nhưng, cách tiếp cận đó thực sự không phải là cách tốt nhất.

Điều tốt nhất bạn có thể làm là dành thêm thời gian để viết và sử dụng xử lý ngoại lệ phù hợp và chỉ thêm vào số rand () nếu và / hoặc khi bạn kết thúc với kết quả bằng không. Và, để đối phó với số âm đúng. Chức năng rand () không hoàn hảo và do đó cần được sử dụng cùng với xử lý ngoại lệ để đảm bảo rằng bạn kết thúc với kết quả mong muốn.

Dành thêm thời gian và công sức để điều tra, nghiên cứu và thực hiện đúng chức năng rand () rất đáng để dành thời gian và công sức. Chỉ hai xu của tôi. Chúc may mắn trong nỗ lực của bạn...


2
rand()không chỉ định sử dụng hạt giống nào. Tiêu chuẩn không chỉ định nó sử dụng trình tạo giả ngẫu nhiên, không liên quan đến bất kỳ lúc nào. Nó cũng không nói về chất lượng của máy phát điện. Vấn đề thực tế rõ ràng là tràn. Lưu ý rằng rand()+1được sử dụng để tránh 0; rand()không trả về giá trị âm. Xin lỗi, nhưng bạn đã bỏ lỡ điểm ở đây. Nó không phải là về chất lượng của PRNG. ...
quá trung thực cho trang web này

... Thực hành tốt theo GNU / Linux để lấy từ /dev/randomvà sử dụng PRNG tốt sau đó (không chắc chắn về chất lượng rand()từ glibc) hoặc tiếp tục sử dụng thiết bị - mạo hiểm ứng dụng của bạn để chặn nếu không có đủ entropy. Cố gắng để có được entropy của bạn trong ứng dụng rất có thể là một lỗ hổng vì điều đó có thể dễ dàng tấn công hơn. Và bây giờ nói đến việc làm cứng - không phải ở đây
quá trung thực cho trang web này
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.