|
| 1 | +#问题 |
| 2 | + |
| 3 | +二叉树查找 |
| 4 | + |
| 5 | +#思路说明 |
| 6 | + |
| 7 | +二叉树查找是一个面对动态数据比较常用的查找算法。本文根据下面地址文章翻译,并根据本人的理解进行适当修改。 |
| 8 | + |
| 9 | +原文地址:http://www.laurentluce.com/posts/binary-search-tree-library-in-python/comment-page-1/ |
| 10 | + |
| 11 | +###二叉树查找的定义 |
| 12 | + |
| 13 | +定义内容可以参阅Wikipedia:http://en.wikipedia.org/wiki/Binary_tree |
| 14 | + |
| 15 | +这里是中文的:http://zh.wikipedia.org/wiki/%E4%BA%8C%E5%85%83%E6%90%9C%E5%B0%8B%E6%A8%B9 |
| 16 | + |
| 17 | +摘要其中对二叉树的描述: |
| 18 | + |
| 19 | +>>二叉树查找的性质: |
| 20 | +
|
| 21 | +>>1. 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值; |
| 22 | +>>2. 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值; |
| 23 | +>>3. 任意节点的左、右子树也分别为二叉查找树。 |
| 24 | +>>4. 没有键值相等的节点(no duplicate nodes)。 |
| 25 | +
|
| 26 | +>>二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低。为O(log n)。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、multiset、关联数组等。 |
| 27 | +
|
| 28 | +>>虽然二叉查找树的最坏效率是O(n),但它支持动态查询,且有很多改进版的二叉查找树可以使树高为O(logn),如SBT,AVL,红黑树等. |
| 29 | +
|
| 30 | +###用python实现二叉树查找 |
| 31 | + |
| 32 | +以下面图示的二叉树为例说明查找算法 |
| 33 | + |
| 34 | + |
| 35 | + |
| 36 | +**Node 类** |
| 37 | + |
| 38 | +创建一个类,命名为Node,做为二叉树节点结构,其中包括:左枝、右枝、节点数据三个变量。 |
| 39 | + |
| 40 | + class Node: |
| 41 | + """ |
| 42 | + 二叉树左右枝 |
| 43 | + """ |
| 44 | + def __init__(self, data): |
| 45 | + """ |
| 46 | + 节点结构 |
| 47 | + |
| 48 | + """ |
| 49 | + self.left = None |
| 50 | + self.right = None |
| 51 | + self.data = data |
| 52 | + |
| 53 | +例如创建一个含整数8的节点。因为仅仅创建一个节点,所以左右枝都是None。 |
| 54 | + |
| 55 | + root = Node(8) |
| 56 | + |
| 57 | +这样就得到如下图所示的只有一个节点的树。 |
| 58 | + |
| 59 | + |
| 60 | + |
| 61 | +**插入方法** |
| 62 | + |
| 63 | +现在已经有了一棵光秃秃的树,要有枝杈和叶子,就必须用插入数据方法,添加新的节点和数据。 |
| 64 | + |
| 65 | + def insert(self, data): |
| 66 | + """ |
| 67 | + 插入节点数据 |
| 68 | + """ |
| 69 | + if data < self.data: |
| 70 | + if self.left is None: |
| 71 | + self.left = Node(data) |
| 72 | + else: |
| 73 | + self.left.insert(data) |
| 74 | + elif data > self.data: |
| 75 | + if self.right is None: |
| 76 | + self.right = Node(data) |
| 77 | + else: |
| 78 | + self.right.insert(data) |
| 79 | + |
| 80 | +承接前面的操作,可以用下面的方式增加树的枝杈和叶子(左右枝以及节点数据)。 |
| 81 | + |
| 82 | + root.insert(3) |
| 83 | + root.insert(10) |
| 84 | + root.insert(1) |
| 85 | + |
| 86 | +当增加了第二个节点数据3,程序会: |
| 87 | + |
| 88 | +- 第一步,root会调用insert(),其参数是data=3 |
| 89 | +- 第二步,比较3和8(已有的根节点),3比8小。并且树的左枝还是None,于是就在左边建立一个新的节点。 |
| 90 | + |
| 91 | +增加第三个节点数据10,程序会: |
| 92 | + |
| 93 | +- 第一步,跟前面的第一步一样,只不过data=10 |
| 94 | +- 第二步,发现10大于8,同时右边是None,于是就把它做为右边新建分支的节点数据。 |
| 95 | + |
| 96 | +增加第四个节点数据1,程序会: |
| 97 | + |
| 98 | +- 第一步,同前,data=1 |
| 99 | +- 第二步,1小于8,所以要放在树的左枝; |
| 100 | +- 第三步,左枝已经有子节点3,该节点再次调用insert()方法,1小于3,所以1就做为3的子节点,且放在原本就是None的左侧。 |
| 101 | + |
| 102 | +如此,就形成了下图的树 |
| 103 | + |
| 104 | + |
| 105 | + |
| 106 | +继续增加节点数据 |
| 107 | + |
| 108 | + root.insert(6) |
| 109 | + root.insert(4) |
| 110 | + root.insert(7) |
| 111 | + root.insert(14) |
| 112 | + root.insert(13) |
| 113 | + |
| 114 | +最终形成下图的树 |
| 115 | + |
| 116 | + |
| 117 | + |
| 118 | +**遍历树** |
| 119 | + |
| 120 | +此方法用于查找树中的某个节点,如果找到了,就返回该节点,否则返回None。为了方便,也返回父节点。 |
| 121 | + |
| 122 | + def lookup(self, data, parent=None): |
| 123 | + """ |
| 124 | + 遍历二叉树 |
| 125 | + """ |
| 126 | + if data < self.data: |
| 127 | + if self.left is None: |
| 128 | + return None, None |
| 129 | + return self.left.lookup(data, self) |
| 130 | + elif data > self.data: |
| 131 | + if self.right is None: |
| 132 | + return None, None |
| 133 | + return self.right.lookup(data, self) |
| 134 | + else: |
| 135 | + return self, parent |
| 136 | + |
| 137 | +测试一下,找一找数据为6的节点 |
| 138 | + |
| 139 | + node, parent = root.lookup(6) |
| 140 | + |
| 141 | +调用lookup()后,程序会这么干: |
| 142 | + |
| 143 | +1. 调用lookup(),传递参数data=6,默认parent=None |
| 144 | +2. data=6,小于根节点的值8 |
| 145 | +3. 指针转到根节点左侧,此时:data=6,parent=8,再次调用lookup() |
| 146 | +4. data=6大于左侧第一层节点数据3 |
| 147 | +5. 指针转到3的右侧分支,data=6,parent=3,再次调用lookup() |
| 148 | +6. 节点数据等于6,于是返回这个节点和它的父节点3 |
| 149 | + |
| 150 | +**删除方法** |
| 151 | + |
| 152 | +删除节点数据。代码如下: |
| 153 | + |
| 154 | + def delete(self, data): |
| 155 | + """ |
| 156 | + 删除节点 |
| 157 | + """ |
| 158 | + node, parent = self.lookup(data) #已有节点 |
| 159 | + if node is not None: |
| 160 | + children_count = node.children_count() #判断子节点数 |
| 161 | + if children_count == 0: |
| 162 | + # 如果该节点下没有子节点,即可删除 |
| 163 | + if parent.left is node: |
| 164 | + parent.left = None |
| 165 | + else: |
| 166 | + parent.right = None |
| 167 | + del node |
| 168 | + elif children_count == 1: |
| 169 | + # 如果有一个子节点,则让子节点上移替换该节点(该节点消失) |
| 170 | + if node.left: |
| 171 | + n = node.left |
| 172 | + else: |
| 173 | + n = node.right |
| 174 | + if parent: |
| 175 | + if parent.left is node: |
| 176 | + parent.left = n |
| 177 | + else: |
| 178 | + parent.right = n |
| 179 | + del node |
| 180 | + else: |
| 181 | + # 如果有两个子节点,则要判断节点下所有叶子 |
| 182 | + parent = node |
| 183 | + successor = node.right |
| 184 | + while successor.left: |
| 185 | + parent = successor |
| 186 | + successor = successor.left |
| 187 | + node.data = successor.data |
| 188 | + if parent.left == successor: |
| 189 | + parent.left = successor.right |
| 190 | + else: |
| 191 | + parent.right = successor.right |
| 192 | + |
| 193 | +在上述方法中,得到当前节点下的子节点数目后,需要进行三种情况的判断 |
| 194 | + |
| 195 | +- 如果没有子节点,直接删除 |
| 196 | +- 如果有一个子节点,要将下一个子节点上移到当前节点,即替换之 |
| 197 | +- 如果有两个子节点,要对自己点的数据进行判断,并从新安排节点排序 |
| 198 | + |
| 199 | +上述方法中用到了统计子节点数目的方法,代码如下: |
| 200 | + |
| 201 | + def children_count(self): |
| 202 | + """ |
| 203 | + 子节点个数 |
| 204 | + """ |
| 205 | + cnt = 0 |
| 206 | + if self.left: |
| 207 | + cnt += 1 |
| 208 | + if self.right: |
| 209 | + cnt += 1 |
| 210 | + return cnt |
| 211 | + |
| 212 | +例1:删除数据为1的节点,它是3的子节点,1后面没有子节点 |
| 213 | + |
| 214 | + root.delete(1) |
| 215 | + |
| 216 | + |
| 217 | + |
| 218 | +例2:删除数据为14的节点,它是10的子节点,它下面有唯一一个子节点13,13替换之。 |
| 219 | + |
| 220 | + root.delete(14) |
| 221 | + |
| 222 | + |
| 223 | + |
| 224 | +例3:来个复杂的,删除节点数据为3的节点,它下面有两个节点,而节点6下面又有两个4,7。需要一个临时变量successor,将节点3下面的子节点进行查询,并把小于3下面的第一级子节点6左测节点数据4(该数据一定小于其父节点6)替换当前节点3,维持二叉树结构。如下图: |
| 225 | + |
| 226 | + root.delete(3) |
| 227 | + |
| 228 | + |
| 229 | + |
| 230 | +**比较两个二叉树** |
| 231 | + |
| 232 | +比较两个二叉树的方法中,只要有一个节点(叶子)与另外一个树的不同,就返回False,也包括缺少对应叶子的情况。 |
| 233 | + |
| 234 | + def compare_trees(self, node): |
| 235 | + """ |
| 236 | + 比较两棵树 |
| 237 | + """ |
| 238 | + if node is None: |
| 239 | + return False |
| 240 | + if self.data != node.data: |
| 241 | + return False |
| 242 | + res = True |
| 243 | + if self.left is None: |
| 244 | + if node.left: |
| 245 | + return False |
| 246 | + else: |
| 247 | + res = self.left.compare_trees(node.left) |
| 248 | + if res is False: |
| 249 | + return False |
| 250 | + if self.right is None: |
| 251 | + if node.right: |
| 252 | + return False |
| 253 | + else: |
| 254 | + res = self.right.compare_trees(node.right) |
| 255 | + return res |
| 256 | + |
| 257 | +例如,比较tree(3,8,10)和tree(3,8,11) |
| 258 | + |
| 259 | + #root2 是tree(3,8,11)的根 |
| 260 | + #root 是tree(3,8,10)的根 |
| 261 | + root.compare_trees(root2) |
| 262 | + |
| 263 | +执行上面的代码,程序会这么走: |
| 264 | + |
| 265 | +1. root调用compare_trees()方法 |
| 266 | +2. root有左侧子节点,调用该节点的compare_trees() |
| 267 | +3. 两个左侧子节点比较,返回true |
| 268 | +4. 按照前面的过程,比较右侧节点,发现不同,则返回False |
| 269 | + |
| 270 | +**打印树** |
| 271 | + |
| 272 | +把二叉树按照一定的顺序打印出来。不需要参数了。做法就是先左后右(左小于右)。 |
| 273 | + |
| 274 | + def print_tree(self): |
| 275 | + """ |
| 276 | + 按顺序打印数的内容 |
| 277 | + """ |
| 278 | + if self.left: |
| 279 | + self.left.print_tree() |
| 280 | + print self.data, |
| 281 | + if self.right: |
| 282 | + self.right.print_tree() |
| 283 | + |
| 284 | +操作一下: |
| 285 | + |
| 286 | + root.print_tree() |
| 287 | + |
| 288 | +输出: 1, 3, 4, 6, 7, 8, 10, 13, 14 |
| 289 | + |
| 290 | +**包含所有树元素的生成器** |
| 291 | + |
| 292 | +创建一个包含所有树元素的生成器,有时候是有必要的。考虑到内存问题,没有必要实时生成所有节点数据列表,而是要每次调用此方法时,它返回的下一个节点的值。为此,使用它返回一个对象,并停止在那里,那么该函数将在下一次调用方法时从那里继续通过yield关键字返回值。在这种情况下,要使用堆栈,不能使用递归。 |
| 293 | + |
| 294 | + def tree_data(self): |
| 295 | + """ |
| 296 | + 二叉树数据结构 |
| 297 | + """ |
| 298 | + stack = [] |
| 299 | + node = self |
| 300 | + while stack or node: |
| 301 | + if node: |
| 302 | + stack.append(node) |
| 303 | + node = node.left |
| 304 | + else: |
| 305 | + node = stack.pop() |
| 306 | + yield node.data |
| 307 | + node = node.right |
| 308 | + |
| 309 | +举例,通过循环得到树: |
| 310 | + |
| 311 | + for data in root.tree_data(): |
| 312 | + print data |
| 313 | + |
| 314 | + |
| 315 | + |
| 316 | +程序会按照先左后右边的原子将数据入栈、出栈,顺序取出值,并返回结果 |
0 commit comments