310.力扣LeetCode_ 最小高度树_直径法_DFS

思路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


注释版本

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; // 返回最小高度树的根节点数组
}
相关推荐
gfdhy17 小时前
【c++】哈希算法深度解析:实现、核心作用与工业级应用
c语言·开发语言·c++·算法·密码学·哈希算法·哈希
百***060117 小时前
SpringMVC 请求参数接收
前端·javascript·算法
一个不知名程序员www18 小时前
算法学习入门---vector(C++)
c++·算法
云飞云共享云桌面18 小时前
无需配置传统电脑——智能装备工厂10个SolidWorks共享一台工作站
运维·服务器·前端·网络·算法·电脑
福尔摩斯张18 小时前
《C 语言指针从入门到精通:全面笔记 + 实战习题深度解析》(超详细)
linux·运维·服务器·c语言·开发语言·c++·算法
橘颂TA18 小时前
【剑斩OFFER】算法的暴力美学——两整数之和
算法·leetcode·职场和发展
Dream it possible!19 小时前
LeetCode 面试经典 150_二叉搜索树_二叉搜索树的最小绝对差(85_530_C++_简单)
c++·leetcode·面试
xxxxxxllllllshi19 小时前
【LeetCode Hot100----14-贪心算法(01-05),包含多种方法,详细思路与代码,让你一篇文章看懂所有!】
java·数据结构·算法·leetcode·贪心算法
前端小L19 小时前
图论专题(二十二):并查集的“逻辑审判”——判断「等式方程的可满足性」
算法·矩阵·深度优先·图论·宽度优先
铁手飞鹰19 小时前
二叉树(C语言,手撕)
c语言·数据结构·算法·二叉树·深度优先·广度优先