树结构,二叉搜索树(笔记)

1. 树结构与其他数据结构的优缺点

  • 数组

    优点:

    • 数组在查询和修改某个值时效率比较高,因为是根据下标直接查找
    • 如果是通过数据来寻找对应的位置,查找效率为O(N)这样的效率很低,也可以先给数组进行排序,在进行二分法查找效率为O(logN),但是给数组排序也要对应的时间

    缺点:

    • 数组在进行插入和删除操作时,效率会很低,因为数组前面的数据增加或减少时,数组后面的每一项数据的下标都要改变
  • 链表

    优点:

    • 链表在进行插入和删除操作时要比数组效率高很多,因为只需要当前数据和前面后面的数据断开引用或改变引用指向(指的是添加)就可以了

    缺点:

    • 链表在查询和修改对应值时效率会很低,因为需要从头或从尾部开始寻找
    • 其实链表在插入和删除时效率也并不是特别高,因为它还是要先查找在去做对应的操作,只是效率相对数组而言要高很多
  • 哈希表

    优点:

    • 哈希表在进行增、删、改、查时效率都非常高

    缺点:

    • 哈希表空间利用率不高,为了提升效率会浪费一定的空间
    • 哈希表底层是数组实现的,但是哈希表是无序的不能被遍历
    • 不能快速的找出最大或最小的这些特殊值
  • 树结构

    树结构的介绍:

    • 首先我们不能说树结构比其他数据结构都要好,各有所长吧
    • 但是树结构综合了上面的数据结构的优先(当然不能取代他们的优点,只是综合),并且也弥补了上面数据结构的缺点
    • 有些场景使用树结构会比较方便,树结构是非线性的,可以表示一对多的关系,比如文件的目录结构,公司的等级分化

2. 树的术语

  • 树(Tree)
    • n(n>=0)个子节点构成的有限集合
    • 当n = 0是,及为空树
    • 对于一个非空树(n>0),他具备以下的性质
      • 树中有一个根(Root)的特殊节点,用r表示
      • 其余节点可以分为m(m>0)个互不相交的有限集T1T2、…,Tm,其中每一个集合又是一颗树,称为原来树的子树(SubTree)
  • 相关术语
    1. 父节点与子节点(Parent与Child):如果有两个节点A和B,A的子节点是B的话,那么B的父节点就是A
    2. 节点的度(Degree):表示当前节点的子节点个数
    3. 树的度:表示这个树下面所有的节点中最大的度
    4. 叶节点(Leaf):度为0的节点,表示它已经没有子节点了
    5. 兄弟节点(Sibling):表示拥有同一个父节点的节点们
    6. 路径和路径长度:假如有三个节点,A是根节点,B是A的子节点,C是B的子节点,那么A到C的路径就为A-B、B-C,长度就是全部的路径(路径只会向下找)
    7. 节点的层次(Level):表示当前节点的层次,根节点在1层,他的子节点们在2层,以此类推每下一层就加1
    8. 树的深度(Depth):树中所有节点中最大的层次就是这棵树的深度

3. 树结构的表现方式

所有树的本质都可以使用二叉树模拟出来

在这里插入图片描述

比如一个根,它有三个子节点,可以每个节点都用子节点和兄弟节点来表示,这样一个根的三个子节点中,他们都有兄弟节点这个属性,都可以找到自己的兄弟,这个表示太抽象了需要画图来表示

在这里插入图片描述

