310.力扣LeetCode_ 最小高度树_三种方法总结

目录

思路1:剥洋葱法

代码实现

代码细节解析

边列表转数组邻接表------一次性扩容

逐轮更新degree数组------剥洋葱法核心

更新degree数组的错误示例

结果统计与内存释放

注释版本

思路2:直径法

DFS+直径法代码实现

代码详细解析

函数1:头插法建立链式邻接表

函数2:dfs深度优先搜索

函数3:findLongestNode

函数4:get_path

主函数:findMinHeightTrees

边界处理

函数调用

结果统计------奇偶性分析

内存释放

DFS_注释版本

BFS+直径法代码实现

BFS_注释版本



思路1:剥洋葱法

核心思路:可以假想存在一条最长链子,其他的较短的链子都是在处理最长的链子时,捎带被处理完的。我们只需要考虑最长的这条链子。每次把度为1的结点以及其相连的边从整个图结构中删去,其实就是删除这个最长链子的头尾节点。由于链长的奇偶性不同,最终只可能会剩下一个或者两个结点

代码实现

cpp 复制代码
int* findMinHeightTrees(int n, int** edges, int edgesSize, int* edgesColSize, int* returnSize) {

    int degree[n],adjColSize[n];
    memset(degree,0,n*sizeof(int));
    memset(adjColSize,0,n*sizeof(int));
    for(int i=0;i<edgesSize;i++){
        int x=edges[i][0];
        int y=edges[i][1];
        degree[x]++;
        degree[y]++;
    }

    int **adj=(int**)malloc(n*sizeof(int*));
    int index[n];
    memset(index,0,n*sizeof(int));
    for(int i=0;i<n;i++){
        adj[i]=(int*)malloc(degree[i]*sizeof(int));
        adjColSize[i]=degree[i];
    }

    for(int i=0;i<edgesSize;i++){
        int x=edges[i][0];
        int y=edges[i][1];
        adj[x][index[x]++]=y;
        adj[y][index[y]++]=x;
    }

int count=n,Stack[n],top=-1;
    while(count>2){
        for(int i=0;i<n;i++){
            if(degree[i]==1) Stack[++top]=i;
        }
        while(top!=-1){
            int cur=Stack[top--];
            count--;
            degree[cur]=-1;
            for(int i=0;i<adjColSize[cur];i++){
                int cur_neighbor=adj[cur][i];
                if(degree[cur_neighbor]>=0){
                    degree[cur_neighbor]--;
                }
            }
        }
    }

    int *ans=(int *)malloc(2*sizeof(int));
    (*returnSize)=0;
    for(int i=0;i<n;i++){
        if(degree[i]>-1) ans[(*returnSize)++]=i;
    }

    for(int i=0;i<n;i++){
        free(adj[i]);
    }
    free(adj);

    return ans;
}

代码细节解析

边列表转数组邻接表------一次性扩容

核心思路:先统计每个节点的度数,初始化数组degree。然后创建二级指针adj,这相当于邻接表的行指针列表。在循环中,邻接表第i行需要malloc的int空间个数其实就是i的度数。顺便在循环内完成对于adjColSize数组的初始化,即adj每行的长度,当然也等于度数。我们还需要一个辅助下标数组index

cpp 复制代码
int degree[n],adjColSize[n];
    memset(degree,0,n*sizeof(int));
    memset(adjColSize,0,n*sizeof(int));
    for(int i=0;i<edgesSize;i++){
        int x=edges[i][0];
        int y=edges[i][1];
        degree[x]++;
        degree[y]++;
    }

    int **adj=(int**)malloc(n*sizeof(int*));
    int index[n];
    memset(index,0,n*sizeof(int));
    for(int i=0;i<n;i++){
        adj[i]=(int*)malloc(degree[i]*sizeof(int));
        adjColSize[i]=degree[i];
    }

    for(int i=0;i<edgesSize;i++){
        int x=edges[i][0];
        int y=edges[i][1];
        adj[x][index[x]++]=y;
        adj[y][index[y]++]=x;
    }

逐轮更新degree数组------剥洋葱法核心

cpp 复制代码
int count=n,Stack[n],top=-1;
    while(count>2){
        for(int i=0;i<n;i++){
            if(degree[i]==1) Stack[++top]=i;
        }
        while(top!=-1){
            int cur=Stack[top--];
            count--;
            degree[cur]=-1;
            for(int i=0;i<adjColSize[cur];i++){
                int cur_neighbor=adj[cur][i];
                if(degree[cur_neighbor]>=0){
                    degree[cur_neighbor]--;
                }
            }
        }
    }

更新degree数组必须按照一层一层的顺序。我们需要把每一轮中度为0的节点保存下来。而且这里并不关心节点的保存和后续访问顺序,因为我们可以用栈,也可以用队列。栈的话只需要一个栈顶指针,而队列的话需要首尾两个指针,用栈的话会稍微简单一点点

当这一轮节点被保存完毕,我们再逐个访问。假设当前节点为cur,我们将其度数置为无效值-1,然后再根据邻接表adj找到cur所有的邻居节点,并将其邻居节点的度数减1

由于图中最长链节点个数的奇偶性不同,最终可能会剩下一个或者两个节点。因此当计数器小于等于2时,循环结束。如果最后剩下一个节点,那么它的度数为0。如果最后剩下两个节点,那么它们两个的度数都为1

更新degree数组的错误示例

