初始状态
假设当前从根节点 b
开始,此时栈为空 。
第一步:处理根节点 b
的左子树
- 调用
goAlongLeftBranch
函数,从节点b
开始,因为b
有左子树(节点a
),将b
入栈,此时栈:[b]
;继续沿着左分支,a
没有左子树了,将a
入栈 ,此时栈:[a, b]
。 - 回到主循环,栈非空,执行
x = S.pop()
,a
出栈并访问a
;然后x
指向a
的右子树,a
右子树为空,回到主循环继续判断栈状态。
第二步:处理根节点 b
- 此时栈顶是
b
,执行x = S.pop()
,b
出栈并访问b
;然后x
指向b
的右子树(节点f
)。
第三步:处理 b
右子树(以 f
为根的子树 )
- 调用
goAlongLeftBranch
函数,从节点f
开始,因为f
有左子树(节点d
),将f
入栈,此时栈:[f]
;继续沿着左分支,d
有左子树(节点c
),将d
入栈 ,此时栈:[d, f]
;c
没有左子树了,将c
入栈 ,此时栈:[c, d, f]
。 - 回到主循环,栈非空,执行
x = S.pop()
,c
出栈并访问c
;然后x
指向c
的右子树,c
右子树为空,回到主循环继续判断栈状态。
第四步:继续处理 f
左子树剩余部分
- 此时栈顶是
d
,执行x = S.pop()
,d
出栈并访问d
;然后x
指向d
的右子树(节点e
)。 - 将
e
入栈,此时栈:[e, f]
,执行x = S.pop()
,e
出栈并访问e
;e
右子树为空,回到主循环继续判断栈状态。
第五步:处理根节点 f
- 此时栈顶是
f
,执行x = S.pop()
,f
出栈并访问f
;然后x
指向f
的右子树(节点g
)。 - 将
g
入栈,此时栈:[g]
,执行x = S.pop()
,g
出栈并访问g
;g
右子树为空,此时栈为空,满足S.empty()
条件,break
跳出循环,中序遍历结束。
时间复杂度的证明:
简要分析:
关键操作包括:入栈( goAlongLeftBranch
函数)和出栈及节点访问(主循环)
入栈
goAlongLeftBranch
函数:沿着二叉树的左分支不断将节点入栈。最坏情况下,二叉树退化为一条单链(eg:所有节点只有左孩子 ),此时对于一个具有 n
个节点的二叉树,调用 goAlongLeftBranch
函数时 需将 n
个节点依次入栈,每次入栈操作可视为 O(1)
,所以单次 goAlongLeftBranch
调用 在最坏情况下的时间复杂度为 Ω(n) 。
出栈及节点访问
主循环中,每次迭代会执行一次出栈并访问出栈节点。每个节点只会入栈一次且出栈一次(每个节点在沿着左分支时入栈,出栈访问后不会再次入栈 )。出栈和访问节点都是O(1)
。由于二叉树共有 n
个节点,所以出栈及节点访问操作的总时间复杂度为 O(n) 。
分摊
虽然单次 goAlongLeftBranch
调用可能花费 Ω(n) 时间(如二叉树为单链时 ),但从整个遍历过程来看,每个节点入栈和出栈操作总共最多各一次。
例如,当沿着左分支集中进行多次入栈(耗时较长 )时,后续这些入栈节点会依次出栈并被访问。把前面集中入栈的较长时间开销分摊到后续这些节点出栈及访问的过程中。从整体 n
个节点的处理来看,平均每个节点的操作时间是常数级别的。所以整个中序遍历算法的时间复杂度为 O(n) 。
具体分析:
template <typename T>
static void goAlongLeftBranch( BinNodePosi(T) x, Stack <BinNodePosi(T)> &S ) {while ( x ) { S.push( x ); x = x->lc; } // 反复地入栈,沿左分支深入
}
作用:沿当前节点 x 的左分支不断将节点入栈。每次循环执行 S.push(x)(入栈,时间复杂度为 O(1))和 x=x−>lc(指针移动,时间复杂度为 O(1))。
对于每个节点,它只会从左分支方向被访问一次并入栈。整个二叉树共有 n 个节点,因此所有节点入栈的总次数为 n,总时间复杂度为 O(n)。
template <typename T, typename V>
void travIn_I1( BinNodePosi(T) x, V& visit ) { Stack <BinNodePosi(T) > S; // 辅助栈while ( true ) { // 反复地goAlongLeftBranch( x, S ); // 从当前节点出发,逐批入栈if ( S.empty() ) break; // 直至所有节点处理完毕x = S.pop(); // x的左子树或为空,或已遍历(等效于空),故可以visit( x->data ); // 立即访问之x = x->rc; // 再转向其右子树(可能为空,留意处理手法)}
}
出栈:x = S.pop()
为出栈,时间复杂度为 O(1)。每个节点入栈后仅出栈一次,总出栈次数为 n,总时间复杂度为 O(n)。
访问节点:visit( x->data )
用于访问节点数据,每个节点仅被访问一次,时间复杂度为 O(1),n 个节点总时间复杂度为 O(n)。
转向右子树:x = x->rc
是指针移动,时间复杂度为 O(1)。每个节点处理完后转向右子树(即使右子树为空也会执行一次该操作),总操作次数为 n,总时间复杂度为 O(n)。
虽然 goAlongLeftBranch
函数单次调用可能沿长左链做多次入栈(如树退化为左单链时),但从全局看,每个节点的入栈、出栈、访问及转向右子树操作均为 O(1) 且仅执行一次。
总时间复杂度为各部分时间复杂度之和:O(n)+O(n)+O(n)+O(n)=O(n)。
此二叉树中序遍历代码的时间复杂度为 O(n),每个节点都被且仅被处理一次,所有操作的总耗时与节点数 n 呈线性关系。
1.入栈
goAlongLeftBranch
函数中 while (x) { S.push(x); x = x->lc; }
会沿左分支将节点入栈。每个节点仅入栈一次,总入栈次数为 n(n 为节点总数),入栈总时间为 O(n)。
2. 外层循环
- 出栈:
x = S.pop()
每个节点仅出栈一次,总出栈次数为 n,时间为 O(n)。 - 访问节点:
visit(x->data)
每个节点仅访问一次,时间为 O(n)。 - 转向右子树:
x = x->rc
每个节点处理后转向右子树(即使为空也执行一次),总操作次数为 n,时间为 O(n)。
虽然有循环嵌套,但每个节点的入栈、出栈、访问、转向右子树操作均为 O(1) 且仅执行一次。所有操作的总时间复杂度为各部分之和:
O(n)(入栈)+O(n)(出栈)+O(n)(访问)+O(n)(转向右子树)=O(n)
外层循环,每次出栈一个节点,处理它的右子树。每个节点出栈一次,访问一次。虽然外层是 while (true),但每次循环都处理一个节点,并且每个节点的入栈和出栈都是一次。
时间复杂度看的是每个操作的总次数。入栈总次数是 n,出栈总次数是 n,每个节点处理一次。所以总的时间复杂度是 O (n)
一句话:循环嵌套看似为n^2,但在while(true)中每个节点只被处理一次,没有重复处理,所以是 O (n)
DSACPP