二叉树的操作(递归和非递归遍历,树深度,结点个数等)

二叉树的操作(递归和非递归遍历,树深度,结点个数等)

1. node structure

struct Node {
public:
	char data = 0;
	Node* leftChild = NULL;
	Node* rightChild = NULL;
};

在这里插入图片描述

2. 遍历 traverse

2.1. preOrder

规则:

	if currNode == null
		return;
	else
		visit currNode
		visit leftChild
		visit rightChild
2.1.1. preOrder, recursion
void Node::preOrderTraverseRecursion(Node* root) {
	if (root == NULL) return;
	cout << root->data << ", ";
	preOrderTraverseRecursion(root->leftChild);
	preOrderTraverseRecursion(root->rightChild);
}
2.1.2. preOrder, stack,

思路:

  1. 准备工作
    设立指针p, 指向要处理的node, 起始时p指向tree root 这个 node, 因为你总得设定一个访问的其实点, 而对于一个tree, 这个起始点疑问就是这个tree的root
  2. 怎么遍历tree?
    2.1 需要一个loop. 当然可以通过recursion, 但任何recursion也都可以通过loop实现. 本方法, 即通过loop实现, 所以, 需要一个loop.
    2.2 还需要一个stack. 原因就是stack具有first in last out特性. tree的traverse, 都分3个steps, 或者根左右(preOrder), 或者左根右(inOrder), 或者左右根(postOrder). 如果程序仅有一根指针p指向当前要处理的node, 那么当这个node的leftChild处理完毕后, 需要有方法能使指针p指向这个node的rightChild, 也就是说, 在这个node的leftChild处理完毕后, 需要有方法能得到这个node, 再通过这个node找到它的rightChild, 从而也就实现了使p指向这个node的rightChild的目标. 至此, 问题转化为如何在一个node的leftChild处理完毕后, 再得到这个node. 要解决这个问题, 就需要将curr node保存在一个地方. 那么保存在哪里呢? 顺序表? 链表? stack? queue? 显然, stack最为合适, 原因就是stack的FILO特性, 使得stack可以先将curr node封存(push)在stack之中, 待合适的时机, 再解封(pop)出来.
    2.3 原则上, recursion都可以通过stack来实现. 事实上, recursion也正是利用stack的FILO特性, 完成tree的traverse.
  3. loop用while还是for?
    没有确定循环次数的loop, 当然用while loop
  4. while的condition如何设定?
    currNode != NULL || Stack.size != NULL
    如果curr node为null, 则按照preOrder的规则, 可以考虑结束遍历(循环), 但并不必然结束遍历, 因为还要看Stack是否为空. Stack不为空, 说明还有node待处理, 所以不能退出
    如果Stack的size为 0, 则可以
  5. 访问curr node.
    进入循环后, 如果p!=null,说明当前node非空, 则按照preOrder的遍历规则, 先输出curr node的data
    One more thing, 在访问完curr node后, 需要将curr node封存在stack中, 即stack.push( p ), 以待将来解封后用来访问curr node的rightChild, 否则rightChild将不可得
  6. 访问curr node 的 leftChild. 第1个问题, when开始访问curr node 的leftChild? 答案是访问完curr node后. 什么时候是访问完curr node呢? 答案是print了curr node后. 第2个问题, how访问curr node的leftChild? 那么你得有curr node, 而p正是curr node. 所以令p = p->leftChild, 即p指向curr node的leftChild, 再令循环continue, 这样下个loop里, 就开始访问curr node的leftChild.
  7. 访问curr node 的 rightChild. 第1个问题, when开始访问curr node 的rightChild? 答案是访问完curr node 的leftChild后, 但这个答案毫无意义, 因为计算机不会知道什么叫curr node的leftChild已经访问完毕, 这条标准毫无可操作性. 我们必须给出一个清晰的可操作性的时刻, 告诉计算机此时leftChild已经访问完毕. 只要这一讯号出现, 即代表leftChild已访问完毕, stack即可出栈curr node, 即可开展对rightChild的访问. 所以一切的问题的核心是, what是这个讯号?

第2个问题, 那么when是访问完curr node的leftChild了呢? 答案就是stack中的curr node出栈之时, 这是因为, 只要一个node出栈, 就意味着这个node的leftChild已经处理完毕, 否则这个node不会出栈而是被它的leftChild压在栈中. 因此, 只要一个node出栈了, 那么就可以开始访问这个node的rightChild了. 随之而来的第3个问题是, when一个node该出栈呢? 答案很明显, 只需按照stack的规则即可, 即在stack中轮到它出栈了, 它自然就可以出栈了, 这也是为什么使用了stack而不是其他的顺序表或者链式表的原因, 因为解封的时间可直接遵循stack的规则.