cpp 复制代码
int count=n,queue[n+1],front=0,rear=0;
    for(int i=0;i<n;i++){
        if(degree[i]==1) queue[rear++]=i;
    }
    while(front!=rear){
        int cur=queue[front++];
        degree[cur]=0;
        for(int i=0;i<adjColSize[cur];i++){
            int cur_neighbor=adj[cur][i];
            if(degree[cur_neighbor]>0)
            degree[cur_neighbor]--;
            if(degree[cur_neighbor]==1){
                queue[rear++]=cur_neighbor;
            }
        }
        count--;
        if(count<=2) break;
    }

我们需要特别注意"更新degree数组必须按照一层一层的顺序"这句话,上面的代码看似简洁,实则是完全错误的。一边出队一边入队,完全打乱了层级顺序,会导致最终得到的结果偏离最长链的中心。而且这种方法无法区分最终得到的到底是一个还是两个节点,最终固定会输出两个节点。由此可见,这种优化方法在拓扑排序中是没问题的,但是对于这道题而言,是完全错误的

结果统计与内存释放

cpp 复制代码
    int *ans=(int *)malloc(2*sizeof(int));
    (*returnSize)=0;
    for(int i=0;i<n;i++){
        if(degree[i]>-1) ans[(*returnSize)++]=i;
    }

    for(int i=0;i<n;i++){
        free(adj[i]);
    }
    free(adj);
int *ans=(int *)malloc(2*sizeof(int));
    (*returnSize)=0;
    for(int i=0;i<n;i++){
        if(degree[i]>-1) ans[(*returnSize)++]=i;
    }

    for(int i=0;i<n;i++){
        free(adj[i]);
    }
    free(adj);

最终的结果只可能是1个或者2个,因此我们malloc两个int型空间即可。一定需要malloc,因为我们需要返回这个结果,就需要保证ans数组在堆空间中。如果直接使用ans[2],数组在栈空间中,会被释放,无法将结果转递给上级调用函数


注释版本

cpp 复制代码
/**
 * 寻找无向树中所有最小高度树(MHT)的根节点
 * 核心逻辑:通过"剥洋葱法"逐层移除叶子节点,最终剩余的1-2个节点即为树的中心(MHT的根)
 * 
 * @param n 树的总节点数(节点编号从0开始)
 * @param edges 边列表,edges[i]存储第i条边连接的两个节点(如edges[i][0]与edges[i][1]相邻)
 * @param edgesSize 边的总数量
 * @param edgesColSize 每个边数组的长度(固定为2,仅满足参数格式要求)
 * @param returnSize 输出参数,用于返回结果数组的实际长度(1或2)
 * @return 存储MHT根节点的数组(堆内存分配,外部需按需释放以避免内存泄漏)
 */
int* findMinHeightTrees(int n, int** edges, int edgesSize, int* edgesColSize, int* returnSize) {
    // 1. 初始化节点度数数组和邻接表列长度数组
    // degree[i]:记录节点i的度数(即与该节点直接相连的边数)
    // adjColSize[i]:记录邻接表第i行的长度(等价于节点i的邻居数,与度数一致)
    int degree[n], adjColSize[n];
    // 用memset将度数数组初始化为0(所有节点初始无连接,度数为0)
    memset(degree, 0, n * sizeof(int));
    // 邻接表列长度数组初始化为0(后续根据节点度数赋值)
    memset(adjColSize, 0, n * sizeof(int));

    // 2. 统计每个节点的度数(遍历所有边,更新边两端节点的度数)
    for (int i = 0; i < edgesSize; i++) {
        int x = edges[i][0];  // 当前边的第一个节点
        int y = edges[i][1];  // 当前边的第二个节点
        degree[x]++;          // 节点x的度数+1(新增一条连接)
        degree[y]++;          // 节点y的度数+1(新增一条连接)
    }

    // 3. 构建邻接表(二级指针adj:adj[i]是存储节点i所有邻居的数组)
    // 分配邻接表的行指针数组(共n行,对应n个节点)
    int** adj = (int**)malloc(n * sizeof(int*));
    // index[i]:辅助下标,记录节点i的邻居数组当前已填充到的位置
    int index[n];
    // 初始化辅助下标数组为0(从邻居数组的第0位开始填充)
    memset(index, 0, n * sizeof(int));

    // 为邻接表的每一行分配内存(按节点度数分配,避免内存冗余)
    for (int i = 0; i < n; i++) {
        // 节点i有degree[i]个邻居,因此分配degree[i]个int大小的内存
        adj[i] = (int*)malloc(degree[i] * sizeof(int));
        // 邻接表第i行的长度 = 节点i的度数(后续遍历邻居时用)
        adjColSize[i] = degree[i];
    }

    // 填充邻接表(遍历所有边,将邻居关系存入对应节点的邻居数组)
    for (int i = 0; i < edgesSize; i++) {
        int x = edges[i][0];
        int y = edges[i][1];
        // 把y存入x的邻居数组,同时index[x]后移(指向下一个待填充位置)
        adj[x][index[x]++] = y;
        // 把x存入y的邻居数组,同时index[y]后移(无向图双向存储)
        adj[y][index[y]++] = x;
    }

    // 4. 剥洋葱法:逐层移除叶子节点,逼近树的中心
    int count = n;                // 剩余未移除的节点数(初始为总节点数n)
    int Stack[n];                 // 栈:暂存当前层的所有叶子节点(度数=1的节点)
    int top = -1;                 // 栈顶指针(初始为-1,表示栈为空)

    // 循环终止条件:剩余节点数≤2(MHT的根最多2个,剩余节点即为中心)
    while (count > 2) {
        // 第一步:收集当前层所有叶子节点(度数=1的节点),压入栈
        for (int i = 0; i < n; i++) {
            if (degree[i] == 1) {
                Stack[++top] = i;  // 栈顶指针上移,将叶子节点存入栈
            }
        }

        // 第二步:处理当前层所有叶子节点(移除叶子,更新其邻居的度数)
        while (top != -1) {        // 栈不为空时,持续弹出叶子节点处理
            int cur = Stack[top--];// 弹出栈顶叶子节点cur,栈顶指针下移
            count--;               // 剩余节点数-1(cur已被移除)
            degree[cur] = -1;      // 标记cur为已移除(用-1区分,避免后续重复处理)

            // 遍历cur的所有邻居,更新邻居的度数(cur被移除,邻居的连接数减少1)
            for (int i = 0; i < adjColSize[cur]; i++) {
                int cur_neighbor = adj[cur][i];  // 获取cur的第i个邻居
                // 仅更新未被移除的邻居(度数≥0表示节点未被标记删除)
                if (degree[cur_neighbor] >= 0) {
                    degree[cur_neighbor]--;      // 邻居的度数-1
                }
            }
        }
    }

    // 5. 收集结果:剩余未被标记(度数>-1)的节点即为MHT的根
    // 结果最多2个节点,分配2个int的堆内存(堆内存可跨函数返回)
    int* ans = (int*)malloc(2 * sizeof(int));
    *returnSize = 0;  // 初始化结果数组长度为0(后续逐步累加)

    // 遍历所有节点,筛选出未被移除的中心节点
    for (int i = 0; i < n; i++) {
        if (degree[i] > -1) {
            ans[(*returnSize)++] = i;  // 存入结果数组,同时长度+1
        }
    }

    // 6. 释放邻接表内存(避免内存泄漏)
    // 先释放邻接表每行的邻居数组(子内存块)
    for (int i = 0; i < n; i++) {
        free(adj[i]);
    }
    // 再释放邻接表的行指针数组(父内存块)
    free(adj);

    // 返回MHT的根节点数组(堆内存)
    return ans;
}

