Java教程

快速入门数据结构与算法之一文读懂树结构(近8000字长文)

本文主要是介绍快速入门数据结构与算法之一文读懂树结构(近8000字长文),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

本篇我们还要学习一个非线性的数据结构,虽然它是比较简单的,但是它是非常的重要的,我们接下来详细的学习它。篇幅过长,但请坚持读下去,会对你有帮助的。

定义和实现

树是一个非线性的数据结构,它在计算机科学中的各个领域被广泛使用,如文件系统就是树结构的,还有我们前端攻城狮最熟悉的HTML文档,还有我们最常用的域名体系,都是树结构。从它们的特征中我们可以总结出树是由父节点分叉连接子节点,就像自然界中的树一样。它分为树根和树枝,一棵树有一个总的树根,每个树根有多个树枝。一个树结构只有一个树根,而且没有子节点的节点叫做叶节点。这样一看是不是就和树一模一样呢?

实现

通过图像更加理解了之后,我们用JavaScript代码来实现一下

//实现一个树结构
class Node {
	constructor(value) {
		this.key = value
		this.left = null
		this.right = null
	}
	insertLeft(value) {
		if (this.left === null) {
			this.left = new Node(value)
		} else {
			let t = new Node(value)
			t.left = this.left
			this.left = t
		}
	}
	insertRight(value) {
		if (this.right === null) {
			this.right = new Node(value)
		} else {
			let t = new Node(value)
			t.right = this.right
			this.right = t
		}
	}
	getRightChild() {
		return this.right
	}
	getLeftChild() {
		return this.left
	}
	setRootVal(value) {
		this.key = value
	}
	getRootVal() {
		return this.key
	}
}
复制代码

代码实现非常简单,也非常好理解。它借助了链表的思维,在每个节点中存储左右子节点的引用,配合着插入左右子树的操作,最后链接成一棵树。下面我们通过最简单的一种树——二叉树,来了解下树的遍历

树的遍历

树的遍历有三种方式

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

这其中的先中后说的其实就是访问父节点的次序,而左右子树都是遵循先访问左子树后访问右子树的顺序。所以先序遍历的顺序就是父->左子->右子、中序遍历的顺序就是左子->父->右子、后序遍历的顺序就是左子->右子->父。

每次需要访问一个节点之前都会先根据相应的规则去检测,拿中序遍历举个例子,每次访问一个节点,都会先检测这个节点是否有左子节点,如果有就去把目光移到左子节点,检测它是否有左子节点,以此类推,直到我们检测的节点没有左子节点,那就遍历它,然后再回过头来遍历它的父节点,最后遍历它的右子节点,目光到右子节点之后们也是这样进行检测之后再遍历。也许这里说的有点啰嗦,我们通过三幅图来感受一下具体的遍历步骤吧

这样一看,是不是清楚多了呢?那我们就用JavaScript代码来实现一下吧

//先序遍历
function preorder(tree) {
	if (!!tree) { // tree不为null也不为undefined
		console.log(tree.getRootVal() + ' =>>')
		preorder(tree.getLeftChild())
		preorder(tree.getRightChild())
	}
}
//中序遍历
function postorder(tree) {
	if (!!tree) {
		preorder(tree.getLeftChild())
		console.log(tree.getRootVal() + ' =>>')
		preorder(tree.getRightChild())
	}
}
//后序遍历
function inorder(tree) {
	if (!!tree) {
		preorder(tree.getLeftChild())
		preorder(tree.getRightChild())
		console.log(tree.getRootVal() + ' =>>')
	}
}
复制代码

看到这里,是不是觉得树结构太简单了,不要太天真,我们后面可能要掌握非常复杂的树,做好心理准备,接下来我们慢慢的加大树的难度。

二叉堆

二叉堆是一个完全二叉树,完全二叉树通俗点说就是,只有最后一层和倒数第二层有叶节点,而且最后一层的叶节点一定在左部连续,而倒数第二层的叶节点一定在右部连续。这里还是放几个图吧,好理解一点。

它在完全二叉树的基础上又进一步进行了限制,二叉堆分为最小堆和最大堆,最小堆的子节点都大于它们的父节点,所以根节点是最小值,最大堆则相反。虽然二叉堆的结构是树结构,但是它是以数组来存储数据的,左子节点的数组下标等于父节点下标的二倍,右子节点的数组下标是父节点下标的二倍加一,父节点数组下标是它子节点的二分之一。二叉堆的难点在于它节点的插入和删除,插入和删除时为了不破坏它的完全二叉树的结构,需要不断的交换节点。话不多说(好像说的够多了)我们用JavaScript代码来实现一下

