5 个使用 Swift 高阶函数简化的复杂算法

896 阅读3分钟

作为开发人员,我们经常需要处理需要数小时甚至数天才能开发的复杂算法。由于斯威夫特的高阶功能,如mapreducefilter,等,其中的一些复杂的算法现在可以轻松地只用几行代码解决。

在本文中,我想向您展示五种以前难以实现但现在由于 Swift 中的高阶函数而非常容易实现的算法。

在整篇文章中,我将使用以下students数组作为模型对象,以便您可以更好地了解这些高阶函数的工作原理:

// The model object of upcoming examples
let students = [
    Student(id: "001", name: "Jessica", gender: .female, age: 20),
    Student(id: "002", name: "James", gender: .male, age: 25),
    Student(id: "003", name: "Mary", gender: .female, age: 19),
    Student(id: "004", name: "Edwin", gender: .male, age: 27),
    Student(id: "005", name: "Stacy", gender: .female, age: 18),
    Student(id: "006", name: "Emma", gender: .female, age: 22),
]

enum Gender {
    case male
    case female
}

struct Student {
    let id: String
    let name: String
    let gender: Gender
    let age: Int
}

让我们开始吧!

1. 按条件对数组元素进行分组

假设我们想按学生姓名的第一个字母对学生进行分组。传统上,我们必须手动循环遍历数组中的每个元素并相应地对它们进行分组。

现在,在Dictionary(grouping:by:)初始化程序的帮助下,我们可以在不使用for-in循环的情况下实现这一点。就是这样:

let groupByFirstLetter = Dictionary(grouping: students) { student in
    return student.name.first!
}

/*
 Output:
 [
    "E": [Edwin, Emma],
    "M": [Mary],
    "J": [Jessica, James],
    "S": [Stacy]
 ]
 */

从上面的示例代码中可以看出,初始化程序将生成一个类型为 的字典[KeyType: Student]。如果我们想按标准对学生进行分组并在多部分表格视图中显示他们,这将特别有用。

我们可以通过在 Swift 中使用速记参数名称或键路径语法来进一步简化此代码:

// Using shorthand argument names
let groupByFirstLetter = Dictionary(grouping: students, by: { $0.name.first! })

// Using key path syntax
let groupByFirstLetter = Dictionary(grouping: students, by: \.name.first!)

想知道如何按自定义对象对数组元素进行分组吗?查看我之前的文章“ 在 Swift 中使用字典对数组元素进行分组 ”。

2. 计算数组元素的出现次数

计算数组中元素的总数很容易,但是如果我们想根据某些条件计算元素的出现次数怎么办?例如,假设我们想知道students数组中有多少男学生和女学生。

解决这个问题的一种方法是使用Dictionary(grouping:by:)我们刚刚看到的初始化程序:

let groupByGender = Dictionary(grouping: students, by: \.gender)

let femaleCount = groupByGender[.female]!.count // Output: 4
let maleCount = groupByGender[.male]!.count     // Output: 2

上面的方法可能会给我们想要的结果。但是,它确实有一些内存开销。如您所见,初始化程序将生成我们并不真正需要的男女学生数组。我们需要的只是男女学生的出现。

为了克服内存开销,我们可以利用数组的reduce(into:)功能。我们来看看下面的示例代码:

let genderCount = students.reduce(into: [Gender: Int]()) { result, student in
    
    guard var count = result[student.gender] else {
        // Set initial value to `result`
        result[student.gender] = 1
        return
    }
    
    // Increase counter by 1
    count += 1
    result[student.gender] = count
}

let femaleCount = genderCount[.female]! // Output: 4
let maleCount = genderCount[.male]!     // Output: 2

此示例代码的作用是将students数组缩减为类型为 的字典[Gender: Int]。在闭包中,我们通过计算男女学生的出现次数来累积填充最终字典。

现在您已经了解了如何使用reduce(into:)函数计算出现次数,让我们通过为result字典提供如下默认值来进一步简化示例代码0

let genderCount = students.reduce(into: [Gender: Int]()) { result, student in
    result[student.gender, default: 0] += 1
}

let femaleCount = genderCount[.female]! // Output: 4
let maleCount = genderCount[.male]!     // Output: 2

这样,我们在保持代码简单和干净的同时避免了内存开销。

3. 获取数组的总和

接下来,我想向您展示如何仅用一行代码获得数组的总和。假设我们想要获得学生年龄的总和。为此,我们可以reduce(_:_:)像这样使用数组的函数:

let sum = students.reduce(0) { result, student in
    return result + student.age
}
// Output: 131

您可能已经猜到了,我们可以通过使用简写参数名称来进一步改进示例代码:

sum = student.reduce(0, { $0 + $1.age })

对于数组元素类型支持加法运算符 ( +) 的情况,我们可以通过省略简写参数名称来进一步简化它:

let sum1 = [2, 3, 4].reduce(0, +)          // Output: 9
let sum2 = [5.5, 10.7, 9.43].reduce(0, +)  // Output: 44.435
let sum3 = ["a","b","c"].reduce("", +)     // Output: "abc"

很酷,不是吗?

4. 通过 ID 访问数组元素

在处理数组时,我们需要执行的最常见操作之一是使用对象 ID 查找特定的数组元素。最直接的方法是使用for-in循环或数组的filter(_:)函数来循环遍历每个数组元素。

对于大多数情况,这两种方法都足够好。但是,它们都有O(n) 的时间复杂度,这意味着当数组变大时,它们将花费更多时间来查找特定元素。对于追求速度和响应能力的应用程序,这些方法肯定会造成性能瓶颈。

为了使其成为O(1) 复杂度的操作,我们可以将students数组转换为字典,其中键是学生 ID,值是Student对象。

为此,我们将首先利用数组的map(_:)函数将数组转换为包含学生 ID 和Student对象的元组数组。之后,我们将使用Dictionary(uniqueKeysWithValues:)初始化器将元组数组转换为字典。

// Transform [Student] --> [(String, Student)]
let studentsTuple = students.map { ($0.id, $0) }

// Transform [(String, Student)] --> [String: Student]
let studentsDictionary = Dictionary(uniqueKeysWithValues: studentsTuple)

// Read from dictionary (this is O(1) operation)
let emma = studentsDictionary["006"]!

请注意,将数组转换为字典的过程仍然是O(n) 操作。但是,我们只需要做一次。一旦字典准备好,对字典执行的任何读取操作都是O(1) 操作。

5. 从数组中获取一些随机元素

最后这个例子没有涉及任何高阶函数,但我认为还是值得分享的。从数组中获取大量随机元素曾经是一种难以实现的算法,因为我们需要处理各种边缘情况。

现在借助Swift 数组中的shuffled()prefix(_:)函数,这个操作变得非常容易实现。

以下是从students数组中随机选择三个学生的方法:

// Randomize all elements within the array
let randomized = students.shuffled()

// Get the first 3 elements in the array
let selected = randomized.prefix(3)

这种方法的一个好处是,即使我们试图获取的元素数量大于数组的总元素,它也不会触发“索引超出范围”异常。

包起来

上面显示的所有示例绝对可以通过使用传统for-in循环来解决。但是,这需要我们手动处理各种边缘情况。因此,它非常容易出错。

通过使用高阶函数,我们可以大大降低代码的复杂性,从而减少出错的可能性。最重要的是,它使我们的代码更易于维护。

文末推荐:iOS热门文集