总结:
中序遍历能够保持各个节点的左右顺序不变; 前序遍历最先访问的是根节点;后续遍历最后访问的是根节点。局部的来看子树的情况仍然成立。
假若前序的遍历顺序为 A,B,C,D,E,F ,中序的遍历顺序为 C,B,A,E,D,F.
1、根据前序遍历顺序,知道 A 是根节点。C,B 在 A 的左边; E,D,F在A的右边。
2、B,C 构成棵子树,先序遍历顺序为 B,C (看整个先序遍历的局部情况),所以肯定B是根节点,再看中序遍历为 C,B ,所以C在B的左边,即C 为 B的左节点。
D,E,F 构成子树。先看先序遍历顺序为 D,E,F ,D最先遍历,所以D 肯定是子树的根节点。注意剩下的E,F 可能构成子树,可能不构成子树(即不能从先序遍历顺序上判断E和F谁是谁的根节点),这个时候看中序遍历顺序 E,D,F,说明E在D的左边,F在D的右边,则只可能E是D的左节点,F是D的右节点。
3、合成一颗完整的二叉树:
同样的方法,对于只有后序和中序,可以一样得到一颗二叉树。注意后序中,无论局部还是整体来看,最后访问的都是根节点,然后结合中序的左右关系作判断。
前序和中序能够决定一颗二叉树;后序和中序也能决定一颗二叉树;但是只有前序和后序,无法决定一颗二叉树。
二叉树的一个节点的结构如下:
struct BinaryTree
{
int m_value;
BinaryTree* left;
BinaryTree* right;
};
二叉树的递归遍历
用递归来表达二叉树的遍历,算法思想最明白:
void pre_order_access(BinaryTree* pRoot)//先序遍历 先、中、后都是相对根节点来说的
{
if (pRoot != nullptr)
{
cout << pRoot->m_value <<" "; //先访问根节点
pre_order_access(pRoot->left); //左子树
pre_order_access(pRoot->right); //右子树
}
}
void mid_order_access(BinaryTree* pRoot) //中续遍历
{
if (pRoot != nullptr)
{ //1、如果在这里定义int i;
mid_order_access(pRoot->left); //先访问左子树
cout << pRoot->m_value <<" "; //访问根节点 //2、在这里给i赋值 ipRoot->m_value
mid_order_access(pRoot->right); //访问右子树 //3、在末尾为i赋值,那么最终返回的i的值将是根节点的值 i
} //4、虽然最内层的i 最先被赋值
}
void post_order_access(BinaryTree* pRoot) //后续遍历
{
if (pRoot != nullptr)
{
post_order_access(pRoot->left); //先访问左子树
post_order_access(pRoot->right); //然后访问右子树
cout << pRoot->m_value <<" "; //最后根节点
}
}
用数组元素来构建一棵二叉树
对于遍历的测试,需要先构建一棵二叉树,一般的,可以用一个数组来存储一个完全二叉树(按层序来存储),对于索引为 i 的节点,其左右孩子节点的索引分别为 2*i + 1 和 2*i + 2;
BinaryTree* creatBinaryTree(int arr[], int i, int n) //递归的来构造一棵完全二叉树
{
BinaryTree* pRoot;
if (i >= n)
return nullptr;
pRoot = (BinaryTree*)malloc(sizeof(BinaryTree));
pRoot->m_value = arr[i];
pRoot->left = creatBinaryTree(arr, 2 * i + 1, n);
pRoot->right = creatBinaryTree(arr, 2 * i + 2, n);
return pRoot;
}
调用时,这样就行
int arr[] = { 8, 4, 10, 2, 6, 9, 11, 1, 3, 5, 7};
BinaryTree* proot = creatBinaryTree(arr, 0, sizeof(arr) / sizeof(int));
对于非完全二叉树,可以将左右节点为空的节点扩展出子节点来,相当于虚节点,节扩展点值为特殊的值,用来标记其父节点的对应左右为空。
BinaryTree* creatPreBinaryTree(int arr[], int& i) //注意这里的第二个参数用来表示索引 必须是个引用类型
{ //表示每层递归使用的是同一个索引变量 不至于混乱
BinaryTree* proot = nullptr;
if (arr[i] == 0)
return nullptr;
proot = (BinaryTree*)malloc(sizeof(BinaryTree));
proot->m_value = arr[i]; //先创建根节点
++i;
proot->left = creatPreBinaryTree(arr, i); //然后创建左子树
++i;
proot->right = creatPreBinaryTree(arr, i); //然后创建右子树
return proot; //实际相当于一个前序遍历的创建
}
调用时
int a[] = { 8, 4, 2, 1,0, 0, 3, 0, 0, 6, 5, 0, 0, 7, 0, 0,10, 9, 0, 0, 11, 0, 0 };
int i = 0;
BinaryTree* root = creatPreBinaryTree(a, i);
层序遍历二叉树
层序遍历借助一个队列即可,从根节点开始入列之后,
当队列不为空,循环的的操作(注意循环的必须保持一致性,每个节点的操作相同):出列,将左右子节点入列即可
void level_access(BinaryTree* pRoot) //分层遍历 相当于广度优先搜索
{
if (pRoot != nullptr)
{
queue<BinaryTree*> queueBinaryNode;
queueBinaryNode.push(pRoot);
BinaryTree* curNode = nullptr;
while (!queueBinaryNode.empty()) //每次访问队列的首元素 同时把其连个孩子放入队列中
{
curNode = queueBinaryNode.front();
cout << curNode->m_value << " ";
queueBinaryNode.pop();
if (curNode->left != nullptr)
queueBinaryNode.push(curNode->left);
if (curNode->right != nullptr)
queueBinaryNode.push(curNode->right);
}
}
}
二叉树的非递归遍历
先序,中序,后续的遍历需要借助栈来实现,既然是栈,每次就只能对栈顶的元素进行操作,也就是每次访问输出都只能是栈顶的元素(当然要满足输出条件),而且必须保持操作的一致性,也就是循环体内对每个节点的处理相同。
前序遍历的非递归遍历:
void pre_deep_access(BinaryTree* pRoot) //深度优先搜索 在这里相当于前序遍历
{
if (nullptr != pRoot)
{
stack<BinaryTree*> stackBinaryTree;
stackBinaryTree.push(pRoot); //根节点入栈
BinaryTree* curNode = nullptr;
while (!stackBinaryTree.empty())
{
curNode = stackBinaryTree.top(); //每次对栈顶元素进行处理
stackBinaryTree.pop();
cout << curNode->m_value <<" ";
if (curNode->right != nullptr)
stackBinaryTree.push(curNode->right); //先右子节点入栈
if (curNode->left != nullptr)
stackBinaryTree.push(curNode->left); //再左子节点入栈 这样保证左子节点先于右子节点访问
}
}
}
中序遍历的非递归遍历:
void mid_deep_access(BinaryTree* pRoot)
{
if (nullptr != pRoot)
{
stack<BinaryTree*> stackBinaryTree;
BinaryTree* curNode = pRoot;
while (!stackBinaryTree.empty() || (curNode != nullptr))
{ //根节点弹出之后 变空了 但是还有右边的子树 curNode用来标记的
while (nullptr != curNode) //沿着左下方向,将节点入栈
{
stackBinaryTree.push(curNode);
curNode = curNode->left;
}
if (!stackBinaryTree.empty())
{
curNode = stackBinaryTree.top();
cout << curNode->m_value << " ";
stackBinaryTree.pop(); //如果将根节点弹出 此时栈为空但是右子树还没遍历所以wile 的循环条件中 需要或上curNode != nullptr
curNode = curNode->right;
}
}
//对于跟节点 最后弹出来 但是右子树还是不为空 所以这里需要用个或运算
}
}
后续遍历逻辑相对麻烦一些,先要沿着左下方向把节点入栈,不过最左下的子节点(左子节点为空),也就是栈顶的节点A,若右子节点非空,需要对右子节点进行沿着左下方向压入,最后返回到栈顶的节点又是A时,由于A的右子树已经遍历过,所以不能再重复,于是引入一个变量,指示当前访问(这里的访问在程序里就是输出节点值)节点的上一个访问的节点,如果栈顶的元素是A,但是上一个访问的节点是 A的右子节点,说明A的右子节点已经访问过,代码如下
void post_deep_access(BinaryTree* pRoot)
{
if (nullptr != pRoot)
{
stack<BinaryTree*> stackBinaryTree;
BinaryTree* curNode = pRoot;
BinaryTree* preNode = nullptr; //指向前一个被访问的节点 也就是输出过的
while (curNode != nullptr || !stackBinaryTree.empty())
{
while (curNode != nullptr)
{
stackBinaryTree.push(curNode);
curNode = curNode->left;
}
curNode = stackBinaryTree.top(); //栈顶节点
if (curNode->right == nullptr || curNode->right == preNode) //栈顶元素可以马上访问的条件 没有右子树 或者右子树刚访问过
{
cout << curNode->m_value << " ";
stackBinaryTree.pop();
preNode = curNode;
curNode = nullptr;
}
else
curNode = curNode->right; //栈顶元素不能马上访问
}
}
}
还有一种使用双栈来进行后序遍历的,将根节点入栈1,然后每次弹出时,压入到栈2当中,且把弹出的节点的左,右子节点压入栈1当中;这样保证最终所有的节点都会入栈2,且栈2中,所有的右子树在左子树的下边。void post_deep_access2stack(BinaryTree* pRoot)
{
if (nullptr != pRoot)
{
stack<BinaryTree*> stackBinaryTree1, stackBinaryTree2;
BinaryTree* curNode = nullptr;
stackBinaryTree1.push(pRoot); //将根节点入栈1
while (!stackBinaryTree1.empty())
{
curNode = stackBinaryTree1.top();
stackBinaryTree1.pop();
stackBinaryTree2.push(curNode);
if (curNode->left != nullptr)
stackBinaryTree1.push(curNode->left);
if (curNode->right != curNode)
stackBinaryTree1.push(curNode->right);
}
while (!stackBinaryTree2.empty())
{
curNode = stackBinaryTree2.top();
stackBinaryTree2.pop();
cout << curNode->m_value << " ";
}
}
}
总结:沿着左下方向把节点入栈的,在循环内压入根节点,且循环判断的条件会多一个。循环外压入根节点的,会在循环里弹栈顶元素,然后压入左,右子节点。
对于队列,每次访问输出的只能队列的首元素;对于栈,每次访问输出的只能是栈顶元素。
将二叉树按中续遍历顺序改成链表
每访问到一个节点cur,将该节点的指向左子节点的指针指向前一个访问的节点 pre,所以这里需要保存下来前一个访问的节点,同时将pre 的指向右节点的指针指向当前节点即可,即cur->left = pre. pre->right = cur. 代码如下:
void binary2list(BinaryTree* pRoot, BinaryTree*& pre, BinaryTree* &result)
{
if (pRoot != nullptr)
{
//BinaryTree* pHead = nullptr;
binary2list(pRoot->left, pre, result);
cout << pRoot->m_value << " ";
pRoot->left = pre; //返回时赋值
if (nullptr == result)
result = pRoot;
//对于非静态成员 pHead = pRoot; //最外层的递归执行到这里 当前的pRoot是根节点了 此时左节点已经访问完毕
//static BinaryTree* result = pRoot;
if (nullptr != pre)
pre->right = pRoot;
pre = pRoot; //pre最后指向尾节点
//空行这之间的代码第一次执行是左下边的那个节点 也就是中序遍历访问的第一个节点才执行
//而最外层的 binary2list() 执行到这里时,左子树已经遍历玩,所以想要使用一个局部变量pHead保存pRoot
//结果保存下来的这个pRoot实际指向的是根节点 而不是第一个访问的节点
binary2list(pRoot->right, pre, result);
}
}
这里需要说明的是在不能在函数内部使用 static BinaryTree* pre = nullptr; 静态变量在静态存储区,只会赋值一次,也就是第一次调用该函数进入该函数的第一层会赋值,这样虽然每层函数使用的也是同一个pre,不过在外部调用完成之后,由于pre 不再为空,所以第二次在外部调用该函数,会发生错误(链表的头节点的左指针不为空)
该函数的调用如下:
BinaryTree* pTail = nullptr;
BinaryTree* pHead = nullptr;
binary2list(root, pTail, pHead); //在这里 pHead一定要传一个空指针进去 否则会出错
//pHead为指向链表表头 pTail指向链表的最后一个元素