如何按照值对Map[string]int进行排序?

111

考虑以下代码块

map[string]int {"hello":10, "foo":20, "bar":20}

我希望能够打印出来。

foo, 20
bar, 20
hello, 10

按照从高到低的顺序


正如您所知道的,这种情况需要在映射上进行迭代,这是不推荐的,因为它涉及到大 O 问题。 - Amin Shojaei
6个回答

111

我在Golang-nuts上找到了Andrew Gerrand的答案。

你可以通过编写len/less/swap函数来实现排序接口。

func rankByWordCount(wordFrequencies map[string]int) PairList{
  pl := make(PairList, len(wordFrequencies))
  i := 0
  for k, v := range wordFrequencies {
    pl[i] = Pair{k, v}
    i++
  }
  sort.Sort(sort.Reverse(pl))
  return pl
}

type Pair struct {
  Key string
  Value int
}

type PairList []Pair

func (p PairList) Len() int { return len(p) }
func (p PairList) Less(i, j int) bool { return p[i].Value < p[j].Value }
func (p PairList) Swap(i, j int){ p[i], p[j] = p[j], p[i] }

请在此处查找原始帖子 https://groups.google.com/forum/#!topic/golang-nuts/FT7cjmcL7gw


1
除了 Less 返回错误的结果之外,使用 > 进行反向排序。 - Fred Foo
3
@larsmans 不好意思!谢谢你指出来。我改用sort.Reverse来获取反向结果了。 - samol
2
更好的是,我甚至不知道 sort.Reverse 的存在。+1。 - Fred Foo

106

在Go 1.8中有一个新的sort.Slice函数,因此现在这变得更加简单了。

package main

import (
    "fmt"
    "sort"
)

func main() {
    m := map[string]int{
        "something": 10,
        "yo":        20,
        "blah":      20,
    }

    type kv struct {
        Key   string
        Value int
    }

    var ss []kv
    for k, v := range m {
        ss = append(ss, kv{k, v})
    }

    sort.Slice(ss, func(i, j int) bool {
        return ss[i].Value > ss[j].Value
    })

    for _, kv := range ss {
        fmt.Printf("%s, %d\n", kv.Key, kv.Value)
    }
}

https://play.golang.org/p/y1_WBENH4N


3
我不喜欢输出结果不是我开始使用的地图。 - hendry
@hendry 这个答案是特别针对原问题中的格式而给出的。在go1.12中,你可以直接打印map,它会被排序,参见这个issue: https://github.com/golang/go/issues/21095 - voutasaurus
1
@TommasoBarbugli,当我说“稳定”的排序时,我的意思并不是我或教科书上的这种方式。但是,按字母顺序对键进行排序是可能的,但这并不是在此发布的问题中所要求的。 - voutasaurus
2
还有一个 sort.SliceStable(也在 Go 1.8 中添加)可以保留相等元素的原始顺序。 - Dave Yarwood
1
SliceStable是一个很好的函数,但它不能稳定地排序映射。映射迭代顺序是随机的,这是有原因的。如果您希望每次获得相同的结果,最好使用另一个决定元素(正如Tommaso建议的那样,可以使用字符串本身)。 - voutasaurus
显示剩余4条评论

23

先按值排序键,然后迭代地图:

package main

import (
    "fmt"
    "sort"
)

func main() {
    counts := map[string]int{"hello": 10, "foo": 20, "bar": 20}

    keys := make([]string, 0, len(counts))
    for key := range counts {
        keys = append(keys, key)
    }
    sort.Slice(keys, func(i, j int) bool { return counts[keys[i]] > counts[keys[j]] })

    for _, key := range keys {
        fmt.Printf("%s, %d\n", key, counts[key])
    }
}

6
我不确定是谁在点踩,可能是那些没有认真阅读比较函数的人。 而且被接受的答案并没有产生原提问者要求的输出结果,而是引入了一个需要在实际代码中维护的新数据结构。 这是我答案的游乐场链接:https://play.golang.org/p/Y4lrEm2-hT5 - AlexanderYastrebov
2
这应该是被接受的答案,最简单的解决方案。 - Simon
1
我认为这个答案是最好的,因为它不需要一个结构体。 - guettli

19
例如:
package main

import (
        "fmt"
        "sort"
)

func main() {
        m := map[string]int{"hello": 10, "foo": 20, "bar": 20}
        n := map[int][]string{}
        var a []int
        for k, v := range m {
                n[v] = append(n[v], k)
        }
        for k := range n {
                a = append(a, k)
        }
        sort.Sort(sort.Reverse(sort.IntSlice(a)))
        for _, k := range a {
                for _, s := range n[k] {
                        fmt.Printf("%s, %d\n", s, k)
                }
        }
}

Playground


输出:

foo, 20
bar, 20
hello, 10

1
这假设这些值没有身份。 - newacct
2
@newacct:它只解决了原始问题,而不是一般情况;-) - zzzz
这个解决方案对我的情况也起作用了,而且很容易理解。 - Tommy

3

我经常需要对一个map[string]int进行排序,这个map是用来计数的。我一直使用以下方法。

func rankMapStringInt(values map[string]int) []string {
    type kv struct {
        Key   string
        Value int
    }
    var ss []kv
    for k, v := range values {
        ss = append(ss, kv{k, v})
    }
    sort.Slice(ss, func(i, j int) bool {
        return ss[i].Value > ss[j].Value
    })
    ranked := make([]string, len(values))
    for i, kv := range ss {
        ranked[i] = kv.Key
    }
    return ranked
}

使用它按照值的顺序迭代键

values := map[string]int{"foo": 10, "bar": 20, "baz": 1}

for i, index := range rankMapStringInt(values) {
    fmt.Printf("%3d: %s -> %d", i, index, values[index])
}

1
在我的情况下,我正在处理一个我创建的程序。在这个程序中,我创建了一个与您一样的 Map,带有 stringint。然后我像您一样发现,Go 实际上并没有内置的方式来对这样的东西进行排序。我阅读了其他答案,但并不喜欢我所读到的内容。
因此,我尝试以不同的方式考虑问题。Go 可以使用 sort.Ints 来排序切片。此外,Go 还可以使用具有自定义比较器的 sort.Slice。因此,我创建了一个 struct,其中包含 stringint,而不是创建 stringint 的 Map。然后,您可以进行排序:
package main

import (
   "fmt"
   "sort"
)

type file struct {
   name string
   size int
}

func main() {
   a := []file{
      {"april.txt", 9}, {"may.txt", 7},
   }
   sort.Slice(a, func (d, e int) bool {
      return a[d].size < a[e].size
   })
   fmt.Println(a)
}

这种方法并不适用于所有人,因为你可能被迫处理其他人创建的地图。但对我来说很有用。好处是,与所有其他答案不同,这个方法不使用任何循环。


你的回答回答了一个略微不同的问题。请坚持原始问题的输入结构(map)。 - guettli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接