05、数据结构与算法(树)

198 阅读11分钟

树结构

树(tree)是n(n>=0)个结点的有穷集。n=0时称为空树。在任意一个非空树中:
(1)每个元素称为结点(node);
(2)仅有一个特定的结点被称为根结点或树根(root)。
(3)当n>1时,其余结点可分为m(m≥0)个互不相交的集合T1,T2,……Tm,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作根的子树(subtree)。

显然,树的定义是递归的,即在树的定义中又用到了自身,树是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:

  1. 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
  2. 树中所有结点可以有零个或多个后驱结点。

因此n个结点的树中有n-1条边。

树的术语

22.png

  • 节点的度(Degree) :节点的子树个数,比如节点B的度为2;
  • 树的度:树的所有节点中最大的度数,如上图树的度为3;
  • 叶节点(Leaf)度为0的节点(也称为叶子节点),如上图的H,I,j等;
  • 父节点(Parent) :度不为0的节点称为父节点,如上图节点B是节点D和E的父节点;
  • 子节点(Child) :若B是D的父节点,那么D就是B的子节点;
  • 兄弟节点(Sibling) :具有同一父节点的各节点彼此是兄弟节点,比如上图的B和C,D和E互为兄弟节点;
  • 路径和路径长度:路径指的是一个节点到另一节点的通道,路径所包含边的个数称为路径长度,比如A->H的路径长度为3;
  • 节点的层次(Level) :规定根节点在1层,其他任一节点的层数是其父节点的层数加1。如B和C节点的层次为2;
  • 树的深度(Depth) :树种所有节点中的最大层次是这棵树的深度,如上图树的深度为4;

树的性质

##树的存储结构 树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间 的关系,实际中树有很多种表示方式。下面介绍三种常用的表示树的方法:双亲表示法、孩子表示法和孩子兄弟表示法。

(1)双亲表示法

由于树中每个结点都仅有一个双亲结点(根节点没有),我们可以使用指向双亲结点的指针来表示树中结点的关系。这种表示法有点类似于前面介绍的静态链表的表示方法。具体做法是以一组连续空间存储树的结点,同时在每个结点中,设一个「游标」指向其双亲结点在数组中的位置。代码如下:

class TreeNode {
  constructor(data, parentIndex) {
    this.data = data;
    this.parentIndex = parentIndex;
  }
}

class ParentTree {
  constructor() {
    this.nodes = [];
  }

  addNode(data, parentIndex) {
    const newNode = new TreeNode(data, parentIndex);
    this.nodes.push(newNode);
  }
}

// 示例用法
const parentTree = new ParentTree();
parentTree.addNode('A', -1); // 根节点
parentTree.addNode('B', 0); // A的子节点
parentTree.addNode('C', 0); // A的子节点
parentTree.addNode('D', 1); // B的子节点
parentTree.addNode('E', 1); // B的子节点
parentTree.addNode('F', 2); // C的子节点
parentTree.addNode('G', 2); // C的子节点
parentTree.addNode('H', 2); // C的子节点
parentTree.addNode('I', 3); // D的子节点
parentTree.addNode('I', 3); // D的子节点
console.log(parentTree.nodes);
// 输出:[ TreeNode { data: 'A', parentIndex: -1 },
  TreeNode { data: 'B', parentIndex: 0 }, 
  TreeNode { data: 'C', parentIndex: 0 }, 
  TreeNode { data: 'D', parentIndex: 1 },
  TreeNode { data: 'E', parentIndex: 1 },
  TreeNode { data: 'F', parentIndex: 2 },
  TreeNode { data: 'G', parentIndex: 2 },
  TreeNode { data: 'H', parentIndex: 2 },
  TreeNode { data: 'I', parentIndex: 3 },
  TreeNode { data: 'J', parentIndex: 3 }]

由于根结点没有双亲结点,我们约定根节点的parent域值为-1。树的双亲表示法如下所示:

33.png

这样的存储结构,我们可以根据结点的parent域在O(1)的时间找到其双亲结点,但是只能通过遍历整棵树才能找到它的孩子结点。一种解决办法是在结点结构中增加其孩子结点的域,但若结点的孩子结点很多,结点结构将会变的很复杂。

所以双亲表示法特点是:找双亲容易,找孩子难。