class MinHeap {
	constructor() {
        //下标为0的项
		this.heap = [0]
	}

	//向堆中插入新的值
	insert(value) {
		if (value != null) {
			this.heap.push(value)
            let index = this.heap.length - 1,
            //获取父节点
                parent = index === 0 ? undefined : Math.floor(index / 2)
            //只要节点不是根节点,而且小于它的父节点则交换节点
			while (
				index > 1 &&
				this.heap[parent] > this.heap[index]
			) {
				let temp = this.heap[index]
				this.heap[index] = this.heap[parent]
				this.heap[parent] = temp
				index = parent
				parent =
					index === 0 ? undefined : Math.floor(index / 2)
			}
			return true
		}
		return false
	}

	//返回堆的大小
	size() {
		return this.heap.length - 1
	}

	//堆是否为空
	isEmpty() {
		return this.heap.length === 1
	}

	//查找堆的最小值
	findMin() {
		return this.isEmpty() ? undefined : this.heap[1]
	}

	//移除最小值
	delMin() {
		if (this.isEmpty()) {
			return undefined
        }
        //如果只有一个节点则没必要再组合
		if (this.heap.length === 2) {
			return this.heap.splice(1,1)
		}
		const removedValue = this.heap.splice(1,1)
		this.swapDown(1)
		return removedValue
	}

	//下移操作
	swapDown(index) {
        let element = index
		const left = 2 * index
		const right = 2 * index + 1
        const size = this.size()
        //如果左子节点大于当前节点
		if (left < size && this.heap[left] < this.heap[index]) {
			element = left
        }
        //如果右子节点大于当前节点
		if (right < size && this.heap[right] < this.heap[index]) {
			element = right
        }
        //如果当前节点有比它大的子节点,那就交换
		if (index !== element) {
			let temp = this.heap[index]
			this.heap[index] = this.heap[element]
            this.heap[element] = temp
            this.swapDown(element)
		}
	}
}
复制代码

这里只实现了最小堆,0下标处弃掉不用,这样在计算下标时比较清晰。插入和删除时的整理操作稍微有些复杂,但是仔细研究一下就能看懂。看懂之后可以自己实现一下最大堆,收获满满的成就感。

二叉查找树

二叉查找树(Binary Search Tree)就是我们常说的BST,它也是在二叉树的基础上进行了限制,每个节点的左子节点需要小于它,而右子节点需要大于它。也是很好理解哈。它的插入操作是比较好实现了,比较困难的是它的删除操作。删除一个节点后,也要保证BST保持相应的性质,它分为三种情况

  • 这个节点没有子节点
  • 这个节点有1个子节点
  • 这个节点有2个子节点

第一种情况是比较容易的,直接删除掉就行了;第二种情况也还行,就将唯一的这个子节点上移代替它就行了;最困难的是第三种情况,当它有两个节点的时候没有办法简单的上移哪个节点,于是我们就找一个节点来替代它,那么是怎么找呢,就找右子树的最小的节点,也就是右子树的最左边的左节点。我们还是放一个图会更清晰些。

明白原理之后,我们就用JavaScript代码来实现一下吧

class BinarySearchTree {
	constructor() {
		this.root = null
	}

	insert(key) {
		if (this.root === null) {
			this.root = new Node(key)
		} else {
			this.insertNode(this.root, key)
		}
	}
	insertNode(node, key) {
        //如果小于当前节点则向左子树查询
		if (node.key > key) {
			if (node.left == null) {
				node.left = new Node(key)
			} else {
				this.insertNode(node.left, key)
			}
		} else {
			if (node.right == null) {
				node.right = new Node(key)
			} else {
				this.insertNode(node.right, key)
			}
		}
	}

	min() {
		return this.minNode(this.root)
	}
	minNode(node) {
        let current = node
        //循环直到找到最左的左子节点
		while (current != null && current.left != null) {
			current = current.left
		}
		return current
	}

	max() {
		return this.maxNode(this.root)
    }
	maxNode() {
        let current = node
        //循环直到找到最右的右子节点
		while (current != null && current.right != null) {
			current = current.right
		}
		return current
	}

