数据结构之树结构

210 阅读7分钟

一、树的术语

  1. 节点的度(Degree :节点的子树个数.
  2. 树的度(Degree :树的所有节点中最大的度数. (树的度通常为节点的个数 N-1)
  3. 叶节点(Leaf :度为 0 的节点. (也称为叶子节点)
  4. 父节点(Parent :有子树的节点是其子树的根节点的父节点
  5. 子节点(Child :若 A 节点是 B 节点的父节点,则称 B 节点是 A 节点的子节点;子节点也称孩子节点。
  6. 兄弟节点(Sibling :具有同一父节点的各节点彼此是兄弟节点。
  7. 路径和路径长度:从节点 n1到 nk 的路径为一个节点序列 n1 , n2,… , nkni是 ni+1 的父节点。路径所包含边的个数为路径的长度。
  8. 节点的层次(Level) :规定根节点在 1 层,其它任一节点的层数是其父节点的层数加1。
  9. 树的深度(Depth) :树中所有节点中的最大层次是这棵树的深度。

二、认识二叉树

  • 定义
    • 二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。
  • 二叉树重要的特性:
  1. 一棵二叉树第 i 层的最大节点数为:2^(i-1), i >= 1;
  2. 深度为 k 的二叉树有最大节点总数为: 2^k - 1, k >= 1;
  3. 对任何非空二叉树 T,若 n0 表示叶节点(度为 0 的节点)的个数、n2 是度为 2 的非叶节点个数,那么两者满足关系 n0 = n2 + 1
  • 特殊的二叉树
  1. 满二叉树(Full Binary Tree
    在二叉树中, 除了最下一层的叶结点外, 每层节点都有 2 个子节点, 就构成了满二叉树。
  2. 完全二叉树(Complete Binary Tree)
    最后一层从左向右的叶节点连续存在, 只缺右侧若干节点。满二叉树是特殊的完全二叉树。

三、认识二叉搜索树

  • 定义
    • 其是二叉树的一种,但是只允许你在左侧节点存储(比父节点)小的值, 在右侧节点存储(比父节点)大的值。
    • 平衡二叉树,查找效率O(logN)
  • 缺陷
    • 如果插入有序连续数据,(如10,9,8,7,6,5,4,3,2,1)造成分布不均匀,非平衡树,查找效率O(n);

四、封装二叉搜索树

一个二叉搜索树的常见操作应该包含以下:

  • 插入操作
    1. insert(key): 向树中插入一个新的数据
  • 查找操作
    1. search(key):在树中查找一个数据,如果节点存在,则返回 true;如果不存在,则返回 false
    2. searchParent(key)函数,在二叉树中查找一个节点的父节点,并返回
    3. min():返回树中最小的值
    4. max():返回树中最大的值
  • 遍历操作
    • 深度优先遍历:
    1. preOrderTraverse:通过先序遍历方式遍历所有节点
    2. inOrderTraverse:通过中序遍历方式遍历所有节点
    3. postOrderTraverse:通过后序遍历方式遍历所有节点
    • 广度优先遍历
    1. levelOrderTraverse:通过层序遍历方式遍历所有节点
  • 删除操作
    1. remove(key):从树中移除某个数据

1. 基础架构

  • 封装一个节点类TreeNode和一个树类BinarySearchTree 。
class TreeNode<T>{
    value: T;
    left: TreeNode<T> | null;
    right: TreeNode<T> | null;
    constructor(key: T) {
        this.value = key;
        this.left = null;
        this.right = null;
    }
}
class BinarySearchTree<T>{
    root: TreeNode<T> | null;
    constructor() {
        this.root = null;
    }
}

2.插入操作

在 BSTree 中添加 insert 方法,向二叉树插入数据。

//插入
insert(key: T): void {
    let newNode: TreeNode<T> = new TreeNode(key);
    if (this.root === null) {
        this.root = newNode;
    } else {
        this.insertNode(this.root, newNode);
    }
}
insertNode(node: TreeNode<T>, newNode: TreeNode<T>): void {
    if (node.value > newNode.value) {
        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);
        }
    }
}

3.查找操作

  1. search(key)函数,在树中查找一个数据,如果节点存在,则返回 true;如果不存在,则返回 false
  • 写法一:递归
 //查询
seach(key: T): boolean {
    return this.searchNode(this.root, key);
}
searchNode(node: TreeNode<T> | null, key: T): boolean {
    if (node === null) {
        return false;
    }
    if (node.value > key) {
        return this.searchNode(node.left, key);
    } else if (node.value < key) {
        return this.searchNode(node.right, key);
    } else {
        return true;
    }
}
  • 写法二:迭代
seach(key: T): boolean {
    let cur: TreeNode<T> | null = this.root;
    if (cur === null) {
        return false;
    }
    while (cur){
        if(cur.value === key){
            return true;
        }else if(cur.value > key){
            cur = cur.left;
        }else if(cur.value < key){
            cur = cur.right;
        } 
    }
    return false;
}
  1. searchParent(key)函数,在二叉树中查找一个节点的父节点,并返回。
searchParent(key: T): TreeNode<T> | null {
    let cur: TreeNode<T> | null = this.root;
    let parent: TreeNode<T> | null = null;
    while (cur) {
        if(cur.value > key){
            parent = cur;
            cur = cur.left;
        }else if(cur.value < key){
            parent = cur;
            cur = cur.right;
        }else{
            break;
        }
    }
    return parent;
}
  1. min()函数,返回树中最小的值。
min(): T | null {
    let cur: TreeNode<T> | null = this.root;
    while (cur !== null && cur.left !== null) {
        cur = cur.left;
    }
    return cur?.value ?? null;
}
  1. max()函数返回树中最大的值。
max(): T | null {
    let cur: TreeNode<T> | null = this.root;
    while (cur !== null && cur.right !== null) {
        cur = cur.right;
    }
    return cur?.value ?? null;
}

4.遍历操作

  1. preOrderTraverse()函数,通过先序遍历方式遍历所有节点。
    树的先序遍历的过程:
       a. 优先访问根节点
       b. 之后访问左子树
       c. 最后访问右子树
  • 写法一:递归
preOrderTraverse(callBack: Function): void {
    this.preOrderTraverseNode(this.root, callBack);
}
preOrderTraverseNode(node: TreeNode<T> | null, callBack: Function): void {
    if(node !== null){
        callBack(node.value);
        this.preOrderTraverseNode(node.left, callBack);
        this.preOrderTraverseNode(node.right, callBack);
    }
}
  • 写法二:迭代(栈)
preOrderTraverse(callBack: Function): void {
    if (this.root === null) {
        return;
    }
    const stack: TreeNode<T>[] = [];
    stack.push(this.root); //根节点入栈
    while (stack.length > 0) {
        let curNode: TreeNode<T> = stack.pop()!;
        callBack(curNode.value);
        if(curNode.right){ //先入栈右节点
            stack.push(curNode.right);
        }
        if(curNode.left){ //再入栈左节点
            stack.push(curNode.left);
        }
    }
}
//第二种迭代写法
preOrderTraverse(callBack: Function): void {
    const stack: TreeNode<T>[] = [];
    let p: TreeNode<T> | null = this.root;
    while (p || stack.length > 0) {
        while(p){
            callBack(p.value);
            stack.push(p);
            p = p.left;
        }
        let curNode: TreeNode<T> = stack.pop()!;
        p = curNode.right;
    }
}
  1. inOrderTraverse函数,通过中序遍历方式遍历所有节点。
    树的中序遍历的过程:
       a. 优先访问左子树
       b. 之后访问根节点
       c. 最后访问右子树
  • 写法一:递归
inOrderTraverse(callBack: Function): void {
    this.inOrderTraverseNode(this.root, callBack);
}
inOrderTraverseNode(node: TreeNode<T> | null, callBack: Function): void {
    if(node !== null){
        this.inOrderTraverseNode(node.left, callBack);
        callBack(node.value);
        this.inOrderTraverseNode(node.right, callBack);
    }
}
  • 写法二:迭代(栈)
inOrderTraverse(callBack: Function): void {
    const stack: TreeNode<T>[] = [];
    let p: TreeNode<T> | null = this.root;
    while (p || stack.length > 0) {
        while (p) {
            stack.push(p);//根节点入栈
            p = p.left;//左子节点入栈
        }
        let curNode: TreeNode<T> = stack.pop()!;
        callBack(curNode.value);
        p = curNode.right; //右子节点当成目标节点,重新执行一遍操作
    }
}
  1. postOrderTraverse函数,通过后序遍历方式遍历所有节点。
    树的后序遍历的过程:
       a. 优先访问左子树
       b. 之后访问右子树
       c. 最后访问根节点
  • 写法一:递归
postOrderTraverse(callBack: Function): void {
    this.postOrderTraverseNode(this.root, callBack);
}
postOrderTraverseNode(node: TreeNode<T> | null, callBack: Function): void {
    if(node !== null){
        this.postOrderTraverseNode(node.left, callBack);
        this.postOrderTraverseNode(node.right, callBack);
        callBack(node.value);
    }
}
  • 写法二:迭代(栈)
    借用先序遍历的方式,然后把数组反转,从而实现后序遍历。
postOrderTraverse(): Array<T> {
    if (this.root === null) {
        return [];
    }
    const arr: Array<T> = [];
    const stack: TreeNode<T>[] = [];
    stack.push(this.root); //根节点入栈
    while (stack.length > 0) {
        let curNode: TreeNode<T> = stack.pop()!;
        arr.push(curNode.value);
        if (curNode.left) { //再入栈左节点
            stack.push(curNode.left);
        }
        if (curNode.right) { //先入栈右节点
            stack.push(curNode.right);
        }
    }
    //根 右 左 翻转
    return arr.reverse();
}
  1. levelOrderTraverse函数,通过层序遍历方式遍历所有节点。
  • 迭代(队列)
levelOrderTraverse(callBack: Function): void {
    if (this.root === null) {
        return;
    }
    const queue: TreeNode<T>[] = [];
    queue.push(this.root);
    while (queue.length > 0) {
        let curNode: TreeNode<T> = queue.shift()!;//头部出队
        callBack(curNode.value);
        if (curNode.left) {
            queue.push(curNode.left);//左节点尾部入队
        }
        if (curNode.right) {
            queue.push(curNode.right);//右节点尾部入队
        }
    }
}

5.删除操作

remove(key)函数,从树中移除某个数据。
删除可以分为以下三种情况:

  • 第一种情况:无子节点
  • 第二种情况:只有一个子节点
  • 第三种情况:有两个子节点
      1. 找到右节点的最小值节点
      1. 目标节点替换要删除的节点
      1. 删除目标节点
//删除
remove(key: T) {
    this.root = this.removeNode(this.root, key);
}
removeNode(node: TreeNode<T> | null, key: T): TreeNode<T> | null {
    if (node === null) {
        return null;
    }
    if (node.value > key) {
        node.left = this.removeNode(node.left, key);
        return node;
    } else if (node.value < key) {
        node.right = this.removeNode(node.right, key);
        return node;
    } else {
        //第一种情况:无子节点
        if (node.left === null && node.right === null) {
            node = null;
            return node;
        }
        //第二种情况:只有一个子节点
        else if (node.left === null) {
            node = node.right;
            return node;
        } else if (node.right === null) {
            node = node.left;
            return node;
        }
        //第三种情况:有两个子节点
        /*
            1.找到右节点的最小值节点
            2.目标节点替换要删除的节点
            3.删除目标节点
        */
        else {
            let target = this.minNode(node.right)!;
            node.value = target.value;
            node.right = this.removeNode(node.right, target.value);
            return node;
        }
    }
}
//找到最小节点
minNode(node: TreeNode<T> | null): TreeNode<T> | null {
    let curNode: TreeNode<T> | null = node;
    while (curNode !== null && curNode.left !== null) {
        curNode = curNode.left;
    }
    return curNode;
}

五、红黑树

1.红黑树概念

  • 由于非平衡的二叉搜索树存在缺陷(时间复杂度为O(n)),所以出现红黑树,其是一种自平衡二叉搜索树(时间复杂度为O(logN))
  • 存在平衡性:左子树与右子树高度差不会超过2倍。

2.红黑树性质

  1. 每个节点黑色或是红色
  2. 根节点是黑色
  3. 每个叶子节点(NIL)是黑色
  4. 每个红色节点的两个子节点都是黑色(表示不能有两个连续的红色节点)
  5. 任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(确保没有一条路径会比其他路径长出俩倍)

5012_118_689r4yIfN9YCfbNJ

3.红黑树操作

  • 变色
    • 通常插入的新节点是红色
  • 左旋转
    • 逆时针(向左)

5015_118_8pW79l2DKVySt6t3

  • 右旋转
    • 顺时针(向右)

5018_118_2dwG5ci6SVpdzpdZ

4.插入操作

  • N表示当前插入节点,P表示父节点,U表示叔叔节点,G表示祖父节点
  • 通常插入的新节点是红色

插入操作分以下五种情况:

  • 情况一:新节点位于根节点
    • 直接将插入的节点红色变成黑色
  • 情况二:新节点的父节点P是黑色
    • 直接插入,不需要操作
  • 情况三:父P红,叔U红,祖G黑
    1. 将祖G变成红
    2. 父P,叔U变成黑
    3. 如果祖G没有父节点,则将祖G变成黑色;否则向上递归。
  • 情况四:父P红,叔U黑,祖G黑,且N为左孩子
    1. 父P变成黑
    2. 祖G变成红
    3. 对祖G进行右旋转
  • 情况五:父P红,叔U黑,祖G黑,且N为右孩子
    1. 对父P进行左旋转(变成了情况四)
    2. N变成黑色
    3. G变成红色
    4. 对祖G进行右旋转