(2)孩子表示法

孩子表示法存储普通树采用的是:顺序表(在计算机内存中以数组的形式保存的线性表)和链表的组合结构。
例如,使用孩子表示法存储下图左侧的普通树,则最终存储状态如下图右侧所示:

44.png

示例代码:

class ChildNodes {
    constructor(data) {
      this.data = data
      this.next = null
    }
}

class TreeNode {
    constructor(data, children) {
        this.data = data;
        this.children = children;
    }
}
  
class ChildTree {
    constructor() {
        this.nodes = [];
    }
    addNode(data, parentIndex) {
        const newNode = new TreeNode(data, null)
        this.nodes.push(newNode);
        if (parentIndex !== -1) {
            if(!this.nodes[parentIndex].children){
                let node = new ChildNodes(this.nodes.length)  // 存储顺序表的下标
                this.nodes[parentIndex].children = node
            }else{
                let node = new ChildNodes(this.nodes.length)
                let current = this.nodes[parentIndex].children
                while(current.next){
                    current = current.next
                }
                current.next = node
            }
            
        } 
    }
}

// 示例用法
const childTree = new ChildTree();
childTree.addNode('A', -1); // 根节点
childTree.addNode('B', 0); // A的子节点
childTree.addNode('C', 0); // A的子节点
childTree.addNode('D', 1); // B的子节点
childTree.addNode('E', 1); // B的子节点
childTree.addNode('F', 2); // C的子节点
childTree.addNode('G', 2); // C的子节点
childTree.addNode('H', 2); // C的子节点
childTree.addNode('I', 3); // D的子节点
childTree.addNode('J', 3); // D的子节点

console.log(childTree.nodes);

// [ TreeNode {
    data: 'A',
    children:
     ChildNodes { data: 2, next: ChildNodes { data: 3, next: null } } },
  TreeNode {
    data: 'B',
    children:
     ChildNodes { data: 4, next: ChildNodes { data: 5, next: null } } },
  TreeNode {
    data: 'C',
    children:
     ChildNodes {
       data: 6,
       next:
        ChildNodes { data: 7, next: ChildNodes { data: 8, next: null } } } },
  TreeNode {
    data: 'D',
    children:
     ChildNodes { data: 9, next: ChildNodes { data: 10, next: null } } },
  TreeNode { data: 'E', children: null },
  TreeNode { data: 'F', children: null },
  TreeNode { data: 'G', children: null },
  TreeNode { data: 'H', children: null },
  TreeNode { data: 'I', children: null },
  TreeNode { data: 'J', children: null } ]

(3)孩子兄弟表示法

孩子兄弟表示法,采用的是链式存储结构

其存储的实现思想是:从树的根节点开始,依次用链表存储各个节点的孩子节点和兄弟节点

因此,该链表中的节点应包含以下 3 部分内容:

  1. 节点的值
  2. 指向孩子节点的指针
  3. 指向兄弟节点的指针

55.png

这种表示方法可以完整地记录每个节点的数据,优点是每一个节点中引用的数量都是确定的。 使用孩子兄弟表示法进行存储的结果如下图所示:

66.png

代码示例:


class TreeNode {
    constructor(value) {
        this.value = value;
        this.firstChild = null;
        this.nextSibling = null;
    }
} 
class Tree {
    constructor() {
        this.root = null;
    }
    addChild(parent, value) {
        // console.log(parent,'---')
        const newNode = new TreeNode(value);
        if (parent.firstChild === null) {
            parent.firstChild = newNode;
        } else {
            let current = parent.firstChild;
            while (current.nextSibling !== null) {
                current = current.nextSibling;
            }
            current.nextSibling = newNode;
        }
    }
} 
// 创建一个树的示例
const tree = new Tree();
// 添加根节点
tree.root = new TreeNode('A');
// 添加子节点
const nodeB = tree.root.firstChild = new TreeNode('B');
const nodeC = nodeB.nextSibling = new TreeNode('C');

// 添加孩子节点
tree.addChild(nodeB, 'D');
tree.addChild(nodeB, 'E');
tree.addChild(nodeC, 'F');
tree.addChild(nodeC, 'G');
tree.addChild(nodeC, 'H');

const nodeD = nodeB.firstChild

