【算法】算法进阶训练(经典题目选讲(2))

本文精选数据结构与算法题目,深入解析搜索二叉树、完全二叉树、平衡二叉树的判断方法,探讨二叉树节点间最大距离问题,最大活跃值问题,子数组最大异或和,以及完全二叉树节点计数的高效算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

经典题目选讲(2)


题目一:搜索二叉树vs完全二叉树vs平衡二叉树

  1. 判断一棵二叉树是否是搜索二叉树
  2. 判断一棵二叉树是否是完全二叉树
  3. 判断一棵二叉树是否是平衡二叉树

解答:

1)判断一棵二叉树是否是搜索二叉树,只要改写一个二叉树中序遍历,在遍历的过程中看节点值是否都是递增的即可。

2)判断一棵二叉书是否是完全二叉树,依据以下标准会使判断过程变得简单且易实现:

  1. 按层遍历二叉树,从每层的左边向右边依次遍历所有的节点
  2. 如果当前节点有右孩子,但没有左孩子,直接返回false。
  3. 如果当前节点并不是左右孩子全有,那之后的节点必须都为叶结点,否则返回false
  4. 遍历过程中如果不返回false,遍历结束后返回true。

3)平衡二叉树的性质为:要么是一棵空树,要么任何一个节点的左右子树高度差的绝对值不超过1。解法的整体过程为二叉树的后序遍历:

  1. 对任何一个节点的节点来说,先遍历节点的左子树,遍历过程中收集两个信息,节点的左子树是否为平衡二叉树,节点的左子树最深到哪一层记为LH。
  2. 如果发现节点的左子树不是平衡二叉树,无序进行任何后续过程,此时返回什么已不重要,因为已经发现整棵树不是平衡二叉树,退出遍历过程;
  3. 如果节点的左子树是平衡二叉树,再遍历节点的右子树,遍历过程中再收集两个信息,节点的右子树是否为平衡二叉树,节点的右子树最深到哪一层记为的的rH。
  4. 如果发现节点的右子树不是平衡二叉树,无须进行任何后续过程,返回什么也不重要,因为已经发现整棵树不是平衡二叉树,退出遍历过程;
  5. 如果节点的右子树也是平衡二叉树,就看LH和的的rH差的绝对值是否大于1,如果大于1,说明已经发现整棵树不是平衡二叉树,果不大于1,则返回LH和的的rH较大的一个。

代码如下:

package NowCoder2.Class05;

import java.util.LinkedList;
import java.util.Queue;

public class IsBSTAndCBT {
    public class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }

    /*
    判断一棵二叉树是否是搜索二叉树
     */
    public boolean isBST(Node head) {
        if (head == null) {
            return true;
        }
        boolean res = true;
        Node pre = null;
        Node cur1 = head;
        Node cur2 = null;
        while (cur1 != null) {
            cur2 = cur1.left;
            if (cur2 != null) {
                while (cur2.right != null && cur2.right != cur1) {
                    cur2 = cur2.right;
                }
                if (cur2.right == null) {
                    cur2.right = cur1;
                    cur1 = cur1.left;
                    continue;
                } else {
                    cur2.right = null;
                }
            }
            if (pre != null && pre.value > cur1.value) {
                res = false;
            }
            pre = cur1;
            cur1 = cur1.right;
        }
        return res;
    }

    /*
    判断一棵树是否为完全二叉树
     */
    public boolean isCBT(Node head) {
        if (head == null) {
            return true;
        }
        Queue<Node> queue = new LinkedList<>();
        boolean leaf = false;
        Node l = null;
        Node r = null;
        queue.offer(head);
        while (!queue.isEmpty()) {
            head = queue.poll();
            l = head.left;
            r = head.right;
            if ((leaf && (l != null || r != null)) || (l == null && r != null)) {
                return false;
            }
            if (l != null) {
                queue.offer(l);
            }
            if (r != null) {
                queue.offer(r);
            } else {
                leaf = true;
            }
        }
        return true;
    }

    /*
    判断二叉树是否为平衡二叉树
     */
    public boolean isBalance(Node head) {
        boolean[] res = new boolean[1];
        res[0] = true;
        getHeight(head, 1, res);
        return res[0];
    }

    public int getHeight(Node head, int level, boolean[] res) {
        if (head == null) {
            return level;
        }
        int lH = getHeight(head.left, level + 1, res);
        if (!res[0]) {
            return level;
        }
        int rH = getHeight(head.right, level + 1, res);
        if (!res[0]) {
            return level;
        }
        if (Math.abs(lH - rH) > 1) {
            res[0] = false;
        }
        return Math.max(lH, rH);
    }
}

