按顺序循环映射

时间:2016-09-12 12:17:15

标签: loops dictionary go hashmap for-range

我正在寻找一种确定的方式来按顺序排列Go map

Golang spec声明如下:

  

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

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

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

我找到的解决方案是:

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

  • 使用地图但跟踪不同切片中的密钥:这意味着数据重复可能导致数据错位,最终可能带来大量错误和痛苦的调试。

你有什么建议?

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

我的问题与提供的问题(this question,还有this one)之间存在细微差别,这两个问题都要求在按键字典顺序后循环显示地图;相反,我特意问过:

  

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

不是词典,因此与@gramme.ninja question不同:

  

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

1 个答案:

答案 0 :(得分:9)

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

使用键片

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

密钥片只需要包含密钥(而不是值),因此开销很小。

将此新功能(map + keys slice)包装到新类型中并为其提供方法,并隐藏地图和切片。然后不会发生数据错位。

示例实施:

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()

Go Playground上尝试。

使用实现链表的值包装器

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

例如,假设你想要一个像map[Key]Value这样的地图:

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

每当您向地图添加一对时,都会将valueWrapper设置为值,并且您必须链接到上一个(最后一个)对。要链接,您必须将最后一个包装器的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
    }
}

使用它是一样的。在Go Playground上尝试。

注意:您可以根据自己的喜好改变一些事项:

  • 您可以声明内部地图,例如m map[Key]*valueWrapper,因此Set()您可以更改next字段,而无需分配新的valueWrapper

  • 您可以选择firstlast字段为*valueWrapper类型

  • 您可以选择next类型*valueWrapper

比较

使用额外切片的方法更简单,更清洁。但是,如果地图变大,则从中移除元素可能会变慢,因为我们还必须在切片中找到“未排序”的键,因此它的O(n)复杂度。

如果您还将prev字段添加到valueWrapper结构中,即使地图很大,也可以轻松扩展value-wrapper中包含链表的方法以支持快速元素删除。因此,如果您需要删除元素,您可以超级快速找到包装器(O(1)),更新prev和下一个包装器(指向彼此),并执行简单的delete()操作,它O(1)

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

请参阅相关问题:Why can't Go iterate maps in insertion order?