思路2:直径法

最长的链子一定存在。其它的链子可以看做是和最长链子相交的,类似字母"X"型或者"Y"型。任取一个节点,距离其最远的节点一定是最长链子的某个端点,记为u。再找到距离u最远的点,就一定是最长链子的另一个端点v了。我们找到了最长链子的首尾两端,也就是找到了这个图的"直径",由于最长链子节点数量的奇偶性,其中心位置可能是一个或者两个节点

DFS+直径法代码实现

cpp 复制代码
typedef struct ListNode ListNode;

ListNode** create_adj(int** edges,int edgesSize,int n){

    ListNode** adj=(ListNode **)calloc(n,sizeof(ListNode *));

    for(int i=0;i<edgesSize;i++){
        int x=edges[i][0];
        int y=edges[i][1];

        ListNode *y_node=(ListNode *)malloc(sizeof(ListNode));
        y_node->val=x;
        y_node->next=adj[y];
        adj[y]=y_node;

        ListNode *x_node=(ListNode *)malloc(sizeof(ListNode));
        x_node->val=y;
        x_node->next=adj[x];
        adj[x]=x_node;
    }
    
    return adj;
}

void dfs(int cur, int distance[], int parent[],ListNode ** adj) {
    ListNode* p=adj[cur];
    while(p){
        int cur_neighbor=p->val;
        if(distance[cur_neighbor]<0){
            distance[cur_neighbor]=distance[cur]+1;
            parent[cur_neighbor]=cur;
            dfs(cur_neighbor,distance,parent,adj);
        }
        p=p->next;
    }
}

int findLongestNode(int cur, int parent[], ListNode ** adj, int n) {
    int distance[n];
    memset(distance,-1,n*sizeof(int));
    distance[cur]=0;
    dfs(cur,distance,parent,adj);
    int distance_max=0,ans=-1;
    for(int i=0;i<n;i++){
        if(distance[i]>distance_max){
            distance_max=distance[i];
            ans=i;
        }
    }
    return ans;
}

int get_path(int path[],int parent[],int x,int y){
    int path_length = 0;
    parent[x] = -1;
    while (y != -1) {
        path[path_length++] = y;
        y = parent[y];
    }
    return path_length;
}

int* findMinHeightTrees(int n, int** edges, int edgesSize, int* edgesColSize, int* returnSize){
    int * res = NULL;
    if (n == 1) {
        res = (int *)malloc(sizeof(int));
        res[0] = 0;
        *returnSize = 1;
        return res;
    }

    ListNode** adj=create_adj(edges,edgesSize,n);
    
    int * parent = (int *)malloc(sizeof(int) * n);
    int x = findLongestNode(0, parent, adj, n);
    int y = findLongestNode(x, parent, adj, n);

    int * path = (int *)malloc(sizeof(int) * n);
    int path_length=get_path(path,parent,x,y);
    
    if (path_length % 2 == 0) {
        res = (int *)malloc(sizeof(int) * 2);
        res[0]=path[path_length/2-1];
        res[1]=path[path_length/2];
        *returnSize = 2;
    } else {
        res = (int *)malloc(sizeof(int));
        *res = path[path_length/ 2];
        *returnSize = 1;
    }

    free(path);
    free(parent);
    for (int i = 0; i < n; i++) {
        ListNode * p= adj[i];
        while (p) {
            ListNode * temp = p;
            p = p->next;
            free(temp);
        }
    }
    free(adj);
    return res;
}

