310.力扣LeetCode_ 最小高度树_剥洋葱法

目录

思路1:剥洋葱法

代码实现

代码细节解析

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

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

更新degree数组的错误示例

结果统计与内存释放

注释版本


  • 思路1:剥洋葱法

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

  • 代码实现

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

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数组——剥洋葱法核心

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数组的错误示例

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

  • 结果统计与内存释放

    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],数组在栈空间中,会被释放,无法将结果转递给上级调用函数

  • 注释版本

/**
 * 寻找无向树中所有最小高度树(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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值