Ruixiang Li

二叉树Binary Cross Tree

1. 概念

由节点的有限集合组成。可以是空集。

根节点(root)、左子树和右子树(left / right subtree)

5种基本类型:空、独根、空左、空右、左右都不空。

节点

:两个结点的有序对

路径:连接 祖先 <—> 后代。

层数:根结点为 0 层

深度:最大层数

高度:深度 + 1

满二叉树:任何结点,或者是树叶,或者恰有两棵非空子树,则此二叉树称作满二叉树。

    A
   / \
  B   C
     / \
    D   E
   / \ / \
  F  G H  I

完全二叉树:最多只有最下面的两层结点度数可以小于2,且最下一层的结点都集中最左边。

       A
     /  \
    B    C
  /  \  /  \
 D   E  F   G
/ \ /  /
H I J L

扩充二叉树:所有空子树,都增加空树叶。

外部路径长度(所有外部结点到根结点的路径长度之和 $E$,内部路径长度(所有内部结点到根节点的路径长度之和)$I$,有:$E = I + 2n$(n为内部结点个数)

性质:

2. 抽象数据类型ADT

逻辑结构+ 运算。

对于整棵树:初始化、合并

对于结点:访问子结点、父节点、存储数据

二叉树结点ADT:

template <class T>
class BinaryTreeNode {
    // 声明二叉树类为友元类
    friend class BinaryTree<T>;
private:
    T info;  // 二叉树结点数据域
public:
    BinaryTreeNode();  // 缺省构造函数
    BinaryTreeNode(const T& ele);  // 给定数据的构造
    BinaryTreeNode(const T& ele, BinaryTreeNode<T>* l, BinaryTreeNode<T>* r);  // 子树构造结点

    T value() const;  // 返回当前结点数据
    BinaryTreeNode<T>* leftchild() const;  // 返回左子树
    BinaryTreeNode<T>* rightchild() const;  // 返回右子树
    void setLeftchild(BinaryTreeNode<T>*);  // 设置左子树
    void setRightchild(BinaryTreeNode<T>*);  // 设置右子树
    void setValue(const T& val);  // 设置数据域
    bool isLeaf() const;  // 判断是否为叶结点
    BinaryTreeNode<T>& operator=(const BinaryTreeNode<T>& Node);  // 重载赋值操作符
};

在此条件下,有二叉树ADT:

template <class T>
class BinaryTree {
private:
    BinaryTreeNode<T>* root;  // 二叉树根结点

public:
    BinaryTree() { root = NULL; }  // 构造函数
    ~BinaryTree() { DeleteBinaryTree(root); }  // 析构函数
    bool isEmpty() const;  // 判断二叉树是否为空树
    BinaryTreeNode<T>* Root() { return root; }  // 返回根结点

    BinaryTreeNode<T>* Parent(BinaryTreeNode<T>* current);  // 返回父
    BinaryTreeNode<T>* LeftSibling(BinaryTreeNode<T>* current);  // 左兄
    BinaryTreeNode<T>* RightSibling(BinaryTreeNode<T>* current);  // 右兄

    void CreateTree(const T& info, BinaryTree<T>& leftTree, BinaryTree<T>& rightTree);  // 构造新树
    void PreOrder(BinaryTreeNode<T>* root);  // 前序遍历二叉树或其子树
    void InOrder(BinaryTreeNode<T>* root);  // 中序遍历二叉树或其子树
    void PostOrder(BinaryTreeNode<T>* root);  // 后序遍历二叉树或其子树
    void LevelOrder(BinaryTreeNode<T>* root);  // 按层次遍历二叉树或其子树
    void DeleteBinaryTree(BinaryTreeNode<T>* root);  // 删除二叉树或其子树
};

遍历 or 周游(travelsal):二叉树结点的线性化。

2.1 DFS

e.g.:

        A
       / \
      B   C
     / \   \
    D   E   F
       /   / \
      G   H   I

表达式二叉树:

       -
     /   \
    +     *
   / \   / \
  *   c a   +
 / \       / \
a   b     b   c

2.1.1 递归实现

// 递归实现:
void BinaryTree<T>::DepthOrder(BinaryTreeNode<T>* root){
    if (root != NULL){
        Visit(root); // 前序
        DepthOrder(root->leftchild());
        // Visit(root); // 中序
        DepthOrder(root->rightchild());
        // Visit(root); // 后序
    }
}

非递归实现

非递归前序遍历:

Stack S, x = root
while(x):
    visit(x)
    if(x->right != NULL) S.push(x->right)
    if(x->left != NULL) x = x->left
    else x = S.pop()

非递归中序遍历:

Stack S, x = root
while(x || !S.empty()):
    if(x)
        S.push(x)
        x = x->left
    else
        x = S.pop()
        visit(x)
        x = x->right

非递归后序遍历:

同上。但是要注意,我们需要给栈中元素加上特征位:Left 和 Right,表示已经进入该节点的左/右子树,将从左/右侧回来。

Stack S, x = root
while(x || !S.empty()):
    while(x):
        x.tag = Left
        S.push(x)
        x = x->left
    x = S.pop()
    if(x.tag == Left)
        x.tag = Right
        S.push(x)
        x = x->right
    else
        visit(x)
        x = NULL

实质上三种遍历都只是“访问节点数据”在左子树和右子树之间的位置的变化关系。只需要改变访问/压栈顺序即可。

时间复杂度:O(n)(非递归前序、中序不超过O(n))

空间复杂度:最好O(log n),最差O(n)

2.2 BFS

使用队列进行维护。

void BinaryTree<T>::LevelOrder(BinaryTreeNode<T>* root) {
    using std::queue;  // 使用STL的队列
    queue<BinaryTreeNode<T>*> aQueue;
    BinaryTreeNode<T>* pointer = root;  // 保存输入参数
    if (pointer) aQueue.push(pointer);  // 根结点入队列
    while (!aQueue.empty()) {  // 队列非空
        pointer = aQueue.front();  // 取队列首结点
        aQueue.pop();  // 当前结点出队列
        Visit(pointer->value());  // 访问当前结点
        if (pointer->leftchild()) 
            aQueue.push(pointer->leftchild());  // 左子树进队列
        if (pointer->rightchild()) 
            aQueue.push(pointer->rightchild());  // 右子树进队列
    }
}

时间复杂度:O(n);

空间复杂度:最好O(1),最差O(n)

3. 存储结构

二叉链表:指针left、right和信息info

三叉链表:指针left、right、parent和信息info

存储密度$\alpha\le1$,表示数据结构存储的效率:

\[\alpha = \frac{数据本身的存储量}{整个结构的存储总量}\]

结构性开销$\gamma=1-\alpha$。

完全二叉树的下标:

令根节点为 0,对于结点 i:

左子节点 右子节点 父节点 左兄弟 右兄弟
2i+1 2(i+1) (i-1)/2(向下取整) i - 1 i + 1

4. 二叉搜索树BST

也叫二叉排序树。

空树 or 有如下性质的二叉树:

性质:中序遍历为正序的。

5. 堆、优先队列

最小堆:对一组序列${K_O,K_1,…,K_{n-1}}$来说,有如下性质:

\[K_i\le\min\{K_{2i+1},K_{2i+2}\}\]

表示为完全二叉树:父节点不大于子节点

局部有序,不唯一。

插入、删除最小值,删除普通节点的平均和最差时间都是$O(\log n)$

建堆–筛选法

思路:向下调整节点,将大的节点向下筛选,小的节点向上筛选,最终得到一个有序的堆(另一种方法是向上调整)

流程:

  1. 对于初始的数组/完全二叉树,从最后一个非叶子节点(人话:倒数第二层最右边的非叶子节点)开始,逐步向上执行筛选操作;

  2. 如果父节点与左右子节点不满足最小堆性质,就将父节点与较小的子节点交换。

  3. 继续递归调整,直到堆的结构满足最小堆性质。

算法实现:

build(array):
    n = array.length()
    for (int i = n / 2 - 1; i >= 0; i--):
        SiftDown(array, i, n)
    # 对于顺序结构存储的二叉树,最后一个非叶子节点就是 n//2-1的位置
    # 对 i 个节点(及其左右节点)进行递归调整(为了保子树)
SiftDown(array, index, n):
    smallest = index
    left = 2 * index + 1
    right = 2 * index + 2
    if (right < n and array[right] < array[index])
        smallest = right
    if (left < n and array[left] < array[index])
        smallest = left
    # 查找是否满足最小堆性质,并查找较小的子节点
    if smallest != index:
        # 如果不满足最小堆性质,递归调整使之满足性质
        SWAP(array[index], array[smallest])
        SiftDown(array, smallest, n)

时间复杂度:$O(n)$(注,向上调整法的时间复杂度为$O(n\log n)$)

插入

新元素加入末尾,然后向上更新:

Insect(array, val):
    array.append(val)
    Siftup(array)

Siftup(array):
    index = array.size() - 1
    while(index > 0):
        parent = (i - 1) // 2
        if array[index] < array[parent]
            SWAP(array[index], array[parent])
            index = parent
        else
            break

删除

将待删除元素与最后一个元素互换,弹出元素后向上/向下更新。

6. Huffman树

哈夫曼树,或称最优二叉树,是具有最小带权外部路径长度的二叉树

建树

按权(频率)从小到大排列字符,取前两个字符作为子节点/叶节点,构建其父节点,新节点的权是两子节点权的和,并放回序列。重复上述步骤。

外部路径指从根节点到所有叶节点的路径。外部路径权重是所有叶节点权重乘以其路径长度的总和。

通过归纳法证明每次贪心选择的最优性:

  1. 基础:初始队列的两个权值最小的节点贪心选择合并,必然是此状态下最优的外部权重路径。
  2. 归纳假设:假设第 k 次合并后构造的部分树满足最优性。
  3. 归纳推导:第 k+1 次合并后,一方面保持队列中的权值更新最小,另一方面保持路径长度的最优分配。最终将归纳得到整棵树的最优性。

译码

从左至右逐位判别代码串,直至确定一个字符。

效率分析

Huffman编码树压缩比率随字符频率分布变化。当外部数目不能构成满 k 叉Huffman树时,需要添加权为0的虚节点。