数据结构:用链式队列实现层序遍历 (Level-order Traversal)

目录

打造我们的工具------从零推导链式队列

代码实现

回归任务------层序遍历的本质需求

连接!用我们的队列完成遍历

完整代码与回顾


我们放慢节奏,把这个问题拆解成最基本、最独立的部分,然后再把它们组合起来。

很多人感觉"队列结合树就不会写了",这很正常。根本原因是我们往往试图一次性理解两个概念的互动。

根据第一性原理,正确的做法是:先将每个概念彻底分解,独立理解,然后再看它们如何以最简单的方式连接。

数据结构:用链表实现队列(Implementing Queue Using List)-CSDN博客

数据结构:层序遍历 (Level-order Traversal)-CSDN博客

所以,我们的计划是:

  1. 第一步:彻底忘记树。 我们先从零开始,推导出什么是"链式队列",以及如何实现它。把它当作一个独立的工具来学习。

  2. 第二步:回归树的遍历问题。 我们重新审视"层序遍历"这个任务的本质需求,看看它到底需要一个什么样的"工具"。

  3. 第三步:连接。 把我们打造好的"工具"(链式队列)应用到我们的"任务"(层序遍历)上,你会发现一切都水到渠成。


打造我们的工具------从零推导链式队列

问题的起点:为什么要用"链式"?

我们之前用数组实现过队列,它很好用,但有个天生的缺陷:容量固定

#define MAX_QUEUE_SIZE 100 这行代码意味着队列最多只能装100个元素。如果树很大,有101个节点在同一层,程序可能就出错了。

我们希望能有一个"用多少,装多少",容量可以动态增长的队列。

怎么实现动态增长?在C/C++里,最基础的动态内存技术就是 malloc 和指针,也就是链表。

核心思想:如何用链表模拟"排队"?

一个队列,就是一个排队队伍。它有两个最重要的操作:

  • 入队 (Enqueue): 一个人(新元素)站到队尾。

  • 出队 (Dequeue): 队头的人(最早来的元素)办理业务,离开队伍。

为了高效地实现这两个操作,我们需要能瞬间定位到队头和队尾。

  • 如果只用一个 head 指针指向链表头部,要找到队尾就必须遍历整个链表,效率太低。

  • 所以, 一个高效的链式队列,必须有两个指针:一个 front 指针指向队头,一个 rear 指針指向队尾。

代码实现

定义队列的"节点"

队列里要存放的是树的节点指针 (Node*)。所以,队列自己的节点 QueueNode 结构就需要包含两部分:

a. 要存放的数据 (Node* data)

b. 指向下一个队列节点的指针 (struct QueueNode* next)

cpp 复制代码
// 首先,这是我们熟悉的树节点
typedef struct TreeNode {
    char data;
    struct TreeNode* left;
    struct TreeNode* right;
} Node; // 为了方便,我们还是叫它 Node

// 这是队列自己的节点结构
typedef struct QueueNode {
    Node* data; // 存放的数据是树节点的指针
    struct QueueNode* next; // 指向下一个队列节点
} QueueNode;

定义"队列"本身

队列本身的管理结构,只需要包含我们刚才推导出的两个核心指针:frontrear

cpp 复制代码
typedef struct Queue {
    QueueNode* front;
    QueueNode* rear;
} Queue;

实现队列的基本操作

  • 创建队列 (createQueue) 一个空的队列,就是队头和队尾指针都为 NULL
cpp 复制代码
Queue* createQueue() {
    Queue* q = (Queue*)malloc(sizeof(Queue));
    q->front = NULL;
    q->rear = NULL;
    return q;
}

入队 (enqueue) - 这是最关键的一步,我们放慢点

当一个新元素(比如树节点A)要入队时:

  1. 肯定要先为这个新元素创建一个队列节点 newNode

  2. 然后要考虑两种情况:

    • 情况一:队列当前是空的 (q->rear == NULL) 这时队伍里一个人都没有。新来的 newNode 既是队头,也是队尾。所以 frontrear 都应该指向 newNode

    • 情况二:队列里已经有人了 rear 指针正指向当前的队尾。我们要做的是:让当前的队尾的 next 指针指向 newNode,然后更新 rear 指针,让它指向新的队尾 newNode

