How to generate a list of random numbers without d

2019-01-15 20:18发布

问题:

I want to generate a list of random numbers with a predefined number of items %RND_TOTAL% in the given range from %RND_MIN% to %RND_MAX% and with a certain interval %RND_INTER%. Of course this can be accomplished with the following code snippet:

@echo off
setlocal EnableExtensions EnableDelayedExpansion
rem total number of random numbers:
set /A "RND_TOTAL=8"
rem range for random numbers (minimum, maximum, interval):
set /A "RND_MIN=1, RND_MAX=10, RND_INTER=1"
rem loop through number of random numbers:
for /L %%I in (1,1,%RND_TOTAL%) do (
    rem compute a random number:
    set /A "RND_NUM[%%I]=!RANDOM!%%((RND_MAX-RND_MIN)/RND_INTER+1)*RND_INTER+RND_MIN"
    echo !RND_NUM[%%I]!
)
endlocal
exit /B

Here is an example of the corresponding output (4 and 9 both apear twice here):

2
4
9
8
9
4
7
3

But how can I create such a list without any duplicate items?


Of course I could use the following script which checks each item whether it is already avalable in the array-like variable RND_NUM[], but this approach is quite inefficient due to nested for /L loops and, particularly when %RND_TOTAL% comes close to the available count of random numbers covered by the the range specification, due to numerous calculation repetitions:

@echo off
setlocal EnableExtensions EnableDelayedExpansion
rem total number of random numbers, duplicate flag (`0` means no duplicates):
set /A "RND_TOTAL=20, FLAG_DUP=0"
rem range for random numbers (minimum, maximum, interval):
set /A "RND_MIN=1, RND_MAX=30, RND_INTER=1"
rem loop through number of random numbers, generate them in a subroutine:
for /L %%I in (1,1,%RND_TOTAL%) do (
    call :SUB %%I
    echo !RND_NUM[%%I]!
)
endlocal
exit /B

:SUB
rem get number of already collected random numbers:
set /A "RND_COUNT=%1-1"
:LOOP
rem compute a random number:
set /A "RND_NUM[%1]=!RANDOM!%%((RND_MAX-RND_MIN)/RND_INTER+1)*RND_INTER+RND_MIN"
rem check whether random number appears in the previous collection:
if %FLAG_DUP% EQU 0 (
    for /L %%I in (1,1,%RND_COUNT%) do (
        rem re-compute random number if duplicate has been encountered:
        if !RND_NUM[%1]! EQU !RND_NUM[%%I]! (
            goto :LOOP
        )
    )
)
exit /B

This is the related sample output (no duplicates here):

4
1
2
10
6
7
3
5

回答1:

I slightly modified a code I wrote some time ago:

@echo off
setlocal EnableDelayedExpansion

rem total number of random numbers:
set /A "RND_TOTAL=8"
rem range for random numbers (minimum, maximum):
set /A "RND_MIN=1, RND_MAX=10"

set /A "range=RND_MAX-RND_MIN+1"

rem Create an input list with all numbers in given range
set "input="
for /L %%i in (%RND_MIN%,1,%RND_MAX%) do (
   set "input=!input! %%i"
)
set "input=%input% "
echo  IN: [%input%]

rem Extract RND_TOTAL elements from input list in random order
set "output="
for /L %%i in (%RND_TOTAL%,-1,1) do (
   set /A "randIndex=(!random!*range)/32768+1, range-=1"
   call :MoveInputToOutput !randIndex!
)
echo OUT: [%output%]

goto :EOF

:MoveInputToOutput randIndex
for /F "tokens=%1" %%n in ("%input%") do (
   set output=%output% %%n
   set input=!input: %%n = !
)
exit /B

Output example:

> test
 IN: [ 1 2 3 4 5 6 7 8 9 10 ]
OUT: [ 8 7 9 3 1 4 5 2]

