二叉树
二叉树是一种非线性数据结构,体现了“一分为二”的分治逻辑。与链表类似,其基本单元是节点每个节点包含值和左右两个节点引用。
C语言实现二叉树如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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; }
|
二叉树常见术语
- 根节点
位于二叉树顶层的节点,没有父节点
- 叶节点
没有子节点的节点,其两个指针均指向None
- 边
连接两个节点的线段,即节点引用(指针)
- 节点所在的层
从顶至底递增,根节点所在层为 1
- 节点的度
节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2
- 二叉树的高度
从根节点到最远叶节点所经过的边的数量
- 节点的深度
从根节点到该节点所经过的边的数量
- 节点的高度
从距离该节点最远的叶节点到该节点所经过的边的数量
二叉树基本操作
初始化
直接调用上文中的newTreeNode()函数即可。
1 2 3 4 5 6 7 8 9 10 11
| 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;
|
插入与删除节点
类比链表,可通过修改指针实现。
1 2 3 4 5 6 7
| TreeNode *P=newTreeNode(0);
n1->left=P; P->left=n2;
n1->left=n2; free(P);
|
常见的二叉树类型
完美二叉树
所有层的节点都被完全填满的二叉树被称为完美二叉树或满二叉树。在完美二叉树中,叶节点的度为0,其余所有节点的度均为2,而且若树高为h,则节点总数为2h+1−1,呈现标准的指数级关系。

完全二叉树
其仅允许最底层的节点不被完全填满,且必须从左至右依次连续填充。

完美二叉树也是一棵完全二叉树。
完满二叉树
其除了叶节点外其余所有节点都有两个子节点,即也满足叶节点的度为0,其余所有节点的度均为2。

平衡二叉树
其任意节点的左子树和右子树高度差的绝对值不超过1。

二叉树的退化
当二叉树的每层节点都被填满时,达到“完美二叉树”;而当所有节点都偏向一侧时,二叉树退化为“链表”。
完美二叉树是理想情况,可以充分发挥二叉树“分治”的优势。
链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至O(n)
如下表所示,在最佳结构和最差结构下,二叉树的叶节点数量、节点总数、高度等达到极大值或极小值。
| 项目 |
完美二叉树 |
链表 |
| 第 i 层的节点数量 |
(2^{i-1}) |
1 |
| 高度为 h 的树的叶节点数量 |
(2^h) |
1 |
| 高度为 h 的树的节点总数 |
(2^{h+1}-1) |
(h+1) |
| 节点总数为 n 的树的高度 |
(\log_2(n+1)-1) |
(n-1) |
二叉树遍历
层序遍历
这种方式从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的方式访问节点。层序遍历属于广度优先遍历(广度优先搜索),它体现了一种“一圈一圈向外扩展”的逐层遍历方式。

广度优先遍历通常借助“队列”来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| int *levelOrder(TreeNode *root,int *size){ int front,rear; int index,*arr; TreeNode *node; TreeNode **queue; queue=(TreeNode**)malloc(sizeof(treeNode*)*MAX_SIZE); front=0,rear=0; queue[rear++]=root; arr=(int*)malloc(sizeof(int)*MAX_SIZE); index=0; while(front<rear){ node=queue[front++]; arr[index++]=node->val; if(node->left!=NULL){ queue[rear++]=node->left; } if(node->right!=NULL){ queue[rear++]=node->right; } } *size=index; arr=realloc(arr,sizeof(int_*(*size)));
free(queue); return arr; }
|
该算法的时间和空间复杂度均为O(n)
前、中、后序遍历
这几种遍历都属于深度优先遍历(深度优先搜索),它体现了一种“先走到尽头,再回溯继续”的遍历方式。
深度优先遍历就像是绕着整棵二叉树的外围“走”一圈,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。

深度优先搜索通常基于递归实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void preOrder(TreeNode *root,int *size){ if(root==NULL) return; arr[(*size)++]=root->val; preOrder(root->left,size); preOrder(root->right,size); }
void inOrder(TreeNode *root,int *size){ if(root==NULL) return; inOrder(root->left,size); arr[(*size)++]=root->val; inOrder(root->right,size); }
void postOrder(TreeNode *root,int *size){ if(root==NULL) return; postOrder(root->left,size); postOrder(root->right,size); arr[(*size)++]=root->val; }
|
这种遍历方式的时间复杂度为O(n),树退化为链表时的最差情况下系统占用的栈帧空间为O(n)。
二叉树的数组表示
二叉树可以用数组表示,完美二叉树可以按顺序将每个元素放入数组,而其他类型的二叉树可以通过将空缺位对应的数组元素置为空实现,如下图所示:

用该方法表示完全二叉树时可以省略存储所有None。
用数组表示的这种表示方法有如下的优点:
- 其存储在连续的内存空间中,对缓存友好,访问和遍历的速度较快。
- 无需存储指针,比较节省空间。
- 允许随机访问节点。
但是存在如下缺点:
- 其需要连续的内存空间进行存储,故不适合存储数据量过大的树。
- 增删节点需要通过数组的插入与删除操作是实现,效率较低。
- 当二叉树中存在大量None时空间利用率低
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| typedef struct{ int*tree; int size; }ArrayBinaryTree;
ArrayBinaryTree *newArrayBinaryTree(int *arr,int arrSize){ ArrayBinaryTree*abt=(ArrayBinaryTree*)malloc(sizeof(ArrayBinaryTree)); abt->tree=malloc(sizeof(int)*arrSize); memcpy(abt->tree,arr,sizeof(int)*arrSize); abt->size=arrSize; return abt; }
void delArrayBinaryTree(ArrayBinaryTree *abt){ free(abt->tree); free(abt); }
int size(ArrayBinaryTree *abt){ return abt->size; }
int val(ArrayBinaryTre*abt,int i){ if(i<0||i>=size(abt)) return INT_MAX; return abt->tree[i]; }
int *levelOrder(ArrayBinaryTree *abt,int *returnSize){ int *res=(int*)malloc(sizeof(int)*size(abt)); int index=0; for(int i=0;i<size(abt);i++){ if(val(abt,i)!=INT_MAX) res[index++]=val(abt,i); } *returnSize=index; return res; }
void dfs(ArrayBinaryTree *abt,int i,char *order,int *res,int*index){ if(val(abt,i)==INT_MAX) return; if(strcmp(order,"pre")==0) res[(*index)++]=val(abt,i); dfs(abt,left(i),order,res,index); if(strcmp(order,"in")==0) res[(*index)++]=val(abt,i); dfs(abt,right(i),order,res,index); if(strcmp(order,"post")==0) res[(*index)++]=val(abt,i); }
int *preOrder(ArrayBinaryTree *abt, int *returnSize) { int *res = (int *)malloc(sizeof(int) * size(abt)); int index = 0; dfs(abt, 0, "pre", res, &index); *returnSize = index; return res; }
int *inOrder(ArrayBinaryTree *abt, int *returnSize) { int *res = (int *)malloc(sizeof(int) * size(abt)); int index = 0; dfs(abt, 0, "in", res, &index); *returnSize = index; return res; }
int *postOrder(ArrayBinaryTree *abt,int *returnSize){ int*res=(int*)malloc(sizeof(int)*size(abt)); int index=0; dfs(abt,0,"post",res,&index); *returnSize=index; return res; }
|
注:本文是作者阅读《hello算法》一书的读书笔记,内容会与原书有重合,特此声明。