结点的深度(depth):根结点到当前结点唯一路径的结点树
结点的高度(height):当前结点到最远叶子结点的路径上的结点数
二叉树的性质:
- 非空二叉树的第i层,最多有2^(i-1)个结点
- 高度为h的二叉树上最多有2^h-1个结点
- 设叶子结点n0,度为1的结点为n1,度为2的结点为n2;n0 = n2 + 1;结点总数n = n0 + n1 + n2;因此二叉树的边数T = n1 + n2*2 = n - 1 = n0 + n1 + n2 - 1
真二叉树:结点的度只有0或2
完全二叉树:从根结点到倒数第二层都是满二叉树,叶子结点只在倒数后两层出现,且最后一层叶子结点位于树的左侧
性质:完全二叉树从上到下、从左到右对结点从0开始编号,对任意i结点
- 如果i=0,它是根结点
- 如果i>0,它的父节点编号为 floor((i-1)/2)(floor()意为向下取整)
- 如果 2i+1<=n-1,它的左子结点编号为 2i+1
- 如果 2i+1>n-1,它没有左子结点
- 如果 2i+2<=n-1,它的右子结点编号为 2i+2
面试题:如果一棵完全二叉树有768个结点,求叶子结点的个数
假设叶子结点个数为n0,度为1的结点个数为n1,度为2的结点个数为n2
则总结点个数n = n0 + n1 + n2,且n0 = n2 + 1
所以 n = 2n0 + n1 - 1
因为完全二叉树的n1要么是0,要么是1
所以n1为1时,总结点数是偶数,n0 = n/2
反之n1为0时,总结点数是奇数,n0 = (n+1)/2
综上,n0 = floor(n+1/2) = 384
二叉搜索树
任意一个结点的值都大于其左子树所有结点的值,任意结点的值都小于其右子树所有结点的值,并且它的左右子树也为二叉搜索树
二叉搜索树的元素不能为空
添加结点:
- 找到父结点
- 创建新结点node
- 根据大小,使得 parent.left = node 或 parent.right = node
- (如果遇到值相等的结点,可以直接将旧的覆盖)
public void add(E element) {
ElementNotNullCheck(element);
//添加第一个结点
if (root == null) {
root = new Node<E>(element,null);
size++;
return;
}
//不是第一个结点,找到父结点
Node<E> parent = root;
Node<E> node = root;
int cmp = 0;
while (node != null) {
cmp = compare(element, node.element);
parent = node; //在向下传递之前保存父结点
if (cmp > 0) {
node = node.right; //结点和右子结点继续比较
}else if (cmp < 0) {
node = node.left; //结点和左子结点继续比较
}else {
return; //相等
}
}
Node<E> newNode = new Node<E>(element, parent); //此结点位置为元素插入位置
if (cmp < 0) {
parent.left = newNode; //成为左子结点
}else if (cmp > 0) {
parent.right = newNode; //成为右子结点
}
size++;
}
里面的比大小compare根据需求自己调节
二叉树的遍历
1、前序遍历
根结点,前序遍历左子树,前序遍历右子树
private void preorderTraversal(Node<E> node){
if (node == null) return;
System.out.println(node.element);
preorderTraversal(node.left);
preorderTraversal(node.right);
}
2、中序遍历
中序遍历左子树,根结点,中序遍历右子树
中序遍历的结果是升序或降序的
private void inorderTraversal(Node<E> node){
if (node == null) return;
inorderTraversal(node.left);
System.out.println(node.element);
inorderTraversal(node.right);
}
3、后序遍历
后序遍历右子树,根结点,后序遍历左子树
private void postorderTraversal(Node<E> node){
if (node == null) return;
postorderTraversal(node.left);
postorderTraversal(node.right);
System.out.println(node.element);
}
4、层序遍历(重点)
使用队列,先将根结点入队,再进行循环
循环包括:先将队头结点出队,然后将该结点的左子结点入队,再将该结点的右子结点入队
public void levelOrderTraversal(){
if(root == null) return;
Queue<Node<E>> queue = new LinkedList<>(); //官方队列
queue.offer(root); //根结点入队
while(!queue.isEmpty()){
Node<E> node = queue.poll(); //头结点出队
System.out,println(node);
if(node.left != null){
queue.offer(node.left); //结点左子结点入队
}
if(node.right != null){
queue.offer(node.right); //结点右子结点入队
}
}
}
5、利用前序遍历树状打印二叉树
这个树状图大致为
根结点
L---左子树根结点
L---L---左子树左子结点
L---R---左子树右子结点
R---右子树根结点
R---L---右子树左子结点
R---R---右子树右子结点
public String toString() {
StringBuilder sb = new StringBuilder();
toString(root, sb, ""); //打印根结点
return sb.toString();
}
private void toString(Node<E> node, StringBuilder sb, String prefix) {
if (node == null) return;
sb.append(prefix).append(node.element).append("\n"); //输出内容为前缀+元素+换行
toString(node.left, sb, prefix + "L---"); //输出左子结点
toString(node.right, sb, prefix + "R---"); //输出右子结点
}
练习:
1、计算二叉树高度
二叉树高度从叶子结点算起,叶子结点高度为一,根结点高度最大
结点的高度 = 它的左子结点与右子结点高度最大的那个 + 1
所以利用递归,从根结点一层层直到叶子结点,算高度
public int height(){
return height(root);
}
private int height(){
if(node == null) return 0; //如果结点为空,那这个二叉树肯定是空的,高度为0
return 1 + Math.max(height(node.left),height(node.right)); //max函数选出最大的那个
}
非递归,使用层序遍历,每遍历完一层,就让高度+1
层序遍历使用队列,通过观察可以发现,当上一层元素全部出队时,队列的长度等于一层元素的个数,因此设定一个变量levelSize,在while循环中,levelSize不断递减,每当levelSize = 0时,即为一层元素全部出队,这时这个变量的值被赋上队列长度,继续循环,此时记录一层
public int height() {
if (root == null) return 0;
int height = 0;
int levelSize = 1; // 存储着每一层的元素数量
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node<E> node = queue.poll();
levelSize--;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
if (levelSize == 0) { // 意味着即将要访问下一层
levelSize = queue.size();
height++;
}
}
return height;
}
2、判断一棵树是否为完全二叉树
使用层序遍历,判断每一个结点的左右子结点情况
如果左右子结点都有,就继续入队
如果有右子结点,但是左子结点为空,那么这不是完全二叉树,返回false
如果有左子结点,但是右子结点为空,那么要看他后面的结点是不是都是叶子结点,如果都是,说明是完全二叉树,反之就不是完全二叉树
public boolean isComplete(){
if (root == null) return;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root); //根结点入队
if(leaf && node.left != null && node.right != null){
return false; //如果要求node是叶子结点但却不是
}
boolean leaf = false; //看node是否为叶子结点
while(queue.size != 0){
Node<E> node = queue.poll(); //node存放出队元素
if(node.left != null && node.right != null){ //都不为空,入队,继续遍历
queue.offer(node.left);
queue.offer(node.right);
}else if(node.left == null && node.right != null){
return false;
}else{
leaf = true; //要不就是只有左子结点,要不就是叶子结点
if (node.left != null){
queue.offer(node.left); //只有左子结点也入队,继续遍历
}
}
}
return true; //以上都不符合,说明是完全二叉树
}
第二种写法:判断方式改为左右子结点分开单独成条件判断
public boolean isComplete() {
if (root == null) return false;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
boolean leaf = false;
while (!queue.isEmpty()) {
Node<E> node = queue.poll();
if (leaf && node.left != 0 && node.right != 0) return false;
if (node.left != null) {
queue.offer(node.left);
} else if (node.right != null) { //node.left == null && node.right != null
return false;
}
if (node.right != null) {
queue.offer(node.right);
} else { //node.right == null
leaf = true;
}
}
return true;
}
前驱结点:中序遍历时的前一个结点
如果是二叉搜索树,前驱结点就是比这个结点小一点的那个结点
查找位置,在左子树找最右边的那个结点
如果左子树为空,但是父结点不是空,且父结点比它小,那么父结点就是它的前驱结点;如果父结点比它大,那就找父结点的父结点,直到找到比它小的结点(结点在这个父结点的右子树中)
如果左子树为空,父结点也为空,说明这个结点没有前驱结点
private Node<E> predecessor(Node<E> node) {
if (node == null) return null;
//前驱结点在左子树当中(left.right.right.right....)
Node<E> p = node.left;
if (p != null) {
while (p.right != null) {
p = p.right;
}
return p;
}
// 从父结点、祖父结点中寻找前驱结点
while (node.parent != null && node == node.parent.left) {
//父结点不为空且结点是父结点的左子树
node = node.parent;
}
//剩下父结点为空或者结点是右子树
return node.parent;
}
后继结点同理
删除结点
1、叶子结点直接删除
2、度为1的结点,用子结点替代原结点的位置
child.parent = node.parent;
node.parent.left(right) = child;
3、如果要删除的结点是根结点,让root指向子结点即可
root = child;
child.parent = null;
4、度为2的结点,用它的前驱结点或后继结点取代它,再删除原来的前驱结点或后继结点
如果一个结点度为2,那它的前驱或后继结点的度只能是1或0,那么删除过程也就转换成上面两种情况
public void remove(E element){ //调用函数一般直接删内容,但是要找结点
remove(node(element));
}
public void remove(Node<E> node){
if(node == null) return;
size--; //树长度减1
if(node.left != null && node.right != null){
Node<E> s = successor(node); //找到后继结点
node.element = s.element; //后继结点覆盖原结点
node = s; //删除原结点
}
//此时只剩下度为0或1的情况
Node<E> replacement = node.left != null ? node.left : node.right; //找到代替值
if(replacement != null){ //度为1
replacement.parent = node.parent; //更换父结点
if(node.parent == null){ //结点度为1且为根结点
root = replacement;
}else if(node == node.parent.left){ //是左结点
node.parent.left = replacement;
}else{
node.parent.right = replacement; //右结点
}
}else if(node.parent == null){ //这个树只有这一个结点
root = null;
}else{ //叶子结点
if(node == node.parent.left){ //是左结点
node.parent.left = null;
}else{
node.parent.right = null; //是右结点
}
}
}
private Node<E> node(E element){ //通过元素找结点
Node<E> node = root;
int cmp = compare(element,node.element); //比大小,看往哪里遍历
if(cmp == 0) return node;
if(cmp > 0){
node = node.right;
}else{
node = node.left;
}
}