> test
 IN: [ 1 2 3 4 5 6 7 8 9 10 ]
OUT: [ 9 2 10 6 4 8 5 1]

> test
 IN: [ 1 2 3 4 5 6 7 8 9 10 ]
OUT: [ 1 2 4 3 8 5 7 9]

Excuse me, but I don't understand what %RND_INTER% value is used for...

EDIT: New version that use the %RND_INTER% value and have not limit in the number of random numbers generated.

@echo off
setlocal EnableDelayedExpansion

rem total number of random numbers:
set /A "RND_TOTAL=8"
rem range for random numbers (minimum, maximum, interval):
set /A "RND_MIN=1, RND_MAX=10, RND_INTER=1"

rem Create an input vector with all numbers in given range
set "n=0"
for /L %%i in (%RND_MIN%,%RND_INTER%,%RND_MAX%) do (
   set /A n+=1
   set "in[!n!]=%%i"
)
echo Input:
set in[
echo/

rem Extract RND_TOTAL elements from input vector in random order
for /L %%i in (1,1,%RND_TOTAL%) do (
   set /A "randIndex=(!random!*n)/32768+1"
   set /A "out[%%i]=in[!randIndex!], in[!randIndex!]=in[!n!], n-=1"
)
echo Output:
set out[


回答2:

Aacini's original solution is efficient, however, it can only support a maximum of 31 possible values because FOR /F cannot read more than 31 tokens. EDIT - His 2nd solution eliminated the limitation.

Below is a similar concept that uses a constant width for the numbers in the list. This enables me to easily use substring operations to both extract the randomly selected values, and to remove each value from the list.

As written, this solution supports values from 0 to 9999, with a maximum number of possible values <= 1354.

I used a very similar strategy for managing random food placement in my SNAKE.BAT game. I had to keep track of all empty locations within the playing field, and randomly select a new food location from that list.

@echo off
setlocal enableDelayedExpansion

:: This script has the following limitations:
::   RND_MIN >= 0
::   RND_MAX <= 9999
::   ((RND_MAX - RND_MIN + 1) / RND_INTER) <= 1354
::
set "RND_MIN=1"
set "RND_MAX=10"
set "RND_INTER=1"
set "RND_TOTAL=8"

set /a cnt=(RND_MAX - RND_MIN + 1) / RND_INTER

:: Define a string containing a space delimited list of all possible values,
:: with each value having 10000 added
set "pool="
set /a "beg=RND_MIN+10000, end=RND_MAX+10000, cnt=(RND_MAX-RND_MIN+1)/RND_INTER"
for /l %%N in (%beg% %RND_INTER% %end%) do set "pool=!pool!%%N "

:: Build the randomly sequenced array of numbers
for /l %%N in (1 1 %RND_TOTAL%) do (

  %= Randomly select a value from the pool of all possible values  =%
  %= and compute the index within the string, as well as the index =%
  %= of the next value                                             =%
  set /a "loc=(!random!%%cnt)*6, next=loc+6"

  %= Transfer the index values to FOR variables =%
  for %%A in (!loc!) do for %%B in (!next!) do (

    %= Assign the selected value to the output array =%
    set /a "RND_NUM[%%N]=!pool:~%%A,5!-10000"

    %= Remove the value from the pool =%
    set "pool=!pool:~0,%%A!!pool:~%%B!"
    set /a cnt-=1

  )
)

:: Display the results
for /l %%N in (1 1 %RND_TOTAL%) do echo !RND_NUM[%%N]!


回答3:

replace

for /L %%I in (1,1,%RND_TOTAL%) do (
    rem compute a random number:
    set /A "RND_NUM[%%I]=!RANDOM!%%((RND_MAX-RND_MIN)/RND_INTER+1)*RND_INTER+RND_MIN"
    echo !RND_NUM[%%I]!
)

with

for /L %%I in (%rnd_min%,1,%RND_max%) do set "rnd_num{%%I}="
set /a rnd_count=rnd_total
:rnd_loop
rem compute a random number:
set /A "RND_selection=%RANDOM%%%((RND_MAX-RND_MIN)/RND_INTER+1)*RND_INTER+RND_MIN"
if not defined rnd_num{%rnd_selection%} (
 SET "rnd_num{%rnd_selection%}=Y"
 set /a rnd_num[%count%]=rnd_selection
 echo %rnd_selection%
 set /a rnd_count-=1
)
if %rnd_count% neq 0 goto rnd_loop

Each time a selection is made, rnd_num{selectionmade} is set to Y so if it is selected again, it is defined and the recording/output/count-1-less is skipped.



回答4:

Although you guys posted really great solutions, I don't have the heart to just give up and so I had to try it on my own to find a better code I posted within the question; here we go...

The basic idea is to generate a table consisting of two columns where the first one contains random numbers with duplicates and the second one contains indexes that constitute all the possible random numbers. The next step is to sort this table by the first column. Finally you need to pick values from the second column from as many rows as you need random numbers (in my approach I use the last rows). All the retrieved numbers are unique as the second column does not contain any duplicates. Here is an example, provided that we need 8 random numbers out of 1,2,3,4,5,6,7,8,9,10:

generated   sorted      returned
table       table       numbers
--------------------------------
1,1         1,1         -
9,2         1,6         -
8,3         10,8        8
3,4         3,4         4
9,5         4,7         7
1,6         5,9         9
4,7         8,10        10
10,8        8,3         3
5,9         9,2         2
8,10        9,5         5

As you can see there are no duplicate numbers returned. The great advantage of this method is that the computation of random numbers does not have to be repeated in case the same one has already been used. The disadvantage is that the full table needs to be created even in case only a few unique random numbers are needed out of a huge pool of possible numbers.

I have to admit that this technique is not my idea, but unfortunately I don't know to whom it is credited...


Here is my code, containing some explanatory remarks:

@echo off
setlocal EnableExtensions EnableDelayedExpansion
rem total number of random numbers, duplicate flag (`0` means no duplicates):
set /A "RND_TOTAL=8, FLAG_DUP=0"
rem range for random numbers (minimum, maximum, interval):
set /A "RND_MIN=1, RND_MAX=10, RND_INTER=1"
rem write table-like data to temporary file:
> "%~n0.tmp" (
    rem loop through all possible random numbers:
    for /L %%I in (%RND_MIN%,%RND_INTER%,%RND_MAX%) do (
        rem compute a random number:
        set /A "RND_NUM=!RANDOM!%%((RND_MAX-RND_MIN)/RND_INTER+1)*RND_INTER+RND_MIN"
        if %FLAG_DUP% EQU 0 (
            rem duplicates denied, so build row with random number as first column:
            echo !RND_NUM!,%%I
        ) else (
            rem duplicates allowed, so build row with loop counter as first column:
            echo %%I,!RND_NUM!
        )
    )
)
rem determine how many items of the table need to be skipped to get total number:
set /A "SKIP=(RND_MAX-RND_MIN)/RND_INTER+1, SKIP-=RND_TOTAL"
if %SKIP% GTR 0 (
    set "SKIP=skip=%SKIP% "
) else (
    set "SKIP="
)
rem read table-like data from temporary file:
set /A "RND_COUNT=0"
< "%~n0.tmp" (
    rem sort rows (lines) of table-like data alphabetically, enumerate them:
    for /F "%SKIP%tokens=2 delims=," %%N in ('sort') do (
        set /A "RND_COUNT+=1"
        rem store and return random number:
        set /A "RND_NUM[!RND_COUNT!]=%%N"
        echo %%N
    )
)
rem clean up temporary file:
del /Q "%~n0.tmp"
endlocal
exit /B

In this script, the above described table-like data is stored into a temporary file, which is then passed over to the sort command which does the alphabetic sorting.

The resulting random sequence is output to the console and stored into the array-like variables RND_NUM[1:8] (which is only available until the endlocal command is executed, of course).


Update:

Based on the fact that set is capable of sorting as well, and that the above described table can also be reflected in names of environment variables in a 2D-array-like style (I used RND_NUM[_,_] here), the following script does no longer use a temporary file. the random sequence is again output to the console and also stored into the 1D-array RND_NUM[1:8]).

But before we come to the script, I want to show you the 2D-array that represents the said table, with the same sample data as above (note that the values of the variables are totally irrelevant):

generated           sorted              returned
2D-array            2D-array            numbers
------------------------------------------------
RND_NUM[1,1]        RND_NUM[1,1]        -
RND_NUM[9,2]        RND_NUM[1,6]        -
RND_NUM[8,3]        RND_NUM[10,8]       8
RND_NUM[3,4]        RND_NUM[3,4]        4
RND_NUM[9,5]        RND_NUM[4,7]        7
RND_NUM[1,6]        RND_NUM[5,9]        9
RND_NUM[4,7]        RND_NUM[8,10]       10
RND_NUM[10,8]       RND_NUM[8,3]        3
RND_NUM[5,9]        RND_NUM[9,2]        2
RND_NUM[8,10]       RND_NUM[9,5]        5

Here is the code (including some explanatory remarks):

@echo off
setlocal EnableExtensions EnableDelayedExpansion
rem total number of random numbers, duplicate flag (`0` means no duplicates):
set /A "RND_TOTAL=8, FLAG_DUP=0"
rem range for random numbers (minimum, maximum, interval):
set /A "RND_MIN=1, RND_MAX=10, RND_INTER=1"
rem clean up all variables named `RND_NUM*` (optionally):
call :CLEANUP1
rem loop through all possible random numbers:
for /L %%I in (%RND_MIN%,%RND_INTER%,%RND_MAX%) do (
    rem compute a random number:
    set /A "RND_INDEX=!RANDOM!%%((RND_MAX-RND_MIN)/RND_INTER+1)*RND_INTER+RND_MIN"
    if %FLAG_DUP% EQU 0 (
        rem duplicates denied, so build 2D-array with random number as first index:
        set "RND_NUM[!RND_INDEX!,%%I]=#"
    ) else (
        rem duplicates allowed, so build 2D-array with loop counter as first index:
        set "RND_NUM[%%I,!RND_INDEX!]=#"
    )
)
rem determine how many items of the 2D-array need to be skipped to get total number:
set /A "SKIP=(RND_MAX-RND_MIN)/RND_INTER+1-RND_TOTAL"
if %SKIP% GTR 0 (
    set "SKIP=skip=%SKIP% "
) else (
    set "SKIP="
)
rem let `set` output all `RND_NUM*` variables sorted alphabetically, enumerate them:
set /A "RND_COUNT=0"
for /F "%SKIP%tokens=3 delims=[,]" %%I in ('set "RND_NUM"') do (
    set /A "RND_COUNT+=1"
    rem store and return random number:
    set "RND_NUM[!RND_COUNT!]=%%I"
    echo %%I
)
rem clean up all 2D-array variables named `RND_NUM*,*` (optionally):
call :CLEANUP2
endlocal
exit /B

:CLEANUP1
for /F "tokens=1 delims==" %%I in ('2^> nul set "RND_NUM"') do (
    set "%%I="
)
exit /B

:CLEANUP2
for /F "tokens=1,2,3 delims=,=" %%I in ('set "RND_NUM"') do (
    if not "%%K"=="" (
        set "%%I,%%J="
    )
)
exit /B

The clean-up routines :CLEANUP1 and :CLEANUP2 are considered as optional and just delete any variables whose names start with RND_NUM (see :CLEANUP1) at the beginning and whose names contain a , (which is true for the 2D-array but not for the 1D-array; see :CLEANUP2) at the end.