目录
中序遍历:(morris遍历)空间复杂度O(1),非递归,不用栈
二叉树
相信各位小伙伴能进来,就已经知道二叉树是什么,这里就不一一描述了.
下面给出二叉树结点:
class Node {
public:
int data;
Node*left;
Node*right;
};
二叉树的递归遍历
在递归遍历一颗二叉树的任意非空节点时,我们会先后调用函数进入左子树和右子树,在这个过程中,我们有三次处理数据的机会,即进入左子树之前一次,出左子树后/进右子树之前一次,出右子树之后一次,根据处理时机的不同,有:先序遍历(先处理根结点数据->根左右),中序遍历(在左右子树中间处理根结点->左根右),后序遍历(最后处理根结点->左右根);
如图先序遍历:124536
中序遍历:425136
后序遍历:452631
有了以上解释,下面三种简单的递归遍历就不需要多说了
先序递归遍历
void PreOrderTreeWalk(Node* root) {
if (root == nullptr)return;
cout << root->data << " ";
PreOrderTreeWalk(root->left);
PreOrderTreeWalk(root->right);
}
中序递归遍历
void InOrderTreeWalk(Node* root) {
if (root == nullptr)return;
InOrderTreeWalk(root->left);
cout << root->data << " ";
InOrderTreeWalk(root->right);
}
后序递归遍历
void PostOrderTreeWalk(Node* root) {
if (root == nullptr)return;
PostOrderTreeWalk(root->left);
cout << root->data << " ";
PostOrderTreeWalk(root->right);
}
二叉树的非递归遍历
先序遍历使用栈结构(不知道栈的评论区留言,下次可以出栈)
由于栈是先进后出结构,先序遍历是根左右形式,我们可以考虑先处理根节点,再将右树根压入栈中待处理(下面代码push的根节点,但pop后会让它指向右子树,可以避免push空节点,也可以在push前判断右子树是否为空),去处理左树根,以根节点root为例:先push右根,处理左根,左根有右树时会继续push,当处理到无左节点时开始pop,作为新根开始处理;上面过程模拟了递归调用,我个人通俗的理解是,处理根,存右树,走左树
void PreOrderTreeWalkWithStack(Node* root) {
//建栈
stack<Node*>st;
Node*p = root;
while (p != nullptr || !st.empty()) {
//当p空时栈不空,栈空时p不空
if (p != nullptr) {
cout << p->data << " ";
st.push(p);
p = p->left;
}
else {
p = st.top();
st.pop();
p = p->right;
}
}
}
中序遍历使用栈结构
这个...中序遍历和先序遍历一样,我就改了两行代码,因为中序遍历是左根右,所以直接压根结点,处理左子树,处理完后pop,接着处理根结点,然后根结点右指,处理右子树;
void InOrderTreeWalkWithStack(Node* root) {
//建栈
stack<Node*>st;
Node*p = root;
while (p != nullptr || !st.empty()) {
//当p空时栈不空,栈空时p不空
if (p != nullptr) {
st.push(p);
p = p->left;
}
else {
p = st.top();
st.pop();
cout << p->data << " ";
p = p->right;
}
}
}
后序遍历使用栈结构
后序遍历非递归栈有一点难,我们找一种简单的方法吧,前面我们提到过,一个节点可以有三次处理的机会,后序遍历就是在第三次时处理的,我们只需要找一种简单的方法判断是第几次遇见该结点,就可以实现后序遍历
/*
改变结点结构,这个有点难
每次visited++,判断visited%3+1
class Node {
public:
int data;
Node*left;
Node*right;
int visited = 0;
};
*/
/*
哈希表这么好用,不会有人不会吧
map<Node*, int>m;
*/
//建新结点,复制原树
class pNode {
public:
int data;
pNode*left;
pNode*right;
int visited = 0;
};
//利用了递归先序遍历
pNode* CopyTree(Node* root) {
if (root == nullptr)return nullptr;
pNode*p = new pNode;
p->data = root->data;
p->left = CopyTree(root->left);
p->right = CopyTree(root->right);
return p;
}
void PostOrderTreeWalkWithStack(pNode*root) {
stack<pNode*>st;
pNode*p = root;
while (p != nullptr || !st.empty()) {
//当p空时栈不空,栈空时p不空
if (p != nullptr) {
//第一次
p->visited++;
st.push(p);
p = p->left;
}
else {
p = st.top();
st.pop();
//这里是第二次
if (p->visited % 3 + 1 == 2) {
p->visited++;
st.push(p);
p = p->right;
}
//第三次
else {
//visited++三次,不妨碍下次使用
p->visited++;
cout << p->data << " ";
//该结点处理时子树已经处理完了,需要置空
p = nullptr;
}
}
}
}
上述代码时间复杂度O(n),空间复杂度看起来也不低
下面开始介绍不用栈结构和递归实现的二叉树遍历
中序遍历:(morris遍历)空间复杂度O(1),非递归,不用栈
空间复杂度O(1)难点在于,遍历子结点时如何返回父节点,由于二叉树有许多叶结点,他们的左右指针都是空,我们可以利用这些指针返回父节点;
步骤如下:
1.如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
2.如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
void InOrderTreeWalkWithoutStack(Node* root) {
Node*cur = root;
Node*pre = nullptr;
while (cur!=nullptr) {
if (cur->left == nullptr) {
//cur没有左子树,只会停留一次
cout << cur->data << " ";
cur = cur->right;
}
else {
pre = cur->left;
//找到cur左树最右节点
while (pre->right != nullptr&&pre->right != cur) {
pre = pre->right;
}
//第一次停留,储存父结点,cur处理左边
if (pre->right == nullptr) {
pre->right = cur;
cur = cur->left;
}
//第二次停留,恢复叶结点,左边处理完成,cur处理右边
else {
pre->right = nullptr;
cout << cur->data << " ";
cur = cur->right;
}
}
}
}
先序遍历:空间复杂度O(1),非递归,不用栈
先序和中序只有一行不同,先序是第一次到达时处理,中序是第二次
void PreOrderTreeWalkWithoutStack(Node* root) {
Node*cur = root;
Node*pre = nullptr;
while (cur != nullptr) {
if (cur->left == nullptr) {
//cur没有左子树,只会停留一次
cout << cur->data << " ";
cur = cur->right;
}
else {
pre = cur->left;
//找到左树最右节点
while (pre->right != nullptr&&pre->right != cur) {
pre = pre->right;
}
//第一次停留,储存父结点,处理左边
if (pre->right == nullptr) {
pre->right = cur;
cout << cur->data << " ";
cur = cur->left;
}
//第二次停留,恢复叶结点,左边处理完成,处理右边
else {
pre->right = nullptr;
cur = cur->right;
}
}
}
}
后序遍历:空间复杂度O(1),非递归,不用栈
如图,对于后序遍历,可以看成--逆序打印每个结点左树右边界,最后逆序打印大树右边界
如果用递归
void PrintReverse(Node*root) {
if (root == nullptr) return;
PrintReverse(root->right);
cout << root->data << " ";
}
但我们说了不用递归,栈.所以我们将它看成链表,先反转,然后处理,处理后再反转
void Reverse(Node* from, Node* to) {
if (from == to)return;
Node*pre = from->right;
Node*cur = from;
Node*ptr = nullptr;
while (cur != to) {
ptr = pre->right;
pre->right = cur;
cur = pre;
pre = ptr;
}
}
void PrintReverse1(Node* from, Node* to) {
Reverse(from, to);
Node*p = to;
while (p != from) {
cout << p->data << " ";
p = p->right;
}
cout << p->data << " ";
Reverse(to, from);
}
//使用了dump,这样就不用单独打印大树右边界了
void PostOrderTreeWalkWithoutStack(Node* root) {
Node dump;
dump.right = nullptr;
dump.data = 0;
dump.left = root;
Node*cur = &dump;
Node*pre = nullptr;
while (cur != nullptr) {
if (cur->left == nullptr) {
cur = cur->right;
}
else {
pre = cur->left;
//找到左树最右节点
while (pre->right != nullptr&&pre->right != cur) {
pre = pre->right;
}
//第一次停留,储存父结点,处理左边
if (pre->right == nullptr) {
pre->right = cur;
cur = cur->left;
}
//第二次停留,恢复叶结点,左边处理完成,处理右边
else {
PrintReverse1(cur->left, pre);
pre->right = nullptr;
cur = cur->right;
}
}
}
}
层序遍历
上图层序遍历结果为:1 2 3 4 5 6
我们可以知道这是从上到下从左到右输出的
有一种比较好懂的层序遍历,先用深度搜索找最大深度d,然后循环1-d,递归先序遍历记录当前层,相等就输出
void PreOrderWalk(Node*root, int i, int j) {
if (root == nullptr)return;
if (i == j) {
cout << root->data << " ";
return;
}
PreOrderWalk(root->left, i, j + 1);
PreOrderWalk(root->right, i, j + 1);
}
int DeepSearch(Node*root, int i) {
if(root==nullptr)return i;
return max(DeepSearch(root->left, i + 1), DeepSearch(root->right, i + 1));
}
void LevelOrderTreeWalk(Node*root) {
int d = DeepSearch(root, 0);
for (int i = 0; i < d; i++) {
PreOrderWalk(root, i, 0);
}
}
使用先序遍历保证了每层之间元素的相对次序,这种方法时间复杂度较高
层序遍历:使用队列辅助(不知道队列的也可以留言哈)
队列是先入先出的结构,这个是真简单,执行以下步骤即可
首先,根结点 1 入队;
根结点 1 出队,出队的同时,将左孩子 2 和右孩子 3 分别入队;
队头结点 2 出队,出队的同时,将结点 2 的左孩子 4 和右孩子 5 依次入队;
队头结点 3 出队,出队的同时,将结点 3 的左孩子 6 和右孩子 7 依次入队;
不断地循环,直至队列内为空。
从左到右使子结点入队,可以保证下一层的相对次序,可以看成从第一层开始相对次序就是正确的
void LevelOrderTreeWalkWithQueue(Node*root) {
queue<Node*>qu;
qu.push(root);
Node*p = nullptr;
while (!qu.empty()) {
p = qu.front();
qu.pop();
cout << p->data << " ";
if (p->left != nullptr) {
qu.push(p->left);
}
if (p->right != nullptr) {
qu.push(p->right);
}
}
}
如果你想知道哪些数据是哪一层的,可以从第一层开始,记录当前队尾元素,每次处理到该元素时,说明到达该层结尾了,处理结束后刷新记录的队尾,重复上述操作即可.
如果有问题可以评论区,大家一起讨论