Как я могу определить собственный порядок алфавита для сравнения и сортировки строк в Go? - PullRequest
0 голосов
/ 23 декабря 2018

Пожалуйста, прочитайте до того, как пометить это как дубликат

Я хотел бы иметь возможность сортировать массив строк (или часть структур на основе одного строкового значения) в алфавитном порядке, но на основе пользовательского алфавита или юникодных букв .

В большинстве случаев люди советуют использовать средство сортировки, которое поддерживает различные предопределенные локали / алфавиты.(См. этот ответ для Java ), но что можно сделать для редких языков / алфавитов, которые не доступны в этих наборах локалей?

Язык, который я хотел бы использовать , этонедоступно в списке языков , поддерживаемых и используемых collate Голанга, поэтому Мне нужно иметь возможность определить собственный алфавит или порядок символов Unicode /руны для сортировки.

Другие предлагают сначала перевести строки в сортируемый алфавит английский / ASCII, а затем отсортировать.Вот что было предложено подобным вопросом в , это решение сделано в Javascript или , это решение в Ruby . Но, безусловно, должен быть более эффективный способ сделать это с помощью Go.

Можно ли создать Collator в Go, который использует собственный алфавит / набор символов?Это то, для чего func NewFromTable ?

Кажется, я должен быть в состоянии использовать функцию Reorder , но похоже, что еще не реализованна языке? Исходный код показывает это:

func Reorder(s ...string) Option {
    // TODO: need fractional weights to implement this.
    panic("TODO: implement")
}

Как мне определить собственный порядок алфавита для сравнения и сортировки строк в go?

1 Ответ

0 голосов
/ 23 декабря 2018

Заранее обратите внимание:

Следующее решение было очищено и оптимизировано и опубликовано здесь как библиотека многократного использования: github.com/icza/abcsort.

Используя abcsort, пользовательская сортировка фрагмента строки (используя пользовательский алфавит) так же проста:

sorter := abcsort.New("bac")

ss := []string{"abc", "bac", "cba", "CCC"}
sorter.Strings(ss)
fmt.Println(ss)

// Output: [CCC bac abc cba]

Выборочная сортировка фрагмента структуры одним из полей структуры выглядит следующим образом:

type Person struct {
    Name string
    Age  int
}

ps := []Person{{Name: "alice", Age: 21}, {Name: "bob", Age: 12}}
sorter.Slice(ps, func(i int) string { return ps[i].Name })
fmt.Println(ps)

// Output: [{bob 12} {alice 21}]

Исходный ответ следующий:


Мы можем реализовать пользовательскую сортировку, которая использует пользовательский алфавит.Нам просто нужно создать соответствующую функцию less(i, j int) bool, а пакет sort сделает все остальное.

Вопрос в том, как создать такую ​​функцию less()?

Начнем с определения пользовательского алфавита.Удобным способом является создание string, содержащего буквы пользовательского алфавита, нумерованные (упорядоченные) от наименьшего к наибольшему.Например:

const alphabet = "bca"

Давайте создадим карту из этого алфавита, которая сообщит вес или порядок каждой буквы нашего пользовательского алфавита:

var weights = map[rune]int{}

func init() {
    for i, r := range alphabet {
        weights[r] = i
    }
}

(Примечание:i в приведенном выше цикле - это индекс байтов, а не индекс rune, но, поскольку оба монотонно растут, оба прекрасно подойдут для веса рун.)

Теперь мы можем создать нашless() функция.Чтобы получить «приемлемую» производительность, мы должны избегать преобразования входных значений string в байтовые или рунические фрагменты.Для этого мы можем вызвать функцию помощи из функции utf8.DecodeRuneInString(), которая декодирует первые rune из string.

Таким образом, мы выполняем сравнение рун за руной.Если обе руны являются буквами пользовательского алфавита, мы можем использовать их веса, чтобы сказать, как они сравниваются друг с другом.Если хотя бы одна из рун не из нашего пользовательского алфавита, мы вернемся к простому числовому сравнению рун.

Если 2 руны в начале 2 входных строк равны, мы переходим к следующим рунам вкаждая входная строка.Мы можем сделать это, разрезая входные строки: разрезание их не создает копию, оно просто возвращает новый заголовок строки, который указывает на данные исходных строк.

Хорошо, теперь давайте посмотрим реализациюэта less() функция:

