Swift | Data Structures & Algorithms 2

Tree

It is used to tackle many recurring challenges in software development, such as:

  • representing hierarchical relationships

  • managing sorted data

  • facilitating fast lookup operations

  • Node
    Like the linked list, trees are made up of nodes.

class TreeNode<T> {
    var value: T
    var children: [TreeNode] = []
    
    init(value: T) {
        self.value = value
    }
    
    func add(child: TreeNode<T>) {
        children.append(child)
    }
}
  • Parent & Child
    Every child has only one parent.

  • Root
    The topmost node in the tree is called the root of the tree. It is the only node that has no parent.

  • Leaf
    A node is a leaf if it has no children

Traversal algorithms

Iterating through linear collections such as arrays or linked lists is straightforward. Linear collections have a clear start and end, iterating through trees is a bit more complicated.

Should nodes on the left have precedence? How should the depth of a node relate to its precedence? Your traversal strategy depends on the problem you’re trying to solve.

  • Depth-first traversal
func forEachDepthFirst(visit: (TreeNode)->Void) {
        visit(self)
        children.forEach{ $0.forEachDepthFirst(visit: visit) }
    }
  • Level-order traversal
func forEachLevelOrder(visit: (TreeNode)->Void) {
        visit(self)
        var queue = ArrayQueue<TreeNode>()
        children.forEach{ queue.enqueue(element: $0) }
        while let node = queue.dequeue() {
            visit(node)
            node.children.forEach{ queue.enqueue(element: $0) }
        }
    }
  • Search
    underlying Level-order, Depth-first traversal
func searchLevelOrder(value: T) -> TreeNode? {
        var result: TreeNode?
        forEachLevelOrder { node in
            if node.value == value {
                result = node
            }
        }
        return result
    }
    
    func searchDepthFirst(value: T) -> TreeNode? {
        var result: TreeNode?
        forEachDepthFirst { node in
            if node.value == value {
                result = node
            }
        }
        return result
    }

If there are multiple matches, the last match will win.

Binary Tree

A binary tree is a tree where each node has at most two children, often referred to as the left and right children.

class BinaryNode<T> {
    var value: T
    var leftChild: BinaryNode?
    var rightChild: BinaryNode?
    init(value: T) {
        self.value = value
    }
}

you’ll look at three traversal algorithms for binary trees: in-order, pre-order, and post-order traversals.

Pre-order traversal 前序

Pre-order traversal always visits the current node first, then recursively visits the left and right child.

func traversePreOrder(visit: (T)->Void) {
        visit(value)
        leftChild?.traversePreOrder(visit: visit)
        rightChild?.traversePreOrder(visit: visit)
    }

In-order traversal 中序

  • If the current node has a left child, recursively visit this child first.
  • Then visit the node itself.
  • If the current node has a right child, recursively visit this child.

This prints the tree in ascending order.

func traverseInOrder(visit: (T)->Void) {
        leftChild?.traverseInOrder(visit: visit)
        visit(value)
        rightChild?.traverseInOrder(visit: visit)
    }

Post-order traversal 后序

Post-order traversal only visits the current node after the left and right child have been visited recursively.

func traversePostOrder(visit: (T)->Void) {
        leftChild?.traversePostOrder(visit: visit)
        rightChild?.traversePostOrder(visit: visit)
        visit(value)
    }

In other words, given any node, you’ll visit its children before visiting itself. An interesting consequence of this is that the root node is always visited last.

Binary Search Tree (BST)

A binary search tree (or BST) is a data structure that facilitates fast lookup, addition, and removal operations.
Each operation has an average time complexity of O(log n), which is considerably faster than linear data structures such as arrays and linked lists.

A binary search tree achieves this performance by imposing two rules on the binary tree:

  • The value of a left child must be less than the value of its parent.
  • The value of a right child must be greater than or equal to the value of its parent.

Picking a side forfeits all the possibilities of the other side. Binary search trees use this property to save you from performing unnecessary checking.

Lookup

Every time the search algorithm visits a node in the BST, it can safely make these two assumptions:

  • If the search value is less than the current value, it must be in the left subtree.
  • If the search value value is greater than the current value, it must be in the right subtree.

By leveraging the rules of the BST, you can avoid unnecessary checks and cut the search space in half every time you make a decision. That’s why element lookup in a BST is an O(log n) operation.

Insertion

You only needed to make three traversals to find the location for the insertion, and you didn’t have to shuffle all the elements around! Inserting elements in a BST is again an O(log n) operation.

Removal

There are complications to manage when the node you’re removing has children.Even with those complications, removing an element from a BST is still an O(log n) operation.

Binary search trees drastically reduce the number of steps for add, remove, and lookup operations.

Inserting elements

In accordance with the rules of the BST, nodes of the left child must contain values less than the current node. Nodes of the right child must contain values greater than or equal to the current node.

struct BinarySearchTree<T: Comparable> {
    private(set) var root: BinaryNode<T>?
    init(){}
}

extension BinarySearchTree {
    mutating func insert(value: T) {
        root = _insert(from: root, value: value)
    }
    
