数据结构之二叉搜索树

本文探讨了二叉搜索树这种特殊的数据结构,其特点在于每个节点的左子节点值小于父节点,右子节点值大于或等于父节点。文章介绍了如何插入节点,通过递归实现中序遍历,并讲解了删除节点的三种情况,特别是处理具有两个子节点的节点的复杂情况。此外,还提到了找到最小值和最大值节点的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

      树是一种很有意思的数据结构,它既能像链表那样快速的插入和删除,又能像有序数组那样快速的查找,一般我们探究的树是一种特殊的树 --  二叉树。如果树中每个节点最多只能有两个子节点,这样的树就成为“二叉树”,否则,就是多路树了。二叉搜索树是一种更特殊的树 -- 父节点的左子节点的关键字值小于父节点,右子节点的关键字的值大于或等于这个父节点。

      一般树的节点包括三个部分:data、leftNode、rightNode。节点的数值,左子节点和右子节点。

public class Node {
	int data;
	Node leftChild;
	Node rightChild;
}
 1、 二叉搜索树的查找:

public Node find(int key) {
		Node current = root;
		while (current.data != key) {
			if (current.data > key) {
				current = current.leftChild;
			}
			if (current.data < key) {
				current = current.rightChild;
			}
			if (current == null) {
				return null;
			}
		}
		return current;
	}
2、二叉搜索树的插入新节点:

思路:1、如果没有节点,则待插入的节点直接插入到根节点;
    2、寻找合适的位置以便插入节点:current、parent分别表示当前搜索到的节点及其父节点,如果待搜索节点的值大于current节点的值,则current = current.rightChild;,否则,current = current.leftChild;结束的条件是:current==null为true,则可以将待插入节点插入到parent的后面了。

	/**
	 * @param key
	 *            待插入的节点的值 1、如果没有节点,则待插入的节点直接插入到根节点;
	 *            2、通过两个引用:current、parent分别表示当前搜索到的节点及其父节点
	 *            找到的条件是:current==null为true
	 */
	public void insert(int key) {
		Node insertNode = new Node();
		insertNode.data = key;
		Node current, parent;
		if (root == null) {
			root = insertNode;
		} else {
			current = root;
			while (true) {
				parent = current;
				if (current.data < key) {
					current = current.rightChild;
				} else {
					current = current.leftChild;
				}
				if (current == null) {
					if (parent.data < key) {
						parent.rightChild = insertNode;
					} else {
						parent.leftChild = insertNode;
					}
					break;
				}
			}
		}
	}
3、二叉搜索树的遍历(前序、中序、后序遍历)
思路:

遍历适用于所有的二叉树,而不只是二叉搜索树。这个遍历的原理不关心节点的关键字值; 它只是看这个节点是否有子节点; 遍历树最简单的方式是用递归方法。用递归的方法遍历整棵树要用一个节点作为参数。初始化 时这个节点是根。这个方法只需要做三件事,以中序遍历为例:
1、调用自身来遍历节点的左子树; 2、访问这个节点; 3、调用自身来遍历节点的右子树;

/**
	 * 遍历适用于所有的二叉树,而不只是二叉搜索树。这个遍历的原理不关心节点的关键字值; 它只是看这个节点是否有子节点;
	 * 遍历树最简单的方式是用递归方法。用递归的方法遍历整棵树要用一个节点作为参数。初始化 时这个节点是根。这个方法只需要做三件事:
	 * 1、调用自身来遍历节点的左子树; 2、访问这个节点; 3、调用自身来遍历节点的右子树;
	 */
	// -------------------------------------------------------
	// 前序遍历
	public void preTravel(Node localRoot) {
		if (localRoot != null) {
			System.out.println(localRoot.data + "~~~~");
			preTravel(localRoot.leftChild);
			preTravel(localRoot.rightChild);
		}
	}

	// 中序遍历<最常用> -- 中序遍历二叉搜索树会使所有的节点按照关键字升序被访问到,比如 root是10,左子节点是2,右子节点是
	// 12.则中序遍历的结果是2,10,12.可以看到是按照升序被访问的
	public void inTravel(Node localRoot) {
		if (localRoot != null) {
			inTravel(localRoot.leftChild);
			System.out.println(localRoot.data + "^^^^");
			inTravel(localRoot.rightChild);
		}
	}

	// 后序遍历:要中序遍历一棵树的原因很清楚,但要通过前序或后序来遍历树的目的就不是那么清楚了。不过,如果要编写程序来解析
	// 或分析袋鼠表达式,这两种遍历方法就很有用了。
	public void postTravel(Node localRoot) {
		if (localRoot != null) {
			postTravel(localRoot.leftChild);
			postTravel(localRoot.rightChild);
			System.out.println(localRoot.data + "$$$$");
		}
	}
