二叉树
顺序存储
从根结点开始,自上至下,自左至右从 1 开始编号,编号 k 存放在数组下标 k-1 的位置。
需要注意的是:即便是从数组下标0开始存储,也是满足完全二叉树的性质的(王道中描述片面),只不过从编号到下标需要先按性质再-1,从下标到编号需要先+1再按性质。当然,编程的时候还是从 1 开始比较好,减少编译后的代码量。
Ps:对于高度为h的完全二叉树,叶结点可以在第h-1和h层,故可分别就该点讨论最大/小总结点数。
链式存储
结构体定义
typedef struct btNode{ // binary tree node
int val;
struct btNode *lchild;
struct btNode *rchild;
}btNode;
// 一个更简明的声明
typedef struct node btNode;
struct node{
int val;
btNode *lchild;
btNode *rchild;
};
基本操作
btNode *createBTree(void)
{
btNode *r = (btNode *)malloc(sizeof(btNode));
r->val = 0;
r->lchild = NULL;
r->rchild = NULL;
return r;
}
先序遍历(根左右)
递归
int* preorder(struct TreeNode* root, int* returnSize, int *ret);
int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
int *ret = (int *)malloc(sizeof(int) * 100);
*returnSize = 0;
return preorder(root, returnSize, ret);
}
int* preorder(struct TreeNode* root, int* returnSize, int *ret)
{
if (root){
ret[*returnSize] = root->val;
(*returnSize)++;
preorder(root->left, returnSize, ret);
preorder(root->right, returnSize, ret);
}
return ret;
}
非递归
思路:以递归思路做锚点,为了描述方便,分为队列q和栈s两种数据结构
- 若 p 非空,则入队列记录数据,然后入栈保存当前结点信息
- 令 p = p->left,重新循环
- 若 p 为空,则按照递归思路,需要获得 p 的父结点后访问 p->right
- p = pop(s), p = p->right
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* };
*/
#define MAXSIZE 100
typedef struct Stack Stack;
struct Stack{
int top;
struct TreeNode *array[MAXSIZE];
};
void push(Stack *s, struct TreeNode *node);
struct TreeNode *pop(Stack *s);
int* preorderTraversal(struct TreeNode* root, int* returnSize){
Stack *s = (Stack *)malloc(sizeof(Stack));
s->top = -1;
struct TreeNode *p = root;
int *ret = (int *)malloc(sizeof(int) * 100);
int i = 0;
while(s->top != -1 || p){
if (p){
ret[i++] = p->val;
push(s, p);
p = p->left;
}else{
p = pop(s);
p = p->right;
}
}
*returnSize = i;
return ret;
}
void push(Stack *s, struct TreeNode *node){
s->array[++s->top] = node;
}
struct TreeNode *pop(Stack *s){
return s->array[s->top--];
}
随笔:关于栈和递归
栈可以模拟递归,所以有了递归代码就可以写非递归代码。
很早之前有个粗略的想法:若只有一个递归调用,那么在调用递归之前都可以看成循环,直到满足终止条件,之后的代码按顺序执行即可。(当时并没有理解两个递归在一起的情况,或者说有点畏难情绪,就没有深入思考)
综合一下现在的想法,关于连续的两个不返回参数的简单递归调用(中间没有代码):
循环判定的条件有两个,只要满足一个就循环:一是非终止条件,另一个是信息栈非空。这应该是很自然的,如果栈空,则代表此时位于最开始的递归,如果此时终止条件被满足,那么就可以结束。栈不空,代表可以跳回上次递归,没有满足终止条件,代表可以到下一次递归。
当不满足终止条件时,将当前所用的信息 m 压栈,根据第一个递归的传参(不妨称为next1(m)),令 m = next1(m),然后进入下一个循环,如果此时满足终止条件,那么令 m = pop(s),m = next2(m)。因为当前是不满足终止条件的,这代表此次调用结束,需要回到上次调用,但是当前的 m 并非上次调用的信息,所以需要出栈。
如果两个调用附近有代码,可以根据以下原则插入:
- 两个调用前:在 m = next1(m) 之前
- 两个调用中:在 m = pop(s) 后,m = next2(m) 前
- 两个调用后:放在循环后(待验证)
层次遍历
#define MAXSIZE 100
typedef struct Queue Queue;
struct Queue{
int front;
int rear;
int capacity;
struct TreeNode *array[MAXSIZE];
};
Queue *createQueue(int queueSize);
int isEmpty(Queue *q);
int enQueue(Queue *q, struct TreeNode *node);
int deQueue(Queue *q, struct TreeNode **node);
void levelOrder(struct TreeNode* root){
struct TreeNode *p;
Queue *q = createQueue(MAXSIZE);
enQueue(q, root);
while(!isEmpty(q)){
deQueue(q, &p);
/* 这里插入访问操作 */
if (p->left)
enQueue(q, p->left);
if (p->right)
enQueue(q, p->right);
}
}
/* 创建空队列 */
Queue *createQueue(int queueSize){
Queue *q = (Queue *)malloc(sizeof(Queue));
q->capacity = queueSize;
q->front = 0;
q->rear = 0;
return q;
}
/* 判空 */
int isEmpty(Queue *q){
return q->front == q->rear;
}
/* 入队 */
int enQueue(Queue *q, struct TreeNode *node){
if ((q->rear+1) % q->capacity == q->front)
return 1;
q->array[q->rear] = node;
q->rear = (q->rear+1) % q->capacity;
return 0;
}
/* 出队 */
int deQueue(Queue *q, struct TreeNode **node){
if (q->front == q->rear)
return 1;
*node = q->array[q->front];
q->front = (q->front+1) % q->capacity;
return 0;
}
构造二叉树
已知先序序列 Pre 和中序序列 In
初始思路:
- 看 Pre[0] 在 In[] 中的左右序列,优先处理左序列,因为两个序列中,左的相对位置都是在右之前。
- 若左序列非空,那么 Pre[1] 就是根结点的左孩子,
- 若左序列为空,右序列非空,那么 Pre[1] 就是根结点的右孩子
- 否则孩子结点为 NULL
TreeNode *preInCreat(int *preorder, int preorderSize, int *inorder, int inorderSize)
{
int *root = (TreeNode *)malloc(sizeof(TreeNode));
root->val = preorder[0];
if (preorderSize > 1)
}
二叉排序树(二叉查找树)
就是在中序遍历的情况下,结点的val升序排列,按递归定义描述就是,左子树所有的值 < 根结点的值 < 右子树所有的值。
一个序列符合二叉排序树的查找路径:第 i 项之后的所有项满足相同的大/小于关系。
插入
- 递归代码
struct TreeNode* insertIntoBST(struct TreeNode* root, int val){
if (root == NULL){
root = (struct TreeNode *)malloc(sizeof(struct TreeNode));
root->val = val;
root->left = NULL;
root->right = NULL;
}
if (val < root->val)
root->left = insertIntoBST(root->left, val);
else if (val > root->val)
root->right = insertIntoBST(root->right, val);
// else: 相等不需要插入
return root;
}
- 非递归代码
struct TreeNode* insertIntoBST(struct TreeNode* root, int val){
struct TreeNode *p, *q, *node; // q 在循环后指向 p 的父节点
node = (struct TreeNode *)malloc(sizeof(struct TreeNode));
node->val = val;
node->left = node->right = NULL;
if (!root){
root = node;
}else{
p = root;
while (p){
q = p;
if (val < p->val)
p = p->left;
else if (val > p->val)
p = p->right;
else
break;
}
if (val < q->val)
q->left = node;
else if (val > q->val)
q->right = node;
}
return root;
}
删除
删除当前结点分为三种情况:
- 当前结点为叶子结点:直接删除
- 当前结点度为1,删除该结点,并使指针指向其孩子结点
- 当前结点度为2,令当前结点的值为直接前驱的值(左子树中的最右结点:左子树中最大的值)或直接后继的值(右子树的最左结点:右子树中最小的值),然后删除直接前驱/后继对应的结点,注意,若该结点上还有孩子结点,需要令父节点指向它。
// TODO: 优化
int findRMinDel(struct TreeNode *root); // 返回右子树最左的值,并删除对应结点
struct TreeNode* deleteNode(struct TreeNode* root, int key){
if (!root){
return root;
}
if (key < root->val){
root->left = deleteNode(root->left, key);
}else if(key > root->val){
root->right = deleteNode(root->right, key);
}else{
if (!root->left && !root->right){
free(root);
return NULL;
}else if(root->left && root->right){
root->val = findRMinDel(root);
}else{
return (root->left != NULL) ? root->left : root->right;
}
}
return root;
}
int findRMinDel(struct TreeNode *root){
struct TreeNode *p, *q; // p初始指向右子树,后续指向子树中的最小结点,q始终为p的父结点
int min;
q = root;
p = root->right;
min = p->val;
if (!p->left){ // 若无左子树,则当前结点即为最小
q->right = p->right;
free(p);
}else{
while (p->left){
q = p;
p = p->left;
min = p->val;
}
q->left = p->right;
free(p);
}
return min;
}