数据结构教程:一、二叉树(前端工程师)

242 阅读5分钟

基础概念

普通二叉树:

特点:

1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
2)左子树和右子树是有顺序的,次序不能任意颠倒。
3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

满二叉树:

在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。 满二叉树的特点有:
1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。
2)非叶子结点的度一定是2。
3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。

二叉树的存储结构---二叉链表:

二叉树遍历:
二叉树的遍历是指从二叉树的根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次,且仅被访问一次。 二叉树的访问次序可以分为四种:

先序遍历
中序遍历
后序遍历

先序遍历:根据根->左->右的顺序便利整个二叉树,每个节点都遵照根->左->右这个顺序规则执行。比如先序遍历上图的二叉树,遍历访问的顺序为:ABDHIEJCFG
中序遍历:根据左->根->右的顺序便利整个二叉树,每个节点都遵照左->根->右这个顺序规则执行。比如中序遍历上图的二叉树,遍历访问的顺序为:HDIBEJAFCG
后序遍历:根据左->右->根的顺序便利整个二叉树,每个节点都遵照左->右->根这个顺序规则执行。比如后序遍历上图的二叉树,遍历访问的顺序为:HIDJEBFGCA

二叉搜索树

二叉搜索树(Binary Search Tree),又名二叉排序树(Binary Sort Tree)。
下图是一棵二叉搜索树:

二叉搜索树是具有有以下性质的二叉树:
(1)若左子树不为空,则左子树上所有节点的值均小于或等于它的根节点的值。
(2)若右子树不为空,则右子树上所有节点的值均大于或等于它的根节点的值。
(3)左、右子树也分别为二叉搜索树。
二叉搜索树数据结构的组织方式:
和链表一样,将通过指针来表示节点之间的关系。
然后,我们需要实现一些方法:

  • insert(key): 向树中插入一个新的节点,key表示插入的节点的值(健)
  • search(key): 在书中查找一个键,如何节点存在,则返回true;如果不存在,则返回false
  • inOrderTraverse:通过中序遍历方式遍历所有节点
  • preOrderTraverse:通过先序遍历方式遍历所有节点
  • postOrderTraverse:通过后序遍历方式遍历所有节点
  • min: 返回树中最小的键
  • max:返回树中最大的键
  • remove(key): 从树中移除某个键

我们用BinarySearchTree类来表示二叉搜索树,下面是类的完整代码:

/**
 * 二叉搜索树
 */
function BinarySearchTree() {
  // 根节点
  var root = null

  // 节点的类名,key:键值;left:左子树的指针;right:右子树的指针
  var Node = function(key) {
    this.key = key
    this.left = null
    this.right = null
  }

  var insertNode = function(node, newNode) {
    if(node.key > newNode.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)
      }
    }
  }

  var inOrderTraverseNode = function(node, callback) {
    if(node.left) {
      inOrderTraverseNode(node.left, callback)
    }
    callback && callback(node.key)
    if(node.right) {
      inOrderTraverseNode(node.right, callback)
    }
  }

  var preOrderTraverseNode = function(node, callback) {
    callback && callback(node.key)
    if(node.left) {
      preOrderTraverseNode(node.left, callback)
    }
    if(node.right) {
      preOrderTraverseNode(node.right, callback)
    }
  }

  var postOrderTraverseNode = function(node, callback) {
    if(node.left) {
      postOrderTraverseNode(node.left, callback)
    }
    if(node.right) {
      postOrderTraverseNode(node.right, callback)
    }
    callback(node.key)
  }

  var searchNode = function(node, key) {
    if(key === node.key) {
      return true
    }
    if(key < node.key && node.left) {
      return searchNode(node.left, key)
    }
    if(key > node.key && node.right) {
      return searchNode(node.right, key)
    }
    return false
  }

  var findMinNode = function(node) {
    while(node.left) {
      node = node.left
    }
    return node
  }

  var removeNode = function(node, key) {
    if(key < node.key && node.left) {
      node.left = removeNode(node.left, key)
      return node
    }
    if(key > node.key && node.right) {
      node.right = removeNode(node.right, key)
      return node
    }
    if(key === node.key) {
      // 情况一:删除的节点是叶子节点
      if(node.left === null && node.right === null) {
        node = null
        return node
      }

      // 情况二:删除的节点度为1
      if(node.left === null) {
        node = node.right
        return node
      }
      if(node.right === null) {
        node = node.left
        return node
      }

      // 情况三:删除的节点度为2
      if(node.left && node.right) {
        var rMinNode = findMinNode(node.right)
        node.key = rMinNode.key
        node.right = removeNode(node.right, rMinNode.key)
        return node
      }
    }
  }

  this.insert = function(key) {
    var newNode = new Node(key)
    if(root === null) {
      root = newNode
    } else {
      insertNode(root, newNode)
    }
  }

  this.inOrderTraverse = function(callback) {
    root && inOrderTraverseNode(root, callback)
  }

  this.preOrderTraverse = function(callback) {
    root && preOrderTraverseNode(root, callback)
  }

  this.postOrderTraverse = function(callback) {
    root && postOrderTraverseNode(root, callback)
  }

  this.min = function() {
    if(!root) {
      return
    }
    var node = root
    while(node.left) {
      node = node.left
    }
    return node.key
  }

  this.max = function() {
    if(!root) {
      return
    }
    var node = root
    while(node.right) {
      node = node.right
    }
    return node.key
  }

  this.search = function(key) {
    if(!root) {
      return false
    }
    return searchNode(root, key)
  }

  this.remove = function(key) {
    root && (root = removeNode(root, key))
  }
}

下面构造生成上图中的二叉搜索树,并执行遍历/移除等操作:

var binarySearchTree = new BinarySearchTree() // 实例化一个二叉搜索树,目前没有一个节点

// 通过插入新的节点生成一棵又节点的二叉搜索树
binarySearchTree.insert(17)
binarySearchTree.insert(5)
binarySearchTree.insert(35)
binarySearchTree.insert(2)
binarySearchTree.insert(11)
binarySearchTree.insert(9)
binarySearchTree.insert(8)
binarySearchTree.insert(29)
binarySearchTree.insert(38)

// 中序遍历,遍历的顺序正好是所有键从小到大排序的结果
console.log(`%c 中序遍历:`, `color: #ff0000`)
binarySearchTree.inOrderTraverse(function(key) {
    console.log(key)
  })

// 先序遍历
console.log(`%c 先序遍历:`, `color: #ff0000`)
binarySearchTree.preOrderTraverse(function(key) {
    console.log(key)
})

// 后序遍历
console.log(`%c 后序遍历:`, `color: #ff0000`)
binarySearchTree.postOrderTraverse(function(key) {
    console.log(key)
})

// 查找一个键
console.log(`%c 查找一个键:`, `color: #ff0000`)
console.log(binarySearchTree.search(35))

// 最小键
console.log(`%c 最小键:`, `color: #ff0000`)
console.log(binarySearchTree.min())

// 最大键
console.log(`%c 最大键:`, `color: #ff0000`)
console.log(binarySearchTree.max())

// 移除某个键
console.log(`%c 移除某个键后:`, `color: #ff0000`)
binarySearchTree.remove(5)

二叉搜索树更多知识
最坏的情况下,一棵二叉搜索树所有节点可能都只有左节点或右节点

想象一下,如果深度很深(即节点很多),这会在需要在某条边上添加/移除/搜索某个节点时引起一些性能问题。比如搜索上图中的键2,每个节点都会被访问到。如果二叉搜索树变成这样就好多了:

AVL树/红黑树就是为了解决这种性能问题