    search(key) {
        return this.searchNode(this.root,key)
    }
	searchNode(node,key) {
		if (node == null) {
			return false
        }
        //判断小于当前节点则向左查找大于当前节点则向右查找
		if (node.key > key) {
			return this.searchNode(node.left, key)
		} else if (this.root.key < key) {
			return this.searchNode(node.right, key)
		} else {
			return true
		}
	}

	remove(key) {
		this.root = this.removeNode(this.root, key)
	}
	removeNode(node, key) {
		if (node == null) {
			return null
		}
		if (node.key > key) {
			node.left = this.removeNode(node.left, key)
            //需要返回值去移除父节点中对被移除的子节点的引用
            return node 
		} else if (node.key < key) {
			node.right = this.removeNode(node.right, key)
			return node
		} else {
            //第一种情况 没有子节点
			if (node.left == null && node.right == null) {
				node = null
				return node
            }
            //第二种情况 有一个子节点
			if (node.left == null) {
				node = node.right
				return node
			} else if (node.right == null) {
				node = node.left
				return node
            }
            //第三种情况 有两个子节点
			const replaceNode = this.minNode(node.right) //取出其右子树的最小值来替代它
            //修改键的值
            node.key = replaceNode.key
			node.right = this.removeNode(
				node.right,
				replaceNode.key
            )
            //移除右子树中最小值的键
			return node
		}
	}
}
复制代码

用代码实现之后,是不是更加清晰了呢,如果还有些疑惑的话,自己实现一遍,困难就迎刃而解啦。二叉查找树的时间复杂度可以达到O(logn),这已经是效率很高了,但是如果出现极端情况,也就是像每个节点都只有右子节点、每个节点都只有左子节点,这样就相当于是线性链表结构了,时间复杂度就变成了O(n),这也就是二叉查找树的缺陷了,为了解决这个缺陷,自平衡二叉树便登场了。

AVL树

在讲AVL树之前,我们需要再了解一些树的基础知识。树的层级——从根节点开始到达一个节点的路径所包含的边的数量。树的高度——树中所有节点的最大层级。平衡因子——左子树的高度-右子树的高度。了解了这些知识之后,我们就可以介绍平衡二叉树了,即如果一个二叉查找树中每个节点的平衡因子都在-1、0、1之间,那么这个二叉树就叫做平衡二叉树。自然自平衡二叉树就是如果不平衡会自己自动平衡。AVL就是自平衡二叉树的一种,注意,AVL不是什么高端的名词,而是三个人的人名Adelson-Velskii-Landi。AVL树的难点就在于树的平衡了。有两种不平衡的情况出现,即左重和右重。

左重也就是节点的平衡因子大于1,这个时候就进行右旋转,右旋转就是将它的左子节点作为它的父节点,而它作为它左子节点的右子节点,如果它的左子节点有右子节点,就将这个右子节点作为它的左子节点。右重则相反,进行左旋转,左旋转就是将它的右子节点作为它的父节点,而它作为它右子节点的左子节点,如果它的右子节点有左子节点,就将这个左子节点作为它的右子节点。说的有点绕,我们看一个图就明白了

看了图是不是又是理解原理了呢,但是还没完呢,还有更有(复)趣(杂)的情形,我们还是通过图来了解它们
这两种情况出现的情况下,只进行单旋转只会一直重复,所以我们要进行改变,先将他的子节点旋转形成能进行单旋转的结构后再进行单旋转。光说不练假把式,我们还得用JavaScript代码来实现一下,因为是在二叉查找树的基础上构建的,所以我们只需要重写一些方法。

class AVLNode extends Node {
    constructor(key, parent) {
        super(key);
        this.parent = null;
    }
}
class AVLTree extends BinarySearchTree {
    constructor() {
        super()
        this.root = null;
    }

    //获取树的高度
    getNodeHeight(node) {
        if (node == null) {
            return -1;
        }
        //递归调用子树高度+1
        return Math.max(this.getNodeHeight(node.left), this.getNodeHeight(node.right)) + 1;
    }

    //计算一个节点的平衡因子
    getBalanceFactor(node) {
        return this.getNodeHeight(node.left) - this.getNodeHeight(node.right);
    }

