概要
树本身是一种简单化的图 ;
DFS对应前中后序遍历,BFS对应层序遍历
二叉树结构
struct treenode {int val;treenode *left;treenode *right;treenode() : val(0), left(nullptr), right(nullptr) {}treenode(int x) : val(x), left(nullptr), right(nullptr) {}treenode(int x, treenode *left, treenode *right) : val(x), left(left), right(right) {}};
1-中序遍历
二叉树的中序遍历就是首先遍历左子树,然后访问当前节点,最后遍历右子树。对于下面的二叉树,中序遍历结果如下:
直观来看,二叉树的中序遍历就是将节点投影到一条水平的坐标上。如图:
方法一:递归
这是思路最简单的方法,容易想到并且容易实现。递归的终止条件是当前节点是否为空。首先递归调用遍历左子树,然后访问当前节点,最后递归调用右子树。代码如下:
//recursive
class Solution1 {
public:vector<int> inorderTraversal(TreeNode* root) {vector<int> ret;if(root==NULL)return ret;inorderHelper(ret,root);return ret;}
private:void inorderHelper(vector<int>& ret,TreeNode* root){if(root==NULL)return;inorderHelper(ret,root->left);ret.push_back(root->val);inorderHelper(ret,root->right);}
};
- 时间复杂度:O(n),其中 n 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。
- 空间复杂度:O(n)。空间复杂度取决于递归的栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。
方法二:迭代
在迭代方法中,从根节点开始找二叉树的最左节点,将走过的节点保存在一个栈中,找到最左节点后访问,对于每个节点来说,它都是以自己为根的子树的根节点,访问完之后就可以转到右儿子上了。代码如下:
class Solution {
public:vector<int> inorderTraversal(TreeNode* root) {vector<int> res;stack<TreeNode*> stk;while (root != nullptr || !stk.empty()) {while (root != nullptr) {stk.push(root); //要在循环之后执行push(root)root = root->left;}root = stk.top();stk.pop();res.push_back(root->val);root = root->right;}return res;}
};
时间复杂度和空间复杂度同递归。
非递归的空间复杂度和二叉树的深度和高度有关
方法三:Morris 中序遍历
Morris 遍历算法是另一种遍历二叉树的方法,它能将非递归的中序遍历空间复杂度降为 O(1)。
Morris 遍历算法整体步骤如下(假设当前遍历到的节点为 x):
如果 x 无左孩子,先将 x 的值加入答案数组,再访问 x 的右孩子,即 x=x.right。
如果 x 有左孩子,则找到 x 左子树上最右的节点(即左子树中序遍历的最后一个节点,x 在中序遍历中的前驱节点),我们记为 predecessor。根据 predecessor 的右孩子是否为空,进行如下操作。
如果predecessor 的右孩子为空,则将其右孩子指向 x,然后访问 x 的左孩子,即 x=x.left。
如果predecessor 的右孩子不为空,则此时其右孩子指向 x,说明我们已经遍历完 x 的左子树,我们将predecessor 的右孩子置空,将 x 的值加入答案数组,然后访问 x 的右孩子,即 x=x.right。
重复上述操作,直至访问完整棵树。
class Solution {
public:vector<int> inorderTraversal(TreeNode* root) {vector<int> res;TreeNode *predecessor = nullptr;while (root != nullptr) {if (root->left != nullptr) {// predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止predecessor = root->left;while (predecessor->right != nullptr && predecessor->right != root) {predecessor = predecessor->right;}// 让 predecessor 的右指针指向 root,继续遍历左子树if (predecessor->right == nullptr) {predecessor->right = root;root = root->left;}// 说明左子树已经访问完了,我们需要断开链接else {res.push_back(root->val);predecessor->right = nullptr;root = root->right;}}// 如果没有左孩子,则直接访问右孩子else {res.push_back(root->val);root = root->right;}}return res;}
};
- 时间复杂度:O(n),其中 nn 为二叉搜索树的节点个数。Morris 遍历中每个节点会被访问两次,因此总时间复杂度为 O(2n)=O(n)。
- 空间复杂度:O(1)。
2-先序遍历
方法一:递归
调整下访问顺序即可。代码如下:
//recursion
class Solution1 {
public:vector<int> preorderTraversal(TreeNode* root) {vector<int> ret;preHelper(ret,root);return ret;}
private:void preHelper(vector<int>& ret,TreeNode* root){if(root==NULL)return;ret.push_back(root->val);preHelper(ret,root->left);preHelper(ret,root->right);}
};
class Solution:def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:l=[]def dfs(root:Optional[TreeNode])->List[int]:if not root:return Nonel.append(root.val)dfs(root.left)dfs(root.right)return ldfs(root)return l
方法二:迭代
迭代法使用一个栈来保存当前不需要访问的节点。从根节点开始,访问当前节点,按照先右儿子后左儿子的顺序将当前节点的两个儿子压栈。当栈为空时说明遍历完毕。代码如下:
//iterative
class Solution2 {
public:vector<int> preorderTraversal(TreeNode* root) {vector<int> ret;stack<TreeNode*> st;if(root==NULL)return ret;st.push(root);while(!st.empty()){TreeNode *curr=st.top();st.pop();if(curr->right)st.push(curr->right);if(curr->left)st.push(curr->left);ret.push_back(curr->val);}return ret;}
};
class Solution:def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:res=list()stk=[]if not root:return resstk.append(root)while stk:node=stk.pop()if node.right:stk.append(node.right)if node.left:stk.append(node.left)res.append(node.val)return res
- 时间复杂度 O(N) : 其中 N 为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点,占用 O(N) 时间。
- 空间复杂度 O(N) :最差情况下,栈 stack 最多同时存储 (N+1)/2个节点(N最差的情况下只能为奇数),占用 O(N) 额外空间。
方法三:Morris 先序遍历
前序遍历和中序遍历的Morris方法基本一样,不同之处在访问的顺序。代码如下:
//Morris
class Solution3 {
public:vector<int> preorderTraversal(TreeNode* root) {vector<int> ret;if(root==NULL)return ret;TreeNode *curr=root;TreeNode *pre;while(curr){if(curr->left==NULL){ret.push_back(curr->val);curr=curr->right;}else{pre=curr->left;while(pre->right&&pre->right!=curr)pre=pre->right;if(pre->right==NULL){ret.push_back(curr->val);pre->right=curr;curr=curr->left;}else{pre->right=NULL;curr=curr->right;}}}return ret;}
};
class Solution:def preorderTraversal(self, root: TreeNode) -> List[int]:res = list()if not root:return resp1 = rootwhile p1:p2 = p1.leftif p2:while p2.right and p2.right != p1:p2 = p2.rightif not p2.right:res.append(p1.val)p2.right = p1p1 = p1.leftcontinueelse:p2.right = Noneelse:res.append(p1.val)p1 = p1.rightreturn res
3-层序遍历
二叉树的层序遍历:一般基于队列的实现
首先将二叉树的根节点push到队列中。
判断队列不为空就输出队头元素。
判断当前对头节点是否有孩子节点,有则push到队列中。
循环操作,直到队列为空。
方法一:迭代
vector<int> levelOrder(TreeNode* Tree) //层序遍历_队列实现
{vector<int>res;queue < TreeNode* > q;if (Tree != NULL)q.push(Tree); //根节点进队列while (q.empty() == false) //队列不为空{TreeNode* node=q.front();res.push_back(node->val); if (node->left != NULL) //如果有左孩子,入队q.push(node->left); if (node->right != NULL) //如果有右孩子,入队q.push(node->right);q.pop(); //已经遍历过的节点出队列}return res;
}
- 时间复杂度:O(N),N 为二叉树的节点数量,即 BFS 需循环 N 次。
- 空间复杂度:O(N),最差情况下,即当树为平衡二叉树时,最多有 N/2 个树节点同时在 queue 中,使用O(N) 大小的额外空间。
方法二a:分行进行层序遍历
class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector<vector<int>> res;vector<int> level_data;if(root==NULL)return res;queue<TreeNode*> q;q.push(root);q.push(NULL);while(!q.empty()){TreeNode* temp=q.front();q.pop();if(temp){level_data.push_back(temp->val);if(temp->left)q.push(temp->left);if(temp->right)q.push(temp->right);}else{if(!level_data.empty()){q.push(NULL);res.push_back(level_data);level_data.clear();}}}return res;}
};
方法三: leetcode形式
可以归纳出这样的循环不变式:第 i 次迭代前,队列中的所有元素就是第 i 层的所有元素,并且按照从左向右的顺序排列。证明它的三条性质(你也可以把它理解成数学归纳法):
初始化:i=1 的时候,队列里面只有 root,是唯一的层数为 1 的元素,因为只有一个元素,所以也显然满足「从左向右排列」;
保持:如果 i=k 时性质成立,即第 k 轮中出队sk 的元素是第 k层的所有元素,并且顺序从左到右。因为对树进行广度优先搜索的时候由低 k 层的点拓展出的点一定也只能是 k+1 层的点,并且 k+1 层的点只能由第 k 层的点拓展到,所以由这 sk 个点能拓展到下一层所有的 sk+1 个点。又因为队列的先进先出(FIFO)特性,既然第 k 层的点的出队顺序是从左向右,那么第 k+1层也一定是从左向右。至此,我们已经可以通过数学归纳法证明循环不变式的正确性。
终止:因为该循环不变式是正确的,所以按照这个方法迭代之后每次迭代得到的也就是当前层的层次遍历结果。至此,我们证明了算法是正确的。
class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector <vector <int>> ret;if (!root) {return ret;}queue <TreeNode*> q;q.push(root);while (!q.empty()) {int currentLevelSize = q.size();ret.push_back(vector <int> ());for (int i = 1; i <= currentLevelSize; ++i) {auto node = q.front(); q.pop();ret.back().push_back(node->val);if (node->left) q.push(node->left);if (node->right) q.push(node->right);}} return ret;}
};
- 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n),树上所有节点的个数为n。
- 空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为O(n),满二叉树的时候队列元素是最多的。
二叉树的层序遍历II
给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
class Solution {
public:vector<vector<int>> levelOrderBottom(TreeNode* root) {vector<vector<int>>ret;if(!root)return ret;queue<TreeNode*>q;q.push(root);while(!q.empty()){int currentLevelSize=q.size();ret.push_back(vector<int>());for(int i=1;i<=currentLevelSize;++i){auto node=q.front();q.pop();ret.back().push_back(node->val);if(node->left)q.push(node->left);if(node->right)q.push(node->right);}}reverse(ret.begin(),ret.end());return ret;}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
扩展:之字形打印二叉树
利用两个栈
//按之字形打印void Print(TreeNode *root){if (root == NULL)return;stack<TreeNode*>levels[2];int current = 0, next = 1;levels[current].push(root);while (!levels[0].empty() || !levels[1].empty()){TreeNode*node = levels[current].top();levels[current].pop();printf("%d", node->val);if (current == 0){if (node->left != nullptr)levels[next].push(node->left);if (node->right != nullptr)levels[next].push(node->right);}else{if (node->right != nullptr)levels[next].push(node->right);if (node->left != nullptr)levels[next].push(node->left);}if (levels[current].empty()){cout << endl;current = 1 - current;next = 1 - next;}}}
- 时间复杂度:每个点进栈出栈各一次,故渐进时间复杂度为 O(n),树上所有节点的个数为n。
- 空间复杂度:栈中元素的个数不超过 n 个,故渐进空间复杂度为O(n),满二叉树的时候栈的元素是最多的。
二叉树的锯齿形层序遍历
class Solution {
public:vector<vector<int>> zigzagLevelOrder(TreeNode* root) {vector<vector<int>>vec;if(root==NULL)return vec;stack<TreeNode*>levels[2];int current=0,next=1;levels[current].push(root);vec.push_back(vector<int>());while(!levels[current].empty()||!levels[next].empty()){TreeNode *pNode=levels[current].top();levels[current].pop();if(pNode==NULL)continue;vec.back().push_back(pNode->val);if(current==0){if(pNode->left)levels[next].push(pNode->left);if(pNode->right)levels[next].push(pNode->right); }else{if(pNode->right)levels[next].push(pNode->right);if(pNode->left)levels[next].push(pNode->left); }if(levels[current].empty()){vec.push_back(vector<int>());current=1-current;next=1-next;}}vec.pop_back();return vec;}
};
4-后序遍历
方法一:递归
//recursion
class Solution1 {
public:vector<int> postorderTraversal(TreeNode* root) {vector<int> ret;postHelper(ret,root);return ret;}
private:void postHelper(vector<int>& ret,TreeNode* root){if(root==NULL)return;postHelper(ret,root->left);postHelper(ret,root->right);ret.push_back(root->val);}
};
- 时间复杂度:O(N)
- 空间复杂度:O(N)
方法二:迭代
迭代法使用一个栈来保存当前不需要访问的节点。不过,不同于中序遍历与前序遍历,在后序遍历中每一个节点需要一个标志位,来标识当前节点的左右子树是否被访问。因为在后序遍历中,只有一个节点的左右子树被访问后它才能被访问。因此,压入栈中的数据类型需要是一个pair<TreeNode*,int>,其中用1来表示当前节点的左右子树正被访问,当再次访问到此节点时可以访问此节点;用0表示当前节点的左右子树未被访问,再次访问到此节点时需要首先访问此节点的左右子树。
//iteration
class Solution1 {
public:vector<int> postorderTraversal(TreeNode* root) {vector<int> ret;if(root==NULL)return ret;stack<pair<TreeNode*,int>> st;st.push(make_pair(root,0));while(!st.empty()){TreeNode *curr=st.top().first;if(st.top().second==1){ret.push_back(curr->val);st.pop();}else{st.top().second=1;if(curr->right)st.push(make_pair(curr->right,0));if(curr->left)st.push(make_pair(curr->left,0));}}return ret;}
};
- 时间复杂度:O(N),每个节点需访问两次
- 空间复杂度:O(N)
方法三:迭代法
这种方法就是按照根、右、左的顺序访问,然后将结果取反即可。后序遍历的顺序是左、右、根。这种方法就可以在前序遍历的基础上修改即可。代码如下:
class Solution3 {
public:vector<int> postorderTraversal(TreeNode* root) {vector<int> ret;if(root==NULL)return ret;stack<TreeNode*> st;st.push(root);while(!st.empty()){TreeNode *curr=st.top();st.pop();if(curr->left)st.push(curr->left);if(curr->right)st.push(curr->right);ret.push_back(curr->val);}reverse(ret.begin(),ret.end());return ret;}
};
- 时间复杂度:O(N),按照根、右、左的顺序遍历时,每个节点被访问一次,将结果取反时,每个节点也被访问一次,所以每个节点需访问两次
- 空间复杂度:O(N)
Morris方法
后序遍历的Morris方法思路比较难。但整体上还是一样的,对原来的二叉树的修改也是一样的,不同的是访问的顺序。而在后序遍历中,访问时比较麻烦。下面是整个算法的工作过程;
首先建立一个临时节点dump,令其左儿子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。
步骤:
当前节点设置为临时节点dump。
(1)如果当前节点的左儿子为空,则将其右儿子作为当前节点;
(2)如果当前节点的左儿子非空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点;
a) 如果前驱节点的右孩子为空,将它的右儿子设置为当前节点。当前节点更新为当前节点的左儿子;
b) 如果前驱节点的右儿子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左儿子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右儿子;
(3)重复以上(1)(2)直到当前节点为空。
代码如下:
//morris
class Solution4 {
public:vector<int> postorderTraversal(TreeNode* root) {vector<int> ret;TreeNode *dump=new TreeNode(0);dump->left=root;TreeNode *curr=dump;TreeNode *pre;while(curr){if(curr->left==NULL){curr=curr->right;}else{pre=curr->left;while(pre->right&&pre->right!=curr)pre=pre->right;if(pre->right==NULL){pre->right=curr;curr=curr->left;}else{reverseAddNodes(curr->left,pre,ret);pre->right=NULL;curr=curr->right;}}}return ret;}
private:void reverseAddNodes(TreeNode *begin,TreeNode *end,vector<int>& ret){reverseNodes(begin,end);TreeNode *curr=end;while(true){ret.push_back(curr->val);if(curr==begin)break;curr=curr->right;}reverseNodes(end,begin);}void reverseNodes(TreeNode *begin,TreeNode *end){TreeNode *pre=begin;TreeNode *curr=pre->right;TreeNode *post;while(pre!=end){post=curr->right;curr->right=pre;pre=curr;curr=post;}}
};
- 时间复杂度:O(N)
- 空间复杂度:O(1)