经典题目选讲(2)
题目一:搜索二叉树vs完全二叉树vs平衡二叉树
- 判断一棵二叉树是否是搜索二叉树
- 判断一棵二叉树是否是完全二叉树
- 判断一棵二叉树是否是平衡二叉树
解答:
1)判断一棵二叉树是否是搜索二叉树,只要改写一个二叉树中序遍历,在遍历的过程中看节点值是否都是递增的即可。
2)判断一棵二叉书是否是完全二叉树,依据以下标准会使判断过程变得简单且易实现:
- 按层遍历二叉树,从每层的左边向右边依次遍历所有的节点
- 如果当前节点有右孩子,但没有左孩子,直接返回false。
- 如果当前节点并不是左右孩子全有,那之后的节点必须都为叶结点,否则返回false
- 遍历过程中如果不返回false,遍历结束后返回true。
3)平衡二叉树的性质为:要么是一棵空树,要么任何一个节点的左右子树高度差的绝对值不超过1。解法的整体过程为二叉树的后序遍历:
- 对任何一个节点的节点来说,先遍历节点的左子树,遍历过程中收集两个信息,节点的左子树是否为平衡二叉树,节点的左子树最深到哪一层记为LH。
- 如果发现节点的左子树不是平衡二叉树,无序进行任何后续过程,此时返回什么已不重要,因为已经发现整棵树不是平衡二叉树,退出遍历过程;
- 如果节点的左子树是平衡二叉树,再遍历节点的右子树,遍历过程中再收集两个信息,节点的右子树是否为平衡二叉树,节点的右子树最深到哪一层记为的的rH。
- 如果发现节点的右子树不是平衡二叉树,无须进行任何后续过程,返回什么也不重要,因为已经发现整棵树不是平衡二叉树,退出遍历过程;
- 如果节点的右子树也是平衡二叉树,就看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树中最远的距离。
根据如上分析,设计解法的过程如下:
- 整个过程为后序遍历,在二叉树的每棵子树上执行步骤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,具体过程如下:
- 如果head==null,说明是空树,直接返回0;
- 如果不是空树,就求树的盖度,求法是找到数的最左节点看能到哪一层,层数记为h
- 这一步是求解的主要逻辑,也是一个递归过程记为bs(node,l,h),node表示当前节点,l表示node所在的层数,h表示整棵树的层数是始终不变的。bs(node,l,h)的返回值表示以node为头的完全二叉树的节点数是多少。初始时node为头节点head,l为1,因为head在第一层,一共有h层始终不变。
- 找到node右子树的最左节点,发现它能到达最后一层,说明node的整课左子树都是满二叉树,并且层数为h-1层,一棵层数为h-1的满二叉树,其节点数为
个。如果加上node节点自己,那么节点数为
个。如果再知道node右子树的节点数,那么以node为头的完全二叉树上到底有多少个节点就求出来了。那么node右子树的节点数到底是多少呢?就是bs(node.right,l+1,h)的结果,递归去求即可。最后整体返回
。
- 找到node右子树的最左节点,发现它没能到达最后一层,说明node的整棵右子树都是满二叉树,并且层数为h-l-1层。一棵层数为h-l-1的满二叉树,其节点数为
个。如果在加上node节点自己,那么节点数为
个。此时如果再知道node左子树的节点数,那么以node为头的完全二叉树上到底有多少个节点就求出来了。node左子树的节点数到底有多少呢?就是bs(node.left,l+1,h)的结果,递归去求。最后整体返回
。
- 找到node右子树的最左节点,发现它能到达最后一层,说明node的整课左子树都是满二叉树,并且层数为h-1层,一棵层数为h-1的满二叉树,其节点数为
代码如下:
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));
}
}