    //左旋转
    leftRotation(node) {
        //取出它的右节点
        let newNode = node.right
        //判断当前节点是父节点的哪个节点或者是否有父节点,并相对将取出的节点替换他
        if (node.parent == null) {
            newNode.parent = null
            this.root = newNode
        } else if (node.parent.right === node) {
            node.parent.right = newNode
            newNode.parent = node.parent

        } else {
            node.parent.left = newNode
            newNode.parent = node.parent
        }
        //判断取出的节点是否有左子节点,如果有就把它挂到当前节点的右节点,否则将当前节点的右节点置为null
        if (newNode.left !== null) {
            node.right = newNode.left
            newNode.left.parent = node
        } else {
            node.right = null
        }
        //建立他俩的新关系,也就是旋转
        newNode.left = node
        node.parent = newNode
    }
    //右旋转
    //与左旋转逻辑类似
    rightRotation(node) {
        let newNode = node.left
        if (node.parent == null) {
            newNode.parent = null
            this.root = newNode
        } else if (node.parent.right === node) {
            node.parent.right = newNode
            newNode.parent = node.parent
        } else {
            node.parent.left = newNode
            newNode.parent = node.parent
        }

        if (newNode.right !== null) {
            node.left = newNode.right
            newNode.right.parent = node
        } else {
            node.left = null
        }
        newNode.right = node
        node.parent = newNode
    }

    //向平衡树中插入节点
    insert(key) {
        if (this.root === null) {
            this.root = new AVLNode(key)
        } else {
            this.insertNode(this.root, key)
        }
    }
    insertNode(node, key) {
        if (key < node.key) {
            if (node.left !== null) {
                this.insertNode(node.left, key)
            } else {
                node.left = new AVLNode(key, node)
                //存储父节点
                node.left.parent = node
                //平衡节点
                this.updateBalance(node.left)
            }
        } else {
            if (node.right !== null) {
                this.insertNode(node.right, key)
            } else {
                node.right = new AVLNode(key, node)
                node.right.parent = node
                this.updateBalance(node.right)
            }
        }
        return node;
    }
    //平衡节点的函数
    updateBalance(node) {
        //获取平衡因子
        const balanceFactor = this.getBalanceFactor(node);
        //左子树不平衡
        if (balanceFactor > 1) {
            //判断其左子树是否右重(也就是更复杂的情况)
            if (this.getBalanceFactor(node.left) < 0) {
                this.leftRotation(node.left)
            }
            this.rightRotation(node)
        }
        //右子树不平衡
        if (balanceFactor < -1) {
            if (this.getBalanceFactor(node.right) > 0) {
                this.rightRotation(node.right)
            }
            this.leftRotation(node)
        }
        //如果父节点存在则递归进行检查平衡
        if (node.parent !== null) {
            this.updateBalance(node.parent)
        }
    }
    remove(key) {
        this.removeNode(this.root, key)
    }
    removeNode(node, key) {
        //判断它是大于还是小于当前节点,进一步去子树中删除
        if (node.key > key) {
            this.removeNode(node.left, key)
        } else if (node.key < key) {
            this.removeNode(node.right, key)
        } else {
            //第一种情况 没有子节点
            if (node.left == null && node.right == null) {
                //根节点 直接制空树,根据它是父节点的哪个节点进行修改
                if (node.parent == null) {
                    this.root = null
                } else if (node.parent.right === node) {
                    node.parent.right = null

                } else {
                    node.parent.left = null
                }
            } else if (node.left == null) {//第二种情况 有一个子节点
                //将值替换并删除子节点
                node.key = node.right.key
                node.right = null
            } else if (node.right == null) {
                node.key = node.left.key
                node.left = null
            } else {//第三种情况 有两个子节点
                //取出其右子树的最小值来替代它
                const replaceNode = this.minNode(node.right)
                //修改键的值
                node.key = replaceNode.key
                //修改子树中的这个值
                this.removeNode(
                    node.right,
                    replaceNode.key
                )
            }
        }
        this.updateBalance(node)

    }
    print() {
        this.printTree(this.root)
    }
    //用来测试树结构
    printTree(node) {
        if (node === this.root) {
            console.log(node.key)
        }
        if (node.left !== null) {
            console.log(node.key + " left=>" + node.left.key)
            this.printTree(node.left)

        }
        if (node.right !== null) {
            console.log(node.key + " right=>" + node.right.key)
            this.printTree(node.right)
        }
    }
}
复制代码

这里需要将继承NODE类,因为我们需要存储每个节点的父节点。注释给的比较详细,这里特意把代码写的详细简单些,更有助于理解这个复杂的东西,可能代码有点长,不过静下心来看肯定能看懂,逻辑是比较清晰的。如果你理解了这堆代码并能够自己写出来,那么说明你已经很厉害了,如果还心有余力的话,我们继续进击红黑树。

