【路飞】数据结构--树

116 阅读5分钟

树是一种非线性的数据结构,以分层的方式存储数据。
树被用来存储具有层级关系的数据。比如文件系统中的文件
树还被用来存储有序列表。

树的定义

树由一组以连接的节点组成。
公司的组织结构图就是一个树的例子
每个方框都是一个节点,节点代表了该组织中的各个职位。
连接方框的线叫做边,边描述了各职位间的关系。

组织结构图.png

关于树的描述

一棵树最上面的节点称为【根节点】
如果一个节点下面连接多个节点,那么该节点称为【父节点】,它下面的节点称为【子节点】
一个节点可以有0个、1个或多个子节点。
没有任何子节点的节点称为【叶子节点】

树.png

描述:
1、沿着一组特定的边,可以从一个节点走到另一个与它不直接相连的节点。从一个节点到另一个节点的这一组边称为【路径】(图中虚线部分)以某种特定顺序访问树中所有的节点称为【树的遍历】。
2、树可以分为几个【层次】,根节点是第0层,它的子节点是第一层,子节点的子节点是第二层,以此类推。
3、树中的任何一层的节点可以都看作是【子树的根】,该子树包含【根节点的子节点】,【子节点的子节点】等。
4、定义树的层次就是树的【深度】。
5、每个节点都有一个与之相关的值,该值有时被称为【键】

二叉树

二叉树是一种特殊的树。
它的子节点数不超过两个。 二叉树.png
一个父节点的两个子节点分别称为【左节点】和【右节点】 左子节点比父节点小,右子节点比父节点大
即使某节点只有一个子节点,也是区分左右的。
在二叉树上查找,为二叉树添加或删除元素都非常快。 二叉树.png

二叉查找树

二叉查找树是一种特殊的二叉树,相对较小的值保存在左节点中,较大的值保存在右节点中。

实现二叉查找树

二叉查找树由节点组成,所以要先定一个一个Node构造函数

// Node 
function Node (data, left, right) {
  this.data = data;
  this.count = 1;
  this.left = left;
  this.right = right;
  this.show = show;
 
}
// 返回保存在节点中的数据
function show () {
  return this.data;
}

创建一个类,表示二叉查找树(BST)

// 首先让类只包含一个数据成员:
// 一个表示二叉查找树的根节点的Node对象
// 该类的构造函数将根节点初始化为null,以此创建一个空节点
// BST类
function BST () {
  this.root = null;
  this.insert = insert; // 插入方法
  this.inOrder = inOrder; // 遍历方法
  this.preOrder = preOrder;
  this.inOrder = inOrder;
  this.postOrder = postOrder;
  this.getMin = getMin;
  this.getMax = getMax;
  this.find = find;
  this.remove = remove;
  this.removeNode = removeNode;
  this.update = update;
}

插入方法 用来向树中插入新节点。

  • 首先检查BST中是否有根节点,如果没有,那么是棵新树,该节点就是根节点,这个方法到此结束
  • 若带插入节点不是根节点,那么需要遍历BST,找到插入的适当位置。
function insert (data) {
  let n = new Node(data, null, null);
  // 若没有根节点,这证明是新树
  if (this.root === null) {
    this.root = n;
  } else {
    // 假设根节点为当前节点
    let current = this.root
    let parent;
    while (true) {
      parent = current;
      // 判断当前带插入节点是否小于当前节点
      // 若小于并且当前节点的左节点为null,则插入并退出循环
      // 否则进入下一次循环
      if (data < current.data) {
        current = current.left;
        if (current === null) {
          parent.left = n;
          break;
        }
      } else {
        // 判断当前带插入节点是否大于当前节点
        // 若大于并且当前节点的右节点为null,则插入并退出循环
        // 否则进入下一次循环
        current = current.right;
        if (current === null) {
          parent.right = n;
          break;
        }
      }
    }
  }
  
}

connst bst = new BST();
bst.insert(23);
bst.insert(45);
bst.insert(16);
bst.insert(37);
bst.insert(3);
bst.insert(99);
bst.insert(22);

Untitled Diagram.drawio.png

遍历二叉查找树

遍历BST有三种方式:

  • 先序遍历:先访问根节点,然后以同样的方式访问左子树和右子树
  • 中序遍历:按照节点上的键值,以升序访问BST上的所有节点
  • 后序遍历:先访问叶子节点,从左子树到右子树,再到根节点

先序遍历

先序遍历.png

function preOrder (node) {
  if (!node) return [];
  const resArr = [];
  resArr.push(node.show());
  resArr.push(...preOrder(node.left))
  resArr.push(...preOrder(node.right))
  return resArr;
}
const preNums = preOrder(bst.root);
// 结果:[ 23, 16, 3, 22, 45, 37, 99 ]

中序遍历

中序遍历.png

function inOrder (node) {
  if (!node) return []
  const resArr = []
  resArr.push(...inOrder(node.left))
  resArr.push(node.show());
  resArr.push(...inOrder(node.right))
  return resArr
}
const inNums = inOrder(bst.root);
// 结果:[ 3, 16, 22, 23, 37, 45, 99 ]

后序遍历

后序遍历.png

