原文:https://subetter.com/algorith...php
本篇针对面试中常见的二叉树操做做个总结:
(1)前序遍历,中序遍历,后序遍历;
(2)层次遍历;
(3)求树的节点数;
(4)求树的叶子数;
(5)求树的深度;
(6)求二叉树第k层的节点个数;
(7)判断两棵二叉树是否结构相同;
(8)求二叉树的镜像;
(9)求两个节点的最低公共祖先节点;
(10)求任意两节点距离;
(11)找出二叉树中某个节点的全部祖先节点;
(12)不使用递归和栈遍历二叉树;
(13)二叉树前序中序推后序;
(14)判断二叉树是否是彻底二叉树;
(15)判断是不是二叉查找树的后序遍历结果;
(16)给定一个二叉查找树中的节点,找出在中序遍历下它的后继和前驱;
(17)二分查找树转化为排序的循环双链表;
(18)有序链表转化为平衡的二分查找树。html
参见二叉树基础。node
int GetDepth(Node * node) { if (node == nullptr) return 0; int left_depth = GetDepth(node->left) + 1; int right_depth = GetDepth(node->right) + 1; return left_depth > right_depth ? left_depth : right_depth; }
int GetKLevel(Node * node, int k) { if (node == nullptr || k < 1) return 0; if (k == 1) return 1; return GetKLevel(node->left, k - 1) + GetKLevel(node->right, k - 1); }
不考虑数据内容。结构相赞成味着对应的左子树和对应的右子树都结构相同。c++
bool StructureCmp(Node * node1, Node * node2) { if (node1 == nullptr && node2 == nullptr) return true; else if (node1 == nullptr || node2 == nullptr) return false; bool result_left = StructureCmp(node1->left, node2->left); bool result_right = StructureCmp(node1->right, node2->right); return result_left && result_right; }
对于每一个节点,咱们交换它的左右孩子便可。面试
void Mirror(Node * node) { if (node == nullptr) return; Node * temp = node->left; node->left = node->right; node->right = temp; Mirror(node->left); Mirror(node->right); }
最低公共祖先,即LCA(Lowest Common Ancestor),见下图:算法
结点3和结点4的最近公共祖先是结点2,即LCA(3 ,4)=2。在此,须要注意到当两个结点在同一棵子树上的状况,如结点3和结点2的最近公共祖先为2,即 LCA(3,2)=2。同理LCA(5,6)=4,LCA(6,10)=1。数组
Node * FindLCA(Node * node, Node * target1, Node * target2) { if (node == nullptr) return nullptr; if (node == target1 || node == target2) return node; Node * left = FindLCA(node->left, target1, target2); Node * right = FindLCA(node->right, target1, target2); if (left && right) //分别在左右子树 return node; return left ? left : right; //都在左子树或右子树 }
首先找到两个节点的LCA,而后分别计算LCA与它们的距离,最后相加便可。数据结构
int FindLevel(Node * node, Node * target) { if (node == nullptr) return -1; if (node == target) return 0; int level = FindLevel(node->left, target); //先在左子树找 if (level == -1) level = FindLevel(node->right, target); //若是左子树没找到,在右子树找 if (level != -1) //找到了,回溯 return level + 1; return -1; //若是左右子树都没找到 } int DistanceNodes(Node * node, Node * target1, Node * target2) { Node * lca = FindLCA(node, target1, target2); //找到最低公共祖先节点 int level1 = FindLevel(lca, target1); int level2 = FindLevel(lca, target2); return level1 + level2; }
若是给定节点5,则其全部祖先节点为4,2,1。函数
bool FindAllAncestors(Node * node, Node * target) { if (node == nullptr) return false; if (node == target) return true; if (FindAllAncestors(node->left, target) || FindAllAncestors(node->right, target)) //找到了 { cout << node->data << " "; return true; //回溯 } return false; //若是左右子树都没找到 }
1968年,高德纳(Donald Knuth)提出一个问题:是否存在一个算法,它不使用栈也不破坏二叉树结构,可是能够完成对二叉树的遍历?随后1979年,James H. Morris提出了二叉树线索化,解决了这个问题。(根据这个概念咱们又提出了一个新的数据结构,即线索二叉树,因线索二叉树不是本文要介绍的内容,因此有兴趣的朋友请移步线索二叉树。)
前序,中序,后序遍历,不论是递归版本仍是非递归版本,都用到了一个数据结构--栈,为什么要用栈?那是由于其它的方式无法记录当前节点的parent,而若是在每一个节点的结构里面加个parent份量显然是不现实的,而线索化正好解决了这个问题,其含义就是利用节点的右孩子空指针,指向该节点在中序序列中的后继。下面具体来看看如何使用线索化来完成对二叉树的遍历。spa
先看前序遍历,步骤以下:
(1)若是当前节点的左孩子为空,则输出当前节点并将其右孩子做为当前节点;
(2)若是当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点;
(2.1)若是前驱节点的右孩子为空,将它的右孩子设置为当前节点,输出当前节点并把当前节点更新为当前节点的左孩子;
(2.2)若是前驱节点的右孩子为当前节点,将它的右孩子从新设为空,当前节点更新为当前节点的右孩子;
(3)重复以上(1)和(2),直到当前节点为空。
/* 前序遍历 */ void PreOrderMorris(Node * root) { Node * cur = root; Node * pre = nullptr; while (cur) { if (cur->left == nullptr) //(1) { cout << cur->data << " "; cur = cur->right; } else { pre = cur->left; while (pre->right && pre->right != cur) //(2),找到cur的前驱节点 pre = pre->right; if (pre->right == nullptr) //(2.1),cur未被访问,将cur节点做为其前驱节点的右孩子 { cout << cur->data << " "; pre->right = cur; cur = cur->left; } else //(2.2),cur已被访问,恢复树的原有结构,更改right指针 { pre->right = nullptr; cur = cur->right; } } } }
再来看中序遍历,和前序遍历相比只改动一句代码,步骤以下:
(1)若是当前节点的左孩子为空,则输出当前节点并将其右孩子做为当前节点;
(2)若是当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点;
(2.1)若是前驱节点的右孩子为空,将它的右孩子设置为当前节点,当前节点更新为当前节点的左孩子;
(2.2)若是前驱节点的右孩子为当前节点,将它的右孩子从新设为空,输出当前节点,当前节点更新为当前节点的右孩子;
(3)重复以上(1)和(2),直到当前节点为空。
/* 中序遍历 */ void InOrderMorris(Node * root) { Node * cur = root; Node * pre = nullptr; while (cur) { if (cur->left == nullptr) //(1) { cout << cur->data << " "; cur = cur->right; } else { pre = cur->left; while (pre->right && pre->right != cur) //(2),找到cur的前驱节点 pre = pre->right; if (pre->right == nullptr) //(2.1),cur未被访问,将cur节点做为其前驱节点的右孩子 { pre->right = cur; cur = cur->left; } else //(2.2),cur已被访问,恢复树的原有结构,更改right指针 { cout << cur->data << " "; pre->right = nullptr; cur = cur->right; } } } }
最后看下后序遍历,后续遍历有点复杂,须要创建一个虚假根节点dummy,令其左孩子是root。而且还须要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。
步骤以下:
(1)若是当前节点的左孩子为空,则将其右孩子做为当前节点。
(2)若是当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
(2.1)若是前驱节点的右孩子为空,将它的右孩子设置为当前节点,当前节点更新为当前节点的左孩子;
(2.2)若是前驱节点的右孩子为当前节点,将它的右孩子从新设为空,倒序输出从当前节点的左孩子到该前驱节点这条路径上的全部节点,当前节点更新为当前节点的右孩子;
(3)重复以上(1)和(2),直到当前节点为空。
struct Node { int data; Node * left; Node * right; Node(int data_, Node * left_, Node * right_) { data = data_; left = left_; right = right_; } }; void ReversePrint(Node * from, Node * to) { if (from == to) { cout << from->data << " "; return; } ReversePrint(from->right, to); cout << from->data << " "; } void PostOrderMorris(Node * root) { Node * dummy = new Node(-1, root, nullptr); //一个虚假根节点 Node * cur = dummy; Node * pre = nullptr; while (cur) { if (cur->left == nullptr) //(1) cur = cur->right; else { pre = cur->left; while (pre->right && pre->right != cur) //(2),找到cur的前驱节点 pre = pre->right; if (pre->right == nullptr) //(2.1),cur未被访问,将cur节点做为其前驱节点的右孩子 { pre->right = cur; cur = cur->left; } else //(2.2),cur已被访问,恢复树的原有结构,更改right指针 { pre->right = nullptr; ReversePrint(cur->left, pre); cur = cur->right; } } } }
dummy用的很是巧妙,建议读者配合上面的图模拟下算法流程。
前序:[1 2 4 7 3 5 8 9 6]
中序:[4 7 2 1 8 5 9 3 6]
后序:[7 4 2 8 9 5 6 3 1]
以上式为例,步骤以下:
第一步:根据前序可知根节点为1;
第二步:根据中序可知4 7 2为根节点1的左子树和8 5 9 3 6为根节点1的右子树;
第三步:递归实现,把4 7 2当作新的一棵树和8 5 9 3 6也当作新的一棵树;
第四步:在递归的过程当中输出后序。
/* 前序遍历和中序遍历结果以长度为n的数组存储,pos1为前序数组下标,pos2为后序下标 */ int pre_order_arry[n]; int in_order_arry[n]; void PrintPostOrder(int pos1, int pos2, int n) { if (n == 1) { cout << pre_order_arry[pos1]; return; } if (n == 0) return; int i = 0; for (; pre_order_arry[pos1] != in_order_arry[pos2 + i]; i++); PrintPostOrder(pos1 + 1, pos2, i); PrintPostOrder(pos1 + i + 1, pos2 + i + 1, n - i - 1); cout << pre_order_arry[pos1]; }
固然咱们也能够根据前序和中序构造出二叉树,进而求出后序。
/* 该函数返回二叉树的根节点 */ Node * Create(int pos1, int pos2, int n) { Node * p = nullptr; for (int i = 0; i < n; i++) { if (pre_order_arry[pos1] == in_order_arry[pos2]) { p = new Node(pre_order_arry[pos1]); p->left = Create(pos1 + 1, pos2, i); p->right = Create(pos1 + i + 1, pos2 + i + 1, n - i - 1); return p; } } return p; }
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层全部的结点都连续集中在最左边,这就是彻底二叉树(Complete Binary Tree)。以下图:
首先若一个节点只有右孩子,确定不是彻底二叉树;其次若只有左孩子或没有孩子,那么对于一个高度为h的彻底二叉树,当前节点的高度确定是h-1,也就是高度h的全部节点都没有孩子,不然不是彻底二叉树,所以设置flag标记当前节点是否是到了h-1高度。
bool IsCBT(Node * node) { bool flag = false; queue<Node*> Q; Q.push(node); while (!Q.empty()) { Node * p = Q.front(); Q.pop(); if (flag) { if (p->left || p->right) return false; } else { if (p->left && p->right) { Q.push(p->left); Q.push(p->right); } else if (p->right) //只有右节点 return false; else if (p->left) //只有左节点 { Q.push(p->left); flag = true; } else //没有节点 flag = true; } } return true; }
在后续遍历获得的序列中,最后一个元素为树的根结点。从头开始扫描这个序列,比根结点小的元素都应该位于序列的左半部分;从第一个大于跟结点开始到跟结点前面的一个元素为止,全部元素都应该大于跟结点,由于这部分元素对应的是树的右子树。根据这样的划分,把序列划分为左右两部分,咱们递归地确认序列的左、右两部分是否是都是二元查找树。
int array[n]; //长度为n的序列,begin和end遵循的是左闭右闭原则 bool IsSequenceOfBST(int begin, int end) { if (end - begin <= 0) return true; int root_data = array[end]; //数组尾元素为根节点 int i = begin; for (; array[i] < root_data; i++); //取得左子树 int j = i; for (; j < end; j++) if (array[j] < root_data) //此时右子树应该都大于根节点;若存在小于的, return false; return IsSequenceOfBST(begin, i - 1) && IsSequenceOfBST(i, end - 1); //左右子树是否都知足 }
一棵二叉查找树的中序遍历序列,正好是升序序列。
若是节点中有指向父亲节点的指针(假如根节点的父节点为nullptr),则:
(1)若是当前节点有右孩子,则后继节点为这个右孩子的最左孩子;
(2)若是当前节点没有右孩子;
(2.1)当前节点为根节点,返回nullptr;
(2.2)当前节点只是个普通节点,也就是存在父节点;
(2.2.1)当前节点是父亲节点的左孩子,则父亲节点就是后继节点;
(2.2.2)当前节点是父亲节点的右孩子,沿着父亲节点往上走,直到n-1代祖先是n代祖先的左孩子,则后继为n代祖先)或遍历到根节点也没找到符合的,则当前节点就是中序遍历的最后一个节点,返回nullptr。
/* 求后继节点 */ Node * Increment(Node * node) { if (node->right) //(1) { node = node->right; while (node->left) node = node->left; return node; } else //(2) { if (node->parent == nullptr) //(2.1) return nullptr; Node * p = node->parent; //(2.2) if (p->left == node) //(2.2.1) return p; else //(2.2.2) { while (p->right == node) { node = p; p = p->parent; if (p == nullptr) return nullptr; } return p; } } }
仔细观察上述代码,总以为有点啰嗦。好比,过多的return,(2)的层次太多。综合考虑全部状况,改进代码以下:
Node * Increment(Node * node) { if (node->right) { node = node->right; while (node->left) node = node->left; } else { Node * p = node->parent; while (p && p->right == node) { node = p; p = p->parent; } node = p; } return node; }
上述的代码是基于节点有parent指针的,若题意要求没有parent呢?网上也有人给出了答案,我的以为没有什么价值,有兴趣的朋友能够到这里查看。
而求前驱节点的话,只需把上述代码的left与right互调便可,很简单。
二分查找树的中序遍历即为升序排列,问题就在于如何在遍历的时候更改指针的指向。一种简单的方法时,遍历二分查找树,将遍历的结果放在一个数组中,以后再把该数组转化为双链表。若是题目要求只能使用$O(1)$内存,则只能在遍历的同时构建双链表,即进行指针的替换。
咱们须要用递归的方法来解决,假定每一个递归调用都会返回构建好的双链表,可把问题分解为左右两个子树。因为左右子树都已是有序的,当前节点做为中间的一个节点,把左右子树获得的链表链接起来便可。
/* 合并两个a,b两个循环双向链表 */ Node * Append(Node * a, Node * b) { if (a == nullptr) return b; if (b == nullptr) return a; //分别获得两个链表的最后一个元素 Node * a_last = a->left; Node * b_last = b->left; //将两个链表头尾相连 a_last->right = b; b->left = a_last; a->left = b_last; b_last->right = a; return a; } /* 递归的解决二叉树转换为双链表 */ Node * TreeToList(Node * node) { if (node == nullptr) return nullptr; //递归解决子树 Node * left_list = TreeToList(node->left); Node * right_list = TreeToList(node->right); //把根节点转换为一个节点的双链表。方便后面的链表合并 node->left = node; node->right = node; //合并以后即为升序排列 left_list = Append(left_list, node); left_list = Append(left_list, right_list); return left_list; }
咱们能够采用自顶向下的方法。先找到中间节点做为根节点,而后递归左右两部分。全部咱们须要先找到中间节点,对于单链表来讲,必需要遍历一边,可使用快慢指针加快查找速度。
struct TreeNode { int data; TreeNode* left; TreeNode* right; TreeNode(int data_) { data = data_; left = right = nullptr; } }; struct ListNode { int data; ListNode* next; ListNode(int data_) { data = data_; next = nullptr; } }; TreeNode * SortedListToBST(ListNode * list_node) { if (!list_node) return nullptr; if (!list_node->next) return (new TreeNode(list_node->data)); //用快慢指针找到中间节点 ListNode * pre_slow = nullptr; //记录慢指针的前一个节点 ListNode * slow = list_node; //慢指针 ListNode * fast = list_node; //快指针 while (fast && fast->next) { pre_slow = slow; slow = slow->next; fast = fast->next->next; } TreeNode * mid = new TreeNode(slow->data); //分别递归左右两部分 if (pre_slow) { pre_slow->next = nullptr; mid->left = SortedListToBST(list_node); } mid->right = SortedListToBST(slow->next); return mid; }
由$f(n)=2f(\frac n2)+\frac n2$得,因此上述算法的时间复杂度为$O(nlogn)$。
不妨换个思路,采用自底向上的方法:
TreeNode * SortedListToBST(ListNode *& list, int start, int end) { if (start > end) return nullptr; int mid = start + (end - start) / 2; TreeNode * left_child = SortedListToBST(list, start, mid - 1); //注意此处传入的是引用 TreeNode * parent = new TreeNode(list->data); parent->left = left_child; list = list->next; parent->right = SortedListToBST(list, mid + 1, end); return parent; } TreeNode * sortedListToBST(ListNode * node) { int n = 0; ListNode * p = node; while (p) { n++; p = p->next; } return SortedListToBST(node, 0, n - 1); }
如此,时间复杂度降为$O(n)$。