二叉树(BinaryTree)
为什么要选择二叉树?
二叉树的出现是一种折中的选择.因为本人是做Java开发的,所以下面的举例都以Java为开发语言.
在Java的集合框架中,有两个常见的List,一个是ArrayList,一个是LinkedList.
ArrayList是基于数组实现的List,在数组中遍历元素很快,因为只需要获取下标就可以拿到元素,但是在增删元素的时候效率不好.设想一下,一个数组的中间插入一个元素,那么这个元素后面的元素都需要往后面挪一位,加上数组的扩容,在频繁增减元素的时候这个性能不好.
LinkedList是基于链表实现的List,里面的每个元素都会保存着上一个元素(prevNode)和下一个元素(nextNode)的内存地址,所以在增删元素方面有着很便捷的操作,只需要将插入位置的上一个元素的nextNode和下一个元素的prevNode指向插入的newNode,那么这个新的元素就加到链表中.但是这种结构有一个问题就是遍历效率不好,需要一直查找下一个元素的内存地址来遍历.
以上的问题在集合的元素数量少的时候并没有太大的区别,但是随着数据量增大的时候,性能问题开始出现.
那么,有没有什么数据接口可以平衡插入和遍历的性能呢?答案就是二叉树.
什么是二叉树?
树结构中每个节点(Node)最多只有左子树(left subtree)和右子树(right subtree)两个子节点的结构.
二叉树由这样的n(其中:n >= 0)个节点组成.所以,有可能是空集.
示意图
二叉树术语
- 深度: 二叉树最长的一条支的节点数
- 根节点: 不为空的二叉树的第一层的节点
…
二叉树的常见操作
二叉树的实现
树是由一个个节点组成,那么先实现一个节点
final class Node<T> {
//左子节点
private Node<T> leftNode;
//右子节点
private Node<T> rightNode;
//节点的值
private T value;
private Node() {}
private Node(Node<T> leftNode, Node<T> rightNode, T value) {
this.leftNode = leftNode;
this.rightNode = rightNode;
this.value = value;
}
public Node<T> getLeftNode() {
return leftNode;
}
public void setLeftNode(Node<T> leftNode) {
this.leftNode = leftNode;
}
public Node<T> getRightNode() {
return rightNode;
}
public void setRightNode(Node<T> rightNode) {
this.rightNode = rightNode;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
@Override
public String toString() {
return "Node {leftNode=" + leftNode + ", rightNode=" + rightNode + ", value=" + value + "}";
}
}
二叉树的遍历
树的遍历主要分成两种,深度优先和广度优先。深度优先又分为三种: 前序遍历, 中序遍历, 后序遍历
假设当前有一个二叉树如下:
a
/ \
b c
/ \ \
d e f
深度优先
前序遍历
前序遍历: 先遍历根节点,然后是左子节点,最后右子节点,即根左右
/**
* 前序遍历DLR
*/
public static void dlr(Node<String> node) {
if (node == null)
return;
//先是根节点
System.out.println(node.getValue());
//然后左节点
dlr(node.getLeft());
//最后右节点
dlr(node.getRight());
}
中序遍历
中序遍历: 先遍历左子节点,然后是根节点,最后是右子节点,即左根右
/**
* 中序遍历LDR
*/
public static void ldr(Node<String> node) {
if (node == null)
return;
//先是左节点
ldr(node.getLeft());
//然后根节点
System.out.println(node.getValue());
//最后右节点
ldr(node.getRight());
}
后序遍历
后序遍历: 先遍历左子节点,然后是右子节点,最后是根节点
/**
* 后序遍历LRD
*/
public static void lrd(Node<String> node) {
if (node == null)
return;
//先是左节点
lrd(node.getLeft());
//然后右节点
lrd(node.getRight());
//最后根节点
System.out.println(node.getValue());
}
广度优先
广度优先其实就是一层一层的遍历,这个需要通过队列来实现
public static void lay(Node<String> node) {
//这边的6是因为我测试的时候已经树的节点数而给的容量,实际应该维护树的节点数然后用这个树来初始化队列
Queue<Node<String>> queue = new ArrayBlockingQueue<Node<String>>(6);
System.out.println(node.getValue());
if (node.getLeft() != null)
queue.add(node.getLeft());
if (node.getRight() != null)
queue.add(node.getRight());
Node<String> _node;
while ((_node = queue.poll()) != null) {
System.out.println(_node.getValue());
if (_node.getLeft() != null)
queue.add(_node.getLeft());
if (_node.getRight() != null)
queue.add(_node.getRight());
}
}
二叉树的增删
因为此处的二叉树没有涉及到平衡等性质,所以新增和删除没有什么特殊的性质,可以任意添加和删除,将左右子节点直接进行赋值。
后续学习
接下来学习平衡二叉树、红黑树、N叉树。红黑树在HashMap中有用到,N叉树是数据库索引的一种实现,都可以学习看看。