第六节:树详解(各种概念术语、二叉搜索树的封装)

一. 树详解

1. 树优点及横向比较

(1). 数组

(2). 链表

(3). 哈希表

(4). 树

2. 相关术语

 ◼ 1.节点的度(Degree):节点子树个数

 ◼ 2.树的度 (Degree) :树的所有节点中最大的度数。

 ◼ 3.叶子节点(Leaf):度为0的节点。(也称为叶节点)

 ◼ 4.父节点(Parent):有子树的节点是其子树的根节点的父节点

 ◼ 5.子节点(Child):若A节点是B节点的父节点,则称B节点是A节点的子节点;子节点也称孩子节点。

 ◼ 6.兄弟节点(Sibling):具有同一父节点的各节点彼此是兄弟节点。

 ◼ 7.路径和路径长度:从节点n1到nk的路径为一个节点序列n1 ,n2,… ,nk ni是 n(i+1)的父节点, 路径所包含 边的个数为路径的长度

 ◼ 8.节点的层次(Level):规定根节点在1层,其它任一节点的层数是其父节点的层数加1。

 ◼ 9.树的深度(Depth):对于任意节点n, n的深度为从根到n的唯一路径长,根的深度为0。

 ◼ 10.树的高度(Height):对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0

 

3. 表示方法

儿子兄弟表示法

 

二. 二叉树详解

1. 二叉树概念

 树中每个节点最多只能有两个子节点,这样的树就成为"二叉树"。几乎上所有的树都可以表示成二叉树的形式。

(1).  二叉树的定义

 二叉树可以为空,也就是没有节点。

 若不为空,则它是由根节点 和 称为其 左子树TL和 右子树TR 的两个不相交的二叉树组成。

(2). 二叉树有五种形态:

2. 二叉树特性

二叉树有几个比较重要的特性,在笔试题中比较常见:

(1). 一颗二叉树第 i 层的最大节点数为:2^(i-1),i >= 1;

(2). 深度为k的二叉树有最大节点总数为: 2^k - 1,k >= 1;

(3). 对任何非空二叉树 T,若n0表示叶子节点的个数、n2是度为2的非叶节点个数,那么两者满足关系n0 = n2 + 1。

eg:  如下图,叶子节点的个数为:4, 分别事D、J、K、H; 度为2的非叶子节点个数为:3,分别是A、B、E,  满足上述公式: 4=3+1

3. 完美二叉树

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

4. 完全二叉树

 (1). 除二叉树最后一层外其他各层的节点数都达到最大个数

 (2). 且最后一层从左向右的叶节点连续存在只缺右侧若干节点

 (3). 完美二叉树是特殊的完全二叉树。

eg: 下面不是完全二叉树,因为D节点还没有右节点,但是E节点就有了左右节点。

5. 二叉搜索树(重点)

(1). 别名

   也称二叉排序树或二叉查找树

(2). 概念

 二叉搜索树是一颗二叉树,可以为空 如果不为空满足以下性质

 A. 非空左子树的所有键值小于其根节点的键值。

 B. 非空右子树的所有键值大于其根节点的键值。

   C. 左、右子树本身也都是二叉搜索树。

(3). 判断哪些是二叉搜索树

(4). 特性

    二叉搜索树的特点就是相对较小的值总是保存在左节点上,相对较大的值总是保存在右节点上。

 查找效率非常高,这也是二叉搜索树中,搜索的来源。

6. 二叉树存储分析

(1). 数组

(2). 链表 【推荐】

    每个节点封装成一个Node,Node中包含存储的数据,左节点的引用,右节点的引用

 

三. 二叉搜索树封装

1. 节点封装

(1). 首先要有个树节点类TreeNode,具有的属性:value、left、right.

(2). 二叉搜索树类:BSTree,  具有的属性:根节点root

/**
 * 树的节点类
 */
class TreeNode<T> {
	value: T;
	left: TreeNode<T> | null = null; //左子树
	right: TreeNode<T> | null = null; //右子树
	constructor(value: T) {
		this.value = value;
	}
}

/**
 * 二叉搜索树类
 */
class BSTree<T> {
	private root: TreeNode<T> | null = null; //根节点
}

2.  插入 

(1). 分两种情况讨论:

    1. 根节点为空:直接插入根节点即可

    2. 插入的是非根节点:

     (1). 新节点的value 小于 原节点的value

       A. 左子树为null, 直接插入即可       B. 左子树有内容, 需要继续向下查找, 走递归即可

     (2). 新节点的value 大于 原节点的value

       A. 右子树为null, 直接插入即可       B. 右子树有内容, 需要继续向下查找, 走递归即可

