《算法导论》学习笔记之Chapter12二叉树基本特点,及二叉搜索树(查找树)
2017-12-12 15:18
387 查看
第12章 二叉搜索树
在学习二叉搜索树之前,我准备先预习一下二叉树的概念和相关算法。
二叉树拥有结合有序数组和链表的优点,查找数据和在数组中查找一样快,插入删除数据则有和链表一样高效的性能,所以是面试中必问的知识点。
二叉树的基本概念:
结点的度-结点所拥有子树的个数称为该结点的度;
叶结点-度为0的结点称为叶结点,或者终端节点;
分枝结点-度不为0的结点称为分枝结点,或者非终端结点。一棵树的结点除叶结点外,其余均为分枝结点;
左孩子、右孩子、双亲-树中一个结点的子树的根节点称为这个结点的孩子。这个结点称为他孩子节点的双亲。具有同一个双亲的孩子结点互称为兄弟;
路径、路径长度-如果一棵树的一串结点n1,n2,...,nk有如下关系:结点ni是ni+n1的父结点(1<=i<=k),就把n1,n2,...,nk称为一条由n1至nk的路径。这条路径的长度是k-1;
祖先、子孙-在树中,如果有一条路径从结点M到结点N,那么结点M就成为N的祖先,而N称为M的子孙;
结点的层数-规定树的根结点的层数为1,其余结点的层数等于他的双亲结点的层数加1;
树的深度-树中所有结点的最大层数就是树的深度;
树的度-树中各结点度的最大值称为该树的度,叶子结点的度为0。
满二叉树-在一棵二叉树中,如果所有分枝结点都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的一棵二叉树称作满二叉树;
完全二叉树-一颗深度为K的有n个结点的二叉树,对树中的结点按从上至下,从左至右的顺序进行编号,如果编号为i(1<=i<=n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,那么这棵二叉树被称为完全二叉树。完全二叉树的特点是:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。注意:满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
二叉树的基本性质:
一颗非空二叉树的第i层上最多有2.^(i-1)个结点(i>=1);
一颗深度为k的二叉树中,最多具有2.^k-1个结点,最少有k个结点;
对于一颗非空二叉树,度为0的结点(即叶子结点),总是比度为2的结点多1个,如果叶子结点数为n0,度为2的结点数为n2,则n0=n2+1;
具有n个结点的完全二叉树的深度为[log2n]+1;
对于具有n个结点的完全二叉树,如果按照从上至下,从左至右的顺序对二叉树所有结点从1开始顺序编号,则对于任意的序号为i的结点,有1:如果i>1,那么序号为i的结点的双亲结点的序号为i/2;如果i=1,那么序号为i的结点为根节点。2:如果2i<=n,那么序号为i的结点的左孩子结点为2i,如果2i>n,那么序号为i的结点无左孩子。3:如果2i+1<=n,那么序号为i的结点的右孩子结点为2i+1,如果2i+1>n,那么序号为i的结点无右孩子。
注:如果对二叉树的根结点从0开始编号,那么相应的i号结点的双亲结点的编号为(i-1)/2,左孩子的编号为2i+1,右孩子的结点为2i+2;
二叉排序树:又称二叉查找树,二叉查找树。在不为空的前提下,具有如下性质:1如果左子树不为空,那么左子树上所有结点的值均小于它的根结点的值;2如果右子树不为空,那么右子树上所有结点的值均大于根结点的值;3左右子树也分别为二叉排序树。
下面用代码实现如何构建二叉排序树,以及对构建的二叉树进行多种方式的遍历(中序遍历,先序遍历,后序遍历和层序遍历):
上面是遍历的递归实现,这里给出一个参考链接,给出的有非递归遍历的实现,使用栈来保存结点。http://blog.csdn.net/lr131425/article/details/60755706
下面再介绍一个二叉树的应用:如何求二叉树中结点的最大距离,思想:首先求左子树距离根结点的最大距离leftMaxDis,之后求右子树距离根结点的最大距离rightMaxDis,二叉树中结点间的最大距离为:leftMaxDis+rightMaxDis。代码实现如下:
该代码是是在上面的代码基础上添加的。运行之后结果是正确的:
中序遍历:
1
2
2
4
5
6
7
8
9
先序遍历:
2
1
8
7
4
2
6
5
9
后序遍历:
1
2
5
6
4
7
9
8
2
//最大结点距离
6
BST之所以存在是因为人们想让他干更多的事:主要目的是同时保证动态插入,删除,查询复杂度为O(log n),同时保证操作之后得到的树还是有序的。而排序就不能做到这一点。
对应的代价就是BST编码复杂度更高,同时运行时还有更高的常数复杂度。所以当用不上BST提供的那些厉害的功能的时候,就不要用BST,不然就是拜拜浪费你和你的CPU的时间。
下面给出在二叉搜索树上进行查找操作的代码:
下面是查找二叉搜索树的最大和最小元素结点:
下面介绍查找任一节点的前驱结点和后继结点的方法:
前驱节点:是该节点的左子树中的最大节点。
后继节点:是该节点的右子树中的最小节点。
二叉搜索树的删除操作:
删除函数处理4种情况:1-2行处理结点z没有左孩子的情况,3-4行处理z有一个左孩子没有右孩子的情况.5-12行处理剩下的两种情况:z有两个孩子的情形。第5行查找结点y,是z的后继结点。因为z的右子树非空,所以后继一定是这个子树中具有最小关键字的结点,因此就调用MinOfTree(z.right)。此时,y没有左孩子(因为他是z的后继结点),将y移出原来位置进行拼接,并替换树中的z。如果y是z的右孩子,那么第10-12行用y替换z并成为z的双亲的一个孩子,之后将z的左孩子转变为y的左孩子。如果y不是z的左孩子,第7-9行用y的右孩子替换y成为y的双亲的一个孩子,之后将z的右孩子变为y的右孩子。
以上除了第5行,调用MinOfTree(z.right)之外,删除操作都只花费常数时间,所以,一颗高度为h的二叉树,删除操作时间为O(h)=O(logn)。插入操作一样。
在学习二叉搜索树之前,我准备先预习一下二叉树的概念和相关算法。
二叉树拥有结合有序数组和链表的优点,查找数据和在数组中查找一样快,插入删除数据则有和链表一样高效的性能,所以是面试中必问的知识点。
二叉树的基本概念:
结点的度-结点所拥有子树的个数称为该结点的度;
叶结点-度为0的结点称为叶结点,或者终端节点;
分枝结点-度不为0的结点称为分枝结点,或者非终端结点。一棵树的结点除叶结点外,其余均为分枝结点;
左孩子、右孩子、双亲-树中一个结点的子树的根节点称为这个结点的孩子。这个结点称为他孩子节点的双亲。具有同一个双亲的孩子结点互称为兄弟;
路径、路径长度-如果一棵树的一串结点n1,n2,...,nk有如下关系:结点ni是ni+n1的父结点(1<=i<=k),就把n1,n2,...,nk称为一条由n1至nk的路径。这条路径的长度是k-1;
祖先、子孙-在树中,如果有一条路径从结点M到结点N,那么结点M就成为N的祖先,而N称为M的子孙;
结点的层数-规定树的根结点的层数为1,其余结点的层数等于他的双亲结点的层数加1;
树的深度-树中所有结点的最大层数就是树的深度;
树的度-树中各结点度的最大值称为该树的度,叶子结点的度为0。
满二叉树-在一棵二叉树中,如果所有分枝结点都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的一棵二叉树称作满二叉树;
完全二叉树-一颗深度为K的有n个结点的二叉树,对树中的结点按从上至下,从左至右的顺序进行编号,如果编号为i(1<=i<=n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,那么这棵二叉树被称为完全二叉树。完全二叉树的特点是:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。注意:满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
二叉树的基本性质:
一颗非空二叉树的第i层上最多有2.^(i-1)个结点(i>=1);
一颗深度为k的二叉树中,最多具有2.^k-1个结点,最少有k个结点;
对于一颗非空二叉树,度为0的结点(即叶子结点),总是比度为2的结点多1个,如果叶子结点数为n0,度为2的结点数为n2,则n0=n2+1;
具有n个结点的完全二叉树的深度为[log2n]+1;
对于具有n个结点的完全二叉树,如果按照从上至下,从左至右的顺序对二叉树所有结点从1开始顺序编号,则对于任意的序号为i的结点,有1:如果i>1,那么序号为i的结点的双亲结点的序号为i/2;如果i=1,那么序号为i的结点为根节点。2:如果2i<=n,那么序号为i的结点的左孩子结点为2i,如果2i>n,那么序号为i的结点无左孩子。3:如果2i+1<=n,那么序号为i的结点的右孩子结点为2i+1,如果2i+1>n,那么序号为i的结点无右孩子。
注:如果对二叉树的根结点从0开始编号,那么相应的i号结点的双亲结点的编号为(i-1)/2,左孩子的编号为2i+1,右孩子的结点为2i+2;
二叉排序树:又称二叉查找树,二叉查找树。在不为空的前提下,具有如下性质:1如果左子树不为空,那么左子树上所有结点的值均小于它的根结点的值;2如果右子树不为空,那么右子树上所有结点的值均大于根结点的值;3左右子树也分别为二叉排序树。
下面用代码实现如何构建二叉排序树,以及对构建的二叉树进行多种方式的遍历(中序遍历,先序遍历,后序遍历和层序遍历):
import java.util.LinkedList; import java.util.Queue;; public class BinaryTree { private Node root; public BinaryTree() { root = null; } // 将数据data插入到排序二叉树中 public void insert(int data) { Node newNode = new Node(data); if (root == null) { root = newNode; } else { Node cur = root; Node parent; while (true) { parent = cur; if (data < cur.data) { cur = cur.left; if (cur == null) { parent.left = newNode; return; } } else { cur = cur.right; if (cur == null) { parent.right = newNode; return; } } } } } // 将数值输入构建二叉树 public void buildBinaryTree(int[] data) { for (int i = 0; i < data.length; i++) { insert(data[i]); } } /** * 构建二叉树之后,下一个知识点就是对二叉树进行遍历,遍历输出二叉树中的数据 分为中序遍历:顺序为左结点-中间结点-右结点 * 先序遍历顺序:中间结点-左结点-右结点 后序遍历顺序为:左结点-右结点-中间结点 其实可以看出,遍历顺序是按照中间结点被遍历的顺序来划分的 * 最后还有一种:层序遍历,一层一层的遍历二叉树中的结点数据 * * @param args */ // 中序遍历,中序遍历的二叉搜索树输出是单调的,即已经排好序的序列 public void inOrder(Node localRoot) { if (localRoot != null) { inOrder(localRoot.left); System.out.println(localRoot.data + " "); inOrder(localRoot.right); } } public void inOrder() { this.inOrder(this.root); } // 先序遍历 public void preOrder(Node localRoot) { if (localRoot != null) { System.out.println(localRoot.data + " "); preOrder(localRoot.left); preOrder(localRoot.right); } } public void preOrder() { this.preOrder(this.root); } // 后序遍历 public void postOrder(Node localRoot) { if (localRoot != null) { postOrder(localRoot.left); postOrder(localRoot.right); System.out.println(localRoot.data + " "); } } public void postOrder() { this.postOrder(this.root); } /** * 二叉树的层序遍历:思想-借助队列的先入先出属性,将根结点放入队列 检查是否有子结点,将子结点依次也放入队列,逐步从队列中取出元素进行打印 * * @param args */ public void layerOrder() { if (this.root == null) { return; } Queue<Node> q = new LinkedList<Node>(); q.add(this.root); while (!q.isEmpty()) { Node n = q.poll(); System.out.println(n.data + " "); if (n.left != null) { q.add(n.left); } if (n.right != null) { q.add(n.right); } } } public static void main(String[] args) { // TODO Auto-generated method stub BinaryTree bTree = new BinaryTree(); int[] data = { 2, 8, 7, 4, 6, 2, 9, 1, 5 }; bTree.buildBinaryTree(data); System.out.println("中序遍历:"); bTree.inOrder(); System.out.println("先序遍历:"); bTree.preOrder(); System.out.println("后序遍历:"); bTree.postOrder(); } } class Node { public int data; public Node left; public Node right; public Node(int data) { this.data = data; this.left = null; this.right = null; } }
上面是遍历的递归实现,这里给出一个参考链接,给出的有非递归遍历的实现,使用栈来保存结点。http://blog.csdn.net/lr131425/article/details/60755706
下面再介绍一个二叉树的应用:如何求二叉树中结点的最大距离,思想:首先求左子树距离根结点的最大距离leftMaxDis,之后求右子树距离根结点的最大距离rightMaxDis,二叉树中结点间的最大距离为:leftMaxDis+rightMaxDis。代码实现如下:
private int max(int a, int b) { return a > b ? a : b; } public int findMaxDis(Node root) { if (root == null) { return 0; } if (root.left == null) { root.leftMaxDis = 0; } if (root.right == null) { root.rightMaxDis = 0; } if (root.left != null) { findMaxDis(root.left); } if (root.right != null) { findMaxDis(root.right); } // 计算左子树距离根结点的距离 if (root.left != null) { root.leftMaxDis = max(root.left.leftMaxDis, root.left.rightMaxDis) + 1; } // 计算左子树距离根结点的距离 if (root.right != null) { root.rightMaxDis = max(root.right.leftMaxDis, root.right.rightMaxDis) + 1; } // 获取二叉树所有结点最大距离 if (root.leftMaxDis + root.rightMaxDis > MaxLen) { MaxLen = root.leftMaxDis + root.rightMaxDis; } return MaxLen; }
该代码是是在上面的代码基础上添加的。运行之后结果是正确的:
中序遍历:
1
2
2
4
5
6
7
8
9
先序遍历:
2
1
8
7
4
2
6
5
9
后序遍历:
1
2
5
6
4
7
9
8
2
//最大结点距离
6
BST之所以存在是因为人们想让他干更多的事:主要目的是同时保证动态插入,删除,查询复杂度为O(log n),同时保证操作之后得到的树还是有序的。而排序就不能做到这一点。
对应的代价就是BST编码复杂度更高,同时运行时还有更高的常数复杂度。所以当用不上BST提供的那些厉害的功能的时候,就不要用BST,不然就是拜拜浪费你和你的CPU的时间。
下面给出在二叉搜索树上进行查找操作的代码:
// 针对二叉搜索树的查找操作,通过递归实现查找,时间复杂度为O(logn)=O(h),h为树的高度 public Node search(Node root, int k) { if (root.data == k || root == null) { return root; } if (root.data > k) { return search(root.left, k); } else { return search(root.right, k); } } // 下面是使用迭代版本的搜索操作 public Node searchOfIterative(Node root, int k) { while (root != null && k != root.data) { if (k < root.data) { root = root.left; } else { root = root.right; } } return root; }
下面是查找二叉搜索树的最大和最小元素结点:
// 搜索最大关键字元素和最小关键字元素 public int MinOfTree(Node root) { while (root.left != null) { root = root.left; } return root.data; } public int MaxOfTree(Node root) { while (root.right != null) { root = root.right; } return root.data; }
下面介绍查找任一节点的前驱结点和后继结点的方法:
前驱节点:是该节点的左子树中的最大节点。
后继节点:是该节点的右子树中的最小节点。
public int successor(Node x) { // 如果结点x有右子树,则右子树的最小值就是节点x的后继 if (x.right != null) { return MinOfTree(x.right); } // 如果x没有右子树。则x有以下两种可能: // (01) x是"一个左子树",则"x的后继结点"为 "它的父结点"。 // (02) x是"一个右子树",则查找"x的最低的父结点,并且该父结点要具有左孩子",找到的这个"最低的父结点"就是"x的后继结点"。 Node y = x.parent; while (y != null && x == y.right) { x = y; y = y.parent; } return y.data; }
二叉搜索树的删除操作:
// 删除树中的结点z public void delete(BinaryTree T, Node z) { if (z.left == null) { transplant(T, z, z.right); } else if (z.right == null) { transplant(T, z, z.left); } else { Node y = MinOfTree(z.right); if (y.parent != z) { transplant(T, y, y.right); y.right = z.right; y.right.parent = y; } transplant(T, z, y); y.left = z.left; y.left.parent = y; } } //重新布局二叉树 public void transplant(BinaryTree T, Node u, Node v) { if (u.parent == null) { T.root = v; } else if (u == u.parent.left) { u.parent.left = v; } else { u.parent.right = v; } if (v != null) { v.parent = u.parent; } }
删除函数处理4种情况:1-2行处理结点z没有左孩子的情况,3-4行处理z有一个左孩子没有右孩子的情况.5-12行处理剩下的两种情况:z有两个孩子的情形。第5行查找结点y,是z的后继结点。因为z的右子树非空,所以后继一定是这个子树中具有最小关键字的结点,因此就调用MinOfTree(z.right)。此时,y没有左孩子(因为他是z的后继结点),将y移出原来位置进行拼接,并替换树中的z。如果y是z的右孩子,那么第10-12行用y替换z并成为z的双亲的一个孩子,之后将z的左孩子转变为y的左孩子。如果y不是z的左孩子,第7-9行用y的右孩子替换y成为y的双亲的一个孩子,之后将z的右孩子变为y的右孩子。
以上除了第5行,调用MinOfTree(z.right)之外,删除操作都只花费常数时间,所以,一颗高度为h的二叉树,删除操作时间为O(h)=O(logn)。插入操作一样。
相关文章推荐
- 《算法导论》学习笔记之Chapter10基本数据结构---栈的数组实现
- 《算法导论》读书笔记之第10章 基本数据结构之二叉树
- 【二叉树】二叉搜索树创建、插入、删除、查找等操作
- 《算法导论》学习笔记(3)——二叉搜索树
- 查找二叉树的基本操作以及层次遍历
- 【二叉树】二叉搜索树创建、插入、删除、查找等操作
- 查找二叉树的基本操作以及层次遍历
- 《算法导论》学习笔记之Chapter8线性时间排序
- C语言基本数据结构之四(线性,对分,二叉树查找及二叉树删除)
- 《算法导论》— Chapter 12 二叉查找树
- 搜索二叉树的基本操作(增加,删除,查找)递归与非递归算法
- 《算法导论》学习笔记之Chapter13红黑树
- 《算法导论》— Chapter 12 二叉查找树
- 《算法导论》学习笔记之Chapter4.2矩阵乘法Strassen
- 算法导论第十二章关于搜索二叉树的建立,查询,查找最大最小元素值,查找给定结点的直接后继
- 算法导论------------基本数据结构之二叉树
- 数据结构基础5.2:二叉搜索树(BST)的基本操作(插入、查找、删除)
- 《算法导论》 — Chapter 10 基本数据结构
- 《算法导论》 — Chapter 10 基本数据结构
- 二叉树查找树的基本操作