tree.addChild(nodeD, 'I');
tree.addChild(nodeD, 'J');
// 打印树的结构
console.log(tree.root);

//TreeNode {
  value: 'A',
  firstChild:
   TreeNode {
     value: 'B',
     firstChild:
      TreeNode {
        value: 'D',
        firstChild:
         TreeNode {
           value: 'I',
           firstChild: null,
           nextSibling: TreeNode { value: 'J', firstChild: null, nextSibling: null } },
        nextSibling: TreeNode { value: 'E', firstChild: null, nextSibling: null } },
     nextSibling:
      TreeNode {
        value: 'C',
        firstChild:
         TreeNode {
           value: 'F',
           firstChild: null,
           nextSibling:
            TreeNode {
              value: 'G',
              firstChild: null,
              nextSibling: TreeNode { value: 'H', firstChild: null, nextSibling: null } } },
        nextSibling: null } },
  nextSibling: null }

二叉树

二叉树简介

二叉树的概念:如果树中的每一个节点最多只能由两个子节点,这样的树就称为二叉树;

二叉树十分重要,不仅仅是因为简单,更是因为几乎所有的树都可以表示成二叉树形式。

二叉树的组成

  • 二叉树可以为空,也就是没有节点;
  • 若二叉树不为空,则它由根节点和称为其左子树TL和右子树TR的两个不相交的二叉树组成;

二叉树的特性

  • 一个二叉树的第 i 层的最大节点树为:2(i-1),i >= 1;
  • 深度为k的二叉树的最大节点总数为:2k - 1 ,k >= 1;
  • 对任何非空二叉树,若 n0 表示叶子节点的个数,n2表示度为2的非叶子节点个数,那么两者满足关系:n0 = n2 + 1;如下图所示:H,E,I,J,G为叶子节点,总数为5;A,B,C,F为度为2的非叶子节点,总数为4;满足n0 = n2 + 1的规律。

特殊的二叉树

完美二叉树

完美二叉树(Perfect Binary Tree)也成为满二叉树(Full Binary Tree),在二叉树中,除了最下一层的叶子节点外,每层节点都有2个子节点,这就构成了完美二叉树。

完美二叉树: 77.png

完全二叉树

完全二叉树(Complete Binary Tree):

  • 除了二叉树最后一层外,其他各层的节点数都达到了最大值;
  • 并且,最后一层的叶子节点从左向右是连续存在,只缺失右侧若干叶子节点;
  • 完美二叉树是特殊的完全二叉树;

完全二叉树: 88.png

二叉搜索树

二叉搜索树BST,Binary Search Tree),也称为二叉排序树二叉查找树

二叉搜索树是一棵二叉树,可以为空;

如果不为空,则满足以下性质:

  • 条件1:非空左子树的所有键值小于其根节点的键值。
  • 条件2:非空右子树的所有键值大于其根节点的键值。

99.png

总结: 二叉搜索树的特点主要是较小的值总是保存在左节点上,相对较大的值总是保存在右节点上。这种特点使得二叉搜索树的查询效率非常高,这也就是二叉搜索树中"搜索"的来源。

二叉搜索树应用举例

下面是一个二叉搜索树:

111.png

若想在其中查找数据10,只需要查找4次,查找效率非常高。

  • 第1次:将10与根节点9进行比较,由于10 > 9,所以10下一步与根节点9的右子节点13比较;
  • 第2次:由于10 < 13,所以10下一步与父节点13的左子节点11比较;
  • 第3次:由于10 < 11,所以10下一步与父节点11的左子节点10比较;
  • 第4次:由于10 = 10,最终查找到数据10 。

222.png

同样是15个数据,在排序好的数组中查询数据10,需要查询9次:

333.png

如果是排序好的数组,可以通过二分查找:第一次找9,第二次找13,第三次找15...。我们发现如果把每次二分的数据拿出来以树的形式表示的话就是二叉搜索树。这就是数组二分法查找效率之所以高的原因。

二叉搜索树代码封装:

二叉树搜索树的基本属性

如图所示:二叉搜索树有四个最基本的属性:指向节点的(root),节点中的(val)、左指针(right)、右指针(right)。

444.png

