跳转至

EC22-统计顺序存储的二叉树中叶子节点的个数

代码实现

C
// 函数 count:统计顺序存储的二叉树中叶子节点的个数
// 参数:
// A[] - 存储二叉树的一维数组
// i - 当前节点在数组中的索引位置
// N - 数组的总长度
int count(char A[], int i, int N) {
    // 递归终止条件1:索引超出数组范围 或 当前节点为空节点
    if (i >= N || A[i] == '#') {
        return 0;
    }

    // 判断当前节点是否为叶子节点
    // 叶子节点的条件:没有左子节点 且 没有右子节点
    if ((2 * i + 1 >= N || A[2 * i + 1] == '#') && 
        (2 * i + 2 >= N || A[2 * i + 2] == '#')) {
        return 1; // 是叶子节点,返回 1
    }

    // 递归统计左子树和右子树的叶子节点个数
    int n1 = count(A, 2 * i + 1, N); // 统计左子树叶子节点数
    int n2 = count(A, 2 * i + 2, N); // 统计右子树叶子节点数

    // 返回左右子树叶子节点数的总和
    return n1 + n2;
}

代码逻辑解析

1. 顺序存储的二叉树

  • 二叉树通过一维数组存储,节点按照层次遍历的顺序排列
  • 对于索引为 i 的节点:
  • 左子节点索引:2*i + 1
  • 右子节点索引:2*i + 2
  • 父节点索引:(i-1)/2(向下取整)
  • 空节点通常用特殊字符(如 '#')表示

2. 叶子节点的定义

  • 叶子节点是指没有子节点的节点
  • 判断条件:左子节点为空 且 右子节点为空

3. 算法思路

  • 使用递归遍历二叉树
  • 对每个节点判断是否为叶子节点
  • 如果是叶子节点,返回 1;否则递归统计左右子树的叶子节点数

4. 递归过程

  • 递归终止条件
  • 索引超出数组范围
  • 当前节点为空节点(用 '#' 表示)
  • 叶子节点判断
  • 左子节点索引超出范围 或 左子节点为空
  • 右子节点索引超出范围 或 右子节点为空
  • 递归统计:统计左右子树的叶子节点数并求和

时间复杂度分析

1. 访问节点数

  • 每个数组元素最多被访问一次
  • 时间复杂度:O(N),其中 N 是数组长度

2. 每次操作复杂度

  • 每次访问和判断操作都是 O(1)
  • 总时间复杂度:O(N)

空间复杂度分析

1. 额外空间使用

  • 只使用了常量级别的局部变量
  • 没有使用额外的数据结构

2. 递归调用栈空间

  • 递归深度取决于树的高度
  • 对于完全二叉树:O(log N)
  • 最坏情况(退化为链表):O(N)

3. 总体空间复杂度

O(h),其中 h 是树的高度 在最坏情况下为 O(N)


较难理解部分的说明

1. 叶子节点判断条件详解

C
1
2
3
4
5
6
7
8
9
// 判断左子节点是否为空
(2 * i + 1 >= N || A[2 * i + 1] == '#')

// 判断右子节点是否为空  
(2 * i + 2 >= N || A[2 * i + 2] == '#')

// 叶子节点:左右子节点都为空
if (左子节点为空 && 右子节点为空)
    return 1; // 是叶子节点

条件解释:

  • 2 * i + 1 >= N:左子节点索引超出数组范围
  • A[2 * i + 1] == '#':左子节点位置存储的是空节点标记
  • 两个条件用 ||(或)连接,满足其一即表示左子节点为空

2. 图解说明

假设顺序存储的二叉树数组如下:

Text Only
索引:  0   1   2   3   4   5   6   7   8   9   10  11  12
数组: [A,  B,  C,  D,  #,  E,  F,  #,  #,  #,  #,  G,  #]

对应的二叉树结构:

Text Only
1
2
3
4
5
6
7
        A(0)           // 非叶子节点
       /     \
     B(1)     C(2)     // 非叶子节点
    /  \     /  \
  D(3)  #(4) E(5) F(6) // D,E,F 是叶子节点
  / \       / \   / \
 #(7)#(8)  #(9)#(10) G(11) #(12) // G 是叶子节点

叶子节点识别过程:

Text Only
1
2
3
4
5
6
7
节点 A(0): 左子 B(1)≠'#', 右子 C(2)≠'#' → 非叶子
节点 B(1): 左子 D(3)≠'#', 右子 #(4)='#' → 非叶子  
节点 C(2): 左子 E(5)≠'#', 右子 F(6)≠'#' → 非叶子
节点 D(3): 左子 #(7)='#', 右子 #(8)='#' → 叶子节点
节点 E(5): 左子 #(9)='#', 右子 #(10)='#' → 叶子节点
节点 F(6): 左子 G(11)≠'#', 右子 #(12)='#' → 非叶子
节点 G(11): 左子越界, 右子越界 → 叶子节点

总叶子节点数:3(D, E, G)


延伸知识点

1. 非递归版本 - 使用栈

C
// 非递归统计叶子节点数
int countIterative(char A[], int N) {
    if (N == 0 || A[0] == '#') return 0;

    int leafCount = 0;
    Stack* stack = createStack(); // 假设有栈实现
    push(stack, 0); // 从根节点开始

    while (!isEmpty(stack)) {
        int i = pop(stack);

        // 检查当前节点是否为空
        if (i >= N || A[i] == '#') continue;

        // 检查是否为叶子节点
        bool leftEmpty = (2 * i + 1 >= N || A[2 * i + 1] == '#');
        bool rightEmpty = (2 * i + 2 >= N || A[2 * i + 2] == '#');

        if (leftEmpty && rightEmpty) {
            leafCount++; // 是叶子节点
        } else {
            // 将子节点入栈
            if (2 * i + 2 < N && A[2 * i + 2] != '#') 
                push(stack, 2 * i + 2); // 右子节点
            if (2 * i + 1 < N && A[2 * i + 1] != '#') 
                push(stack, 2 * i + 1); // 左子节点
        }
    }

    return leafCount;
}

2. 相关统计函数

C
// 统计内部节点数(非叶子节点数)
int countInternalNodes(char A[], int i, int N) {
    if (i >= N || A[i] == '#') return 0;

    // 如果是叶子节点,不计入内部节点
    if ((2 * i + 1 >= N || A[2 * i + 1] == '#') && 
        (2 * i + 2 >= N || A[2 * i + 2] == '#')) {
        return 0;
    }

    // 当前节点是内部节点,加上左右子树的内部节点数
    return 1 + countInternalNodes(A, 2 * i + 1, N) + 
               countInternalNodes(A, 2 * i + 2, N);
}

// 统计树的高度
int getHeight(char A[], int i, int N) {
    if (i >= N || A[i] == '#') return 0;

    int leftHeight = getHeight(A, 2 * i + 1, N);
    int rightHeight = getHeight(A, 2 * i + 2, N);

    return 1 + (leftHeight > rightHeight ? leftHeight : rightHeight);
}

3. 完全二叉树的优化

C
// 对于完全二叉树的优化版本
int countLeavesCompleteTree(char A[], int N) {
    if (N == 0) return 0;

    // 完全二叉树的叶子节点都在最后几层
    int leafStart = (N - 1) / 2 + 1; // 叶子节点开始位置
    int leafCount = 0;

    for (int i = leafStart; i < N; i++) {
        if (A[i] != '#') {
            leafCount++;
        }
    }

    return leafCount;
}

4. 实际应用场景

  • 文件系统:统计目录树中的文件数量(文件相当于叶子节点)
  • 组织架构:统计组织中的基层员工数量
  • 决策树:统计决策树中的叶节点(决策结果)
  • 语法分析:统计语法树中的终结符节点
  • 网络拓扑:统计网络中的终端设备数量

5. 性能对比分析

方法 时间复杂度 空间复杂度 适用场景
递归版本 O(N) O(h) 通用,代码简洁
非递归版本 O(N) O(N) 避免栈溢出风险
完全二叉树优化 O(√N) O(1) 特定于完全二叉树

这种方法体现了递归分治顺序存储结构特性利用的思想,是处理树形数据的经典算法之一。