【小基础】数据结构与算法——JS二叉搜索树

前言

大学的东西都忘的差很少了吧?下面咱们一块儿用js简单复习一下大学里《数据结构与算法》中的树。 本文仅使用js实现二叉树,实际工做中可能并用不到(瞎说什么大实话),可是面试挺喜欢问的,毕竟这是计算机相关专业的基础嘛。同时,代码里用了不少递归,这对后期对代码量优化仍是颇有帮助的。 虚拟dom,使用的就是树结构。 若是你对二叉树很熟悉,那么此文对你可能毫无价值。node

参考资料:学习JavaScript数据结构与算法面试

树数据结构简介

生活中常见树结构有企业的组织架构图、家谱图等。 一个树结构包含一系列存在父子关系的节点。每一个节点都有一个父节点(除了顶部的第一个节点,称为根结点)以及零个或多个子节点:算法

二叉树和二叉搜索树

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

二叉搜索树(BST)是二叉树的一种,可是它只容许你在左侧节点存储(比父节点)小的值, 在右侧节点存储(比父节点)大(或者等于)的值。数据结构

二叉搜索树将是本文所复习的主要内容。架构

二叉搜索树的实现

实现二叉搜索树

二话不说,直接上代码,实现一个二叉搜索树的类,复制代码,放到浏览器便可。能够直接看代码,后面再细谈。dom

function BinarySearchTree() {
  // 初始化根结点root为null
  let root = null;

  // 用于初始化节点,key为值,left right分别为左右子节点
  function Node(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  };

  // 获取树并打出
  this.getRoot = function () {
    return root
  }

  // 向树中插入新数据
  this.insert = function (key) {
    let newNode = new Node(key);
    if (root === null) {
      root = newNode;
    } else {
      insertNode(root, newNode);
    }
  };

  // 插值处理递归函数
  function insertNode(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);
      }
    }
  };

  // 中序遍历
  this.inorderTraversal = function (callback) {
    inorderTraversalNode(root, callback);
  };

  // 中序遍历处理递归函数
  function inorderTraversalNode(node, callback) {
    if (node !== null) {
      inorderTraversalNode(node.left, callback);
      callback(node.key);
      inorderTraversalNode(node.right, callback);
    }
  };

  // 先序遍历
  this.preorderTraversal = function (callback) {
    preorderTraversalNode(root, callback);
  };

  // 先序遍历递归函数
  function preorderTraversalNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      preorderTraversalNode(node.left, callback);
      preorderTraversalNode(node.right, callback);
    }
  };

  // 后序遍历
  this.postorderTraversal = function (callback) {
    postorderTraversalNode(root, callback);
  };

  // 后序遍历递归函数
  function postorderTraversalNode(node, callback) {
    if (node !== null) {
      postorderTraversalNode(node.left, callback);
      postorderTraversalNode(node.right, callback);
      callback(node.key);
    }
  };

  // 查询节点,若存在则返回true 反之 false
  this.search = function (key) {
    return searchNode(root, key);
  };

  // 查询节点递归函数
  function searchNode(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;
    }
  };

  // 查询最小值
  this.min = function () {
    return minNode(root);
  };

  function minNode(node) {
    if (node) {
      while (node && node.left !== null) {
        node = node.left;
        return node.key;
      }
      return null;
    };
  }

  // 查询最大值
  this.max = function () {
    return maxNode(root);
  };

  function maxNode(node) {
    if (node) {
      while (node && node.right !== null) {
        node = node.right;
      }
      return node.key;
    }
    return null;
  };

  // 移除一个节点
  this.remove = function (key) {
    root = removeNode(root, key);
  }
  function removeNode(node, key) {
    if (node === 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 { //键等于node.key
      //第一种状况——一个叶节点
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }
      //第二种状况——一个只有一个子节点的节点 
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }
      //第三种状况——一个有两个子节点的节点
      var aux = findMinNode(node.right);
      node.key = aux.key;
      node.right = removeNode(node.right, aux.key);
      return node;
    }
  };

  function findMinNode(node) {
    while (node && node.left !== null) {
      node = node.left;
    }
    return node;
  };
}

// 上面代码已经实现二叉搜索树,下面开始用起来!

// 先new一个树
let tree = new BinarySearchTree();
console.log('原始树为', tree.getRoot())
// 依次向树中插入数据
tree.insert(10);
tree.insert(2);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(13);
tree.insert(12);
tree.insert(14);

// 获取当前树
console.log('插值后的树为:', tree.getRoot())

// 用于遍历的回调函数
function printNode(value) {
  console.log(value);
}
// 先序遍历
console.log('下方为先序遍历结果')
tree.preorderTraversal(printNode);
// 中序遍历
console.log('下方为中序遍历结果')
tree.inorderTraversal(printNode);
// 后序遍历
console.log('下方为后序遍历结果')
tree.postorderTraversal(printNode);
// 查询
console.log('查询10的结果', tree.search(10))
// 最大值最小值
console.log('最大值为:', tree.max())
console.log('最小值为:', tree.min())
// 移除节点
console.log('移除节点 10 以前', tree.getRoot())
tree.remove(10)
console.log('移除节点 10 以后', tree.getRoot())
复制代码

声明一个Node类表示节点

二叉树须要记录当前节点的值,以及其2个子节点,此处以left right 分别表明左子节点和右子节点。 当建立新当节点时,new 一个Node类便可。koa

function Node(key) {
  this.key = key;
  this.left = null;
  this.right = null;
};
复制代码

实现插值函数

