数据结构--树(低阶树)

树的表达方式

树的定义

树是n个节点的有限集合,满足1.有且只有一个根节点;2.其余节点可以分为m个互不相交的子集,每一个子集也是一颗树,称为根的子树

树的概念

节点:树结构存储的每一个数据元素都被称为“节点”。

根节点:该节点没有前驱。

父节点:子节点和兄弟节点。如图

叶子节点:该节点没有任何子节点

节点的度:节点拥有子树的个数,称为节点的度。

树的度:节点的度的最大值。

树的深度(高度):根节点开始,根为第一层,根孩子为第二层,直到最深的叶子节点

树的存储结构

1.双亲表示法

利用顺序表存储普通树,核心是存储各个节点时,给每个节点添加一个父节点位置的变量。由于根节点没有父节点,通常设置为-1,寻找父亲节点时间复杂度 O(1),由于可以通过孩子节点直接找到父亲节点,寻找孩子节点时间复杂度O(N),因为要遍历整个数组

2.孩子表示法

使用的是“顺序表+链表的结构”,从根节点开始,用顺序表储存树的各个节点,每个节点是一个链表,连接和他的孩子,它查找孩子效率较高,但是不擅长查找父节点,

3.孩子兄弟表示法

从根节点开始,依次存储各个节点的孩子和兄弟节点。内容:孩子指针域|数据域|兄弟指针域

二叉树

每个节点最多两颗子树,不允许存在度大于2的树,以下都可以称为二叉树

满二叉树:分支节点都存在左右子树,而且所有叶子节点在同一层,一颗深度为k的满二叉树节点有(2^k)-1个

完全二叉树:节点数介于2^(k-1)-1到2^k-1之间,若某节点编号为i,那么在满二叉树中节点编号也是i,叶子节点只能在最下层和次下层

二叉树性质

1:第i层最多2^(i-1)个节点
2:深度为k的二叉树至多有2^k - 1个节点。
3:叶节点数目比度为2节点数目多一个。推导:总结点=n0+n1+n2=n1+2*n2+1(这个1是根节点),n0,n1,n2表示度
4:N个节点的完全二叉树深度为[logN]+1

代码实现

首先是定义节点,一般是需要左孩子,右孩子以及本身的值

static class TreeNode {
        public char val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(char val) {
            this.val = val;
        }
    }

二叉树的遍历:

1.中序遍历:

先找左边,再输出自己,最后寻找右树,例如上面树的中序遍历为D,B,E,A,F,C,G,先是采用递归进行实现树的中序遍历:
我们先是采用递归来进行树的中序遍历:

public void inOrder(TreeNode root) {
        if(root == null)
            return;
        System.out.print(root.val+" ");
        inOrder(root.left);
        inOrder(root.right);
    }

我们用栈来进行递归的模拟:

首先定义一个cur指针,指向树的根节点,当这个指针不为空时,我们先将cur压入栈中,然后再向左走,即:

while (cur != null) {
  stack.push(cur);
  cur = cur.left;
}

但是cur可能向左指向空,因此我们需要找到cur节点的前驱,即top=stack.pop(),因为当cur指向空时,stack栈顶存放的就是cur节点的前驱,打印top的value值,在进行向右寻找,右边遇到空时,还是找前驱:这里分为两种情况,左空+右空,由于左边执行pop时已经弹出了前驱,右边执行时需要找前驱的前驱,正好执行pop弹出前驱的前驱,代码无误,如果实在不理解,看下图:

一个为空只执行一次弹出,前序遍历有写,非递归代码如下:

public void inOrderNor(TreeNode root) {
        if(root == null) return;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;

        while (cur != null || !stack.isEmpty()) {
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            TreeNode top = stack.pop();
            System.out.print(top.val + " ");

            cur = top.right;
        }
    }

2.前序遍历:

先输出自己,再向左树寻找,找完左树,在寻找右树,例如上面的树前序遍历为:A,B,D,E,C,F,G,。

public void preOrder(TreeNode root) {
    if(root == null) return;
    System.out.print(root.val+" ");
    preOrder(root.left);
    preOrder(root.right);
}

用栈进行递归模拟:

首先定义一个cur指针,初始化指向头节点,当这个指针不指向空的时候,我们将cur指向的节点压入栈中,并打印当前节点的value值,然后cur向左走,遇到一个值执行上面操作,因此我们得到:

