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阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容