目录


-
思路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;
}
2208

被折叠的 条评论
为什么被折叠?



