|
| 1 | +# JavaScript 数据结构与算法(十一)树 |
| 2 | + |
| 3 | +## 树结构 |
| 4 | + |
| 5 | +### 什么是树? |
| 6 | + |
| 7 | +#### 真实的树: |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +#### 树的特点: |
| 12 | + |
| 13 | +- 树一般都有一个根,连接着根的是树干; |
| 14 | +- 树干会发生分叉,形成许多树枝,树枝会继续分化成更小的树枝; |
| 15 | +- 树枝的最后是叶子; |
| 16 | + |
| 17 | +现实生活中很多结构都是树的抽象,模拟的树结构相当于旋转 `180°` 的树。 |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +#### 树结构对比于数组/链表/哈希表有哪些优势呢? |
| 22 | + |
| 23 | +数组: |
| 24 | + |
| 25 | +- 优点:可以通过下标值访问,效率高; |
| 26 | +- 缺点:查找数据时需要先对数据进行排序,生成有序数组,才能提高查找效率;并且在插入和删除元素时,需要大量的位移操作; |
| 27 | + |
| 28 | +链表: |
| 29 | + |
| 30 | +- 优点:数据的插入和删除操作效率都很高; |
| 31 | +- 缺点:查找效率低,需要从头开始依次查找,直到找到目标数据为止;当需要在链表中间位置插入或删除数据时,插入或删除的效率都不高。 |
| 32 | + |
| 33 | +哈希表: |
| 34 | + |
| 35 | +- 优点:哈希表的插入/查询/删除效率都非常高; |
| 36 | +- 缺点:空间利用率不高,底层使用的数组中很多单元没有被利用;并且哈希表中的元素是无序的,不能按照固定顺序遍历哈希表中的元素;而且不能快速找出哈希表中最大值或最小值这些特殊值。 |
| 37 | + |
| 38 | +树结构: |
| 39 | + |
| 40 | +- 优点:树结构综合了上述三种结构的优点,同时也弥补了它们存在的缺点(虽然效率不一定都比它们高),比如树结构中数据都是有序的,查找效率高;空间利用率高;并且可以快速获取最大值和最小值等。 |
| 41 | + |
| 42 | +总的来说:每种数据结构都有自己特定的应用场景。 |
| 43 | + |
| 44 | +树结构: |
| 45 | + |
| 46 | +- 树(Tree):由 n(n ≥ 0)个节点构成的有限集合。当 n = 0 时,称为空树。 |
| 47 | + |
| 48 | +- 对于任意一棵非空树(n > 0),它具备以下性质: |
| 49 | + - 数中有一个称为根(Root)的特殊节点,用 **r** 表示; |
| 50 | + - 其余节点可分为 m(m > 0)个互不相交的有限集合 T~1~,T~2~,...,T~m~,其中每个集合本身又是一棵树,称为原来树的子树(SubTree)。 |
| 51 | + |
| 52 | +#### 树的常用术语: |
| 53 | + |
| 54 | + |
| 55 | + |
| 56 | +- 节点的度(Degree):节点的子树个数,比如节点 B 的度为 2; |
| 57 | +- 树的度:树的所有节点中最大的度数,如上图树的度为 2; |
| 58 | +- 叶节点(Leaf):度为 0 的节点(也称为叶子节点),如上图的 H,I 等; |
| 59 | +- 父节点(Parent):度不为 0 的节点称为父节点,如上图节点 B 是节点 D 和 E 的父节点; |
| 60 | +- 子节点(Child):若 B 是 D 的父节点,那么 D 就是 B 的子节点; |
| 61 | +- 兄弟节点(Sibling):具有同一父节点的各节点彼此是兄弟节点,比如上图的 B 和 C,D 和 E 互为兄弟节点; |
| 62 | +- 路径和路径长度:路径指的是一个节点到另一节点的通道,路径所包含边的个数称为路径长度,比如 A->H 的路径长度为 3; |
| 63 | +- 节点的层次(Level):规定根节点在 1 层,其他任一节点的层数是其父节点的层数加 1。如 B 和 C 节点的层次为 2; |
| 64 | +- 树的深度(Depth):树种所有节点中的最大层次是这棵树的深度,如上图树的深度为 4; |
| 65 | + |
| 66 | +#### 树结构的表示方式 |
| 67 | + |
| 68 | +##### 最普通的表示方法: |
| 69 | + |
| 70 | + |
| 71 | + |
| 72 | +如图,树结构的组成方式类似于链表,都是由一个个节点连接构成。不过,根据每个父节点子节点数量的不同,每一个父节点需要的引用数量也不同。比如节点 A 需要 3 个引用,分别指向子节点 B,C,D;B 节点需要 2 个引用,分别指向子节点 E 和 F;K 节点由于没有子节点,所以不需要引用。 |
| 73 | + |
| 74 | +这种方法缺点在于我们无法确定某一结点的引用数。 |
| 75 | + |
| 76 | +##### 儿子-兄弟表示法: |
| 77 | + |
| 78 | + |
| 79 | + |
| 80 | +这种表示方法可以完整地记录每个节点的数据,比如: |
| 81 | + |
| 82 | +```js |
| 83 | +//节点A |
| 84 | +Node{ |
| 85 | + //存储数据 |
| 86 | + this.data = data |
| 87 | + //统一只记录左边的子节点 |
| 88 | + this.leftChild = B |
| 89 | + //统一只记录右边的第一个兄弟节点 |
| 90 | + this.rightSibling = null |
| 91 | +} |
| 92 | + |
| 93 | +//节点B |
| 94 | +Node{ |
| 95 | + this.data = data |
| 96 | + this.leftChild = E |
| 97 | + this.rightSibling = C |
| 98 | +} |
| 99 | + |
| 100 | +//节点F |
| 101 | +Node{ |
| 102 | + this.data = data |
| 103 | + this.leftChild = null |
| 104 | + this.rightSibling = null |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +这种表示法的优点在于每一个节点中引用的数量都是确定的。 |
| 109 | + |
| 110 | +##### 儿子-兄弟表示法旋转 |
| 111 | + |
| 112 | +以下为儿子-兄弟表示法组成的树结构: |
| 113 | + |
| 114 | + |
| 115 | + |
| 116 | +将其顺时针旋转 45° 之后: |
| 117 | + |
| 118 | + |
| 119 | + |
| 120 | +这样就成为了一棵二叉树,由此我们可以得出结论:任何树都可以通过二叉树进行模拟。但是这样父节点不是变了吗?其实,父节点的设置只是为了方便指向子节点,在代码实现中谁是父节点并没有关系,只要能正确找到对应节点即可。 |
| 121 | + |
| 122 | +### 二叉树 |
| 123 | + |
| 124 | +#### 二叉树的概念 |
| 125 | + |
| 126 | +如果树中的每一个节点最多只能由两个子节点,这样的树就称为二叉树; |
| 127 | + |
| 128 | +#### 二叉树的组成 |
| 129 | + |
| 130 | +- 二叉树可以为空,也就是没有节点; |
| 131 | +- 若二叉树不为空,则它由根节点和称为其左子树 TL 和右子树 TR 的两个不相交的二叉树组成; |
| 132 | + |
| 133 | +#### 二叉树的五种形态 |
| 134 | + |
| 135 | + |
| 136 | + |
| 137 | +上图分别表示:空的二叉树、只有一个节点的二叉树、只有左子树 TL 的二叉树、只有右子树 TR 的二叉树和有左右两个子树的二叉树。 |
| 138 | + |
| 139 | +#### 二叉树的特性 |
| 140 | + |
| 141 | +- 一个二叉树的第 i 层的最大节点树为:2^(i-1)^,i >= 1; |
| 142 | +- 深度为 k 的二叉树的最大节点总数为:2^k^ - 1 ,k >= 1; |
| 143 | +- 对任何非空二叉树,若 n~0~ 表示叶子节点的个数,n~2~表示度为 2 的非叶子节点个数,那么两者满足关系:n~0~ = n~2~ + 1;如下图所示:H,E,I,J,G 为叶子节点,总数为 5;A,B,C,F 为度为 2 的非叶子节点,总数为 4;满足 n~0~ = n~2~ + 1 的规律。 |
| 144 | + |
| 145 | + |
| 146 | + |
| 147 | +#### 特殊的二叉树 |
| 148 | + |
| 149 | +##### 完美二叉树 |
| 150 | + |
| 151 | +完美二叉树(Perfect Binary Tree)也成为满二叉树(Full Binary Tree),在二叉树中,除了最下一层的叶子节点外,每层节点都有 2 个子节点,这就构成了完美二叉树。 |
| 152 | + |
| 153 | + |
| 154 | + |
| 155 | +##### 完全二叉树 |
| 156 | + |
| 157 | +完全二叉树(Complete Binary Tree): |
| 158 | + |
| 159 | +- 除了二叉树最后一层外,其他各层的节点数都达到了最大值; |
| 160 | +- 并且,最后一层的叶子节点从左向右是连续存在,只缺失右侧若干叶子节点; |
| 161 | +- 完美二叉树是特殊的完全二叉树; |
| 162 | + |
| 163 | + |
| 164 | + |
| 165 | +在上图中,由于 H 缺失了右子节点,所以它不是完全二叉树。 |
| 166 | + |
| 167 | +#### 二叉树的数据存储 |
| 168 | + |
| 169 | +常见的二叉树存储方式为数组和链表: |
| 170 | + |
| 171 | +##### 使用数组 |
| 172 | + |
| 173 | +- 完全二叉树:按从上到下,从左到右的方式存储数据。 |
| 174 | + |
| 175 | + |
| 176 | + |
| 177 | +| 节点 | A | B | C | D | E | F | G | H | I | |
| 178 | +| ---- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | |
| 179 | +| 序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
| 180 | + |
| 181 | +使用数组存储时,取数据的时候也十分方便:左子节点的序号等于父节点序号 _ 2,右子节点的序号等于父节点序号 _ 2 + 1 。 |
| 182 | + |
| 183 | +- 非完全二叉树:非完全二叉树需要转换成完全二叉树才能按照上面的方案存储,这样会浪费很大的存储空间。 |
| 184 | + |
| 185 | + |
| 186 | + |
| 187 | +| 节点 | A | B | C | ^ | ^ | F | ^ | ^ | ^ | ^ | ^ | ^ | M | |
| 188 | +| ---- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | |
| 189 | +| 序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
| 190 | + |
| 191 | +##### 使用链表 |
| 192 | + |
| 193 | +二叉树最常见的存储方式为链表:每一个节点封装成一个 Node,Node 中包含存储的数据、左节点的引用和右节点的引用。 |
| 194 | + |
| 195 | + |
| 196 | + |
| 197 | +### 二叉搜索树 |
| 198 | + |
| 199 | +二叉搜索树(BST,Binary Search Tree),也称为二叉排序树和二叉查找树。 |
| 200 | + |
| 201 | +二叉搜索树是一棵二叉树,可以为空。 |
| 202 | + |
| 203 | +如果不为空,则满足以下性质: |
| 204 | + |
| 205 | +- 条件 1:非空左子树的所有键值小于其根节点的键值。比如三中节点 6 的所有非空左子树的键值都小于 6; |
| 206 | +- 条件 2:非空右子树的所有键值大于其根节点的键值;比如三中节点 6 的所有非空右子树的键值都大于 6; |
| 207 | +- 条件 3:左、右子树本身也都是二叉搜索树; |
| 208 | + |
| 209 | + |
| 210 | + |
| 211 | +如上图所示,树二和树三符合 3 个条件属于二叉树,树一不满足条件 3 所以不是二叉树。 |
| 212 | + |
| 213 | +总结:二叉搜索树的特点主要是较小的值总是保存在左节点上,相对较大的值总是保存在右节点上。这种特点使得二叉搜索树的查询效率非常高,这也就是二叉搜索树中“搜索”的来源。 |
| 214 | + |
| 215 | +#### 二叉搜索树应用举例 |
| 216 | + |
| 217 | +下面是一个二叉搜索树: |
| 218 | + |
| 219 | + |
| 220 | + |
| 221 | +若想在其中查找数据 10,只需要查找 4 次,查找效率非常高。 |
| 222 | + |
| 223 | +- 第 1 次:将 10 与根节点 9 进行比较,由于 10 > 9,所以 10 下一步与根节点 9 的右子节点 13 比较; |
| 224 | +- 第 2 次:由于 10 < 13,所以 10 下一步与父节点 13 的左子节点 11 比较; |
| 225 | +- 第 3 次:由于 10 < 11,所以 10 下一步与父节点 11 的左子节点 10 比较; |
| 226 | +- 第 4 次:由于 10 = 10,最终查找到数据 10 。 |
| 227 | + |
| 228 | + |
| 229 | + |
| 230 | +同样是 15 个数据,在排序好的数组中查询数据 10,需要查询 10 次: |
| 231 | + |
| 232 | + |
| 233 | + |
| 234 | +其实:如果是排序好的数组,可以通过二分查找:第一次找9,第二次找13,第三次找15...。我们发现如果把每次二分的数据拿出来以树的形式表示的话就是二叉搜索树。这就是数组二分法查找效率之所以高的原因。 |
| 235 | + |
| 236 | + |
| 237 | +#### 二叉搜索树的封装 |
| 238 | + |
| 239 | +二叉搜索树有四个最基本的属性:指向节点的根(root),节点中的键(key)、左指针(right)、右指针(right)。 |
| 240 | + |
| 241 | + |
| 242 | + |
| 243 | +所以,二叉搜索树中除了定义root属性外,还应定义一个节点内部类,里面包含每个节点中的left、right和key三个属性: |
| 244 | + |
| 245 | +```js |
| 246 | +// 节点类 |
| 247 | +class Node { |
| 248 | + |
| 249 | + constructor(key) { |
| 250 | + this.key = key; |
| 251 | + this.left = null; |
| 252 | + this.right = null; |
| 253 | + } |
| 254 | + |
| 255 | +} |
| 256 | + |
| 257 | +``` |
| 258 | + |
| 259 | +二叉搜索树的常见操作: |
| 260 | + |
| 261 | +- insert(key):向树中插入一个新的键; |
| 262 | +- search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回false; |
| 263 | +- inOrderTraverse:通过中序遍历方式遍历所有节点; |
| 264 | +- preOrderTraverse:通过先序遍历方式遍历所有节点; |
| 265 | +- postOrderTraverse:通过后序遍历方式遍历所有节点; |
| 266 | +- min:返回树中最小的值/键; |
| 267 | +- max:返回树中最大的值/键; |
| 268 | +- remove(key):从树中移除某个键; |
0 commit comments