4. 认识二叉树

  • 二叉树中每个节点最多只能有两个节点

    二叉树不仅仅是因为简单,也是因为基本上所有的树,都可以被二叉树模拟出来

  • 二叉树可以为空,也就是没有节点。如果不为空那么他的两个节点,称为左子节点和右子节点,这两个节点就是兄弟节点

  • 二叉树有五种形态

    1. 空树
    2. 只有自己本身
    3. 只有左子节点
    4. 只有右子节点
    5. 左子节点和右子节点都有
  • 二叉树的特征

    • 一个二叉树第 i 层的最大节点数为:2^(i - 1),i >= 1
    • 深度为 k 的二叉树的最大全部节点为:2^k - 1,k >= 1
    • n0 = n2 + 1:n0表示叶节点(度为0)的个数,n2表示度为2节点的个数
  • 完美二叉树(满二叉树):除叶节点外,每一层节点都有两个节点

  • 完全二叉树:除二叉树最后一层外,其他各层的节点数都达到最大个数。且最后一层从左向右的叶节点连续存在,只能缺右侧若干节点

在这里插入图片描述

5. 二叉树的存储

  • 数组存储:数组一般只能存储完全二叉树或完美二叉树,因为在使用数组存储是由左向右、由上向下去存储的,如果不是完美或完全树的话,存储会浪费大量空间
  • 链表存储:二叉树最常见的存储方式,每一个节点都有两个节点的引用,子左节点和子右节点

6. 二叉搜索树

6.1 认识二叉搜索树

  • 二叉搜索树( BST,Binary Search Tree ),也称二叉排序树或二叉查找树
  • 二叉搜索树是一颗二叉树,可以为空
  • 如果不为空要满足以下条件
    • 非空左子树的所有键值小于其根节点的键值
    • 非空右子树的所有键值大于其根节点的键值
    • 左、右子树本身也都是二叉搜索树
  • 二叉搜索树的查询数据就是二分法的思想,二叉搜索树也是有序的
6.2 二叉搜索树增、删、改、查实现过程
  • 添加、修改

    添加和修改的方法是一样的,当没有key时就是添加,有key时修改value即可

    在添加时,先判断根节点是否为空,为空添加给它,不为空使用递归查找深层的节点

    判断要添加的key,与当前节点的key做对比,如果小于向left走,大于向right走,直到找到null然后把这个数据添加给它

// 1.insert(key, value):向树中添加或修改某个数据
// 提示:没有key就是添加,有相同key就是修改
insert(key, value) {
  // 创建子节点
  let newNode = new Node(key, value)

  if (this.root == null) {
    this.root = newNode
  } else {
    this.insertNode(this.root, newNode)
  }
}

// 需要递归添加的节点
insertNode(node, newNode) {
  // 向左走
  if (node.key > newNode.key) {
    // 查看左子节点是否为空,为空就可以添加
    // 不为空就需要递归继续向下层找
    if (node.left == null) {
      node.left = newNode
    } else {
      this.insertNode(node.left, newNode)
    }
    // key相等,修改value
  } else if (node.key == newNode.key) {
    node.value = newNode.value

    // 向右走
  } else {
    if (node.right == null) {
      node.right = newNode
    } else {
      this.insertNode(node.right, newNode)
    }
  }
}
  • 查询

    查询使用的循环,和递归的原理一样

    当key小于当前节点时,向左走,右边也一样,匹配到key返回value,如果找到最后还没有返回代表没有该值,返回false即可

// 2.search(key):在树中查找一个键,存在返回value,不存在返回false
search(key) {
  let node = this.root

  if (node == null) return false

  while (node != null) {
    // 如果传入的key比当前树的key要小,那么就指向左子树
    if (key < node.key) {
      node = node.left
      // 找到了就返回value
    } else if (node.key == key) {
      return node.value
      // 否则指向右子树
    } else {
      node = node.right
    }
  }
  return false
}
  • 先序遍历、中序遍历、后序遍历

    代码中注释比较详细,看注释或者看下面的图都可以

在这里插入图片描述

