|
| 1 | +> 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [cloud.tencent.com](https://cloud.tencent.com/developer/article/1513280) |
| 2 | +
|
| 3 | +> 在 Android 下,UI 的布局结构,对标到数据结构中,本质就是一个由 View 和 ViewGroup 组成的多叉树结构。其中 View 只能作为叶子节点... |
| 4 | +
|
| 5 | + |
| 6 | + |
| 7 | +一. 审题 |
| 8 | +----- |
| 9 | + |
| 10 | +**面试题:** |
| 11 | + |
| 12 | +**给定一个 RootView,打印其内 View Tree 的每个 View。** |
| 13 | + |
| 14 | +在 Android 下,UI 的布局结构,对标到数据结构中,本质就是一个由 View 和 ViewGroup 组成的多叉树结构。其中 View 只能作为叶子节点,而 ViewGroup 是可以存在子节点的。 |
| 15 | + |
| 16 | + |
| 17 | + |
| 18 | +上图就是一个典型的 ViewTree 的结构,而想要遍历这个 ViewTree,还需要用到两个 ViewGroup 的方法。 |
| 19 | + |
| 20 | +* `getChildCount()`:获取其子 View 的个数。 |
| 21 | +* `getChildAt(int)`:获取对应索引的子 View。 |
| 22 | + |
| 23 | +对于 View,无需过多处理,直接打印输出即可。而 ViewGroup,除了打印自身的这个节点之外,还需要打印其子节点。 |
| 24 | + |
| 25 | +二. 解题的三种实现 |
| 26 | +---------- |
| 27 | + |
| 28 | +### 2.1 递归实现 |
| 29 | + |
| 30 | +### |
| 31 | + |
| 32 | + |
| 33 | + |
| 34 | +**当一个大问题,可以被拆分成多个小问题,并且分解后的小问题,和大问题相比,只是数据规模不同,求解思路完全一致的问题,非常适合递归来实现。** |
| 35 | + |
| 36 | +``` |
| 37 | +fun recursionPrint(root: View) { |
| 38 | + printView(root) |
| 39 | + if (root is ViewGroup) { |
| 40 | + for (childIndex in 0 until root.childCount) { |
| 41 | + val childView = root.getChildAt(childIndex) |
| 42 | + recursionPrint(childView) |
| 43 | + } |
| 44 | + } |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +递归确实可以很清晰的实现功能,但是它有一个致命的问题,当递归深度过深的时候,会爆栈。反应在程序上,就是会抛出 `StackOverflowError`这个异常。 |
| 49 | + |
| 50 | +面试的时候,面试者解决问题的思路,使用了递归思想,通常都会很自然的问问 JVM 的栈帧,以及为什么会出现 StackOverflowError 异常。 |
| 51 | + |
| 52 | +当然这不是本文的重点,大家了解一下即可。 |
| 53 | + |
| 54 | +简单来说,每启动一个线程,JVM 都会为其分配一个 Java 栈,每调用一个方法,都会被封装成一个栈帧,进行**压栈**操作,当方法执行完成之后,又会执行**弹栈**操作。而每个栈帧中,当前调用的方法的一些局部变量、动态连接,以及返回地址等数据。 |
| 55 | + |
| 56 | +Java 栈和数据结构的栈结构一样,有两个操作,压栈(入栈)、弹栈(出栈),是一个先入后出(FILO)的结构。这一块的东西,延伸出来就比较多了,你可以简单的理解为调用方法就会压栈,方法执行完会弹栈。 |
| 57 | + |
| 58 | + |
| 59 | + |
| 60 | +每次方法的调用,执行压栈的操作,但是每个栈帧,都是要消耗内存的。一旦超过了限制,就会爆掉,抛出 StackOverflowError。 |
| 61 | + |
| 62 | +递归的代码确实清晰简单,但是问题不少。面试官也不担心面试者写递归代码,后续可以有一连串问题等着。 |
| 63 | + |
| 64 | +### 2.2 广度优先实现 |
| 65 | + |
| 66 | +前面也提到,这道题本质上就是数据结构中,多叉树的遍历。那最先想到的就是深度优先和广度优先两种遍历策略。 |
| 67 | + |
| 68 | +我们先来看看广度优先的实现 |
| 69 | + |
| 70 | +广度优先的过程,就是对每一层节点依次访问,访问完了再进入下一层。就是**按树的深度,一层层的遍历访问**。 |
| 71 | + |
| 72 | + |
| 73 | + |
| 74 | +ABCDEFGHI 就是上图这个多叉树,使用广度优先算法的遍历结果。 |
| 75 | + |
| 76 | +**广度优先非常适合用先入先出的队列来实现**,每次子 View 都入队尾,而从对头取新的 View 进行处理。 |
| 77 | + |
| 78 | + |
| 79 | + |
| 80 | +代码如下: |
| 81 | + |
| 82 | +``` |
| 83 | +fun breadthFirst(root :View){ |
| 84 | + val viewDeque = LinkedList<View>() |
| 85 | + var view = root |
| 86 | + viewDeque.push(view) |
| 87 | + while (!viewDeque.isEmpty()){ |
| 88 | + view = viewDeque.poll() |
| 89 | + printView(view) |
| 90 | + if(view is ViewGroup){ |
| 91 | + for(childIndex in 0 until view.childCount){ |
| 92 | + val childView = view.getChildAt(childIndex) |
| 93 | + viewDeque.addLast(childView) |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +这里直接利用 `LinkedList` 来实现队列,它本身就实现了双端队列 `Deque`接口。 |
| 101 | + |
| 102 | +### 2.3 深度优先实现 |
| 103 | + |
| 104 | +说完广度深度,再继续看看深度优先。 |
| 105 | + |
| 106 | +深度优先的过程,就是对每个可能的分支路径,深度到叶子节点,并且每个节点只访问一次。 |
| 107 | + |
| 108 | + |
| 109 | + |
| 110 | +ADIHCBGFE 就是上图这个多叉树,使用深度优先算法的遍历结果。 |
| 111 | + |
| 112 | +在实现上,**深度优先非常适合用先入后出的栈来实现**。逻辑不复杂,直接上执行时,栈的数据变换。 |
| 113 | + |
| 114 | + |
| 115 | + |
| 116 | + |
| 117 | + |
| 118 | +代码实现如下: |
| 119 | + |
| 120 | +``` |
| 121 | +fun depthFirst(root :View){ |
| 122 | + val viewDeque = LinkedList<View>() |
| 123 | + var view = root |
| 124 | + viewDeque.push(view) |
| 125 | + while (!viewDeque.isEmpty()){ |
| 126 | + view = viewDeque.pop() |
| 127 | + printView(view) |
| 128 | + if(view is ViewGroup){ |
| 129 | + for(childIndex in 0 until view.childCount){ |
| 130 | + val childView = view.getChildAt(childIndex) |
| 131 | + viewDeque.push(childView) |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +依然利用 `LinkedList` 来当栈使用,利用 `push()` 和 `pop()` 实现栈的逻辑。 |
| 139 | + |
| 140 | +三. 小结时刻 |
| 141 | +------- |
| 142 | + |
| 143 | +今天聊的 View 树的遍历,本质上就是数据结构中,多叉树的遍历,不同的实现方式用来解决不同的问题。 |
| 144 | + |
| 145 | +其实这道题,还有一些变种,例如统计 ViewGroup 子 View 的数量、分层打印 ViewTree、查找 ID 为 Xxx 的 View 等,有兴趣可以试着写写代码。 |
| 146 | + |
| 147 | +算法题就是这样,有一些是考验编码能力,另一些是解决问题的思路,多思考多写,才是正道。 |
| 148 | + |
| 149 | +本文参与[腾讯云自媒体分享计划](https://cloud.tencent.com/developer/support-plan),欢迎正在阅读的你也加入,一起分享。 |
0 commit comments