1. 二叉树的概念
二叉树是另一种树形结构,其特点是每个结点至多只有两棵子树,并且二叉树的子树有左右之分,其次序不能任意颠倒.
二叉树使用递归的方式定义,二叉树是 n n n ( n ≥ 0 ) (n\ge 0) (n≥0) 个结点的有限集合:
- 或者为空二叉树,即 n = 0 n=0 n=0
- 或者由一个根节点和两个互不相交的被称为根的左子树和右子树组成. 左子树和右子树又分别是一棵二叉树.
二叉树是有序树,若将其左、右子树颠倒,则构成另一棵不同的二叉树.
二叉树可以使用顺序存储结构和链式存储结构,一般主要使用链式存储,其结点定义为:
public class Node<E> {
E data;
Node<E> lchild;
Node<E> rchild;
public Node(E data) {
this(data, null, null);
}
public Node(E data, Node lchild, Node rchild) {
this.data = data;
this.lchild = lchild;
this.rchild = rchild;
}
@Override
public String toString() {
return new StringJoiner(", ", Node.class.getSimpleName() + "[", "]")
.add("data=" + data)
.toString();
}
}
2. 完全二叉树的性质
对完全二叉树从上到下、从左到右的顺序依次编号 1 , 2 , ⋯ , n 1,2,\cdots,n 1,2,⋯,n,则有以下关系:
- 当 i > 1 i>1 i>1 时,结点 i i i 的双亲编号为 ⌊ i 2 ⌋ \displaystyle\lfloor \frac{i}{2}\rfloor ⌊2i⌋,即当 i i i 为偶数时,其双亲的编号为 i 2 \displaystyle\frac{i}{2} 2i,它是双亲的左孩子,当 i i i 为奇数时其双亲的编号为 i − 1 2 \displaystyle\frac{i-1}{2} 2i−1,它是双亲的右孩子
- 当 2 i ≤ n 2i\le n 2i≤n 时,结点 i i i 的左孩子编号为 2 i 2i 2i,否则无左孩子
- 当 2 i + 1 ≤ n 2i+1\le n 2i+1≤n 时,结点 i i i 的右孩子编号为 2 i + 1 2i+1 2i+1,否则无右孩子
- 结点 i i i 所在层次(深度)为 ⌊ log 2 i ⌋ + 1 \displaystyle\lfloor \log_2i\rfloor+1 ⌊log2i⌋+1
所以在使用顺序存储结构时,下标最好从 1 1 1 开始,这样和上面的规律能匹配.
3. 二叉树的遍历
3.1 辅助数据结构
3.1.1 栈
import java.util.StringJoiner;
@SuppressWarnings("unchecked")
public class Stack<E> {
private Object[] data;
private final int length;
private int top;
public Stack() {
this(10);
}
public Stack(int length) {
this.top = -1;
this.length = length;
this.data = new Object[length];
}
public void clear() {
this.top = -1;
}
public boolean isEmpty() {
return this.top == -1;
}
public boolean isFull() {
return this.top == this.length - 1;
}
public boolean push(E e) {
if (isFull()) {
return false;
}
this.data[++this.top] = e;
return true;
}
public E pop() {
if (isEmpty()) {
return null;
}
E e = (E) this.data[this.top];
this.top--;
return e;
}
public E peek() {
if (isEmpty()) {
return null;
}
return (E) this.data[this.top];
}
@Override
public String toString() {
StringJoiner s = new StringJoiner("\n\t", Stack.class.getSimpleName() + "{\n\t", "\n}");
int point = top;
while (point >= 0) {
s.add("[" + point + "] = " + data[point--]);
}
return s.toString();
}
}
3.1.2 队列
import java.util.StringJoiner;
@SuppressWarnings("unchecked")
public class Queue<E> {
private Node front;
private Node rear;
private class Node {
Object data;
Node next;
public Node(Object data) {
this(data, null);
}
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
@Override
public String toString() {
return new StringJoiner(", ", Node.class.getSimpleName() + "[", "]")
.add("data=" + data)
.toString();
}
}
public Queue() {
this("head");
}
public Queue(String name) {
this.front = new Node(name);
this.rear = this.front;
}
public void clear() {
front.next = null;
rear = front;
}
public boolean isEmpty() {
return front == rear;
}
public void offer(E e) {
Node node = new Node(e);
rear.next = node;
rear = node;
}
public E poll() {
if (isEmpty()) {
return null;
}
Node node = front.next;
if (rear == node) {
rear = front;
front.next = null;
} else {
front.next = node.next;
}
return (E) node.data;
}
public E peek() {
if (isEmpty()) {
return null;
}
return (E) front.next;
}
@Override
public String toString() {
StringJoiner s = new StringJoiner(",\n\t", this.front.data.toString() + "{\n\t", "\n}");
Node node = front.next;
while (node != null) {
s.add(node.toString());
node = node.next;
}
return s.toString();
}
}
3.2 先序遍历
先序遍历的过程是:
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
最直接的代码实现就是递归的方式:
public void preOrder(Node<T> root) {
if (root != null) {
root.print();
preOrder(root.lchild);
preOrder(root.rchild);
}
}
其迭代算法如下所示:
public void preOrder(Node<T> root) {
if (root == null) {
return;
}
Stack<Node<T>> stack = new Stack<>();
Node<T> r = root;
while (r != null || !stack.isEmpty()) {
if (r != null) {
visit(r);
stack.push(r);
r = r.lchild;
} else {
r = stack.pop();
r = r.rchild;
}
}
}
3.3 中序遍历
中序遍历的过程是:
- 中序遍历左子树
- 访问根节点
- 中序遍历右子树
最直接的代码实现就是递归的方式:
public void inOrder(Node<T> root) {
if (root != null) {
inOrder(root.lchild);
visit(root);
inOrder(root.rchild);
}
}
其迭代算法如下所示:
public void inOrder(Node<T> root) {
if (root == null) {
return;
}
Node<T> node = root;
Stack<Node<T>> stack = new Stack<>(20);
while (node != null || !stack.isEmpty()) {
while (node != null) {
stack.push(node);
node = node.lchild;
}
node = stack.pop();
visit(node);
node = node.rchild;
}
}
3.4 后序遍历
后序遍历的过程是:
- 后序遍历左子树
- 后序遍历右子树
- 访问根节点
最直接的代码实现就是递归的方式:
public void postOrder(Node<T> root){
if (root!=null){
postOrder(root.lchild);
postOrder(root.rchild);
visit(root);
}
}
其迭代算法如下所示:
public void postOrder(Node<T> root) {
if (root == null) {
return;
}
Node<T> pre = null;
Node<T> node = root;
Stack<Node<T>> stack = new Stack<>(20);
while (node != null || !stack.isEmpty()) {
while (node != null) { // 后序遍历仍然要先遍历左子树,所以如果有左子树,则当前结点先入栈,等待访问
stack.push(node);
node = node.lchild;
}
node = stack.peek(); // 不能直接出栈,因为如果右子树存在且没被遍历,还要先遍历右子树
if (node.rchild != null && node.rchild != pre) { // 如果右子树存在且没有被遍历过时
node = node.rchild; // 先遍历右子树,也需要经过一开始的操作
} else {
node = stack.pop(); // 确认确实该遍历它了才出栈
visit(node);
pre = node; // 遍历之后对于下一个结点它就是前结点,记录一下
node = null; // 后序遍历出站的结点是子树的根结点,其左右子树已经遍历完成,所以应该重置为 null,避免重复遍历
}
}
}
这种思想是比较直接的迭代算法,其实还有一种比较巧妙的后序遍历算法,它将问题转化为先序遍历的问题,但这种方法是以空间换简单,需要用到 2 个栈:
- 我们首先需要将要遍历的二叉树翻转,左右子树地位交换
- 对翻转过的二叉树按照先序遍历的顺序将所有结点进栈
- 挨个出栈访问,访问的顺序就是元二叉树后序遍历的顺序
public void postOrder(Node<T> root) {
if (root == null) {
return;
}
Node<T> r = root;
Stack<Node<T>> stack1 = new Stack<>(20);
Stack<Node<T>> stack2 = new Stack<>(20);
while (r != null || !stack1.isEmpty()) {
if (r != null) {
stack2.push(r);
stack1.push(r);
r = r.rchild;
} else {
r = stack1.pop();
r = r.lchild;
}
}
while (!stack2.isEmpty()) {
visit(stack2.pop());
}
}
3.5 层次遍历
这需要借助队列进行遍历了,我们访问的结点必须是从队列里取出来的
public void levelOrder(Node<T> root) {
if (root == null) {
return;
}
Queue<Node<T>> queue = new Queue<>();
Node<T> r = root;
queue.offer(r);
while (!queue.isEmpty()) {
r = queue.poll();
visit(r);
if (r.lchild != null) {
queue.offer(r.lchild);
}
if (r.rchild != null) {
queue.offer(r.rchild);
}
}
}
4. 二叉排序树
4.1 二叉排序树的定义
二叉排序树或者是一棵空树,或者是具有下列特性的二叉树:
- 若左子树非空,则左子树上所有结点的值均小于根结点的值
- 若右子树非空,则右子树上所有结点的值均大于根结点的值
- 左右子树也分别是一棵二叉排序树
所以对二叉树进行中序遍历,可以得到一个递增的一个有序序列
4.2 二叉排序树的查找
就是在这种结构中查询某个元素是否在其中,递归算法比较简单,这里给出迭代算法
public T bstSearch(Node<T> root, T key) {
Node<T> target = new Node<>(key);
while (root != null && root != target) { // 这里注意要实现 Comparable 接口定义比较规则,这里简单写成这个样子了
if (key < root) {
root = root.lchild;
}else{
root = root.rchild;
}
}
return root;
}
4.3 二叉排序树的插入
这个是动态创建的
public bool bstInsert(Node<T> root, T key) {
if (root == null){
Node<T> root = new Node<>(key);
return true;
}else if (root.data = key){
return false;
}else if (key < root.data){
bstInsert(root.lchild, key);
}else{
bstInsert(root.rchild, key);
}
}
4.4 二叉排序树的构造
给定数组序列构造 BST
public Node<T> BST(T[] data){ // 其实不允许泛型数组的,这里为了示意清楚
root= null;
for (T e: data){
bstInsert(root, e);
}
}
4.5 二叉排序树的删除
被删除的结点有三种情况:
- 若被删除的结点是叶结点,则直接删除
- 若被删除的结点只有一颗左子树或右子树,则将它的子树的根结点直接移动到被删除结点的位置
- 若被删除的节点有两个子树,则用它的直接后继(或前驱)替代它,并将替代的结点删除
具体参照示例如下图所示:
5. 平衡二叉树
5.1 平衡二叉树的定义
为了避免树的高度增长过快,降低二叉排序树的性能,规定在插入和删除二叉树节点时,要保证任意结点的左、右子树高度差的绝对值不超过 1,将这样的二叉树称为平衡二叉树.
定义结点左子树与右子树的高度差为该节点的平衡因子.
-
为了简单起见,假定 AVL 树的高度是 h h h, N ( h ) N(h) N(h) 表示高度为 h h h 的 AVL 树的节点数
-
为了得到高度为 h h h 的 AVL 树的最小节点数,应该尽可能用最少的节点数来填充这棵树
-
即假定填充左子树的高度为 h − 1 h−1 h−1,那么右子树的高度只能填充到 h − 2 h−2 h−2,这样,高度为 h h h 的 AVL 树的最小节点数为(假定只有根节点高度为 1 1 1): N ( h ) = N ( h − 1 ) + N ( h − 2 ) + 1 N(h)=N(h-1)+N(h-2)+1 N(h)=N(h−1)+N(h−2)+1, N ( 1 ) = 1 , N ( 0 ) = 0 N(1)=1,N(0)=0 N(1)=1,N(0)=0
-
最大结点数: N ( h ) = 2 N ( h − 1 ) + 1 N(h)=2N(h-1)+1 N(h)=2N(h−1)+1, N ( 1 ) = 1 , N ( 0 ) = 0 N(1)=1,N(0)=0 N(1)=1,N(0)=0,即满二叉树
5.2 平衡二叉树的旋转
平衡二叉树最大特点就是在插入或删除时的特殊操作,我们称之为旋转,我们先研究旋转是怎么实现的
当平衡二叉树进行插入(或删除)一个结点时,将会导致子树的高度加 1 1 1 或减 1 1 1,有可能造成平衡性的破坏,这时就要通过旋转来保证 AVL 树的平衡性,假设平衡性在 A \mathcal{A} A 结点开始破坏,即 A \mathcal{A} A 左右子树高度差为 2 2 2,主要有以下 4 4 4 中情况:
- 在结点 A \mathcal{A} A 的左孩子的左子树中插入元素——LL 旋转
- 在结点 A \mathcal{A} A 的左孩子的右子树中插入元素——LR 旋转
- 在结点 A \mathcal{A} A 的右孩子的左子树中插入元素——RL 旋转
- 在结点 A \mathcal{A} A 的右孩子的右子树中插入元素——RR 旋转
5.2.1 LL 旋转
A 的左孩子 B 绕着 A 顺时针旋转,B 的右孩子变为 A 的左孩子
5.2.2 RR 旋转
A 的右孩子 B 绕着 A 逆时针旋转,B 的左孩子变为 A 的右孩子
5.2.3 LR 旋转
先对 B 进行 RR 旋转,再对 A 进行 LL 旋转
5.2.4 RL 旋转
先对 B 进行 LL 旋转,再对 A 进行 RR 旋转
5.3 平衡二叉树的插入
这里偷懒了,用了以前 C++ 写的代码
AVLTree AVLInsert(AVLTree & r, int k) {
if (r == NULL) {
r = new AVLNode;
r->data = k; r->height = 1;
r->lchild = NULL;
r->rchild = NULL;
}else {
if (k < r->data) {
r->lchild = AVLInsert(r->lchild, k);
if (r->lchild->height - r->rchild->height == 2) {
if (k < r->lchild->data) {
r = LL(r);
}else {
r = LR(r);
}
}
}else if (k > r->data) {
r->rchild = AVLInsert(r->rchild, k);
if (r->rchild->height - r->lchild->height == 2) {
if (k < r->rchild->data) {
r = RL(r);
}else {
r = RR(r);
}
}
}
r->height = (r->lchild->height > r->rchild->height ? r->lchild->height : r->rchild->height) + 1;
}
return r;
}