// 3.preOrderTraverse():先序遍历
// (1).访问根节点
// (2).先序遍历所有左子树
// (3).先序遍历所有右子树
// (4).循环2、3操作
// 先序遍历是先查找根节点,在依次先查找左子树
// 如果左子树中还有左子树,也会先查找,直到左子树(多个左子树)中没有左子节点
// 就会返回返回最深层的上一层去查找右子树,在右子树中也会先查找它的左子节点
preOrderTraverse() {
  let resutlStr = this.preOrderTraverseNode(this.root)
  let resultArr = resutlStr.split(",")
  // 因为遍历到最后一个节点时,也会返回并拼上逗号
  // 所以变成数组后会多一位空项
  resultArr.pop()
  return resultArr
}

// preOrderTraverseNode():先序递归查找
preOrderTraverseNode(node) {
  let resultStr = ""
  if (node != null) {
    // 把排序好的数据用逗号拼接
    // 这个递归的返回是从内向外的,这里采用的是+=所以每当返回一个值,就会拼接到这个数的后面
    // 所以排序好之后,就是从头开始到最深处
    resultStr = node.key + ","
    // 先查找全部左子节点
    resultStr += this.preOrderTraverseNode(node.left)
    // 先查找全部右子节点
    resultStr += this.preOrderTraverseNode(node.right)

  }
  return resultStr
}

// 4.inOrderTraverse():中序遍历
// (1).中序遍历左子树
// (2).访问根节点
// (3).中序遍历右子树
// (4).循环1、3操作
// 中序遍历中的其中一个树节点,一定是先把左子树查找完才会查找右子树
// 注意:中序遍历因为是先找左在找右,所以返回的key一定是顺序的
// 因为使用的是递归,左子节点全部找完才会向上层查找
inOrderTraverse() {
  let resultStr = this.inOrderTraverseNode(this.root)
  let resultArr = resultStr.split(",")
  resultArr.pop()
  return resultArr
}

// inOrderTraverseNode():中序递归查找
inOrderTraverseNode(node) {
  let resultStr = ""
  if (node != null) {
    resultStr += this.inOrderTraverseNode(node.left)

    resultStr += node.key + ","

    resultStr += this.inOrderTraverseNode(node.right)
  }
  return resultStr
}

// 5.postOrderTraverse():后序遍历
// (1).后序遍历左子树
// (2).后序遍历右子树
// (3).循环1、2操作
// (4).访问根节点
// 后序遍历,一个树节点先遍历(都是从最深层开始查找)左子节点
// 在遍历右子节点,当一个树的子节点遍历完才会返回它本身
postOrderTraverse() {
  let resultStr = this.postOrderTraverseNode(this.root)
  let resultArr = resultStr.split(",")
  resultArr.pop()
  return resultArr
}

