Swift 中的递归函数在使用大量输入调用时崩溃
Recursive function in Swift crashes when called with a high amount of input
我有来自 API 的评论,这些评论要么是根评论,要么是子评论。这些评论来自 API,我必须对它们进行排序。
一条评论是这样的:
struct Comment: Codable, Equatable {
var depth = 0
var id: Int = 0
var parent: Int?
var content: String = ""
var created: Int = 0
var up: Int = 0
var down: Int = 0
var confidence: Double = 0
var name: String = ""
var mark: Int = 0
enum CodingKeys: String, CodingKey {
case id = "id"
case parent = "parent"
case content = "content"
case created = "created"
case up = "up"
case down = "down"
case confidence = "confidence"
case name = "name"
case mark = "mark"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(Int.self, forKey: .id)
parent = try values.decodeIfPresent(Int.self, forKey: .parent)
content = try values.decode(String.self, forKey: .content)
created = try values.decode(Int.self, forKey: .created)
up = try values.decode(Int.self, forKey: .up)
down = try values.decode(Int.self, forKey: .down)
confidence = try values.decode(Double.self, forKey: .confidence)
name = try values.decode(String.self, forKey: .name)
mark = try values.decode(Int.self, forKey: .mark)
}
init(with message: String, name: String, depth: Int) {
self.depth = depth
self.up = 1
self.content = message
self.name = name
}
}
所以我开始制作树结构,并将这些注释放入节点中:
fileprivate class Node<T> {
var value: T
weak var parent: Node?
var children: [Node] = []
init(value: T) {
self.value = value
}
func add(child: Node) {
children.append(child)
child.parent = self
}
}
extension Node where T == Comment {
func search(id: Int) -> Node? {
if id == self.value.id {
return self
}
for child in children {
if let found = child.search(id: id) {
return found
}
}
return nil
}
var description: String {
"Id: \(value.id), parent: \(value.parent ?? -1)"
}
}
extension Node where T: Equatable {
func search(value: T) -> Node? {
if value == self.value {
return self
}
for child in children {
if let found = child.search(value: value) {
return found
}
}
return nil
}
}
所以收到的评论被分成两组,如下所示:
private func split(_ comments: [Comment]) {
let parentNodes = comments.filter { [=13=].parent == 0 }.map { Node(value: [=13=]) }
let childNodes = comments.filter { [=13=].parent != 0 }.map { Node(value: [=13=]) }
self.sortComments(parentNodes: parentNodes, childNodes: childNodes)
}
然后我用这个递归函数对评论进行排序:
private func sortComments(parentNodes: [Node<Comment>], childNodes: [Node<Comment>]) {
let parentNodes = parentNodes
var childNodes = childNodes
if let firstChild = childNodes.first {
let parentId = firstChild.value.parent!
if let parentNode = parentNodes.first(where: { [=14=].value.id == parentId }) {
firstChild.value.depth = parentNode.value.depth + 1
parentNode.add(child: firstChild)
childNodes.removeFirst()
self.sortComments(parentNodes: parentNodes, childNodes: childNodes)
} else {
//Comment is child of child
//Search children for parent
//Search also parentNodes, they may have a child already that is the parent
//of the current child we are looking at
parentNodes.forEach {
if let foundNode = [=14=].search(id: parentId) {
firstChild.value.depth = foundNode.value.depth + 1
foundNode.add(child: firstChild)
childNodes.removeFirst()
self.sortComments(parentNodes: parentNodes, childNodes: childNodes)
}
}
childNodes.forEach {
if let foundNode = [=14=].search(id: parentId) {
firstChild.value.depth = foundNode.value.depth + 1
foundNode.add(child: firstChild)
childNodes.removeFirst()
self.sortComments(parentNodes: parentNodes, childNodes: childNodes)
}
}
}
} else {
let sortedNodes = parentNodes.sorted { [=14=].value.confidence > .value.confidence }
self.convertCommentNodesToArray(nodes: sortedNodes, currentArray: [])
}
}
最后我将节点转换为数组,以便我的 TableView 可以显示数据:
private func convertCommentNodesToArray(nodes: [Node<Comment>], currentArray: [Comment]) {
var nodes = nodes
var commentsArray = currentArray
if let firstNode = nodes.first {
commentsArray.append(firstNode.value)
if firstNode.children.count > 0 {
let remainingNodes = nodes.dropFirst()
let sortedChildren = firstNode.children.sorted { [=15=].value.confidence > .value.confidence }
convertCommentNodesToArray(nodes: sortedChildren + remainingNodes, currentArray: commentsArray)
} else {
nodes.removeFirst()
convertCommentNodesToArray(nodes: nodes, currentArray: commentsArray)
}
} else {
self.comments.value = commentsArray
}
}
这段代码在有大量评论的情况下工作得很好。但是在某些时候,当我面对成千上万条评论时,这个函数会因为 Whosebug 崩溃而崩溃。
所以我的问题是:我怎样才能让这个函数在性能上更好而不至于崩溃?我很高兴听到你的建议:)
您的最后一个函数中有几点可以针对 Stack Overflow 崩溃进行一些优化:
迭代与递归算法
不说内存优化,先看看这两种算法:
- 迭代方法可能看起来更丑陋且更难阅读,但它几乎没有对内存大小施加任何限制(您基本上拥有设备提供的内存)。
- 相反,递归方法通常更干净,但受堆栈大小的限制(堆栈大小总是比整个内存大小小得多)。
在深入研究惊人的递归方法之前,您应该真正注意要处理的输入的大小。如果它要处理超过几千个周期,那么你真的应该坚持迭代方法。
尽管您可能会使用各种内存优化,但如果您多次调用递归函数,您将导致堆栈溢出。所以这是要考虑的第一点。
值类型与引用类型
但是循环次数并不是唯一会导致堆栈溢出的因素。如果您在堆栈中放置了太多变量,也会发生这种情况。如果您考虑存储内容的位置,就可以解决这个问题。
值类型将始终分配在堆栈中,而引用类型通常分配在堆中。 (由于Stack Promotion,这并不总是正确的,但我想你现在可以忽略它)。
因此,对于初学者,您可以考虑将 Comment
结构实现为 class。
或者至少您可以尝试通过将其分配回(再次)进入堆栈的另一个局部变量来避免在每个循环中复制整个数组。
var commentsArray = currentArray
例如,对于上面的行,您不仅复制了数组(它是一个值类型),而且还复制了数组中的每个评论(自 Comments是值类型)。而且你在每个周期复制它们,使堆栈的大小呈指数增长。
为了避免重复注释,您可以简单地使用 class 代替(这样您只会重复它们的引用),但是如果您想避免复制数组你也可以将它作为 inout 参数传递.
节点数组也是如此(但至少 Node
是 class)。
最后,这是我会针对你的情况做的(但以上所有可能适用于其他情况):
- 发表评论 class
- 使用迭代算法(甚至不需要传递数组,因此无需为 inout 参数操心)
在您的案例中,迭代算法的最简单实现类似于:
private func convertCommentNodesToArray(nodes: [Node<Comment>]) {
var nodes = nodes
var commentsArray = [Comment]()
while(!nodes.isEmpty) {
// get the first node
// insert all children of that node to the nodes array in the position you prefer
// then take that node comment and add it to the array
// finally remove that node from the array
}
self.comments.value = commentsArray
}
我有来自 API 的评论,这些评论要么是根评论,要么是子评论。这些评论来自 API,我必须对它们进行排序。
一条评论是这样的:
struct Comment: Codable, Equatable {
var depth = 0
var id: Int = 0
var parent: Int?
var content: String = ""
var created: Int = 0
var up: Int = 0
var down: Int = 0
var confidence: Double = 0
var name: String = ""
var mark: Int = 0
enum CodingKeys: String, CodingKey {
case id = "id"
case parent = "parent"
case content = "content"
case created = "created"
case up = "up"
case down = "down"
case confidence = "confidence"
case name = "name"
case mark = "mark"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(Int.self, forKey: .id)
parent = try values.decodeIfPresent(Int.self, forKey: .parent)
content = try values.decode(String.self, forKey: .content)
created = try values.decode(Int.self, forKey: .created)
up = try values.decode(Int.self, forKey: .up)
down = try values.decode(Int.self, forKey: .down)
confidence = try values.decode(Double.self, forKey: .confidence)
name = try values.decode(String.self, forKey: .name)
mark = try values.decode(Int.self, forKey: .mark)
}
init(with message: String, name: String, depth: Int) {
self.depth = depth
self.up = 1
self.content = message
self.name = name
}
}
所以我开始制作树结构,并将这些注释放入节点中:
fileprivate class Node<T> {
var value: T
weak var parent: Node?
var children: [Node] = []
init(value: T) {
self.value = value
}
func add(child: Node) {
children.append(child)
child.parent = self
}
}
extension Node where T == Comment {
func search(id: Int) -> Node? {
if id == self.value.id {
return self
}
for child in children {
if let found = child.search(id: id) {
return found
}
}
return nil
}
var description: String {
"Id: \(value.id), parent: \(value.parent ?? -1)"
}
}
extension Node where T: Equatable {
func search(value: T) -> Node? {
if value == self.value {
return self
}
for child in children {
if let found = child.search(value: value) {
return found
}
}
return nil
}
}
所以收到的评论被分成两组,如下所示:
private func split(_ comments: [Comment]) {
let parentNodes = comments.filter { [=13=].parent == 0 }.map { Node(value: [=13=]) }
let childNodes = comments.filter { [=13=].parent != 0 }.map { Node(value: [=13=]) }
self.sortComments(parentNodes: parentNodes, childNodes: childNodes)
}
然后我用这个递归函数对评论进行排序:
private func sortComments(parentNodes: [Node<Comment>], childNodes: [Node<Comment>]) {
let parentNodes = parentNodes
var childNodes = childNodes
if let firstChild = childNodes.first {
let parentId = firstChild.value.parent!
if let parentNode = parentNodes.first(where: { [=14=].value.id == parentId }) {
firstChild.value.depth = parentNode.value.depth + 1
parentNode.add(child: firstChild)
childNodes.removeFirst()
self.sortComments(parentNodes: parentNodes, childNodes: childNodes)
} else {
//Comment is child of child
//Search children for parent
//Search also parentNodes, they may have a child already that is the parent
//of the current child we are looking at
parentNodes.forEach {
if let foundNode = [=14=].search(id: parentId) {
firstChild.value.depth = foundNode.value.depth + 1
foundNode.add(child: firstChild)
childNodes.removeFirst()
self.sortComments(parentNodes: parentNodes, childNodes: childNodes)
}
}
childNodes.forEach {
if let foundNode = [=14=].search(id: parentId) {
firstChild.value.depth = foundNode.value.depth + 1
foundNode.add(child: firstChild)
childNodes.removeFirst()
self.sortComments(parentNodes: parentNodes, childNodes: childNodes)
}
}
}
} else {
let sortedNodes = parentNodes.sorted { [=14=].value.confidence > .value.confidence }
self.convertCommentNodesToArray(nodes: sortedNodes, currentArray: [])
}
}
最后我将节点转换为数组,以便我的 TableView 可以显示数据:
private func convertCommentNodesToArray(nodes: [Node<Comment>], currentArray: [Comment]) {
var nodes = nodes
var commentsArray = currentArray
if let firstNode = nodes.first {
commentsArray.append(firstNode.value)
if firstNode.children.count > 0 {
let remainingNodes = nodes.dropFirst()
let sortedChildren = firstNode.children.sorted { [=15=].value.confidence > .value.confidence }
convertCommentNodesToArray(nodes: sortedChildren + remainingNodes, currentArray: commentsArray)
} else {
nodes.removeFirst()
convertCommentNodesToArray(nodes: nodes, currentArray: commentsArray)
}
} else {
self.comments.value = commentsArray
}
}
这段代码在有大量评论的情况下工作得很好。但是在某些时候,当我面对成千上万条评论时,这个函数会因为 Whosebug 崩溃而崩溃。
所以我的问题是:我怎样才能让这个函数在性能上更好而不至于崩溃?我很高兴听到你的建议:)
您的最后一个函数中有几点可以针对 Stack Overflow 崩溃进行一些优化:
迭代与递归算法
不说内存优化,先看看这两种算法:
- 迭代方法可能看起来更丑陋且更难阅读,但它几乎没有对内存大小施加任何限制(您基本上拥有设备提供的内存)。
- 相反,递归方法通常更干净,但受堆栈大小的限制(堆栈大小总是比整个内存大小小得多)。
在深入研究惊人的递归方法之前,您应该真正注意要处理的输入的大小。如果它要处理超过几千个周期,那么你真的应该坚持迭代方法。
尽管您可能会使用各种内存优化,但如果您多次调用递归函数,您将导致堆栈溢出。所以这是要考虑的第一点。
值类型与引用类型
但是循环次数并不是唯一会导致堆栈溢出的因素。如果您在堆栈中放置了太多变量,也会发生这种情况。如果您考虑存储内容的位置,就可以解决这个问题。
值类型将始终分配在堆栈中,而引用类型通常分配在堆中。 (由于Stack Promotion,这并不总是正确的,但我想你现在可以忽略它)。
因此,对于初学者,您可以考虑将 Comment
结构实现为 class。
或者至少您可以尝试通过将其分配回(再次)进入堆栈的另一个局部变量来避免在每个循环中复制整个数组。
var commentsArray = currentArray
例如,对于上面的行,您不仅复制了数组(它是一个值类型),而且还复制了数组中的每个评论(自 Comments是值类型)。而且你在每个周期复制它们,使堆栈的大小呈指数增长。
为了避免重复注释,您可以简单地使用 class 代替(这样您只会重复它们的引用),但是如果您想避免复制数组你也可以将它作为 inout 参数传递.
节点数组也是如此(但至少 Node
是 class)。
最后,这是我会针对你的情况做的(但以上所有可能适用于其他情况):
- 发表评论 class
- 使用迭代算法(甚至不需要传递数组,因此无需为 inout 参数操心)
在您的案例中,迭代算法的最简单实现类似于:
private func convertCommentNodesToArray(nodes: [Node<Comment>]) {
var nodes = nodes
var commentsArray = [Comment]()
while(!nodes.isEmpty) {
// get the first node
// insert all children of that node to the nodes array in the position you prefer
// then take that node comment and add it to the array
// finally remove that node from the array
}
self.comments.value = commentsArray
}