JavaScript数据结构——树的学习总结

二叉树与红黑树的学习总结

先附上学习地址

树及二叉树
红黑树

一、二叉树的概念特性等
  1. 二叉树的五种结构
    在这里插入图片描述
  2. 特性:
    • 一个二叉树第i层的最大节点数为 2^(i - 1) ,i >= 1
    • 深度为k的二叉树的最大节点总数为 2^k -1,i >= 1
    • 对于任何非空二叉树T,若n0表示叶子节点的个数,n2是度为2的非叶子节点,那么两者关系满足 n0 = n2 + 1
  3. 二叉树的存储常见的是数组和链表,对于完全二叉树按从上至下,从左到右的方式存储,对于非完全二叉树来说,需要转化为完全二叉树,即把缺少的节点置为null,再进行存储,这样会造成很大的空间浪费
    在这里插入图片描述
    在这里插入图片描述
    最常见的还是使用链表进行存储

在这里插入图片描述
对于的树的基本概念,这里就不多进行赘述,我们学习的重点在于对二叉树的操作

二、二叉搜索树(Binary Search Tree)的操作
  1. BST的封装
// 创建BinarySearchTree类
function BinarySearchTree(){
    // 创建节点构造函数
    function Node(key) {
        this.key = key
        this.left = null
        this.right = null
    }

    // 保存根的属性
    this.root = null

    // 相关操作方法
}
  1. 插入操作
BinarySearchTree.prototype.insert = function (key) {
     // 根据key创建对应的node
   const node = new Node(key);

     // 判断根节点是否有值  
   if (this.root === null) {
     this.root = node;
   } else {
     this.insertNode(this.root, node);
   }
 };

BinarySearchTree.prototype.insertNode = function (node, newNode) {
    if (newNode.key < node.key) {//往左找
        if (node.left === null) {
            node.left = newNode
        } else {
            this.insertNode(node.left,newNode)
        }
    } else {//往右找
        if (node.right === null) {
            node.right = newNode
        } else {
            this.insertNode(node.right,newNode)
        }
    }
};
  1. 遍历方式
  • 先序遍历(递归实现)
// 先序遍历
BinarySearchTree.prototype.preOrderTraversal = function (handler) {
    this.preOrderTraversalNode(this.root,handler)
}

BinarySearchTree.prototype.preOrderTraversalNode = function(node,handler) {
    if (node !== null) {
        handler(node.key)
        this.preOrderTraversalNode(node.left,handler)
        this.preOrderTraversalNode(node.right,handler)
    }
}

测试

let bst = new BinarySearchTree();

bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

let res = ''
bst.preOrderTraversal(function(node){
  res += node + ' '
})

console.log(res);//11 7 5 3 9 8 10 15 13 12 14 20 18 25 
  • 中序遍历(递归实现)
// 中序遍历
BinarySearchTree.prototype.minOrderTraversal = function (handler) {
    this.minOrderTraversalNode(this.root,handler)
}

BinarySearchTree.prototype.minOrderTraversalNode = function (node,handler) {
    if (node !== null) {
        this.minOrderTraversalNode(node.left,handler)
        handler(node.key)
        this.minOrderTraversalNode(node.right,handler)
    }
}

测试

let res2 = ''
bst.minOrderTraversal(function(node){
  res2 += node + ' '
})
console.log(res2);//3 5 7 8 9 10 11 12 13 14 15 18 20 25 
  • 后序遍历(递归实现)
// 后序遍历
BinarySearchTree.prototype.postOrderTraversal = function (handler) {
   this.postOrderTraversalNode(this.root,handler)
}

BinarySearchTree.prototype.postOrderTraversalNode = function (node,handler) {
   if (node !== null) {
       this.postOrderTraversalNode(node.left,handler)
       this.postOrderTraversalNode(node.right,handler)
       handler(node.key)
   }
}

测试

