目录
什么是二叉树?
二叉树(binary tree)是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。
/* 二叉树节点结构体 */
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;
}
每个节点都有两个引用(指针),分别指向左子节点(left-child node)和右子节点(right-child node),该节点被称为这两个子节点的父节点(parent node)。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的左子树(left subtree),同理可得右子树(right subtree)。
在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树。如图 7-1 所示,如果将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。
1 二叉树常见术语
- 根节点(root node):位于二叉树顶层的节点,没有父节点。
- 叶节点(leaf node):没有子节点的节点,其两个指针均指向
None
。 - 边(edge):连接两个节点的线段,即节点引用(指针)。
- 节点所在的层(level):从顶至底递增,根节点所在层为 1 。
- 节点的度(degree):节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
- 二叉树的高度(height):从根节点到最远叶节点所经过的边的数量。
- 节点的深度(depth):从根节点到该节点所经过的边的数量。
- 节点的高度(height):从距离该节点最远的叶节点到该节点所经过的边的数量。
2 二叉树基本操作
(1). 初始化二叉树
与链表类似,首先初始化节点,然后构建引用(指针)。
/* 初始化二叉树 */
// 初始化节点
TreeNode *n1 = newTreeNode(1);
TreeNode *n2 = newTreeNode(2);
TreeNode *n3 = newTreeNode(3);
TreeNode *n4 = newTreeNode(4);
TreeNode *n5 = newTreeNode(5);
// 构建节点之间的引用(指针)
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;
(2). 插入与删除节点
与链表类似,在二叉树中插入与删除节点可以通过修改指针来实现。图中给出了一个示例。
/* 插入与删除节点 */
TreeNode *P = newTreeNode(0);
// 在 n1 -> n2 中间插入节点 P
n1->left = P;
P->left = n2;
// 删除节点 P
n1->left = n2;
3 常见二叉树类型
(1). 完美二叉树
如图所示,完美二叉树(perfect binary tree)所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 0 ,其余所有节点的度都为 2 ;若树的高度为 ℎ ,则节点总数为 2ℎ+1−1 ,呈现标准的指数级关系。
(2). 完全二叉树
如图所示,完全二叉树(complete binary tree)只有最底层的节点未被填满,且最底层节点尽量靠左填充。
(3). 完满二叉树
如图所示,完满二叉树(full binary tree)除了叶节点之外,其余所有节点都有两个子节点。
(4). 平衡二叉树
如图所示,平衡二叉树(balanced binary tree)中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。
4 二叉树的退化
图中展示了二叉树的理想结构与退化结构。当二叉树的每层节点都被填满时,达到“完美二叉树”;而当所有节点都偏向一侧时,二叉树退化为“链表”。
- 完美二叉树是理想情况,可以充分发挥二叉树“分治”的优势。
- 链表则是另一个极端,各项操作都变为线性操作,时间复杂度化至 O(n) 。
二叉树的遍历
1 前序、中序、后序遍历
前序、中序和后序遍历都属于深度优先遍历(depth-first traversal),也称深度优先搜索(depth-first search, DFS),它体现了一种“先走到尽头,再回溯继续”的遍历方式。
下图展示了对二叉树进行深度优先遍历的工作原理。深度优先遍历就像是绕着整棵二叉树的外围“走”一圈,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。
当然,二叉树的前序、中序、后序遍历也可以用迭代方法实现,下面的代码中会给出递归代码和迭代代码。
(1). 前序遍历
/* 递归方法 */
void preOrder(struct TreeNode* root, int* ans, int* count) {
if(root == NULL) {
return;
}
// 访问优先级:根节点 -> 左子树 -> 右子树
ans[(*count)++] = root->val;
preOrder(root->left, ans, count);
preOrder(root->right, ans, count);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
int count = 0;
int* ans = (int*)malloc(sizeof(int) * 100);
preOrder(root, ans, &count);
*returnSize = count;
return ans;
}
/* 迭代方法(栈) C++*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root == NULL) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top(); // 中
st.pop();
result.push_back(node->val);
if (node->right) st.push(node->right); // 右(空节点不入栈)
if (node->left) st.push(node->left); // 左(空节点不入栈)
}
return result;
}
};
(2). 中序遍历
/* 中序遍历 */
void inOrder(struct TreeNode* root, int* ans, int* count) {
if (root == NULL) {
return;
}
//访问优先级: 左子树 -> 根节点 -> 右子树
inOrder(root->left, ans, count);
ans[(*count)++] = root->val;
inOrder(root->right, ans, count);
}
int* inorderTraversal(struct TreeNode* root, int* returnSize) {
int* ans = (int*)malloc(100 * sizeof(int));
int count = 0;
inOrder(root, ans, &count);
*returnSize = count;
return ans;
}
/* 中序遍历(栈) C++*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top(); //从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
};
(3). 后序遍历
/* 后序遍历 */
void postTraversal(struct TreeNode* root, int* ans, int* count) {
if(root == NULL) {
return;
}
//访问优先级: 左子树 -> 右子树 -> 根节点
postTraversal(root->left, ans, count);
postTraversal(root->right, ans, count);
ans[(*count)++] = root->val;
}
int* postorderTraversal(struct TreeNode* root, int* returnSize) {
int* ans = (int*)malloc(100 * sizeof(int));
int count = 0;
postTraversal(root, ans, &count);
*returnSize = count;
return ans;
}
/* 后序遍历(栈) C++*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root == NULL) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if (node->left) st.push(node->left); //相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
if (node->right) st.push(node->right); // 空节点不入栈
}
reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
return result;
}
};
2 层序遍历
如图所示,层序遍历(level-order traversal)从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。
层序遍历本质上属于广度优先遍历(breadth-first traversal),也称广度优先搜索(breadth-first search, BFS),它体现了一种“一圈一圈向外扩展”的逐层遍历方式。
int** levelOrder(struct TreeNode* root, int* returnSize, int** returnColumnSizes){
*returnSize = 0;// 数组指针
if(root == NULL) {
return NULL;
}
int **res = malloc(sizeof(int*)*2000);
*returnColumnSizes = malloc(sizeof(int*)*2000);
struct TreeNode *queue[2000];//辅助队列
int rear = 0, front = 0, count = 0;// 队列指针
queue[rear++] = root;// 加入根节点
while(front != rear){
int len = rear - front;//队列长度,即上一个父节点的子节点数
count = 0;
res[*returnSize] = malloc(sizeof(int)*(len));
for(int i = 0; i < len; i++){
struct TreeNode *temp = queue[front++];// 队列出队
res[*returnSize][count++] = temp->val;// 保存节点值
if(temp->left)
queue[rear++] = temp->left;// 左子节点入队
if(temp->right)
queue[rear++] = temp->right;// 右子节点入队
}
(*returnColumnSizes)[*returnSize] = count;//每行的列数
(*returnSize)++;
}
return res;
}
3 遍历的一道简单练手题
可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。
关键在于遍历顺序,前中后序应该选哪一种遍历顺序?
其实,这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次。那么层序遍历可以不可以呢?依然也是可以的。
下面给出层序遍历的代码,其他顺序的在递归法中把加入数组换成交换节点即可。
void swap(struct TreeNode** left, struct TreeNode** right) {//接受指向左右子节点指针的指针,以便能够直接修改节点
struct TreeNode* temp = *left;
*left = *right;
*right = temp;
}
struct TreeNode* invertTree(struct TreeNode* root) {
if (root == NULL) {
return NULL;
}
struct TreeNode* queue[100];//辅助数组
int front = 0, rear = 0;
queue[rear++] = root;
while (front != rear) {
int len = rear - front;
for (int i = 0; i < len; i++) {
swap(&(queue[front]->left), &(queue[front]->right)); //交换左右子节点
if (queue[front]->left) {
queue[rear++] = queue[front]->left;
}
if (queue[front]->right) {
queue[rear++] = queue[front]->right;
}
front++;
}
}
return root;
}