func less(s1, s2 string) bool {
    for {
        switch e1, e2 := len(s1) == 0, len(s2) == 0; {
        case e1 && e2:
            return false // Both empty, they are equal (not less)
        case !e1 && e2:
            return false // s1 not empty but s2 is: s1 is greater (not less)
        case e1 && !e2:
            return true // s1 empty but s2 is not: s1 is less
        }

        r1, size1 := utf8.DecodeRuneInString(s1)
        r2, size2 := utf8.DecodeRuneInString(s2)

        // Check if both are custom, in which case we use custom order:
        custom := false
        if w1, ok1 := weights[r1]; ok1 {
            if w2, ok2 := weights[r2]; ok2 {
                custom = true
                if w1 != w2 {
                    return w1 < w2
                }
            }
        }
        if !custom {
            // Fallback to numeric rune comparison:
            if r1 != r2 {
                return r1 < r2
            }
        }

        s1, s2 = s1[size1:], s2[size2:]
    }
}

Давайте посмотрим некоторые тривиальные тесты этой less() функции:

pairs := [][2]string{
    {"b", "c"},
    {"c", "a"},
    {"b", "a"},
    {"a", "b"},
    {"bca", "bac"},
}
for _, pair := range pairs {
    fmt.Printf("\"%s\" < \"%s\" ? %t\n", pair[0], pair[1], less(pair[0], pair[1]))
}

Вывод (попробуйте на Go Playground ):

"b" < "c" ? true
"c" < "a" ? true
"b" < "a" ? true
"a" < "b" ? false
"bca" < "bac" ? true

А теперь давайте проверим эту less() функцию на фактической сортировке:

ss := []string{
    "abc",
    "abca",
    "abcb",
    "abcc",
    "bca",
    "cba",
    "bac",
}
sort.Slice(ss, func(i int, j int) bool {
    return less(ss[i], ss[j])
})
fmt.Println(ss)

Вывод (попробуйте на Go Playground ):

[bca bac cba abc abcb abcc abca]

Опять же, если производительность важна для вас, вы не должны использовать sort.Slice(), поскольку для этого нужно использовать отражение под капотом, а вместо этого создавать свой собственный тип среза, который реализуетsort.Interface, и в своей реализации вы можете рассказать, как это сделать без использования отражения.

Вот как это может выглядеть:

type CustStrSlice []string

func (c CustStrSlice) Len() int           { return len(c) }
func (c CustStrSlice) Less(i, j int) bool { return less(c[i], c[j]) }
func (c CustStrSlice) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }

Когда выхотите отсортировать фрагмент строки, используя собственный алфавит, просто конвертируйте фрагмент в CustStrSlice,поэтому его можно передать непосредственно в sort.Sort() (это преобразование типов не делает копию среза или его элементов, оно просто изменяет информацию о типе):

ss := []string{
    "abc",
    "abca",
    "abcb",
    "abcc",
    "bca",
    "cba",
    "bac",
}
sort.Sort(CustStrSlice(ss))
fmt.Println(ss)

Выводснова и снова (попробуйте на Go Playground ):

[bca bac cba abc abcb abcc abca]

Некоторые замечания:

Сравнение строк по умолчаниюсравнивает строки побайтноТо есть, если входные строки содержат недопустимые последовательности UTF-8, фактические байты все равно будут использоваться.

Наше решение отличается в этом отношении, поскольку мы декодируем руны (мы должны это сделать, потому что мы используем собственный алфавитв котором мы разрешаем руны, которые не обязательно отображаются на байты 1-в-1 в кодировке UTF-8).Это означает, что если ввод не является допустимой последовательностью UTF-8, поведение может не соответствовать порядку по умолчанию.Но если ваши входные данные являются действительными последовательностями UTF-8, это будет делать то, что вы ожидаете.

Последнее замечание:

Мы видели, как можно отсортировать фрагмент строки.Если у нас есть фрагмент структур (или фрагмент указателей структур), алгоритм сортировки (функция less()) может быть таким же, но при сравнении элементов фрагмента мы должны сравнивать поля элементов, а несами элементы структуры.

Итак, допустим, у нас есть следующая структура:

type Person struct {
    Name string
    Age  int
}

func (p *Person) String() string { return fmt.Sprint(*p) }

(добавлен метод String(), поэтому мы увидим фактическое содержимоеструктуры, а не только их адреса ...)

И скажем, мы хотим применить нашу пользовательскую сортировку к срезу типа []*Person, используя поле Name элементов Person,Поэтому мы просто определяем этот пользовательский тип:

type PersonSlice []*Person

func (p PersonSlice) Len() int           { return len(p) }
func (p PersonSlice) Less(i, j int) bool { return less(p[i].Name, p[j].Name) }
func (p PersonSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

И это все.Остальное тоже самое, например:

ps := []*Person{
    {Name: "abc"},
    {Name: "abca"},
    {Name: "abcb"},
    {Name: "abcc"},
    {Name: "bca"},
    {Name: "cba"},
    {Name: "bac"},
}
sort.Sort(PersonSlice(ps))
fmt.Println(ps)

Вывод (попробуйте на Go Playground ):

[{bca 0} {bac 0} {cba 0} {abc 0} {abcb 0} {abcc 0} {abca 0}]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...