// postOrderTraverseNode():后序递归查找
postOrderTraverseNode(node) {
  let resultStr = ""
  if (node != null) {
    resultStr += this.postOrderTraverseNode(node.left)

    resultStr += this.postOrderTraverseNode(node.right)

    resultStr += node.key + ","
  }
  return resultStr
}
  • 删除

    删除是二叉搜索树中最难的一部分,以下会具体说明

    1. 当删除的节点为叶节点时(它没有子节点)
      • 在删除之前要先找到这个节点,找的方法和查询的一样,但是这里需要提三个变量,要删除的节点、要删除节点的父节点、一个布尔值来判断,要删除的节点为当前父节点的左子节点还是右子节点,以便删除
      • 当删除的节点为父节点时,符合叶节点的情况,表示只有一个根节点,直接为null即可
      • 找到不为父节点的叶节点后,把它的父节点的左子节点或右子节点直接为null即可(根据刚才的变量名来确定是左是右)
    2. 当删除的节点只有一个子节点时
      • 当被删除节点只有左子节点时,为根节点时表示根节点只有一个左子树,直接把左子树覆盖根节点即可
      • 为子节点时,先确定这个子节点是它父节点的左还是右,如果是左就把父节点的left,指向它子节点的左,这样中间这个要删的节点就断了联系,浏览器会垃圾回收
      • 当被删除节点只有右子节点时,原理一样
    3. 当删除的节点有两个子节点时
      • 有一个规律,就是被删除的节点为两个节点时,需要来找新的节点来代替它,在被删除节点的左子树中找最大,右子树中最小的来代替,都可以,这样不会打乱二叉搜索树的结构,这两个节点称为前驱和后继,这里采用的是后继,原理都一样只是相反就可以了
      • 这里需要额外添加三个变量,一个是后继节点的左儿子,它会一直寻找left只到找到null,一个是后继节点,后继节点的左儿子为null时代表已经找到了删除节点右子树中最小的一个节点了(后继节点),还有一个就是后继节点的父节点,方便改指向
      • 这里需要把后继的右子树赋给,指向它父节点的左节点,因为这个后继节点的左节点是一定为空的,它作为代替节点它后面的数据也不能丢失,接着他父节点left的引用,在二叉搜索树中一个树的根节点是一定会比他所有左子节点大的,所以这点不用担心,再把删除节点的右子节点等于后继的右节点
      • 如果后继节点为删除节点的右子节点,那么就代表这个后继节点的父节点就是删除节点,就不能让删除节点的右子树给它的父左节点,这样会把删除节点的左子树覆盖,会好的办法就是这里不动,类似于一个节点的做法,在这个后继节点下,他没有左节点就代表它就是最小值的,这种情况比较特殊,这里后继替换后的右节点已经处理完了
      • 把后继右边的数据指向处理完后,就来更改后继左边的指向和原删除父节点指向的问题
      • 如果原删除节点为根节点时,把原删除节点的左节点等于后继的左节点
      • 如果原删除节点不为根时,就通过父节点和删除节点是父节点的左子还是右子,来确定哪个父去指向它,然后在把原删除节点的左节点等于后继的左节点就完成了
// 8.remove(key):通过键删除某个数据
remove(key) {
  // 当前节点
  let node = this.root
  // 当前节点的上一层节点
  let prev = null
  // 记录是上一层节点的左子节点还是右子节点,以便删除
  let isLeftChild = true

  // 寻找要删除的节点
  while (node.key != key) {
    prev = node
    if (key < node.key) {
      node = node.left
      // 进入左节点
      isLeftChild = true
    } else {
      node = node.right
      // 进入右节点
      isLeftChild = false
    }

    // 表示没要找到要删除的节点
    if (node == null) return false
  }

  // 删除节点
  // 1.删除的是叶子节点(没有子节点)
  if (node.left == null && node.right == null) {
    // 表示只有一个根节点,一个子节点都没有
    if (node == this.root) {
      this.root = null
      return true
    }
    // 表示要删除的这个节点是,它父节点的左子节点
    if (isLeftChild) {
      prev.left = null
      // 反之
    } else {
      prev.right = null
    }

    // 2.删除的有一个子节点
    // 在此之前,有一个条件是node的left和right都为空
    // 所以当左为空时,右边一定不为空
  } else if (node.left == null) {
    // 当要删除的是根节点,并且根节点只有一个子节点时
    if (node == this.root) {
      this.root = node.right
      return true
    }
    // 进入的是左节点,把当前节点直接指向他的,的左子节点的右子节点
    // 这样中间要删除的那个节点就断了联系
    if (isLeftChild) {
      prev.left = node.right
    } else {
      prev.right = node.right
    }
    // 当右为空,左不为空时
  } else if (node.right == null) {
    if (node == this.root) {
      this.root = node.left
      return true
    }
    if (isLeftChild) {
      prev.left = node.left
    } else {
      prev.right = node.left
    }
    // 3.删除的有两个子节点
    // 删除这个节点,应该找所有子节点中,key值最接近它的来替换
    // 也就是左子树中最大的(前驱),或右子树中最小的(后继)
  } else {
    // 获取后继节点,因为要替换删除的节点
    let successor = this.getSuccessor(node)

    // 如果删除的为根节点,直接让后继等于它
    // 左子树右子树同理
    if (this.root == node) {
      this.root = successor
    } else if (isLeftChild) {
      prev.left = successor
    } else {
      prev.right = successor
    }
    successor.left = node.left
  }
  return true
}