红黑树

红黑树也是自平衡树的一种,它也经常出现在面试中,所以即使它很困难,我们也非常有必要去学习它。红黑树最重要就是这些支撑它的规则:

  1. 顾名思义,每个节点不是红色的就是黑色的。
  2. 树的根节点永远是黑色
  3. 所有叶节点都是黑色的,它们都是NULL来表示
  4. 红色的节点的两个子节点都是黑色的,红色的节点不能相邻,也就说红色节点的父节点和子节点都必须为黑色
  5. 从当前节点到它的叶节点(也就是NULL)的所有路径中都包含相同数量的黑色节点

把这些规则罗列起来一看都很简单哈,非常好理解,红黑树的平衡全靠它。那么它的难点在哪呢,也许你已经猜到了,就是在插入和移除节点的时候可能需要调整节点的位置或者颜色。接下来我们就仔细研究一下它们。

插入操作

每一个插入的节点都应该是红色的,因为这样就不会在插入的时候影响到规则5。当插入的节点的父节点是黑色的,那将是最好的情况,不会影响树的平衡。在这个前提下,如果父节点也是红色,那么将会影响规则4,这时我们将要平衡它,我们有两种平衡方式,即变色和旋转。

变色

如果插入节点的父节点为红色,而且它的叔节点也为红色,那么我们就可以通过依次改变它们的颜色来保持树的平衡,具体改动如图

变色是比较简单的,一看图就明白了。

旋转

如果插入节点的父节点为红色,而且它的叔节点为黑色或者没有叔节点(为null)则需要旋转才能解决问题了,旋转也像AVL树一样,有四种旋转的情况。

  • 如果作为左子节点插入到父节点,而且父节点也是祖父节点的左子节点则需要进行右旋转;
  • 如果作为右子节点插入到父节点,而且父节点也是祖父节点的左子节点则需要先进行左旋转再右旋转;
  • 如果作为右子节点插入到父节点,而且父节点也是祖父节点的右子节点则需要进行左旋转;
  • 如果作为左子节点插入到父节点,而且父节点也是祖父节点的右子节点则需要先进行右旋转再左旋转;

这里具体的旋转方法不再赘述,放一个图来小小的感受下吧

红黑树的旋转和AVL树的旋转方式是一样的,只是需要再加入颜色的改变。

删除操作

插入操作说完之后,我们还要细讲一下删除操作
因为它的规则限制,和二叉搜索树的删除情况还是有些不同的,有一些情况是一定不会发生的,因为规则5的约束,不会出现节点的一个子节点为黑色,另一个子节点为空、的情况。因为规则4的约束,也不会出现当前节点为红色,子节点也为红色的情况。知道这些之后,我们将详细讨论其他情况

  • 1 如果删除的节点为红色
    • 1.1 删除的节点为叶节点
      这样直接删除该节点就可以,不会影响红黑树的平衡
    • 1.2 删除的节点只有一个节点
      此情况不可能发生,因为红色节点的子节点只能是黑色的,而上面已经说过这种情况下不可能发生
  • 2 如果删除的节点为黑色
    • 2.1 如果删除的节点是叶节点
      • 2.1.1 删除节点的兄弟节点为红色
        • a. 删除节点是父节点的左子节点
          这样会违反规则5,我们将其父节点左旋转,然后将父节点颜色改为红色,兄弟节点改为黑色,这样我们就变成了2.1.2的情况
        • b. 删除节点是父节点的右子节点
          和上一个类似,将其父节点右旋转,然后将父节点颜色改为红色,兄弟节点改为黑色,这样我们就变成了2.1.2的情况
      • 2.1.2 删除节点的兄弟节点为黑色
        • a. 兄弟节点的离删除节点远的子节点为红色
          • ① 删除节点为父节点的左子节点
            此时兄弟节点的离删除节点远的子节点为其兄弟节点的右子节点,需要将其父节点和兄弟节点的颜色进行交换,然后再将父节点进行左旋转操作,最后将兄弟节点的离删除节点远的子节点置为黑色
          • ② 删除节点为父节点的右子节点
            此时兄弟节点的离删除节点远的子节点为其兄弟节点的左子节点,和上面的类似,将其父节点和兄弟节点的颜色进行交换,然后再将父节点进行右旋转操作,最后将兄弟节点的离删除节点远的子节点置为黑色
        • b. 兄弟节点仅有的离删除节点近的子节点为红色
          • ① 删除节点为父节点的左子节点
            此时兄弟节点的离删除节点近的子节点为其兄弟节点的左子节点,这时就需要先将兄弟节点右旋并交换其和左子节点的颜色,然后再将父节点左旋,交换父节点和兄弟节点的颜色,也就是AVL里的先右旋再左旋操作
          • ② 删除节点为父节点的右子节点
            此时兄弟节点的离删除节点近的子节点为其兄弟节点的右子节点,和上面类似,先将兄弟节点左旋并交换其和右子节点的颜色,然后再将父节点右旋,交换父节点和兄弟节点的颜色,也就是AVL里的先左旋再右旋操作
        • c. 兄弟节点为叶节点
          • ① 删除节点的父节点为红色
            此时删除后会违反规则5,我们就需要将其父节点改为黑色,并把其兄弟节点改为红色
          • ② 删除节点的父节点为黑色
            此时删除后也会违反规则5,我们将其兄弟节点改成红色就行了
    • 2.2 如果删除的节点只有左子树或者右子树,这时候它独有的子节点只能是红色的,上面说过不可能出现单个黑色子节点
      这种情况下直接拿他的子节点来代替,并且改成黑色就好了。

