Криптографическая перетасовка Голанга

Я пытаюсь реализовать функцию перетасовки строк в Go, которая использует крипто/ранд вместо математики/ранда. Для Fisher-Yates Shuffle требуются случайные целые числа, поэтому я попытался реализовать эту функциональность без необходимости использовать crypto/rand Int, который опирается на math/big. Ниже приведено лучшее, что я придумал до сих пор, но есть ли лучший метод? Тот факт, что я не могу найти существующие примеры, заставляет меня задаться вопросом, есть ли веская причина, по которой никто этого не делает!

package main

import "crypto/rand"
import "fmt"
import "encoding/binary"

func randomInt(max int) int {
    var n uint16
    binary.Read(rand.Reader, binary.LittleEndian, &n)
    return int(n) % max
}

func shuffle(s *[]string) {
        slice := *s
        for i := range slice {
                j := randomInt(i + 1)
                slice[i], slice[j] = slice[j], slice[i]
        }
        *s = slice
    }

func main() {
        slice := []string{"a", "b", "c", "d", "e", "f", "h", "i", "j", "k"}
        shuffle(&slice)
        fmt.Println(slice)
}

person Steve Crook    schedule 04.02.2016    source источник


Ответы (3)


Библиотека Go math/rand имеет хорошие средства для создания случайных числовых примитивов из Source.

// A Source represents a source of uniformly-distributed 
// pseudo-random int64 values in the range [0, 1<<63).

type Source interface {
    Int63() int64
    Seed(seed int64)
}

NewSource(seed int64) возвращает встроенный детерминированный PRNG, но New(source Source) разрешает все, что удовлетворяет интерфейсу Source.

Вот пример Source, поддерживаемого crypto/rand.

type CryptoRandSource struct{}

func NewCryptoRandSource() CryptoRandSource {
    return CryptoRandSource{}
}

func (_ CryptoRandSource) Int63() int64 {
    var b [8]byte
    rand.Read(b[:])
    // mask off sign bit to ensure positive number
    return int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1))
}

func (_ CryptoRandSource) Seed(_ int64) {}

Вы можете использовать его следующим образом:

r := rand.New(NewCryptoRandSource())

for i := 0; i < 10; i++ {
    fmt.Println(r.Int())
}

Библиотека math/rand имеет правильно реализованный метод Intn(), который обеспечивает равномерное распределение.

func (r *Rand) Intn(n int) int {
    if n <= 0 {
        panic("invalid argument to Intn")
    }
    if n <= 1<<31-1 {
        return int(r.Int31n(int32(n)))
    }
    return int(r.Int63n(int64(n)))
}

func (r *Rand) Int31n(n int32) int32 {
    if n <= 0 {
        panic("invalid argument to Int31n")
    }
    if n&(n-1) == 0 { // n is power of two, can mask
        return r.Int31() & (n - 1)
    }
    max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
    v := r.Int31()
    for v > max {
        v = r.Int31()
    }
    return v % n
}

func (r *Rand) Int63n(n int64) int64 {
    if n <= 0 {
        panic("invalid argument to Int63n")
    }
    if n&(n-1) == 0 { // n is power of two, can mask
        return r.Int63() & (n - 1)
    }
    max := int64((1 << 63) - 1 - (1<<63)%uint64(n))
    v := r.Int63()
    for v > max {
        v = r.Int63()
    }
    return v % n
}

Криптографические хэш-функции также могут быть обернуты как Source для альтернативных средств случайности.

person Jason Mooberry    schedule 04.02.2016
comment
Я думаю, вы хотите, чтобы int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1)) у вас было положительное число - person JimB; 04.02.2016
comment
Хороший улов @JimB. Обновил пример. Спасибо. - person Jason Mooberry; 04.02.2016
comment
Это хорошо, спасибо. Стоит добавить, что stackoverflow.com/questions/10408646/ объясняет, как импортировать crypt/rand и math/rand. - person Steve Crook; 05.02.2016

Числа из n % max распределены неравномерно. Например,

package main

import (
    "fmt"
    "math"
)

func main() {
    max := 7
    size := math.MaxUint8
    count := make([]int, size)
    for i := 0; i < size; i++ {
        count[i%max]++
    }
    fmt.Println(count[:max])
}

Вывод:

[37 37 37 36 36 36 36]
person peterSO    schedule 04.02.2016

Основываясь на полученных комментариях, я думаю, что могу улучшить пример в моем вопросе, добавив функцию uniformInt, заполнив uint32 вместо uint16 и удалив указатель на срез.

package main

import "crypto/rand"
import "fmt"
import "encoding/binary"

func randomInt() int {
        var n uint32
        binary.Read(rand.Reader, binary.LittleEndian, &n)
        return int(n)
}

func uniformInt(max int) (r int) {
        divisor := 4294967295 / max // Max Uint32
        for {
                r = randomInt() / divisor
                if r <= max {
                        break
                }
        }
        return
}

func shuffle(slice []string) {
        for i := range slice {
                j := uniformInt(i + 1)
                slice[i], slice[j] = slice[j], slice[i]
        }
}

func main() {
        slice := []string{"a", "b", "c", "d", "e", "f", "h", "i", "j", "k"}
        shuffle(slice)
        fmt.Println(slice)
}
person Steve Crook    schedule 04.02.2016
comment
Или вы можете просто использовать алгоритм из Rand.Intn, Rand.Int31n и Rand.Int63n (или использовать math/rand с источником crypto/rand). Ваш uniformInt может возвращать отрицательное значение в системах с 32-битным целым числом, и я вполне уверен, что ваше целочисленное деление добавляет некоторое смещение к выводу, но мне не нужно беспокоиться о правильном выполнении математики, если я возьму заведомо хороший алгоритм . - person JimB; 04.02.2016