// 内部节点类
class Node{
    constructor(val){
        this.val = val
        this.left = null
        this.right = null
    }
}

// 封装二叉搜索树
class BinarySearchTree{
    constructor(root){
        this.root = root
    }
}

所以,二叉搜索树中除了定义root属性外,还应定义一个节点内部类,里面包含每个节点中的left、right和key三个属性。

二叉搜索树的常见操作:

  • insert(key):向树中插入一个新的键;
  • search(key):在树中查找一个键,如果节点存在;
  • inOrderTraverse:通过中序遍历方式遍历所有节点;
  • preOrderTraverse:通过先序遍历方式遍历所有节点;
  • postOrderTraverse:通过后序遍历方式遍历所有节点;
  • min:返回树中最小的值/键;
  • max:返回树中最大的值/键;

代码封装:

// 内部节点类
class Node{
    constructor(val){
        this.val = val
        this.left = null
        this.right = null
    }
}

// 封装二叉搜索树
class BinarySearchTree{
    constructor(){
        this.root = null
    }
    insert(val){
        let newNode = new Node(val)
        if(this.root === null){
            this.root = newNode
        }else{
            this.insertNode(this.root,newNode)
        }
    }
    insertNode(node,newNode){ 
        if(newNode.val < node.val){ //当newNode.val < node.val向左查找
            if (node.left == null) { //情况1:node无左子节点,直接插入
                node.left = newNode
            }else{//情况2:node有左子节点,递归调用insertNode(),直到遇到无左子节点成功插入newNode后,不再符合该情况,也就不再调用insertNode(),递归停止。
                this.insertNode(node.left, newNode)
            }
        }else{//当newNode.val >= node.val向右查找
            if(node.right == null){//情况1:node无右子节点,直接插入
                node.right = newNode
            }else{//情况2:node有右子节点,依然递归调用insertNode(),直到遇到无右子节点成功插入newNode为止
                this.insertNode(node.right, newNode)
            }
        }
    }
    //1.先序遍历
    preOrderTraversal(handler){
        this.preOrderTraversalNode(this.root, handler)
    }
    preOrderTraversalNode(node,handler){
        if(node != null){ 
            handler(node.val) //1.处理经过的节点
            this.preOrderTraversalNode(node.left, handler)//2.遍历经过节点的左子节点
            this.preOrderTraversalNode(node.right, handler)//3.遍历经过节点的右子节点
        }
    }
    //2.中序遍历
    midOrderTraversal(handler){
        this.midOrderTraversalNode(this.root, handler)
    }
    midOrderTraversalNode(node, handler){
        if (node != null) {
            this.midOrderTraversalNode(node.left, handler)            //1.遍历左子树中的节点
            handler(node.val)            //2.处理节点
            this.midOrderTraversalNode(node.right, handler)            //3.遍历右子树中的节点
        }
    }
    //3.后序遍历
    postOrderTraversal(handler){
        this.postOrderTraversalNode(this.root, handler)
    }
    postOrderTraversalNode(node, handler){
        if (node != null) {
            this.postOrderTraversalNode(node.left, handler)//1.遍历左子树中的节点 
            this.postOrderTraversalNode(node.right, handler)//2.遍历右子树中的节点
            handler(node.val)//3.处理节点
        }
    }
    //寻找最大值
    max(){
        let node = this.root//1.获取根节点
        let val = null //2.定义val保存节点值
        while (node != null) {//3.依次向右不断查找,直到节点为null
            val = node.val
            node = node.right
        }
        return val
    }
    //寻找最小值
    min(){
        let node = this.root//1.获取根节点
        let val = null//2.定义val保存节点值
        while (node != null) {//3.依次向左不断查找,直到节点为null
            val = node.val
            node = node.left
        }
        return val
    }
    //查找特定的val
    search(val){
        let node = this.root//1.获取根节点
        while(node != null){//2.循环搜索val
            if (val < node.val) {//小于根(父)节点就往左边找
                node = node.left
            }else if(val > node.val){//大于根(父)节点就往右边找
                node = node.right
            }else{
                return true
            }
        } 
        return false
    }
}


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);
bst.insert(6);

let resultString = ""
bst.postOrderTraversal(function(key){
    resultString += key + "->"
})
console.log(resultString,bst.max())
console.log(bst);