cpp 复制代码
void enqueue(Queue* q, Node* treeNode) {
    // 1. 为新来的树节点创建一个队列节点
    QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
    newNode->data = treeNode;
    newNode->next = NULL;

    // 2. 情况一:队列为空
    if (q->rear == NULL) {
        q->front = newNode;
        q->rear = newNode;
        return;
    }

    // 3. 情况二:队列不为空
    q->rear->next = newNode; // 让旧队尾指向新节点
    q->rear = newNode;       // 更新队尾指针
}

出队 (dequeue) - 同样放慢

出队就是让队头元素离开。

  1. 首先检查队列是否为空。如果为空,无队可出。

  2. 如果不为空,front 正指向要离开的那个节点。我们要做的就是:

  • 先把这个要离开的节点存起来,因为我们待会要返回它里面的数据。

  • front 指针向后移动一位,指向它的下一个节点 front->next

  • 一个非常重要的边界情况: 如果出队的是队伍里最后一个人,那么出队后,队列就空了。这时 front 会变成 NULL。此时,我们必须同时把 rear 也更新为 NULL!否则 rear 就会成为一个指向无效内存的"野指针"。

  • 返回我们存起来的那个节点里的数据。

cpp 复制代码
Node* dequeue(Queue* q) {
    // 1. 检查队列是否为空
    if (q->front == NULL) {
        return NULL;
    }

    // 2. 保存队头节点和它里面的数据
    QueueNode* temp = q->front;
    Node* treeNodeData = temp->data;

    // 3. 移动 front 指针
    q->front = q->front->next;

    // 4. 处理边界情况:如果出队后队列变空
    if (q->front == NULL) {
        q->rear = NULL;
    }

    // 5. 释放出队的这个队列节点本身的内存,并返回树节点数据
    free(temp);
    return treeNodeData;
}

检查队列是否为空 (isQueueEmpty)

这个最简单,看 front 指针是不是 NULL 就行了。

cpp 复制代码
int isQueueEmpty(Queue* q) {
    return q->front == NULL;
}

到此为止,我们已经成功打造了一个功能完备、独立可靠的"链式队列"工具。

你现在可以彻底忘记它的内部实现,只需要记住它有三个接口:enqueue (在队尾放东西)、dequeue (从队头取东西)、isQueueEmpty (判断是不是空的)。


回归任务------层序遍历的本质需求

现在,我们把队列放一边,重新思考"层序遍历"这个任务。 它的口语化描述是: "先访问第0层,再访问第1层,再访问第2层..."

我们手动模拟一下:

  1. 我们站在树的根节点 A (第0层)。我们访问它 (打印 A)。

  2. 好了,A 访问完了。接下来该访问谁?是第1层的所有节点:BC

  3. 核心问题: 当我们在处理 A 的时候,我们怎么"记录"下 BC 是"待办事项"?并且,我们要保证 BC 之前被访问。

  4. 当我们访问 B 的时候,我们又发现了新的待办事项:DE。这些事项的办理顺序应该在 C 之后。

  5. 这个过程像什么?

  • 访问 A,产生待办 B, C。待办列表:[B, C]

  • 从列表头取出 B 访问,产生待办 D, E。把它们加到列表尾部。待办列表:[C, D, E]

  • 从列表头取出 C 访问,产生待办 F。加到列表尾部。待办列表:[D, E, F]

  • ...

序遍历的本质,就是一个"先进先出"的任务处理过程。它需要的工具,正是一个队列 (Queue)。


连接!用我们的队列完成遍历

现在,是时候把工具和任务连接起来了。这个过程非常简单,因为我们已经分别把两边都想清楚了。

算法逻辑:

  1. 创建一个队列。

  2. 如果树不是空的,把树的根节点入队 (enqueue)。

  3. 只要队列不为空 (isQueueEmpty),就循环执行以下操作:

a. 从队列中出队 (dequeue) 一个树节点,我们叫它 current

b. 访问 current 节点(比如打印它的数据)。

c. 检查 current 的孩子们: * 如果它有左孩子,把左孩子入队 (enqueue)。 * 如果它有右孩子,把右孩子入队 (enqueue)。

你会发现,下面的代码几乎就是上面算法逻辑的逐字翻译。我们完全不需要关心 enqueuedequeue 内部是怎么用链表实现的,我们只管用它们提供的功能。