// getSuccessor(delNode):寻找后继节点
getSuccessor(delNode) {
  // 要删除的节点
  let parent = delNode
  // 后继节点
  let successor = delNode
  // 后继节点的所有右节点
  let rightChild = successor.right

  while (rightChild != null) {
    // 三个相连节点
    parent = successor
    successor = rightChild
    rightChild = rightChild.left
  }

  // 后继节点不能等于要删除节点的右子节点
  // 如果等于就需要把左子节点给连接过来
  if (delNode.right != successor) {
    parent.left = successor.right
    successor.right = delNode.right
  }

  // 返回这个后继节点
  return successor
}
6.3 二叉搜索树的封装(全部代码)
// 内部类,存储每一个节点
class Node {
  constructor(key, value) {
    // 节点自身的key(索引)
    this.key = key
    // 表示自身的数据
    this.value = value
    // 指向左子节点
    this.left = null
    // 指向右子节点
    this.right = null
  }
}

// 二叉搜索树类
class BinarySerachTree {
  constructor() {
    // 根节点
    this.root = null
  }

  // 1.insert(key, value):向树中添加或修改某个数据
  // 提示:没有key就是添加,有相同key就是修改
  insert(key, value) {
    // 创建子节点
    let newNode = new Node(key, value)

    if (this.root == null) {
      this.root = newNode
    } else {
      this.insertNode(this.root, newNode)
    }
  }

  // 需要递归添加的节点
  insertNode(node, newNode) {
    // 向左走
    if (node.key > newNode.key) {
      // 查看左子节点是否为空,为空就可以添加
      // 不为空就需要递归继续向下层找
      if (node.left == null) {
        node.left = newNode
      } else {
        this.insertNode(node.left, newNode)
      }
      // key相等,修改value
    } else if (node.key == newNode.key) {
      node.value = newNode.value

      // 向右走
    } else {
      if (node.right == null) {
        node.right = newNode
      } else {
        this.insertNode(node.right, newNode)
      }
    }
  }

  // 2.search(key):在树中查找一个键,存在返回value,不存在返回false
  search(key) {
    let node = this.root

    if (node == null) return false

    while (node != null) {
      // 如果传入的key比当前树的key要小,那么就指向左子树
      if (key < node.key) {
        node = node.left
        // 找到了就返回value
      } else if (node.key == key) {
        return node.value
        // 否则指向右子树
      } else {
        node = node.right
      }
    }
    return false
  }

  // 3.preOrderTraverse():先序遍历
  // (1).访问根节点
  // (2).先序遍历所有左子树
  // (3).先序遍历所有右子树
  // (4).循环2、3操作
  // 先序遍历是先查找根节点,在依次先查找左子树
  // 如果左子树中还有左子树,也会先查找,直到左子树(多个左子树)中没有左子节点
  // 就会返回返回最深层的上一层去查找右子树,在右子树中也会先查找它的左子节点
  preOrderTraverse() {
    let resutlStr = this.preOrderTraverseNode(this.root)
    let resultArr = resutlStr.split(",")
    // 因为遍历到最后一个节点时,也会返回并拼上逗号
    // 所以变成数组后会多一位空项
    resultArr.pop()
    return resultArr
  }

  // preOrderTraverseNode():先序递归查找
  preOrderTraverseNode(node) {
    let resultStr = ""
    if (node != null) {
      // 把排序好的数据用逗号拼接
      // 这个递归的返回是从内向外的,这里采用的是+=所以每当返回一个值,就会拼接到这个数的后面
      // 所以排序好之后,就是从头开始到最深处
      resultStr = node.key + ","
      // 先查找全部左子节点
      resultStr += this.preOrderTraverseNode(node.left)
      // 先查找全部右子节点
      resultStr += this.preOrderTraverseNode(node.right)

    }
    return resultStr
  }

