化繁为简:Swift剔除数组中重复元素的几种姿势

版权声明:大熊猫猪·侯佩原创或翻译作品.谢绝转载! hopy https://blog.csdn.net/mydo/article/details/85275735

本文向大家介绍稍许算法的实现:关于如何去除数组中的重复元素,并比较了三种算法之间的效率.全部代码在Xcode的Playground中实现,直观明了,适合Swift学习入门童鞋观赏.

有个前提

如题,很多童鞋立即给出解决方法,无外乎是利用Swift内置的集合(Set)或字典(Dict)的一个特性:过滤重复元素.

但由于集合和字典中元素的顺序是无法保证的,所以这建立在一个前提基础之上:结果数组元素顺序和原数组可以不同!

但本文的算法要求:在剔除重复元素之后,元素顺序和原数组必须相同,这正是数组核心特点:有序性的一种体现.

第一种实现

如果想要简单,就必须要多些限制.

如果数组元素能满足Hashable协议,我们利用内置集合也未尝不可:

extension Array where Element:Hashable{
    /// 返回剔除重复元素后的数组,其元素顺序不变
    public var noRepetitionUseSet:[Element]{
        var set = Set<Element>(self)
        var resultAry = [Element]()
        
        for item in self{
            if set.contains(item){
            	//只会保留第一个重复元素!!!
                resultAry.append(item)
                set.remove(item)
            }
        }
        return resultAry
    }
}

如上,我们首先用集合过滤所有重复的元素,然后遍历数组,只保留第一个重复的元素.这样原有数组的顺序即得以保持不变.

数组操作是麻烦之源

如果不能保证数组元素满足Hashable协议,至少它们要满足Equatable协议.

这里略微复杂的地方在于,动态删除数组的元素需要考虑到它们删除的顺序.

删除后的黑洞立即会被后续所填满,仿佛根本不存在一样. — 鲁迅

在这里插入图片描述

还有一个稍微棘手的问题在于:如何计算多个中间数组(它们只是原始数组的部分拷贝)元素在原始数组中对应的索引位置.

所以,这里不如让我们另辟蹊径,利用Swift的另一个特性: Optional!!!

有种类型叫:可有可无!

如果可以解决下面问题,就会使算法简单很多:

  • 不用直接删除重复元素
  • 简化定位元素的位置

这时Optional前来拯救我们了.

其核心思想是:不删除重复元素而是将其设置为nil!!!

extension Array where Element:Equatable{
    public var noRepetition:[Element]{
        var copy:[Element?] = self
        //下面只有非nil值才有比较的必要.
        for (i,x) in copy.enumerated(){
            if x == nil {continue}
            for (j,y) in copy[(i+1)...].enumerated(){
                if y == nil {continue}
                if x! == y! {
                    //将原数组中对应的重复元素设置为nil
                    copy[j+i+1] = nil
                }
            }
        }
        //剔除非空值,保持元素顺序
        return copy.compactMap {$0}
    }
}

如上,算法比较复杂,理解起来略微费劲!

永无止境

这个世界永远有更好的解决方法,就看你能不能发现了.

其实在不使用集合和字典的前提下,我们还可以这么写:

public var noRepetition2:[Element]{
    var result = [Element]()
    for item in self{
        if !result.contains(item) {
            result.append(item)
        }
    }
    return result
}

我们只需要将每一个非重复的元素,放到一个新数组中,就完美的绕开了上面所有这些麻烦的东东…

问题来了:谁更快?快多少?

那么上面3种方法到底谁更快呢???

因为是在Playground中运行,所以很快能写一个性能计算器来得到答案:

struct Timing{
    var start:Date!
    
    mutating func timingStart(){
        start = Date()
    }
    
    func timingStop()->Double{
        return Date().timeIntervalSince(start)
    }
    
    mutating func timing(count:Int,working:()->Void){
        var totalTiming:Double = 0.0
        for _ in 0..<count{
            timingStart()
            working()
            totalTiming += timingStop()
        }
        print("\(count)次平均执行时间 : \(totalTiming/Double(count))")
    }
}

因为系统的复杂性,我们不可能只用一两次执行来统计准确的运行时间.

测试代码如下:

let a = [1,1,1,2,3,4,1,3,4,5,6,2,1,1,1,1,17,7,7,8,9,2,1,0,1]
var t = Timing()
t.timing(count: 1000){
    _ = a.noRepetitionUseSet
}
print(a.noRepetitionUseSet)

t.timing(count: 1000){
    _ = a.noRepetition
}
print(a.noRepetition)

t.timing(count: 1000){
    _ = a.noRepetition2
}
print(a.noRepetition2)

结果如下

1000次平均执行时间 : 0.002724636197090149
[1, 2, 3, 4, 5, 6, 8, 17, 7, 9, 0]
1000次平均执行时间 : 0.004712764143943787
[1, 2, 3, 4, 5, 6, 8, 17, 7, 9, 0]
1000次平均执行时间 : 0.002008106589317322
[1, 2, 3, 4, 5, 6, 8, 17, 7, 9, 0]

首先可以看到三者返回剔除重复元素后的结果是完全一样的(这是必须的),另外它们的速度是第二种比第一种慢大约2ms左右,而最后一种比第一种还快一点点.我想日常的使用还是可以接受的.

(更正:只是对于[1,1,1,2,3,4,1,3,4,5,6,2,1,1,1,1,17,7,7,8,9,2,1,0,1]这个数组来说,第二种比第一种慢2ms,但随着数组元素个数的增加,后者会比前者慢更多!)

结尾

最后总结一下:

  • 第一种算法利用内置集合类,对数组元素类型有限制,但速度快,逻辑简单.
  • 第二种算法依赖更少,但速度稍慢,逻辑略复杂.
  • 第三种算法速度最快,依赖少,逻辑最简单,可以作为首选.

虽然第一种方法利用了内部Swift库的优化,但创建集合的时间抵消了优化的时间,所以最后一种方法胜出!!!

上面我们实现了3种剔除重复元素的算法,并且比较了它们的优劣.

大家可以自由选择,如果你有更好的算法,欢迎讨论 😉

这就是所有的内容了.

(PS:最后还有一种很简单的实现:

public var noRepetition3:[Element]{
    var curIdx = -1
    return self.filter {
        curIdx += 1
        return self.index(of: $0) == curIdx
    }
}

至于原理和速度大家可以遐想一下)

早睡早起,感谢观赏

在这里插入图片描述

没有更多推荐了,返回首页

私密
私密原因:
请选择设置私密原因
  • 广告
  • 抄袭
  • 版权
  • 政治
  • 色情
  • 无意义
  • 其他
其他原因:
120
出错啦
系统繁忙,请稍后再试