代码详细解析

函数1:头插法建立链式邻接表
cpp 复制代码
ListNode** create_adj(int** edges,int edgesSize,int n){

    ListNode** adj=(ListNode **)calloc(n,sizeof(ListNode *));

    for(int i=0;i<edgesSize;i++){
        int x=edges[i][0];
        int y=edges[i][1];

        ListNode *y_node=(ListNode *)malloc(sizeof(ListNode));
        y_node->val=x;
        y_node->next=adj[y];
        adj[y]=y_node;

        ListNode *x_node=(ListNode *)malloc(sizeof(ListNode));
        x_node->val=y;
        x_node->next=adj[x];
        adj[x]=x_node;
    }
    
    return adj;
}

用calloc给链式邻接表adj分配空间,可以顺便将每行的头指针初始化。否则还需要额外写循环或者使用memset进行初始化

核心的构建过程为头插法。由于是双向边,所以需要分别对节点x和y对应的链表进行插入操作。以对adj[y]进行插入为例,先创建一个新的节点y_node,将其值赋为x,表示x是y的邻居节点,然后将y_node指向头结点adj[y],然后再将adj[y]赋值为y_node,相当于让adj[y]重新指向链表头结点

函数2:dfs深度优先搜索
cpp 复制代码
void dfs(int cur, int distance[], int parent[],ListNode ** adj) {
    ListNode* p=adj[cur];
    while(p) {
        int cur_neighbor = p->val;
        if (distance[cur_neighbor] < 0) {
            distance[cur_neighbor] = distance[cur] + 1;
            parent[cur_neighbor] = cur;
            dfs(cur_neighbor, distance, parent, adj); 
        }
        p=p->next;
    }
}

在一次深度优先搜索中,我们需要一边遍历,一边对两个辅助数组对进行创建,分别是distance和parent

在常规的深度优先搜索中,为了防止重复访问,我们需要借助visited数组,来标记那些已访问过的节点。但是在这个算法中,distance数组恰好可以完成visited数组标记结点任务,就无需再额外引入visited数组了。

distance数组在传入dfs函数时,已经被全部初始化为-1,只有当前节点distance[cur]被赋值为0。我们在这里通过distance数组记录的数值的正负来区分哪些节点被访问过,而哪些节点还未被访问

因为后续还需要获取两个节点之间的路径path,因此我们还记录了parent数组

函数3:findLongestNode
cpp 复制代码
int findLongestNode(int cur, int parent[], ListNode ** adj, int n) {
    int distance[n];
    memset(distance,-1,n*sizeof(int));
    distance[cur]=0;
    dfs(cur,distance,parent,adj);
    int distance_max=0,ans=-1;
    for(int i=0;i<n;i++){
        if(distance[i]>distance_max){
            distance_max=distance[i];
            ans=i;
        }
    }
    return ans;
}

这个函数的主要功能就是通过调用dfs,找到距离cur最远的结点。

distance数组不用返回给上层函数,在本层内调用,直接在栈空间申请即可。在调用dfs之前,需要对distance数组进行初始化,先将所有的值全部赋为-1,然后再单独将开始位置的distance[cur]设置为0。在dfs中distance有着双重身份,首先根据存入值的正负,用来区分哪些节点还未被访问,扮演visited数组的角色,同时还记录着距离信息,用于后续找到最远节点,因此在传入dfs之前进行初始化尤为重要

调用dfs后,我们需要找到距离最远的节点,然后将其下标返回给上层函数

函数4:get_path
cpp 复制代码
int get_path(int path[],int parent[],int x,int y){
    int path_length=0;
    parent[x]=-1;
    while(y!=-1){
        path[path_length++]=y;
        y=parent[y];
    }
    return path_length;
}

这个函数的主要作用是,通过parent数组来生成从x到y的路径,并且返回路径的长度。这里有一个非常重要的细节

parent[x]赋值为-1是一个极其重要的步骤。首先,我们在最后的主函数中创建parent数组时,并没有进行初始化的操作,然后我们一共调用了两次findlongest函数,每次调用findlongest都会调用一次dfs函数,而只有在dfs函数中,parent数组的值会被修改。接下来就是最关键的一点,dfs 不会对parent[cur]进行任何修改,除了parent[cur]的其他n-1个元素都进行了修改。

我们在两次调用findlongest函数之间也并未对parent数组进行任何重置,因此,最后传入的get_path函数中的parent[x],相当于记录了是0号节点到x的路径中,x的父亲节点

因此,在生成path时,没有对parent[x]进行重新赋值为-1,会导致y的循环条件错误,相当于是被之前的"脏数据"影响了

cpp 复制代码
int get_path(int path[],int parent[],int x,int y){
    int path_length=0;
    while(y!=parent[x]){
        path[path_length++]=y;
        y=parent[y];
    }
    return path_length;
}

更详细的解释:如果按照上面的函数进行执行,此时记录中的parent[x]可能在x与y之间,导致记录的距离路径长度缩短,得到错误的结果

主函数:findMinHeightTrees
边界处理
cpp 复制代码
    int * res = NULL;
    if (n == 1) {
        res = (int *)malloc(sizeof(int));
        res[0] = 0;
        *returnSize = 1;
        return res;
    }