插入一个新的值,就是插入一个新节点。这个时候就须要new一个Node类了。若是根结点不存在,即root为null,则表示当前值为第一个节点。不然,则须要一个插值函数用来循环插值。 在二叉树中,基本都是递归,因此这块须要详细思考一下代码的具体运行方式。 代码解释见注释:函数

// 向树中插入新数据
this.insert = function (key) {
  let newNode = new Node(key);
  if (root === null) {
    root = newNode;
  } else { // 若是根结点不为空,则就须要调用插值函数来计算往哪插值了
    insertNode(root, newNode);
  }
};

// 插值处理递归函数
function insertNode(node, newNode) {
  // 若是要插入的值小于当前所在节点的值
  if (newNode.key < node.key) {
    // 若是新值小于当前节点的值且左侧子节点为空,则当前节点的左子节点就为新插入的值 (1)
    if (node.left === null) {
      node.left = newNode;
    } else {
      // 若是当前节点左侧子节点不为空,则把当前节点的左子节点传入insertNode中,开始递归,直到知足上面的 (1)才能顺利插入值 
      insertNode(node.left, newNode);
    }
  } else {  // 若是要插入的值大于当前所在节点的值,则说明要插的值须要在右侧子节点,递归逻辑同上
    if (node.right === null) {
      node.right = newNode;
    } else {
      insertNode(node.right, newNode);
    }
  }
};
复制代码

实现先序、中序、后续遍历

这3种遍历没有本质区别,只不过是回调函数的位置不一样而已。对比代码一看即懂。post

// 中序遍历
this.inorderTraversal = function (callback) {
  // 传root,完整的树,做为初始值
  inorderTraversalNode(root, callback);
};

// 下面才是遍历的真正方法
function inorderTraversalNode(node, callback) {
  // 最开始root为完整的树,当node不为空,就一直递归。为空时,则说明遍历完毕
  if (node !== null) {
    // node不为空时,继续调用,查看左子节点是否为空。若是左子节点不为空,则还会再次进入方法,直到左子节点为null
    inorderTraversalNode(node.left, callback);
    // 这个回调函数的调用有点相似koa的洋葱圈模型。当inorderTraversalNode递归到左子节点为空时,才不会继续调用。
    // 因此最早最早执行 callback(node.key) 中的节点值是最小的,而后依次愈来愈大。
    callback(node.key);
    inorderTraversalNode(node.right, callback);
  }
};
复制代码

节点查询

节点查询就比较简单了,就是循环全部节点,看看是否有相同的值,若是有就返回true,不然false

// 查询节点
this.search = function (key) {
  return searchNode(root, key);
};

// 查询节点递归函数
function searchNode(node, key) {
  // 此处每次递归时,都会走到。若是全部节点都走完了,也没走到(1) ,那就放弃吧,确实没有
  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;  // 相等(1)
  }
};
复制代码

查询最大、最小值

这个也比较简单,在二叉树中,最小值确定在左侧,最大值确定在右侧。因此查询最小值,只要循环树左侧的节点,直到节点没了。下图的箭头分别表明寻找最大最小值的访问路径。 这里有个特殊状况须要处理,那就是树自己就为null,直接返回null。最大值同理。

// 查询最小值
this.min = function () {
  return minNode(root);
};

function minNode(node) {
  if (node) {
    // 若是节点存在且左子节点不为空,则一直循环,把node设为node.left
    while (node && node.left !== null) {
      node = node.left;
      return node.key;
    }
    return null;
  };
}

// 查询最大值
this.max = function () {
  return maxNode(root);
};

function maxNode(node) {
  if (node) {
    while (node && node.right !== null) {
      node = node.right;
    }
    return node.key;
  }
  return null;
};
复制代码

删除节点

删除节点稍微比较复杂。见注释。 移除含有2个子节点的节点比较复杂,若是所示,须要在他的子树中(注意,是第一层子树),右子树寻找最小的节点,用这个最小的节点替换须要删除的节点。

// 移除一个节点
this.remove = function (key) {
  // 删除key后,新的树为removeNode的返回值
  root = removeNode(root, key);
}
function removeNode(node, key) {
  if (node === null) {
    return null;
  }
  if (key < node.key) {
    // 若是要删除的值小于当前节点的值,则说明还没找到那个要删除的节点。
    // 递归removeNode时,只有找到了那个节点,即执行了 (1),才会有返回值。并把当前节点的左子节点设为返回值。而后返回node。
    // 注意,给node.left赋值时,是递归赋值,node在不一样的循环指向不一样的节点
    node.left = removeNode(node.left, key);
    return node;
  } else if (key > node.key) {
    node.right = removeNode(node.right, key);
    return node;
  } else { //键等于node.key (1)
  /* * 若是逻辑走到了这边的代码,则说明已经找到了须要删除的节点。 * 可是,该节点有3种状况。 * 1. 该节点没有子节点:则说明该节点为直接设为null便可,也就是说,他的父节点的left设为null, node.left = null; * 2. 该节点有一个左或者右子节点:若左子节点为null,右子节点直接上移,替换该节点便可node = node.right;,其余相似 * 3. 该节点有2个子节点。这个时候,须要在他的子树中(注意,是第一层子树),右子树寻找最小的节点,用这个最小的节点替换须要删除的节点。 */
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }
    //第二种状况——一个只有一个子节点的节点 
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }
    //第三种状况——一个有两个子节点的节点
    var aux = findMinNode(node.right);
    node.key = aux.key;
    node.right = removeNode(node.right, aux.key);
    return node;
  }
};
// 这个方法用来寻找子树中的最小节点
function findMinNode(node) {
  while (node && node.left !== null) {
    node = node.left;
  }
  return node;
};
复制代码

平衡树 红黑树 堆积树

此类后续再详细讲。

相关文章
相关标签/搜索