(2). 封装类的使用

     借助 btPrint 方法进行二叉树的打印

(3). 递归的深入理解

     这里的递归相对好理解,没有返回值,最后结束都是在以下两步结束了

     要么: if (originNode.left === null) originNode.left = newNode; //直接赋值

     要么: if (originNode.right === null) originNode.right == newNode; //直接赋值

代码分享:

/**
	 * 打印树结构
	 */
	print() {
		btPrint(this.root);
	}

	/**
	 * 1. 插入
	 * @param value 节点值
	 */
	insert(value: T): void {
		// 1. 创建树节点
		let newNode = new TreeNode(value);
		// 2. 判断根节点是否为空
		if (!this.root) this.root = newNode; //根节点为空,直接赋值
		else {
			//调用递归
			this.insertNode(this.root, newNode);
		}
	}
	/**
	 * 非根节点的插入
	 * @param originNode 参考节点、原节点
	 * @param newNode 需要被插入的新节点
	 */
	private insertNode(originNode: TreeNode<T>, newNode: TreeNode<T>): void {
		// 1. 新节点的value 小于 原节点的value
		if (newNode.value < originNode.value) {
			// 1.1 左子树为null
			if (originNode.left === null) originNode.left = newNode; //直接赋值
			// 1.2 左子树有内容
			else {
				this.insertNode(originNode.left, newNode); //进入递归调用
			}
		}
		// 2.  新节点的value 大于 原节点的value
		else {
			// 2.1 右子树为null
			if (originNode.right === null) originNode.right = newNode; //直接赋值
			// 2.2 右子树有内容
			else {
				this.insertNode(originNode.right, newNode); //进入递归调用
			}
		}
	}

测试: (后面都是用这组数据!!!)

const bst = new BSTree<number>();
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);

bst.print();

3. 遍历-先序遍历

(1). 说明

   访问根节点→先序遍历左子树→先序遍历右子树      (记忆方式:先序、中序、后续都是指的根节点的位置,然后先左后右)

(2). 递归写法

/**
	 * 2. 先序遍历
	 */
	preOrderTraverse() {
		this.preOrderTraverseNode(this.root);
	}
	/**
	 * 先序遍历节点
	 * @param node 从该节点开始先序遍历
	 */
	// private preOrderTraverseNode(node: TreeNode<T> | null) {
	// 	if (node) {
	// 		console.log(node.value);
	// 		this.preOrderTraverseNode(node.left);
	// 		this.preOrderTraverseNode(node.right);
	// 	}
	// }

	/**
	 * 先序遍历节点 【严格卡递归格式】
	 * @param node 从该节点开始先序遍历
	 */
	public preOrderTraverseNode(node: TreeNode<T> | null) {
		//1. 递归结束条件
		if (node === null) {
			return;
		}
		//2. 输出节点内容
		console.log(node.value);

		//3. 递归调用1
		this.preOrderTraverseNode(node.left);

		// console.log('--------------------');

		//4. 递归调用2
		this.preOrderTraverseNode(node.right);

		// console.log('!!!!!!!!!!!!!!!!!!!!');
	}

(3). 非递归--难

  利用栈的特性,详见下面代码,需要仔细调试,不是很好理解
/**
	 * 2. 先序遍历-非递归
	 */
	preOrderTraverseNoRecursion() {
		let stack: TreeNode<T>[] = []; //模拟栈
		let current: TreeNode<T> | null = this.root;

		let tempArray: T[] = []; //临时数组,用来存放最后的打印结果

		while (current !== null || stack.length !== 0) {
			while (current !== null) {
				// console.log(current.value);
				tempArray.push(current.value); //加入临时数组,用于最后打印

				stack.push(current);
				current = current.left;
			}
			current = stack.pop()!;
			current = current.right;
		}

		//最终打印
		console.log(tempArray.join(","));
	}

4. 遍历-中序遍历

(1). 说明

    中序遍历左子树→访问根节点→中序遍历右子树

(2). 递归写法

/**
	 * 3. 中序遍历
	 */
	inOrderTraverse() {
		this.inOrderTraverseNode(this.root);
	}
	/**
	 * 中序遍历节点
	 * @param node 从该节点开始中序遍历
	 */
	private inOrderTraverseNode(node: TreeNode<T> | null) {
		if (node) {
			this.inOrderTraverseNode(node.left);
			console.log(node.value);
			this.inOrderTraverseNode(node.right);
		}
	}