let res3 = ''
bst.postOrderTraversal(function(node){
  res3 += node + ' '
})
console.log(res3);//3 5 8 10 9 7 12 14 13 18 25 20 15 11 
  • 层序遍历(非递归实现,利用队列)
BinarySearchTree.prototype.levelOrderTraversal = function (){
  let node = this.root
  if (!node) {
    return false
  }
  let queue = [node]
  let pointer = 0
  let res = ''
  while(pointer < queue.length){
    node = queue[pointer++]
    res += `${node.key} `
    if (node.left) {
      queue.push(node.left)
    }
    if (node.right) {
      queue.push(node.right)
    }
  }
  return res
}

测试

console.log('bfs',bst.levelOrderTraversal())//bfs 11 7 15 5 9 13 20 3 8 10 12 14 18 25 
  • 最大值
BinarySearchTree.prototype.max = function(){
  let node = this.root
  while(node !== null){
    if (node.right) {
      node = node.right
    }else{
      return node.key
    }
  }
  return false
}
console.log('max',bst.max())//max 25
  • 最小值
BinarySearchTree.prototype.min = function(){
  let node = this.root
  while(node !== null){
    if (node.left) {
      node = node.left
    }else{
      return node.key
    }
  }
  return false
}
console.log('min',bst.min())//min 3
  • 搜索特定值
BinarySearchTree.prototype.search = function (key) {
  return this.searchNode(this.root, key);
};

BinarySearchTree.prototype.searchNode = function (node, key) {
  if (node === null) {
    return false;
  }
  if (key < node.key) {
    return this.searchNode(node.left, key);
  } else if (key > node.key) {
    return this.searchNode(node.right, key);
  } else {
    return true;
  }
};
console.log("search", bst.search(13));//search true
console.log("search", bst.search(20));//search true
console.log("search", bst.search(100));//search false
console.log("search", bst.search(68));//search false
  • 删除节点
    这个操作比较麻烦,首先要找到节点,删除的节点分为三种情况:
    第一种是删除的是叶子节点,这种情况直接删除就好
    第二种情况是删除的是有一个子节点的节点,这种情况删除节点后还要把删除节点的子节点与删除节点的父节点相连
    第三种情况是删除的有两个子节点的节点,这个时候就要寻找删除节点的前驱或者后继节点与删除节点进行替换,前驱节点即删除节点的左子树最大值,后继为右子树的最小值,前驱和后继节点的值是最接近删除节点的值的,可以保证该树仍为二叉树,这里以寻找后继为例
BinarySearchTree.prototype.remove = function(key){
  let isLeftChild = true
  let parent = null
  let current = this.root

  // 寻找节点
  while(current.key !== key){
    parent = current
    if (key < current.key) {
      current= current.left
      isLeftChild = true
    } else {
      current = current.right
      isLeftChild = false
    }
    if (current === null) {
      return false
    }
  }

  // 删除节点
  // 1.删除叶子节点
  if (current.left === null && current.right === null) {
    if (current === this.root) {
      this.root = null
    } else if (isLeftChild) {
      parent.left = null
    } else {
      parent.right = null
    }
  } 
  
  // 2.删除一个子节点的节点
  else if(current.left === null && current.right !== null){
    if (current === this.root) {
      this.root = current.right
    }else if (isLeftChild) {
      parent.left = current.right
    } else {
      parent.right = current.right
    }
  }

  else if(current.right === null && current.left !== null){
    if (current === this.root) {
      this.root = current.left
    }else if (isLeftChild) {
      parent.left = current.left
    } else {
      parent.right = current.left
    }
  }

  // 3.删除两个子节点的节点
  else {
    // 获取后继
    let successor = this.getSuccessor(current)

    if (current == this.root) {
      this.root = successor
    } else if(isLeftChild){
      parent.left = successor
    }else{
      parent.right =successor
    }

    successor.left = current.left
  }
}

