這幾天在刷樹相關的習題,今天來整體的總結一下關於樹的題目。
首先給出了樹的節點定義:
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
樹
Esay
1. 相同的樹
100. 相同的樹
我覺得對於樹相關的習題,使用遞歸往往很有好處,可以只全局的考慮左子樹和右子樹的條件,然後進行合理的遞歸。對於相同的樹這個題目,只需要判斷兩個樹根節點相同,左子樹右子樹分別相同就可以決定兩個樹是相同的。
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null)return true;
if (p == null || q == null) return false;
if (p.val != q.val) return false;
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}
}
當然可以使用非遞歸,維護一個隊列,按照層序遍歷的思路來比較節點的相同。
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
Queue<TreeNode> queue = new LinkedList<>();
queue.add(p);
queue.add(q);
while (!queue.isEmpty()) {
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
if (node1 == null && node2 == null) continue;
if (node1 == null || node2 == null) return false;
if (node1.val != node2.val) return false;
queue.add(node1.left);
queue.add(node2.left);
queue.add(node1.right);
queue.add(node2.right);
}
return true;
}
}
2.對稱二叉樹
101. 對稱二叉樹
和上面的思路一樣,只是變成了對比左節點的右孩子和右節點的左孩子是否相等,連代碼都只是簡單改了改:
遞歸:
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return Symmetric(root.left,root.right);
}
public boolean Symmetric(TreeNode left, TreeNode right) {
if(left == null && right == null) return true;
if (left == null || right == null)return false;
if(left.val != right.val) return false;
return Symmetric(left.left,right.right) &&
Symmetric(left.right,right.left);
}
}
迭代方法:
class Solution {
public boolean isSymmetric(TreeNode root) {
Queue<TreeNode>queue = new LinkedList<>();
queue.add(root);
queue.add(root);
while (!queue.isEmpty()) {
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
if(node1 == null && node2 == null) continue;
if(node1 == null || node2 == null) return false;
if(node1.val != node2.val) return false;
queue.add(node1.left);
queue.add(node2.right);
queue.add(node1.right);
queue.add(node2.left);
}
return true;
}
}
3. 二叉樹最大深度
第一種同樣是遞歸,每個節點的深度都是自己這個節點的1 在加上左子樹右子樹中的最大深度。
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}else{
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return 1 + Math.max(left,right);
}
}
}
第二種使用廣度優先遍歷,每到一層,層數加一,將該層的節點都加入隊列
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
// bfs
Queue<TreeNode> queue = new LinkedList<>();
int depth = 0;
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
depth++;
for (int i = 0; i < size; i++) {
TreeNode temp = queue.poll();
if (temp.left != null) {
queue.add(temp.left);
}
if (temp.right != null) {
queue.add(temp.right);
}
}
}
return depth;
}
}
4.二叉樹的層序遍歷 II
107. 二叉樹的層次遍歷 II
和層序遍歷類似,只是由於是自底向上的打印,所以可以採用頭插的方法。
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root){
LinkedList<List<Integer>> ans = new LinkedList<>();
if (root == null)
return ans;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(! queue.isEmpty()){
List<Integer> list = new LinkedList<>();
int size = queue.size();
for (int i = 0; i < size; i ++){
TreeNode tmp = queue.poll();
list.add(tmp.val);
if (tmp.left != null) {
queue.add(tmp.left);
}
if (tmp.right != null) {
queue.add(tmp.right);
}
}
ans.addFirst(list);
}
return ans;
}
}
5.將有序數組轉化爲二叉搜索樹
108. 將有序數組轉換爲二叉搜索樹
這裏需要注意的就是二叉搜索樹的中序遍歷是有序的! 本題中還說明了這棵樹高度平衡,所以可以利用這兩個個性質,得到根節點就是數組最中間的數(高度平衡)然後使用遞歸的方式分別構建左右子樹,最後與根節點連接。
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return nums == null ? null : buildTree(nums,0,nums.length - 1);
}
private TreeNode buildTree(int[] nums, int left, int right) {
if(left > right) {
return null;
}
int mid = left + (right - left) / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = buildTree(nums,left,mid - 1);
root.right = buildTree(nums,mid + 1,right);
return root;
}
}
6.平衡二叉樹
110. 平衡二叉樹
還是遞歸,首先明確對於一棵樹是平衡二叉樹需要滿足:
- 左子樹和右子樹的高度差步大於1
- 左子樹也是一棵平衡二叉樹
- 右子樹也是一顆平衡二叉樹
找到條件後,就是寫遞歸,從分析中可以看出需要一個求高度的方法,所以我們再提供一個方法求高度。
class Solution {
private int height(TreeNode root) {
if (root == null) {
return -1;
}
return 1 + Math.max(height(root.left), height(root.right));
}
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
}
return Math.abs(height(root.left) - height(root.right)) < 2
&& isBalanced(root.left)
&& isBalanced(root.right);
}
}
7.路徑總和
112. 路徑總和
對這道題也是進行遞歸,要想返回true,就要符合:
- 節點的Val相加爲給定的值
- 左子樹爲空
- 右子樹爲空
於是,同樣寫出遞歸方法,每次的傳入的值是sum值減去走過的節點的值
class Solution {
public boolean hasPathSum(TreeNode root, int sum) {
return isTarget(root,sum);
}
private boolean isTarget(TreeNode root, int target){
if(root == null) return false;
if(root.val == target && root.left == null && root.right == null) {
return true;
}
return isTarget(root.left,target-root.val) ||
isTarget(root.right,target-root.val);
}
}
8. 二叉樹的公共祖先
235. 二叉搜索樹的最近公共祖先
首先二叉搜索樹的特徵就是左子樹永遠小於根節點,右子樹永遠大於根節點
接着這道題同樣遞歸解決,從根節點開始遍歷,逐步縮小範圍:
- p 和 q 的值如果都大於根節點的值,說明都在根節點的右子樹上,以右子樹爲根遍歷
- p 和 q 的值如果都小於根節點的值,說明都在根節點的左子樹上,以左子樹爲根遍歷
- 都不符合,那只有當前根節點就是它們的祖先
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
int parentVal = root.val;
int pVal = p.val;
int qVal = q.val;
if (pVal > parentVal && qVal > parentVal) {
return lowestCommonAncestor(root.right, p, q);
} else if (pVal < parentVal && qVal < parentVal) {
return lowestCommonAncestor(root.left, p, q);
} else {
return root;
}
}
}
9. 二叉樹的直徑
543. 二叉樹的直徑
定義一個全局變量來存放節點之間的最大路徑,遍歷每一個節點作爲根節點,那麼最長的路徑就是當前的節點的左子樹的最大深度與右子樹最大深度之和。比較該節點的最大路徑值和原有最大路徑值,選擇最大的作爲答案。
class Solution {
int max = 0;
public int diameterOfBinaryTree(TreeNode root) {
if(root == null) return 0;
depth(root);
return max;
}
public int depth(TreeNode root){
if(root == null){
return 0;
}
int left = depth(root.left);
int right = depth(root.right);
max = (left + right) > max ? (left + right) : max;
return 1 + Math.max(left,right);
}
}
10. 二叉樹的所有路徑
257. 二叉樹的所有路徑
本題要求求出根節點到葉節點的路徑,很明顯是一種深度優先遍歷的方式,我們可以使用遞歸來解決:
- 當前節點不是葉子節點 —— 將當前節點加到路徑中,並遞歸調用它的孩子節點
- 當前節點是葉子節點 —— 將當前節點加到路徑中,得到的路徑加到答案中
class Solution {
public void construct_paths(TreeNode root, String path, LinkedList<String> paths) {
if (root != null) {
path += Integer.toString(root.val);
if ((root.left == null) && (root.right == null)) // 當前節點是葉子節點
paths.add(path); // 把路徑加入到答案中
else {
path += "->"; // 當前節點不是葉子節點,繼續遞歸遍歷
construct_paths(root.left, path, paths);
construct_paths(root.right, path, paths);
}
}
}
public List<String> binaryTreePaths(TreeNode root) {
LinkedList<String> paths = new LinkedList();
construct_paths(root, "", paths);
return paths;
}
}
以上就是自己篩選的一些LeetCode上樹這一專題的簡單題,大家還可以自己去多刷些,循序漸進。
Medium
1. 樹的先序非遞歸遍歷
對於樹的三種遍歷方式的遞歸寫法,相信大多數人已經沒問題,那對於非遞歸的寫法,也是面試的常考點。先來看看先序非遞歸遍歷。
我們模擬遞歸棧的效果,先將根節點入棧,然後只要棧不爲空,就重複取出棧頂元素並將其左右子樹入棧。
public static void preOrderNoRecur(Node head) {
if(head != null) {
Stack<Node> s = new Stack<Node>();
s.push(head); //壓入根節點
while(!s.isEmpty()) {
head = s.pop(); //彈出節點
System.out.print(head.data+" ");
if(head.right != null) { //壓入右孩子
s.push(head.right);
}
if(head.left != null) { //壓入左孩子
s.push(head.left);
}
}
}
}
2.樹的中序非遞歸遍歷
中序遍歷相對於先序會有一些複雜,不過也好理解,首先就是入棧根節點,接着不斷壓入左節點,直到找到最左孩子也就是中序遍歷序列的第一個節點。接着逐個出棧打印(此時就相當於遍歷上一個節點的根節點)接着將這個節點的右孩子入棧,重複這個步驟直至棧爲空。
public static void inOrderNoRecur(Node head) {
if(head != null) {
Stack<Node> s = new Stack<Node>();
while(!s.isEmpty() || head != null) {
if(head != null) {
s.push(head);
head = head.left;
}else {
head = s.pop();
System.out.print(head.data+" ");
head = head.right;
}
}
}
3.樹的後序非遞歸遍歷
樹的非遞歸後續遍歷更加複雜,它需要先遍歷左子樹再遍歷右子樹,最後遍歷根節點。因爲在遍歷左子節點的過程我們也要記錄下根節點,要通過這個根節點訪問到右子樹。
這裏定義了 h 節點表示上一次打印的節點,c 表示每次的棧頂元素。
對於每一個棧頂的節點就有:
- 如果此節點的左孩子和右孩子都不是上一次打印過的節點,那說明下一次就應該繼續將左孩子入棧(左孩子不爲空的情況下)
- 如果此節點的左孩子是上一次打印過的,那就證明此節點的右孩子應該是下一個被入棧的節點,所以將右孩子入棧(右孩子不爲空的情況下)
- 否則就是該節點的左右孩子都爲空,或者就是該節點的左右孩子都被打印過了,此時出棧該節點並打印
按照這個流程就是一次非遞歸的後續遍歷。
public void postorderNoRecur(Node head) {
Node h = head;
if (head != null) {
Stack<Node> stack = new Stack<Node>();
stack.push(head);
Node c = null;
while (! stack.isEmpty()) {
c = stack.peek();
if (c.left != null && h != c.left && h != c.right) {
stack.push(c.left);
} else if (c.right != null && h != c.right) {
stack.push(c.right);
} else {
System.out.print(stack.pop().val + " ");
h = c;
}
}
}
}
4. 不同的二叉搜索樹
96. 不同的二叉搜索樹
對於本題,只是需要返回可組成樹的個數,事實上,對於每個數量的節點組成的不同二叉樹的個數都是相同的。比如:1個節點有一種,兩個節點有兩種,三個節點有五種,所以本題我們可以使用動態規劃的方法(如果不熟悉動態規劃可以看——> 動態規劃:適合新手的動態規劃入門常見題目詳解)
對於本題,可以將樹分爲左右兩部分考慮:
對於每個數量,先將節點分爲左右兩部分(在分的時候就有一層循環就是內(j)循環),每個數量的節點可以組成的數量是左子樹的鐘類和右子樹鍾類的乘積,而且每次的分的方式得到的節點數量的總和就是dp的值。
在下面代碼中,dp[i] 表示 數量爲 i 時可以有的組合方式;dp[j] 表示分出的左子樹的組成數量;dp[i - j + 1]相應就是右子樹的數量(i 表示總節點數,j 表示分出的左節點個數,i - j - 1 就是右節點個數(-1 是減去了根節點))
class Solution {
public int numTrees(int n) {
if(n == 0) return 0;
if(n == 1) return 1;
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i ++) {
for(int j = 0; j < i; j ++){
dp[i] += (dp[j] * dp[i - j - 1]);
}
}
return dp[n];
}
}
5. 不同的二叉搜索樹 II
95. 不同的二叉搜索樹 II
上一題的升級版,不是隻要求 找出個數,而是直接返回所有的組合方式。
當然,我們還是使用遞歸,接着來考慮遞歸怎麼寫。
與上一題相似,還是將樹分爲左子樹和右子樹來構建。所以需要一個構建樹的一個方法,這個方法的參數就是給定的樹的數據區域。
在這個方法中再次遞歸產生左右子樹,然後每次都將得到的左右子樹的各種可能性都串起來。
class Solution {
public List<TreeNode> generateTrees(int n) {
if (n == 0) {
return new LinkedList<TreeNode>();
}
return buildTree(1, n);
}
private List<TreeNode> buildTree(int start,int end){
List<TreeNode> list = new LinkedList<>();
if (start > end){
list.add(null);
return list;
}
for(int i = start; i <= end; i ++){ //每次以i爲root 分成左右兩半
List<TreeNode> leftList = buildTree(start,i - 1); //得到左子樹的所有可能情況
List<TreeNode> rightList = buildTree(i + 1, end); // 得到右子樹的所有可能情況
//以i爲根節點將左右子樹合成一棵樹
for(TreeNode l : leftList){
for(TreeNode r : rightList){
TreeNode tmp = new TreeNode(i);
tmp.left = l;
tmp.right = r;
list.add(tmp);
}
}
}
return list;
}
}
6.驗證二叉搜索樹
98.驗證二叉搜索樹
首先,題目上說明白了驗證的三個條件,這種左子樹右子樹都是同樣要求的很適合使用遞歸方法來解決。
對於根節點,它一定是左子樹中的最大值,所以在驗證左子樹是否符合條件的時候,選用根節點的值作爲upper(lower的值無所謂)。而對於右子樹,它一定是右子樹中的最小值,所以在驗證右子樹是否符合條件時,只需要選用根節點的值作爲lower(upper的值無所謂)。
初始時,將root傳入,upper和lower都爲null,此後,只要upper不爲null,說明傳入了最大值,也就是判斷的就是左子樹是否符合條件,只需要當前節點的值小於upper就繼續判斷,否則就一定不是搜索樹。反之亦然,對於lower不爲null的情況,判斷的就是右子樹。
class Solution {
public boolean isValidBST(TreeNode root) {
return helper(root, null, null);
}
public boolean helper(TreeNode node, Integer lower, Integer upper) {
if (node == null) return true;
int val = node.val;
if (lower != null && val <= lower) return false;
if (upper != null && val >= upper) return false;
if (! helper(node.right, val, upper)) return false;
if (! helper(node.left, lower, val)) return false;
return true;
}
}
迭代方式也可以做,採用中序遍歷的思路。由於二叉搜索樹的中序遍歷總是有序的,所以我們可以先求得中序遍歷的結果,在進行判斷是否有序。
當然其實只需要證明每一個遍歷到的節點一定比下一個遍歷到的節點小就可以了。也就是本次遍歷的一定比上一次的要大。
class Solution {
public boolean isValidBST(TreeNode root) {
Stack<TreeNode> stack = new Stack<TreeNode>();
double inorder = -Double.MAX_VALUE;
while(!stack.isEmpty() || root != null) {
while(root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
if(root.val <= inorder) return false; //本次節點如果比上一次小,就false
inorder = root.val; //更新inorder的值便於下一次比較
root = root.right;
}
return true;
}
}
7.二叉樹的層序遍歷
102. 二叉樹的層次遍歷
和前面的題目有相似之處,使用Queue輔助完成層序遍歷
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
if (root == null) return ans;
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.add(root);
int level = 0;
while(!queue.isEmpty()) {
List<Integer> list = new ArrayList<>();
int size = queue.size();
for(int i = 0; i < size; i++) {
TreeNode tmp = queue.poll();
list.add(tmp.val);
if(tmp.left != null) {
queue.add(tmp.left);
}
if(tmp.right != null) {
queue.add(tmp.right);
}
}
ans.add(new ArrayList(list));
}
return ans;
}
}
8.二叉樹的鋸齒形層次遍歷
103. 二叉樹的鋸齒形層次遍歷
這個相對於直接的層序遍歷要複雜些,因爲使用鋸齒型遍歷,本層先遍歷的節點的孩子節點在下一層要後遍歷到,這符合我們所說的棧的先進後出的原理,所以我們採用兩個棧來實現:每一層都在其中一層遍歷所有節點而另一層則加入這一層的所有孩子節點,直到兩個棧都爲空。
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
if (root == null) {
return list;
}
//棧1來存儲右節點到左節點的順序
Stack<TreeNode> stack1 = new Stack<>();
//棧2來存儲左節點到右節點的順序
Stack<TreeNode> stack2 = new Stack<>();
//根節點入棧
stack1.push(root);
//每次循環中,都是一個棧爲空,一個棧不爲空,結束的條件兩個都爲空
while (!stack1.isEmpty() || !stack2.isEmpty()) {
List<Integer> subList = new ArrayList<>(); // 存儲這一個層的數據
TreeNode cur = null;
if (!stack1.isEmpty()) { //棧1不爲空,則棧2此時爲空,需要用棧2來存儲從下一層從左到右的順序
while (!stack1.isEmpty()) { //遍歷棧1中所有元素,即當前層的所有元素
cur = stack1.pop();
subList.add(cur.val); //存儲當前層所有元素
if (cur.left != null) { //左節點不爲空加入下一層
stack2.push(cur.left);
}
if (cur.right != null) { //右節點不爲空加入下一層
stack2.push(cur.right);
}
}
list.add(subList);
}else {//棧2不爲空,則棧1此時爲空,需要用棧1來存儲從下一層從右到左的順序
while (!stack2.isEmpty()) {
cur = stack2.pop();
subList.add(cur.val);
if (cur.right != null) {//右節點不爲空加入下一層
stack1.push(cur.right);
}
if (cur.left != null) { //左節點不爲空加入下一層
stack1.push(cur.left);
}
}
list.add(subList);
}
}
return list;
}
}
9.從前序與中序遍歷序列構造二叉樹
105. 從前序與中序遍歷序列構造二叉樹
每次以先序遍歷中第一個節點爲根節點,在中序遍歷中找到改節點的位置,以該點的位置就將整個區間劃分爲左右子樹。然後將根節點和左右子樹串聯,繼續遞歸
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder.length == 0 || inorder.length == 0) return null;
TreeNode root = new TreeNode(preorder[0]);
int i; //先序遍歷的第一個節點在找到中序遍歷中的位置
for (i = 0; i < inorder.length; i ++) {
if (inorder[i] == root.val) {
break;
}
}
//按這個位置將先序中序遍歷分爲左右子樹繼續遍歷
int[] leftPreorder = Arrays.copyOfRange(preorder,1,i+1); //i+1 不包含
int[] rightPreorder = Arrays.copyOfRange(preorder,i+1,preorder.length);
int[] leftInorder = Arrays.copyOfRange(inorder,0,i);
int[] rightInorder = Arrays.copyOfRange(inorder,i+1,inorder.length);
root.left = buildTree(leftPreorder,leftInorder);
root.right = buildTree(rightPreorder,rightInorder);
return root;
}
}
10. 從中序與後序遍歷序列構造二叉樹
106.從中序與後序遍歷序列構造二叉樹
同上一題思路相似。後序遍歷的最後一個節點就是整個樹得根節點,
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
if(inorder.length==0) return null;
TreeNode root = new TreeNode(postorder[postorder.length-1]);
int i;
for (i = 0; i < inorder.length; i ++) {
if (inorder[i] == root.val) {
break;
}
}
int[] leftInorder = Arrays.copyOfRange(inorder,0,i);
int[] rightInorder = Arrays.copyOfRange(inorder,i+1,inorder.length);
int[] leftPostorder = Arrays.copyOfRange(postorder,0,i);
int[] rightPostorder = Arrays.copyOfRange(postorder,i,postorder.length-1);
root.left = buildTree(leftInorder,leftPostorder);
root.right = buildTree(rightInorder,rightPostorder);
return root;
}
}