本文使用Go语言进行描述算法
有以下数列,建立一颗二叉查找树数组
{50,22,30,16,18,43,56, 112,91,32,71,28}
使用以下的规则进行建立:数据结构
0)没有键值相等的结点函数
1)若是要插入的节点键值比当前节点小,则插入到当前节点的左子树,不然插入到当前节点的右子树工具
首先,定义二叉树节点的数据结构ui
type BNode struct{ key int value string lt, rt *BNode }
向二叉树添加新节点的操做以下spa
func add_node(node *BNode, key int) (*BNode) { if nil == node { var n BNode n.key = key node = &n } else if node.key > key { node.lt = add_node(node.lt, key) } else { node.rt = add_node(node.rt, key) } return node }
因此创建二叉查找树的过程以下code
func main(){ list := []int {50,22,30,16,18,43,56, 112,91,32,71,28} var root * BNode = nil for _, v := range list { root = add_node(root, v); } }
其中 BNode 结构中的 value 没有被使用。htm
二叉树创建好了,可是是存在于内存中,怎样才能知道建立的没问题呢?
咱们知道,对于一棵二叉树,其(中根遍历 + 先根遍历),或者(中根遍历 + 后根遍历) 能够逆向推导出二叉树的结构。 因此接下来,咱们要对二叉树进行一次中根遍历和一次先根遍历,并经过这两组数据验证下二叉树结构。
先根遍历的代码以下:
func pre_list(node *BNode) { if nil == node { return } fmt.Printf("%d ", node.key); pre_list(node.lt) pre_list(node.rt) }
中根遍历的代码以下:
func mid_list(node *BNode) { if nil == node { return } mid_list(node.lt) show_node(node) mid_list(node.rt) }
主函数代码以下:
func main(){ list := []int {50,22,30,16,18,43,56, 112,91,32,71,28} var root * BNode = nil for _, v := range list { root = add_node(root, v); } pre_list(root) fmt.Fprintf(os.Stderr, "\n") mid_list(root); fmt.Fprintf(os.Stderr, "\n") }
执行结果以下:
$ go run make_b_tree.go 50 22 16 18 30 28 43 32 56 112 91 71 16 18 22 28 30 32 43 50 56 71 91 112
咱们能够根据上面的结果动手在纸上画一下,看看有没有建立成功。呵呵,开个玩笑。后面会讲如何重建二叉树。
除了动手画出来,咱们还能够借助一些工具把它画出来,好比 Graphviz 。
下面这段代码是使用先根遍历的方法画出二叉树的代码,其做用是输出一段 dot 脚本。
func show_dot_node(node *BNode){ if nil == node { return } fmt.Printf(" %d[label=\"<f0> | <f1> %d | <f2> \"];\n", node.key, node.key) } func show_dot_line(from , to *BNode, tag string) { if nil == from || nil == to { return } fmt.Printf(" %d:%s -> %d:f1;\n", from.key, tag, to.key) } func show_list(node * BNode) { if nil == node { return } show_dot_node(node) show_dot_line(node, node.lt, "f0:sw") show_dot_line(node, node.rt, "f2:se") show_list(node.lt) show_list(node.rt) } func make_dot(root * BNode) { fmt.Printf("digraph G{\n\ node[shape=record,style=filled,color=cadetblue3,fontcolor=white];\n") show_list(root) fmt.Printf("}\n") }
主函数则变动以下:
func main(){ list := []int {50,22,30,16,18,43,56, 112,91,32,71,28} var root * BNode = nil for _, v := range list { root = add_node(root, v); } make_dot(root); }
执行结果以下:
digraph G{ node[shape=record,style=filled,color=cadetblue3,fontcolor=white]; 50[label="<f0> | <f1> 50 | <f2> "]; 50:f0:sw -> 22:f1; 50:f2:se -> 56:f1; 22[label="<f0> | <f1> 22 | <f2> "]; 22:f0:sw -> 16:f1; 22:f2:se -> 30:f1; 16[label="<f0> | <f1> 16 | <f2> "]; 16:f2:se -> 18:f1; 18[label="<f0> | <f1> 18 | <f2> "]; 30[label="<f0> | <f1> 30 | <f2> "]; 30:f0:sw -> 28:f1; 30:f2:se -> 43:f1; 28[label="<f0> | <f1> 28 | <f2> "]; 43[label="<f0> | <f1> 43 | <f2> "]; 43:f0:sw -> 32:f1; 32[label="<f0> | <f1> 32 | <f2> "]; 56[label="<f0> | <f1> 56 | <f2> "]; 56:f2:se -> 112:f1; 112[label="<f0> | <f1> 112 | <f2> "]; 112:f0:sw -> 91:f1; 91[label="<f0> | <f1> 91 | <f2> "]; 91:f0:sw -> 71:f1; 71[label="<f0> | <f1> 71 | <f2> "]; }
咱们把这段 dot 代码写入文件,btree.gv ,执行以下命令:
dot -Tpng -obtree.png btree.gv
成功的话,则会生成 btree.png 图片,以下所示:
下面根据咱们获得的(中根遍历)和(先根遍历)来重建二叉树,两组数据以下:
pre : 50 22 16 18 30 28 43 32 56 112 91 71 mid : 16 18 22 28 30 32 43 50 56 71 91 112
重建规则以下:
0)没有重复的数字
1)从(先根遍历)的数组 pre_list 中取开头的第一个数字A=pre_list[0], 这个数 A 就是这个数组所组成的树BT的树根
2)从(中根遍历)的数组 mid_list 中找到第 1)步的数字A。 在mid_list中,全部在 A 左边的数字都属于 BT 的左子树lt, 全部在 A 右边的数字,都属于 BT 的的右子树rt。
3)递归解析lt和rt两组数字
重建二叉树的代码以下:
定义二叉树节点结构和辅助函数:
type BNode struct{ key int value string lt, rt *BNode } func show_node(node * BNode) { if nil == node { return } fmt.Fprintf(os.Stderr, "%d ", node.key) } func pre_list(root *BNode) { if nil == root { return } show_node(root) pre_list(root.lt) pre_list(root.rt) } func mid_list(root *BNode) { if nil == root { return } mid_list(root.lt) show_node(root) mid_list(root.rt) }
重建二叉树
//查找一个数字在数列中的位置: func get_num_pos(list []int, num int) (int) { var pos int = -1 for i, v := range list { if num == v { pos = i break } } return pos; } //递归建树 func rebuild_tree(tree * BNode, pre, mid []int) (* BNode) { if len(pre) <= 0 || len(mid) <= 0 { return tree } //(先根遍历)的第一个数字就是这棵树的树根 root := pre[0] var pos int if pos = get_num_pos(mid, root); pos < 0 { return tree } if nil == tree { var n BNode n.key = root tree = &n } //重建左子树 tree.lt = rebuild_tree(tree.lt, pre[1 : 1 + pos], mid[:pos]) //重建右子树 tree.rt = rebuild_tree(tree.rt, pre[1 + pos :], mid[pos + 1:]) return tree } func main() { pre := []int {50, 22, 16, 18, 30, 28, 43, 32, 56, 112, 91, 71} mid := []int {16, 18, 22, 28, 30, 32, 43, 50, 56, 71, 91, 112} tree := rebuild_tree(nil, pre, mid) //重建后再进行一次(先根遍历)和一次(中根遍历),检查输出结果是否和咱们输入的相同。 pre_list(tree) fmt.Fprintf(os.Stderr, "\n") mid_list(tree) fmt.Fprintf(os.Stderr, "\n") }
执行代码以下:
$ go run rbulid_binary_tree.go 50 22 16 18 30 28 43 32 56 112 91 71 16 18 22 28 30 32 43 50 56 71 91 112
看样子结果相同 ~.~
接下来分析下二叉查找树的空间复杂度和时间复杂度。
空间复杂度比较好分析。咱们在建树的时候,是否是须要对每个数据申请一次内存呢。 每一个数据一次,那就是有多少数据,就要申请多少次,有n个数据就要 申请n次, 因此空间光是申请用于存放数据的内存次数就是n,这个和数据的规模是正相关的, 而且关系是O(x * n),其中x是每一个数据占用的内存数量。由于这个x在数据结构不变的状况下是不变的, 是不会随着数据规模而变化的,那就能够忽略,由于x是个常数,与n无关。 因此只是申请存放数据的空间的空间复杂度为O(n)。
那还有什么地方须要空间呢?就是递归的时候,须要栈空间。 树每深一层,就须要递归一次,也就须要保存一次栈空间。 在平均状况下,树的深度是lgN。可是在极端状况下,树的深度但是N啊。请看下面的图。
a树就是最差的树,这哪儿还像是一棵树啊,基本就是链表了;而b树就是一棵好树,深度最优。
因此最坏的递归建树栈空间也是O(n),不过最好的是O(lgN)。
综合来讲,空间是[O(n)+O(lgN)] ~ [2 O(n)],这里要取比较大的一个,也就是2O(n),也就是O(n)。
时间复杂度主要是考察增、删、查三个操做所面临的时间复杂度。 不管增长一个节点仍是删除一个节点,首先都是查询这个节点的位置。因此咱们首先介绍查询一个节点的时间复杂度。
5.2.1)查询一个节点的时间复杂度
仍是以上图为表明,若是要查询其中的某一个节点,好比要查询b1,须要比较的节点一次是b4->b2->b1, 因此查询b1节点须要的时间是3。若是查询b4呢,那就只须要和b4比较一次就能够了。 因此查询一个节点所须要的最大时间,是和树的深度成正比的。那么在上图b树上,时间复杂度就是O(lnN)。 那么在a树上查询呢?查a1的话,只须要和a1比较一次就行了,可是若是要查a7呢,那就须要查询7次了。 因此二叉查找树的时间复杂度是O(lgN) ~ O(n),取最坏的状况,那就是O(n)了。
5.2.2)增长一个节点的时间复杂度
增长一个节点,须要查询到该节点须要插入的位置,因此花费时间应该是在查询的基础上在+1,因此是O(n)。
5.2.3)删除一个节点的时间复杂度
二叉查找树删除节点能够分为三种状况:
a)要删除的目标节点是叶子节点。
此时只须要把这个节点删除便可,由于此节点没有子树,直接删除就能够了。以下图,删除节点2。
b)要删除的目标节点有一个子树。
i)若是只有左子树,就让这个节点的父节点指向这个节点的左子树。
ii)若是只有右子树,就让这个节点的父节点指向这个节点的右子树。以下图,删除节点3。
c)要删除的目标节点有两个子树。
i)方法一,找到要删除的节点的前驱,这个节点的前驱确定是没有右子树的,用这个节点的前驱替换这个节点,并删除这个节点。
ii)方法二,找到要删除的节点的后继,这个节点的后继确定是没有左子树的,用这个节点的后继替换这个节点,并删除这个节点。
前驱和后继的含义:
节点key的前驱,就是中序遍历时,比key小的全部节点中最大的那个节点。
节点key的后继,就是中序遍历时,比key小的全部节点中最大的那个节点。
不管是用前驱进行替换,仍是用后继进行替换,思路都是状况c)转换为状况a)或者状况b)。
使用前驱进行替换:
使用后继进行替换:
删除操做说了,那么时间复杂度呢
由于删除一个节点的时候,首先须要进行查找,以后或者直接删除这个节点, 或者使用前驱或者后继替换后进行删除,首先查找的时间复杂度是O(lgN), 直接删除的时间复杂度是O(1)。 替换删除呢,由于替换删除的时候,查找前驱或者后继的时候, 是在当前节点的基础上进行查找的,因此查找前驱或后继的时间加上查找要删除的节点的时间, 一共是O(lgN)。最坏是O(N)。
因此删除操做的时间复杂度在O(lgN)~O(N)之间。
平均来讲会小于O(N),更接近O(lnN)一些。
删除一个节点(采用前驱节点替换) Go语言描述以下:
//根据 key 值移除一个节点 func remove_node(tree * BNode, key int) (n, t *BNode){ if nil == tree { return nil,nil } //找到 key 所在的节点,删除它 if key == tree.key { n, tree = del_node(tree) } else if key > tree.key { n, tree.rt = remove_node(tree.rt, key) } else { n, tree.lt = remove_node(tree.lt, key) } return n, tree } //删除一个节点的操做 func del_node(tree * BNode) (n, t*BNode) { if nil == tree { return nil, nil } //直接删除叶子节点 if nil == tree.lt && nil == tree.rt { return tree, nil } //不是叶子节点,说明有子树存在 //没有左子树,说明只有右子树,直接返回右子树 if nil == tree.lt { return tree, tree.rt } //只有左子树存在,直接返回左子树 if nil == tree.rt { return tree, tree.lt } //左右子树都存在,获取前驱节点 n, t = get_pre_node(tree.lt) n.lt = t n.rt = tree.rt return tree, n } //获取前驱节点 func get_pre_node(node * BNode) (n, t *BNode) { if nil == node { return nil, nil } if nil != node.rt { n, node.rt = get_pre_node(node.rt) return n, node } //删除找到的前驱节点,并删除此节点后返回 return del_node(node) }
能够调用remove_node(tree, key)函数删除key对应的节点,而且返回删除的节点。