while(cur != null) {
     stack.push(cur);
     System.out.print(cur.val + " ");
     cur = cur.left;
}            

像上图,如果我们cur指到D节点,cur=cur.left执行后,cur为空时怎么办呢?由于我们向栈中压入元素,栈不为空,我们需要定义一个指针指向该节点的前驱(即他从哪里来的),这里我们定义一个top指向前驱,然后使cur指向前驱的右孩子(这里是前序遍历,先自己,自己已经打印,再左,左为空,因此轮到了右),右边如果不是空,那么按照while(cur != null)这个循环走下去,如果是空,那么我们需要回到cur前驱的前驱(例如:cur为D节点的右,我们需要回到B节点),怎么回?

TreeNode top = stack.pop();
cur = top.right;

已知我们D左边为空时,第一次执行pop,弹出了D,当右边为空时,再次执行pop,pop了B,即top指向了B,那么可能会问,如果左边不空,右边空怎么办?这样子的代码有错吗?好,那我们分析一下;
假设还是上面的完全二叉树,我们这里去掉E节点,考虑B节点,按照刚才的分析,top现在指向了B(且B已经被弹出),cur=top.right,现在cur为空,执行pop,弹出A,那么现在top指向了A,cur指向了A的右孩子,继续按照代码执行。。。总体代码如下:   第一个while中cur!=null,是为了让第一步进来,因为此时栈为空

public void preOrderNor(TreeNode root) {
        if(root == null) return;
        Stack<TreeNode>stack = new Stack<>();
        TreeNode cur = root;

        while(cur != null || !stack.isEmpty()) {
            while(cur != null) {
                stack.push(cur);
                System.out.print(cur.val + " ");
                cur = cur.left;
            }
            TreeNode top = stack.pop();
            cur = top.right;
        }
    }

3.后序遍历:

先找左边,再找右边,最后输出自己,例如对于上面树的后序遍历,  D,E,B,F,G,C,A先是采用递归进行实现后序遍历:

public void postOrder(TreeNode root) {
        if(root == null) return;
        postOrder(root.left);
        postOrder(root.right);
        System.out.print(root.val+" ");
    }

 用栈进行递归的模拟:

由于是先打印左树,后打印右树,最后才是自己,因此我们还是先定义一个cur指针指向根节点,让他一直左走,当左数为空时寻找右树,当右树为空时以及右树不为空时:有如下代码

while (cur != null) {
   stack.push(cur);
   cur = cur.left;
}
TreeNode top = stack.peek();
if (top.right == null) {
    //左右都是空了这是可以打印弹出打印的元素
    System.out.print(top.val + " ");
    stack.pop();
} else {
    cur = top.right;
    }
}

这样子乍一看没什么问题,对吗?不对!!!就算栈里弹出了E,也不代表B的右树为空,不要混淆这两个概念!!!那么一个栈是行不通的。我们可以增加一个指针p,当弹出后记录下top的节点,由于此时弹出的节点为top节点,当他被父节点指到时,即当top.right=p时,表示ok了。如图


代码如下

public void postOrderNor(TreeNode root) {
        if(root == null) return;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        TreeNode prev = null;
        while (cur != null || !stack.isEmpty()) {
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            TreeNode top = stack.peek();
            if (top.right == null || top.right == prev) {
                System.out.print(top.val + " ");
                stack.pop();
                prev = top;
            } else {
                cur = top.right;
            }
        }
    }

还有另一种方法,申请两个栈:第一个栈储存待处理的节点,按照先左树后右树顺序,弹出栈顶的元素到第二个栈中,这时处理弹出元素的左右节点(孩子),按照先左数后右树顺序。当第一个栈为空时处理完毕,弹出第二个栈的元素,弹出顺序即为后序遍历的顺序。代码如下:

public void postOrderNor(TreeNode root) {
    if (root == null) return;

    Stack<TreeNode> stack1 = new Stack<>();
    Stack<TreeNode> stack2 = new Stack<>();
    stack1.push(root);

    while (!stack1.isEmpty()) {
        TreeNode cur = stack1.pop();
        stack2.push(cur); // 将节点先压入第二个栈中

        // 先压左子树,后压右子树
        if (cur.left != null) {
            stack1.push(cur.left);
        }
        if (cur.right != null) {
            stack1.push(cur.right);
        }
    }

    // 从第二个栈中依次弹出并打印,顺序就是后序遍历的顺序
    while (!stack2.isEmpty()) {
        System.out.print(stack2.pop().val + " ");
    }
}