  // 4.inOrderTraverse():中序遍历
  // (1).中序遍历左子树
  // (2).访问根节点
  // (3).中序遍历右子树
  // (4).循环1、3操作
  // 中序遍历中的其中一个树节点,一定是先把左子树查找完才会查找右子树
  // 注意:中序遍历因为是先找左在找右,所以返回的key一定是顺序的
  // 因为使用的是递归,左子节点全部找完才会向上层查找
  inOrderTraverse() {
    let resultStr = this.inOrderTraverseNode(this.root)
    let resultArr = resultStr.split(",")
    resultArr.pop()
    return resultArr
  }

  // inOrderTraverseNode():中序递归查找
  inOrderTraverseNode(node) {
    let resultStr = ""
    if (node != null) {
      resultStr += this.inOrderTraverseNode(node.left)

      resultStr += node.key + ","

      resultStr += this.inOrderTraverseNode(node.right)
    }
    return resultStr
  }

  // 5.postOrderTraverse():后序遍历
  // (1).后序遍历左子树
  // (2).后序遍历右子树
  // (3).循环1、2操作
  // (4).访问根节点
  // 后序遍历,一个树节点先遍历(都是从最深层开始查找)左子节点
  // 在遍历右子节点,当一个树的子节点遍历完才会返回它本身
  postOrderTraverse() {
    let resultStr = this.postOrderTraverseNode(this.root)
    let resultArr = resultStr.split(",")
    resultArr.pop()
    return resultArr
  }

  // postOrderTraverseNode():后序递归查找
  postOrderTraverseNode(node) {
    let resultStr = ""
    if (node != null) {
      resultStr += this.postOrderTraverseNode(node.left)

      resultStr += this.postOrderTraverseNode(node.right)

      resultStr += node.key + ","
    }
    return resultStr
  }

  // 6.min():返回树中最小的键
  min() {
    let node = this.root
    // 这里需要一个变量来代表树的上一层
    // 因为当找到null的时候停止,我就找不到他上一层的数据了,上一层才是想要的数据
    let prev = null
    // 当左子树不为空的时候一直向左查找
    // 因为二叉搜索树最左边的子节点一定是最小的
    while (node != null) {
      prev = node
      node = node.left
    }
    return prev.key
  }

  // 7.max():返回树中最大的键
  max() {
    let node = this.root
    let prev = null
    while (node != null) {
      prev = node
      node = node.right
    }
    return prev.key
  }

  // 8.remove(key):通过键删除某个数据
  remove(key) {
    // 当前节点
    let node = this.root
    // 当前节点的上一层节点
    let prev = null
    // 记录是上一层节点的左子节点还是右子节点,以便删除
    let isLeftChild = true

    // 寻找要删除的节点
    while (node.key != key) {
      prev = node
      if (key < node.key) {
        node = node.left
        // 进入左节点
        isLeftChild = true
      } else {
        node = node.right
        // 进入右节点
        isLeftChild = false
      }

      // 表示没要找到要删除的节点
      if (node == null) return false
    }

    // 删除节点
    // 1.删除的是叶子节点(没有子节点)
    if (node.left == null && node.right == null) {
      // 表示只有一个根节点,一个子节点都没有
      if (node == this.root) {
        this.root = null
        return true
      }
      // 表示要删除的这个节点是,它父节点的左子节点
      if (isLeftChild) {
        prev.left = null
        // 反之
      } else {
        prev.right = null
      }

      // 2.删除的有一个子节点
      // 在此之前,有一个条件是node的left和right都为空
      // 所以当左为空时,右边一定不为空
    } else if (node.left == null) {
      // 当要删除的是根节点,并且根节点只有一个子节点时
      if (node == this.root) {
        this.root = node.right
        return true
      }
      // 进入的是左节点,把当前节点直接指向他的,的左子节点的右子节点
      // 这样中间要删除的那个节点就断了联系
      if (isLeftChild) {
        prev.left = node.right
      } else {
        prev.right = node.right
      }
      // 当右为空,左不为空时
    } else if (node.right == null) {
      if (node == this.root) {
        this.root = node.left
        return true
      }
      if (isLeftChild) {
        prev.left = node.left
      } else {
        prev.right = node.left
      }
      // 3.删除的有两个子节点
      // 删除这个节点,应该找所有子节点中,key值最接近它的来替换
      // 也就是左子树中最大的(前驱),或右子树中最小的(后继)
    } else {
      // 获取后继节点,因为要替换删除的节点
      let successor = this.getSuccessor(node)

      // 如果删除的为根节点,直接让后继等于它
      // 左子树右子树同理
      if (this.root == node) {
        this.root = successor
      } else if (isLeftChild) {
        prev.left = successor
      } else {
        prev.right = successor
      }
      successor.left = node.left
    }
    return true
  }

