目录


思路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; // 返回最小高度树的根节点集合
}