(3). 非递归-难

/**
	 * 3. 中序遍历
	 */
	inOrderTraverseNoRecursion() {
		let stack: TreeNode<T>[] = []; //模拟栈
		let current: TreeNode<T> | null = this.root;
		let tempArray: T[] = []; //临时数组,用来存放最后的打印结果

		while (current !== null || stack.length !== 0) {
			while (current !== null) {
				stack.push(current);
				current = current.left;
			}
			current = stack.pop()!;

			// console.log(current.value);
			tempArray.push(current.value); //加入临时数组,用于最后打印

			current = current.right;
		}

		//最终打印
		console.log(tempArray.join(","));
	}

 

5. 遍历-后序遍历

(1). 说明

   后续遍历左子树→后续遍历右子树→访问根节点

(2). 递归写法

	/**
	 * 4. 后序遍历
	 */
	postOrderTraverse() {
		this.postOrderTraverseNode(this.root);
	}
	/**
	 * 后序遍历节点
	 * @param node 从该节点开始后序遍历
	 */
	private postOrderTraverseNode(node: TreeNode<T> | null) {
		if (node) {
			this.postOrderTraverseNode(node.left);
			this.postOrderTraverseNode(node.right);
			console.log(node.value);
		}
	}

(3). 非递归-难

/**
	 * 4. 后序遍历
	 */
	postOrderTraverseNoRecursion() {
		let stack: TreeNode<T>[] = [];
		let current: TreeNode<T> | null = this.root;
		let lastVisitedNode: TreeNode<T> | null = null;

		let tempArray: T[] = []; //临时数组,用来存放最后的打印结果

		while (current !== null || stack.length !== 0) {
			while (current !== null) {
				stack.push(current);
				current = current.left;
			}
			current = stack[stack.length - 1];
			if (current.right === null || current.right === lastVisitedNode) {
				// console.log(current.value);
				tempArray.push(current.value); //加入临时数组,用于最后打印

				lastVisitedNode = current;
				stack.pop();
				current = null;
			} else {
				current = current.right;
			}
		}

		//最终打印
		console.log(tempArray.join(","));
	}

6. 遍历-层序遍历

(1). 说明

   从上往下,逐层遍历
 

(2). 实操

   利用队列解决的经典场景

     ①. 数组模拟队列,先进先出,push、shift方法

     ②. 核心思路

       A. 根节点不存在,不需要遍历

       B. 用数组模拟队列,并将根节点入队

       C. while遍历,出队的同时,将其左右节点入队

/**
	 *5 层序遍历
	 */
	levelOrderTraverse() {
		//1. 根节点不存在,不需要遍历
		if (!this.root) return;
		//2. 用数组模拟队列,并将根节点入队
		let queue: TreeNode<T>[] = [];
		queue.push(this.root);
		//3. while遍历,出队的同时,将其左右节点入队
		while (queue.length > 0) {
			//3.1 出队
			let current = queue.shift()!; //一定存在,所以加!
			console.log(current.value);

			//3.2 出队元素的左右节点依次入队(存在的情况下)
			if (current.left) queue.push(current.left);
			if (current.right) queue.push(current.right);
		}
	}

7. 最值

(1). 最小值:叶子节点中最左侧的那个值

(2). 最大值:叶子节点中最右侧的那个值

代码实操
/**
	 * 6. 返回最小值
	 * @returns 返回最小值,可能为null
	 */
	getMinValue(): T | null {
		if (!this.root) return null;
		// 遍历获得最小值
		let current = this.root;
		while (current && current.left) {
			current = current.left;
		}
		return current.value;
	}
	/**
	 * 7. 返回最大值
	 * @returns 返回最大值,可能为null
	 */
	getMaxValue(): T | null {
		if (!this.root) return null;
		// 遍历获得最小值
		let current = this.root;
		while (current && current.right) {
			current = current.right;
		}
		return current.value;
	}
8. 搜索

(1). 说明

    二叉搜索树不仅仅获取最值效率非常高,搜索特定的值效率也非常高

    无论采用哪种方法,核心思路都是:左侧树节点比根节点小, 右侧节点比根节点大

