启迪思维:链式树
最近公司项目比较忙,又加上要给十几个新人做培训,晚上和周末又要个姐姐装修店铺(太痛苦的事情,而且装修的效果也不是很好),前些天天涯上有一篇寒门难出贵子帖子很火,那个作者语文水平和我一个级别的(错别字太多了),大家可以看淘宝鬼脚七整理版本(关注他的微信公众号),虽然里边有些观点不是很认同,但对我影响很大,出生寒门的兄弟一定去静下心来好读读,这个远比我下面写的这边技术文章带给你更多思考,非常欢迎大家留言讨论文章的观点。
前面的文章一直在分析数据结构中线性结构部分,从本篇文章以后将进入非线性结构部分,前面未弄懂部分可以在网上找些视频结合我写的文章在补补,只有这样后面学习起来才相对容易些,下面进入正题,首先还是先分享一个软件带来误会故事。
以前学校旁边有一个火车票代售点,每次到过年学生放假的时候,偶尔会看有人因为先买没有买到票,而后面人确买到票和售票员吵架,估计每次发生这样事情,售票员自己也解释不清楚,连当时学计算机的我也猜测后面人给售票员好处,后来偶然看到一篇帖子才明白,原来火车票售票系统,会把一定数量的票锁定给一个售票厅,前一个人买票时候,余票被其他售票厅锁住了,后面去人再买票的时候正好赶上其他售票厅释放余票(太幸运),现在技术和硬件越来越好,从09年后我就没有见过为这事吵架的。
一:概念
树状图是一种数据结构,它是由n(n>=1)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
1、每个结点有零个或多个子结点;
2、没有前驱的结点称为根结点;
3、每一个非根结点有且只有一个父结点;
4、除了根结点外,每个子结点可以分为m个不相交的子树;
二:名称解释
子孙:以某节点为根的子树中任一节点都称为该节点的子孙;
森林:由m(m>=0)棵互不相交的树的集合称为森林;
树的度:一棵树中,最大的节点的度称为树的度;
节点的度:一个节点含有的子树的个数称为该节点的度;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
堂兄弟节点:双亲在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次;
双亲节点或父节点:若一个结点含有子节点,则这个节点称为其子节点的父节点;
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
叶节点或终端节点:度为零的节点;
非终端节点或分支节点:度不为零的节点;
三:示例图
四:树的应用
1、查找算法中应用(数据库索引-B-tree)
2、哈夫曼编码
3、最短路径问题
4、几乎所有的编译器都需要实现一个表达式树
上面这些树的应用,面试中也会经常问到,争取后面能写比较好分析它们的文章
五:树的分类
上面的截图是维基百科里边的解释,画红框的在后面的文章会分析到;
五:代码分析
1、创建一颗树
1 /** 2 *根据文件内容递归创建树 3 */ 4 void CreateTreeFromFile(ifstream &f) { 5 //每次读取一个字符 6 T e; 7 InputFromFile(f, e); 8 if (e == '#') { 9 return; 10 } 11 12 //创建一个新的节点 13 root = new TNode<T> (e); 14 15 //创建左右两个树对象 16 LinkTree<T> left, right; 17 //递归创建左子树 18 left.CreateTreeFromFile(f); 19 //递归创建右子树 20 right.CreateTreeFromFile(f); 21 22 //设置根节点的左孩子接左子树的根节点 23 root->lchild = left.root; 24 left.root = 0; 25 //设置根节点的右孩子接右子树的根节点 26 root->rchild = right.root; 27 right.root = 0; 28 } 29 /** 30 *读取文件并赋值给遍历c 31 */ 32 void InputFromFile(ifstream &f, T &c) { 33 f >> c; 34 }
2、清空一颗树
1 /** 2 *清空树的所有节点 3 */ 4 void Clear() { 5 Clear(root); 6 } 7 /** 8 *根据节点递归清空树 9 */ 10 void Clear(TNode<T>* t) { 11 //判断指针是否为空 12 if (t) { 13 //获取资源立即放入管理对象(参考Effective C++里边条款13) 14 //tr1::shared_ptr(引用计数智慧指针)管理对象比auto_ptr更强大 15 std::auto_ptr<TNode<T> > new_ptr(t); 16 17 //递归清空右子树 18 Clear(new_ptr->rchild); 19 20 //递归清空左子树 21 Clear(new_ptr->lchild); 22 } 23 24 //清空树的根节点 25 t = 0; 26 }
3、判断树是否为空
1 /** 2 * 若树为空,则返回true;否则返回false 3 */ 4 bool IsEmpty() { 5 return root == 0; 6 }
4、计算树的深度
1 /** 2 * 以传入节点为基础计算树的深度 3 */ 4 int GetTreeDept(const TNode<T> *t) { 5 int i, j; 6 if (t == 0) { 7 return 0; 8 } else { 9 //递归计算左子树的深度 10 i = this->GetTreeDept(t->lchild); 11 //递归计算右子树的深度 12 j = this->GetTreeDept(t->rchild); 13 } 14 15 //t的深度为其左右子树中深度中的大者加1 16 return i > j ? i + 1 : j + 1; 17 }
5、前序非递归遍历树
如下动画flash不知道是网上那位大牛做,在这里借用下,阅读文章的朋友如果有做flash做的比较好,请帮忙指点下我,谢谢
如下中图详细分析下面代码运行过程
如下代码
1 /** 2 *前序非递归(利用栈)遍历二叉树 3 *前序遍历的规则:根左右 4 *非递归遍历树会经常当做面试题,考察面试者的编程能力 5 *防止下次被鄙视,应该深入理解并且动手在纸写出来 6 */ 7 void PreOrderTraverse() { 8 //申明一个栈对象 9 stack<TNode<T>*> s; 10 //t首先指向根节点 11 TNode<T> *t = root; 12 //压入一个空指针,作为判断条件 13 s.push(0); 14 15 //如果t所值节点非空 16 while (t != 0) { 17 //直接访问根节点 18 std::cout << (&t->data) << " "; 19 20 //右孩子指针为非空 21 if (t->rchild != 0) { 22 //入栈右孩子指针 23 s.push(t->rchild); 24 } 25 26 //左孩子指针为非空 27 if (t->lchild != 0) { 28 //直接指向其左孩子 29 t = t->lchild; 30 } else {//左孩子指针为空 31 //获取栈顶元素(右孩子指针) 32 t = s.top(); 33 //清楚栈顶元素 34 s.pop(); 35 } 36 37 } 38 }
6、中序非递归遍历树
如下动画flash不知道是网上那位大牛做,在这里借用下,阅读文章的朋友如果有做flash做的比较好,请帮忙指点下我,谢谢
如下图是代码遍历树的节点顺序,看的不是很明白的朋友,可以像前面先序中那样方式,用图画出每一个步骤,便于跟好的理解
如下是代码
1 /** 2 *中序非递归(利用栈)遍历二叉树 3 *前序遍历的规则:左根右 4 */ 5 void InOrderTraverse() { 6 //申明一个栈对象 7 stack<TNode<T>*> s; 8 //t首先指向根节点 9 TNode<T>* t = root; 10 11 //节点不为空或者栈对象不为空,都进入循环 12 while (t != 0 || !s.empty()) { 13 //如果t节点非空 14 if (t) { 15 //入栈t节点 16 s.push(t); 17 //t节点指向其左孩子 18 t = t->lchild; 19 } else { 20 //获取栈顶元素(左孩子指针) 21 t = s.top(); 22 //清楚栈顶元素 23 s.pop(); 24 //直接访问t节点 25 std::cout << (&t->data) << " "; 26 //t节点指向其右孩子 27 t = t->rchild; 28 } 29 } 30 }
7、后序非递归遍历树
如下动画flash不知道是网上那位大牛做,在这里借用下,阅读文章的朋友如果有做flash做的比较好,请帮忙指点下我,谢谢
如下图是代码遍历树的节点顺序,看的不是很明白的朋友,可以像前面先序中那样方式,用图画出每一个步骤,便于跟好的理解
如下是代码
1 /** 2 *后序非递归(利用栈)遍历二叉树 3 *前序遍历的规则:左右根 4 */ 5 void PostOrderTraverse() { 6 //申明一个栈对象 7 stack<TNode<T>*> s; 8 //t首先指向根节点 9 TNode<T>* t = root; 10 //申请中间变量,用做判断标识 11 TNode<T>* r; 12 //节点不为空或者栈对象不为空,都进入循环 13 while (t != 0 || !s.empty()) { 14 //如果t节点非空 15 if (t) { 16 //入栈t节点 17 s.push(t); 18 //t节点指向其左孩子 19 t = t->lchild; 20 } else { 21 //获取栈顶元素(左孩子指针) 22 t = s.top(); 23 //判断t的右子树是否存在并且没有访问过 24 if (t->rchild && t->rchild != r) { 25 //t节点指向其右孩子 26 t = t->rchild; 27 //入栈t节点 28 s.push(t); 29 //t节点指向其左孩子 30 t = t->lchild; 31 } else { 32 //获取栈顶元素(左孩子指针) 33 t = s.top(); 34 //清楚栈顶元素 35 s.pop(); 36 //直接访问t节点 37 std::cout << (&t->data) << " "; 38 //设置已经访问过的节点,防止多次访问(右孩子指针) 39 r = t; 40 t = 0; 41 } 42 } 43 } 44 }
8、根据模式递归遍历二叉树
1 /** 2 * 根据模式递归遍历二叉树 3 * 下面代码相对比较简单,只要记住遍历树的规则并且弄一点递归,都可以写出来 4 * 如果在面试中实在写不出来非递归方式,可以写一个递归版本,也许可以争取一个好的工作 5 */ 6 void OrderTraverse(const TNode<T>* t, Style mode) { 7 if (t) { 8 //先序遍历二叉树:根左右 9 if (mode == Pre) { 10 //直接访问t节点 11 std::cout << (&t->data) << " "; 12 //递归遍历左子树 13 this->OrderTraverse(t->lchild, mode); 14 //递归遍历右子树 15 this->OrderTraverse(t->rchild, mode); 16 } 17 //中序遍历二叉树:左根右 18 if (mode == In) { 19 //递归遍历左子树 20 this->OrderTraverse(t->lchild, mode); 21 //直接访问t节点 22 std::cout << (&t->data) << " "; 23 //递归遍历右子树 24 this->OrderTraverse(t->rchild, mode); 25 } 26 //后序遍历二叉树:左右根 27 if (mode == Post) { 28 //递归遍历左子树 29 this->OrderTraverse(t->lchild, mode); 30 //递归遍历右子树 31 this->OrderTraverse(t->rchild, mode); 32 //直接访问t节点 33 std::cout << (&t->data) << " "; 34 } 35 } 36 }
9、运行结果,由于在虚拟机中打中文,实在太痛苦,弄一点英文装下B
测试代码如下:
1 void test() { 2 3 ifstream fin("data.txt"); 4 this->CreateTreeFromFile(fin); 5 std::cout << "create tree success" << std::endl; 6 7 std::cout << "create tree after is null ? "; 8 std::cout << boolalpha << this->IsEmpty(); 9 10 std::cout << std::endl; 11 std::cout << "calculated depth of the tree begins" << std::endl; 12 std::cout << "tree is dept = " << this->GetTreeDept(this->GetRoot()); 13 std::cout << std::endl; 14 std::cout << "calculated depth of the tree end" << std::endl; 15 16 std::cout << std::endl; 17 std::cout << "not recursion--------------------begin" << std::endl; 18 std::cout << "pre order traverse: "; 19 this->PreOrderTraverse(); 20 std::cout << std::endl; 21 22 std::cout << "in order traverse: "; 23 this->InOrderTraverse(); 24 std::cout << std::endl; 25 26 std::cout << "post order traverse: "; 27 this->PostOrderTraverse(); 28 std::cout << std::endl; 29 std::cout << "not recursion--------------------end" << std::endl; 30 31 std::cout << std::endl; 32 std::cout << "recursion--------------------begin" << std::endl; 33 std::cout << "pre order traverse:"; 34 this->OrderTraverse(this->GetRoot(), Pre); 35 std::cout << std::endl; 36 37 std::cout << "in order traverse:"; 38 this->OrderTraverse(this->GetRoot(), In); 39 std::cout << std::endl; 40 41 std::cout << "post order traverse:"; 42 this->OrderTraverse(this->GetRoot(), Post); 43 std::cout << std::endl; 44 std::cout << "recursion--------------------end" << std::endl; 45 }
10、完整代码
TNode.h
1 /* 2 * TLNode.h 3 * 4 * Created on: 2013-7-6 5 * Author: sunysen 6 */ 7 8 #ifndef TLNODE_H_ 9 #define TLNODE_H_ 10 template <class T> 11 class TNode{ 12 public: 13 T data; 14 TNode *rchild,*lchild; 15 TNode(T value):data(value),rchild(0),lchild(0){} 16 }; 17 18 #endif /* TLNODE_H_ */
LinkTree.h
1 /* 2 * LinkTree.h 3 * Created on: 2013-7-6 4 * Author: sunysen 5 */ 6 7 #ifndef LINKTREE_H_ 8 #define LINKTREE_H_ 9 #include "core/common/Common.h" 10 #include "core/node/TNode.h" 11 #include <queue> 12 #include <stack> 13 14 //树的三种遍历方式 15 enum Style { 16 Pre, In, Post 17 }; 18 19 /** 20 *链式树 21 */ 22 template<class T> 23 class LinkTree { 24 private: 25 TNode<T> *root;//树的根节点 26 27 /** 28 *读取文件并赋值给遍历c 29 */ 30 void InputFromFile(ifstream &f, T &c) { 31 f >> c; 32 } 33 34 /** 35 *根据节点递归清空树 36 */ 37 void Clear(TNode<T>* t) { 38 //判断指针是否为空 39 if (t) { 40 //获取资源立即放入管理对象(参考Effective C++里边条款13) 41 //tr1::shared_ptr(引用计数智慧指针)管理对象比auto_ptr更强大 42 std::auto_ptr<TNode<T> > new_ptr(t); 43 44 //递归清空右子树 45 Clear(new_ptr->rchild); 46 47 //递归清空左子树 48 Clear(new_ptr->lchild); 49 } 50 51 //清空树的根节点 52 t = 0; 53 } 54 public: 55 /** 56 * 构造函数初始化树根节点 57 */ 58 LinkTree() : 59 root(0) { 60 } 61 /** 62 *析构行数释放所有构造的资源 63 */ 64 ~LinkTree() { 65 Clear(); 66 } 67 68 /** 69 *根据文件内容递归创建树 70 */ 71 void CreateTreeFromFile(ifstream &f) { 72 //每次读取一个字符 73 T e; 74 InputFromFile(f, e); 75 if (e == '#') { 76 return; 77 } 78 79 //创建一个新的节点 80 root = new TNode<T> (e); 81 82 //创建左右两个树对象 83 LinkTree<T> left, right; 84 //递归创建左子树 85 left.CreateTreeFromFile(f); 86 //递归创建右子树 87 right.CreateTreeFromFile(f); 88 89 //设置根节点的左孩子接左子树的根节点 90 root->lchild = left.root; 91 left.root = 0; 92 //设置根节点的右孩子接右子树的根节点 93 root->rchild = right.root; 94 right.root = 0; 95 } 96 97 /** 98 * 若树为空,则返回true;否则返回false 99 */ 100 bool IsEmpty() { 101 return root == 0; 102 } 103 /** 104 *清空树的所有节点 105 */ 106 void Clear() { 107 Clear(root); 108 } 109 110 /** 111 *获取const修饰的根节点,相当于做一次类型转换 112 */ 113 const TNode<T>* GetRoot() { 114 return root; 115 } 116 117 /** 118 * 以传入节点为基础计算树的深度 119 */ 120 int GetTreeDept(const TNode<T> *t) { 121 int i, j; 122 if (t == 0) { 123 return 0; 124 } else { 125 //递归计算左子树的深度 126 i = this->GetTreeDept(t->lchild); 127 //递归计算右子树的深度 128 j = this->GetTreeDept(t->rchild); 129 } 130 131 //t的深度为其左右子树中深度中的大者加1 132 return i > j ? i + 1 : j + 1; 133 } 134 135 136 /** 137 *前序非递归(利用栈)遍历二叉树 138 *前序遍历的规则:根左右 139 *非递归遍历树会经常当做面试题,考察面试者的编程能力 140 *防止下次被鄙视,应该深入理解并且动手在纸写出来 141 */ 142 void PreOrderTraverse() { 143 //申明一个栈对象 144 stack<TNode<T>*> s; 145 //t首先指向根节点 146 TNode<T> *t = root; 147 //压入一个空指针,作为判断条件 148 s.push(0); 149 150 //如果t所值节点非空 151 while (t != 0) { 152 //直接访问根节点 153 std::cout << (&t->data) << " "; 154 155 //右孩子指针为非空 156 if (t->rchild != 0) { 157 //入栈右孩子指针 158 s.push(t->rchild); 159 } 160 161 //左孩子指针为非空 162 if (t->lchild != 0) { 163 //直接指向其左孩子 164 t = t->lchild; 165 } else {//左孩子指针为空 166 //获取栈顶元素(右孩子指针) 167 t = s.top(); 168 //清楚栈顶元素 169 s.pop(); 170 } 171 172 } 173 } 174 175 /** 176 *中序非递归(利用栈)遍历二叉树 177 *前序遍历的规则:左根右 178 */ 179 void InOrderTraverse() { 180 //申明一个栈对象 181 stack<TNode<T>*> s; 182 //t首先指向根节点 183 TNode<T>* t = root; 184 185 //节点不为空或者栈对象不为空,都进入循环 186 while (t != 0 || !s.empty()) { 187 //如果t节点非空 188 if (t) { 189 //入栈t节点 190 s.push(t); 191 //t节点指向其左孩子 192 t = t->lchild; 193 } else { 194 //获取栈顶元素(左孩子指针) 195 t = s.top(); 196 //清楚栈顶元素 197 s.pop(); 198 //直接访问t节点 199 std::cout << (&t->data) << " "; 200 //t节点指向其右孩子 201 t = t->rchild; 202 } 203 } 204 } 205 /** 206 *后序非递归(利用栈)遍历二叉树 207 *前序遍历的规则:左右根 208 */ 209 void PostOrderTraverse() { 210 //申明一个栈对象 211 stack<TNode<T>*> s; 212 //t首先指向根节点 213 TNode<T>* t = root; 214 //申请中间变量,用做判断标识 215 TNode<T>* r; 216 //节点不为空或者栈对象不为空,都进入循环 217 while (t != 0 || !s.empty()) { 218 //如果t节点非空 219 if (t) { 220 //入栈t节点 221 s.push(t); 222 //t节点指向其左孩子 223 t = t->lchild; 224 } else { 225 //获取栈顶元素(左孩子指针) 226 t = s.top(); 227 //判断t的右子树是否存在并且没有访问过 228 if (t->rchild && t->rchild != r) { 229 //t节点指向其右孩子 230 t = t->rchild; 231 //入栈t节点 232 s.push(t); 233 //t节点指向其左孩子 234 t = t->lchild; 235 } else { 236 //获取栈顶元素(左孩子指针) 237 t = s.top(); 238 //清楚栈顶元素 239 s.pop(); 240 //直接访问t节点 241 std::cout << (&t->data) << " "; 242 //设置已经访问过的节点,防止多次访问(右孩子指针) 243 r = t; 244 t = 0; 245 } 246 } 247 } 248 } 249 /** 250 * 根据模式递归遍历二叉树 251 * 下面代码相对比较简单,只要记住遍历树的规则并且弄一点递归,都可以写出来 252 * 如果在面试中实在写不出来非递归方式,可以写一个递归版本,也许可以争取一个好的工作 253 */ 254 void OrderTraverse(const TNode<T>* t, Style mode) { 255 if (t) { 256 //先序遍历二叉树:根左右 257 if (mode == Pre) { 258 //直接访问t节点 259 std::cout << (&t->data) << " "; 260 //递归遍历左子树 261 this->OrderTraverse(t->lchild, mode); 262 //递归遍历右子树 263 this->OrderTraverse(t->rchild, mode); 264 } 265 //中序遍历二叉树:左根右 266 if (mode == In) { 267 //递归遍历左子树 268 this->OrderTraverse(t->lchild, mode); 269 //直接访问t节点 270 std::cout << (&t->data) << " "; 271 //递归遍历右子树 272 this->OrderTraverse(t->rchild, mode); 273 } 274 //后序遍历二叉树:左右根 275 if (mode == Post) { 276 //递归遍历左子树 277 this->OrderTraverse(t->lchild, mode); 278 //递归遍历右子树 279 this->OrderTraverse(t->rchild, mode); 280 //直接访问t节点 281 std::cout << (&t->data) << " "; 282 } 283 } 284 } 285 286 void test() { 287 288 ifstream fin("data.txt"); 289 this->CreateTreeFromFile(fin); 290 std::cout << "create tree success" << std::endl; 291 292 std::cout << "create tree after is null ? "; 293 std::cout << boolalpha << this->IsEmpty(); 294 295 std::cout << std::endl; 296 std::cout << "calculated depth of the tree begins" << std::endl; 297 std::cout << "tree is dept = " << this->GetTreeDept(this->GetRoot()); 298 std::cout << std::endl; 299 std::cout << "calculated depth of the tree end" << std::endl; 300 301 std::cout << std::endl; 302 std::cout << "not recursion--------------------begin" << std::endl; 303 std::cout << "pre order traverse: "; 304 this->PreOrderTraverse(); 305 std::cout << std::endl; 306 307 std::cout << "in order traverse: "; 308 this->InOrderTraverse(); 309 std::cout << std::endl; 310 311 std::cout << "post order traverse: "; 312 this->PostOrderTraverse(); 313 std::cout << std::endl; 314 std::cout << "not recursion--------------------end" << std::endl; 315 316 std::cout << std::endl; 317 std::cout << "recursion--------------------begin" << std::endl; 318 std::cout << "pre order traverse:"; 319 this->OrderTraverse(this->GetRoot(), Pre); 320 std::cout << std::endl; 321 322 std::cout << "in order traverse:"; 323 this->OrderTraverse(this->GetRoot(), In); 324 std::cout << std::endl; 325 326 std::cout << "post order traverse:"; 327 this->OrderTraverse(this->GetRoot(), Post); 328 std::cout << std::endl; 329 std::cout << "recursion--------------------end" << std::endl; 330 } 331 332 }; 333 334 #endif /* LINKTREE_H_ */
Common.h
1 /* 2 * Common.h 3 * 4 * Created on: May 17, 2012 5 * Author: sunysen 6 */ 7 8 #ifndef COMMON_H_ 9 #define COMMON_H_ 10 11 #include <iostream> 12 #include <fstream> 13 #include "memory" 14 #include "string" 15 #include "string.h" 16 #include <stdio.h> 17 #include <stdlib.h> 18 #include <math.h> 19 20 21 using namespace std; 22 #endif /* COMMON_H_ */
六:环境
1、运行环境:Ubuntu 10.04 LTS+VMware8.0.4+gcc4.4.3;
2、开发工具:Eclipse+make
七:题记
1、上面的代码难免有bug,如果你发现代码写的有问题,请你帮忙指出,让我们一起进步,让代码变的更漂亮和更健壮;
2、我自己能手动写上面代码,离不开郝斌、高一凡、侯捷、严蔚敏等老师的书籍和视频指导,在这里感谢他们;
3、鼓励自己能坚持把更多数据结构方面的知识写出来,让自己掌握更深刻,也顺便冒充下"小牛";
欢迎继续阅读“启迪思维:数据结构和算法”系列