题目二:二叉树节点间的最大距离问题

题目描述:二叉树中,一个节点可以往上走和往下走,那么从节点A总能走到节点B。节点A走到节点B的距离为:A走到B最短路径上的节点个数。求一棵二叉树上的最远距离。

要求:如果二叉树的节点数为N,时间复杂度要求为O(n)。

解法:一个以head为头的树上,最大距离只可能来自以下三种情况:

  • head的左子树上的最大距离
  • head的右子树上的最大距离
  • head左子树上离head.left最远的距离+1(head)+head右子树上离head.right最远的距离

三个值中最大的那个就是整棵head树中最远的距离。

根据如上分析,设计解法的过程如下:

  1. 整个过程为后序遍历,在二叉树的每棵子树上执行步骤2
  2. 假设子树头为head,处理head左子树,得到两个信息,左子树上的最大距离记为lMax,左子树上距离head左孩子的最远距离记为maxfromLeft。同理,处理head右子树得到右子树上的最大距离记为rMax和距离head右孩子的最远距离记为maxFromRight。那么maxfromLeft+1+maxFromRight就是跨head节点情况下的最大距离,再与lMax和rMax比较,把三者中的最值作为head树上的最大距离返回,maxFromLeft+1就是head左子树上离head最远的点到head的距离,maxFromRight+1就是head右子树上离head最远的点到head的距离,选两者中最大的一个作为head树上距离head最远的距离返回。如何返回两个值?一个正常返回,另一个用全局变量表示。

代码如下,其中record[0]就表示另一个返回值。

package NowCoder2.Class05;

public class MaxDistanceInTree {
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }

    public int maxDistance(Node head) {
        int[] record = new int[1];
        return posOrder(head, record);
    }

    public int posOrder(Node head, int[] record) {
        if (head == null) {
            record[0] = 0;
            return 0;
        }
        int lMax = posOrder(head.left, record);
        int maxFromLeft = record[0];
        int rMax = posOrder(head.right, record);
        int maxFromRight = record[0];
        int curNodeMax = maxFromLeft + maxFromRight + 1;
        record[0] = Math.max(maxFromLeft, maxFromRight) + 1;
        return Math.max(Math.max(lMax, rMax), curNodeMax);
    }
}

题目三:最大活跃值

题目描述:一个公司的上下级关系是一棵多叉树,这个公司要举办晚会,你作为组织者已经摸清了大家的心理:一个员工的直接上级如果到场,这个员工肯定不会来。每个员工都有一个活跃度的值,决定谁来你会给这个员工发邀请函,怎么让晚会的气氛最活跃?返回最大的活跃值。

例如:给定一个矩阵来表述这种关系

matrix = {

1,6

1,5

1,4

}

这个矩阵的含义是:

matrix[0] = {1,6},表示0这个员工的直接上级为1,0这个员工自己的活跃度为6

matrix[1] = {1,5},表示1这个员工的直接上级为1(他自己是这个公司的最大boss),1这个员工自己的活跃度为5

matrix[2] = {1,4},表示2这个员工的直接上级为1,2这个员工自己的活跃度为4

为了让晚会活跃度最大,应该让1不来,0和2来。最后返回活跃度为10.

思路:本题属于树形动态规划,设当前节点为x,有以下两种可能性:

  • 可能性1:x来,则结果就是x所有子节点(x1、x2、x3)不来的活跃度+x的活跃度
  • 可能性2:x不来,则结果就是Math.max(x的任一子节点x1来的活跃度,x的任一子节点x1不来的活跃度)+Math.max(x的任一子节点x2来的活跃度,x的任一子节点x2不来的活跃度)+Math.max(x的任一子节点x3来的活跃度,x的任一子节点x3不来的活跃度)

