数据结构之二分搜索树

数据结构——树

(PS:终于到了树,对于树的一些理论在我之前的博客中有,我之前将树的基本的定义整理了出来,https://blog.csdn.net/weixin_44077141/article/details/100107069)

二叉搜索树

在这里先对二叉搜索树进行一个简单的学习
首先是一些对于树的基本介绍,树的结构在大自然和计算机中都有很多的出现和利用,之所以使用树,肯定是也是为了更加的高效,我们在这里主要学习二叉搜索树(Binary Search Tree),二叉搜索树,左子树小于根,根小于右子树,一个根上至多可以有两个子树,以此类推。

二叉搜索树的基本结构
	private class Node  {
		public E e;
		public Node left,right;
		public Node(E e) {
			this.e=e;
			left=null;
			right=null;
		}
			@Override
				public String toString() {
					// TODO Auto-generated method stub
					return this.e+"";
				}
	}
	private Node root;
	private int size;
	public BST() {
		root=null;
		size=0;
	}
public int  size(){
		return size;
	}
	public boolean isEmpty() {
		return size==0;
	}

在这里还有一些小点,二叉搜索树主要是使用就是为了使用他的排序功能,所以你需要去继承他的Comparable这个借口,还有就是因为是二叉树,所以说底层的存储结构就是左子树,右子树,自己的值,一个树只有一个根,一个记录树大小的属性size。
####二叉树的添加

//非递归的实现(自己操作)
	/**
	 * 最开始的情况就是会判断是否为空,如果为空则元素是跟,如果不是空调用自己的方法实现插入和递归
	 * 然后自己的方法是:先判断值,如果是一样的直接返回,如果是大并且是右子树为空直接插入,如果是小并且左子树为空
	 * 如果不为空在自己调用自己自己
	 * 
	 * 修改后
	 * 删除了代码的冗余性,将判断是否为空直接加在自己的方法里面
	 * 自己的方法也是进行了修改,如果为空返回值一个跟,如果小去左,如果右去右,
	 * @param e
	 */
	public void add(E e) {
//		//非空才递归
//		if(root==null) {
//			root=new Node(e);
//			size++;
//		}
//		else {
//			add(root, e);
//		}
		
		root = add(root, e);
	}
	
	/**
	 * 因为Node不需要对用户可见所以是private
	 * 原来没有返回值,但是现在有了
	 * 现在返回的是插入新节点后二叉树的根
	 * @param node
	 * @param e
	 */
	private Node add(Node node ,E e) {
		
		//你看上面的添加代码不纯粹,所以我们要将代码完成的更加的纯粹,
//		if(e.equals(node.e)){
//			return;
//		}
//		else if(e.compareTo(node.e)<0&&node.left==null){
//			node.left=new Node(e);
//			size++;
//			return;
//		}
//		else if(e.compareTo(node.e)>0&&node.right==null){
//			node.right=new Node(e);
//			size++;
//			return;
//		}
		
		
		if(node==null) {
			size++;
			return new Node(e);
		}
		if(e.compareTo(node.e)<0) {
			node.left = add(node.left,e);
		}
		
		else if(e.compareTo(node.e)>0){
			node.right = add(node.right,e);
		}
		
//		/**
//		 *如果是小的就去左,大或者==去右 
//		 */
//		else {
//			add(node.right,e);
//		}
		return node;
	}

在这里主要是使用递归的做法,我如果有时间也会去完成不需要递归的方法去完成递归操作,我们在这里就是单纯的只是完成递归的方法。
因为递归,所以将树的根放入方法中,进行递归,进行递归有两个条件
第一就是递归中止的条件,你要搞清楚在什么条件下需要停止,其实也就是临界值,特殊情况
第二就是常规的情况,如何去递归
我们在这里的逻辑就是,直接插入,如果没有根,插入元素做根,如果有根元素,看比根元素大还是小,大的话去右子树,查看右子树是否为空,进行上一步的递归,空则直接插入,不空则去看比右子树大还是小,左子树是一样的逻辑。
我写方法的是一种模式,即使用户只能够看得到和调用public的方法,他们不需要知道的底层是如何去执行的。

是否包含某元素
/**
	 * 递归的
	 * @param e
	 * @return
	 */
	public boolean contains(E e) {
		return contains(root,e);
	}
	
	private boolean contains(Node node,E e) {
		if(node==null) {
			return false;
		}
		if(e.compareTo(node.e)==0) {
			return true;
		}
		else if(e.compareTo(node.e)>0) {
			return contains(node.right,e);
		}
		else {
			return contains(node.left,e);
		}
	}

直接就递归,如果一样就输出true,如果比当前节点小的话就去左子树进行递归,大的话去右子树进行递归。

二叉搜索树的遍历

二叉搜索树的遍历乃至所有的树的遍历都是有三种情况的
分别是前序遍历中序遍历后序遍历
我先在这里稍微讲一下具体自己的理解
前序遍历:在树的中,每一个节点都存在三个结构,分别是左子树,右子树,自己的值,而前序遍历就实现访问自己的值,然后去看自己的左子树,如果左子树有,就先访问左子树,以此递归,右子树也一样。
中序遍历:先访问左子树,在访问自己的值,最后后访问右子树。
后序遍历:先访问左子树,在访问右子树,最后采访问自己的值
我们要的答案就是访问的值,没有看懂的看我下面的这个例子

在这里插入图片描述以此图为例
前序遍历:20 15 10 5 13 16 25 30 50
中序遍历:5 10 13 15 16 20 25 30 50
后序遍历:5 13 10 16 15 30 50 25 20
PS(如果有错请指出来谢谢了)
如果仔细观察就能够发现其实,之所以使用的二叉搜索树就是因为二叉搜索树的中序遍历是有序的。

二叉搜索树前序遍历具体实现
/**
	 * 先序遍历先去访问他的值
	 */
	public void preOrder() {
		preOrder(root);
	}
	
	/**
	 * 前序节点的非递归算法
	 */
	public void preOrderNP() {
		Stack<Node> stack=new  Stack<Node>();
		stack.push(root);
		while(!stack.isEmpty()) {
			Node cur=stack.pop();
			System.out.println(cur.e);
			if(cur.right!=null) {
				stack.push(cur.right);
			}
			if(cur.left!=null) {
				stack.push(cur.left);
			}
		}
	}
	
	/**
	 * 隐藏的前序遍历的做法
	 * @param node
	 */
	private void preOrder(Node node) {
			if(node==null) {
				return ;
			}
			System.out.println(node.e);
			preOrder(node.left);
			preOrder(node.right);
			
			//递归的中止
//			if(node!=null) {
//				System.out.println(node.e);
//				preOrder(node.left);
//				preOrder(node.right);
//			}
			
		}

这上面是前序遍历,递归的写法和非递归的写法。
递归的写法是先去打印节点的值,然后去执行左子树的值,再去执行右子树的值。
非递归的写法我们借鉴了系统栈,Stack,将根节点放进栈中,先放右子树,在放左子树,这样的话出栈,先出左子树,然后才是右子树。

中序遍历
/**
	 * 中序遍历
	 */
	public void inOrder() {
		inOrder(root);
	}
	
	/**
	 * 不能给用户看的中序遍历
	 * @param node
	 */
	private void inOrder(Node node) {
		if(node==null) {
			return ;
		}
		inOrder(node.left);
		System.out.println(node.e);
		inOrder(node.right);
	}

上面是中序遍历的代码,一样的代码就是优先去找他的左子树,然后才是输出他的值,然后才是右子树。

后序遍历
/**
	 * 后序遍历
	 */
	public void postOrder() {
		postOrder(root);
	}
	
	/**
	 * 不能给用户看的后序遍历
	 * @param node
	 */
	private void postOrder(Node node) {
		if(node==null) {
			return ;
		}
		postOrder(node.left);
		postOrder(node.right);
		System.out.println(node.e);
	}

后序遍历就是先去找左子树,然后是右子树,然后才是自己的值,

层序遍历

其实我们上面的搜索可以是算作深度优先搜索,就是一直搜索到最深的位置,为止,然后回退到能够进行搜索的位置为止,在这里我们完成另一种的搜索方式,叫做层序遍历,就是到了同一层,先遍历完这一层才会继续往下去找,不能说哪一种搜索方式更加的优秀,但是每一种方式都有自己的优点和缺点。

/**
	 * 层序遍历,一层一成的进行遍历
	 */
	public void levelOrder(){
		Queue<Node> q=new LinkedList<>();
		q.add(root);
		while(!q.isEmpty()) {
			Node remove = q.remove();
			System.out.println(remove.e);
			if(remove.left!=null) {
				q.add(remove.left);
			}
			if(remove.right!=null) {
				q.add(remove.right);
			}
		}
	}

在这里我利用了队这个数据结构,因为是一层,所以到一层就把这一层中所有的子树按照,先放左子树后放右子树的顺序放进队中,因为队是先进先出所以,先输出左子树,然后是右子树。

二叉树中元素的删除

因为结构是树,是一个非线性结构,跟我们平时操作的线性结构不同,所以我们的删除方式也不同。

删除二叉树中最小值

因为删除的算法可能不好表达,所以我开始先从最小的值开始删除。

/**
	 * 最小的元素
	 * 再删除最小的节点时,如果有右子树,删除以后当成跟左右子树
	 * @return
	 */
	public E minimun() {
		if(size==0) {
			throw new IllegalArgumentException("shuweikong");
		}
		return minimun(root).e;
				
	}
	
	private Node minimun(Node node) {
		if(node.left==null) {
			return node;
		}
		return minimun(node.left);
	}
	
/**
 // 从二分搜索树中删除最小值所在节点, 返回最小值
	 * @return
	 * 删除搜索二叉树时不要考虑一个问题,就是删除的元素和删一个元素的关系吗
	 */
	public E removeMin() {
		E ret=minimun();
		//这一行的意思是可能根节点会称为最小的节点被删除
		root=removeMin(root);
		return ret;
	}
	
	/**
	 * 第一步递归找到最左边的元素进行(如果有节点和没有节点)
	 * 第二部删除最左边的元素并且返回
	 * 没有节点的直接返回,
	 * 如果有节点,就把右子树变成左子树,
	 * 第三步通过递归实现,递归去,递归会,左子树=左子树
	 * @param node
	 * @return
	 */
	private Node removeMin(Node node) {
		//左子树为空,如果有右子树就把右子树保存一下
		if(node.left==null) {
			Node rightNode=node.right;
			node.right=null;
			size--;
			return rightNode;
		}
		//如果左子树不为空,一直递归
		node.left=removeMin(node.left);
		return node;
	}

我们在这里提供了四个方法,但是其实也只是为了实现两个功能,
第一个是将搜索二叉树中的最小值,输出。
第二个是删除搜索二叉树的最小值
在这里插入图片描述在这里插入图片描述你们看我画的图,如果最左边的也叶子结点有元素的话,那就是最左边的叶子节点是最小,这一点是二叉搜索树的定义,这一点没有问题,但是我们的问题是,如果最左边的元素没有左子树怎么办?
这两张图的区别就是第二张图是第一张图去掉了最左边的元素,那么现在谁是最小的呢?
经过观察,试验,总结,我们得出结论,其实还是最左边元素是最小值,不管他有没有子树,只要是他是最左边的元素他就是最小的。
这样我们的寻找最左边的元素就逻辑出来了。
找到最左边的元素。
那么我们删除最左边的元素的逻辑是什么的呢?
我们的逻辑是,如果最左边的元素是叶子节点,也就是没有子树,那么直接删除,并且取出。但万一有右子树呢,就像我上面的第二张图,那就是先去承将他的右子树,然后再去删除这个元素。
在这里我遇到了一个小问题,我们再删除的时候,我们将有右孩子的结点进行删除,将他的右孩子找一个新的元素去接,然后我就在想,那这个新的元素没有别的元素指向吗?是一个单独的嘛?
事实证明,是我小看了递归,递归不仅仅是递归大左子树为空的位置,而且是在返回时,node.left=removeMin(node.left);这一句代码,被递归回来,左子树从上向下完成了指向的功能。
还有就是在root=removeMin(root);这一行的代码,就是容易出现这个问题,我之前没有想过这个东西会出现问题,但是我在研究上一个问题的时候发现我,删除了前边的root=,没有什么变化,然后我通过读代码才发现,原来这句话意思是,如果咱们在删除最小元素的时候,删除到了根节点,这是根节点是会变得,如果根节点也被删除了根节点会发生改变,如果没有删除到根节点,他到最后返回还是根节点,这是我之前一直没有意识到的东西。

删除二叉树中最大值

一样的逻辑我就不多论述了

/**
	 * 删除node的最大节点
	 * 返回删除节点的根
	 * @return
	 */
	public E removeMax() {
		E ret=maximun();
//		System.out.println(ret);
		root=removeMax(root);
		return ret;
	}
	
	private Node removeMax(Node node) {
		if(node.right==null) {
			Node leftNode=node.left;
			node.left=null;
			size--;
			return leftNode;
		}
		node.right=removeMax(node.right);
		return node;
	}
删除任意元素
public void remove(E e) {
		root=remove(root,e);
	}
	
	private Node remove(Node node,E e) {
			if(node==null) {
				return null;
			}
			
			//左子树找到位置
			if(e.compareTo(node.e)<0) {
				node.left=remove(node.left, e);
				return node;
			}
			
			//右子树找到位置
			else if(e.compareTo(node.e)>0) {
				node.right=remove(node.right, e);
				return node;
			}
			
			//找到位置
			else {
				//左子树为空
				if(node.left==null) {
					Node rightNode=node.right;
					node.right=null;
					size--;
					return rightNode;
				}
				
				//右子树为空
				if(node.right==null) {
					Node leftNode=node.left;
					node.left=null;
					size--;
					return leftNode;
				}
				//找到指定位置右子树中最小的一个节点,当做根
				Node success=minimun(node.right);
				
				//删除这个最小的元素
				success.right=removeMin(node.right);
				
				//承接他的左子树
				success.left=node.left;
				
				node.left=node.right=null;
				return success;
			}
			
		}

删除的逻辑跟我之前的说的逻辑是一样的,先去寻找位置,通过递归的想法,然后找到位置e.compareTo(node.e)>0也就是那个else的时候,我们就开始准备删除元素,首先先判断这个元素左子树为空还是右子树为空,如果他的左子树为空,用一个新的元素来指向他的右子树,然后通过递归和上一个他的上一个元素,建立联系实现删除功能。
如果这个元素的左右子树都不为为空,就将找删除的指定位置最小的节点这个节点为叶子节点,删除这个最小的节点,将这个节点顶替我们要删除的位置,将我们要删除的节点左右子树都==null。

后记

后面关于二叉树的复杂度的比较我在后面会写。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

又是重名了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值