二、二叉搜索树(BST,也称二叉查找树):
1、定义:
是指一棵空树或者具有下列性质的二叉树:
任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
任意节点的左、右子树也分别为二叉查找树;
没有键值相等的节点。
2、性质:
构建一个有n个不同关键字的二查搜索树的期望高度为h = O(lgn);
下述所有查找、删除、插入等操作的时间复杂度为O(h)
3、定义二叉搜索树:
// 对于任一节点x,满足其左子树上的节点key都不大于x.key
// 右子树上的节点key都不小于x.key
private class SearchTree {
int key;
SearchTree parent;
SearchTree left;
SearchTree right;
}
4、查询:(时间复杂度O(lgn))
1)递归实现:
public SearchTree Search1(SearchTree TNode, int target) {
if (TNode == null || target == TNode.key) {
return TNode;
}
if (target > TNode.key) {
return Search(TNode.right, target);// 递归查询右子树
} else {
return Search(TNode.left, target);
}
}
2)迭代实现(效率相对要高的多):
public SearchTree Search(SearchTree TNode, int target) {
while (TNode != null && target != TNode.key) {
if (target > TNode.key) {
TNode = TNode.right;
} else {
TNode = TNode.left;
}
}
return TNode;
}
5、查询最大最小关键字:(时间复杂度O(lgn))
1)查找最小关键字:即寻找最左节点
public SearchTree MinTreeNode(SearchTree TNode) {
while (TNode.left != null) {
TNode = TNode.left;
}
return TNode;
}
2)查找最大关键字:即寻找最右节点
public SearchTree MaxTreeNode(SearchTree TNode) {
while (TNode.right != null) {
TNode = TNode.right;
}
return TNode;
}
6、查询后继和前驱:(时间复杂度O(lgn))
1)查找后继:
分为两种情况进行讨论:
(1)若该节点有右节点,则后继为其右节点的最左节点
(2)若该节点无右子树,则应该回溯到期祖宗节点进行考虑
(I)若节点为其父节点x.p的左子节点,即x.key小于x.p.key
(II)若节点为父节点的右子节点,查找大于其key的后继依然无法满足;故仍向上追溯直至某一节点z,直至使得x在其z左子树中。
public SearchTree TreeSuccessor(SearchTree XNode) {
if (XNode.right != null) {
return MinTreeNode(XNode.right);// 即查找右子树的最小关键字
}
SearchTree YNode = XNode.parent;
while (YNode != null && XNode == YNode.right) {// 循环至根节点之前的NIL或者该节点为父节点的左子节点
XNode = YNode;
YNode = YNode.parent;
}
return YNode;// 注意返回值
}
2)查找前驱:
类比于查找前驱的问题,
(1)当该节点有左子树,则前驱即为其左子树的最大关键字
(2)若该节点只有右子树,则返回其祖宗节点考虑,直至找到该节点x为z的右子树中的节点,则z为x的前驱
public SearchTree TreePreDecessor(SearchTree XNode) {
if (XNode.left != null) {
return MaxTreeNode(XNode.left);
}
SearchTree YNode = XNode.parent;
while (YNode != null && XNode == YNode.left) {
XNode = YNode;
YNode = YNode.parent;
}
return YNode;
}
7、插入节点:(时间复杂度O(lgn))
插入较为简单,类似于二分查找进行插入即可
注意搜索二叉树的特点,插入的节点最后一定成为了二叉树的叶子节点 通过不断比较x.key与节点y的key,确定节点x应该在y的左子树还是右子树
同时处理二叉树要时常注意根节点的特殊性,进行考虑
public void TreeInsert(SearchTree TRoot, SearchTree XNode) {
SearchTree YNode = null;
while (TRoot != null) {
YNode = TRoot;// 避免退出循环时TRoot为null,而无法进行访问
if (XNode.key > TRoot.key) {
TRoot = TRoot.right;
} else {
TRoot = TRoot.left;
}
}
XNode.parent = YNode;
// 注意根节点情况
if (YNode == null) {// 即TRoot=null,未进入循环
TRoot = XNode;
}
// 要判断XNode是左节点还是右节点
else if (XNode.key < YNode.key) {
YNode.left = XNode;
} else {
YNode.right = XNode;
}
}
8、删除节点:(时间复杂度O(lgn))
删除要考虑三种情况:(暂不考虑XNode不在树中的情况)
1)XNode为叶子节点,没有子孩子,则将其删除,并将其父节点对应孩子节点置为null即可
2)XNode有一个孩子节点YNode,无论是左右孩子,将YNode替代XNode即可
3)XNode有两个孩子节点时,查找XNode的后继节点MNode(因为XNode左右节点都存在,则其后继节点一定存在)
然后让MNode的值替换XNode的值,删除MNode即可;于是下面问题就转化成为删除节点MNode的问题,仍然也需要分成这些情况(但是后继节点一定为叶子节点,故不用开率第三种情况,只需要考虑前两种情况即可)
// 替代辅助函数,注意这里面的替代仅是将VNode交换到UNode的位置,UNode本身并未作改变,后继也未继承
private void Transplant(SearchTree TRoot, SearchTree UNode, SearchTree VNode) {
// 考虑根节点的情况
if (UNode.parent == null) {
TRoot = VNode;
} else if (UNode == UNode.parent.left) {
UNode.parent.left = VNode;
} else {
UNode.parent.right = VNode;
}
if (VNode != null) // 注意此条件判断,null是无法设置各种状态的
{
VNode.parent = UNode.parent;
}
}
/********************** 删除函数 ************************/
public void TreeDelete(SearchTree TRoot, SearchTree XNode) {
// 情况一
if (XNode.left == null && XNode.right == null) {
XNode = null;// 省去判断XNode是否为根节点
}
// 情况二
else if ((XNode.left != null)&&(XNode.right == null)) {
Transplant(TRoot, XNode, XNode.left);
} else if ((XNode.right != null)&&(XNode.left == null)) {
Transplant(TRoot, XNode, XNode.right);
}
// 情况三
else {
SearchTree YNode = XNode.right;
// 情况3.1
if (YNode.left == null) {
Transplant(TRoot, XNode, YNode);
// 交换之后注意设置子孩子
YNode.left = XNode.left;
YNode.left.parent = YNode;
}
// 情况3.2
else {
SearchTree MNode = MinTreeNode(YNode);
Transplant(TRoot, MNode, MNode.right);// 用NNode替换MNode
// 注意MNode发生改变
MNode.right = XNode.right;
MNode.right.parent = MNode;// 要注意设置XNode孩子的节点的parent
Transplant(TRoot, XNode, MNode);
MNode.left = XNode.left;
MNode.left.parent = MNode;
}
}
}
三、二叉树三种遍历
二叉树三种遍历方式,即先序,中序,后序遍历,是根据访问根节点的优先顺序来划分的;
先序遍历就是先访问根节点,再访问左右节点;中序遍历则是先访问左节点,再访问根节点,最后访问右节点;后序遍历类推;
1、定义二叉树:
二叉树的定义分为两种方式,一种是链式存储,即使用链表形式;一种是顺序存储,使用数组来实现,为二叉树每个节点规定标号;
使用顺序存储的缺点是不够灵活,适合表示完全二叉树,非完全二叉树使用顺序存储方式可能会存在多个空值情况,造成空间浪费;
1)链式存储:
//链式存储
public static class BinaryTreeNode {
int mValue;
BinaryTreeNode mLeft;
BinaryTreeNode mRight;
public BinaryTreeNode(int mValue) {
this.mValue = mValue;
}
}
2)顺序存储:
private final int MAX_SIZE = 10;
//顺序存储
class BinaryTreeNode2 {
int[] data = new int[MAX_SIZE];
int length;
}
2、递归实现三种遍历:
递归实现较为简单,只需要调整访问根节点的位置顺序即可;
/******************递归实现***************************/
//先序遍历
public int PreOrderTreeWalk(BinaryTreeNode pNode) {
if(pNode == null)
return 0;
visitNode(pNode);
PreOrderTreeWalk(pNode.mLeft);
PreOrderTreeWalk(pNode.mRight);
return 1;
}
//中序遍历
public int InOrderTreeWalk(BinaryTreeNode pNode) {
if(pNode == null)
return 0;
InOrderTreeWalk(pNode.mLeft);
visitNode(pNode);
InOrderTreeWalk(pNode.mRight);
return 1;
}
//后序遍历
public int PostOrderTreeWalk(BinaryTreeNode pNode) {
if(pNode == null)
return 0;
PostOrderTreeWalk(pNode.mLeft);
PostOrderTreeWalk(pNode.mRight);
visitNode(pNode);
return 1;
}
3、先序遍历(非递归)
使用栈为辅助来实现先序遍历
1)一般实现方法:
即先一直往左访问,知道访问到某一节点的左节点为空,再以该节点的右节点为始,继续往下访问;
使用Stack后入先出,注意先序的访问顺序为先进行访问:
public int PreOrderTraverse(BinaryTreeNode pNode) {
Stack<BinaryTreeNode> stack = new Stack<>();
if (pNode == null)
return 0;
while (!stack.isEmpty() || pNode != null) {
while (pNode != null) {
//先访问
visitNode(pNode);
stack.push(pNode);
//遍历左节点
pNode = pNode.mLeft;
}
//返回顶层元素
pNode = stack.peek();
stack.pop();
//遍历右节点
pNode = pNode.mRight;
}
return 1;
}
2)根据Stack的特性,后入先出,则访问一个节点值只有,先push右子节点,再push左子节点,则在之后的访问顺序中,一定是先访问左节点,再访问右节点(根节点已经访问过了);
public int PreOrderTraverse2(BinaryTreeNode pNode) {
if (pNode == null)
return 0;
Stack<BinaryTreeNode> stack = new Stack<>();
stack.push(pNode);
while (!stack.isEmpty()) {
pNode = stack.pop();
visitNode(pNode);
if (pNode.mRight != null)
stack.push(pNode.mRight);
if (pNode.mLeft != null)
stack.push(pNode.mLeft);
}
return 1;
}
4、中序遍历(非递归):
类同于先序遍历,只不过注意根节点的访问顺序的不同
//中序遍历
public int InOrderTraverse(BinaryTreeNode pNode) {
Stack<BinaryTreeNode> stack = new Stack<>();
if (pNode == null)
return 0;
while (!stack.isEmpty() || pNode != null) {
while (pNode != null) {
stack.push(pNode);
pNode = pNode.mLeft;
}
pNode = stack.pop();
visitNode(pNode);
pNode = pNode.mRight;
}
return 1;
}
5、后序遍历(非递归):
后序遍历比较复杂,需要确定左右节点都访问之后,才对根节点进行访问;
1)方法一:
用一个标记标记右子树是否访问过
对于任一结点P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,此时该结点出现在栈顶,但是此时不能将其出栈并访问,因此其右孩子还为被访问。所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。
//用以实现后续遍历的辅助结构
private class HelpNode {
BinaryTreeNode treeNode;
boolean isFirst;
}
public int PostOrderTraverse(BinaryTreeNode pNode) {
if (pNode == null)
return 0;
Stack<HelpNode> stack = new Stack<>();
HelpNode helpNode;
while (!stack.isEmpty() || pNode != null) {
//一直循环至最左节点
while (pNode != null) {
HelpNode temp = new HelpNode();
temp.treeNode = pNode;
temp.isFirst = true;
stack.push(temp);
pNode = pNode.mLeft;
}
if (!stack.isEmpty()) {
helpNode = stack.pop();
if (helpNode.isFirst)//表示第一次,即每一个要被访问的根节点要被push两次
{
helpNode.isFirst = false;
stack.push(helpNode);
pNode = helpNode.treeNode.mRight;//右节点的是否有效则移至循环的开始出进行判断
} else {
visitNode(helpNode.treeNode);
pNode = null;
}
}
}
return 1;
}
2)方法二:双栈法
即再使用一个辅助栈stack2,stack1的访问顺序有点像先序遍历中第二种方法,先序遍历的访问顺序为根,左,右;在这里调整stack1的push顺序,使得stack1的访问顺序为根,右,左,则依次push进stack2中,stack2弹出顺序可想而知,就是后序遍历的顺序;
public int PostOrderTraverse2(BinaryTreeNode pNode) {
if (pNode == null)
return 0;
Stack<BinaryTreeNode> stack1 = new Stack<>();
Stack<BinaryTreeNode> stack2 = new Stack<>();//辅助栈
//存入根节点,初始化
stack1.push(pNode);
//stack1弹出的元素,压入stack2,在将该元素的左右节点压入stack1
while (!stack1.isEmpty()) {
pNode = stack1.pop();
stack2.push(pNode);
if (pNode.mLeft != null) {
stack1.push(pNode.mLeft);
}
if (pNode.mRight != null) {
stack1.push(pNode.mRight);
}
}
//stack弹出的即是后序遍历的顺序
while (!stack2.isEmpty()) {
visitNode(stack2.pop());
}
return 1;
}
3)方法三:
要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点P,先将其入栈。
I)如果P不存在左孩子和右孩子,则可以直接访问它;
II)或者P存在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了,则同样可以直接访问该结点。
若非上述两种情况,则将P的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子前面被访问,左孩子和右孩子都在根结点前面被访问。
public int PostOrderTraverse3(BinaryTreeNode pNode) {
if (pNode == null)
return 0;
BinaryTreeNode preVisitedNode = null;
Stack<BinaryTreeNode> stack = new Stack<>();
stack.push(pNode);
while (!stack.isEmpty()) {
pNode = stack.peek();
if ((pNode.mLeft == null && pNode.mRight == null)//左右子树均为空的情况,即叶子节点
|| (preVisitedNode != null &&
(preVisitedNode == pNode.mLeft || preVisitedNode == pNode.mRight)))//左右子树已经被访问的情况,如果有右子树,则下一次栈顶一 //定为右子树;若无右子树,则栈顶为根节点;故保证了左子/ //树-右子树-根节点的访问顺序
{
visitNode(pNode);
preVisitedNode = stack.pop();
} else {
if (pNode.mRight != null)
stack.push(pNode.mRight);//注意push的顺序,先访问右子树
if (pNode.mLeft != null)
stack.push(pNode.mLeft);
}
}
return 1;
}