是它的leftChild访问完毕. 随之而来的第3个问题是, 什么时候一个node的leftChild访问完毕了呢? 答案是当一个node为null时, 即说明该node的parent的leftChild或者rightChild已访问完毕. 第4个问题, 如何访问rightChild? 令p指向这个出栈的node的rightChild了, 即p = p->rightChild, 再令循环continue, 这样下个loop里, 就开始访问curr node的rightChild.
9.将一个node保存在stack中的意义是什么? 1)代表这个node的rightChild有待访问, 2)将来可以通过出栈这个node, 来找到它的待访问的rightChild.
9. stack中的node, 代表什么? 代表 1)该node的leftChild, 尚未访问完毕, 2)该node的rightChild, 有待访问
10. 当前出栈的node, 代表什么? 代表1)该node的leftChild, 已访问完毕, 2)该node的rightChild, 应当即刻展开访问
11. 入栈时机: 访问每个非null的node时, 都将其入栈
12. 出栈时机: 每当curr node为null时, 都出栈一个node
13. 一个node为null, 意味着什么? 意味着, 1)某条路径, 已经走到了尽头, 2)此时, 或者curr node的parent的leftChild已经访问完毕(如图中的node a, c, d, f), 或者curr node的某个ancestor的leftChild已经访问完毕(如图中的node b代表node 2的leftChild已访问完毕, node g代表node 4的leftChild已访问完毕, ), 或者整棵tree已经访问完毕(如图中的node e)

1)如果curr node是个leftChild, 则代表着curr node的parent的leftChild已经访问完毕. 接下来需要做的是, 开展对parent的rightChild的访问. 如何找到parent进而找到parent的rightChild? 由于此刻parent一定存在于stack的最上层, 所以直接stack.pop()即可得parent

2)如果curr node是个rightChild, 则代表着, 某个curr node的ancestor node(parent属于ancestor的子集)已经访问完毕. 这个ancestor node, 可能是curr node的parent, 可能是parent的parent, 最极端的情况下, 可能是整棵树的root(此时, 意味着整棵tree已访问完毕). 接下来需要做的是, 开展对parent的rightChild的访问

(1)以curr node的parent为root的tree已经整体访问完毕. 如图, 因node b为null,故代表以node b的parent 3为root的tree已访问完毕; 因node g为null, 故代表以node g的parent 7为root的tree已访问完毕; 因node e为null, 故代表以node e的parent 6为root的tree, 已访问完毕; 因node h为null, 故代表以node h的parent 1为root的tree, 已访问完毕.

此时要做的是, 结束本层的访问, 跳至下一个要处理的任务. 但哪个是下一个要处理的任务呢? 事实上, 它保存在stack中, 因为stack中的node, 保存的就是待处理的mission, 他们的leftChild已经开始访问, rightChild尚待访问. 所以这些stack 中的node的mission, 就是有待去访问这些node的rightChild. 当stack中的node为0时, 意味着没有任何node的rightChild有待访问, 意即整颗tree已访问完毕.

进行访问. 而每一次都使用同样的规则进行访问. 如何跳至上层进行访问呢?

(2)可能, 仅仅是可能, 以curr node的某个或某几个除parent外的ancestor(事实上parent属于ancestor的子集)为root的tree已经整体访问完毕. 如图, node g为null, 代表以node g的ancestor 5为root的tree, 已访问完毕; node e为null, 代表以node e的ancestor 4, 2为root的tree, 都已访问完毕.

此时要做的是, 找到业已访问完毕的层级最高的ancestor node, 以这个node为curr node,

p作为指针, 指向要处理的node

当一个node !=null , 这是mission接收、分配、部分执行阶段,对应于recursion中的if(root != null) {visit root; visit leftChild; visit rightChild} 。mission分3个steps, 即依次访问根、左、右。 第1个step, 接收mission时即可执行,即直接print node.data即可。 第2个step, 接收mission时只能通过调整p的指向进行分配,在下个loop里完成, 即令 p==p.leftChild(分配mission), 再continue(完成mission)。 第3个step, 接收mission时只能通过压栈入stack进行分配,在无法进行, 需要等到2nd step完成后, 再进行, 所以, 需要将3rd step保存在stack中, 将来需要执行的时候才拿得出来执行. 所以stack中保存的每个node都意味着有待对这个node的rightChild进行访问. 所以, 所谓接收,指接收p指向的node为当前mission的对象。 所谓部分执行, 指只执行mission的1st step。 所谓分配, 指调整p指向leftChild和压栈rightChild入stack