这里只讨论了叶节点和只有一个节点的情况,因为两个节点的删除,本质上也是找一个后继来替代他,然后删掉这个后继,也就是还是回归到了前两种情况。原理明白了之后,我们动手用JavaScript代码来实现它,因为它也是二叉查找树,所以我们只需要重写插入和删除方法即可

class RedBlackNode extends Node {
    constructor(key) {
        super(key);
        this.key = key;
        this.color = 'red';
        this.parent = null;
    }
}
class RedBlackTree extends BinarySearchTree {
    constructor() {
        super();
        this.root = null;
    }
    //获取树的高度
    getNodeHeight(node) {
        if (node == null) {
            return -1;
        }
        //递归调用子树高度+1
        return Math.max(this.getNodeHeight(node.left), this.getNodeHeight(node.right)) + 1;
    }

    //计算一个节点的平衡因子
    getBalanceFactor(node) {
        return this.getNodeHeight(node.left) - this.getNodeHeight(node.right);
    }

    minNode(node) {
        let current = node
        //循环直到找到最左的左子节点
        while (current != null && current.left != null) {
            current = current.left
        }
        return current
    }

    //左旋转
    leftRotation(node) {
        //取出它的右节点
        let newNode = node.right
        //判断当前节点是父节点的哪个节点或者是否有父节点,并相对将取出的节点替换他
        if (node.parent == null) {
            newNode.parent = null
            this.root = newNode
        } else if (node.parent.right === node) {
            node.parent.right = newNode
            newNode.parent = node.parent

        } else {
            node.parent.left = newNode
            newNode.parent = node.parent
        }
        //判断取出的节点是否有左子节点,如果有就把它挂到当前节点的右节点,否则将当前节点的右节点置为null
        if (newNode.left !== null) {
            node.right = newNode.left
            newNode.left.parent = node
        } else {
            node.right = null
        }
        //建立他俩的新关系,也就是旋转
        newNode.left = node
        node.parent = newNode
    }
    //右旋转
    //与左旋转逻辑类似
    rightRotation(node) {
        let newNode = node.left
        if (node.parent == null) {
            newNode.parent = null
            this.root = newNode
        } else if (node.parent.right === node) {
            node.parent.right = newNode
            newNode.parent = node.parent
        } else {
            node.parent.left = newNode
            newNode.parent = node.parent
        }

        if (newNode.right !== null) {
            node.left = newNode.right
            newNode.right.parent = node
        } else {
            node.left = null
        }
        newNode.right = node
        node.parent = newNode
    }
    //插入元素
    insert(key) {
        if (this.root == null) {
            this.root = new RedBlackNode(key);
            this.root.color = 'black';
        } else {
            this.insertNode(this.root, key);
        }
    }

    insertNode(node, key) {
        if (key < node.key) {
            if (node.left !== null) {
                this.insertNode(node.left, key)
            } else {
                node.left = new RedBlackNode(key, node)
                //存储父节点
                node.left.parent = node
                //平衡节点
                this.adjustNodes(node.left)
            }
        } else {
            if (node.right !== null) {
                this.insertNode(node.right, key)
            } else {
                node.right = new RedBlackNode(key, node)
                node.right.parent = node
                this.adjustNodes(node.right)
            }
        }
    }

