树的相关术语

位于树顶部的节点叫做根节点。他没有父节点。树中的每个元素都叫做节点,节点分为内部节点和外部节点。至少有一个子节点的节点称为内部节点。没有子元素的节点称为外部节点或叶节点。子树由节点和它的后代构成。节点的一个属性是深度,节点的深度取决于它的祖先节点的数量。比如节点有3个祖先节点,它的深度为3。树的高度取决于所有节点深度的最大值。

二叉树和二叉搜索树

二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。

二叉搜索树(BST)是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大或相等的值。

创建BinarySearchTree类

  1. function BinarySearchTree(){
  2. let Node = function(key) {
  3. this.key = key;
  4. this.left = null;
  5. this.right = null;
  6. };
  7. let root = null;
  8. }

和链表一样,将通过指针来表示节点之间的关系(术语称其为边)。因此声明一个Node类表示树中的每个节点。

键是树相关的术语中对节点的称呼。

向树中插入一个键

  1. //向树中插入一个键
  2. this.insert = function(key){
  3. var newNode = new Node(key);
  4. if(root === null){
  5. root = newNode;
  6. } else {
  7. insertNode(root,newNode);
  8. }
  9. };

第一步创建用来表示新节点的Node类实例。只需要向构造函数传递我们想用来插入树的节点值,它的左指针和右指针的值会由构造函数自动设置为null。

第二步验证这个插入操作是否是插入根节点。

第三步是将节点加在非根节点的其他位置。需要一个私有的辅助函数:

  1. var insertNode = function(node, newNode){
  2. if(newNode.key < node.key) {
  3. if(node.left === null) {
  4. node.left = newNode;
  5. } else {
  6. insertNode(node.left, newNode);
  7. }
  8. } else {
  9. if(node.right === null) {
  10. node.right = newNode;
  11. } else {
  12. insertNode(node.right, newNode);
  13. }
  14. }
  15. }

树的遍历

中序遍历

中序遍历是一种以上行顺序访问BST所有节点的遍历方式,也就是以从最小到最大的顺序访问所有节点。中序遍历的一种应用就是对树进行排序操作。

  1. this.inOrderTraverse = function(callback) {
  2. inOrderTraverseNode(root, callback);
  3. };

inOrderTraverse方法接收一个回调函数作为参数。回调函数用来定义我们对遍历到的每个节点进行的操作(这也叫访问者模式)。由于我们在BST中最常实现算法是递归,这里使用了一个私有的辅助函数,来接收一个节点和对应的回调函数作为参数。

  1. var inOrderTraverseNode = function(node, callback){
  2. if(node!==null){
  3. inOrderTraverseNode(node.left, callback);
  4. callback(node.key);
  5. inOrderTraverseNode(node.right, callback);
  6. }
  7. };

要通过中序遍历的方法遍历一棵树,首先要检查以参数形式传入的节点是否为null。然后递归调用相同的函数来访问左侧子节点。接着对这个节点进行一些操作(callback),然后再访问右侧子节点。

  1. function printNode(value){
  2. console.log(value);
  3. }
  4. tree.inOrderTraverse(printNode);

先序遍历

先序遍历是以优先于后代节点的顺序访问每个节点的。

  1. this.preOrderTraverse = function(callback){
  2. preOrderTraverseNode(root, callback);
  3. };

preOrderTraverseNode方法的实现如下:

  1. var preOrderTraverseNode = fucntion(node, callback) {
  2. if(node !== null){
  3. callback(node.key);
  4. preOrderTraverseNode(node.left, callback);
  5. preOrderTraverseNode(node.right, callback);
  6. }
  7. };

先序遍历和中序遍历的不同点是,先序遍历会先访问节点本身,然后再访问他的左侧子节点,最后是右侧子节点;中序遍历是←本→。

后序遍历