    private mutating func _insert(from node: BinaryNode<T>?, value: T) -> BinaryNode<T> {
        guard let node = node else {
            return .init(value: value)
        }
        if value < node.value {
            node.leftChild = _insert(from: node.leftChild, value: value)
        }
        else {
            node.rightChild = _insert(from: node.rightChild, value: value)
        }
        return node
    }
}

extension BinarySearchTree: CustomStringConvertible {
    var description: String {
        root?.description ?? "empty tree"
    }
}

var bst = BinarySearchTree<Int>()
[7,1,9,0,5,8].forEach{ bst.insert(value: $0) }
print(bst)

An unbalanced tree affects performance. If you insert 5 into the unbalanced tree you’ve created, it becomes an O(n) operation.

Finding elements

Implement with traversal algorithm:

func contains(_ value: T) -> Bool {
        var found = false
        root?.traverseInOrder{
            if value == $0 {
                found = true
            }
        }
        return found
    }

In-order traversal has a time complexity of O(n), thus this implementation of contains has the same time complexity as an exhaustive search through an unsorted array.

But you can rely on the rules of the BST to avoid needless comparisons:

func contains(_ value: T) -> Bool {
        var found = root
        while let node = found {
            if value == node.value {
                return true
            }
            if value < node.value {
                found = node.leftChild
            } else {
                found = node.rightChild
            }
        }
        return false
    }

Here’s how this works:

  1. Start off by setting current to the root node.
  2. While current is not nil, check the current node’s value.
  3. If the value is equal to what you’re trying to find, return true.
  4. Otherwise, decide whether you’re going to check the left or the
    right child.

In a balanced binary search tree, this implementation of contains is
an O(log n) operation.

Removing elements

  • Removing a leaf node is straightforward, Simply detaching the leaf node is enough.

  • When removing nodes with one child, you’ll need to reconnect that
    one child with the rest of the tree.

  • When removing a node with two children, replace the node you removed with smallest node in its right subtree. Based on the rules of the BST, this is the leftmost node of the right subtree.

It’s important to note that this produces a valid binary search tree. Because the new node was the smallest node in the right subtree, all nodes in the right subtree will still be greater than or equal to the new node. And because the new node came from the right subtree, all nodes in the left subtree will be less than the new node.

After performing the swap, you can simply remove the value you copied, which is just a leaf node.

extension BinaryNode {
    var min: BinaryNode {
        leftChild?.min ?? self
    }
    var isLeaf: Bool {
        leftChild == nil && rightChild == nil
    }
}

    mutating func remove(_ value: T) {
        root = _remove(from: root, value: value)
    }
    
    private mutating func _remove(from node: BinaryNode<T>?, value: T) -> BinaryNode<T>? {
        guard let node = node else {
            return nil
        }
        // if found the node of `value`, handle removal
        if value == node.value {
            if node.isLeaf {
                return nil
            }
            if node.leftChild == nil {
                return node.rightChild
            }
            if node.rightChild == nil {
                return node.leftChild
            }
            // 1. swap the value with the min value of node in right subtree
            node.value = node.rightChild!.min.value
            // 2. remove the node with min value in right subtree
            node.rightChild = _remove(from: node.rightChild, value: node.value)
        }
        // else continue to find the left or right child's children nodes
        else if value < node.value {
            node.leftChild = _remove(from: node.leftChild, value: value)
        } else {
            node.rightChild = _remove(from: node.rightChild, value: value)
        }
        return node
    }

var bst = BinarySearchTree<Int>()
[3,1,5,4,7,6,8].forEach{ bst.insert(value: $0) }
print(bst)
print(bst.remove(5))
print(bst)

The BST is a powerful data structure that can delivers great performance when managing sorted data.

AVL Tree

In 1962, Georgy Adelson-Velsky and Evgenii Landis came up with the first self-balancing binary search tree: the AVL Tree.

The three main states of balance:

  • Perfect balance
    The ideal form of a binary search tree is the perfectly balanced state. In technical terms, this means every level of the tree is filled with nodes, from top to bottom.

Not only is the tree perfectly symmetrical, the nodes at the bottom level are completely filled. This is the requirement for being perfectly balanced.

  • "Good-enough" balance
    A perfectly balanced tree has to contain the exact number of nodes to fill every level to the bottom, so it can only be perfect with a particular number of elements.

As an example, a tree with 1, 3, or 7 nodes can be perfectly balanced, but a tree with 2, 4, 5, or 6 cannot be perfectly balanced, since the last level of the tree will not be filled.

  • Unbalanced
    Binary search trees in this state suffer from various levels of performance loss, depending on the degree of imbalance.

AVL trees maintain balance by adjusting the structure of the tree when the tree becomes unbalanced.

Binary search trees and AVL trees share much of the same implementation;

Measuring balance
This is a balanced tree, where all levels except the bottom one are filled.

A balanceFactor of 2 or -2 is an indication of an unbalanced tree.

The AVL tree achieves this with a height property in each node. In tree-speak, the height of a node is the longest distance from the current node to a leaf node.

You’ll use the relative heights of a node’s children to determine
whether a particular node is balanced.

The height of the left and right children of each node must differ by at most 1. This is known as the balance factor.