    //填色
    adjustNodes(node) {
        //node的父节点不为空,而且它的父节点为红色
        while (node.parent !== null && node.parent.color === 'red') {
            let parent = node.parent;
            const grandParent = parent.parent;
            //父节点是祖父节点的左子节点
            if (grandParent !== null && grandParent.left === parent) {
                const uncle = grandParent.right;
                //叔节点也是红色 那么只需要重新填色
                if (uncle !== null && uncle.color === 'red') {
                    grandParent.color = 'red';
                    parent.color = 'black';
                    uncle.color = 'black';
                    node = grandParent;
                } else { //叔节点为黑色
                    //如果插入的节点是其父节点的左子节点
                    if (node === parent.left) {
                        this.rightRotation(grandParent)
                        parent.color = 'black'
                        grandParent.color = 'red'
                        node = parent
                    } else {//如果插入的节点是其父节点的右子节点
                        this.leftRotation(parent)
                        this.rightRotation(grandParent)
                        node.color = 'black'
                        grandParent.color = 'red'
                    }
                }
            } else { //父节点是祖父节点的右子节点
                const uncle = grandParent.left;
                //叔节点是红色则只需要重新填色
                if (uncle !== null && uncle.color === 'red') {
                    grandParent.color = 'red';
                    parent.color = 'black';
                    uncle.color = 'black';
                    node = grandParent;
                } else { // 叔节点为黑色
                    //如果插入的节点是其父节点的右子节点
                    if (node === parent.right) {
                        this.leftRotation(grandParent);
                        parent.color = 'black'
                        grandParent.color = 'red'
                        node = parent;
                    } else {//如果插入的节点是其父节点的左子节点
                        this.rightRotation(parent)
                        this.leftRotation(grandParent);
                        node.color = 'black';
                        grandParent.color = 'red';
                    }
                }
            }
        }
        this.root.color = 'black';
    }
    /**
    remove(key) {
        this.removeNode(this.root, key)
    }
    /** 实现的具体逻辑
     *  - 删除的节点为黑色
     *      - 删除的节点为叶节点
     *          - 删除节点是父节点的左子节点
     *              - 删除节点的兄弟节点为红色
     *              - 删除节点的兄弟节点为黑色 
     *                  - 兄弟节点的离删除节点远的子节点为红色
     *                  - 兄弟节点的离删除节点近的子节点为红色
     *                  - 兄弟节点为叶节点
     *                      - 父节点为红色
     *                      - 父节点为黑色
     *          - 删除节点是父节点的右子节点
     *              - 删除节点的兄弟节点为红色
     *              - 删除节点的兄弟节点为黑色 
     *                  - 兄弟节点的离删除节点远的子节点为红色
     *                  - 兄弟节点的离删除节点近的子节点为红色
     *                  - 兄弟节点为叶节点
     *                      - 父节点为红色
     *                      - 父节点为黑色
     *      - 删除的节点只有一个子节点
     *      - 删除的节点有两个子节点
     *  - 删除的节点为红色
     *      - 删除的节点为叶节点
     *      - 删除的节点有两个子节点
     */
    removeNode(node, key) {
        //判断它是大于还是小于当前节点,进一步去子树中删除
        if (node.key > key) {
            this.removeNode(node.left, key)
        } else if (node.key < key) {
            this.removeNode(node.right, key)
        } else {
            //节点颜色为红色
            if (node.color === 'red') {
                //红色叶节点直接删除
                if (node.left == null && node.right == null) {
                    //根节点 直接制空树,根据它是父节点的哪个节点进行修改
                    if (node.parent == null) {
                        this.root = null
                    } else if (node.parent.right === node) {
                        node.parent.right = null
                    } else {
                        node.parent.left = null
                    }
                } else { //有两个子节点
                    //取出其右子树的最小值来替代它
                    const replaceNode = this.minNode(node.right)
                    //修改键的值
                    node.key = replaceNode.key
                    //直接改为删除替换的那个节点
                    this.removeNode(
                        node.right,
                        replaceNode.key
                    )
                }
            } else { //节点颜色为黑色
                //第一种情况 没有子节点
                if (node.left == null && node.right == null) {
                    //保存父节点的引用
                    let parent = node.parent
                    //根节点 直接制空树,根据它是父节点的哪个节点进行修改
                    if (parent == null) {
                        this.root = null
                    } else if (parent.left === node) { //父节点的左子节点
                        //删除该节点
                        parent.left = null
                        //保存兄弟节点
                        let brother = parent.right
                        //兄弟节点为红色,先处理一下,再进入黑色处理
                        if (brother !== null && brother.color === 'red') {
                            this.leftRotation(parent)
                            parent.color = 'red'
                            brother.color = 'black'
                        }
                        // 兄弟节点为黑色
                        let distantNephew = brother.right,
                            nearNephew = brother.left
                        //兄弟节点的离删除节点远的子节点为红色
                        if (distantNephew !== null && distantNephew.color === 'red') {
                            this.leftRotation(parent)
                            const temp = parent.color
                            parent.color = brother.color
                            brother.color = temp
                            distantNephew.color = 'black'
                        } else if (distantNephew == null && nearNephew == null) {// 兄弟节点为叶节点
                            if (parent.color === 'red') {
                                parent.color = 'black'
                                brother.color = 'red'
                            } else {
                                brother.color = 'red'
                            }
                        } else { //兄弟节点仅有的离删除节点近的子节点为红色
                            this.rightRotation(brother)
                            this.leftRotation(parent)
                            const temp = parent.color
                            parent.color = brother.color
                            brother.color = temp
                            distantNephew.color = 'black'

                        }
                    } else {
                        parent.right = null
                        //保存兄弟节点
                        let brother = parent.left
                        if (brother !== null && brother.color === 'red') {
                            parent.right = null
                            this.rightRotation(parent)
                            parent.color = 'red'
                            brother.color = 'black'
                        }
                        let distantNephew = brother.left,
                            nearNephew = brother.right
                        if (distantNephew !== null && distantNephew.color === 'red') {
                            this.rightRotation(parent)
                            const temp = parent.color
                            parent.color = brother.color
                            brother.color = temp
                            distantNephew.color = 'black'
                        } else if (distantNephew == null && nearNephew == null) {
                            if (parent.color === 'red') {
                                parent.color = 'black'
                                brother.color = 'red'
                            } else {
                                brother.color = 'red'
                            }
                        } else {
                            this.leftRotation(brother)
                            this.rightRotation(parent)
                            const temp = parent.color
                            parent.color = brother.color
                            brother.color = temp
                            distantNephew.color = 'black'
                        }

                    }
                } else if (node.left == null) {//第二种情况 有一个子节点
                    //将值替换并删除子节点
                    node.key = node.right.key
                    node.right = null
                } else if (node.right == null) {
                    node.key = node.left.key
                    node.left = null
                } else {//第三种情况 有两个子节点
                    //取出其右子树的最小值来替代它
                    const replaceNode = this.minNode(node.right)
                    //修改键的值
                    node.key = replaceNode.key
                    //删除子树中的这个值
                    this.removeNode(
                        node.right,
                        replaceNode.key
                    )
                }
            }
        }
    }
    print() {
        this.printTree(this.root)
    }
    //用来测试树结构
    printTree(node) {
        console.log(node)
        if (node === this.root) {
            console.log(node.key)
        }
        if (node.left !== null) {
            console.log(node.key + "(" + node.color + ") left=>" + node.left.key)
            this.printTree(node.left)

        }
        if (node.right !== null) {
            console.log(node.key + "(" + node.color + ") right=>" + node.right.key)
            this.printTree(node.right)
        }
    }
}
复制代码

我们还是要继承Node类来创建RedBlackNode类,因为我们需要存储颜色和父节点的引用。这里的代码和AVL树一样,也是尽量写的简单、详细,便于理解一些。所以建议读者去自己实现一下,哪里不会了再回来看,只有自己真正的实现一下,体会才会更深。学到这里我们已经学会了树的大部分知识,如果觉得意犹未尽可以自己再去学习B树、B+树,它们是更加复杂的树,在这里就不去介绍了。上面的这些都掌握的话就已经很棒了。

小结

本篇文章篇幅过长,因为想尽量讲的详细些,通俗易懂些。图都是自己画的,代码纯手打。如果觉得对你有帮助或者真的学到东西了,可以给一个小赞作为鼓励。如果有什么表达不当或者其他错误请尽管指出,让我们一起进步,加油!!!

这篇关于快速入门数据结构与算法之一文读懂树结构(近8000字长文)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!