【问题标题】:How can I define a custom alphabet order for comparing and sorting strings in go?如何定义自定义字母顺序以在 go 中比较和排序字符串?
【发布时间】:2019-05-22 23:21:41
【问题描述】:

请在标记为重复之前阅读到底部

我希望能够按字母顺序对一组字符串(或基于一个字符串值的结构切片)进行排序,但 基于自定义字母或 unicode 字母

大多数时候,人们建议使用支持不同预定义语言环境/字母的整理器。 (请参阅 this answer for Java),但是对于这些语言环境包中不可用的稀有语言/字母表可以做什么?

我想使用的语言不可用 in the list of languages 被 Golangs 的 collate 支持和使用,所以 我需要能够定义自定义字母表或顺序用于排序的 Unicode 字符/符文。

其他人建议先将字符串翻译成英文/ASCII 可排序字母表,然后再对其进行排序。这就是this solution done in Javascriptthis solution in Ruby 中类似问题的建议。 但肯定有一种更有效的方法可以用 Go 做到这一点。

是否可以在使用自定义字母/字符集的 Go 中创建 Collator?这就是func NewFromTable 的用途吗?

看来我应该可以使用Reorder 函数,但它看起来还没有在语言中实现?source code 显示了这一点:

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

如何在 go 中定义自定义字母顺序以比较和排序字符串

【问题讨论】:

  • 你检查过排序包吗?
  • 是的,我已经检查了排序包,但是我看不到使用自定义字母表对字符串进行排序的选项。
  • 使用 sort.StringSlice 结构并检查排序是否适用于您的字母表
  • "如何定义自定义字母顺序以在 go 中比较和排序字符串?"为什么不实施 Reorder 并帮助 Marcel 完成 golang.org/x/text/collat​​e ?

标签: string algorithm sorting go


【解决方案1】:

事先说明:

以下解决方案已被清理和优化,并在此处作为可重用库发布: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() 函数中调用帮助,该函数解码 string 的第一个 rune

所以我们逐个符文进行比较。如果两个符文都是自定义字母表中的字母,我们可以使用它们的权重来判断它们之间的比较。如果至少有一个符文不是来自我们的自定义字母表,我们将回退到简单的数字符文比较。

如果 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 序列,仍将使用实际字节。

我们的解决方案在这方面有所不同,因为我们解码符文(我们必须这样做,因为我们使用自定义字母表,在其中我们允许符文不一定映射到 UTF-8 编码中的字节 1 到 1)。这意味着如果输入不是有效的 UTF-8 序列,则行为可能与默认排序不一致。但是,如果您的输入是有效的 UTF-8 序列,这将符合您的预期。

最后一点:

我们已经看到了如何对字符串切片进行自定义排序。如果我们有一个结构体切片(或结构体指针切片),排序算法(less()函数)可能是一样的,但是在比较切片的元素时,我们必须比较元素的字段,而不是结构元素本身。

假设我们有以下结构:

type Person struct {
    Name string
    Age  int
}

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

(添加了String() 方法,因此我们将看到结构的实际内容,而不仅仅是它们的地址...)

假设我们想对[]*Person 类型的切片应用自定义排序,使用Person 元素的Name 字段。所以我们简单地定义这个自定义类型:

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}]

【讨论】:

  • 这太棒了,非常感谢。 Go 和社区确实是一个切入点。
  • 您是否介意简单地添加一下如何使用 带有字符串字段的结构数组/切片来完成此操作?使用类似于 this answer 的 slice.Sort() ?非常感谢您的帮助,我相信这对其他人来说会是一个有用的问题。
  • 非常感谢!这是完美的,如果这一切都可以包装在 sort 包中的一个简洁函数中那就太好了(人们所要做的就是定义一个自定义字母表和排序,但也许这有点太极端了。:- ))
  • @AdamD 这个自定义排序可以包装在一个实用程序库中,我刚刚在github.com/icza/abcsort 上发布了它。请参阅编辑后的答案以获取简单的用法示例。
【解决方案2】:

使用table_test.go [1] 作为起点,我想出了以下内容。这 Builder.Add [2] 正在完成真正的工作:

package main

import (
   "golang.org/x/text/collate"
   "golang.org/x/text/collate/build"
)

type entry struct {
   r rune
   w int
}

func newCollator(ents []entry) (*collate.Collator, error) {
   b := build.NewBuilder()
   for _, ent := range ents {
      err := b.Add([]rune{ent.r}, [][]int{{ent.w}}, nil)
      if err != nil { return nil, err }
   }
   t, err := b.Build()
   if err != nil { return nil, err }
   return collate.NewFromTable(t), nil
}

结果:

package main
import "fmt"

func main() {
   a := []entry{
      {'a', 3}, {'b', 2}, {'c', 1},
   }
   c, err := newCollator(a)
   if err != nil {
      panic(err)
   }
   x := []string{"alfa", "bravo", "charlie"}
   c.SortStrings(x)
   fmt.Println(x) // [charlie bravo alfa]
}
  1. https://github.com/golang/text/blob/3115f89c/collate/table_test.go
  2. https://pkg.go.dev/golang.org/x/text/collate/build#Builder.Add

【讨论】:

    猜你喜欢
    • 2018-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-29
    • 1970-01-01
    相关资源
    最近更新 更多