hello算法笔记 03
1 二叉树
/* 二叉树节点结构体 */
typedef struct TreeNode {int val; // 节点值int height; // 节点高度struct TreeNode *left; // 左子节点指针struct TreeNode *right; // 右子节点指针
} TreeNode;/* 构造函数 */
TreeNode *newTreeNode(int val) {TreeNode *node;node = (TreeNode *)malloc(sizeof(TreeNode));node->val = val;node->height = 0;node->left = NULL;node->right = NULL;return node;
}
二叉树节点结构体(TreeNode)这个结构体定义了二叉树中每个节点的组成:
int val
:存储节点的值,这里定义为整数类型int height
:存储节点的高度,这在平衡二叉树(如 AVL 树)中非常重要,用于维持树的平衡性struct TreeNode *left
:指向左子节点的指针struct TreeNode *right
:指向右子节点的指针
构造函数(newTreeNode)这个函数用于创建一个新的二叉树节点:
- 首先声明一个 TreeNode 类型的指针
node
- 使用
malloc
函数为新节点分配内存空间,大小为sizeof(TreeNode)
- 初始化节点的值为传入的参数
val
- 初始化节点高度为 0(表示叶子节点初始高度)
- 初始化左右子节点指针为
NULL
(新节点初始没有子节点) - 返回创建好的节点指针
- 首先声明一个 TreeNode 类型的指针
使用示例:
// 创建一个值为5的新节点
TreeNode *node = newTreeNode(5);
// 为该节点添加左子节点,值为3
node->left = newTreeNode(3);
// 为该节点添加右子节点,值为7
node->right = newTreeNode(7);
这段代码的特点是包含了height
字段,这表明它很可能是为实现平衡二叉树(如 AVL 树)而设计的,普通二叉树通常不需要维护节点高度。
节点高度在平衡树中用于判断树是否失衡,以及需要进行何种旋转操作来恢复平衡。
2 二叉树的结构体
在 C 语言中,结构体(struct
)的使用需要遵循特定的语法规则,结合你提供的代码,我们来详细说明结构体 TreeNode
的常见用法:
1. 结构体的声明与定义
你提供的代码首先声明了一个名为 TreeNode
的结构体:
typedef struct TreeNode {int val; // 节点值int height; // 节点高度struct TreeNode *left; // 左子节点指针struct TreeNode *right; // 右子节点指针
} TreeNode;
这里的 typedef
是关键:它为 struct TreeNode
起了一个别名 TreeNode
,因此后续可以直接用 TreeNode
代替 struct TreeNode
来使用,简化代码。
2. 结构体变量的创建
有两种方式可以创建 TreeNode
类型的变量:
(1)直接声明变量
TreeNode node1; // 创建一个TreeNode类型的变量node1(在栈上分配内存)
此时可以通过 .
运算符访问其成员:
node1.val = 10; // 给val赋值
node1.height = 1; // 给height赋值
node1.left = NULL; // 左子节点为空
node1.right = NULL; // 右子节点为空
(2)创建指针(动态分配内存)
通过 malloc
在堆上分配内存,返回指针(更常用,尤其在树结构中):
TreeNode *node2 = (TreeNode*)malloc(sizeof(TreeNode)); // 动态分配内存
通过 ->
运算符访问指针指向的结构体成员:
node2->val = 20; // 给val赋值
node2->height = 1; // 给height赋值
node2->left = NULL; // 左子节点为空
node2->right = NULL; // 右子节点为空
3. 结合构造函数 newTreeNode
使用
你提供的 newTreeNode
函数本质上是一个封装了动态内存分配和初始化的工具函数,简化了节点创建过程:
TreeNode *newTreeNode(int val) {TreeNode *node;node = (TreeNode *)malloc(sizeof(TreeNode)); // 分配内存node->val = val; // 初始化值node->height = 0; // 初始化高度node->left = NULL; // 初始化左子节点node->right = NULL; // 初始化右子节点return node; // 返回新节点指针
}
使用时直接调用即可创建一个初始化完成的节点:
TreeNode *root = newTreeNode(5); // 创建值为5的根节点
root->left = newTreeNode(3); // 为根节点添加左子节点(值为3)
root->right = newTreeNode(7); // 为根节点添加右子节点(值为7)
4. 结构体指针的特殊用法(树结构核心)
在二叉树中,节点的 left
和 right
成员本身就是 TreeNode*
类型的指针,用于指向其子节点,从而构建树形结构:
// 构建一个简单的二叉树
TreeNode *root = newTreeNode(1);
root->left = newTreeNode(2); // 根节点的左子节点是2
root->right = newTreeNode(3); // 根节点的右子节点是3
root->left->left = newTreeNode(4); // 节点2的左子节点是4
此时树的结构为:
plaintext
1/ \2 3/
4
5. 注意事项
- 内存释放:动态分配的节点(通过
malloc
或newTreeNode
创建)在不需要时需用free
释放,避免内存泄漏:free(node); // 释放单个节点
struct TreeNode*
与TreeNode*
:由于使用了typedef
,两者完全等价,后者更简洁。height
成员:如前所述,这个成员用于平衡二叉树(如 AVL 树)中维护平衡性,普通二叉树可忽略。
通过以上方式,结构体 TreeNode
被用来构建二叉树的基本单元,配合指针操作实现树形结构的创建和遍历。
3 二叉树(java)
/* 二叉树节点类 */
class TreeNode {int val; // 节点值TreeNode left; // 左子节点引用TreeNode right; // 右子节点引用TreeNode(int x) { val = x; }
}
初始化二叉树
// 初始化节点
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// 构建节点之间的引用(指针)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
插入与删除节点
TreeNode P = new TreeNode(0);
// 在 n1 -> n2 中间插入节点 P
n1.left = P;
P.left = n2;
// 删除节点 P
n1.left = n2;
4 数组的长度是固定的
由于数组的长度是固定的,因此插入一个元素必定会导致数组尾部元素“丢失”。我们将这个问题的解决方案留在“列表”章节中讨论。
在 C 语言中,传统数组的长度确实是固定的,必须在定义时就确定大小,且后续无法直接修改。这是数组和动态内存(如 malloc
分配的空间)最核心的区别。
C 语言没有原生的 “动态数组” 类型(像 Java 的 ArrayList
那样能自动扩容的结构),但可以通过 “指针 + 内存分配函数” 手动实现动态数组的功能,核心是自己管理内存的申请、扩容和释放。
1. 传统数组的 “固定长度” 特性
- 定义时必须指定长度:数组的大小必须是编译时就能确定的常量(如字面量、
#define
定义的宏),不能用变量动态指定。// 正确:长度是字面量 5(编译时确定) int arr1[5] = {1,2,3,4,5};// 正确:长度是宏定义的常量(编译时替换为 10) #define SIZE 10 int arr2[SIZE];// 错误:length 是变量(C99 前不支持,C99 支持变长数组但有局限性) int length = 5; int arr3[length];
- 长度不可修改:数组一旦定义,占用的内存空间就固定了,无法像 Java 的
ArrayList
那样 “自动扩容” 或 “缩短”。若想调整大小,只能重新定义新数组,再手动拷贝数据。
2. 为什么会有 “长度固定” 的限制?
- 内存分配机制:传统数组的内存是在 “栈区” 分配的,栈区内存的大小在编译时就需要确定,程序运行时会直接划分固定空间,无法动态调整。
- 性能考量:固定长度能让编译器提前计算数组的内存地址,访问元素时(如
arr[i]
)可以直接通过公式定位,效率极高。
3. 如何实现 “动态长度” 需求?
如果需要类似 “可变长度” 的数组,可以用 C 语言的动态内存分配函数(需包含 stdlib.h
),本质是在 “堆区” 申请内存,而非传统的栈区数组:
#include <stdlib.h>int main() {int length = 5;// 1. 动态申请内存(类似“创建可变长度数组”)int *dynamicArr = (int*)malloc(length * sizeof(int));// 2. 使用动态数组(和普通数组用法类似)for (int i = 0; i < length; i++) {dynamicArr[i] = i + 1; // 赋值:1,2,3,4,5}// 3. 动态调整大小(扩容到 10)length = 10;dynamicArr = (int*)realloc(dynamicArr, length * sizeof(int));// 4. 释放内存(堆区内存需手动释放,否则内存泄漏)free(dynamicArr);dynamicArr = NULL; // 避免野指针return 0;
}
【用malloc的是动态数组?什么是动态数组,是一开始对于数组长度不是硬编码的数组吗?根据输入自动分配吗?】
用 malloc
(或 calloc
/realloc
)实现的数组可以称为动态数组,它的核心特点是数组长度可以在程序运行时动态确定和调整,而不是像传统数组那样在编译时就固定(硬编码)。
1. 什么是动态数组?
动态数组是指长度可以在程序运行时根据需求动态分配、调整的数组,它的关键特性包括:
- 长度不固定:不需要在代码中硬编码数组大小(如
int arr[10]
中的10
就是硬编码)。 - 运行时确定:数组长度可以由用户输入、程序计算结果等动态决定(例如根据
scanf
读取的数值分配大小)。 - 可调整大小:可以在程序运行中通过
realloc
扩容或缩容,灵活适应数据量变化。
简单说:动态数组的大小 “由程序运行时的情况决定”,而不是 “写代码时就固定死”。
2. 动态数组 vs 传统数组:核心区别在 “长度何时确定”
类型 | 长度确定时机 | 长度是否可修改 | 内存分配位置 | 典型代码示例 |
---|---|---|---|---|
传统数组 | 编译时(写代码时) | 不可修改(固定大小) | 栈内存或全局区 | int arr[5]; (硬编码长度 5) |
动态数组 | 运行时(程序执行时) | 可修改(支持扩容) | 堆内存 | int *arr = malloc(n * sizeof(int)); (n 由运行时确定) |
3. 为什么 malloc
能实现动态数组?
malloc
的作用是在程序运行时从堆内存中申请指定大小的内存块,这正好满足动态数组的核心需求:
- 例如
int n; scanf("%d", &n); int *arr = malloc(n * sizeof(int));
中,n
是用户输入的数值(运行时确定),malloc
根据n
的值分配对应大小的内存,实现了 “长度由输入动态决定”。 - 后续如果需要更大的空间,可以用
realloc(arr, new_n * sizeof(int))
调整内存大小,实现 “动态扩容”。
4. 动态数组的典型使用场景
当数组长度无法在写代码时确定,需要根据运行时的情况(如用户输入、文件内容大小等)灵活分配时,就需要动态数组:
- 例如读取一个未知行数的文件,需要先统计行数,再动态分配对应大小的数组存储内容。
- 又如实现一个可动态添加元素的列表(类似 Java 的
ArrayList
),初始分配小空间,元素满了就自动扩容。
总结
动态数组的核心是 “长度在运行时动态确定和调整”,而 malloc
等函数提供了在运行时申请 / 调整内存的能力,因此是实现动态数组的基础工具。它解决了传统数组 “长度硬编码、无法修改” 的局限,让数组能更灵活地适应不同场景的需求。
简单说:“动态” 体现在长度可以随程序运行时的情况变化,而不是写死在代码里。