  // getSuccessor(delNode):寻找后继节点
  getSuccessor(delNode) {
    // 要删除的节点
    let parent = delNode
    // 后继节点
    let successor = delNode
    // 后继节点的左儿子
    let leftChild = successor.right

    while (rightChild != null) {
      // 三个相连节点
      parent = successor
      successor = leftChild
      leftChild = leftChild.left
    }

    // 后继节点不能等于要删除节点的右子节点
    // 如果等于就需要把左子节点给连接过来
    if (delNode.right != successor) {
      parent.left = successor.right
      successor.right = delNode.right
    }

    // 返回这个后继节点
    return successor
  }
}

let BST = new BinarySerachTree()

BST.insert(11, "root")
BST.insert(7, "子节点1")
BST.insert(15, "子节点1")
BST.insert(5, "子节点2")
BST.insert(3, "子节点3")
BST.insert(9, "子节点2")
BST.insert(8, "子节点3")
BST.insert(10, "子节点3")
BST.insert(13, "子节点2")
BST.insert(12, "子节点3")
BST.insert(14, "子节点3")
BST.insert(20, "子节点2")
BST.insert(18, "子节点3")
BST.insert(25, "子节点3")

// console.log(BST.preOrderTraverse());

// console.log(BST.inOrderTraverse());

// console.log(BST.postOrderTraverse());

// console.log(BST.min());

// console.log(BST.max());

// console.log(BST.search(15));

console.log(BST.remove(20));

console.log(BST.inOrderTraverse());

console.log(BST);
6.4 二叉搜索树的优缺点
  • 优点:
    • 二叉搜索树的曾、删、改、查效率都比较高,效率为O(logN)
  • 缺点:
    • 要知道,二叉搜索树的效率是根据深度来判断的
    • 如果我最开始插入一个100的key,然后我在依次插入99、98、97…1,以这样的顺序去插入的话,它所有的数据都会添加到对应的左子树上,就相等于链表了(非平衡树),这样它的效率就从O(logN)变为O(N)了

7. 树的平衡

  • 为了能较快的时间O(logN)来操作一棵树,我们需要保证树总是平衡的
  • 至少大部分是平衡的,那么时间复杂度也是接近O(logN)的
  • 也就是每个的左子节点的子孙个数,尽可能与右边的相等
  • 常见的平衡树有哪些?
  • AVL树:
    • AVL 树是最早的一种平衡树,它有些办法保持树的平衡(每个节点多存储了一个额外的数据)
    • 因为 AVL 树是平衡的,所以时间复杂度也是O(logN)
    • 但是,每次插入、删除操作相当于红黑树效率都不高,所以整体效率不如红黑树
  • 红黑树:
    • 红黑树也是通过一些特性来保持树的平衡
    • 因为是平衡树,所以时间复杂度也是在O(logN)
    • 而且插入、删除等操作,红黑树的性能要优于AVL树,所以现在平衡树的应用基本都是红黑树
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值