【问题标题】:Go: What is the fastest/cleanest way to remove multiple entries from a slice?Go:从切片中删除多个条目的最快/最干净的方法是什么?
【发布时间】:2011-06-28 14:28:14
【问题描述】:

你将如何在下面的代码中实现 deleteRecords 函数:

Example:

type Record struct {
  id int
  name string
}

type RecordList []*Record

func deleteRecords( l *RecordList, ids []int ) {
   // Assume the RecordList can contain several 100 entries.
   // and the number of the of the records to be removed is about 10.
   // What is the fastest and cleanest ways to remove the records that match
   // the id specified in the records list.
}

【问题讨论】:

    标签: go slice


    【解决方案1】:

    我在我的机器上做了一些微基准测试,尝试了这里回复中给出的大多数方法,当你在 ids 列表中拥有大约 40 个元素时,这段代码最快:

    func deleteRecords(data []*Record, ids []int) []*Record {
        w := 0 // write index
    
    loop:
        for _, x := range data {
            for _, id := range ids {
                if id == x.id {
                    continue loop
                }
            }
            data[w] = x
            w++
        }
        return data[:w]
    }
    

    您没有说保持列表中记录的顺序是否重要。如果你不这样做,那么这个函数比上面的更快并且仍然相当干净。

    func reorder(data []*Record, ids []int) []*Record {
        n := len(data)
        i := 0
    loop:
        for i < n {
            r := data[i]
            for _, id := range ids {
                if id == r.id {
                    data[i] = data[n-1]
                    n--
                    continue loop
                }
            }
            i++
        }
        return data[0:n]
    }
    

    随着 id 数量的增加,线性搜索的成本也会增加。在大约 50 个元素时,使用 map 或进行二分搜索来查找 id 会变得更有效,只要您可以避免每次都重新构建 map(或重新排序列表)。在数百个 id 时,使用 map 或二分搜索会变得更有效,即使您每次都必须重新构建它。

    如果您希望保留切片的原始内容,则更合适的是:

    func deletePreserve(data []*Record, ids []int) []*Record {
        wdata := make([]*Record, len(data))
        w := 0
    loop:
        for _, x := range data {
            for _, id := range ids {
                if id == x.id {
                    continue loop
                }
            }
            wdata[w] = x
            w++
        }
        return wdata[0:w]
    }
    

    【讨论】:

    • 我自己做了一些基准测试,并确认您的方法非常快。与您的第一个函数相比,我没有发现重新排序函数有太多的加速。似乎函数调用仍然很慢(至少在 Windows 8g 上。(也许如果编译器开始内联,这会改变。)
    • 那很好,我只是想知道为什么 Go 团队不提供从切片中删除单个/多个条目的安全方法。这显然是一种惯用的方法。
    • 如果需要经常这样做,可以考虑使用container/list提供的双向链表。
    【解决方案2】:

    对于一个个人项目,我做了这样的事情:

    func filter(sl []int, fn func(int) bool) []int {
        result := make([]int, 0, len(sl))
        last := 0
        for i, v := range sl {
            if fn(v) {
                result = append(result, sl[last:i]...)
                last = i + 1 
            }   
        }   
        return append(result, sl[last:]...)
    }
    

    它不会改变原始的,但应该是相对有效的。 这样做可能会更好:

    func filter(sl []int, fn func(int) bool) (result []int) {
        for _, v := range sl {
           if !fn(v) {
             result = append(result, v)
           }
        }
        return
    }
    

    更简单、更干净。 如果你想就地做,你可能想要这样的东西:

    func filter(sl []int, fn func(int) bool) []int {
        outi := 0
        res := sl
        for _, v := range sl {
            if !fn(v) {
                res[outi] = v 
                outi++
            }   
        }   
        return res[0:outi]
    }
    

    您可以优化它以使用 copy 复制元素范围,但这是两次 代码,可能不值得。

    因此,在这种特定情况下,我可能会这样做:

    func deleteRecords(l []*Record, ids []int) []*Record {
        outi := 0
    L:
        for _, v := range l { 
            for _, id := range ids {
                if v.id == id {
                    continue L
                }   
            }   
            l[outi] = v 
            outi++
        }   
        return l[0:outi]
    }
    

    (注:未经测试。)

    没有分配,没有什么花哨的,并且假设记录列表和您提供的 id 列表的粗略大小,简单的线性搜索可能会做得更好,但没有任何开销。我意识到我的版本改变了切片 并且 返回一个新切片,但这在 Go 中并不是惯用的,它避免了强制调用站点上的切片进行堆分配。

    【讨论】:

      【解决方案3】:

      对于您描述的情况,其中 len(ids) 大约为 10,而 len(*l) 为数百个,这应该相对较快,因为它通过就地更新来最小化内存分配。

      package main
      
      import (
          "fmt"
          "strconv"
      )
      
      type Record struct {
          id   int
          name string
      }
      
      type RecordList []*Record
      
      func deleteRecords(l *RecordList, ids []int) {
          rl := *l
          for i := 0; i < len(rl); i++ {
              rid := rl[i].id
              for j := 0; j < len(ids); j++ {
                  if rid == ids[j] {
                      copy(rl[i:len(*l)-1], rl[i+1:])
                      rl[len(rl)-1] = nil
                      rl = rl[:len(rl)-1]
                      break
                  }
              }
          }
          *l = rl
      }
      
      func main() {
          l := make(RecordList, 777)
          for i := range l {
              l[i] = &Record{int(i), "name #" + strconv.Itoa(i)}
          }
          ids := []int{0, 1, 2, 4, 8, len(l) - 1, len(l)}
          fmt.Println(ids, len(l), cap(l), *l[0], *l[1], *l[len(l)-1])
          deleteRecords(&l, ids)
          fmt.Println(ids, len(l), cap(l), *l[0], *l[1], *l[len(l)-1])
      }
      

      输出:

      [0 1 2 4 8 776 777] 777 777 {0 name #0} {1 name #1} {776 name #776}
      [0 1 2 4 8 776 777] 772 777 {1 name #1} {3 name #3} {775 name #775}
      

      【讨论】:

        【解决方案4】:

        您可以使用地图,而不是重复搜索 ID。此代码预先分配地图的完整大小,然后将数组元素移动到位。没有其他分配。

        func deleteRecords(l *RecordList, ids []int) {
            m := make(map[int]bool, len(ids))
            for _, id := range ids {
                m[id] = true
            }
            s, x := *l, 0
            for _, r := range s {
                if !m[r.id] {
                    s[x] = r
                    x++
                }
            }
            *l = s[0:x]
        }
        

        【讨论】:

          【解决方案5】:

          使用vector package's Delete method 作为指导,或者只使用向量而不是切片。

          【讨论】:

          • 等一下,看到您的回复后,我意识到我误解了这个问题。
          • 有趣。我会等着看这里还有什么其他解决方案,然后做一些基准测试,看看这些解决方案是否有很大的不同。
          • 我敢打赌这并不重要,因为你只是在复制指针而不是整个结构。
          【解决方案6】:

          这是一种选择,但我希望有更清洁/更快更实用的选择:

          func deleteRecords( l *RecordList, ids []int ) *RecordList {
              var newList RecordList
              for _, rec := range l {
                  toRemove := false
                  for _, id := range ids {
                  if rec.id == id {
                      toRemove = true
                  }
                  if !toRemove {
                      newList = append(newList, rec)
                  }
              }
              return newList
          }
          

          【讨论】:

          • append() 可以在该循环的每次迭代中分配。
          • 如果需要重新分配,我假设 append 将加倍容量。我在文档中找不到它...
          • 为什么不用make([]RecordList, len(*l)) 创建newList
          • 虽然append () 可以在循环的每次迭代中进行分配,但事实并非如此。当前实现在文件src/pkg/runtime/slice.c 中的runtime appendslice1 函数中过度分配。
          • 返回值与类型不匹配
          【解决方案7】:

          如果 l 和 id 足够大,首先对两个列表进行 Sort() 然后对它们执行单个循环而不是两个嵌套循环会更有效

          【讨论】:

            猜你喜欢
            • 2011-03-12
            • 2015-01-19
            • 1970-01-01
            • 2010-10-12
            • 2012-11-30
            • 1970-01-01
            • 1970-01-01
            • 2015-06-19
            • 2020-11-02
            相关资源
            最近更新 更多