4、查找二叉搜索树的最大与最小值

思路:基于二叉搜索树的特征,最小值位于树的最左边的子节点是最小值节点,而最右边是最大值的节点。

// 二叉搜索树的特点,决定了最左边的子节点是最小值的节点,而最右边的子节点是最大值的节点
	public Node findMin() {
		Node current = root, parent = null;
		if (current == null) {
			return null;
		}
		while (current != null) {
			parent = current;
			current = current.leftChild;
		}
		return parent;
	}

	public Node findMax() {
		Node current = root, parent = null;
		if (current == null) {
			return null;
		}
		while (current != null) {
			parent = current;
			current = current.rightChild;
		}
		return parent;
	}
5、删除

删除操作是二叉搜索树中最复杂的操作,分三种情况: 

1、该节点是叶节点(没有子节点);

 2、该节点有一个子节点 ;

3、该节点有两个子节点;

 第一种与第二种都比较简单,最复杂的是第三种情况。

首先在二叉搜索树中查找待删除的节点,如果有,则执行删除;否则,结束删除操作。

5.1、查找待删除节点

                Node parent = root;
		Node current = root;// 待删除的节点
		boolean isLeftNode = false;// 待删除的子节点是左子节点还是右子节点,true:左子节点,false:右子节点
		if (root == null || root.rightChild == null) {
			return;
		}
		// 查找待删除的Node
		while (current.data != key) {
			parent = current;
			if (current.data < key) {
				isLeftNode = false;
				current = current.rightChild;
			} else {
				isLeftNode = true;
				current = current.leftChild;
			}
			if (current == null) {// 说明没有该节点,故结束删除操作
				return;
			}
		}

5.2、待删除的节点是叶子节点


这种情况很简单,根据isLeftChild判断待删除的是左子节点还是右子节点,相应的将parent的子节点置为null。

// 第一种情况:待删除的节点是叶子节点
		if (current.leftChild == null && current.rightChild == null) {
			if (current == root) {
				root = null;
			} else if (isLeftNode) {
				parent.leftChild = null;
			} else {
				parent.rightChild = null;
			}
		}
5.2、待删除的节点有一个子节点

这种情况也比较简单:直接将待删除的节点的子节点插入到parent节点下面即可。

// 第二种情况:待删除的节点有一个子节点
		if (current.leftChild == null) {// 只有右子节点
			if (current == root) {
				root = current.rightChild;
			} else {
				if (isLeftNode) {
					parent.leftChild = current.rightChild;
				} else {
					parent.rightChild = current.rightChild;
				}
			}
		} else if (current.rightChild == null) {// 只有左子节点
			if (current == root) {
				root = current.leftChild;
			} else {
				if (isLeftNode) {
					parent.leftChild = current.leftChild;
				} else {
					parent.rightChild = current.leftChild;
				}
			}
		}
5.3、 待删除的节点有两个子节点

这种情况需要寻找后继节点,后继节点用于取代待删除的节点,后继节点位于待删除节点的右子树的最小值。

后继结点也能为两种情况:1、是左节点(即右子树的最左子节点);2、是右节点(右子树无左节点,则第一个右节点是后继结点);
第一种情况:


第二种情况:

	// 获取后继节点 -- 后继节点用于取代待删除的节点,后继节点位于待删除节点的右子树的最小值
	/**
	 * @param delNode
	 *            待删除的Node
	 * @return 待删除的Node的后继Node
	 */
	private Node getSuccessor(Node delNode) {
		Node successorParent = delNode;// 后继节点的父节点
		Node successor = delNode;// 后继节点
		Node current = delNode.rightChild;
		while (current != null) {
			successorParent = successor;
			successor = current;
			current = current.leftChild;
		}
		if (successor != delNode.rightChild) {// 针对后继节点是待删除子节点的非直接右子节点的情况
			// 调整successor所属的子树
			successorParent.leftChild = successor.rightChild;
			successor.rightChild = delNode.rightChild;
		}
		return current;
	}
得到后继节点之后,替换待删除节点即可。

// 第三种情况:待删除的节点同时有左子节点和右子节点
			Node successor = getSuccessor(current);
			if (current == root) {
				root = successor;
			}
			if (isLeftNode) {
				parent.leftChild = successor;
			} else {
				parent.rightChild = successor;
			}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值