如何使用多个排序参数对结构进行排序?

问题描述 投票:0回答:13

我有一个成员数组/切片:

type Member struct {
    Id int
    LastName string
    FirstName string
}

var members []Member

我的问题是如何按

LastName
然后按
FirstName
对它们进行排序。

sorting go
13个回答
93
投票

使用 sort.Slice (自 Go 1.8 起可用)或 sort.Sort 函数对值切片进行排序。

对于这两个函数,应用程序提供了一个函数来测试一个切片元素是否小于另一个切片元素。要按姓氏和名字排序,请比较姓氏和名字:

// If last names are different, then use last
// name to determine whether element i is less than
// element j.
if members[i].LastName != members[j].LastName {
    return members[i].LastName < members[j].LastName
}
// Otherwise, use first name to determine whether
// element i is less than element j. 
return members[i].FirstName < members[j].FirstName

less 函数是使用带有 sort.Slice 的匿名函数指定的:

var members []Member
sort.Slice(members, func(i, j int) bool {
    if members[i].LastName != members[j].LastName {
        return members[i].LastName < members[j].LastName
    }
    return members[i].FirstName < members[j].FirstName
})

less 函数是通过 sort.Sort 函数的接口指定的:

type byLastFirst []Member

func (members byLastFirst) Len() int           { return len(members) }
func (members byLastFirst) Swap(i, j int)      { members[i], members[j] = members[j], members[i] }
func (members byLastFirst) Less(i, j int) bool { 
    if members[i].LastName != members[j].LastName {
        return members[i].LastName < members[j].LastName
    }
    return members[i].FirstName < members[j].FirstName    }

sort.Sort(byLastFirst(members))

除非性能分析表明排序是热点,否则请使用最适合您的应用程序的功能。


32
投票

使用较新的

sort.Slice
函数,如下所示:

sort.Slice(members, func(i, j int) bool {
    switch strings.Compare(members[i].FirstName, members[j].FirstName) {
    case -1:
        return true
    case 1:
        return false
    }
    return members[i].LastName > members[j].LastName
})

或者类似的东西。


22
投票

另一种模式,我觉得稍微干净一些:

if members[i].LastName != members[j].LastName {
    return members[i].LastName < members[j].LastName
}

return members[i].FirstName < members[j].FirstName

8
投票

我为此编写的最短且仍然易于理解的代码是:

package main

import (
    "fmt"
    "sort"
)

type Member struct {
    Id        int
    LastName  string
    FirstName string
}

func sortByLastNameAndFirstName(members []Member) {
    sort.SliceStable(members, func(i, j int) bool {
        mi, mj := members[i], members[j]
        switch {
        case mi.LastName != mj.LastName:
            return mi.LastName < mj.LastName
        default:
            return mi.FirstName < mj.FirstName
        }
    })
}

使用

switch
语句的模式可以轻松扩展到两个以上的排序标准,并且仍然足够短以便于阅读。

这是程序的其余部分:

func main() {
    members := []Member{
        {0, "The", "quick"},
        {1, "brown", "fox"},
        {2, "jumps", "over"},
        {3, "brown", "grass"},
        {4, "brown", "grass"},
        {5, "brown", "grass"},
        {6, "brown", "grass"},
        {7, "brown", "grass"},
        {8, "brown", "grass"},
        {9, "brown", "grass"},
        {10, "brown", "grass"},
        {11, "brown", "grass"},
    }

    sortByLastNameAndFirstNameFunctional(members)

    for _, member := range members {
        fmt.Println(member)
    }
}

完全不同的想法,但相同的API

如果你想避免多次提及字段

LastName
FirstName
,并且如果你想避免混合
i
j
(这可能一直发生),我玩了一下基本想法是:

func sortByLastNameAndFirstNameFunctional(members []Member) {
    NewSorter().
        AddStr(member -> member.LastName).
        AddStr(member -> member.FirstName).
        AddInt(member -> member.Id).
        SortStable(members)
}

由于 Go 不支持用于创建匿名函数的

->
运算符,并且也没有像 Java 那样的泛型,因此需要一些语法开销:

func sortByLastNameAndFirstNameFunctional(members []Member) {
    NewSorter().
        AddStr(func(i interface{}) string { return i.(Member).LastName }).
        AddStr(func(i interface{}) string { return i.(Member).FirstName }).
        AddInt(func(i interface{}) int { return i.(Member).Id}).
        SortStable(members)
}

使用

interface{}
和反射的实现和 API 有点难看,但它只提及每个字段一次,并且应用程序代码没有一次机会意外地混合索引
i
j
,因为它不涉及他们。

我本着 Java 的 Comparator.comparing 的精神设计了这个 API。

上述排序器的基础设施代码是:

type Sorter struct{ keys []Key }

func NewSorter() *Sorter { return new(Sorter) }

func (l *Sorter) AddStr(key StringKey) *Sorter { l.keys = append(l.keys, key); return l }
func (l *Sorter) AddInt(key IntKey) *Sorter    { l.keys = append(l.keys, key); return l }

func (l *Sorter) SortStable(slice interface{}) {
    value := reflect.ValueOf(slice)
    sort.SliceStable(slice, func(i, j int) bool {
        si := value.Index(i).Interface()
        sj := value.Index(j).Interface()
        for _, key := range l.keys {
            if key.Less(si, sj) {
                return true
            }
            if key.Less(sj, si) {
                return false
            }
        }
        return false
    })
}

type Key interface {
    Less(a, b interface{}) bool
}

type StringKey func(interface{}) string

func (k StringKey) Less(a, b interface{}) bool  { return k(a) < k(b) }

type IntKey func(interface{}) int

func (k IntKey) Less(a, b interface{}) bool  { return k(a) < k(b) }

5
投票

比当前接受的答案稍微干净的解决方案是做类似这样的事情:

sort.Slice(members, func(i, j int) bool {
    if members[i].FirstName != members[j].FirstName {
        return members[i].FirstName < members[j].FirstName
    }
    return members[i].LastName < members[j].LastName
})

这样做的好处是,如果你添加一个新字段

Age
(或其他东西),你会更清楚如何扩展它:

sort.Slice(members, func(i, j int) bool {
    if members[i].FirstName != members[j].FirstName {
        return members[i].FirstName < members[j].FirstName
    }
    if members[i].Age != members[j].Age {
        return members[i].Age < members[j].Age
    }
    return members[i].LastName < members[j].LastName
})

因此,模式是为所有属性添加一个

if X[i] != X[j]
语句,直到最后一个属性,这只是正常比较。


3
投票

我通常都是这样做的:

// You can edit this code!
// Click here and start typing.
package main

import (
    "fmt"
    "sort"
    "strings"
)

type User struct {
    Name string
    Age  int
}

func main() {
    users := []User{
        {"A", 10},
        {"B", 10},
        {"C", 20},
        {"D", 5},
        {"A", 100},
    }
    sort.Slice(users, func(i, j int) bool {
        lhs, rhs := users[i], users[j]
        byAge := lhs.Age - rhs.Age
        byName := strings.Compare(lhs.Name, rhs.Name) // Returns 0 if equal, -1 if lhs is less than rhs, and 1 if lhs is greater than rhs
        
        // The + sign is not necessary, but adds clarity as it means increasing in value, aka ascending.
        // sortBy(+byAge, +byName) // Sort by age asc, by name asc
        // sortBy(-byAge, +byName) // Sort by age desc, by name asc
        // sortBy(+byName, +byAge) // Sort by name asc, by age asc
        return sortBy(-byAge, -byName) // Sort by age desc, by name desc
    })
    fmt.Println(users)
}

func sortBy(sc ...int) bool {
    for _, c := range sc {
        if c != 0 {
            return c < 0
        }
    }
    return sc[len(sc)-1] < 0
}

1
投票

您可以创建函数切片:

package main

type (
   mFunc func(a, b member) bool
   member struct { lastName, firstName string }
)

var members = []member{
   {"Mullen", "Larry"},
   {"Howard", "James"},
   {"Clayton", "Adam"},
   {"Howard", "Ben"},
}

var mFuncs = []mFunc{
   func(a, b member) bool { return a.lastName < b.lastName },
   func(a, b member) bool { return a.firstName < b.firstName },
}

然后迭代函数,直到其中一个函数找到 区别:

