菜鸟修行之路----数据结构:树之二叉树(1)
树的简单定义:
树(tree)是包含n(n>=0)个结点的有穷集,其中:
(1)每个元素称为结点(node);
(2)有一个特定的结点被称为根结点或树根(root)。
(3)除根结点之外的其余数据元素被分为m(m≥0)个互不相交的集合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。
最典型以及最重要的树–二叉树
1.二叉树(binary tree)基础
二叉树,顾名思义,就是这种树最多有2个孩子节点,一个为左孩子(left child),一个为右孩子(right child)。
二叉树的性质:
- 二叉树的第i层至多有
2^(i-1)
个结点 - 深度为k的二叉树至多有
(2^k)-1
个结点。 - 若叶子结点为
n0
,度为2的结点数为n2
,则n0 = n2 + 1
- 性质4:具有n个结点的完全二叉树的深度为 logn + 1 (log n向下取整)
- 性质5:n个结点,按层序编号,对任一结点 i :
如果 i =1,则结点是根。
如果 i >1,则其双亲结点为 i/2 (向下取整)
如果 2i > n,则 i 无左孩子;否则其左孩子为结点 2i .
如果 2i + 1 > n,则 i 无右孩子;否则其右孩子为 2i+1 .
二叉树中存在2类特殊形式:满二叉树以及完全二叉树。
满二叉树:一个二叉树的所有非叶子节点都存在左右孩子并且所有叶子节点都在同一层级上。
完全二叉树:对于一个n个节点的二叉树,对其编号(按照层级顺序),若其1到n节点编号与同样深度的满二叉树编号相同,这个数就是完全二叉树。
具体结构如下图所示:
1.2 二叉树的存储结构
二叉树一般用2种基础结构来存储:
- 链式存储结构
- 数组
1.2.1 链式存储结构
采用一个二叉链表来存储二叉树,该链表的每个节点包含3部分:
- 存储数据的data
- 指向左孩子的left指针
- 指向右孩子的right指针
1.2.2 顺序存储结构–数组
采用数组存储时,按照层级顺序将二叉树的节点放到数组的对应位置上(按照满二叉树存放,某个节点空缺,对应数组中也空出来)。
所以节点满足:若一个节点是parent,则
left=2*parent+1
right=2*parent-1
缺点:比较浪费内存空间。(二叉堆(特殊的满二叉树)采用数组存储)
1.3 二叉树的代码实现
采用java语言实现二叉树。
逻辑分析:二叉树的生成过程
具体代码实现:
先将数据遍历出来按照顺序存储到ArrayList(数组)中,然后在根据二叉树在数组中的存储方式来构建二叉树。构建完成的二叉树依旧存放在数组中
public class BinaryTree<E> {
//根节点
private Node<E> _root;
//使用列表暂时保存节点
private List<Node<E>> nodes=new ArrayList<>();
//构建二叉树
public void createTree(E[] array){
if (array.length==0){
return ;
}
for(E e:array){
nodes.add(new Node<>(null,null,null,e));
}
//获取根节点
if(nodes.get(0)!=null){
_root=nodes.get(0);
}
//根据二叉树在数组中的存储位置来确定每个节点的子节点
for(int i=0;i>nodes.size();i++){
//确定左孩子
if((i*2+1)<nodes.size()&&nodes.get(i*2+1)!=null){
nodes.get(i).left=nodes.get(i*2+1);
}
//确定右孩子
if((i*2+2)<nodes.size()&&nodes.get(i*2+2)!=null){
nodes.get(i).right=nodes.get(i*2+2);
}
}
}
//节点类
static class Node<E>{
//父节点
Node <E> parent;
//左孩子节点
Node <E> left;
//右孩子节点
Node <E> right;
//节点中存储的数据
E data;
public Node(Node<E> parent, Node<E> left, Node<E> right, E data) {
this.parent = parent;
this.left = left;
this.right = right;
this.data = data;
}
//将存储的数据转化为字符串
@Override
public String toString(){
return data.toString();
}
}
//测试
public static void main(String[] args) {
Integer[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8};
BinaryTree<Integer> binaryTree = new BinaryTree<>();
binaryTree.createTree(array);
}
}
1.4 二叉树的遍历
二叉树的遍历分类,一般分为2大类:深度优先遍历和广度优先遍历。
深度优先遍历:
- 前序遍历:根节点–>左节点–>右节点
- 中序遍历:左节点–>根节点–>右节点
- 后序遍历:左节点–>右节点–>根节点
广度优先遍历—层序遍历
1.4.1深度优先遍历
遍历二叉树例子:
**遍历的代码实现:**采用递归的思路
/**
* 先序遍历
* @param node
*/
public void preOrder(Node<E> node){
if (node==null){
return;
}
System.out.print(node+"");
preOrder(node.left);
preOrder(node.right);
}
/**
* 中序遍历
* @param node
*/
public void inOrder(Node<E> node){
if(node==null){
return;
}
inOrder(node.left);
System.out.print(node+"");
inOrder(node.right);
}
/**
* 后续遍历
* @param node
*/
public void postOrder(Node<E> node){
if(node==null){
return;
}
preOrder(node.left);
preOrder(node.right);
System.out.print(node+"");
}
1.4.2 广度优先遍历
层次遍历:根据根节点与子节点的层次关系,一层一层的横向遍历各个节点。
层次遍历举例:
层次遍历的代码实现:
设计思路:依旧采用递归思路,并且采用队列辅助实现:每当根节点出队时,左右子节点依次入队
/**
* 层次遍历
* @param root
*/
public void levelOeder(Node<E> root){
Queue<Node> queue=new LinkedList<Node>();
//入队
queue.offer(root);
while (!queue.isEmpty()){
//出队
Node node=queue.poll();
System.out.print(node.data);
if (node.left!=null){
queue.offer(node.left);
}
if (node.right!=null){
queue.offer(node.right);
}
}
}
1.5 数据结构的拓展:递归与栈
对于递归来说,代码实现较为简单,但是时间复杂度相对高一些,通过空间换时间的思路,一般的递归问题都可以用栈来解决。
例如本节中二叉树的前序遍历,采用栈实现(使用栈进行回溯):
/**
* 非递归的层次遍历
* @param root
*/
public void preOrderWithStack(Node root){
Stack<Node> stack=new Stack<Node>();
Node node=root;
while(node!=null||!stack.isEmpty()){
//迭代访问节点的左孩子,并且入栈
while (node!=null){
System.out.print(node.data);
stack.push(node);
node=node.left;
}
//如果没有左孩子,则弹出栈顶结点,访问结点右孩子
if(!stack.isEmpty()){
node=stack.pop();
node=node.right;
}
}
}
修行之路艰辛,与君共勉
----2020年3月 成都