美文网首页
数据结构和算法(五) - 树

数据结构和算法(五) - 树

作者: 冰三尺 | 来源:发表于2018-11-14 21:58 被阅读9次

    与链表一样,树由节点组成。每个节点包含一些数据并跟踪其子节点。

    父节点和子节点

    每个节点(最顶层的节点除外)都连接到它上面的一个节点。 该节点称为父节点。 直接在其下方并连接到它的节点称为其子节点。 在树上,每个子节点只有一个父节点。

    父节点与子节点

    根节点

    树中最顶层的节点称为树的根。 它是唯一没有父节点的节点

    根节点

    叶子

    如果节点没有子节点,则该节点是叶子

    节点的叶子

    实现

    创建一个树节点

    public class TreeNode<T> {
        
        public var value: T
        public var children: [TreeNode] = []
        
        public init(_ value: T) {
            self.value = value
        }
        
    }
    

    每个节点存储一个值,并使用数组保存对其所有子节点的引用。

    添加一个子节点

    public func add(_ child: TreeNode) {
            children.append(child)        
        }
    

    层次结构是树结构的自然候选者,定义了三个不同的节点并将它们组织成逻辑层次结构。

    let beverages = TreeNode("Root")
    
    let hot = TreeNode("node1")
    let cold = TreeNode("node2")
    
    beverages.add(hot)
    beverages.add(cold)
    beverages.children.forEach { (node) in
        print(node.value)  // node1,  node2
    }
    
    屏幕快照 2018-12-15 下午4.36.54.png

    遍历算法

    迭代线性集合(如数组或链表)非常简单。 线性集合有明确的开始和结束, 但是迭代树却不同, 哪边是开始?哪边又是结束? 从左边的叶子开始迭代还是从右边的叶子开始迭代?

    深度优先遍历(Depth-first traversal)

    对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次.

    举个例子, 下图是一个树, 使用DFS遍历, 从Root点出发(访问顺序不固定, 因为Root 下有两个N1, N2), 若从N1开始, 其顺序为Root->N1->A->a, 此时没有路了, 开始回溯到A, (A)->b, 此时也没有路了, 回溯到A, (A)->c, 至此得到的顺序为Root->N1->A->a->b->c, 依次类推, 最终的结果为Root->N1->A->a->b->c->B->C->N2->D->d->e->E

    屏幕快照 2018-12-16 下午1.42.56.png

    代码实现, 首先构造一下这个树

    func makeBeverageTree() -> TreeNode<String> {
        let tree = TreeNode("Root")
        
        let N1 = TreeNode("N1")
        let N2 = TreeNode("N2")
        
        let A = TreeNode("A")
        let B = TreeNode("B")
        let C = TreeNode("C")
        
        let a = TreeNode("a")
        let b = TreeNode("b")
        let c = TreeNode("c")
        
        let D = TreeNode("D")
        let E = TreeNode("E")
        
        let d = TreeNode("d")
        let e = TreeNode("e")
        
        tree.add(N1)
        tree.add(N2)
        
        N1.add(A)
        N1.add(B)
        N1.add(C)
        
        N2.add(D)
        N2.add(E)
        
        A.add(a)
        A.add(b)
        A.add(c)
        
        D.add(d)
        D.add(e)
        
        return tree
    }
    

    遍历实现

    // 1
     func forEachDepthFirst(node:TreeNode, visit: (TreeNode) -> Void) {
            // 2
            visit(node)
            // 3
            children.forEach { (nextNode) in
                // 4
                nextNode.forEachDepthFirst(node: nextNode, visit: { (n) in
                     //5 
                    visit(n)
                })
            }
        }
    
    
    1. forEachDepthFirst函数需要一个node参数, 当前的节点, visit是一个闭包, 该闭包需要一个TreeNode参数, 无返回值. 这个闭包的主要的作用就是为了返回找到的节点
    2. visit(node) 的参数是node, 代表当前的节点
    3. 这个是比较绕的地方, 首先forEach是一个同步函数, 这就说明遍历是有序的, 而深度优先遍历需要的就是顺着一个分支一直往下查找, 直到结束. 详细的解释再说.
    4. 调用自己, 进行递归操作
    5. 回调
    let tree = makeBeverageTree()
            tree.forEachDepthFirst(node: tree) {
                print($0.value)
            }
    

    要比较清楚的了解forEachDepthFirst的执行过程, 首先要了解递归, 说起递归, 鸡生蛋, 蛋生鸡的现象其实都是抽象出来的递归,但是严格来说并不是递归,因为会一直重复下去,没有终止条件,那就称为死循环了。可以把递归理解为一个无线的循环, 但是在某一个满足条件的时刻就会结束循环.

    用阶乘一个例子来讲解递归的执行流程

    一个正整数的阶乘factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。即n!=1×2×3×...×n

    代码实现

      func factorial(n:Int) -> Int
        {
            //递归边界
            if n == 0 {
                return 1
            }
            //递归公式
            return n * factorial(n: n - 1)
        }
    

    事实上,计算机在运行过程中都是用栈来存储程序中的函数的,在递归运算时,函数也不断地被调用进栈,当运行完毕后再依次出栈。栈是先进后出, factorial被执行的时候回一直被压入栈, 当满足条件 n==0时, 开始返回, 依次出栈.

    1. 当n=3时, 不满足条件, return 3 * factorial(n: 3 - 1), 此时 3 * factorial(n: 3 - 1)入栈, 位于栈底
    2. 在第一步中 factorial 又调用了自己, 此时n=2, 不满足条件, 再次return 2 * factorial(n: 2 - 1), 此时2 * factorial(n: 2 - 1)入栈
    3. factorial又掉了自己, 此时n=1, 不满足条件, 再次return 1 * factorial(n: 1 - 1), 此时1 * factorial(n: 1 - 1)入栈
    4. 再次调用自己 又被压入栈, 此时n = 0, 满足条件, return 1, 函数返回, 被压入栈的函数, 依次从栈顶弹出.

    这就是递归在栈中实现的执行和回退过程,回退过程的顺序是执行过程中的逆序,下一次调用都为上一次调用提供需要,这符合栈的特点,编译器就是利用这个特点来实现递归管理的。

    再来说说我们的forEachDepthFirst递归, 如果说我们的树只有一个分支, 如下图所示

    7AA3BB5D-3874-43D1-8D84-2A6AD25FF271.png
    只考虑划线的部分, 这正好是一个栈的结构, 经过forEachDepthFirst递归之后, 栈底到顶底依次是Root->N1->A->a(因为实现从Root开始执行, Root最先被压入栈), 然后a满足添加, a下面没有节点, 无路可走了, 开始从栈中弹出. 依次弹出a, A, N1, Root. 函数里面的children.forEach做的事情就是从Root开始, 把每一条路径下面的节点分别压入栈, 依次又弹出.
    广度优先遍历

    广度优先遍历是以层为顺序,将某一层上的所有节点都搜索到了之后才向下一层搜索

    其搜索结果为Root->N1->N2->A->B->C->D->E->a->b->c->d->e, 逐层的去遍历

    public func forEachLevelOrder(node:TreeNode, visit: (TreeNode) -> Void) {
            visit(node)
            var queue = Queue<TreeNode>()
            // N1, N2 进入队列
            children.forEach {
                queue.enqueue($0)
            }
        
            /**
             * 搜索的原理就是同一层的节点依次入队列, 入队之后, 依次出队, 每次出队一个节点的时候, 会把出队的节点下的子节点入队, 然后在依次出队, 入队, 这样就达到了一层一层的查找
               以我们的图示为例, 执行顺序为, 先入队N1, N2, 然后N1出队, 把N1下的子节点加入队列, 此时队列是N2, A,B,C
                然后N2出队, D, E 入队, 此时队列是A,B,C,D,E, 依次类推
             */
            while let node = queue.dequeue() {
                visit(node)
                node.children.forEach {
                    queue.enqueue($0)
                }
            }
        }
    

    使用一下广度和深度遍历

    extension TreeNode where T: Equatable {
        public func search(_ value: T, searchNode:TreeNode) -> TreeNode? {
            var result: TreeNode?
            forEachDepthFirst(node: searchNode) { (n) in
                if n.value == value {
                    result = n
                }
            }
            return result
        }
    }
    
           if let node = tree.search("a", searchNode: tree) {
                print(node.value)
            }else {
                print("not found")
            }
    

    如果找到了我们要搜索的节点返回搜索的节点, 如果找不到, 则返回nil. 讲真, 这个深度和广度还真的绕, 需要好好屡屡, 断点打起来, debug一下.

    相关文章

      网友评论

          本文标题:数据结构和算法(五) - 树

          本文链接:https://www.haomeiwen.com/subject/ukpsfqtx.html