关于了解算法和<的主题存在无限的讨论前端的 href="https://dzone.com/articles/the-right-data-struct-at-the-right-time-and-place">数据结构(在我的例子中,移动端)开发人员只需要通过大型科技公司的技术面试,或者在日常工作中使用它有一些好处。我认为事实一如既往,介于两者之间。当然,在为具有 REST API,但是对性能、时间和内存复杂性有基本的了解可以帮助在应用程序中进行小而简单的优化,从长远来看可以带来很大的回报。
我想举一个这样的小优化和决策过程的例子,它可以帮助我们决定额外的努力是否值得。
示例
我正在为我的孩子开发一个简单的 iOS 应用程序,该应用程序应该可以帮助他们学习外语。基本功能之一是词汇,您可以在其中添加您想要学习的单词。我想为每个单词添加一些图像以直观地表示它。然而,到 2024 年,最好的方法可能是使用任何图像生成模型的 API,但这对于我想要制作的简单应用程序来说太多了,所以我决定使用表情符号。有超过 1,000 个可用表情符号,孩子们可以尝试学习的大多数简单单词或短语都会有视觉表示。
这里是一个代码示例,用于获取大部分表情符号并仅过滤掉那些可以正确呈现的表情符号。
var emojis: [字符: 字符串] = [:]
让范围= [
0x1F600...0x1F64F,
9100...9300,
0x2600...0x26FF,
0x2700...0x27BF,
0x1F300...0x1F5FF,
0x1F680...0x1F6FF,
0x1F900...0x1F9FF
]
对于范围中的范围{
对于范围 { 中的项目
警卫
让标量 = UnicodeScalar(项目),
scalar.properties.isEmojiPresentation
别的 {
继续
}
让值=字符(标量)
表情符号[值] = 描述(表情符号: 值)
}
}
对于每个表情符号字符,我们还存储其描述,我们将使用它来找到适合我们的单词或短语的描述。以下是一些示例:
现在让我们考虑如何以最直接、最简单的方式为给定的单词或短语找到正确的表情符号。不幸的是,字符串比较在这里不起作用,因为并非所有表情符号都包含单个单词作为描述,其次是用户可以使用该单词甚至短语的不同版本。幸运的是,Apple 为我们提供了内置的 NaturalLanguage
框架,可以帮助我们。我们将利用它的句子嵌入功能来测量用户给定单词/短语与我们存储的表情符号描述之间的距离。
这是一个它的函数:
funccalculateDistance(text: String, items: [Character]) -> (String?, Double?) {
Guard let embedding = NLEmbedding.sentenceEmbedding(for: .english) else {
返回(零,零)
}
var minDistance: Double = Double.greatestFiniteMagnitude
var 表情符号=“”
对于项目中的关键项{
Guard let value = emojis[key] else { 继续 }
让距离=嵌入.距离(
之间:text.lowercased(),
和: value.lowercased()
)
如果距离 < 最小距离 {
最小距离 = 距离
表情符号=字符串(键)
}
}
返回(表情符号,minDistance)
}
这里的算法很简单:我们遍历所有表情符号字符,获取描述并将其与给定文本进行比较,保存找到的最小距离,最后将与我们的距离最小的表情符号返回文本与距离本身一起进行进一步过滤。该算法的线性时间复杂度为 O(n)。以下是一些结果示例:
最后一个不是我所期望的笑脸,但它是微笑的,所以它有效。我们还可以使用返回的距离来过滤掉一些东西。距离的值在 0 到 2 之间(默认)。通过进行一些实验,我发现 0.85 是一个很好的过滤点,可以过滤所有不代表给定表情符号中短语含义的内容。小于 0.85 的所有内容看起来都不错,大于此的所有内容,我都会过滤掉并返回一个空字符串,以免让用户感到困惑。
我们有算法的第一个版本,虽然它可以工作,但速度相当慢。为了找到任何请求的匹配项,它需要遍历每个表情符号并单独为每个描述执行距离测量。对于用户的每个请求,此过程大约需要 3.8 秒。
现在我们需要做出一个重要的决定:是否投入时间进行优化。为了回答这个问题,让我们思考一下我们到底想通过优化这个额外的工作来改进什么。尽管 3.8 秒的 emoji 生成时间看起来有些难以接受,但我仍然会以此为例,挑战一下这次优化的目的。我的用例如下:
- 用户打开词汇表并想要添加新单词或短语。
- 用户输入这个词。
- 输入完成后,我会对翻译 API 进行网络调用,以获取单词的翻译。
- 理想情况下,我希望这个表情符号在输入完成的同时出现,但我可以承受不超过翻译 API 调用所需时间的延迟,并在我输入的同时显示它收到翻译了。
当我将此行为视为一项要求时,很明显 3.8 秒对于网络调用来说太长了。我想说,如果需要 0.3-0.5 秒,我可能不会在这里优化,因为我不想牺牲用户体验。稍后,我可能需要重新审视这个主题并改进它,但就目前而言,提供一个可用的产品比不提供完美的代码要好。就我而言,我必须优化,所以让我们考虑一下如何去做。
我们已经在这里使用字典,其中表情符号是键,描述是值。我们将添加一个带有交换键和值的附加字典。此外,我会将每个描述分成单独的单词,并使用这些单词作为键。对于值,我将使用与描述中的每个单词相对应的表情符号列表。为了提高效率,我将为表情符号创建一个索引,它可以帮助我在几乎恒定的时间内找到给定单词最相关的描述。这种方法的主要缺点是它只适用于单个单词,而不适用于短语。根据我的目标用户的说法,他们通常会搜索单个单词。因此,我将使用此索引进行单字搜索,并保留罕见短语的旧方法,这些短语在大多数情况下不会因找不到适当的表情符号解释而返回空符号。让我们看一下 Index 字典中的几个示例:
这是一个用于创建索引的函数:
var searchIndex: [字符串: [字符]] = [:]
...
函数准备索引() {
对于表情符号中的项目 {
让words = item.value.components(separatedBy: " ")
字中字{
var emojiItems: [字符] = []
让 lowercaseWord = word.lowercased()
如果让 items = searchIndex[lowercasedWord] {
emojiItems = 项目
}
emojiItems.append(item.key)
searchIndex[lowercasedWord] = emojiItems
}
}
}
现在,让我们为单个单词和短语添加另外两个函数。
func emoji(word: String) -> String {
Guard let options = searchIndex[word.lowercased()] else {
返回表情符号(文本:单词)
}
让结果=计算距离(文本:单词,项目:选项)
守卫让值=结果.0,让距离=结果.1否则{
返回表情符号(文本:单词)
}
返回距离 < singleWordAllowedDistance ?价值 : ””
}
func emoji(文本: 字符串) -> 字符串 {
让结果=计算距离(文本:文本,项目:数组(emojis.keys))
守卫让值=结果.0,让距离=结果.1否则{
返回 ””
}
返回距离 < 允许距离 ?价值 : ””
}
allowedDistance
和 singleWordAllowedDistance
是帮助我配置过滤的常量。
如您所见,我们使用与之前相同的距离计算,但我们不是注入所有表情符号,而是注入描述中包含给定单词的表情符号列表。在大多数情况下,这只是几个甚至只有一种选择。这使得算法在大多数情况下以接近恒定的时间运行。让我们测试一下并测量时间。更新后的算法可在 0.04 - 0.08 秒内给出结果,比以前快了约 50 倍。然而,有一个大问题:单词的拼写应该与描述中的完全一致。我们可以通过使用带有 Neighbors 函数的词嵌入来解决这个问题,该函数将为我们提供一系列与给定词相似或含义相近的词。这是更新的 func emoji(word: String) -> String
函数。
func emoji(word: String) -> String {
Guard let wordEmbedding = NLEmbedding.wordEmbedding(for: .english) else {
返回 ””
}
让neighbors = wordEmbedding.neighbors(for: word, MaximumCount: 2).map({ $0.0 })
让单词 = [单词] + 邻居
字中字{
Guard let options = searchIndex[word.lowercased()] else {
继续
}
让结果=计算距离(文本:单词,项目:选项)
守卫让值=结果.0,让距离=结果.1否则{
返回表情符号(文本:单词)
}
返回距离 < singleWordAllowedDistance ?价值 : ””
}
返回表情符号(文本:单词)
}
现在,在大多数情况下,它的工作速度非常快。
结论
了解基本算法和数据结构可以扩展您的工具集,并帮助您找到代码中可以优化的区域。尤其是在处理具有许多开发人员和应用程序中的大量模块的大型项目时,到处进行优化将有助于应用程序随着时间的推移运行得更快。