《剑指Offer》leetcode刷题笔记(一)

动态规划:

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)
0000
0110
1010
1101

可知,无进位和与异或运算规律相同,进位和与运算规律相同(并需左移一位),可将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”
解释:输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

解题思路:使用双指针,

  1. 倒序遍历字符串s,记录单词左右索引边界i,j;
  2. 每确定一个单词的边界,则将其添加至单词列表res;
  3. 最终,将单词列表拼接为字符串,并返回即可。

代码实现如下:

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。

根据以上特点,考虑以下循环判断:

  1. 判断n最右一位是否为1,根据结果计数;
  2. 将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个一组递归反转链表

根据递归的性质,可以得到大致的算法流程:

  1. 先反转以head开头的k个元素;
  2. 将第k+1个元素作为head递归调用reverseKGroup函数;
  3. 将上述两个过程的结果连接起来。

先按迭代的方式反转整个链表:

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);
}

当考虑一个节点的作用实现递归不可以时,可以增加函数参数,把“将每一层二叉树节点连接起来”可以细化成“将每两个相邻节点都连接起来”。

将二叉树展开为链表

思路:

  1. 将root的左子树和右子树拉平;
  2. 将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:给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下:

  1. 二叉树的根是数组中的最大元素;
  2. 左子树是通过数组中最大值左边部分构造出的最大二叉树;
  3. 右子树是通过数组中最大值右边部分构造出的最大二叉树。

如对于:[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中删除一个数

解析:

  1. 如果待删除节点恰好是末端节点,它的两个子节点都为空,那么可以毫无顾虑地直接将它删除;
  2. 如果待删除节点只有一个非空子节点,那么要让自己的孩子接替自己的位置;
  3. 如果待删除节点有两个子节点,为了不破坏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;
}

总结:

  1. 如果当前节点会对下面的子节点有整体影响,可以通过辅助函数增长参数列表,借助参数传递信息;
  2. 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;
}
	
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Despacito1006

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值