在for循环中删除切片元素

huangapple go评论72阅读模式
英文:

Remove slice element within a for

问题

保持顺序的一种习惯用法是从切片 a 中删除元素 i 的方法似乎是:

a = append(a[:i], a[i+1:]...)

我想知道在循环内部执行此操作的最佳方法是什么。据我了解,不可能在 range 循环中使用它:

for i := range a { // 错误
    if conditionMeets(a[i]) {
        a = append(a[:i], a[i+1:]...)
    }
}

但是可以使用 len(a)。[编辑:这种方法不起作用,请参见下面的答案]

for i := 0; i < len(a); i++ {
    if conditionMeets(a[i]) {
        a = append(a[:i], a[i+1:]...)
    }
}

除了使用 lenappend,是否有更好或更符合习惯的方法?

英文:

An idiomatic method to remove an element i from a slice a, preserving the order, seems to be:

a = append(a[:i], a[i+1:]...)

I was wondering which would be the best way to do it inside a loop. As I understand, it is not possible to use it inside a range for:

for i := range a { // BAD
	if conditionMeets(a[i]) {
		a = append(a[:i], a[i+1:]...)
	}
}

However it is possible to use len(a). [EDIT: this doesn't work, see answers below]

for i := 0; i &lt; len(a); i++ {
	if conditionMeets(a[i]) {
		a = append(a[:i], a[i+1:]...)
	}
}

Is there a better or more idiomatic way than using len or append?

答案1

得分: 12

你提出的解决方案是错误的。问题在于当你从切片中移除一个元素时,所有后续的元素都会被“移动”。但是循环不知道你改变了底层的切片,并且循环变量(索引)会像往常一样递增,即使在这种情况下它不应该递增,因为这样你会跳过一个元素。

而且,如果切片中包含两个相邻的需要被移除的元素,第二个元素将不会被检查和移除。

所以,如果你移除一个元素,循环变量必须手动递减!让我们看一个例子:移除以"a"开头的单词:

func conditionMeets(s string) bool {
	return strings.HasPrefix(s, "a")
}

// Solution
a := []string{"abc", "bbc", "aaa", "aoi", "ccc"}
for i := 0; i < len(a); i++ {
	if conditionMeets(a[i]) {
		a = append(a[:i], a[i+1:]...)
		i--
	}
}
fmt.Println(a)

输出结果为:

[bbc ccc]

**或者更好的方法是:**使用逆向循环,这样你就不需要手动递减变量,因为在这种情况下,被移动的元素位于切片的“已处理”部分。

// Solution
a := []string{"abc", "bbc", "aaa", "aoi", "ccc"}
for i := len(a) - 1; i >= 0; i-- {
	if conditionMeets(a[i]) {
		a = append(a[:i], a[i+1:]...)
	}
}
fmt.Println(a)

输出结果相同。

多个元素移除的替代方法

如果你需要移除“多个”元素,这种方法可能会很慢,因为你需要进行大量的复制(append()会进行复制)。想象一下:你有一个包含1000个元素的切片;仅仅移除第一个元素就需要将999个元素复制到前面。此外,许多新的切片描述符将被创建:每个元素的移除都会创建2个新的切片描述符(a[:i]a[i+1:]加上必须更新aappend()的结果)。在这种情况下,将不可移除的元素复制到一个新的切片中可能更高效。

一个高效的解决方案:

// Solution
a := []string{"abc", "bbc", "aaa", "aoi", "ccc"}
b := make([]string, len(a))
copied := 0
for _, s := range a {
	if !conditionMeets(s) {
		b[copied] = s
		copied++
	}
}
b = b[:copied]
fmt.Println(b)

这个解决方案分配了一个与源切片长度相同的切片,因此不会进行新的分配(和复制)。这个解决方案也可以使用range循环。如果你想要将结果赋给a,可以将结果赋给aa = b[:copied]

输出结果相同。

就地替代多个元素移除(以及一般用途)

我们也可以使用循环“就地”进行移除,通过维护两个索引并在同一个切片中将不可移除的元素赋值(向前复制)。

需要记住的一件事是,我们应该将被移除元素的位置置零,以删除对不可访问值的引用,以便垃圾回收器可以发挥作用。这也适用于其他解决方案,但只在这里提到。

示例实现:

// Solution
a := []string{"abc", "bbc", "aaa", "aoi", "ccc"}
copied := 0
for i := 0; i < len(a); i++ {
	if !conditionMeets(a[i]) {
		a[copied] = a[i]
		copied++
	}
}
for i := copied; i < len(a); i++ {
	a[i] = "" // 将被移除元素的位置置零(允许垃圾回收器工作)
}
a = a[:copied]
fmt.Println(a)

输出结果相同。在Go Playground上尝试所有的例子。

英文:

Your proposed solution is incorrect. The problem is that when you remove an element from a slice, all subsequent elements are shifted. But the loop doesn't know that you changed the underlying slice and loop variable (the index) gets incremented as usual, even though in this case it shouldn't because then you skip an element.

And if the slice contains 2 elements which are right next to each other both of which need to be removed, the second one will not be checked and will not be removed.

So if you remove an element, the loop variable has to be decremented manually! Let's see an example: remove words that start with &quot;a&quot;:

func conditionMeets(s string) bool {
	return strings.HasPrefix(s, &quot;a&quot;)
}

Solution (try it with all other examples below on the Go Playground):

a := []string{&quot;abc&quot;, &quot;bbc&quot;, &quot;aaa&quot;, &quot;aoi&quot;, &quot;ccc&quot;}
for i := 0; i &lt; len(a); i++ {
	if conditionMeets(a[i]) {
		a = append(a[:i], a[i+1:]...)
		i--
	}
}
fmt.Println(a)

Output:

[bbc ccc]

Or better: use a downward loop and so you don't need to manually decrement the variable, because in this case the shifted elements are in the "already processed" part of the slice.

a := []string{&quot;abc&quot;, &quot;bbc&quot;, &quot;aaa&quot;, &quot;aoi&quot;, &quot;ccc&quot;}
for i := len(a) - 1; i &gt;= 0; i-- {
	if conditionMeets(a[i]) {
		a = append(a[:i], a[i+1:]...)
	}
}
fmt.Println(a)

Output is the same.

Alternate for many removals

If you have to remove "many" elements, this might be slow as you have to do a lot of copy (append() does the copy). Imagine this: you have a slice with 1000 elements; just removing the first element requires copying 999 elements to the front. Also many new slice descriptors will be created: every element removal creates 2 new slice descriptors (a[:i], a[i+1:]) plus a has to be updated (the result of append()). In this case it might be more efficient to copy the non-removable elements to a new slice.

An efficient solution:

a := []string{&quot;abc&quot;, &quot;bbc&quot;, &quot;aaa&quot;, &quot;aoi&quot;, &quot;ccc&quot;}
b := make([]string, len(a))
copied := 0
for _, s := range(a) {
	if !conditionMeets(s) {
		b[copied] = s
		copied++
	}
}
b = b[:copied]
fmt.Println(b)

This solution allocates a slice with the same length as the source, so no new allocations (and copying) will be performed. This solution can also use the range loop. And if you want the result in a, assign the result to a: a = b[:copied].

Output is the same.

In-place alternate for many removals (and for general purposes)

We can also do the removal "in place" with a cycle, by maintaining 2 indices and assigning (copying forward) non-removable elements in the same slice.

One thing to keep in mind is that we should zero places of removed elements in order to remove references of unreachable values so the GC can do its work. This applies to other solutions as well, but only mentioned here.

Example implementation:

a := []string{&quot;abc&quot;, &quot;bbc&quot;, &quot;aaa&quot;, &quot;aoi&quot;, &quot;ccc&quot;}
copied := 0
for i := 0; i &lt; len(a); i++ {
	if !conditionMeets(a[i]) {
		a[copied] = a[i]
		copied++
	}
}
for i := copied; i &lt; len(a); i++ {
	a[i] = &quot;&quot; // Zero places of removed elements (allow gc to do its job)
}
a = a[:copied]
fmt.Println(a)

Output is the same. Try all the examples on the Go Playground.

huangapple
  • 本文由 发表于 2015年11月3日 17:52:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/33495995.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定