也就是说,任何一个点的取舍可以看作一种决策,那么状态就是在某个点取的时候或者不取的时候,以它为根的子树能有的最大活跃总值。分别可以用f[i,1]和f[i,0]表示第i个人来和不来。
当i来的时候,dp[i][1] += dp[j][0];//j为i的下属
当i不来的时候,dp[i][0] +=max(dp[j][1],dp[j][0]);//j为i的下属
代码如下:

package NowCoder2.Class05;

/**
 *最大活跃值
 */
public class MaxHappy {
    public static void main(String[] args) {
        int[][] matrix = {{1, 6}, {1, 5}, {1, 4}};
        System.out.println(maxHappy(matrix));
    }

    public static int maxHappy(int[][] matrix) {
        int[][] dp = new int[matrix.length][2];
        boolean[] visited = new boolean[matrix.length];
        int root = 0;
        for (int i = 0; i < matrix.length; i++) {
            if (i == matrix[i][0]) {
                root = i;
            }
        }
        process(matrix, dp, visited, root);
        return Math.max(dp[root][0], dp[root][1]);
    }

    public static void process(int[][] matrix, int[][] dp, boolean[] visited, int root) {
        visited[root] = true;
        dp[root][1] = matrix[root][1];
        for (int i = 0; i < matrix.length; i++) {
            if (matrix[i][0] == root && !visited[i]) {
                process(matrix, dp, visited, i);
                dp[root][1] += dp[i][0];
                dp[root][0] += Math.max(dp[i][1], dp[i][0]);
            }
        }
    }
}

题目四:子数组的最大异或和

题目描述:给定一个数组,求子数组的最大异或和。

一个数组的异或和为:数组中所有的数异或起来的结果。

解答:本题使用前缀树,时间复杂度为O(N).具体过程如下:

假设将0~0,0~1,0~2,...,0~i-1的异或结果全部装在前缀树中,那么以i结尾的最大异或和就是0到某一位置x的异或结果和i异或结果最大。例如,假设x是3,0~3的异或结果和i进行异或得到的结果最大,那么就说明4-i的异或结果是最大的。

但是如何知道x到底是多少?换句话说,0~x中哪个值和i进行异或得到的结果最大。其实这个也比较好像,假设i是0100(最高位0是符号位),只需要沿着前缀树找到0011,异或出来的结果就是0111,一定是最大的,如果不能刚好找到合适的,那就有什么选什么,只要保证从最高位开始往下每次的决策是最优的就行。

有一种特殊情况,假设i还是0100,但是此时前缀树中最高位只有1,没有0,那么最高位得出的异或结果永远是负数,后面的位应该如何选?其实也是按照最优决策去选,假设异或结果是1111,那么转换为十进制就是-1,绝对没有比这个还大的负数了。

代码如下;

package NowCoder2.Class05;

public class Max_EOR {
    public static class Node {
        public Node[] nexts = new Node[2]; //0,1
    }

    public static class NumTrie {
        public Node head = new Node();

        public void add(int num) {
            Node cur = head;
            for (int move = 31; move >= 0; move--) {
                int path = ((num >> move) & 1);//每一位上的数字(0,1)
                cur.nexts[path] = cur.nexts[path] == null ? new Node() : cur.nexts[path];
                cur = cur.nexts[path];
            }
        }

        public int maxXor(int num) { //num=0~i的异或结果
            Node cur = head;
            int res = 0;
            for (int move = 31; move >= 0; move--) {
                int path = (num >> move) & 1;//提取每一位
                int best = move == 31 ? path : (path ^ 1);//最高位期望一样,非最高位期望相反
                best = cur.nexts[best] != null ? best : (best ^ 1);//实际要选的路经(如果没有期待选的路经)
                res |= (path ^ best) << move;//设置答案的每一位
                cur = cur.nexts[best];//继续往下走
            }
            return res;
        }

    }

    public static int maxXorSubarray(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int max = Integer.MIN_VALUE;
        int eor = 0;
        NumTrie numTrie = new NumTrie();
        numTrie.add(0);
        for (int i = 0; i < arr.length; i++) {
            eor ^= arr[i];
            max = Math.max(max, numTrie.maxXor(eor));
            numTrie.add(eor);
        }
        return max;
    }

