二叉树基础

前言

个人认为Trie比较简单,所以主要就‘笔记’一下二叉树。

1、二叉树的定义

二叉树(Binary Tree) 是由n个结点构成的有限集(n≥0),n=0时为空树,n>0时为非空树。对于非空树T TT:

  • 有且仅有一个根结点;

  • 除根结点外的其余结点又可分为两个不相交的子集Tl和Tr,分别称为T的左子树和右子树。且Tl和Tr本身又都是二叉树。

很明显该定义属于递归定义,所以有关二叉树的操作使用递归往往更容易理解和实现。

从定义也可以看出二叉树与一般树的区别主要是两点,一是每个结点的度最多为2;二是结点的子树有左右之分,不能随意调换,调换后又是一棵新的二叉树。

从定义也可以看出二叉树与一般树的区别主要是两点,一是每个结点的度最多为2;二是结点的子树有左右之分,不能随意调换,调换后又是一棵新的二叉树。

2、二叉树的形态

五种基本形态

从上面二叉树的递归定义可以看出,二叉树或为空,或为一个根结点加上两棵左右子树,因为两棵左右子树也是二叉树也可以为空,所以二叉树有5种基本形态:

三种特殊形态

3.二叉树的基本性质

  • 在二叉树的第 i 层上最多有 \(2^{i-1}\) 个结点(i 为正整数)

  • 深度为 k 的二叉树最多有 \(2^{k-1}\) 个结点(k 为正整数)

  • 对任一一棵二叉树,如果其叶结点为 n0,度为2的结点数为 n2,则一定满足:n0=n2+1

  • 具有 n 个结点的完全二叉树的深度为 \(floor(log2^n)+1\)

  • 如果有一棵有 n 个结点的完全二叉树(其深度为 [log2n] + 1,向下取整)的结点按层次序编号(从第 1 层到第 [log2n] + 1,向下取整层,每层从左到右),则对任一结点 i(1 <= i <= n)有
    1.如果 i = 1,则结点 i 是二叉树的根,无双亲;如果 i > 1,则其双亲是结点 [i / 2],向下取整
    2.如果 2i > n 则结点 i 无左孩子,否则其左孩子是结点 2i
    3.如果 2i + 1 > n 则结点无右孩子,否则其右孩子是结点 2i + 1

4.树的结构

在 C 语言中,树的实现和链表的实现有些类似。都是数据区加上指针区。一个典型的树的声明如下:

struct node {
    int             data;
    struct node     *left;
    struct node     *right;
}

typedef struct node node_t;
typedef struct node* nodeptr_t;

一般情况下,如果某一个节点的子节点不存在,我们就使用 NULL 来标记。

5.树的遍历

树的遍历操作有三种,前序遍历,中序遍历和后序遍历。三者的不同之处在于处理子节点的时间不同。前序遍历是先处理根节点,然后处理左孩子最后处理右孩子。将处理根节点的操作挪到处理左右节点的操作之间,我们就得到了中序遍历。如果挪到最后,那就是后序遍历。示例如下:

void PrintTree_PreOrder(nodeptr_t root) {
    if(root) {
        printf("%d\n",root->data);
        PrintTree_PreOrder(root->left);
        PrintTree_PreOrder(root->right);
    }
}

可以看到,我们在遍历树时多次应用了递归。这是由于树的递归定义决定的特点。下面再给出一个遍历的例子:

int treeSize(nodeptr_t root) {
    if(root == 0) {
        return 0;
    }
    return 1 + treeSize(root->left) + treesize(root->right)
}

6.二叉树的查找

nodeptr_t treeSearch(nodeptr_t root, int value) {
    if(root == NULL)
        return NULL;
    else if(root->data == value)
        return root;
    else if(root->data > target)
        return treeSearch(root->left);
    else
        return treeSearch(root->right);
}

为了防止爆栈这种悲剧发生(概率很低),我们还可以写出迭代版本的搜索算法。示例如下:

nodeptr_t treeSearch(nodeptr_t root, int value)
{
    while(root != NULL && root->data != value) {
        if(root->data > value) {
            root = root->left;
        } else {
            root = root->right;
        }
    }

    return root;
}

为了处理其他类型的数据,我们还可以把比较操作那里使用函数进行替代。比如我们在实现字典的查找时,就可以简单地使用 strcmp 函数进行比较操作。

7.插入新结点

void treeInsert(nodeptr_t root, int data)
{
    nodeptr_t newNode;

    newNode = malloc(sizeof(*newNode));
    assert(newNode);

    newNode->data = data;
    newNode->left = 0;
    newNode->right = 0;

    for(;;) {
        if(root->data > data) {
            if(root->left) {
                root = root->left;
            } else {
                root->left = newNode;
                return;
            }
        } else {
            if(root->right) {
                root = root->right;
            } else {
                root->right = newNode;
                return;
            }
        }
    }
}

这种操作的实现是极其简洁的。但是其缺点也是很明显的:这钟插入操作没有尝试平衡树。也就是说,在最坏的情况下,树可能只向某一个方向生长,使之退化为链表。我们会在后面引入改进的树结构来解决问题。

8.删除结点

void treeDelete(nodeptr_t root, int value) {
    nodeptr_t temp;

    if(root == NULL)
        return;
    else if(value < root->data)
        treeDelete(root->left,data);
    else if(value > root->data)
        treeDelete(root->right,data);
    else if(root->left && root->right) {
        temp = __findMin(root->right);
        root->data = temp->data;
        treeDelete(root->right,root->data);
    } else {
        temp = root;
        if(root->left == NULL)
            root = root->right;
        else if(root->right == NULL)
            root = root->left;
        free(temp);
    }
}

nodeptr_t __findMin(nodeptr_t root) {
    if(root == NULL)
        return NULL;
    else if(root->left == NULL)
        return root;
    else
        return __findMin(root->left);
}
posted @ 2022-02-11 20:54  PassName  阅读(136)  评论(0编辑  收藏  举报