The balanceFactor computes the height difference of the left and right child. If a particular child is nil, its height is considered to be -1.

Although more than one node may have a bad balancing factor, you only need to perform the balancing procedure on the bottom-most node containing the invalid balance factor.

Rotations

The procedures used to balance a binary search tree are known as rotations. There are four rotations in total, for the four different ways that a tree can become unbalanced. These are known as left rotation, left-right rotation, right rotation, and right-left rotation.

  • Left rotation

When a series of right children is causing an imbalance, it’s time for a left rotation.

func leftRotate(node: AVLNode<T>) -> AVLNode<T> {
        let pivot = node.rightChild!
        node.rightChild = pivot.leftChild
        pivot.leftChild = node
        node.height = max(node.leftHeight, node.rightHeight) + 1
        pivot.height = max(pivot.leftHeight, pivot.rightHeight) + 1
        return pivot
    }
  1. The right child is chosen as the pivot. This node will replace the
    rotated node as the root of the subtree (it will move up a level).
  2. The node to be rotated will become the left child of the pivot (it moves down a level). This means the current left child of the
    pivot must be moved elsewhere.
    In the generic example shown in the earlier image, this is node b.
    Because b is smaller than y but greater than x, it can replace y as
    the right child of x. So you update the rotated node’s rightChild
    to the pivot’s leftChild.
  3. The pivot’s leftChild can now be set to the rotated node.
  4. You update the heights of the rotated node and the pivot.
  5. Finally, you return the pivot so it can replace the rotated node in
    the tree.
  • Right rotation

Right rotation is the symmetrical opposite of left rotation. When a series of left children is causing an imbalance, it’s time for a right rotation.

func rightRotate(node: AVLNode<T>) -> AVLNode<T> {
        let pivot = node.leftChild!
        node.leftChild = pivot.rightChild
        pivot.rightChild = node // the key step
        node.height = max(node.leftHeight, node.rightHeight) + 1 // level up
        pivot.height = max(pivot.leftHeight, pivot.rightHeight) + 1 // level down
        return pivot
    }
  • Right-left rotation

You may have noticed that the left and right rotations balance nodes that are all left children or all right children.

Doing a left rotation in this case won’t result in a balanced tree. The way to handle cases like this is to perform a right rotation on the right child before doing the left rotation.

func rightLeftRotate(node: AVLNode<T>) -> AVLNode<T> {
        guard let rightChild = node.rightChild else {
            return node
        }
        node.rightChild = rightRotate(node: rightChild)
        return leftRotate(node: node)
    }
  • Left-right rotation
func leftRightRotate(node: AVLNode<T>) -> AVLNode<T> {
        guard let leftChild = node.leftChild else {
            return node
        }
        node.leftChild = leftRotate(node: leftChild)
        return rightRotate(node: node)
    }

Balance
Design a method that uses balanceFactor to decide whether a node requires balancing or not.

func balanced(for node: AVLNode<T>) -> AVLNode<T> {
        switch node.balanceFactor {
        case 2: // lean to `right`, should rotate to `right` or make `left-right` rotation
            if let leftChild = node.leftChild, leftChild.balanceFactor == -1 { // only left child
                return leftRightRotate(node: node)
            } else {
                return rightRotate(node: node)
            }
            
        case -2: // lean to `left`, should rotate to `left` or make `right-left` rotation
            if let rightChild = node.rightChild, rightChild.balanceFactor == 1 { // only right child
                return rightLeftRotate(node: node)
            } else {
                return leftRotate(node: node)
            }
            
        default:
            return node
        }
    }
  1. A balanceFactor of 2 suggests that the left child is “heavier”
    (that is, contains more nodes) than the right child. This means
    you want to use either right or left-right rotations.
  2. A balanceFactor of -2 suggests that the right child is heavier
    than the left child. This means you want to use either left or
    right-left rotations.
  3. The default case suggests that the particular node is balanced.
    There’s nothing to do here except to return the node.

The balanceFactor to determine the proper course of action.

Revisiting insert

private mutating func _insert(from node: AVLNode<T>?, value: T) -> AVLNode<T> {
        guard let node = node else {
            return .init(value: value)
        }
        if value < node.value {
            node.leftChild = _insert(from: node.leftChild, value: value)
        }
        else {
            node.rightChild = _insert(from: node.rightChild, value: value)
        }
        let balancedNode = balanced(for: node)
        balancedNode.height = max(balancedNode.leftHeight, balancedNode.rightHeight)
        return balancedNode
    }

Instead of returning the node directly after inserting, you pass it into balanced. This ensures every node in the call stack is checked for balancing issues. You also update the node’s height.

Revisiting remove

Just rebalance the root node before remove.

let balancedNode = balanced(node)
balancedNode.height = max(balancedNode.leftHeight, balanced
return balancedNode

The self-balancing property guarantees that the insert and remove operations function at optimal performance with an O(log n) time complexity.

While AVL trees were the first self-balancing implementations of a BST, others, such as the red-black tree and splay tree(伸展樹), have since joined the party. If you’re interested, you check those out in the Swift Algorithm Club. Find them at at:

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容