我们需要对节点数为1的特殊场景进行边界处理。因为我们接下来需要调用两次findlongestNode

int x = findLongestNode(0, parent, adj, n);

int y = findLongestNode(x, parent, adj, n);

当只有一个节点时,会得到x=-1,接下来再将x传入findlongest就会报错了

函数调用
cpp 复制代码
    ListNode** adj=create_adj(edges,edgesSize,n);
    
    int * parent = (int *)malloc(sizeof(int) * n);
    int x = findLongestNode(0, parent, adj, n);
    int y = findLongestNode(x, parent, adj, n);

    int * path = (int *)malloc(sizeof(int) * n);
    int path_length=get_path(path,parent,x,y);

这一段主要是函数的调用。

创建链式邻接表adj。

创建parent数组,然后两次调用findlongestNode函数。这也是直径法的核心思想。先选取一个起始节点0,找到距离它最远的节点x,然后再调用函数找到距离节点x最远的结点y,这样x到y的路径就是这棵树的直径

创建路径数组,调用get_path

结果统计------奇偶性分析
cpp 复制代码
    if (path_length % 2 == 0) {
        res = (int *)malloc(sizeof(int) * 2);
        res[0]=path[path_length/2-1];
        res[1]=path[path_length/2];
        *returnSize = 2;
    } else {
        res = (int *)malloc(sizeof(int));
        *res = path[path_length/ 2];
        *returnSize = 1;
    }

我们根据路径长度的奇偶性,就可以判断最终符合条件的节点个数。

如果路径长度为偶数,那么路径最中间的两个节点都符合要求。如果路径长度为奇数,那么只有最中间的一个节点符合要求

内存释放
cpp 复制代码
    free(path);
    free(parent);
    for (int i = 0; i < n; i++) {
        ListNode * p= adj[i];
        while (p) {
            ListNode * temp = p;
            p = p->next;
            free(temp);
        }
    }
    free(adj);

我们需要释放两个一维数组,path和parent。还需要释放链式邻接表adj。adj的释放需要双层循环。最后再释放二级指针adj

DFS_注释版本

cpp 复制代码
// 定义邻接表节点结构(需确保结构体完整定义,此处为前向声明)
typedef struct ListNode ListNode;
struct ListNode {
    int val;               // 存储邻接节点的索引
    struct ListNode *next; // 指向下一个邻接节点的指针
};

/**
 * @brief 构建无向图的链式邻接表
 * @param edges 边的二维数组,edges[i] = {x, y} 表示节点x与y之间有边
 * @param edgesSize 边的总数
 * @param n 图中节点的总数(节点索引从0开始)
 * @return ListNode** 构建完成的链式邻接表,adj[i]表示节点i的邻接链表头指针
 */
ListNode** create_adj(int** edges, int edgesSize, int n) {
    // 用calloc分配n个ListNode*类型的空间,初始值均为NULL(避免野指针)
    ListNode** adj = (ListNode **)calloc(n, sizeof(ListNode *));

    // 遍历所有边,为每个边构建双向邻接关系(无向图)
    for (int i = 0; i < edgesSize; i++) {
        int x = edges[i][0]; // 边的一个节点
        int y = edges[i][1]; // 边的另一个节点

        // 1. 为节点y的邻接链表插入节点x
        ListNode *y_node = (ListNode *)malloc(sizeof(ListNode)); // 分配新节点
        y_node->val = x;                                         // 新节点存储x的索引
        y_node->next = adj[y];                                   // 新节点指向原邻接链表头部
        adj[y] = y_node;                                         // 更新邻接链表头部为新节点(头插法)

        // 2. 为节点x的邻接链表插入节点y(对称操作,保证无向性)
        ListNode *x_node = (ListNode *)malloc(sizeof(ListNode));
        x_node->val = y;
        x_node->next = adj[x];
        adj[x] = x_node;
    }
    
    return adj; // 返回构建好的邻接表
}

/**
 * @brief 深度优先搜索(DFS),遍历图并记录节点距离与父节点
 * @param cur 当前遍历的节点索引
 * @param distance 距离数组:distance[i]表示起始节点到i的距离,-1表示未访问
 * @param parent 父节点数组:parent[i]表示遍历中i的前驱节点(用于后续路径追溯)
 * @param adj 链式邻接表,存储图的结构
 */
void dfs(int cur, int distance[], int parent[], ListNode **adj) {
    ListNode* p = adj[cur]; // 获取当前节点的邻接链表头部

    // 遍历当前节点的所有邻接节点
    while (p) {
        int cur_neighbor = p->val; // 邻接节点的索引

        // 若邻接节点未访问(distance为-1)
        if (distance[cur_neighbor] < 0) {
            distance[cur_neighbor] = distance[cur] + 1; // 更新距离:当前节点距离+1
            parent[cur_neighbor] = cur;                 // 记录父节点:邻接节点的父节点是当前节点
            dfs(cur_neighbor, distance, parent, adj);   // 递归遍历邻接节点(深度优先)
        }
        p = p->next; // 移动到下一个邻接节点
    }
}

/**
 * @brief 找到从指定起始节点出发,距离最远的节点
 * @param cur 起始节点索引
 * @param parent 父节点数组(用于存储DFS中的节点前驱关系,由DFS修改)
 * @param adj 链式邻接表
 * @param n 节点总数
 * @return int 距离起始节点最远的节点索引
 */
