二叉树的各种遍历,二叉树改链表,二叉树复习

总结:

中序遍历能够保持各个节点的左右顺序不变; 前序遍历最先访问的是根节点;后续遍历最后访问的是根节点。局部的来看子树的情况仍然成立。

假若前序的遍历顺序为 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指向链表的最后一个元素



  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值