后序遍历是先访问节点的后代节点,再访问节点本身。后序遍历的一种应用是计算一个目录和它的子目录中所有文件所占空间的大小。

  1. this.postOrderTraverse = function(callback){
  2. postOrderTraverseNode(root, callback);
  3. };

postOrderTraverseNode实现方法如下:

  1. var postOrderTraverseNode = function(node, callback){
  2. if(node !== null) {
  3. postOrderTraverseNode(node.left, callback);
  4. postOrderTraverseNode(node.right, callback);
  5. callback(node.key);
  6. }
  7. };

后序遍历会先访问左侧子节点,然后是右侧子节点,最后是父节点本身。

搜索树中的值

搜索最小值和最大值

  1. //最小值
  2. this.min = function(){
  3. return minNode(root);
  4. };
  5. var minNode = function(node){
  6. if(node) {
  7. while(node && node.left !== null){
  8. node = node.left;
  9. }
  10. return node.key;
  11. }
  12. return null;
  13. };

minNode允许我们从树中任意一个节点开始寻找最小的键。我们可以使用它来找到一棵树或它的子树中最小的键。因此,我们在调用minNode方法的时候传入树的根节点,因为我们想要找到整棵树的最小键。

  1. //最大键
  2. this.max = function() {
  3. return maxNode(root);
  4. };
  5. var maxNode = function(node) {
  6. if(node) {
  7. while(node && node.right !== null) {
  8. node = node.right;
  9. }
  10. return node.key;
  11. }
  12. return null;
  13. };

搜索一个特定的值

  1. this.search = function(key) {
  2. return searchNode(root, key);
  3. }
  4. var searchNode = function(node, key) {
  5. if(node === null) {
  6. return false;
  7. }
  8. if(key < node.key) {
  9. return searchNode(node.left, key);
  10. } else if(key>node.key) {
  11. return searchNode(node.right,key);
  12. }else {
  13. return t
  14. }
  15. }

移除一个节点

先创建这个方法使它能够在树的实例上被调用:

  1. this.remove = function(key) {root = removeNode(root, key)};
  2. var removeNode = function(node, key) {
  3. //如果正在检测的节点是null,那么说明键不存在于树中,所以返回null。
  4. if(node === null) {
  5. return null;
  6. }
  7. if(key < node.key) {
  8. node.left = removeNode(node.left, key);
  9. return node;
  10. } else if (key > node.key) {
  11. node.right = removeNode(node.right, key);
  12. return node;
  13. } else { //键等于node.key
  14. //第一种情况--一个叶节点
  15. if(node.left === null && node.right === null) {
  16. node = null;
  17. //通过返回null来将对应的父节点指针赋予null值。
  18. return node;
  19. }
  20. //第二种情况--一个只有一个子节点的节点
  21. //需要跳过这个节点,直接将父节点指向它的指针指向子节点。
  22. if(node.left === null){
  23. node = node.right;
  24. return node;
  25. } else if(node.right === null) {
  26. node = node.left;
  27. return node;
  28. }
  29. //第三种情况--一个有两个子节点的节点
  30. var aux = findMinNode(node.right);
  31. node.key = aux.key;
  32. node.right = removeNode(node.right, aux.key);
  33. return node;
  34. }
  35. };
  36. //findMinNode方法如下:
  37. var findMinNode = function(node) {
  38. while (node && node.left !== null) {
  39. node = node.left;
  40. }
  41. return node;
  42. };

要移除有两个子节点的节点,需要执行4个步骤:

  • 当找到了需要移除的节点后,需要找到它右边子树中最小的节点(它的继承者)。
  • 然后用它右侧子树中最小节点的键去更新这个节点的值。通过这一步,我们改变了这个节点的键,也就是说他被移除了。
  • 但是,这样在树中就有两个相同键的节点了,这是不行的。要继续把右侧子树中的最小节点移除,毕竟它已经被移至要移除的节点位置了。
  • 最后向它的父节点返回更新后节点的引用。

红黑树