4.层序遍历

利用队列实现二叉树的层序遍历,先把根节点添加到队列中,先使一个cur指针指向root节点,根节点出队并打印,如果cur指向的不为空,将他的左右节点添加到队列中。代码如下

public void levelOrder(TreeNode root) {
    if(root == null) return null;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    
    while(!queue.isEmpty()) {
        TreeNode cur = queue.poll();
        Stystem.out.print(cur.val+" ");
        if(cur.left != null) queue.offer(cur.left);
        if(cur.right != null) queue.offer(cur.right);
    }
}

一些基础题目:

1.总节点个数:

递归:

法一:遇到节点进行++,递归左右子树
法二:左子树节点+右子树节点+1(根节点)

public void size(TreeNode root) {
    if(root == null) {
        return;
    }
    nodeSize++;
    size(root.left);
    size(root.right);
    }

    public int size(TreeNode root) {
        if(root == null) {
            return 0;
        }
    return size(root.left) + size(root.right)+1;
}
非递归形式:

利用层序遍历,遇到一个节点进行++

public int getCount(TreeNode root) {
    if(root == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    int count = 0;
    while(!queue.isEmpty()) {
        TreeNode cur = queue.poll();
        count++;
        if(cur.left != null) queue.offer(cur.left);
        if(cur.right != null) queue.offer(cur.right);
    }
    return count;
}

2.获取叶子节点个数:

递归

法一:遇到叶子节点++,递归左右子树
法二:左叶子节点+右叶子节点

public static int leafSize = 0;
void getLeafCount(TreeNode root) {
  if(root == null) return;
  if(root.left == null && root.right == null) leafSize++;
  getLeafCount(root.left);
  getLeafCount(root.right);
}

int getLeafCount(TreeNode root) {
  if(root == null) {
    return 0;
  }
  if(root.left == null && root.right == null) {
    return 1;
  }
  return getLeafCount(root.left) +
         getLeafCount(root.right);
}
非递归形式

队列实现

public int getCount(TreeNode root) {
    if(root == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    int count = 0;
    while(!queue.isEmpty()) {
        TreeNode cur = queue.poll();
        if(cur.left == null && cur.right == null) {count++};
        else {
        if(cur.left != null) queue.offer(cur.left);
        if(cur.right != null) queue.offer(cur.right);
        }
    }
    return count;
}

栈也是可以的。代码大致结构相同

3.获取第K层节点个数:

找到第k-1层的节点,分别找的左右节点,相加则是第k层节点

public int getK(TreeNode root,int k) {
  if(root == null) return 0;
  if(k == 1) return 1;  // 因为这是第一层
  return getK(root.left,k-1)+getK(root.right,k-1)
}

4.获取树的高度:

求出左子树和右子树的高度,它们的最大值+1(根节点)

public int getHeight(TreeNode root) {
  if(root == null) {
     return 0;
  }
  int leftHeight = getHeight(root.left);
  int rightHeight = getHeight(root.right);
  return leftHeight >rightHeight ? leftHeight+1 : rightHeight+1;
}

5.找到树中某个值

TreeNode find(TreeNode root, char val) {
  if(root == null) {
     return null;
  }

  if(root.val == val) {
    return root;
  }

  TreeNode leftT = find(root.left,val);
  if(leftT != null) {
     return leftT;
  }

  TreeNode rightT = find(root.right,val);
  if(rightT != null) {
     return rightT;
  }

  return null;
}

6.判断两棵树是否相同

这里传入的两个节点
1.首先判断结构是否相同,(是否同时为空或者同时不为空)
2.同时为空则为真,同时非空且数值不同则为假。
3.递归左右

public boolean isSameTree(TreeNode p, TreeNode q) {
    if(p != null && q == null || p == null && q != null) {
        return false;
    }
    if(p == null && q == null) {
        return true;
    }
    if(p.val != q.val) {
       return false;
    }
    return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}

7.翻转二叉树

就是普通的交换+递归左右子树,叶子节点的null交换与否无所谓

public TreeNode invertTree(TreeNode root) {
    if(root == null) return null;
    TreeNode tmp = root.left;
    root.left = root.right;
    root.right = tmp;
    invertTree(root.left);
    invertTree(root.right);
    return root;
}

下一篇发文:C语言实现搜索树和平衡树的增删查改

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值