(2). 非递归写法(推荐)

   while遍历,传入根节点, 进行比较,根据大小关系,向左 或 向右遍历,直到找到位置 或者 遍历结束(即没找到)
	/**
	 * 8-1 搜索(遍历写法)
	 * @param value 被搜索节点的值
	 * @returns true找到,false没找到
	 */
	searchNoRecursion(value: T): boolean {
		let current = this.root;
		while (current) {
			if (current.value === value) {
				return true; //找到了节点
			} else if (value < current.value) {
				current = current.left; //向左侧遍历
			} else {
				current = current.right; //向右侧遍历
			}
		}
		return false;
	}

(3). 递归写法

    (1).递归必须有退出条件,我们这里是两种情况下退出。

     A. node === null,也就是后面不再有节点的时候。

     B. 找到对应的value,也就是node.value === value的时候。

    (2).在其他情况下,根据node.的value和传入的value进行比较来决定向左还是向右查找。

     A. 如果node.value > value,那么说明传入的值更小,需要向左查找。

     B. 如果node.value < value,那么说明传入的值更大,需要向右查找。

PS: 这里的递归很好理解,不需要向上找,直接就返回了

	/**
	 * 8-2 搜索(递归写法)
	 * @param value 被搜索节点的值
	 * @returns true找到,false没找到
	 */
	searchRecursion(value: T): boolean {
		return this.searchNode(this.root, value);
	}
	/**
	 *  递归找节点
	 * @param compareNode 用来比较的节点
	 * @param searchNodeValue  被搜索的节点的值
	 * @returns  true找到,false没找到
	 */
	searchNode(compareNode: TreeNode<T> | null, searchNodeValue: T): boolean {
		if (compareNode === null) return false; //直接退出递归
		if (searchNodeValue === compareNode.value) {
			return true; //表示找到节点了
		} else if (searchNodeValue < compareNode.value) {
			return this.searchNode(compareNode.left, searchNodeValue); //向左边继续查找
		} else {
			return this.searchNode(compareNode.right, searchNodeValue); //向右边继续查找
		}
	}

9. 删除

   详见下面

 

四. 删除--重点剖析

1. 分析

   删除节点比较麻烦,需要以下几步

(1).先找到要删除的节点,如果没有找到,不需要删除

(2).找到要删除节点

  A 删除叶子节点(没有子节点)

  B 删除只有一个子节点的节点

  C 删除有两个子节点的节点

2. 查找要删除的节点

(1).思路:找到要删除的节点 和 其父节点, 如果没有找到要删除的的节点,则不需要删除, 直接就结束了.

(2).实现:

  A. 遍历找节点,根据节点比较,决定向左 or 向右查找

  B. 找到节点后,需要给当前节点的父节点赋值(给TreeNode类添加一个parent属性)

3. 删除叶子节点(没有子节点)

(1).思路

  A. 如果这个节点是根节点, 直接删除即可

  B. 如果不是根节点,则将该节点父节点的left 或 right 设置为null即可

(2).实操

   A. 判断是否是叶子节点, 即current的right和left都为null

   B. 判断为是否为跟节点

   C. 判断是左节点 还是 右节点 (给TreeNode封装两个方法 isLeft、isRight)

4. 删除只有一个子节点的节点

(1).思路

 A. 判断该节点的这一个子节点是左 or 右节点,分两种情况讨论。

 B. 只有左子节点

     又要分三种情况讨论,current是:根节点、左节点、右节点

     最终被赋值的是current.left

 C. 只有右子节点

      又要分三种情况讨论,current是:根节点、左节点、右节点

      最终被赋值的是current.right

(2).实操

      同上思路。

5. 删除有两个子节点的节点

(1).分析

   删除的节点有两个子节点,甚至子节点还有子节点!!,这种情况下我们需要从下面的子节点中找到一个节点,来替换当前的节点。

   要么比current节点小一点点,要么比current节点大一点点

(2).结论-记住

    A. 比current小一点点的节点,一定是current左子树的最大值, 称为前驱节点

    B. 比current大一点点的节点,一定是current右子树的最小值, 称之为后继节点

(3).获取后继节点思路分析

  (这里不光获取了后继节点,还处理了后继节点提升后,左右侧指向的问题)

  A. 遍历获取后继节点

  B. 处理删除节点右侧的指向(如果删除节点的右子节点正好是后继节点,是不需要进行该操作)

  C. 处理删除节点左侧的指向(一定需要进行的操作)

