树是一种分层数据的抽象模型。它和散列表一样是一种非顺序数据结构,它对于存储需要快速查找的数据非常有用。
树的相关术语
一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点。
根节点:位于树顶部的节点叫做根节点,没有父节点。
内部节点和外部节点:树中每个元素都叫做节点,节点分为内部节点和外部节点。至少有一个子节点的节点被称为内部节点(B、D、C、E)。没有子节点的节点被称为外部节点或叶节点(G、H、I、F)。
节点的祖先和后代:一个节点(除了根节点)的祖先包括父节点、祖父节点、曾祖父节点等。一个节点的后代包括子节点、孙子节点、曾孙节点等。例如D节点的祖先节点有B节点和A节点,后代节点有G节点和H节点。
子树:子树由节点和它的后代构成。例如节点D、G、H、就构成了上图中树的一颗子树。
节点深度:节点的深度取决于它的祖先元素的节点数量。比如节点G有三个祖先节点D、B、A,所有它的深度为3。
树的高度取决于所有节点深度的最大值。一棵树可以分解成层级。根节点在0层,B节点在1层、D节点在2层、G节点在3层,以此类推。所有上图中的树高度为3。
二叉树和二叉树搜索
二叉树中的节点最多只能有两个子节点:一个是左侧几点,一个是右侧节点。
二叉树搜索树(BST):是二叉搜索树的一种,但是它只允许在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大的值。
中序遍历:是一种以上行顺序访问BST所有节点的遍历方式,也就是以从小到最大的顺序访问所有节点。
先序遍历:是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档。
后序遍历:是先访问节点的后代节点,在访问节点本身。后序遍历的一种应用是计算一个目录和它的子目录所有文件所占空间的大小。
function BinarySearchTree() {
var Node = function (key) {
this.key = key;
this.left = null;
this.right = null;
};
var insertNode = function (node, newNode) {
if(newNode.key < node.key) {
if (node.left === null) {
node.left = newNode;
} else {
insertNode(node.left, newNode);
}
} else {
if(node.right === null) {
node.right = newNode;
} else {
insertNode(node.right, newNode);
}
}
};
var inOrderTraversalNode = function (node, callback) {
//中序遍历
if(node !== null) {
inOrderTraversalNode(node.left, callback);
callback(node.key);
inOrderTraversalNode(node.right, callback);
}
};
var preOrderTraversalNode = function (node, callback) {
//先序遍历
if(node !== null) {
callback(node.key);
preOrderTraversalNode(node.left, callback);
preOrderTraversalNode(node.right, callback);
}
};
var postOrderTraversalNode = function (node, callback) {
//后序遍历
if(node !== null) {
postOrderTraversalNode(node.left, callback);
postOrderTraversalNode(node.right, callback);
callback(node.key);
}
};
var minNode = function (node) {
//查找最最小值
if(node !== null) {
while (node && node.left !== null) {
node = node.left;
}
return node.key;
}
return null;
};
var maxNode = function (node) {
//查找最大值
if(node !== null) {
while (node && node.right !== null) {
node = node.right;
}
return node.key;
}
return null;
};
var getMixNode = function (node) {
//查找键值最小的节点返回
if(node !== null) {
while (node && node.left !== null) {
node = getMixNode(node.left);
}
return node;
}
return null;
};
var searchNode = function (node, key) {
//在树中查找一个键是否存在
if(node === null) {
return false;
}
if (key < node.key) {
return searchNode(node.left, key);
}else if (key > node.key) {
return searchNode(node.right, key);
}else {
return true;
}
};
var removeNode = function (node, key) {
if (node === null) {
//当节点等于null说明不存在直接返回null
return null;
}
if(key < node.key) {
node.left = removeNode(node.left, key);
return node;
}else if(key > node.key) {
node.right = removeNode(node.right, key);
return node;
}else {
if(node.left === null && node.right === null) {
return null;
}
if(node.left === null) {
node = node.right;
return node;
}
if(node.right === null) {
node = node.left;
return node;
}
var aux = getMixNode(node.right);
node.key = aux.key;
removeNode(node.right, aux.key);
}
};
var root = null;
this.insert = function (key) {
//向树中插入一个新键
var newNode = new Node(key);
if (root === null) {
root = newNode;
}else {
insertNode(root, newNode);
}
};
this.search = function (key) {
//在树中查找一个键,如果存在返回true否则false
return searchNode(root, key);
};
this.inOrderTraversal = function (callback) {
//通过中序遍历方式遍历所有节点
inOrderTraversalNode(root, callback);
};
this.preOrderTraversal = function (callback) {
//通过先序遍历方式遍历所有节点
preOrderTraversalNode(root, callback);
};
this.postOrderTraversal = function (callback) {
//通过后续遍历方式遍历所有节点
postOrderTraversalNode(root, callback);
};
this.min = function () {
//返回树中最小值/键
return minNode(root);
};
this.max = function () {
//返回树中最大值/键
return maxNode(root);
};
this.remove = function (key) {
//从树中移除某个键
return removeNode(root, key);
};
}
var tree = new BinarySearchTree();
tree.insert(55);
tree.insert(88);
tree.insert(17);
tree.insert(65);
tree.insert(4);
tree.insert(5);
tree.insert(6);
tree.insert(5);
tree.insert(8);
tree.insert(78);
tree.insert(87);
tree.insert(27);
tree.insert(17);
tree.insert(28);
function printNode(value) {
console.log(value);
}
tree.inOrderTraversal(printNode); //4 5 5 6 8 17 17 27 28 55 65 78 87 88
tree.preOrderTraversal(printNode);//55 17 4 5 6 5 8 27 17 28 88 65 78 87
tree.postOrderTraversal(printNode);// 5 8 6 5 4 17 28 27 17 87 78 65 88 55
console.log(tree.min()); //4
console.log(tree.search(20)); //false
tree.remove(88); //false
console.log(tree.max()); //87
查找树中的最大最小值:因为在树中,最大值在树的最后一层最右侧,最小值在树的最后一层最左侧。所以通过递归(边界条件为节点是否还有左侧或者右侧子节点)的方式,当子节点为null时返回当前键。
移除一个节点:通过递归调用,查找到符合要移除的键的节点。当键比当前节点的值小,就沿着左侧继续查找。当键比当前节点的值大就沿着右侧查找。当查找到的节点没有子节点时,就会返回一个null值,然后将最后一次递归调用传入的节点(也就是和键相符合的节点)设置成返回的null值,然后返回此节点。
移除有一个左侧或者右侧的节点:当此节点左侧没有节点时,就把把对此节点的引用指向此节点的右侧子节点。当此节点右侧没有节点时,就把把对此节点的引用指向此节点的左侧子节点。
移除有两个子节点的节点:当要移除有两个字节点的节点时,它的继承者就是它右边子树中最小的节点。所以首先查找到它右侧最小节点,然后将此节点的值改为它右侧最小节点的值,然后再移除右侧最小节点,因为它已经替代了别的节点位置。