function postOrder (node) {
  if (!node) return [];
  const resArr = [];
  resArr.push(...postOrder(node.left));
  resArr.push(...postOrder(node.right));
  resArr.push(node.show());
  return resArr;
}
const postNums = postOrder(bst.root);
// 结果:[ 3, 22, 16, 37, 99, 45, 23 ]

在二叉查找树上查找

查找最小值 由于二叉查找树的左节点永远小于当前节点,所以只需要一直遍历左子树,直到找到最后一个节点为止

function getMin (node) { 
  let current = node || this.root;
  while (current.left !== null) {
    current = current.left;
  }
  return current.data;
}

const min = node ? current : nums.getMin();
// 结果: 3

查找最大值 由于二叉查找树的右节点永远大于当前节点,所以只需要一直遍历右子树,直到找到最后一个节点为止

function getMax (node) { 
  let current = node || this.root;
  while (current.right !== null) {
    current = current.right;
  }
  return current.data
}
const max = node ? current || nums.getMax();
// 结果: 99

查找给定值 查找给定值时,需要和当前节点进行比较,若不是当前节点则可以确定是向左遍历还是向右遍历

function find (val) {
  let current = this.root;
  while (current !== null) {
    if (current.data === val) {
      return current;
    } else if (val < current.data) {
      current = current.left
    } else {
      current = current.right;
    }
  }
  return null;
 }
 
 const res = find(45);
 // 结果
//Node {
//  data: 45,
//  left:
//   Node { data: 37, left: null, right: null, show: [Function: show] },
//  right:
//   Node { data: 99, left: null, right: null, show: [Function: show] },
//  show: [Function: show] }

从二叉树上删除节点

  • 从二叉树上删除节点的操作最复杂,复杂程度取决于删除那个节点。为了管理删除操作的复杂度,方法上使用递归操作。
    1. 第一步是判断当前节点是否包含待删除的数据,若包含,则删除。若不包含,则比较当前节点上的数据和带删除的数据,来决定向左还是向右移动
    2. 若待删除的解释安是叶子节点,那么只需要将从父节点指向它的指针指向null即可。
    3. 若待删除节点只包含一个节点,那么原本指向它的节点就要做调整,使其指向其它的子节点
    4. 若待删除的节点包含两个子节点,做法有两种:要么查找待删除节点左子树上的最大值,要么查找其右子树上的最小值。
// 移除节点 只是简单的接收带删除数据,调用removeNode删除它
function remove (data) {
  let root = removeNode(this.root, data);
}

// 删除
function removeNode (node, data) {
  if (node === null) return null;
  // 待删除节点就是当前节点
  if (data === node.data) {
    // 没有子节点的节点
    if (!node.left && !node.right) {
      return null;
    }
    // 没有左子节点的节点
    if (!node.left) {
      return node.right;
    }
    // 没有右子节点的节点
    if (!node.right) {
      return node.left;
    }
    // 有两个子节点的节点
    // 找到右子树上的最小值
    let tempNode = getMin(node.right);
    console.log('tempNode', tempNode);
    node.data = tempNode.data;
    node.right = removeNode(node.right, tempNode.data);
    return node;
    
  }
  else if (data < node.data) {
    node.left = removeNode(node.left, data);
    return node
  }
  else {
    node.right = removeNode(node.right, data);
    return node;
  }
}
console.log(nums.preOrder(nums.root));
// 删除前打印: [ 23, 16, 3, 22, 45, 37, 99 ]
nums.remove(16);
console.log('remove', nums.preOrder(nums.root))
// 删除后打印: [ 23, 22, 3, 45, 37, 99 ]

二叉树的应用

BST的一个用途是记录一组数据集中出现的次数。
比如可以使用BST记录考试成绩的分布。
给定一组考试成绩,可以写一段程序将它们加入一个BST,如果某成绩尚未在BST中出现,就将其加入BST,若已经出现,就将出现的次数+1。

首先修改node对象定义,为其增加记录成绩出现次数的成员

function Node (data, left, right) {
  this.data = data;
  this.count = 1;
  this.left = left;
  this.right = right;
  this.show = show;
}

当向BST插入一条成绩(Node对象)时,将出现的频次设为1 此时BST的insert()方法还能正常工作,但是当次数增加时,就需要一个新方法更新BST中的节点。

function update (data) { 
  let grade = this.find(data);
  grade.count++;
  return grade;
}

测试程序 首先定义一个可以增加一些随机产生成绩及显示它们的函数

function genArray (len) {
  let arr = [];
  for (let i = 0; i < len; i++){
    arr[i] = Math.floor(Math.random() * Number(len + 1));
  }
  return arr;
}

定义BST类

let grades = genArray(100);
// prArray(grades);
let gradedistro = new BST();
for (let i = 0; i < grades.length; i++){
  let g = grades[i];
  let grade = gradedistro.find(g);
  if (!grade) {
    gradedistro.insert(g);
  } else {
    gradedistro.update(g);
  }
}

查看结果
注:由于时随机数,不能保证每次输入同一个数字都有结果出现。别问我是怎么知道的!!!!

console.log('45', gradedistro.find(45).count);
// 结果: 45 3