|
| 1 | +## 介绍 |
| 2 | +**树** 是一种非常常见的数据结构。 |
| 3 | +树的每个节点有一个根节点,还有一个包含所有子节点的列表。当然,从图的观点看,它是一个拥有`N个节点`和`N-1条边`的有向无环图。 |
| 4 | + |
| 5 | +`二叉树`是一种更加典型的树。如名字所示,一个节点最多包含两个子树的结构,通常称为“左子树”和“右子树”。 |
| 6 | + |
| 7 | +### 代码部分 |
| 8 | +我们来看看二叉树的定义。 |
| 9 | +C语言的: |
| 10 | +```c |
| 11 | +typedef _node TreeNode; |
| 12 | +struct _node { |
| 13 | + int data; |
| 14 | + struct _node* left; |
| 15 | + struct _node* right; |
| 16 | +}; |
| 17 | +``` |
| 18 | +Java的: |
| 19 | +```java |
| 20 | +class Node { |
| 21 | + int val; |
| 22 | + Node left, right; |
| 23 | + |
| 24 | + public Node(int val) { |
| 25 | + this.val = val; |
| 26 | + left = right = null; |
| 27 | + } |
| 28 | +} |
| 29 | +``` |
| 30 | +其他语言的都是大同小异。 |
| 31 | +我们以C语言为例子,来构建一棵树。 |
| 32 | +```c |
| 33 | +#include<stdio.h> |
| 34 | +#include<stdlib.h> |
| 35 | + |
| 36 | +typedef _node TreeNode; |
| 37 | +struct _node { |
| 38 | + int data; |
| 39 | + struct _node* left; |
| 40 | + struct _node* right; |
| 41 | +}; |
| 42 | + |
| 43 | +/*构建一个节点*/ |
| 44 | +TreeNode* newNode(int); |
| 45 | + |
| 46 | +int main(int argc, char const *argv[]) |
| 47 | +{ |
| 48 | + TreeNode* root = newNode(1); |
| 49 | + /* 下面是构建出来的树。 |
| 50 | + 1 |
| 51 | + / \ |
| 52 | + NULL NULL |
| 53 | + */ |
| 54 | + root->left = newNode(2); |
| 55 | + root->right = newNode(3); |
| 56 | + /* 2 和 3 成为了子节点。 |
| 57 | + 1 |
| 58 | + / \ |
| 59 | + 2 3 |
| 60 | + / \ / \ |
| 61 | + NULL NULL NULL NULL |
| 62 | + */ |
| 63 | + root->left->left = newNode(4); |
| 64 | + |
| 65 | + getchar(); |
| 66 | + |
| 67 | + return 0; |
| 68 | +} |
| 69 | +TreeNode* newNode(int val) { |
| 70 | + TreeNode* node = malloc(sizeof(TreeNode)); |
| 71 | + if (node == NULL) { |
| 72 | + fprintf(stderr, "%s\n", "malloc fail !"); |
| 73 | + return NULL; |
| 74 | + } |
| 75 | + node->data = val; |
| 76 | + node->left = NULL; |
| 77 | + node->right = NULL; |
| 78 | + return node; |
| 79 | +} |
| 80 | +``` |
| 81 | +从代码上可以看到,二叉树是一个层次的结构。 |
| 82 | +
|
| 83 | +### 二叉树的属性 |
| 84 | +
|
| 85 | +看了二叉树的代码和实例之后,我们可以发现一些关于二叉树的性质。 |
| 86 | +- 在第n层中, 二叉树的节点数最多为 2^(n-1)个。 |
| 87 | +在根节点中,只有一个。 |
| 88 | +要证明这个结论,可以用递推。 |
| 89 | +- 一个高度为h的二叉树,最多有2^h - 1个节点。 |
| 90 | +- 有N个节点的二叉树,最小的高度是? (Log2(N+1)) |
| 91 | +- 同理,有L个叶子的二叉树至少有多高(多少层)? Log2L + 1 |
| 92 | +
|
| 93 | +
|
| 94 | +### 树的遍历 |
| 95 | +四种遍历: |
| 96 | +1. 前序遍历 |
| 97 | +2. 中序遍历 |
| 98 | +3. 后序遍历 |
| 99 | +4. 层次遍历 |
| 100 | +
|
| 101 | +--- |
| 102 | +前序遍历 |
| 103 | +
|
| 104 | +前序遍历就是先访问根节点,然后访问左子树, 最后遍历右子树。 |
| 105 | +
|
| 106 | + F |
| 107 | + / \ |
| 108 | + B G |
| 109 | + / \ \ |
| 110 | + A D I |
| 111 | + / \ / |
| 112 | + C E H |
| 113 | +
|
| 114 | + 上面这个树的前序遍历是 |
| 115 | + `F -> B -> A -> D -> C -> E -> G -> I -> H`. |
| 116 | + 那么我们来看代码怎么写。 |
| 117 | + Leetcode中提供了这题: |
| 118 | +
|
| 119 | + [二叉树的前序遍历Leetcode](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/) |
| 120 | + 递归版本的很简单,直接看代码: |
| 121 | + ```java |
| 122 | +class Solution { |
| 123 | + public List<Integer> preorderTraversal(TreeNode root) { |
| 124 | + List<Integer> res = new LinkedList(); |
| 125 | + helper(root, res); |
| 126 | + return res; |
| 127 | + |
| 128 | + } |
| 129 | + /** |
| 130 | + 定义的helper函数,用来遍历。 |
| 131 | + */ |
| 132 | + public void helper(TreeNode root, List<Integer> res) { |
| 133 | + if (root != null) { |
| 134 | + res.add(root.val); |
| 135 | + if (root.left != null) { |
| 136 | + helper(root.left, res); |
| 137 | + } |
| 138 | + if (root.right != null) { |
| 139 | + helper(root.right, res); |
| 140 | + } |
| 141 | + |
| 142 | + } |
| 143 | + |
| 144 | + } |
| 145 | +} |
| 146 | +``` |
| 147 | +当然,从题目的角度上来说,它还希望我们使用迭代的方式来完成。 |
| 148 | +迭代的话,需要使用到栈。 |
| 149 | +具体也不是很难, 直接看代码: |
| 150 | +```java |
| 151 | +class Solution { |
| 152 | + List<Integer> res = new ArrayList(); |
| 153 | + public List<Integer> preorderTraversal(TreeNode root) { |
| 154 | + // now lets use iterative method. |
| 155 | + Stack<TreeNode> stack = new Stack(); |
| 156 | + if (root == null) return res; |
| 157 | + stack.push(root); |
| 158 | + while (!stack.isEmpty()) { |
| 159 | + TreeNode pop = stack.pop(); |
| 160 | + res.add(pop.val); |
| 161 | + if (pop.right != null) stack.push(pop.right); |
| 162 | + if (pop.left != null) stack.push(pop.left); |
| 163 | + |
| 164 | + } |
| 165 | + return res; |
| 166 | + } |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | + --- |
| 171 | + 中序遍历 |
| 172 | + |
| 173 | +中序遍历就是先访问左子树, 然后访问根节点,最后右子树。 |
| 174 | + |
| 175 | + F |
| 176 | + / \ |
| 177 | + B G |
| 178 | + / \ \ |
| 179 | + A D I |
| 180 | + / \ / |
| 181 | + C E H |
| 182 | + |
| 183 | +还是这个树,这个树的中序遍历是: |
| 184 | +`A -> B -> C -> D -> E -> F -> G -> H -> I`. |
| 185 | +注意遍历出来的顺序。 |
| 186 | +Leetcode也有中序遍历的练习,我们来看看。 |
| 187 | + |
| 188 | +[二叉树的中序遍历 Leetcode94](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/) |
| 189 | +同样的,迭代也不难,我们直接上代码: |
| 190 | +```java |
| 191 | +class Solution { |
| 192 | + public List<Integer> inorderTraversal(TreeNode root) { |
| 193 | + List<Integer> result = new ArrayList(); |
| 194 | + helper(result, root); |
| 195 | + return result; |
| 196 | + } |
| 197 | + |
| 198 | + public void helper(List<Integer> list, TreeNode root) { |
| 199 | + if (root == null) return; |
| 200 | + helper(list, root.left); |
| 201 | + list.add(root.val); |
| 202 | + helper(list, root.right); |
| 203 | + |
| 204 | + } |
| 205 | +} |
| 206 | +``` |
| 207 | +我们看看迭代的写法。 |
| 208 | +```java |
| 209 | +public class Solution { |
| 210 | + public List < Integer > inorderTraversal(TreeNode root) { |
| 211 | + List < Integer > res = new ArrayList < > (); |
| 212 | + Stack < TreeNode > stack = new Stack < > (); |
| 213 | + TreeNode curr = root; |
| 214 | + while (curr != null || !stack.isEmpty()) { |
| 215 | + while (curr != null) { |
| 216 | + stack.push(curr); |
| 217 | + curr = curr.left; |
| 218 | + } |
| 219 | + curr = stack.pop(); |
| 220 | + res.add(curr.val); |
| 221 | + curr = curr.right; |
| 222 | + } |
| 223 | + return res; |
| 224 | + } |
| 225 | +} |
| 226 | +``` |
| 227 | +迭代的写法也是基于栈,但是写法上有点小小的注意,先走左子树,然后根节点,然后右子树。 |
| 228 | + |
| 229 | +--- |
| 230 | +后序遍历 |
| 231 | + |
| 232 | +后序遍历就是先遍历左子树,然后右子树,最后根节点。 |
| 233 | + |
| 234 | + F |
| 235 | + / \ |
| 236 | + B G |
| 237 | + / \ \ |
| 238 | + A D I |
| 239 | + / \ / |
| 240 | + C E H |
| 241 | +还是这个树,这个的后序遍历是: |
| 242 | +`A -> C -> E -> D -> B -> H -> I -> G -> F.` |
| 243 | +同样的,我们来看后序遍历怎么写。[二叉树的后序遍历 Leetcode145](https://leetcode-cn.com/problems/binary-tree-postorder-traversal/) |
| 244 | +```java |
| 245 | +class Solution { |
| 246 | + List<Integer> res = new ArrayList(); |
| 247 | + public List<Integer> postorderTraversal(TreeNode root) { |
| 248 | + if (root == null) return res; |
| 249 | + postorderTraversal(root.left); |
| 250 | + postorderTraversal(root.right); |
| 251 | + res.add(root.val); |
| 252 | + return res; |
| 253 | + } |
| 254 | +} |
| 255 | +``` |
| 256 | +同样的,为了更好的理解后序遍历,我们来看看迭代的写法怎么写。 |
| 257 | +代码有很多,我这里提供一种思路。 |
| 258 | +> 我们回顾一下前序遍历的代码,我们把左右子树都分别压栈,然后从栈里面取元素出来。需要注意的是,我们先访问左子树,根据栈的特性,我们先压栈右子树。 |
| 259 | +
|
| 260 | +那我们在后序遍历的时候要考虑的一个问题是,我们到根节点的时候,不可以直接pop掉,因为我们后面还要回来。 |
| 261 | +怎么办呢? |
| 262 | +我们把每个节点都push两次,然后判断当前pop节点和栈顶节点是否相同。 |
| 263 | +如果相同的话,就是从左子树到的根节点。 |
| 264 | +如果不同的话,就是从右子树到的根节点,此时就可以把节点加入到list里面。 |
| 265 | + |
| 266 | +```java |
| 267 | +public List<Integer> postorderTraversal(TreeNode root) { |
| 268 | + List<Integer> list = new ArrayList<>(); |
| 269 | + if (root == null) { |
| 270 | + return list; |
| 271 | + } |
| 272 | + Stack<TreeNode> stack = new Stack<>(); |
| 273 | + stack.push(root); |
| 274 | + stack.push(root); |
| 275 | + while (!stack.isEmpty()) { |
| 276 | + TreeNode cur = stack.pop(); |
| 277 | + if (cur == null) { |
| 278 | + continue; |
| 279 | + } |
| 280 | + if (!stack.isEmpty() && cur == stack.peek()) { |
| 281 | + stack.push(cur.right); |
| 282 | + stack.push(cur.right); |
| 283 | + stack.push(cur.left); |
| 284 | + stack.push(cur.left); |
| 285 | + } else { |
| 286 | + list.add(cur.val); |
| 287 | + } |
| 288 | + } |
| 289 | + return list; |
| 290 | +} |
| 291 | +``` |
| 292 | +有兴趣的同学,可以去了解一下莫里斯遍历(Morris Traversal) |
| 293 | + |
| 294 | +--- |
| 295 | +层次遍历 |
| 296 | + |
| 297 | +二叉树的层次遍历就略为简单了,还是上面的那个树: |
| 298 | + |
| 299 | + F |
| 300 | + / \ |
| 301 | + B G |
| 302 | + / \ \ |
| 303 | + A D I |
| 304 | + / \ / |
| 305 | + C E H |
| 306 | + |
| 307 | +它的层次遍历是: |
| 308 | +`[[F], [B, G], [A, D, I], [C, E, H]]` |
| 309 | + |
| 310 | +每一层的节点都是一个list里面放入一个新的大list里面。这里我们要使用到一个叫队列的数据结构来帮助我们做广度优先搜索(BFS)。如果你对队列不理解的话,可以回去linkedlist那里面看我写的Quack这部分代码,它实现了栈和队列。[查看Quack](linkedlist/)区别只是在于,当你把它当栈用的时候,调用push来压栈,当把它当成队列的时候,使用qush来入队。 |
| 311 | +那么我们来看层次遍历怎么写: |
| 312 | +```java |
| 313 | +public List<List<Integer>> levelOrder(TreeNode root) { |
| 314 | + List<List<Integer>> result = new ArrayList(); |
| 315 | + Queue<TreeNode> queue = new LinkedList(); |
| 316 | + if (root == null) return result; |
| 317 | + int level = 0; |
| 318 | + queue.add(root); // try to start. |
| 319 | + while (!queue.isEmpty()) { |
| 320 | + result.add(new ArrayList<Integer>()); |
| 321 | + int level_length = queue.size(); |
| 322 | + for (int i = 0; i < level_length; i++) { |
| 323 | + TreeNode node = queue.remove(); |
| 324 | + result.get(level).add(node.val); |
| 325 | + if (node.left != null) queue.add(node.left); |
| 326 | + if (node.right != null) queue.add(node.right); |
| 327 | + |
| 328 | + } |
| 329 | + level++; |
| 330 | + |
| 331 | + |
| 332 | + } |
| 333 | + return result; |
| 334 | + |
| 335 | +``` |
| 336 | +注意到使用了一个变量来跟踪当前的层数。 |
| 337 | + |
| 338 | +--- |
| 339 | +关于删除 |
| 340 | +删除二叉树的节点是件麻烦的事情。一种思路是,按照后序遍历的方法来走,先删除它的左子树,然后删除右子树,最后删除节点自己。 |
| 341 | + |
0 commit comments