动态规划:
1.斐波那契数列问题
2.青蛙跳台阶问题
3.连续数组的最大和
连续数组的最大和:
Q:输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。
示例:
输入nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组[4,-1,2,1]的和最大,为6.
解题思路:
使用动态规划思路进行求解,动态规划问题一般都可递推求解,为了在递推时满足题目的连续子数组的要求,设动态规划列表为dp,dp[i]代表以元素nums[i]为结尾的连续子数组的最大和;此外,若dp[i-1] <=0,说明dp[i-1]对dp[i]产生负贡献,即dp[i-1]+nums[i]还不如nums[i]本身大,因此:
- 当dp[i - 1] > 0时,执行dp[i] = dp[i - 1] + nums[i];
- 当dp[i - 1] <= 0时,执行dp[i] = dp[i - 1] + nums[i];
初始状态时,dp[0] = nums[0] ,即以nums[0]结尾的连续子数组最大和为nums[0],最终返回dp列表中的最大值,代表全局最大值。
代码实现:
public maxSubArray(int[] nums){
int res = nums[0];
for(int i = 1;i < nums.length;i++){
nums[i] += Math.max(nums[i - 1],0);
res = Math.max(res,nums[i]);
}
return res;
}
不用加减乘除做加法
Q:写一个函数,求两个整数之和,要求在函数体内不得使用“+”、“-”、“*”、“/”四则运算符号。
示例:
输入:a = 1,b = 1
输出:2
解题思路:
使用位运算,用位运算实现加法。
设两数字的二进制形式a,b,其求和s = a + b ,a(i)代表a的二进制第i位,则分为以下四种情况:
a(i) | b(i) | 无进位和n(i) | 进位c(i+1) |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 1 | 1 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 0 | 1 |
可知,无进位和与异或运算规律相同,进位和与运算规律相同(并需左移一位),可将s = a + b转化为s = n + c;循环求n 和c,直至进位c = 0;此时s = n,返回n即可。
代码实现如下:
public int add(int a,int b){
while(b != 0){
int c = (a & b) << 1;
a ^= b;
b = c;
}
return a;
}
翻转单词顺序(另一种推荐的做法)
Q:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简答起见,标点符号和普通字母一样处理。例如输入字符串“I am a student.”,则输出"student. a am I"。
示例:
输入:" hello world! "
输出:“world! hello”
解释:输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
解题思路:使用双指针,
- 倒序遍历字符串s,记录单词左右索引边界i,j;
- 每确定一个单词的边界,则将其添加至单词列表res;
- 最终,将单词列表拼接为字符串,并返回即可。
代码实现如下:
public String reverseWords(String s){
s = s.trim();
int j = s.length() - 1,i = j;
StringBuilder res = new StringBuilder();
while(i >= 0){
while(i >= 0 && s.charAt(i) != ' ') i--; //搜索首个空格
res.append(s.substring(i + 1,j + 1) + " "); //添加单词
while(i >= 0 && s.charAt(i) == ' ') i--; // 跳过单词间空格
j = i;
}
return res.toString().trim(); //转化为字符串并返回
}
第一个出现的字符
Q:在字符串s中找出第一个只出现一次的字符。如果没有,返回一个单空格。s只包含小写字母。
题目本身不难,但重点是介绍有序哈希表的使用,在哈希表的基础上,有序哈希表中的键值对是按照插入顺序排序的,基于此,可通过遍历有序哈希表,实现搜索首个“数量为1的字符”。
哈希表是去重的,即哈希表中键值对数量<=字符串s的长度。因此,相比于方法一,方法二减少了第二轮遍历的循环次数。当字符串很长(重复字符很多)时,方法二则效率更高。
代码实现如下:
public char firstUniqChar(String s){
Map<Character,Boolean> dic = new LinkedHashMap<>();
char[] sc = s.toCharArray();
for(char c:sc){
dic.put(c,!dic.containsKey(c));
}
for(Map.Entry<Character,Boolean> d:dic.entrySet()){
if(d.getValue()) return d.getKey();
}
return ' ';
}
二进制中1的个数
Q:请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如,把9表示成二进制是1001,有2位是1。因此,如果输入9,则该函数输出2。
示例1:
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串
00000000000000000000000000001011中,共有三位为‘1’。
解题思路:还是基于位运算
一、逐位判断
根据与运算定义,设二进制数字n,则有:
- 若n&1=0,则n二进制最右一位为0;
- 若n&1=1,则n二进制最右一位为1。
根据以上特点,考虑以下循环判断:
- 判断n最右一位是否为1,根据结果计数;
- 将n右移一位(n为无符号数,因此使用无符号右移操作)
二、巧用n&(n-1)
- (n-1):二进制数字n最右边的1变成0,此1右边的0都变成1;
- n&(n-1):二进制数字n最右边的1变成0,其余不变。
代码实现:
第一种
public class hammingWeight(int n){
int res = 0;
while(n != 0){
res += n&1;
n >>>= 1;
}
return res;
}
第二种
public class hammingWeight(int n){
int res = 0;
while(n != 0){
res++;
n &= n-1;
}
return res;
}
递归反转链表
递归反转整个链表
//链表节点定义
public class ListNode{
int val;
ListNode next;
ListNode(int x){
val = x;
}
}
public ListNode reverse(ListNode head){
if(head.next == null) return head;
ListNode last = reverse(head.next);
head.next.next = head;
head.next = null;
return last;
}
注:递归函数要有base case,即:
if(head.next == null) return head;意思是如果链表只有一个节点的时候反转也是它自己,直接返回即可。
递归反转链表的前n个节点
//链表节点定义
public class ListNode{
int val;
ListNode next;
ListNode(int x){
val = x;
}
}
//定义后驱节点指针
ListNode successor = null;
public ListNode reverseN(ListNode head,int n){
if(n == 1){
successor = head.next;
return head;
}
ListNode last = reverseN(head.next,n-1);
head.next.next = head;
head.next = successor;
return last;
}
注:
1.base case 为n==1,反转一个元素,就是反转它本身,同时要记录后驱节点;
2.前一个反转整个链表是把head.next设置为null,因为整个链表反转之后原来的第一个节点成为了最后一个节点;而在这部分中,第一个节点反转之后不一定会是最后一个节点,因此需要为其设置后驱节点successor,并设置head.next = successor。
递归反转一部分链表
给定一个索引区间[m,n](索引从1开始),仅仅反转区间中的链表元素。
//定义链表节点
public class ListNode{
int val;
ListNode next;
ListNode(int x){
val = x;
}
}
public ListNode reverseBetween(ListNode head,int m,int n){
if(m == 1){
return reverseN(head,n);
}
head.next = reverseBetween(head.next,m-1,n-1);
return head;
}
注:此处的base case成为了m == 1,当m== 1时就是第二种递归反转链表前n个节点的例子。
k个一组递归反转链表
根据递归的性质,可以得到大致的算法流程:
- 先反转以head开头的k个元素;
- 将第k+1个元素作为head递归调用reverseKGroup函数;
- 将上述两个过程的结果连接起来。
先按迭代的方式反转整个链表:
public ListNode reverse(ListNode head){
ListNode pre,last,r;
pre = null;last = head;r = head;
while(last != null){
r = last.next;
last.next = pre;
pre = last;
last = r;
}
return pre;
}
再按迭代方式反转区间[a,b)间的链表:
(其实只要把head换成a,null换成b即可)
public ListNode reverseBetween(ListNode a,ListNode b){
ListNode pre,last,r;
pre = null;last = a;r = a;
while(last != b){
r = last.next;
last.next = pre;
pre = last;
last = r;
}
return pre;
}
之后可以借助按迭代方式反转区间[a,b)间链表来实现k个一组的链表反转:
public ListNode reverseK(ListNode head,int k){
if(head == null) return null;
ListNode a,b;
a = b = head;
for(int i = 0;i < k;i++){
if(b == null) return head;
b = b.next;
}
ListNode newHead = reverseBetween(a,b);
a.next = reverseK(b,k);
return newHead;
}
判断链表是否为回文链表
这个问题的难点在于它不向字符串那样可以使用双指针来遍历,最传统的办法就是将原始链表反转并存入到一条新链表中,之后和原来的链表一一进行元素对比。
链表兼具递归结构,如树结构实质上就是链表的衍生。所以,链表也可以有前序遍历和后序遍历:
如前序遍历:
void traverse(ListNode head){
if(head == null) return;
print(head.val);
traverse(head.next);
}
后序遍历:
void traverse(ListNode head){
if(head == null) return;
traverse(head.next);
print(head.val);
}
方法一:构造双指针+后序遍历
ListNode left;
boolean isPalindrome(ListNode head){
left = head;
return traverseLR(head);
}
boolean traverseLR(ListNode right){
if(right == null) return true;
boolean res = traverseLR(right.next);
res = res && (right.val == left.val);
left = left.next;
return res;
}
虽然采用双指针和后续遍历可以实现,但是仍有较高的空间复杂度,可以进行进一步地优化,首先先来复习一下快慢指针。
这里利用快慢指针可以找到链表的中点:
ListNode slow,fast;
slow = fase = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
//如果fast不为空,说明链表长度为奇数,slow还需要再走一步
if(fast != null){
slow = slow.next;
}
接下来,从slow位置处开始反转后面的链表,即可进行回文字符串的比较:
ListNode left = head;
ListNode right = traverse(slow);
while(right != null){
if(left.val != right.val){
return false;
}
left = left.next;
right = right.next;
}
return true;
当traverse函数用迭代方式实现时,总共的算法时间复杂度为O(n),空间复杂度为O(1)。
注:寻找回文串是从中间向两端扩展,判断回文串是从两端向中间收缩。针对于单链表,涉及到回文的判断问题,由于回文的特殊性,可以不完全反转链表,而是仅仅反转部分链表,不过需要注意链表长度的奇偶。
二叉树系列
翻转二叉树;
将二叉树展开为链表;
填充二叉树节点的右侧指针;
注:排序算法中快速排序其实就是二叉树的前序遍历,而归并排序其实就是二叉树的后序遍历。
计算二叉树共有几个节点
int count(TreeNode root){
if(root == null) return 0;
return 1 + count(root.left) + count(root.right);
}
翻转二叉树
输入一个二叉树根节点root,让你把整棵树镜像反转,比如[4->2->7->1->3->6->9],翻转为[4->7->2->9->6->3->1]
TreeNode reverse(TreeNode root){
if(root == null) return null;
TreeNode temp;
temp = root.lef;
root.left = root.right;
root.right = temp;
//让左右子节点继续翻转它们的子节点
reverse(root.left);
reverse(root.right);
return root;
}
关键:关键思路在于我们发现翻转整棵树就是交换每个节点的左右子节点,于是把交换左右子节点的代码放在了前序遍历的位置。
填充二叉树节点的右侧指针
给定一个完全二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点,二叉树定义如下:
struct Node{
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个next指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将next指针设置为NULL。初始状态下,所有next指针都被设置为NULL。
//主函数
Node connect(Node root){
if(root == null) return null;
connectTwoNode(root.left,root.right);
return root;
}
//定义:输入两个节点,将它俩连接起来
void connectTwoNode(Node node1,Node node2){
if(node1 == null || node2 == null){
return;
}
node1.next = node2;
connectTwoNode(node1.left,node1.right);
connectTwoNode(node2.left,node2.right);
connectTwoNode(node1.right,node2.left);
}
当考虑一个节点的作用实现递归不可以时,可以增加函数参数,把“将每一层二叉树节点连接起来”可以细化成“将每两个相邻节点都连接起来”。
将二叉树展开为链表
思路:
- 将root的左子树和右子树拉平;
- 将root的右子树接到左子树下方,然后将整个左子树作为右子树。
void flatten(TreeNode root){
if(root == null) return null;
flatten(root.left);
flatten(root.right);
TreeNode left = root.left;
TreeNode right = root.right;
root.left = null;
root.right = left;
TreeNode p = root;
while(p.right != null){
p = p.right;
}
p.right = right;
}
关键:二叉树的算法题都是基于递归框架的,要先搞清楚root节点它自己要做什么,然后根据题目选择使用前序、中序和后序的递归框架。
最大二叉树
Q:给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下:
- 二叉树的根是数组中的最大元素;
- 左子树是通过数组中最大值左边部分构造出的最大二叉树;
- 右子树是通过数组中最大值右边部分构造出的最大二叉树。
如对于:[3,2,1,6,0,5]:
TreeNode constructMaximumBinaryTree(int[] nums){
return build(nums,0,nums.length-1);
}
TreeNode build(int[] nums,int left,int right){
if(left > right) return null;
//找到数组中的最大值和对应的索引
int index = -1;
int maxVal = Integer.MIN_VALUE;
for(int i = left,i <= right;i++){
if(maxVal < nums[i]){
idex = i;
maxVal = nums[i];
}
}
TreeNode root = new TreeNode(maxVal);
root.left = build(nums,left,index - 1);
root.right = build(nums,index + 1,right);
return root;
}
根据前序/中序遍历结果构建二叉树
preorder:[3,9,20,15,7]
inorder:[9,3,15,20,7]
TreeNode buildTreePreIn(int[] preorder,int[] inorder){
return build(preorder,0,preorder.length-1,inorder,0,inorder.length-1);
}
TreeNode build(int[] preorder,int prestart,int preend,int[] inorder,int instart,int inend){
if(prestart > preend) return null;
TreeNode root = new TreeNode(preorder[prestart]);
int index = -1;
for(int i = instart;i <= inend;i++){
if(inorder[i] == root.val){
index = i;
break;
}
}
int leftsize = index - instart; //左子树有多少个节点
root.left = build(preorder,prestart+1,prestart+leftsize,inorder,instart,index - 1);
root.right = build(preorder,prestart+leftsize+1,preend,inorder,index + 1,inend);
return root;
}
根据中序/后序遍历结果构建二叉树
inorder:[9,3,15,20,7]
postorder:[9,15,7,20,3]
TreeNode buildTreeInPost(int[] inorder,int[] postorder){
return build(inorder,0,inorder.length-1,postorder,0,postorder.length-1);
}
TreeNode build(int[] inorder,int instart,int inend,int[] postorder,int poststart,int postend){
if (poststart > postend) return null;
TreeNode root = new TreeNode(postorder[postend]);
int index = -1;
for(int i = instart;i <= inend;i++){
if(inorder[i] == root.val){
index = i;
break;
}
}
int leftsize = index - instart;
root.left = build(inorder,instart,index-1,postorder,poststart,poststart+leftsize-1);
root.right = build(inorder,index+1,inend,postorder,poststart+leftsize,postend-1);
return root;
}
总结:做二叉树的问题,关键是把题目的要求细化,搞清楚根节点应该做什么,然后剩下的事情抛给前/中/后序的遍历框架就可以了。
针对于具体的问题,在具备二叉树框架思维的情况下,怎么判断应该用前序还是中序还是后序遍历的框架?
关键:思考一个二叉树节点需要做什么,到底用什么遍历顺序就清楚了。通过“寻找重复的子树:来说明这一问题。
首先引入”序列化二叉树”:
String traverse(TreeNode root){
if(root == null){
return "#"
}
String left = traverse(root.left)
String right = traverse(root.right)
String subTree = left+","+right+","+root.val
return subTree
}
用#表示空指针,并且用字符“,”分隔每个二叉树节点值,这就是将二叉树序列化。因此,上述函数即可用来描述以该节点为根的二叉树。那对于“寻找重复的子树”这个问题,就可以借助上述函数以及HashMap来完成,其中HashMap用来记录重复出现的子树的根节点以及每棵子树的出现次数:
HashMap<String,Integer> memo = new HashMap<>();
LinkedList<TreeNode> res = new LinkedList<>();
List<TreeNode> findDuplicateSubtrees(TreeNode root){
traverse(root);
return res;
}
String traverse(TreeNode root){
if(root == null) return "#";
String left = traverse(root.left);
String right = traverse(root.right);
String subTree = left+","+right+","+root.val;
int freq = memo.getOrDefault(subTree,0) //getOrDefault是在Map中按subTree进行查询,如果map中存在则返回的是map中key(subTree)对应的value,否则返回默认值0。
if(freq == 1){
res.add(root);
}
memo.put(subTree,freq+1);
return subTree;
}
二叉搜索树系列
二叉搜索树中第K小的元素
Q:给定一个二叉搜索树,编写一个函数kthSmallest来查找其中第k个最小的元素。
如:root=[5,3,6,2,4,null,null,1],k=3
int res = 0 ;
int count = 0;
int kthSmallest(TreeNode root,int k){
traverse(root,k);
return res;
}
void traverse(TreeNode root,int k){
if (root == null) return;
traverse(root.left);
count += 1;
if(count == k){
res = root.val;
return ;
}
traverse(root.right,k);
}
利用二叉搜索树的中序遍历是升序的特性求解即可。
把二叉搜索树转换为累加树
Q:给出二叉搜索树的根节点,该树的节点值各不相同,请你将其转换为累加树,使每个节点node的新值等于原树中大于或等于node.val之和。
TreeNode convertBST(TreeNode root){
traverse(root);
return root;
}
int sum = 0;
void traverse(TreeNode root){
if(root == null) return;
traverse(root.right);
sum += root.val;
root.val = sum;
traverse(root.left);
}
这道题的核心还是BST的中序遍历特性,只不过修改了递归顺序,降序遍历BST的元素值,从而契合题目累加树的要求。针对于二叉搜索树来说,要么是利用BST左小右大的特性提升算法效率,要么利用中序遍历的特性满足题目的要求。
和BST相关的一些基础操作
判断BST的合法性(难)
boolean isValidBST(TreeNode root){
return isValidBST(root,null,null);
}
boolean isValidBST(TreeNode root,TreeNode min,TreeNode max){
if(root == null) return true;
if(min != null && root.val <= min.val){
return false;
}
if(max != null && root.val >= max.val){
return false;
}
return isValidBST(root.left,min,root) && isValidBST(root.right,root,max);
}
在BST中搜索一个数
boolean isInBST(TreeNode root,int target){
if(root == null) return false;
if(root.val == target){
return true;
}
if(root.val < target){
return isInBST(root.right,target);
}
if(root.val > target){
return isInBST(root.left,target);
}
}
在BST中插入一个数
TreeNode insertIntoBST(TreeNode root,int val){
if(root == null){
return new TreeNode(val);
}
if(root.val < val){
root.right = insertIntoBST(root.right,val);
}
if(root.val > val){
root.left = insertIntoBST(root.left,val);
}
return root;
}
在BST中删除一个数
解析:
- 如果待删除节点恰好是末端节点,它的两个子节点都为空,那么可以毫无顾虑地直接将它删除;
- 如果待删除节点只有一个非空子节点,那么要让自己的孩子接替自己的位置;
- 如果待删除节点有两个子节点,为了不破坏BST的性质,它必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。
TreeNode deleteFromBST(TreeNode root,int val){
if(root == null){
return null;
}
if(root.val == key){
if(root.left == null) return root.right;
if(root.right == null) return root.left;
TreeNode min = getMin(root.right);
root.val = min.val;
}else if(root.val > val){
root.left = deleteFromBST(root.left,val);
}else if(root.val < val){
root.right = deleteFromBST(root.right,val);
}
return root;
}
TreeNode getMin(TreeNode right){
while(right.left != null){
right = right.left;
}
return right;
}
总结:
- 如果当前节点会对下面的子节点有整体影响,可以通过辅助函数增长参数列表,借助参数传递信息;
- BST代码框架:
void BST(TreeNode root,int target){
if(root.val == target){
//........
}
if(root.val < target){
BST(root.right,target);
}
if(root.val > target){
BST(root.left,target):
}
}
二叉树的序列化和反序列化
public class Codec{
//把一棵二叉树序列化成字符串
public String serialize(TreeNode root){}
//把字符串反序列化成二叉树
public TreeNode deserialize(String data){}
}
前序遍历的方式:
StringBuilder sb = new StringBuilder();
String serialize(TreeNode root){
serialize(root,sb);
return sb.toString();
}
void serialize(TreeNode root,StringBuilder sb){
if(root == null){
sb.append("#").append(",");
}
sb.append(root.val).append(",");
serialize(root.left,sb);
serialize(root.right,sb);
}
反序列化也应根据前序遍历来编写:
TreeNode deserialize(String data){
LinkedList<String> nodes = new LinkedList<>();
for(String s:data.split(","){
nodes.addLast(s):
}
return deserialize(nodes);
}
TreeNode deserialize(LinkedList<String> nodes){
if(nodes.isEmpty()) return null;
String first = nodes.removeFirst();
if(first.equals("#")) return null;
TreeNode root = new TreeNode(Integer.parseInt(first));
root.left = deserialize(nodes);
root.right = deserialize(nodes);
return root;
}
后序遍历的方式:
StringBuilder sb = new StringBuilder();
TreeNode serialize(TreeNode root,StringBuilder sb){
serialize(root,sb);
return sb.toString();
}
void serialize(TreeNode root,StringBuilder sb){
if(root == null){
sb.append("#").append(",");
return;
}
serialize(root.left,sb);
serialize(root.right,sb);
sb.append(root.val).append(",");
}
对于层次遍历是[1,2,3,#,4,#,#,#,#]的二叉树来说,后序遍历为[#,#,#,4,2,#,#,3,1]
如果按照后序遍历的方式反序列化的话,可以看到,root的值是列表的最后一个元素,应该从后往前取出列表元素,先用最后一个元素构造root,然后递归调用生成root的左右子树。注意,从后往前在nodes列表中取元素,一定要先构造root.right子树,后构造root.left子树。
TreeNode deserialize(LinkedList<String> nodes){
if(nodes.isEmpty()) return null;
String last = nodes.removeLast();
if(last.equals("#")) return null;
TreeNode root = new TreeNode(Integer.parseInt(last));
root.right = deserialize(nodes);
root.left = deserialize(nodes);
return root;
}
注:中序遍历的方式可以实现二叉树的序列化,但是无法实现反序列化。
层级遍历的方式:
首先先回顾一下二叉树的层序遍历方式:
void traverse(TreeNode root){
if(root == null) return;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
TreeNode cur = q.poll();
System.out.println(root.val);
if(cur.left != null){
q.offer(cur.left);
}
if(cur.right != null){
q.offer(cur.right);
}
}
}
按照层序遍历的方式将二叉树序列化:
String serialize(TreeNode root){
if(root == null) return "";
StringBuilder sb = new StringBuilder();
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
TreeNode cur = q.poll();
if(cur == null){
sb.append("#").append(",");
continue;
}
sb.append("#").append(",");
q.offer(cur.left);
q.offer(cur.right);
}
return sb.toString();
}
反序列化:
TreeNode deserialize(String data){
if(data.isEmpty()) return null;
String[] nodes = data.split(",");
TreeNode root = new TreeNode(Integer.parseInt(nodes[0]);
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
for(int i = 1;i < nodes.length;){
TreeNode parent = q.poll();
String left = nodes[i++];
if(!left.equals("#")){
parent.left = new TreeNode(Integer.parseInt(left));
q.offer(parent.left);
}
else{
parent.left = null;
}
String right = nodes[i++];
if(!right.equals("#")){
parent.right = new TreeNode(Integer.parseInt(right));
q.offer(parent.right);
}
else{
parent.right = null;
}
}
return root;
}
计算满二叉树的节点个数:
满二叉树是每个节点要么没有子节点,要么有左右两个子节点。
public int countNodes(TreeNode root){
int h = 0;
while(root != null){
root = root.left;
h++;
}
return (int)Math.pow(2,h)-1;
}
计算完全二叉树的节点总数(降低时间复杂度),其实是普通二叉树和满二叉树的结合版:
public int countNodes(TreeNode root){
TreeNode l = root,r = root;
int hl = 0,hr = 0;
while(l != null){
l = l.left;
hl++;
}
while(r != null){
r = r.right;
hr++;
}
//如果左右子树的高度相同,则是一棵满二叉树
if(hl == hr){
return (int)Math.pow(2,hl)-1;
}
//如果左右高度不同,则按照普通二叉树的逻辑计算
return 1+countNodes(root.left)+countNodes(root.right);
}
二叉树的最近公共祖先(难)
Q:给定一个二叉树,找到该树中两个指定节点的最近公共祖先。
案例1:
输入:root=[3,5,1,6,2,0,8,null,null,7,4],p=5,q=1
输出:3
解释:节点5和节点1的最近公共祖先是节点3。
案例2:
输入:root=[3,5,1,6,2,0,8,null,null,7,4],p=5,q=4
输出:5
解释:节点5和节点4的最近公共祖先是节点5.因为根据定义最近公共祖先节点可以为节点本身。
TreeNode lowestCommonAncestor(TreeNode root,TreeNode p,TreeNode q){
if(root == null) return null;
if(root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
if(left != null && right != null){
return root;
}
if(left == null && right == null){
return null;
}
return left == null ? right:left;
}