cpp 复制代码
void levelOrder(Node* root) {
    // 边界情况:如果树是空的,直接返回
    if (root == NULL) {
        return;
    }

    // 1. 创建一个队列
    Queue* q = createQueue();

    // 2. 把根节点入队
    enqueue(q, root);

    // 3. 只要队列不为空,就循环
    while (!isQueueEmpty(q)) {
        // a. 出队一个节点
        Node* current = dequeue(q);

        // b. 访问它
        printf("%c ", current->data);

        // c. 把它的孩子们(如果存在)加入队列
        if (current->left != NULL) {
            enqueue(q, current->left);
        }
        if (current->right != NULL) {
            enqueue(q, current->right);
        }
    }
    
    // 别忘了释放队列本身的内存
    free(q);
}

完整代码与回顾

现在,让我们把所有部分(树的创建、链式队列的完整实现、层序遍历函数)放在一起,形成一个可以运行的完整程序。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// --- 树的节点定义 ---
typedef struct TreeNode {
    char data;
    struct TreeNode* left;
    struct TreeNode* right;
} Node;

// --- 链式队列的节点定义 ---
typedef struct QueueNode {
    Node* data;
    struct QueueNode* next;
} QueueNode;

// --- 链式队列本身的定义 ---
typedef struct Queue {
    QueueNode* front;
    QueueNode* rear;
} Queue;

// --- 队列操作函数的实现 ---
Queue* createQueue() {
    Queue* q = (Queue*)malloc(sizeof(Queue));
    q->front = q->rear = NULL;
    return q;
}

int isQueueEmpty(Queue* q) {
    return q->front == NULL;
}

void enqueue(Queue* q, Node* treeNode) {
    QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
    newNode->data = treeNode;
    newNode->next = NULL;
    if (q->rear == NULL) {
        q->front = q->rear = newNode;
        return;
    }
    q->rear->next = newNode;
    q->rear = newNode;
}

Node* dequeue(Queue* q) {
    if (isQueueEmpty(q)) return NULL;
    QueueNode* temp = q->front;
    Node* treeNodeData = temp->data;
    q->front = q->front->next;
    if (q->front == NULL) {
        q->rear = NULL;
    }
    free(temp);
    return treeNodeData;
}

// --- 层序遍历的实现 ---
void levelOrder(Node* root) {
    if (root == NULL) return;
    Queue* q = createQueue();
    enqueue(q, root);
    while (!isQueueEmpty(q)) {
        Node* current = dequeue(q);
        printf("%c ", current->data);
        if (current->left != NULL) {
            enqueue(q, current->left);
        }
        if (current->right != NULL) {
            enqueue(q, current->right);
        }
    }
    free(q);
}

// --- 用于测试的辅助函数 ---
Node* createTreeNode(char data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->left = newNode->right = NULL;
    return newNode;
}

// --- Main 函数 ---
int main() {
    // 构建示例树
    Node* root = createTreeNode('A');
    root->left = createTreeNode('B');
    root->right = createTreeNode('C');
    root->left->left = createTreeNode('D');
    root->left->right = createTreeNode('E');
    root->right->right = createTreeNode('F');

    printf("Level-order Traversal (with Linked Queue):\n");
    levelOrder(root);
    printf("\n");

    return 0;
}

回顾一下我们的思路: 我们并没有直接去"写队列和树结合的代码"。我们是先独立地把"链式队列"这个工具造好、想透彻。然后,我们再独立地分析"层序遍历"这个任务,发现它天生就需要一个FIFO的工具。最后一步,我们只是简单地把工具的接口和任务的需求对接起来。

通过这种"隔离-分析-连接"的思路,复杂问题就被分解成了几个简单问题,你就可以更有信心地写出正确的代码了。

相关推荐
闪电悠米3 小时前
力扣hot100-41.缺失的第一个正数-原地哈希详解
数据结构·算法·哈希算法
WL学习笔记4 小时前
堆(数据结构)
数据结构·
wabs6668 小时前
关于图论【卡码网100.最大岛屿的面积的思考】
数据结构·算法·图论
yuannl109 小时前
图的最短路径Dijkstra
数据结构
zander25811 小时前
138. 随机链表的复制
数据结构·算法·链表
过期动态11 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
来一碗刘肉面11 小时前
栈在递归中的应用
数据结构·算法
皓月斯语11 小时前
程序设计语言的特点
开发语言·数据结构·c++
邪修king12 小时前
C++ 进阶终章:异常机制与智能指针全解 —— 从错误处理到 RAII 资源管理,打通现代 C++ 的核心命脉
android·数据结构·c++
Adios79419 小时前
设置交集大小至少为2
数据结构·算法·leetcode