int findLongestNode(int cur, int parent[], ListNode **adj, int n) {
    int distance[n]; // 距离数组,存储起始节点到各节点的距离
    // 用memset将distance数组全部初始化为-1(标记未访问)
    memset(distance, -1, n * sizeof(int));
    distance[cur] = 0; // 起始节点到自身的距离为0

    dfs(cur, distance, parent, adj); // 调用DFS填充距离数组和父节点数组

    int distance_max = 0; // 记录最大距离
    int ans = -1;         // 记录距离最远的节点索引

    // 遍历所有节点,找到距离最大的节点
    for (int i = 0; i < n; i++) {
        if (distance[i] > distance_max) {
            distance_max = distance[i]; // 更新最大距离
            ans = i;                    // 更新最远节点索引
        }
    }

    return ans; // 返回最远节点索引
}

/**
 * @brief 基于父节点数组,追溯从节点x到节点y的路径,并记录路径长度
 * @param path 路径数组:存储追溯得到的路径(从y到x的反向路径)
 * @param parent 父节点数组:记录节点的前驱关系(由两次findLongestNode填充)
 * @param x 路径的起点(树直径的一个端点)
 * @param y 路径的终点(树直径的另一个端点)
 * @return int 路径的节点总数(路径长度)
 */
int get_path(int path[], int parent[], int x, int y) {
    int path_length = 0; // 记录路径的节点总数

    // 关键:将x的父节点设为-1,作为路径追溯的终止标记(避免脏数据干扰)
    parent[x] = -1;

    // 从y反向追溯到x(直到y为-1,即追溯到x)
    while (y != -1) {
        path[path_length++] = y; // 将当前节点y存入路径数组,路径长度+1
        y = parent[y];           // 移动到y的父节点(向x方向追溯)
    }

    return path_length; // 返回路径的节点总数
}

/**
 * @brief 求解无向树的最小高度树(MHT)的根节点
 * @param n 节点总数
 * @param edges 边的二维数组
 * @param edgesSize 边的总数
 * @param edgesColSize 每个边数组的长度(此处无用,兼容接口)
 * @param returnSize 输出参数:返回结果数组的长度(1或2)
 * @return int* 最小高度树的根节点数组(1个或2个元素)
 */
int* findMinHeightTrees(int n, int** edges, int edgesSize, int* edgesColSize, int* returnSize) {
    int *res = NULL; // 存储结果的数组(最小高度树的根节点)

    // 边界条件:只有1个节点时,该节点就是唯一的最小高度树根
    if (n == 1) {
        res = (int *)malloc(sizeof(int)); // 分配1个int的空间
        res[0] = 0;                       // 唯一节点索引为0
        *returnSize = 1;                  // 结果数组长度为1
        return res;                       // 直接返回结果,避免后续无效操作
    }

    // 1. 构建链式邻接表
    ListNode** adj = create_adj(edges, edgesSize, n);
    
    // 2. 分配父节点数组(存储节点前驱关系,用于路径追溯)
    int *parent = (int *)malloc(sizeof(int) * n);
    // 3. 两次调用findLongestNode,求解树的直径(x和y是直径的两个端点)
    int x = findLongestNode(0, parent, adj, n);  // 第一步:从0出发找最远节点x
    int y = findLongestNode(x, parent, adj, n);  // 第二步:从x出发找最远节点y(x-y为树直径)

    // 4. 分配路径数组,追溯x到y的直径路径,并获取路径长度
    int *path = (int *)malloc(sizeof(int) * n);
    int path_length = get_path(path, parent, x, y);
    
    // 5. 根据路径长度的奇偶性,确定最小高度树的根节点(直径的中点)
    if (path_length % 2 == 0) {
        // 偶数长度:2个中点(根节点)
        res = (int *)malloc(sizeof(int) * 2);
        res[0] = path[path_length / 2 - 1]; // 前一个中点(反向路径的索引)
        res[1] = path[path_length / 2];     // 后一个中点
        *returnSize = 2;                    // 结果数组长度为2
    } else {
        // 奇数长度:1个中点(根节点)
        res = (int *)malloc(sizeof(int));
        *res = path[path_length / 2];       // 唯一中点(反向路径的中间索引)
        *returnSize = 1;                    // 结果数组长度为1
    }

    // 6. 释放动态分配的内存(避免内存泄漏)
    free(path);   // 释放路径数组
    free(parent); // 释放父节点数组
    // 释放邻接表:先释放每个链表的节点,再释放邻接表数组
    for (int i = 0; i < n; i++) {
        ListNode *p = adj[i]; // 获取第i个节点的邻接链表头部
        while (p) {
            ListNode *temp = p; // 临时保存当前节点(避免释放后丢失后续节点)
            p = p->next;        // 移动到下一个节点
            free(temp);         // 释放当前节点
        }
    }
    free(adj); // 释放邻接表数组

    return res; // 返回最小高度树的根节点数组
}

BFS+直径法代码实现

cpp 复制代码
typedef struct ListNode ListNode;
ListNode** create_adj(int** edges,int edgesSize,int n){

    ListNode** adj=(ListNode **)calloc(n,sizeof(ListNode *));

    for(int i=0;i<edgesSize;i++){
        int x=edges[i][0];
        int y=edges[i][1];

        ListNode *y_node=(ListNode *)malloc(sizeof(ListNode));
        y_node->val=x;
        y_node->next=adj[y];
        adj[y]=y_node;

        ListNode *x_node=(ListNode *)malloc(sizeof(ListNode));
        x_node->val=y;
        x_node->next=adj[x];
        adj[x]=x_node;
    }
    
    return adj;
}