// 寻找后继节点
// 后继节点分为两种情况,一种是后继节点为delNode.right,另一种为不是delNode.right
BinarySearchTree.prototype.getSuccessor = function(delNode){
  let successor = delNode
  let current = delNode.right
  let successorParent = delNode;
  
  while(current !== null){
    successorParent = successor;
    successor = current
    current = current.left
  }

  // 判断寻找的后继结点是否是delNode的right结点
  if (successor !== delNode.right) {
    successorParent.left = successor.right;
    successor.right = delNode.right;
  }

  return successor
}

测试

 console.log("广度优先删除前:", bst.levelOrderTraversal());//广度优先删除前: 11 7 15 5 9 13 20 3 8 10 12 14 18 25 

 bst.remove(5);
 bst.remove(9);
 bst.remove(13);
 bst.remove(20);

 console.log("广度优先删除后:", bst.levelOrderTraversal());//广度优先删除后: 11 7 15 3 10 14 25 8 12 18 
三、红黑树

       对于一个平衡二叉树来说,插入/查找的效率为O(logN),在最坏情况下,插入有序的值,平衡二叉树会转变成一个链表,这时查找的效率变为O(N),为了能以较快的时间来操作一棵树,我们需要保证树总是尽量平衡的。
       常见的平衡树有AVL树和红黑树,两者的时间复杂度都是O(logN),但是AVL树整体效率不如红黑树。

红黑树的规则

  • 节点是黑色或者红色的
  • 根节点为黑色
  • 每个叶子节点是黑色的空节点(NIL节点)
  • 每个红色节点的两个子节点都是黑色的(从根到叶子的路径上不能有连续的红色节点)
  • 从任意节点到其每个叶子节点的所有路径上都包含相同数目的黑色节点
    在这里插入图片描述

这些规则确保了红黑树的关键特性:

从根到叶子的最长可能路径,不会超过最短可能路径的两倍长

结果这个树就是基本平衡的,虽然没有做到绝对的平衡,但是在最坏的情况下,依然是高效的

为什么可以做到最长路径不超过最短路径的两倍呢?
1. 性质4决定了路径不能有连续的红色节点
2. 最短路径可能都是黑色节点
3. 最长路径可能都是红黑交替
4. 性质5所有路径都有相同数目的黑色节点
5. 这就表明了没有路径能多于其他路径的两倍长

  1. 红黑树的变换
         插入一个新节点时,有可能树不再平衡,可以通过三种方式让树变得平衡:
    • 换色,把红色节点变为黑色或者把黑色节点变为红色;
      插入节点通常默认为红色,因为在插入红色节点时,有可能插入一次是不违反红黑树任何规则的,而插入黑色节点,必然使路径上多了一个黑色节点,这是很难调整的,而红色节点导致的红红相连的情况可以通过换色和旋转来调整

    • 左旋转
      在这里插入图片描述

    • 右旋转
      在这里插入图片描述

  2. 变换规则
    设要插入的节点为N,其父节点为P
    其祖父节点为G,其父亲的兄弟节点为U(即P和U是同一个节点的子节点)
  • 情况1:
    • 新节点位于根上,没有父节点
    • 这种情况下,我们直接将红色换成黑色即可,这样满足性质2
  • 情况2:
    • 新节点的父节点P是黑色
    • 这种情况下直接插入即可
  • 情况3:
    • P为红色,U也为红色,此时G必为黑色
    • 将P和U换成黑色,并且将G换为红色
    • 如果G的父节点为红色,可以递归调整颜色
    • 如果递归到根节点上,就需要进行旋转了
      在这里插入图片描述
  • 情况4:
    • N的叔叔节点U为黑色,P为红色,且N是左孩子,此时G必为黑色
    • 这种情况下对将G变为红色,P变为黑色,再进行右旋转
      在这里插入图片描述
  • 情况5:
    • N的叔叔节点U为黑色,P为红色,且N是右孩子,此时G必为黑色
    • 对P进行左旋转,变成情况4
    • 再对G进行右旋转并且改变颜色即可
      在这里插入图片描述
  1. 案例练习
    案例练习
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值