(4).实操

   判断删除节点是根节点、左节点、右节点

   分别将根节点、current.parent.left 、 current.parent.right 指向后继节点

上述完整代码分享 

树节点

/**
 * 树的节点类
 */
class TreeNode<T> {
	value: T;
	left: TreeNode<T> | null = null; //左子树
	right: TreeNode<T> | null = null; //右子树
	parent: TreeNode<T> | null = null; //当前节点的父节点(用于删除)
	//写法1
	// isLeft(node: TreeNode<T>): boolean {
	// 	return node.value === node.parent?.left?.value;
	// }
	// isRight(node: TreeNode<T>): boolean {
	// 	return node.value === node.parent?.right?.value;
	// }
	//写法2--使用set语法
	get isLeft(): boolean {
		return this.value === this.parent?.left?.value;
	}
	get isRight(): boolean {
		return this.value === this.parent?.right?.value;
	}

	constructor(value: T) {
		this.value = value;
	}
}

删除代码

/**
     * 9. 删除节点
     * @param value 删除该值对应的节点
     * @returns true成功、false失败
     */
    remove(value: T): boolean {
        //1. 搜索需要删除的节点
        let current = this.searchForRemove(value);
        if (!current) return false; //表示节点不存在

        //需要获取3个东西,当前节点、父节点、当前节点是左or右节点
        console.log(
            `当前节点:${current.value},父节点:${current.parent?.value},是左节点:${current.isLeft}`
        );

        //2. 删除的节点是叶子节点
        if (current.left === null && current.right === null) {
            //2.1 删除节点是根节点
            if (current.value === this.root?.value) {
                this.root = null;
            }
            //2.2 删除节点是左节点
            else if (current.isLeft) {
                current.parent!.left = null;
            }
            //2.3 删除节点是右节点
            else {
                current.parent!.right = null;
            }
        }
        //3.删除的节点只有一个子节点
        //3.1 删除的节点只有一个左子节点
        else if (current.left && current.right === null) {
            //删除的节点为根节点
            if (current.value === this.root?.value) {
                this.root = current.left;
            }
            //删除的节点为左节点
            else if (current.isLeft) {
                current.parent!.left = current.left;
            }
            //删除的节点为右节点
            else {
                current.parent!.right = current.left;
            }
        }
        //3.2 删除的节点只有一个右子节点
        else if (current.right && current.left === null) {
            //删除的节点为根节点
            if (current.value === this.root?.value) {
                this.root = current.right;
            }
            //删除的节点为左节点
            else if (current.isLeft) {
                current.parent!.left = current.right;
            }
            //删除的节点为右节点
            else {
                current.parent!.right = current.right;
            }
        }
        //4.删除的节点右两个子节点 (子节点可能还有子节点)
        else {
            //获取后继节点
            let successor = this.getSuccessor(current);
            //删除的节点为根节点
            if (current.value === this.root?.value) {
                this.root = successor;
            }
            //删除的节点为左节点
            else if (current.isLeft) {
                current.parent!.left = successor;
            }
            //删除的节点为右节点
            else {
                current.parent!.right = successor;
            }
        }

        return true;
    }
    /**
     * 9-2 查找节点(服务于删除功能)
     * @param value 需要查找的节点值
     * @returns 对应节点,没找到的话-返回null
     */
    private searchForRemove(value: T): TreeNode<T> | null {
        let current = this.root;
        let parent: TreeNode<T> | null = null;

        while (current) {
            if (value === current.value) {
                return current; //找到节点了,进行返回
            }
            parent = current;
            if (value < current.value) {
                current = current.left; //向左查找
            } else {
                current = current.right; //向右查找
            }

            // 当前节点的父节点属性赋值(current是null表示while结束了,没找到节点,没必要赋值parent)
            if (current) current.parent = parent;
        }

        return null; //表示没有找到节点,返回null
    }
    /**
     * 9-3 获取后继节点,并处理后继节点指向问题(服务于删除功能)
     * 后继节点:右子树中的最小值
     * @param delNode 被删除的节点
     * @returns 返回后继节点
     */
    private getSuccessor(delNode: TreeNode<T>): TreeNode<T> {
        //1.遍历获得后继节点
        let current = delNode.right;
        let successor: TreeNode<T> | null = null; //后继节点
        while (current) {
            successor = current;
            current = current.left;
            //服务于下面的 successor!.parent!.left, 否则parent没有值
            if (current) {
                current.parent = successor;
            }
        }

        //2.处理删除节点的右侧指向
        //如果删除节点的右子节点正好是后继节点,是不需要进行该操作
        if (delNode.right?.value != successor?.value) {
            //后继节点有一个右子节点的情况(详见ppt图19节点)
            successor!.parent!.left = successor!.right;

            //后继节点的right指向删除节点的right(通用情况)
            successor!.right = delNode.right;
        }

        //3.处理删除节点的左侧指向
        // 后继节点的left指向删除节点的left
        successor!.left = delNode.left;

        return successor!;
    }