void bfs(int cur, int distance[], int parent[],ListNode ** adj,int n) {

    int queue[n],front=0,rear=0;
    queue[rear++]=cur;

    while(front!=rear){
        int x=queue[front++];
        ListNode* p=adj[x];
        while(p){
            int x_neighbor=p->val;
            if(distance[x_neighbor]<0){
                queue[rear++]=x_neighbor;
                distance[x_neighbor]=distance[x]+1;
                parent[x_neighbor]=x;
            }
            p=p->next;
        }
    }

}

int findLongestNode(int cur, int parent[], ListNode ** adj, int n) {
    int distance[n];
    memset(distance,-1,n*sizeof(int));
    distance[cur]=0;
    bfs(cur,distance,parent,adj,n);
    int distance_max=0,ans=-1;
    for(int i=0;i<n;i++){
        if(distance[i]>distance_max){
            distance_max=distance[i];
            ans=i;
        }
    }
    return ans;
}

int get_path(int path[],int parent[],int x,int y){
    int path_length = 0;
    parent[x] = -1;
    while (y != -1) {
        path[path_length++] = y;
        y = parent[y];
    }
    return path_length;
}

int* findMinHeightTrees(int n, int** edges, int edgesSize, int* edgesColSize, int* returnSize){
    int * res = NULL;
    if (n == 1) {
        res = (int *)malloc(sizeof(int));
        res[0] = 0;
        *returnSize = 1;
        return res;
    }

    ListNode** adj=create_adj(edges,edgesSize,n);
    
    int * parent = (int *)malloc(sizeof(int) * n);
    int x = findLongestNode(0, parent, adj, n);
    int y = findLongestNode(x, parent, adj, n);

    int * path = (int *)malloc(sizeof(int) * n);
    int path_length=get_path(path,parent,x,y);
    
    if (path_length % 2 == 0) {
        res = (int *)malloc(sizeof(int) * 2);
        res[0]=path[path_length/2-1];
        res[1]=path[path_length/2];
        *returnSize = 2;
    } else {
        res = (int *)malloc(sizeof(int));
        *res = path[path_length/ 2];
        *returnSize = 1;
    }

    free(path);
    free(parent);
    for (int i = 0; i < n; i++) {
        ListNode * p= adj[i];
        while (p) {
            ListNode * temp = p;
            p = p->next;
            free(temp);
        }
    }
    free(adj);
    return res;
}

BFS_注释版本

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

// 定义邻接表节点结构
typedef struct ListNode {
    int val;            // 存储节点值(即图中的顶点编号)
    struct ListNode* next; // 指向下一个邻接节点的指针
} ListNode;

/**
 * 创建图的邻接表表示
 * 
 * @param edges 边的数组,edges[i][0]和edges[i][1]表示一条边连接的两个顶点
 * @param edgesSize 边的数量
 * @param n 图中顶点的总数
 * @return 指向邻接表的指针,adj[i]表示顶点i的所有邻接顶点构成的链表
 */