package main

import (
   "fmt"
   "sort"
)

func main() {
   sort.Slice(members, func(a, b int) bool {
      ma, mb := members[a], members[b]
      for _, mf := range mFuncs {
         if mf(ma, mb) {
            return true
         }
         if mf(mb, ma) {
            break
         }
      }
      return false
   })
   fmt.Println(members) // [{Clayton Adam} {Howard Ben} {Howard James} {Mullen Larry}]
}

https://golang.org/pkg/sort#example__sortMultiKeys


0
投票

这非常有帮助。我需要对结构体进行排序,并在这里找到了答案。我实际上将其扩展到三重排序。尽管排序这么多对于运行时目的来说并不是最佳的,但它在某些情况下很有用,特别是当替代方案导致代码难以维护或修改并且更快的运行时并不重要时。

sort.Slice(servers, func(i, j int) bool {
        if servers[i].code < servers[j].code {
            return true
        } else if servers[i].code > servers[j].code {
            return false
        } else {
            if servers[i].group < servers[j].group {
                return true
            } else {
                if servers[i].group > servers[j].group {
                    return false
                }
            }
        }
        return servers[i].IDGroup < servers[j]. IDGroup
    })

此代码首先按代码排序,然后按组排序,然后按 IDGroup 排序。


0
投票

刚刚创建了 go 项目来实现它:https://github.com/itroot/keysort

您可以只返回要排序的字段列表:

package main

import (
    "fmt"

    "github.com/itroot/keysort"
)

type Data struct {
    A int
    B string
    C bool
}

func main() {
    slice := []Data{{1, "2", false}, {2, "2", false}, {2, "1", true}, {2, "1", false}}
    keysort.Sort(slice, func(i int) keysort.Sortable {
        e := slice[i]
        return keysort.Sequence{e.A, e.B, e.C}
    })
    fmt.Println(slice)
}

游乐场链接:https://play.golang.org/p/reEDcoXNiwh


0
投票

只需阅读 Go 官方文档中 https://pkg.go.dev/sort 中的 SortMultiKeys 示例代码,然后将其适应您的

Member
结构。

为了使这个答案保持最新,我不会在此处复制并粘贴整个示例,但最终您将能够按如下方式编写它:

OrderedBy(lastName, firstName).Sort(members)

如果要求更改为也按年龄排序,您只需添加

age
闭包:

OrderBy(lastName, firstName, age).Sort(members)

0
投票

Go 1.21 中引入

import (
"cmp"
"fmt"
"slices"
)

func main() {
type Person struct {
    Name string
    Age  int
}
people := []Person{
    {"Gopher", 13},
    {"Alice", 55},
    {"Bob", 24},
    {"Alice", 20},
}
slices.SortFunc(people, func(a, b Person) int {
    if n := cmp.Compare(a.Name, b.Name); n != 0 {
        return n
    }
    // If names are equal, order by age
    return cmp.Compare(a.Age, b.Age)
})
fmt.Println(people)
}

-1
投票

所有好的答案, 如果你不想写任何配置。您可以使用外部包进行排序。

go get -d github.com/raunakjodhawat/multisort

并像这样调用函数:

sortedSlice, err := multisort.MultiSorted(inputSlice, inputKeys, SortOrders)

要查看具体示例,请访问: https://github.com/raunakjodhawat/multisort


-4
投票

这是已接受答案的稍微更简洁的实现:

package main

import (
    "fmt"
    "sort"
)

type Member struct {
    FirstName string
    LastName  string
}

func main() {
    members := []Member{
        {"John", "Doe"},
        {"Jane", "Doe"},
        {"Mary", "Contrary"},
    }

    fmt.Println(members)

    sort.Slice(members, func(i, j int) bool {
        return members[i].LastName < members[j].LastName || members[i].FirstName < members[j].FirstName
    })

    fmt.Println(members)
}

运行将打印以下输出:

[{John Doe} {Jane Doe} {Mary Contrary}]
[{Mary Contrary} {Jane Doe} {John Doe}]

这符合预期:成员按姓氏升序打印,如果是平局,则按名字顺序打印。

© www.soinside.com 2019 - 2024. All rights reserved.