当一个node == null, 这是mission的处理阶段,对应于recursion中的if(root is null) return; 只不过是,recursion中return后,自动返回上层recursion继续执行,也就是说recursion里的p是由recursion自动改变的,在哪层执行p就指向哪层, 但使用loop的话,p的指向需要由我们手动调整。所以本阶段,需要调整p的指向。 最简单的做法是, 在入栈的阶段, 入栈curr node,这样在调整p的指向时,令p指向这个出栈的node的rightChild,就达到了和recursion同样的效果。再进一步,其实也可以在入栈时,就入栈curr node的rightChild,这样在出栈时,就可令p直接指向出栈的node。

无论如何, 当node==null时,就表示一个2nd step走到了尽头(当curr node为leftChild时),或者一个3rd step走到了尽头(当curr node为rightChild时)。那么此刻需要做的, 就是去step存储器的stack中,去出栈一个step,并开始这个step。

void Node::preOrderTraverseByStack_3(Node* root) {
	Stack s;
	Node* p = root; // p指向要访问的node. 起始时, 指向binary tree的root
	while (s.size != 0 || p != NULL) { //退出循环的条件, 1)stack为空 || 2) 
		// 如果p!=null, 说明p, 即curr root, 也是个root, 因此
		if (p != NULL) { 
			cout << p->data << ", "; // 打印curr root(因为preOrder总是先输出root), 
			s.push(p); //curr root入栈, 以备在leftChild访问完后, 可以通过出栈curr root, 找到curr root的rightChild 
			p = p->leftChild;// p指向curr root的leftChild, 即开始访问left child
		}
		else {	// 如果p==null, 1)说明p是leaf的child, 那么就出栈cuur p的parent, 并将指针p指向出栈的node的righthild, 即开始访问right child
			p = s.pop();
			p = p->rightChild;
		}
	}
}

后序遍历
后序比之于preOrder和inOrder的困难之处在于, 需要在访问完一个node的leftChild与rightChild后, 能再次得到该node(所以需要压栈该node), 并且, 对因出栈而获得的这个node, 需要能判断出该node的leftChild与rightChild是否都已经被访问完, 从而该print 这个node.
这个困难, 在preOrder和inOrder中, 是不存在的. 对preOrder而言, 任何node都可在首次访问时直接print该node. 对inOrder而言, 任何node都可在首次出栈即print. 但对于postOrder, 因为需要判断是第

//设置lastPrinted和lastRoot结点
void Node::postOrderTraverseByStack(Node* root) {
	Stack s;//栈中的即为待执行的mission, 即待访问的node
	Node* cur = root; // 指向此刻或即将要访问的node. 初始化为root而不是NULL,是为了避免当node的leftChild或rightChild为NULL时, cur->rightChild==lastPrinted 或 cur->leftChild == lastPrinted 结果为true, 从而误判cur的leftChild和rightChild都已访问过而print cur, 因为事实上应当是继续去访问cur的非空的child而非print cur
	Node* lastPrinted = root; //指向上一个被print的node, 用来解决当出栈了一个node时, 判断该node的左右孩子是否已经被访问过
	Node* lastRoot = NULL; //指向上一个以root的身份被访问的node,用来解决当出栈了一个node时,判断该node是否已经以root身份被访问过
	while (1) {
		//如果node非空, 那么或者该node已被访问, 打印该node, 或者该node没被访问, 依次左右根压栈入任务清单
		if (cur != NULL) {
			//判断cur是否曾以root被访问过, 是否左孩子/右孩子是否已被访问. 如果是, 则说明轮到访问cur node了, 直接print之
			if (cur == lastRoot || cur->rightChild == lastPrinted || cur->leftChild == lastPrinted) {
				std::cout << cur->data << ", ";
				lastPrinted = cur;//以lastPrinted记录该被print的nonde
				if (s.size == 0)  return;//下一句要pop(), 所以要先确认stack非空. 如果stack为空,意味着所有任务都已完成, 即可return.
				cur = s.pop(); //从stack中解封新mission
			}
			else {
				lastRoot = cur;//让lastRoot记录下曾以root身份访问过该node
				s.push(cur); //压mission入栈. 因为是postOrder, 所以先压root
				if (cur->rightChild != NULL) s.push(cur->rightChild); //压mission入栈. 因为是postOrder, 所以压完root就压rightChild
				cur = cur->leftChild;//leftChild就不必压栈了, 因为接下来要做的就是访问leftChild, 所以直接令cur=leftChild就好了
			}
		}
		//如果node为空, 则某个mission已经走到了头儿了, 所以要出栈新mission
		else {
			cur = s.pop();
		}
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值