查阅了不少资料,拾人牙慧,做个便于自己查阅的简单归纳。
树这种结构具备天然的高效性可以巧妙的避开我们不关心的东西,只需要根据我们的线索快速去定位我们的目标。所以说树代表着一种高效。在了解二分搜索树之前,我们不得不了解一下二叉树,因为二叉树是实现二分搜索树的基础。就像我们后面会详细讲解和实现AVL(平衡二叉树),红黑树等树结构,你不得不在此之前学习二分搜索树一样,他们都是互为基础的。
二叉树(Binary Tree)
1. 二叉树是一种特殊的树类型,最上方的唯一节点称为根节点。
2. 每个节点最多只能有两个子节点,分别叫左孩子或者右孩子。
3. 没有孩子的节点称为叶子节点,有孩子的节点则称为内节点
4. 每个子树也是一个二叉树
二叉树的类型可分为:
满二叉树:官方定义,一棵深度为 h,且有 2^h - 1 个节点称之为满二叉树。
说人话就是,从根节点到每一个叶子节点所经过的节点数都是相同的。看图一目了然
完全二叉树:官方定义,深度为 h,有 n 个节点的二叉树,当且仅当其每一个节点都与深度为 h 的满二叉树中,序号为 1 至 n 的节点对应时,称之为完全二叉树。
说人话,完全二叉树除去最后一层叶子节点,就是一颗满二叉树,并且最后一层的节点只能集中在左侧。从另一个角度来说就是将满二叉树的叶子节点从右往左删除若干个后就变成了一棵完全二叉树,也就是说,满二叉树一定是一棵完全二叉树,反之不成立。
平衡二叉树:平衡二叉树又被称为AVL树,它是一棵二叉树,又是一棵二分搜索树,平衡二叉树的任意一个节点的左右两个子树的高度差的绝对值不超过1,即左右两个子树都是一棵平衡二叉树。后面会详细介绍该树。
二叉树与数组的对比
数组:在java中,数组是一种效率最高的存储和随机访问对象引用序列的方式。数组是一种线性序列,这使得元素访问非常快速。但是为了这种快速所付出的代价是数组对象的大小被固定,并且是在其整个生命周期中不可被改变,简单的来说可以理解为数组一旦被初始化,则其长度不可被改变。数组长度定义后,会在堆内存中开辟指定长度的连续内存空间,并且JVM会自动根据数据类型分配数组元素的初始值。
总结就是,优点:随机访问。数组的查找时间是线性 O(n),但定位时间却是常量 O(1),效率极高。缺点:长度固定。一旦初始化完成,数组的大小被固定。灵活性不足。
二叉树:二叉树不是采用连续的内存存放。实际上,通常 BinaryTree 类的实例仅包含根节点实例的引用,而根节点实例又分别指向它的左右孩子节点实例,以此类推。所以关键的不同之处在于,组成二叉树的节点对象实例可以分散到堆中的任何位置,它们没必要像数组元素那样连续的存放。
如果要访问二叉树中的某一个节点,通常需要逐个遍历二叉树中的节点,来定位那个节点。它不象数组那样能对指定的节点进行直接的访问。所以查找二叉树的渐进时间是线性的 O(n),在最坏的情况下需要查找树中所有的节点。也就是说,随着二叉树节点数量增加时,查找任一节点的步骤数量也将相应地增加。
普通的二叉树确实不能提供比数组更好的性能,甚至于当二叉树全部偏向于一侧时,就成了链表结构。但是按照一定的规则来组织排列二叉树中的元素时,就可以很大程度地改善查询时间和定位时间。
二分搜索树(Binary Search Tree)
又称为二叉查找树,是一种特殊的二叉树,改善了二叉树节点查找的效率。有以下性质:
对于任意一个节点 n,
- 其左子树(left subtree)下的每个后代节点(descendant node)的值都小于节点 n 的值;
- 其右子树(right subtree)下的每个后代节点的值都大于节点 n 的值。
从二叉查找树的性质可知,BST 各节点存储的数据必须能够与其他的节点进行比较。给定任意两个节点,BST 必须能够判断这两个节点的值是小于、大于还是等于。
通过 BST 查找节点,理想情况(二叉树均衡分布,如完全二叉树)下我们需要检查的节点数可以减半,此时的查找时间复杂度为O(lg n)。而糟糕情况下(二叉树完全偏向一侧成链表结构),此时的时间复杂度为O(n),也就与在数组中查找基本类似了。
因此,BST 算法查找时间依赖于树的拓扑结构。最佳情况是 O(log2n),而最坏情况是 O(n)。
我们不仅需要了解如何在二叉查找树中查找一个节点,还需要知道如何在二叉查找树中插入和删除一个节点。
插入节点:
当向树中插入一个新的节点时,该节点将总是作为叶子节点。所以,最困难的地方就是如何找到该节点的父节点。类似于查找算法中的描述,我们将这个新的节点称为节点 n,而遍历的当前节点称为节点 c。开始时,节点 c 为 BST 的根节点。则定位节点 n 父节点的步骤如下:
- 如果节点 c 为空,则节点 c 的父节点将作为节点 n 的父节点。如果节点 n 的值小于该父节点的值,则节点 n 将作为该父节点的左孩子;否则节点 n 将作为该父节点的右孩子。
- 比较节点 c 与节点 n 的值。
- 如果节点 c 的值与节点 n 的值相等,则说明用户在试图插入一个重复的节点。解决办法可以是直接丢弃节点 n,或者可以抛出异常。
- 如果节点 n 的值小于节点 c 的值,则说明节点 n 一定是在节点 c 的左子树中。则将父节点设置为节点 c,并将节点 c 设置为节点 c 的左孩子,然后返回至第 1 步。
- 如果节点 n 的值大于节点 c 的值,则说明节点 n 一定是在节点 c 的右子树中。则将父节点设置为节点 c,并将节点 c 设置为节点 c 的右孩子,然后返回至第 1 步。
当合适的节点找到时,算法结束。从而使新节点被放入 BST 中成为某一父节点合适的孩子节点。
BST 的插入算法的复杂度与查找算法的复杂度是一样的:最佳情况是 O(log2n),而最坏情况是 O(n)。因为它们对节点的查找定位策略是相同的。
删除节点:
删除节点首先要查找要删除的节点,找到后执行删除操作。
删除节点的节点有如下几种情况:
- 删除的节点有两个子节点
- 删除的节点有一个子节点
- 删除的节点没有子节点
Case 1:节点有两个子节点情况下,涉及到节点的“位置变换”,用右子树中的最小节点替换当前节点。从右子树一直 left 到 NULL。最后会被转换为 Case 2 或 Case 3 的情况。所以对于删除有两个孩子的节点,删除的是其右子树的最小节点,最小节点的内容会替换要删除节点的内容。
Case 2:有一个子节点的情况下,将其父节点指向其子节点,然后删除该节点。
Case 3:在没有子节点的情况,其父节点指向空,然后删除该节点。
遍历:
对于线性的连续的数组来说,遍历数组采用的是单向的迭代法。从第一个元素开始,依次向后迭代每个元素。而 BST 则有三种常用的遍历方式:
- 前序遍历(Perorder traversal)
- 中序遍历(Inorder traversal)
- 后序遍历(Postorder traversal)
当然,这三种遍历方式的工作原理是类似的。它们都是从根节点开始,然后访问其子节点。区别在于遍历时,访问节点本身和其子节点的顺序不同。
前序遍历从当前节点(节点 c)开始访问,然后访问其左孩子,再访问右孩子。
中序遍历是从当前节点(节点 c)的左孩子开始访问,再访问当前节点,最后是其右节点。
后序遍历首先从当前节点(节点 c)的左孩子开始访问,然后是右孩子,最后才是当前节点本身。
贴上二叉树的java代码实现:
package tree;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
/**
* 二叉查找树(非递归版)
*/
public class Tree<V extends Comparable<? super V>> {
private Node root;
private int size;
class Node {
public Node right;
public Node left;
public Node parent;
public V value;
Node(V v, Node parent) {
this.value = v;
this.parent = parent;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
/**
* 插入
*
* @param v
* @return 返回插入后的新节点
*/
public Node insert(V v) {
Node tmp = this.root;
if (tmp == null) {
this.root = new Node(v, null);
size++;
return root;
}
int cmp;
Node parent;
do {
parent = tmp;
cmp = tmp.value.compareTo(v);
if (cmp > 0) {
tmp = tmp.left;
} else if (cmp < 0) {
tmp = tmp.right;
} else {
return null; // doNothing
}
} while (tmp != null);
Node node = new Node(v, parent);
if (cmp > 0) {
parent.left = node;
} else if (cmp < 0) {
parent.right = node;
}
size++;
return node;
}
/**
* 查找
*
* @param v
* @return
*/
public Node getNode(V v) {
Node tmp = this.root;
do {
int cmp = tmp.value.compareTo(v);
if (cmp > 0) {
tmp = tmp.left;
} else if (cmp < 0) {
tmp = tmp.right;
} else {
return tmp;
}
} while (tmp != null);
return null;
}
/**
* 移除
*
* @param v
* @return 实际发生高度变化的子树根节点
*/
public Node remove(V v) {
Node first = getNode(v);
if (first == null) return null;
/**
* Case 1:该节点有两个子节点
* 该种情况下,涉及到节点的“位置变换”,用右子树中的最小节点替换当前节点。从右子树一直 left 到 NULL。
* 最后会被转换为 Case 2 或 Case 3 的情况。
*/
if (first.left != null && first.right != null) {
Node s = first.right;
while (s.left != null) {
s = s.left;
}
// 修改first的value,完成替换,此时value为v的节点已被移除
first.value = s.value;
// first指向右子树的最小节点,此时后续问题转换为,删除此最小节点,即Case 2场景
first = s;
}
Node second = first.left != null ? first.left : first.right;
// Case 2:该节点有一个子节点,将其父节点指向其子节点,然后删除该节点。
if (second != null) {
second.parent = first.parent;
if (first.parent == null) {
root = second;
} else if (first == first.parent.left) {
first.parent.left = second;
} else {
first.parent.right = second;
}
} else {
// Case 3:该节点无子节点,其父节点的子节点指向空
if (first == first.parent.left) {
first.parent.left = null;
} else {
first.parent.right = null;
}
}
size--;
return first;
}
/**
* 前序遍历 (按遍历得到的顺序重新构建树,能得到相同的树)
*
* @param root
* @return
*/
public List<V> preOrder(Node root) {
List<V> list = new ArrayList<>();
if (root == null) {
return list;
}
ArrayDeque<Node> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
Node node = stack.pop();
list.add(node.value);
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
return list;
}
/**
* 中序遍历 (所有元素从小到大排列)
*
* @param root
* @return
*/
public List<V> midOrder(Node root) {
List<V> list = new ArrayList<>();
if (root == null) {
return list;
}
ArrayDeque<Node> stack = new ArrayDeque<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
if (!stack.isEmpty()) {
Node node = stack.pop();
list.add(node.value);
root = node.right;
}
}
return list;
}
/**
* 后序遍历
*
* @param root
* @return
*/
public List<V> backOrder(Node root) {
List<V> list = new ArrayList<>();
if (root == null) {
return list;
}
ArrayDeque<Node> stackA = new ArrayDeque<>();
ArrayDeque<Node> stackB = new ArrayDeque<>();
stackA.push(root);
while (!stackA.isEmpty()) {
Node node = stackA.pop();
stackB.push(node);
if (node.left != null) {
stackA.push(node.left);
}
if (node.right != null) {
stackA.push(node.right);
}
}
while (!stackB.isEmpty()) {
Node node = stackB.pop();
list.add(node.value);
}
return list;
}
/**
* 层次遍历 (按遍历得到的顺序重新构建树,能得到相同的树)
*
* @param root
* @return
*/
public List<V> levelOrder(Node root) {
List<V> list = new ArrayList<>();
if (root == null) {
return list;
}
ArrayDeque<Node> queue = new ArrayDeque<>();
ArrayDeque<Node> queue2 = new ArrayDeque<>();
queue.add(root);
while (!queue.isEmpty()) {
root = queue.poll();
queue2.add(root);
if (root.left != null) {
queue.add(root.left);
}
if (root.right != null) {
queue.add(root.right);
}
}
while (!queue2.isEmpty()) {
Node node = queue2.poll();
list.add(node.value);
}
return list;
}
/**
* 高度
*
* @param root
* @return
*/
public int height(Node root) {
if (root == null) {
return 0;
}
int res = 0;
ArrayDeque<Node> queue = new ArrayDeque<>();
queue.add(root);
while (!queue.isEmpty()) {
++res;
int size = queue.size();
for (int i = 0; i < size; i++) {
root = queue.poll();
if (root.left != null) {
queue.add(root.left);
}
if (root.right != null) {
queue.add(root.right);
}
}
}
return res;
}
/**
* 右旋
*
* @param root
*/
public void rightRotate(Node root) {
Node left = root.left;
// 1.1 处理root节点:将root的左节点从重新指向原左节点left的右节点
root.left = left.right;
// 2. 处理左节点的右节点: 假如left的右节点不为null,则其右节点的父亲指向root
if (left.right != null) {
left.right.parent = root;
}
// 3.处理root节点的父节点:父节点的子节点重新指向
if (root.parent != null) {
if (root == root.parent.left) {
//如果为左节点
root.parent.left = left;
} else {
//x为右节点
root.parent.right = left;
}
}
// 4. 处理左节点
left.parent = root.parent;
if (root.parent == null) {
this.root = left;
}
left.right = root;
// 1.2 处理root的左节点,因为root.parent的原值会在上面步骤上用到,所以放到后面处理,重新将parent指向root的原left节点
root.parent = left;
}
/**
* 左旋
*
* @param root
*/
public void leftRotate(Node root) {
Node right = root.right;
//1.1 将root的右节点指向原右节点的左节点
root.right = right.left;
//2. 如果原右节点的左节点不为空 把此节点的父节点指向root
if (right.left != null) {
right.left.parent = root;
}
// 3.处理root节点的父节点:父节点的子节点重新指向原节点的右节点
if (root.parent != null) {
if (root == root.parent.left) {
//如果为左节点
root.parent.left = right;
} else {
//x为右节点
root.parent.right = right;
}
}
// 4. 处理右节点
right.parent = root.parent;
if (root.parent == null) {
this.root = right;
}
right.left = root;
// 1.2 处理root的右节点,因为root.parent的原值会在上面步骤上用到,所以放到后面处理,重新将parent指向root的原left节点
root.parent = right;
}
/*
public List<List<V>> levelOrder2(Node root) {
List<List<V>> list = new ArrayList<>();
if (root == null) {
return list;
}
ArrayDeque<Node> queue = new ArrayDeque<>();
queue.add(root);
while (!queue.isEmpty()) {
List<V> line = new ArrayList<>();
int size = queue.size();
for (int i = 0; i < size; i++) {
root = queue.poll();
line.add(root.value);
if (root.left != null) {
queue.add(root.left);
}
if (root.right != null) {
queue.add(root.right);
}
}
list.add(line);
}
return list;
}*/
public Node getRoot() {
return root;
}
public int getSize() {
return size;
}
}
参考文章
https://www.cnblogs.com/gaochundong/p/binary_search_tree.html