Reputation: 2383
I've finally gotten around to learning about linked lists and all that. I understand that in a garbage collected environment, when a circular linked list head is dereferenced, the entire list is eventually GC'd.
I have quite a lot of experience with Objective-C and Swift which all use automatic reference counting instead of GC. As I understand it, this will create a strong reference cycle (as explained by Apple's ARC docs) since the head node will hold a strong reference to the back, which also holds a strong reference to the head. I've encountered strong retain cycles by accidentally doing exactly this.
This, in my mind, creates a major issue because it makes a circular linked list entirely unreleasable since under ARC, at least in Objective C and Swift, you cannot manually release objects.
Is the solution here to just to keep an extra weak reference property on each node for the "front" and "back" reference as they cannot use the common next
reference (as these would need to be strong or else nodes in the middle of the list would be deallocated)? Or should I be pseudo-manually deallocating my list by breaking all references in the list before I remove the final reference to the head? All of these seem less than beautiful and more hacks than solutions.
Upvotes: 1
Views: 344
Reputation: 240
Indeed, having a circular Linked List, where you got last node strongly referencing first node will result in first node always having at least 1 reference, and hence the list will always remain in the memory.
Though ARC Wikipedia article suggests, that sometimes such cycles can be ignored, in this case it is unacceptable, because as a data structure designers, we do not control the size of the linked list — it can and should be able to grow endlessly by definition.
My solution is to let the nodes know about their container (i.e. weakly reference the list itself). This way we could remove cyclic referencing, by making next
a computed property of a node. When the actual next node is set, we return it as next
. When it is nil
, we return container
's startNode
, and so we are having the API, that presents itself as a Circular Linked List, but under the hood its just a usual one.
Here's some code sample:
class CircularLinkedList<T> {
// MARK: - Internal structures
class Node {
let value: T
private var _next: Node?
weak fileprivate var container: CircularLinkedList<T>!
var next: Node! {
get {
if let next = _next {
return next
} else {
return container.startNode
}
}
set {
_next = newValue
}
}
init(value: T) {
self.value = value
}
}
// MARK: - Properties
var startNode: Node
var endNode: Node
// MARK: - Constructors
init(initialValue: T) {
startNode = Node(value: initialValue)
endNode = startNode
startNode.container = self
}
// MARK: - API
func append(newValue: T) {
let newNode = Node(value: newValue)
newNode.container = self
endNode.next = newNode
endNode = newNode
}
}
One might say, that letting the Node
know about its container list is not a good idea architecture-wise. But making it fileprivate
we're hiding it from the outside world, and inside the data structure we know that we should use it properly. It seems like a better solution than manually breaking the reference cycle during the list's lifetime.
Upvotes: 2