按顺序范围循环映射

2024-04-30

我正在寻找一种确定的方法来范围Go map为了。

Go 规范 https://golang.org/ref/spec#For_statements陈述如下:

映射的迭代顺序未指定,并且不保证从一次迭代到下一次迭代的顺序相同。如果在迭代过程中删除尚未到达的映射条目,则不会产生相应的迭代值。如果在迭代期间创建映射条目,则该条目可以在迭代期间产生或者可以被跳过。对于创建的每个条目以及从一次迭代到下一次迭代,选择可能会有所不同。如果映射为零,则迭代次数为 0。

我在 StackOverflow 和 Google 上找到的所有内容是(imho)我不喜欢的解决方法。

是否有一种可靠的方法来迭代地图并按照插入的顺序检索项目?

我找到的解决方案是:

  • 在两个单独的切片中跟踪键和值:这听起来像“不要使用地图”,失去了使用地图的所有优势。

  • 使用映射但跟踪不同切片中的键:这意味着数据重复,可能导致数据未对齐,并最终可能带来大量错误和痛苦的调试。

你有什么建议?


编辑以响应可能的重复标志。

我的问题和提供的问题之间略有不同(这个问题 https://stackoverflow.com/questions/23330781/sort-go-map-values-by-keys, 但是也this one https://stackoverflow.com/questions/18342784/how-to-iterate-through-a-map-in-golang-in-order),两个问题都要求按照键的字典顺序循环遍历地图;相反,我特别问:

有没有一种可靠的方法来迭代地图并按照插入的顺序检索项目?

这不是字典顺序的,因此不同于@gramme.ninja问题 https://stackoverflow.com/questions/23330781/sort-go-map-values-by-keys:

如何使键按顺序排列/对地图进行排序,以便键按顺序且值相对应?


如果您需要一个map和按顺序排列的键,这是两个不同的东西,您需要两种不同的(数据)类型来提供该功能。

带钥匙片

实现此目的的最简单方法是在不同的切片中维护键顺序。每当您将新对放入地图时,首先检查密钥是否已在其中。如果没有,请将新密钥添加到单独的切片中。当您需要按顺序排列元素时,可以使用键切片。当然,当您删除一对时,您也必须将其从切片中删除。

键切片只需包含键(而不是值),因此开销很小。

将这个新功能(地图+键切片)包装到新类型中并为其提供方法,并隐藏地图和切片。那么就不会出现数据错位的情况。

实施示例:

type Key int   // Key type
type Value int // Value type

type Map struct {
    m    map[Key]Value
    keys []Key
}

func New() *Map {
    return &Map{m: make(map[Key]Value)}
}

func (m *Map) Set(k Key, v Value) {
    if _, ok := m.m[k]; !ok {
        m.keys = append(m.keys, k)
    }
    m.m[k] = v
}

func (m *Map) Range() {
    for _, k := range m.keys {
        fmt.Println(m.m[k])
    }
}

使用它:

m := New()
m.Set(1, 11)
m.Set(2, 22)
m.Range()

尝试一下去游乐场 https://play.golang.org/p/wqhstzhBQT.

使用实现链表的值包装器

另一种方法是包装值,并且沿着实际值还存储下一个/上一个键。

例如,假设您想要一张类似的地图map[Key]Value:

type valueWrapper struct {
    value Value
    next  *Key // Next key
}

每当您将一对添加到地图时,您都会设置一个valueWrapper作为价值,你必须link这是前一个(最后一个)对。到link,你必须设置next最后一个包装器的字段指向这个新键。为了轻松实现这一点,建议还存储最后一个密钥(以避免必须搜索它)。

当您想按插入顺序迭代元素时,您从第一个元素开始(您必须存储它)及其关联的valueWrapper会告诉您下一个键(按插入顺序)。

实施示例:

type Key int   // Key type
type Value int // Value type

type valueWrapper struct {
    v    Value
    next *Key
}

type Map struct {
    m           map[Key]valueWrapper
    first, last *Key
}

func New() *Map {
    return &Map{m: make(map[Key]valueWrapper)}
}

func (m *Map) Set(k Key, v Value) {
    if _, ok := m.m[k]; !ok && m.last != nil {
        w2 := m.m[*m.last]
        m.m[*m.last] = valueWrapper{w2.v, &k}
    }
    w := valueWrapper{v: v}
    m.m[k] = w
    if m.first == nil {
        m.first = &k
    }
    m.last = &k
}

func (m *Map) Range() {
    for k := m.first; k != nil; {
        w := m.m[*k]
        fmt.Println(w.v)
        k = w.next
    }
}

使用它是一样的。尝试一下去游乐场 https://play.golang.org/p/Ih-ZvaGXsl.

Notes:您可以根据自己的喜好更改一些内容:

  • 您可以像这样声明内部地图m map[Key]*valueWrapper所以在Set()你可以改变next字段,而无需分配新的valueWrapper.

  • 您可以选择first and last字段类型*valueWrapper

  • 您可以选择next属于类型*valueWrapper

比较

使用额外切片的方法更简单、更干净。但是,如果地图变大,从中删除元素可能会变得很慢,因为我们还必须在切片中找到“未排序”的键,所以它是O(n)复杂。

值包装器中使用链表的方法可以轻松扩展以支持快速元素删除,即使地图很大,如果您还添加prev场到valueWrapper结构。因此,如果您需要删除一个元素,您可以超快速地找到包装器(O(1)),更新 prev 和 next 包装器(相互指向),并执行一个简单的delete()操作,就是O(1).

请注意,第一个解决方案(使用切片)中的删除仍然可以通过使用 1 个附加映射来加速,该映射将从键映射到切片中键的索引(map[Key]int),所以删除操作仍然可以在O(1),以换取更大的复杂性。加速的另一个选择可能是将映射中的值更改为包装器,它可以保存切片中键的实际值和索引。

查看相关问题:为什么 Go 不能按插入顺序迭代映射? https://stackoverflow.com/questions/28930416/why-cant-go-iterate-maps-in-insertion-order

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

按顺序范围循环映射 的相关文章

随机推荐