ListNode** create_adj(int** edges, int edgesSize, int n) {
    // 为邻接表分配空间,初始化为NULL
    ListNode** adj = (ListNode **)calloc(n, sizeof(ListNode *));

    // 遍历所有边,构建邻接表
    for (int i = 0; i < edgesSize; i++) {
        int x = edges[i][0]; // 边的一个顶点
        int y = edges[i][1]; // 边的另一个顶点

        // 创建一个新节点,值为x,将其插入到y的邻接链表头部
        ListNode *y_node = (ListNode *)malloc(sizeof(ListNode));
        y_node->val = x;
        y_node->next = adj[y[y // 新节点指向y原来的邻接表头
        adj[y] = y_node;       // 更新y的邻接表头为新节点

        // 创建一个新节点,值为y,将其插入到x的邻接链表头部
        ListNode *x_node = (ListNode *)malloc(sizeof(ListNode));
        x_node->val = y;
        x_node->next = adj[x]; // 新节点指向x原来的邻接表头
        adj[x] = x_node;       // 更新x的邻接表头为新节点
    }
    
    return adj; // 返回构建好的邻接表
}

/**
 * 广度优先搜索(BFS)
 * 
 * @param cur 起始搜索的顶点
 * @param distance 输出参数,distance[i]表示从cur到顶点i的距离
 * @param parent 输出参数,parent[i]表示BFS树中顶点i的父节点
 * @param adj 图的邻接表表示
 * @param n 图中顶点的总数
 */
void bfs(int cur, int distance[], int parent[], ListNode ** adj, int n) {
    int queue[n], front = 0, rear = 0; // 定义队列及头尾指针
    queue[rear++] = cur;               // 将起始节点入队

    // BFS主循环,队列不为空时继续
    while (front != rear) {
        int x = queue[front++];        // 出队一个节点
        ListNode* p = adj[x];          // 指向x的第一个邻接节点

        // 遍历x的所有邻接节点
        while (p) {
            int x_neighbor = p->val;   // 获取邻接节点编号

            // 如果邻接节点未被访问过(距离为-1)
            if (distance[x_neighbor] < 0) {
                queue[rear++] = x_neighbor;       // 邻接节点入队
                distance[x_neighbor] = distance[x] + 1; // 更新距离
                parent[x_neighbor] = x;           // 记录父节点
            }
            p = p->next; // 访问下一个邻接节点
        }
    }
}

/**
 * 从指定起始点出发,找到距离它最远的节点
 * 
 * @param cur 起始顶点
 * @param parent 父节点数组,用于在BFS中记录路径
 * @param adj 图的邻接表表示
 * @param n 图中顶点的总数
 * @return 距离cur最远的顶点编号
 */
int findLongestNode(int cur, int parent[], ListNode ** adj, int n) {
    int distance[n]; // 存储各节点到cur的距离
    memset(distance, -1, n * sizeof(int)); // 初始化距离为-1(未访问)
    distance[cur] = 0; // 起始节点到自身距离为0
    bfs(cur, distance, parent, adj, n); // 执行BFS,计算距离

    int distance_max = 0, ans = -1;
    // 遍历所有节点,找到距离最大的节点
    for (int i = 0; i < n; i++) {
        if (distance[i] > distance_max) {
            distance_max = distance[i];
            ans = i;
        }
    }
    return ans; // 返回距离最远的节点编号
}

/**
 * 根据父节点数组,回溯从x到y的路径
 * 
 * @param path 输出参数,存储路径上的节点(从y到x)
 * @param parent 父节点数组
 * @param x 路径的起点
 * @param y 路径的终点
 * @return 路径的长度
 */
int get_path(int path[], int parent[], int x, int y) {
    int path_length = 0;
    parent[x] = -1; // 确保路径搜索能在x节点终止

    // 从y节点开始,沿着父节点指针回溯到x
    while (y != -1) {
        path[path_length++] = y; // 将当前节点加入路径
        y = parent[y[y           // 移动到父节点
    }
    return path_length; // 返回路径长度
}

/**
 * 找出无向图中所有可以作为最小高度树根节点的集合
 * 
 * @param n 图中顶点的总数
 * @param edges 边的数组
 * @param edgesSize 边的数量
 * @param edgesColSize 每个边数组的长度(此处未使用)
 * @param returnSize 输出参数,返回结果数组的长度
 * @return 包含所有最小高度树根节点的数组
 */
int* findMinHeightTrees(int n, int** edges, int edgesSize, int* edgesColSize, int* returnSize) {
    int * res = NULL;

    // 特殊情况:只有一个节点,直接返回该节点
    if (n == 1) {
        res = (int *)malloc(sizeof(int));
        res[0] = 0;
        *returnSize = 1;
        return res;
    }

    ListNode** adj = create_adj(edges, edgesSize, n); // 创建邻接表
    int * parent = (int *)malloc(sizeof(int) * n);    // 用于记录BFS树的父节点

    // 第一步:从任意节点(如0)出发,找到最远节点x
    int x = findLongestNode(0, parent, adj, n);
    // 第二步:从x出发,找到最远节点y,此时x-y为树的直径(最长路径)
    int y = findLongestNode(x, parent, adj, n);

    int * path = (int *)malloc(sizeof(int) * n); // 存储直径路径上的节点
    // 根据parent数组,获取从x到y的路径,path数组存储的是从y到x的路径
    int path_length = get_path(path, parent, x, y);
    
    // 确定最小高度树的根节点
    if (path_length % 2 == 0) { // 路径长度为偶数,有两个根节点
        res = (int *)malloc(sizeof(int) * 2);
        res[0] = path[path_length/2 - 1]; // 中间两个节点中的第一个
        res[1] = path[path_length/2];     // 中间两个节点中的第二个
        *returnSize = 2;
    } else { // 路径长度为奇数,有一个根节点
        res = (int *)malloc(sizeof(int));
        *res = path[path_length / 2]; // 路径的中间节点
        *returnSize = 1;
    }

    // 释放动态分配的内存,避免内存泄漏
    free(path);
    free(parent);
    // 释放邻接表占用的内存
    for (int i = 0; i < n; i++) {
        ListNode * p = adj[i];
        while (p) {
            ListNode * temp = p;
            p = p->next;
            free(temp);
        }
    }
    free(adj);

    return res; // 返回最小高度树的根节点集合
}
相关推荐
AI移动开发前沿1 小时前
AI原生应用开发:链式思考技术面试常见问题解析
ai·面试·职场和发展·ai-native
py有趣1 小时前
LeetCode算法学习之旋转矩阵
学习·算法·leetcode
萘柰奈1 小时前
LeetCode----200.岛屿数量(Medium)
算法·leetcode·职场和发展
MonkeyKing_sunyuhua1 小时前
量化只支持a8w8和w4a8,其中a8w8和w4a8是什么意思?
人工智能·算法
霍格沃兹测试开发学社1 小时前
被裁后,我如何实现0到3份大厂Offer的逆袭?(内附面试真题)
人工智能·selenium·react.js·面试·职场和发展·单元测试·压力测试
未来之窗软件服务1 小时前
幽冥大陆(三十九)php二维数组去重——东方仙盟筑基期
android·开发语言·算法·php·仙盟创梦ide·东方仙盟·东方仙盟sdk
DFT计算杂谈1 小时前
Abinit-10.4.7安装教程
linux·数据库·python·算法·matlab
sali-tec1 小时前
C# 基于halcon的视觉工作流-章65 点云匹配-基于形状
开发语言·人工智能·算法·计算机视觉·c#
AI科技星1 小时前
自然本源——空间元、氢尺、探针与场方程
数据结构·人工智能·算法·机器学习·计算机视觉