Наскоро бях в ситуация, в която исках да тествам метод, като му подавам произволни плаващи елементи и проверявам дали остава без граници. Мислех, че това ще бъде лесен въпрос, но имаше много клопки, които изследваме по време на тази публикация.

Нека започнем със спецификация: искам функция, която може да генерира произволни плаващи стойности без специални свойства. т.е. трябва да използва целия диапазон, без специални стойности и както големи положителни (напр. 1e38), малки положителни (напр. 1e-20), малки отрицателни (напр. -1e-20), така и големи отрицателни (напр. -1e38) .

Във всички примери приемаме, че имаме достъп до тази променлива:

private Random rand = new Random();

Без обхват

В Java получаването на случаен float е лесно:

return rand.nextFloat();

Този метод обаче не отговаря на нашата спецификация, тъй като генерира само числа в диапазона [0;1].

Положителен диапазон

Трябва да разширим обхвата. Нека започнем с разширяване на диапазона до [0;N] за всяко N ≤ Float.MAX_VALUE. Математиката е ясна:

И се превежда направо в код:

return rand.nextFloat() * N;

Малък диапазон

Следващото нещо, което правим, е да разширим нашата функция, така че да можем да променяме и по-ниската стойност. Вече имаме [0;N], можем да се възползваме от това, ако започнем от желания [min;max], след което извадим min от двата компонента на диапазона по следния начин:

В резултат на този метод:

return rand.nextFloat() * (max - min) + min;

Което работи чудесно за малки диапазони, но не забравяйте, че първоначалната ни функция не работи за всички N. Нашият проблем в този случай е, че ако minе отрицателно, тогава max — min може да стане по-голямо от Float.MAX_VALUE, което води до ∞.

Голям диапазон

За да заобиколим този проблем с препълването, можем да използваме трик: можем да разделим диапазона по средата и след това да използваме произволно булево значение, за да определим коя от двете половини да използваме. Но все пак трябва да внимаваме с диапазона, тъй като не можем да добавяме числата (поради същото препълване). Решението е да поставите разделението на всеки компонент:

Ключовото прозрение е, че без значение какви числа добавяме, ако и двете са били разделени на две, те никога не могат да препълнят. Кодът следва от формулата:

float midpoint = max/2 + min/2;
float half_range = max/2 - min/2;
int plus_minus = rand.nextBoolean() ? 1 : -1;
return midpoint + plus_minus * rand.nextFloat() * half_range;

С това можем да генерираме плаващи числа в целия диапазон от -Float.MAX_VALUE (защо не Float.MIN_VALUE?) до Float.MAX_VALUE без да получаваме грешки. Въпреки това, ако погледнем изхода, виждаме само числа в диапазона от „E36“ до „E38“. Защо е това? Нашата функция не е ли равномерно разпределена? Как може да стане това, когато разчитаме само на равномерно разпределени произволни генератори?

Първоначално проблемът е доста фин. Ние наистина генерираме равномерно разпределени числа в пълния диапазон, както може да се види от начертаването им. Всяко число има еднаква вероятност да бъде избрано, но има 1000 пъти повече числа от E37 до E38, отколкото от E34 до E35. Така че избирайки на случаен принцип, е много по-вероятно да избираме числа в по-високите E числа.

Всъщност ние не искаме те да бъдат еднакви. Искаме както голямо и малко (близко до нула) число.

Униформа в експонента

В този момент стигаме до ново прозрение. Искаме числата да са равномерно разпределени в техния битов модел. Така че една стратегия, която можем да предприемем, е да генерираме произволен модел на битове (int) и след това да го преобразуваме в число с плаваща единица. Правенето на това по очевидния начин ((float)rand.nextInt()) не работи, защото Java се опитва да запази стойността, а не битовия модел. Така че трябва да използваме:

return Float.intBitsToFloat(rand.nextInt());

Начертавайки резултатите от това, изглежда, че генерираме само числа точно около нулата, но това е, защото сега трябва да го начертаем в логаритмична скала. Правейки това, виждаме, че това е права линия, съответстваща на генериране на число със същата вероятност във всеки диапазон [Ex;E(x+1)].

Има обаче един последен проблем, той също генерира моделите на битове за специалните стойности Float.NaN, Float.NEGATIVE_INFINITY и Float.POSITIVE_INFINITY.

Няма специални стойности

Вместо да изучаваме битовите модели, които искаме да премахнем, и да измисляме някаква сложна формула за това, вместо това използваме проста техника от двигатели за рендиране.

В двигателите за изобразяване понякога искаме да направим равномерно хвърляне на лъчи през кръгла бленда, вместо да измислим формулата за това как да бъдем еднакви в кръг, ние генерираме лъч през квадрат и след това, ако лъчът е извън кръга, ние просто превъртете го отново.

Превъртането на нежелани стойности е стандартен начин за картографиране на домейн на произволен алгоритми в друг домейн. Той е тривиален за изпълнение и запазва разпределението:

float result;
do {
  result = Float.intBitsToFloat(rand.nextInt());
} while (result != Float.NaN
      && result != Float.POSITIVE_INFINITY
      && result != Float.NEGATIVE_INFINITY);
return result;

Единственият недостатък на този подход е, че не ни е гарантирано, че този алгоритъм ще приключи. Всъщност той се нарича полуалгоритъм точно поради тази причина. На практика обаче той се прекратява и е бърз, и най-важното бърз за изпълнение.

Като се има предвид това, ако намерите действителен алгоритъм за това, който е достатъчно прост, за да се побере в туит, моля, туитнете ми го @themaxipaxi, ще се радвам да го видя. И ако ви харесва да работите върху проблем, да го прецизирате стъпка по стъпка, както направихме току-що, трябва да разгледате моята книга за рефакторинг: