关于二叉查找树的操作还是挺重要的,所以在此实现了BST的相关操作。
一、BST树的相关定义
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
二、BST的相关操作
首先实现BST的遍历操作吧,一般面试也喜欢问,比较重要的是各个遍历的非递归操作,在这里也就只实现非递归操作;
其中先序和中序遍历的非递归操作比较相似,主要思想就是将每个节点当做一个根节点进行操作。后序遍历稍微不同。下面进行代码的演示,以及相关解释:
首先针对先序遍历
Stack<TreeNode> snode = new Stack<>(); TreeNode p = root; while (!snode.isEmpty() || p != null) { if (p!=null) { System.out.print(p.value+" "); snode.push(p); p = p.left; }else{ TreeNode q = snode.pop(); p = q.right; } }
可以看到代码中利用了一个栈进行辅助,因为先序遍历的顺序是根-左-右,所以在向左节点进行遍历的时候,将节点输出,同时将节点放入栈中。当向左边到达顶的时候,就将栈顶的节点pop出来,如果这个节点的右节点不为空,就向这个节点的右边遍历,这时,将这个节点看做根节点,重复之前的操作。
当然,这里还有一个更好的方法,是利用栈的后进先出的特性,这个方法是我在面试思科的时候,面试官告诉我的,所以当时对于思科的影响很好。代码如下:
Stack<TreeNode> snode = new Stack<>(); snode.push(root); while (!snode.isEmpty()) { TreeNode q = snode.pop(); System.out.print(q.value + " "); if (q.right != null) { snode.push(q.right); } if (q.left != null) { snode.push(q.left); } }
可以看到,这里我们向树的左子树遍历的时候,针对每个节点,都是将其左右子节点反过来放入栈中,也就是先放入右节点,然后放入左节点。因为每一次循环,我们就将栈顶的节点pop出去,所以遍历每个节点的时候就要将这个节点的左右节点放入栈中,保存下来,而因为左节点是后放入的,所以每次先pop出,也就实现了根-左-右的遍历顺序。如果感觉表达的不是很清楚,可以画图辅助理解。
中序遍历
Stack<TreeNode> snode = new Stack<>(); TreeNode p = root; while (!snode.isEmpty() || p != null) { if (p != null) { snode.push(p); p = p.left; } else { TreeNode q = snode.pop(); System.out.print(q.value + " "); p = q.right; } }
中序遍历和先序很是相似,只不过,在遍历每个左节点的时候不直接输出,而是将其放入栈中,当后序从栈中pop出来的时候,再进行输出操作。同时,如果这个pop出来的节点不为空的话,就向这个节点的右节点遍历,将这个右节点当做一个根节点进行上面的类似操作。
后序遍历
/** * 后序迭代遍历和先序,中序不一样,需要判断什么时候读取根节点,同时需要考虑将根节点重新放入栈中的情况 */ Stack<TreeNode> snode = new Stack<>(); TreeNode lastnode = null; TreeNode curnode = root; while (curnode != null) { snode.push(curnode); curnode = curnode.left; } while (!snode.isEmpty()) { /** * 只有当右节点不存在或者右节点是上一个被访问的节点的时候才会访问根节点 */ curnode = snode.pop(); if (curnode.right == null || curnode.right == lastnode) { System.out.print(curnode.value + " "); lastnode = curnode; } else { /** * 当右子树存在,并且不是上一个访问的元素的时候,将根节点重新入栈,同时进入右子树,将左子树入栈 */ snode.push(curnode); curnode = curnode.right; while (curnode != null) { snode.push(curnode); curnode = curnode.left; } } }
在上述的代码以及注释中我们可以看到,首先我们需要将对树进行向左边的遍历,知道左边的叶子节点,后序的每次循环,我们都将栈顶的节点pop出来,然后将这个节点看做一个根节点,然后我们要判断什么时候会将根节点输出,可以通过分析知道,只有当此节点的右子节点为空,或者这个右子节点是上一个被访问的节点的时候,我们会访问这个根节点,如果右节点存在并且不是上一个被访问的节点的时候,我们需要将这个刚刚pop出来的节点重新放入栈中,同时向这个节点的右节点遍历,通过一个while循环,遍历到该节点的最左节点,然后重复之前的操作。
接下来我们来讨论BST树的插入,删除和查找
首先针对查找,这个并不难,可以分为递归和非递归,如下就直接献上代码了:
TreeNode curnode = root; while (curnode != null) { if (curnode.value < node3.value) { curnode = curnode.right; }else if(curnode.value > node3.value){ curnode = curnode.left; }else{ System.out.print("查找成功!"); return true; } } System.out.print("找不到该节点!");
这是非递归的形式
if (root != null) { if (node3.value == root.value) { return root; } else if (node3.value > root.value) { return find(node3, root.right); } else { return find(node3, root.left); } } else return null;
这是递归的形式
接着讨论插入
TreeNode curnode = root; if (root == null) { root = node1; return root; } else { while (curnode != null) { if (curnode.value < node1.value) { if (curnode.right == null) { curnode.right = node1; node1.parent = curnode; break; } else { curnode = curnode.right; } } else { if (curnode.left == null) { curnode.left = node1; node1.parent = curnode; break; } else { curnode = curnode.left; } } } } return root;
这里和查找的思想比较相似,只是当需要插入的节点的值小于当前节点的时候,向左边遍历,大于当前节点的时候,向右边遍历,直到当前节点的左节点或者右节点为空的时候,将需要插入的节点赋给这个空节点。
最后讨论删除操作
/** * 节点的删除分为三种情况: * 1、节点没有左右子节点 * 2、只有左节点或者右节点 * 3、既有左,又有右节点 */ TreeNode delnode = find(node2, root); TreeNode child; if (delnode.left != null && delnode.right != null) { /** * 左右子树都不为空,则找到他的后继节点 */ TreeNode pnode = delnode.right; while (pnode.left != null) { pnode = pnode.left; } /** * 此时这个pnode就是原delnode的后继节点 */ delnode.value = pnode.value; delnode = pnode; } if (delnode.left != null) { child = delnode.left; } else { child = delnode.right; } if (child != null) { child.parent = delnode.parent; } if (delnode.parent == null) { root = child; } else if (delnode.parent.left == delnode) { delnode.parent.left = child; } else if (delnode.parent.right == delnode) { delnode.parent.right = child; }
在这里我们需要分为三种情况:需要删除的节点有左右节点,只有左节点或者右节点,没有左右节点。
在这里当左右节点同时存在的时候,我们可以将其转化为只有一个节点,或者没有节点的情况。之后统一操作。
这里情况稍微复杂,暂时引用一个引用,后序有时间补上:
二叉查找树 - 删除节点详解