数据结构 - 二叉树 - 面试中常见的二叉树算法题
數據結構 - 二叉樹 - 面試中常見的二叉樹算法題
數據結構是面試中必定考查的知識點,面試者需要掌握幾種經典的數據結構:線性表(數組、鏈表)、棧與隊列、樹(二叉樹、二叉查找樹、平衡二叉樹、紅黑樹)、圖。
本文主要介紹樹中的常見的二叉樹數據結構。包括
- 概念簡介
- 二叉樹中樹節點的數據結構(Java)
- 二叉樹的遍歷(Java)
- 常見的二叉樹算法題(Java)
概念簡介
如果對二叉樹概念已經基本掌握,可以跳過該部分,直接查看常見鏈表算法題。
二叉樹基本概念
二叉樹在圖論中是這樣定義的:二叉樹是一個連通的無環圖,并且每一個頂點的度不大于3。有根二叉樹還要滿足根結點的度不大于2。有了根結點之后,每個頂點定義了唯一的父結點,和最多2個子結點。二叉樹性質如下:
- 二叉樹的每個結點至多只有二棵子樹(不存在度大于2的結點),二叉樹的子樹有左右之分,次序不能顛倒。
- 二叉樹的第 i 層至多有 2i?1 個結點。
- 深度為 k 的二叉樹至多有 2k?1 個結點。
- 對任何一棵二叉樹T,如果其終端結點數為n0,度為2的結點數為n2,則n0=n2+1。
- 一棵深度為k,且有 2k?1 個節點稱之為滿二叉樹;
- 深度為k,有n個節點的二叉樹,當且僅當其每一個節點都與深度為k的滿二叉樹中,序號為1至n的節點對應時,稱之為完全二叉樹。
- 平衡二叉樹又被稱為AVL樹(區別于AVL算法),它是一棵二叉排序樹,且具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,并且左右兩個子樹都是一棵平衡二叉樹。
二叉樹中樹節點的數據結構
二叉樹由一系列樹結點組成,每個結點包括三個部分:一個是存儲數據元素的數據域,另一個是存儲左子結點地址的指針域,另一個是存儲右子結點地址的指針域。
定義樹節點為類:TreeNode。具體實現如下:
public class TreeNode {public int val; // 數據域public TreeNode left; // 左子樹根節點public TreeNode right; // 右子樹根節點public TreeNode() {}public TreeNode(int val) {this.val = val;}}二叉樹的遍歷
1. 前序遍歷
遞歸解法:
- 如果二叉樹為空,空操作
- 如果二叉樹不為空,訪問根節點,前序遍歷左子樹,前序遍歷右子樹
非遞歸解法:用一個輔助stack,總是先把右孩子放進棧。
/*** 1. 前序遍歷* 非遞歸* @param root 樹根節點*/ public static void preorderTraversal2(TreeNode root) {if (root == null) {return;}Stack<TreeNode> stack = new Stack<>(); // 輔助棧TreeNode cur = root;while (cur != null || !stack.isEmpty()) {while (cur != null) { // 不斷將左子節點入棧,直到cur為空stack.push(cur);System.out.print(cur.val + "->"); // 前序遍歷,先打印當前節點在打印左子節點,然后再把右子節點加到棧中cur = cur.left;}if (!stack.isEmpty()) { // 棧不為空,彈出棧元素cur = stack.pop(); // 此時彈出最左邊的節點cur = cur.right; // 令當前節點為右子節點}} }/*** 1. 前序遍歷* 非遞歸解法2* @param root 樹根節點*/ public static void preorderTraversal(TreeNode root) {if (root == null) {return;}Stack<TreeNode> stack = new Stack<>(); // 輔助棧保存樹節點stack.add(root);while (!stack.isEmpty()) { // 棧不為空TreeNode temp = stack.pop();System.out.print(temp.val + "->"); // 先根節點,因為是前序遍歷if (temp.right != null) { // 先添加右孩子,因為棧是先進后出stack.add(temp.right);}if (temp.left != null) {stack.add(temp.left);}} }2. 中序遍歷
遞歸解法:
- 如果二叉樹為空,空操作
- 如果二叉樹不為空,中序遍歷左子樹,訪問根節點,中序遍歷右子樹
非遞歸解法:用棧先把根節點的所有左孩子都添加到棧內,然后輸出棧頂元素,再處理棧頂元素的右子樹。
/*** 2. 中序遍歷* 非遞歸* @param root 樹根節點*/ public static void inorderTraversal(TreeNode root) {if (root == null) {return;}Stack<TreeNode> stack = new Stack<>(); // 輔助棧TreeNode cur = root;while (cur != null || !stack.isEmpty()) {while (cur != null) { // 不斷將左子節點入棧,直到cur為空stack.push(cur);cur = cur.left;}if (!stack.isEmpty()) { // 棧不為空,彈出棧元素cur = stack.pop(); // 此時彈出最左邊的節點System.out.print(cur.val + "->"); // 中序遍歷,先打印左子節點在打印當前節點,然后再把右子節點加到棧中cur = cur.right; // 令當前節點為右子節點}} }3. 后序遍歷
遞歸解法:
- 如果二叉樹為空,空操作
- 如果二叉樹不為空,后序遍歷左子樹,后序遍歷右子樹,訪問根節點
非遞歸解法:雙棧法。
/*** 3. 后序遍歷* 非遞歸* @param root 樹根節點*/ public static void postorderTraversal(TreeNode root) {if(root == null) {return;}Stack<TreeNode> stack1 = new Stack<>(); // 保存樹節點Stack<TreeNode> stack2 = new Stack<>(); // 保存后序遍歷的結果stack1.add(root);while (!stack1.isEmpty()) {TreeNode temp = stack1.pop();stack2.push(temp); // 將彈出的元素加到stack2中if (temp.left != null) { // 左子節點先入棧stack1.push(temp.left);}if (temp.right != null) { // 右子節點后入棧stack1.push(temp.right);}}while (!stack2.isEmpty()) {System.out.print(stack2.pop().val + "->");} }4. 層次遍歷
思路:分層遍歷二叉樹(按層次從上到下,從左到右)迭代,相當于廣度優先搜索,使用隊列實現。隊列初始化,將根節點壓入隊列。當隊列不為空,進行如下操作:彈出一個節點,訪問,若左子節點或右子節點不為空,將其壓入隊列。
/*** 4. 層次遍歷* @param root 根節點*/ public static void levelTraversal(TreeNode root){if(root == null) {return;}Queue<TreeNode> queue = new LinkedList<>(); // 對列保存樹節點queue.add(root);while (!queue.isEmpty()) {TreeNode temp = queue.poll();System.out.print(temp.val + "->");if (temp.left != null) { // 添加左右子節點到對列queue.add(temp.left);}if (temp.right != null) {queue.add(temp.right);}} }常見的二叉樹算法題
1. 求二叉樹中的節點個數
遞歸解法: O(n)
- 如果二叉樹為空,節點個數為0
- 如果二叉樹不為空,二叉樹節點個數 = 左子樹節點個數 + 右子樹節點個數 + 1
非遞歸解法:O(n)。基本思想同LevelOrderTraversal。即用一個Queue,在Java里面可以用LinkedList來模擬。
/*** 1. 求二叉樹中的節點個數* 非遞歸* @param root 樹根節點* @return 節點個數*/ public static int getNodeNum(TreeNode root) {if (root == null) {return 0;}Queue<TreeNode> queue = new LinkedList<>(); // 用隊列保存樹節點,先進先出queue.add(root);int count = 1; // 節點數量while (!queue.isEmpty()) {TreeNode temp = queue.poll(); // 每次從對列中刪除節點,并返回該節點信息if (temp.left != null) { // 添加左子孩子到對列queue.add(temp.left);count++;}if (temp.right != null) { // 添加右子孩子到對列queue.add(temp.right);count++;}}return count; }2. 求二叉樹的深度(高度)
遞歸解法: O(n)
- 如果二叉樹為空,二叉樹的深度為0
- 如果二叉樹不為空,二叉樹的深度 = max(左子樹深度, 右子樹深度) + 1
非遞歸解法:O(n)。基本思想同LevelOrderTraversal。即用一個Queue,在Java里面可以用LinkedList來模擬。
/*** 求二叉樹的深度(高度)* 非遞歸* @param root 樹根節點* @return 樹的深度*/ public static int getDepth(TreeNode root) {if (root == null) {return 0;}int currentLevelCount = 1; // 當前層的節點數量int nextLevelCount = 0; // 下一層節點數量int depth = 0; // 樹的深度Queue<TreeNode> queue = new LinkedList<>(); // 對列保存樹節點queue.add(root);while (!queue.isEmpty()) {TreeNode temp = queue.remove(); // 移除節點currentLevelCount--; // 當前層節點數減1if (temp.left != null) { // 添加左節點并更新下一層節點個數queue.add(temp.left);nextLevelCount++;}if (temp.right != null) { // 添加右節點并更新下一層節點個數queue.add(temp.right);nextLevelCount++;}if (currentLevelCount == 0) { // 如果是該層的最后一個節點,樹的深度加1depth++;currentLevelCount = nextLevelCount; // 更新當前層節點數量并且重置下一層節點數量nextLevelCount = 0;}}return depth; }3. 求二叉樹第k層的節點個數
遞歸解法: O(n)
思路:求以root為根的k層節點數目,等價于求以root左孩子為根的k-1層(因為少了root)節點數目 加上以root右孩子為根的k-1層(因為 少了root)節點數目。即:
- 如果二叉樹為空或者k<1,返回0
- 如果二叉樹不為空并且k==1,返回1
- 如果二叉樹不為空且k>1,返回root左子樹中k-1層的節點個數與root右子樹k-1層節點個數之和
4. 求二叉樹中葉子節點的個數
遞歸解法:
- 如果二叉樹為空,返回0
- 如果二叉樹是葉子節點,返回1
- 如果二叉樹不是葉子節點,二叉樹的葉子節點數 = 左子樹葉子節點數 + 右子樹葉子節點數
非遞歸解法:基于層次遍歷進行求解,利用Queue進行。
/*** 4. 求二叉樹中葉子節點的個數(迭代)* 非遞歸* @param root 根節點* @return 葉子節點個數*/ public static int getNodeNumLeaf(TreeNode root){if (root == null) {return 0;}int leaf = 0; // 葉子節點個數Queue<TreeNode> queue = new LinkedList<>();queue.add(root);while (!queue.isEmpty()) {TreeNode temp = queue.poll();if (temp.left == null && temp.right == null) { // 葉子節點leaf++;}if (temp.left != null) {queue.add(temp.left);}if (temp.right != null) {queue.add(temp.right);}}return leaf; }5. 判斷兩棵二叉樹是否相同的樹
遞歸解法:
- 如果兩棵二叉樹都為空,返回真
- 如果兩棵二叉樹一棵為空,另外一棵不為空,返回假
- 如果兩棵二叉樹都不為空,如果對應的左子樹和右子樹都同構返回真,其他返回假
非遞歸解法:利用Stack對兩棵樹對應位置上的節點進行判斷是否相同。
/*** 5. 判斷兩棵二叉樹是否相同的樹(迭代)* 非遞歸* @param r1 二叉樹1* @param r2 二叉樹2* @return 是否相同*/ public static boolean isSame(TreeNode r1, TreeNode r2){if (r1 == null && r2 == null) { // 都是空return true;} else if (r1 == null || r2 == null) { // 有一個為空,一個不為空return false;}Stack<TreeNode> stack1 = new Stack<>();Stack<TreeNode> stack2 = new Stack<>();stack1.add(r1);stack2.add(r2);while (!stack1.isEmpty() && !stack2.isEmpty()) {TreeNode temp1 = stack1.pop();TreeNode temp2 = stack2.pop();if (temp1 == null && temp2 == null) { // 兩個元素都為空,因為添加的時候沒有對空節點做判斷continue;} else if (temp1 != null && temp2 != null && temp1.val == temp2.val) {stack1.push(temp1.left); // 相等則添加左右子節點判斷stack1.push(temp1.right);stack2.push(temp2.left);stack2.push(temp2.right);} else {return false;}}return true; }6. 判斷二叉樹是不是平衡二叉樹
遞歸實現:借助前面實現好的求二叉樹高度的函數
- 如果二叉樹為空, 返回真
- 如果二叉樹不為空,如果左子樹和右子樹都是AVL樹并且左子樹和右子樹高度相差不大于1,返回真,其他返回假
7. 求二叉樹的鏡像
遞歸實現:破壞原來的樹,把原來的樹改成其鏡像
- 如果二叉樹為空,返回空
- 如果二叉樹不為空,求左子樹和右子樹的鏡像,然后交換左右子樹
遞歸實現:不能破壞原來的樹,返回一個新的鏡像樹
- 如果二叉樹為空,返回空
- 如果二叉樹不為空,求左子樹和右子樹的鏡像,然后交換左右子樹
非遞歸實現:破壞原來的樹,把原來的樹改成其鏡像
/*** 7. 求二叉樹的鏡像* 非遞歸* @param root 根節點* @return 鏡像二叉樹的根節點*/ public static void mirror(TreeNode root) {if (root == null) {return ;}Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()){TreeNode cur = stack.pop();// 交換左右孩子TreeNode tmp = cur.right;cur.right = cur.left;cur.left = tmp;if(cur.right != null) {stack.push(cur.right);}if (cur.left != null) {stack.push(cur.left);}} }非遞歸實現:不能破壞原來的樹,返回一個新的鏡像樹
/*** 7. 求二叉樹的鏡像* 非遞歸* @param root 根節點* @return 鏡像二叉樹的根節點*/ public static TreeNode mirrorCopy(TreeNode root) {if (root == null) {return null;}Stack<TreeNode> stack = new Stack<TreeNode>();Stack<TreeNode> newStack = new Stack<TreeNode>();stack.push(root);TreeNode newRoot = new TreeNode(root.val);newStack.push(newRoot);while (!stack.isEmpty()) {TreeNode cur = stack.pop();TreeNode newCur = newStack.pop();if (cur.right != null) {stack.push(cur.right);newCur.left = new TreeNode(cur.right.val);newStack.push(newCur.left);}if (cur.left != null) {stack.push(cur.left);newCur.right = new TreeNode(cur.left.val);newStack.push(newCur.right);}}return newRoot; }8. 判斷兩個二叉樹是否互相鏡像
遞歸解法:與比較兩棵二叉樹是否相同解法一致(題5),非遞歸解法省略。
- 比較r1的左子樹的鏡像是不是r2的右子樹
- 比較r1的右子樹的鏡像是不是r2的左子樹
9. 求二叉樹中兩個節點的最低公共祖先節點
遞歸解法:
- 如果兩個節點分別在根節點的左子樹和右子樹,則返回根節點
- 如果兩個節點都在左子樹,則遞歸處理左子樹;如果兩個節點都在右子樹,則遞歸處理右子樹
非遞歸算法:得到從二叉樹根節點到兩個節點的路徑,路徑從頭開始的最后一個公共節點就是它們的最低公共祖先節點
/*** 9. 樹中兩個節點的最低公共祖先節點* 非遞歸* @param root 樹根節點* @param n1 第一個節點* @param n2 第二個節點* @return 第一個公共祖先節點*/ public static TreeNode getLastCommonParent(TreeNode root, TreeNode n1, TreeNode n2) {if (root == null || n1 == null || n2 == null) {return null;}ArrayList<TreeNode> p1 = new ArrayList<>();boolean res1 = getNodePath(root, n1, p1);ArrayList<TreeNode> p2 = new ArrayList<>();boolean res2 = getNodePath(root, n2, p2);if (!res1 || !res2) {return null;}TreeNode last = null;Iterator<TreeNode> iter1 = p1.iterator();Iterator<TreeNode> iter2 = p2.iterator();while (iter1.hasNext() && iter2.hasNext()) {TreeNode tmp1 = iter1.next();TreeNode tmp2 = iter2.next();if (tmp1 == tmp2) {last = tmp1;} else { // 直到遇到非公共節點break;}}return last; }/*** 把從根節點到node路徑上所有的點都添加到path中* @param root 樹根節點* @param node 終點節點* @param path 路徑* @return 是否是目標節點*/ public static boolean getNodePath(TreeNode root, TreeNode node, ArrayList<TreeNode> path) {if (root == null) {return false;}path.add(root); // 把這個節點添加到路徑中if (root == node) {return true;}boolean found = false;found = getNodePath(root.left, node, path); // 先在左子樹中找if (!found) {found = getNodePath(root.right, node, path);}if (!found) { // 如果實在沒找到證明這個節點不在路徑中,刪除剛剛那個節點path.remove(root);}return found; }10. 判斷是否為二分查找樹BST
遞歸解法:中序遍歷的結果應該是遞增的。
/*** 10. 判斷是否為二分查找樹BST* @param root 根節點* @param pre 上一個保存的節點* @return 是否為BST樹*/ public static boolean isValidBST(TreeNode root, int pre){if (root == null) {return true;}boolean left = isValidBST(root.left, pre);if (!left) {return false;}if(root.val <= pre) {return false;}pre = root.val;boolean right = isValidBST(root.right, pre);if(!right) {return false;}return true; }非遞歸解法:參考非遞歸中序遍歷。
/** * 10. 判斷是否為二分查找樹BST* 非遞歸* @param root 根節點*/ public boolean isValidBST2(TreeNode root){Stack<TreeNode> stack = new Stack<>();//設置前驅節點TreeNode pre = null;while(root != null || !stack.isEmpty()){while (root != null) { // 將當前節點,以及左子樹一直入棧,循環結束時,root==nullstack.push(root);root = root.left;}root = stack.pop();//比較并更新前驅,與普通遍歷的區別就在下面四行if(pre != null && root.val <= pre.val){return false;}pre = root;root = root.right; //訪問右子樹}return true; }總結
以上是生活随笔為你收集整理的数据结构 - 二叉树 - 面试中常见的二叉树算法题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构 - 链表 - 面试中常见的链表
- 下一篇: 数据结构 - 字符串 - 最长公共子序列