树结构集合和数组,链表和哈希表等的一些优点。
树的术语:
1.结点的度(Degree):结点的子树个数.
2.树的度:树的所有结点中最大的度数. (树的度通常为结点的个数N-1)
3.叶结点(Leaf):度为0的结点. (也称为叶子结点)
4.父结点(Parent):有子树的结点是其子树的根结点的父结点
5.子结点(Child):若A结点是B结点的父结点,则称B结点是A结点的子结点;子结点也称孩子结点。
6.兄弟结点(Sibling):具有同一父结点的各结点彼此是兄弟结点。
7.路径和路径长度:从结点n1到nk的路径为一个结点序列n1 , n2,… , nk, ni是 ni+1的父结点。路径所包含 边的个数为路径的长度。
8.结点的层次(Level):规定根结点在1层,其它任一结点的层数是其父结点的层数加1。
9.树的深度(Depth):树中所有结点中的最大层次是这棵树的深度。
树的表示
-
树可以有多种表示的方式.
-
最普通的表示方式:
-
儿子-兄弟表示法
-
儿子-兄弟表示法旋转
当儿子兄弟表示法旋转之后,我们可以看出,他就变成了二叉树。
二叉树
如果树中每个节点最多只能有两个子节点, 这样的树就称为"二叉树"。
特性:
一个二叉树第 i 层的最大结点数为:2(i-1), i >= 1;
深度为k的二叉树有最大结点总数为: 2k - 1, k >= 1;
对任何非空二叉树 T,若n0表示叶结点的个数、n2是度为2的非叶结点个数,那么两者满足关系n0 = n2 + 1。
特殊的二叉树
满二叉树:深度为k且含有2k-1个节点的二叉树。
特点:每一层上的结点数都是最大结点数,即每一层i的结点数都具有最大值2i-1
完全二叉树:深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树数的编号从1到n都一一对应时,称为完全二叉树。
特点:
- 除二叉树最后一层外, 其他各层的节点数都达到最大个数.
- 且最后一层从左向右的叶结点连续存在, 只缺右侧若干节点.
- 满二叉树是特殊的完全二叉树.
二叉树的存储
一、线性存储
存储完全二叉树:按从上至下、从左到右顺序存储
存储非完全二叉树:非完全二叉树要转成完全二叉树才可以按照上面的方案存储.会造成空间浪费。
二、链式存储
- 二叉树最常见的方式还是使用链表存储.
- 每个结点封装成一个Node, Node中包含存储的数据, 左结点的引用, 右结点的引用.
二叉搜索树(BST)
满足的条件:左子树的值总是小于根结点的值,右子树的值总是大于根结点的值。
缺点:二叉搜索树在某些情况下是存在很大的缺陷的,例如,当我们连续的插入一组由有序的数值时,可能会导致二叉搜索树的不平衡,查找效率降低。正常的查找效率可以到达logN,但是非平衡的二叉树的搜索效率会降低到N。
实现二叉搜索树:
class Node {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
//二叉搜索树
class binarySearchTree {
constructor() {
this.root = null;
}
//插入数据
insert(key) {
let newNode = new Node(key);
//如果没有根节点,直接插入到根节点当中
if (this.root == null) {
this.root = newNode;
} else {
//插入子节点当中
this.insertNode(this.root, newNode);
}
}
//不是根节点
insertNode(node, newNode) {
if (newNode.key < node.key) {
//准备向左节点插入
if (node.left == null) {
//如果左节点为null,则插入左节点
node.left = newNode;
} else {
//不为null
this.insertNode(node.left, newNode);
}
} else {
if (node.right == null) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
//先序遍历
//handler为回调函数,由外部决定处理输出的格式
preOrderTraversal(handler) {
this.preOrderTraversalNode(this.root, handler);
}
//先序递归遍历
preOrderTraversalNode(node, handler) {
if (node != null) {
//先处理当前结点
handler(node.key);
//处理左结点
this.preOrderTraversalNode(node.left, handler);
//处理有结点
this.preOrderTraversalNode(node.right, handler);
}
}
//中序遍历
midOrderTraversal(handler) {
this.midOrderTraversalNode(this.root, handler);
}
//中序递归遍历
midOrderTraversalNode(node, handler) {
if (node != null) {
//处理左结点
this.midOrderTraversalNode(node.left, handler);
//处理当前结点
handler(node.key);
//处理有结点
this.midOrderTraversalNode(node.right, handler);
}
}
//后续遍历
postOrderTraversal(handler) {
this.postOrderTraversalNode(this.root, handler);
}
postOrderTraversalNode(node, handler) {
if (node != null) {
//处理左结点
this.postOrderTraversalNode(node.left, handler);
//处理有结点
this.postOrderTraversalNode(node.right, handler);
//处理当前结点
handler(node.key);
}
}
//搜索最大值
searchMax() {
let node = this.root;
let key = null;
while (node != null) {
key = node.key;
node = node.right;
}
return key;
}
//搜索最小值
searchMin() {
let node = this.root;
let key = null;
while (node != null) {
key = node.key;
node = node.left;
}
return key;
}
//寻找某一个key是否存在
searchKey(key) {
return this.searchKeyNode(this.root, key);
}
searchKeyNode(node, key) {
if (node == null) {
return false;
} else {
if (key < node.key) {
return this.searchKeyNode(node.left, key);
} else if (key > node.key) {
return this.searchKeyNode(node.right, key);
} else {
return true;
}
}
}
//删除某一个key
remove(key) {
//保存临时变量
let current = this.root;
let parent = null;
let isleftChild = true;
//查找到要删除的结点
while (current.key != key) {
parent = current;
if (key < current.key) {
isleftChild = tree;
current = current.left;
} else if (key > current.key) {
isleftChild = false;
current = current.right;
}
if (current == null) return false;
}
//删除的结点分为三种情况
//1.删除的为叶子结点
if (current.left == null && current.right == null) {
//如果是根节点
if (current == this.root) {
this.root = null;
}
if (isleftChild) {
parent.left = null;
} else {
parent.right = null;
}
}
//2.删除的是有一个子节点的结点
else if (current.left == null) {
if (current == this.root) {
this.root = current.right;
}
if (isleftChild) {
parent.left = current.right;
} else {
parent.right = current.right;
}
} else if (current.right == null) {
if (current == this.root) {
this.root = current.left;
}
if (isleftChild) {
parent.left = current.left;
} else {
parent.right = current.left;
}
}
//3.删除的是由两个子节点的结点
else {
let successor = this.getSuccessor(current);
if (current == this.root) {
this.root = successor;
}
if (isleftChild) {
parent.left = successor;
} else {
parent.right = successor;
}
successor.left = current.left;
}
return true;
}
//查找后继函数
getSuccessor(delNode) {
let successorparent = delNode;
let successor = delNode;
let current = delNode.right; //从删除结点的右子树中查找
//查找后继
while (current != null) {
successorparent = successor;
successor = current;
current = current.left;
}
//如果后继不是要删除结点的右结点,需要执行一下操作
if (successor != delNode.right) {
successorparent.left = successor.right;
successor.right = delNode.right;
}
return successor;
}
}
//测试
let tree = new binarySearchTree();
tree.insert(7);
tree.insert(3);
tree.insert(9);
tree.insert(1);
tree.insert(5);
tree.insert(6);
tree.insert(2);
alert(tree.remove(3));
let result = "";
tree.midOrderTraversal((key) => {
result += key;
});
alert(result);
平衡树
avl树
avl树是最早实现的平衡树,通过平衡因子来确保是平衡树,平衡因子只能还在-1,0,1,平衡因子代表了每一个结点的左子树和右子树的高度差,如果平衡因子大于1,则此树就不是平衡树。
现在使用较少,更多使用的是红黑树。
红黑树
红黑树的规则:
- 结点是红色或者黑色
- 根结点是黑色
- 每个叶子结点都是黑色的空结点(nIL结点)
- 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
- 从任意一个结点到其每个叶子的所有路径都包含相同数据的黑色结点。
红黑树的相对平衡
从根到叶子的最长可能路径,不会超过最短可能路径的两倍。
这样保证了红黑树的相对平衡的,虽然不是绝对平衡,但是效率也是很高的。
从上图我们可以看出,最长路径为5,最短路径为3,因此从根到叶子的最长可能路径,不会超过最短可能路径的两倍。
变换规则
1.变色
当我们插入一个结点后,不符合红黑树的规则的时候,我们需要对结点进行变色,使之符合红黑树的规则。
通常情况下我们在插入结点的时候,默认是设置颜色为红色,因为插入一次可能是不违反红黑树的规则的。
2.坐旋转和右旋转
左旋转就是逆时针旋转
右旋转就是顺时针旋转
变换的情况
为了更好的理解,我们在这里定义一下名字,N代表新插入的结点,P代表N的父节点,G代表N的祖父结点,U代表N的叔叔结点。新插入的结点颜色默认为红色。
然后我们对插入的情况进行讨论:
1、新结点N位于树的根上,没有父节点
这种情况我们只需将N的颜色变为黑色即可,这样就满足性质二了。
2、新结点的父节点P是黑色
这种情况哪一条规则都没有违反,因此不用做任何处理。
3、P为红色,U也是红色
这种情况我们需要将P和U转为黑色,G转为红色。
转换后:但是转换后我们G的父节点可能也是红色,这是就违反了规则四,所以我们还需要其他的操作。
4、父红叔黑祖黑,并且N是左儿子
需要的变换为:父变为黑,祖变为红,然后右旋转
变换颜色后:
右旋转后:
5、父红叔黑组黑,并且N是右儿子
需要的变换为:
1.以P为根进行左旋转
2.自己变成黑色,祖父变成红色,然后以祖父为根进行由旋转