View Code

 

6. 删除重构

(1). 说明

     代码是简洁,但是对于第一次看代码,不方便理解

(2). 重构思路

     第一步:判断删除节点是叶子节点、只有一个子节点、有两个子节点,获取对应的replaceNode

     第二步: 设置指向,根据删除节点是根节点、左节点、右节点,从而设置指向

代码分享 

/**
     * 9. 删除节点
     * @param value 删除该值对应的节点
     * @returns true成功、false失败
     */
    remove(value: T): boolean {
        //1. 搜索需要删除的节点
        let current = this.searchForRemove(value);
        if (!current) return false; //表示节点不存在

        //需要获取3个东西,当前节点、父节点、当前节点是左or右节点
        console.log(
            `当前节点:${current.value},父节点:${current.parent?.value},是左节点:${current.isLeft}`
        );

        let replaceNode: TreeNode<T> | null = null;
        //2. 获取replaceNode
        //2.1 删除的节点是叶子节点
        if (current.left === null && current.right === null) {
            replaceNode = null;
        }
        //2.2 .删除的节点只有一个子节点
        //2.2.1  删除的节点只有一个左子节点
        else if (current.left && current.right === null) {
            replaceNode = current.left;
        }
        //2.2.2 删除的节点只有一个右子节点
        else if (current.right && current.left === null) {
            replaceNode = current.right;
        }
        //2.3.删除的节点右两个子节点 (子节点可能还有子节点)
        else {
            //获取后继节点
            let successor = this.getSuccessor(current);
            replaceNode = successor;
        }

        //3. 设置指向
        //3.1 删除的节点为根节点
        if (current.value === this.root?.value) {
            this.root = replaceNode;
        }
        //3.2 删除的节点为左节点
        else if (current.isLeft) {
            current.parent!.left = replaceNode;
        }
        //3.3 删除的节点为右节点
        else {
            current.parent!.right = replaceNode;
        }

        return true;
    }
    /**
     * 9-2 查找节点(服务于删除功能)
     * @param value 需要查找的节点值
     * @returns 对应节点,没找到的话-返回null
     */
    private searchForRemove(value: T): TreeNode<T> | null {
        let current = this.root;
        let parent: TreeNode<T> | null = null;

        while (current) {
            if (value === current.value) {
                return current; //找到节点了,进行返回
            }
            parent = current;
            if (value < current.value) {
                current = current.left; //向左查找
            } else {
                current = current.right; //向右查找
            }

            // 当前节点的父节点属性赋值(current是null表示while结束了,没找到节点,没必要赋值parent)
            if (current) current.parent = parent;
        }

        return null; //表示没有找到节点,返回null
    }
    /**
     * 9-3 获取后继节点,并处理后继节点指向问题(服务于删除功能)
     * 后继节点:右子树中的最小值
     * @param delNode 被删除的节点
     * @returns 返回后继节点
     */
    private getSuccessor(delNode: TreeNode<T>): TreeNode<T> {
        //1.遍历获得后继节点
        let current = delNode.right;
        let successor: TreeNode<T> | null = null; //后继节点
        while (current) {
            successor = current;
            current = current.left;
            //服务于下面的 successor!.parent!.left, 否则parent没有值
            if (current) {
                current.parent = successor;
            }
        }

        //2.处理删除节点的右侧指向
        //如果删除节点的右子节点正好是后继节点,是不需要进行该操作
        if (delNode.right?.value != successor?.value) {
            //后继节点有一个右子节点的情况(详见ppt图19节点)
            successor!.parent!.left = successor!.right;

            //后继节点的right指向删除节点的right(通用情况)
            successor!.right = delNode.right;
        }

        //3.处理删除节点的左侧指向
        // 后继节点的left指向删除节点的left
        successor!.left = delNode.left;

        return successor!;
    }
View Code

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2023-12-14 10:01  Yaopengfei  阅读(12)  评论(1编辑  收藏  举报