    // for test
    public static int comparator(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++) {
            int eor = 0;
            for (int j = i; j < arr.length; j++) {
                eor ^= arr[j];
                max = Math.max(max, eor);
            }
        }
        return max;
    }

    // for test
    public static int[] generateRandomArray(int maxSize, int maxValue) {
        int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
        }
        return arr;
    }

    // for test
    public static void printArray(int[] arr) {
        if (arr == null) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

    // for test
    public static void main(String[] args) {
        int testTime = 500000;
        int maxSize = 30;
        int maxValue = 50;
        boolean succeed = true;
        for (int i = 0; i < testTime; i++) {
            int[] arr = generateRandomArray(maxSize, maxValue);
            int res = maxXorSubarray(arr);
            int comp = comparator(arr);
            if (res != comp) {
                succeed = false;
                printArray(arr);
                System.out.println(res);
                System.out.println(comp);
                break;
            }
        }
        System.out.println(succeed ? "Nice!" : "Fucking fucked!");
    }
}

题目五:统计一棵完全二叉树的节点个数

题目描述:求一棵完全二叉树的节点个数,要求时间复杂度低于O(N)。

要求:如果完全二叉树的节点数为N,请实现时间复杂度低于O(N)的解法

解法:如果完全二叉树的层数为h,具体过程如下:

  1. 如果head==null,说明是空树,直接返回0;
  2. 如果不是空树,就求树的盖度,求法是找到数的最左节点看能到哪一层,层数记为h
  3. 这一步是求解的主要逻辑,也是一个递归过程记为bs(node,l,h),node表示当前节点,l表示node所在的层数,h表示整棵树的层数是始终不变的。bs(node,l,h)的返回值表示以node为头的完全二叉树的节点数是多少。初始时node为头节点head,l为1,因为head在第一层,一共有h层始终不变。
    1. 找到node右子树的最左节点,发现它能到达最后一层,说明node的整课左子树都是满二叉树,并且层数为h-1层,一棵层数为h-1的满二叉树,其节点数为2^{h-1}-1个。如果加上node节点自己,那么节点数为2^{h-1}个。如果再知道node右子树的节点数,那么以node为头的完全二叉树上到底有多少个节点就求出来了。那么node右子树的节点数到底是多少呢?就是bs(node.right,l+1,h)的结果,递归去求即可。最后整体返回2^{h-1}+bs(node.right,l+1,h)
    2. 找到node右子树的最左节点,发现它没能到达最后一层,说明node的整棵右子树都是满二叉树,并且层数为h-l-1层。一棵层数为h-l-1的满二叉树,其节点数为2^{h-l-1}-1个。如果在加上node节点自己,那么节点数为2^{h-l-1}个。此时如果再知道node左子树的节点数,那么以node为头的完全二叉树上到底有多少个节点就求出来了。node左子树的节点数到底有多少呢?就是bs(node.left,l+1,h)的结果,递归去求。最后整体返回2^{h-l-1}+bs(node.left,l+1,h)

代码如下:

package NowCoder2.Class05;

public class CompleteTreeNodeNumber {
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }

    public static int nodeNum(Node head) {
        if (head == null) {
            return 0;
        }
        return bs(head, 1, mostLeftLevel(head, 1));
    }

    public static int bs(Node node, int l, int h) {
        if (l == h) {
            return 1;
        }
        if (mostLeftLevel(node.right, l + 1) == h) {
            return (1 << (h - l)) + bs(node.right, l + 1, h);
        } else {
            return (1 << (h - l - 1)) + bs(node.left, l + 1, h);
        }
    }

    public static int mostLeftLevel(Node node, int level) {
        while (node != null) {
            level++;
            node = node.left;
        }
        return level - 1;
    }

    public static void main(String[] args) {
        Node head = new Node(1);
        head.left = new Node(2);
        head.right = new Node(3);
        head.left.left = new Node(4);
        head.left.right = new Node(5);
        head.right.left = new Node(6);
        System.out.println(nodeNum(head));

    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

镰刀韭菜

看在我不断努力的份上,支持我吧

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

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

打赏作者

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

抵扣说明:

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

余额充值