Loading

数据结构系列3——树与二叉树

树与二叉树

概念

  • 结点的深度是从根结点开始自顶向下逐层累加的,结点的高度是从根结点开始自底向上逐层累加的
  • 结点的度是该结点孩子个数,树的度是结点的最大度数
  • 路径长度是路径上所经过的边的个数,路径是有序的(从双亲指向孩子)

性质

  • 结点数 = 度数和 + 1 (根结点没有父结点)
  • 度为m的树中第i层上至多有\(m^{i - 1}\)个结点(\(i \ge 1\)
  • 高度为h的m叉树至多有\(\frac{m^{h} - 1}{m - 1}\)个结点

\[等比数列求和:\frac{a_1(1 - q^n)}{1 - q} \\ 因此结点数最多为: \frac{m^0(1 - m^h)}{1- m} = \frac{m^h - 1}{m - 1} \]

  • 具有n个结点的m叉树的最小高度为\(\lceil \log_m(n(m - 1) + 1) \rceil\)

\[\frac{m^{h - 1} - 1}{m - 1} < n \le \frac{m^h - 1}{m - 1} \\ m^{h - 1} < n(m - 1) + 1 \le m^h \\ h - 1 < log_m(n(m - 1) + 1) \le h \\ (h \ge log_m(n(m - 1) + 1)) \\ h_{min} = \lceil log_m(n(m - 1) + 1) \rceil \]

  • 结点数与度之间的关系
    • 总结点数 = $ n_0 + n_1 + n_2 + \cdots + n_m $
    • 总分支数 = $ 1n_1 + 2n_2 + \cdots + mn_m $
    • 总结点数 = 总分支数 + 1

二叉树

概念

  • 二叉树为有序树,左右子树有区别
  • 二叉树可以为空,度为2的树至少有三个结点(其中至少有一个结点有两个孩子)
  • 满二叉树,高度为h,且含有$ 2^h - 1$个结点的二叉树
    • 对于编号为i的结点,若有双亲,为$ \lfloor i / 2 \rfloor \(,若有左孩子,为\) 2i \(,若有右孩子,为\) 2i + 1 $
  • 完全二叉树,高度为h,有n个结点的二叉树,每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应
    • 若$ i \le \lfloor n / 2 \rfloor $,则结点i为分支结点,否则为叶子结点
    • 若某结点i只有左孩子没有右孩子,则编号大于i的结点均为叶子结点
    • 若n为奇数,则每个分支结点都有左孩子和右孩子,若n为偶数,则编号最大的分支结点只有左孩子(编号为$ n / 2 $)

性质

  • 非空二叉树上的叶子结点数等于度为2的结点数加1,即$ n_0 = n_2 + 1$(用结点数 = 度数和 + 1证明)
  • 非空二叉树上第k层至多有$ 2^{k - 1} (k \ge 1)$个结点
  • 高度为h的二叉树至多有$ 2^h - 1 $个结点(等比数列求和)
  • 完全二叉树,从上到下,从左到右编号为$ 1, 2, \ldots , n $
    • $ i > 1 \(,双亲结点\) \lfloor i / 2 \rfloor $
    • 左孩子$ 2i \(, 右孩子\) 2i + 1 $
    • 结点i所在的层次(深度)为$ \lfloor log_2i \rfloor + 1 $
    • 具有n个结点的完全二叉树高度为 $ \lceil log_2(n + 1) \rceil $ 或 $ \lfloor log_2n \rfloor + 1 $

    \[设高度为h,则有2^{h - 1} - 1 < n \le 2^h - 1 或 h^{h - 1} \le n < 2^h \\ 得2^{h - 1} < n + 1 \le h,h为正整数,则h = \lceil log_2(n + 1) \rceil \\ 或得 h - 1 \le log_2n < h,则h = \lfloor log_2n \rfloor + 1\\ (由此可以计算结点i的深度) \]

    • 完全二叉树的构成,结点数为奇数,只含有度为0和2的结点,结点数为偶数,含有一个度为1的结点和若干度为0和2的结点
  • 含有n个结点的二叉链表中,含有n + 1个空链域

有2n个指针,只有n — 1条边,因此有n + 1个空链域

表示

  • 顺序存储,用连续的地址存放,用0占位
    20220717114100
  • 完全二叉树的顺序存储,下标从1开始,详见堆
  • 链式存储结构
struct node {
    int val;
    node *left, *right;
};

基本操作

  • 先序遍历
void preOrder(node* root) {
    if (root == nullptr) return;
    visit(root);
    preOrder(root->left);
    preOrder(root->right);
}
  • 中序便利
void inOrder(node* root) {
    if (root == nullptr) return;
    inOrder(root->left);
    visit(root);
    inOrder(root->right);
}
  • 后序遍历
void postOrder(node* root) {
    if (root == nullptr) return;
    postOrder(root->left);
    postOrder(root->right);
    visit(root);
}
  • 先序遍历(非递归算法)教程
void preOrder(node* root) {
    stack<node*> stk;
    while (root != nullptr || !stk.empty()) {
        while (root != nullptr) {
            visit(root);
            stk.push(root);
            root = root->left;
        }
        root = stk.top();
        stk.pop();
        root = root->right;
    }
}
  • 中序便利(非递归算法)教程
void inOrder(node* root) {
    stack<node*> stk;
    while (root != nullptr || !stk.empty()) {
        while (root != nullptr) {
            stk.push(root);
            root = root->left;
        }
        root = stk.top();
        stk.pop();
        visit(root);
        root = root->right;
    }
}
  • 后序遍历(非递归算法)教程
void postOrder(node* root) {
    stack<node*> stk;
    node* pre; // 用来记录上一个出栈的元素
    while (root != nullptr || !stk.empty()) {
        while (root != nullptr) {
            stk.push(root);
            root = root->left;
        }
        root = stk.top();
        stk.pop();
        // 栈顶元素出栈的前提,右子树为空或者刚刚被访问
        if (root->right == nullptr || root->right == pre) {
            visit(root);
            pre = root;
            root = nullptr; // 置空,下一轮继续取栈顶元素
        } else {
            stk.push(root); // 未满足出栈条件,不能出栈
            root = root->right; // 下一轮模拟递归访问右子树
        }
    }
}
  • 层次遍历
void levelOrder(node* root) {
    queue<node*> q;
    q.push(root);
    while (!q.empty()) {
        node* cur = q.front();
        q.pop();
        visit(cur);
        if (root->left != nullptr) q.push(root->left);
        if (root=>right != nullptr) q.push(root->right);
    }
}
  • 层次遍历(控制到层)
vector<vector<int>> levelOrder(node* root) {
    vector<vector<int>> a;
    queue<node*> q;
    q.push(root);
    while (!q.empty()) {
        vector<int> temp;
        int cnt = q.size();
        while (cnt--) {
            node* cur = q.front();
            temp.push_back(cur->val); // visit(cur)
            q.pop();
            if (cur->left != nullptr) q.push(cur->left);
            if (cur->right != nullptr) q.push(cur->right);
        }
        a.push_back(temp);
    }
    return a;
}

线索二叉树

表示

struct node {
    int val;
    node *left, *right;
    int ltag, rtag; // 0表示左右孩子,1表示前驱后继
};

基本操作

  • 构造中序线索二叉树
void inThread(threadNode* &root, threadNode* &pre) {
    if (root == nullptr) return;
    inThread(root->left, pre); // 此时的pre是原来的,不是root,理解中序遍历左孩子的结点的前驱
    if (root->left == nullptr) {
        root->left = pre;
        root->ltag = 1;
    }
    if (pre != nullptr && pre->right == nullptr) {
        pre->right = root;
        pre->rtag = 1;
    }
    pre = root;
    inThread(root->right, pre);
}

void createInThread(threadNode* root) {
    threadNode* pre = nullptr;
    if (root != nullptr) {
        inThread(root, pre);
        pre->right = nullptr;
        pre->rtag = 1;
    }
}
  • 遍历中序线索二叉树
threadNode* firstNode(threadNode* p) {
    while (p->ltag == 0) p = p->left;
    return p;
}

threadNode* nextNode(threadNode* p) {
    if (p->rtag == 0) return firstNode(p->right);
    else return p->right;
}

void inOrder(threadNode* root) {
    threadNode* cur = firstNode(root);
    while (cur) {
        visit(cur);
        cur = nextNode(cur);
    }
}
  • 构造先序线索二叉树
void preThread(threadNode* &root, threadNode* &pre) {
    if (root == nullptr) return;
    if (root->left == nullptr) {
        root->left = pre;
        root->ltag = 1;
    }
    if (pre != nullptr && pre->right == nullptr) {
        pre->right = root;
        pre->rtag = 1;
    }
    pre = root;
    // 在遍历前前后线索就已经修改,在遍历前保证是左右孩子
    if (root->ltag == 0) preThread(root->left, pre);
    if (root->rtag == 0) preThread(root->right, pre);
}

void createPreThread(threadNode* root) {
    threadNode* pre = nullptr;
    if (root != nullptr) {
        preThread(root, pre);
        pre->right = nullptr;
        pre->rtag = 1;
    }
}
  • 遍历先序线索二叉树
threadNode* nextNode(threadNode* p) {
    if (p->ltag == 0) return p->left;
    if (p->rtag == 0) return p->right;
    else return p->right;
}

void preOrder(threadNode* root) {
    threadNode* cur = root;
    while (cur) {
        visit(cur);
        cur = nextNode(cur);
    }
}

树和森林

表示

  • 双亲表示法,用一组连续的空间来存储每个节点,每个节点除了存储数据外,另设一指针(数组下标)指向其双亲结点位置,根节点下标为0,其指针指向-1

二叉树表的顺序存储中数组下标还表示了结点的位置关系,二叉树也是树,因此二叉树都可以用树的存储结构来存储,但树却不可以用二叉树的存储结构来存储

20220717114119

const int MAXN = 100;
struct node {
    int val;
    int parent;
};
struct PTree[
    node Tree[MAXN];
    int n;// 结点数
]
  • 孩子表示法,将每个节点的孩子用单链表连接起来形成一个线性结构
  • 孩子兄弟表示法,包含三部分,指向结点第一个孩子的指针,指向结点下一个兄弟结点的指针
    20220717114132
struct node {
    int val;
    node *firstChild, *nextsibling;
};

树、森林与二叉树的转换

树和二叉树

  • 左指针指向它的第一个孩子,右指针指向它在树中的相邻的右兄弟(参考孩子兄弟表示法),左孩子右兄弟
  • 由于根节点没有兄弟,因此根节点没有右子树
    20220717114151

森林和二叉树

  • 类似树与二叉树,先将每棵树转换为二叉树,然后将二叉树连在根节点的右子树上

树和森林的遍历

森林 二叉树
先根遍历 先序遍历 先序遍历
后根遍历 中序遍历 中序遍历
  • 树的遍历
    • 先根遍历,先访问根节点,再依次遍历根节点的没棵子树,遍历序列与这棵树对应的先序序列相同
    • 后根遍历,先依次遍历根节点的每棵子树,再访问根节点,遍历序列与这棵树相应二叉树的中序序列相同
  • 森林的遍历,依次遍历各树,遍历逻辑与树的遍历相同

并查集

表示

  • 用连续地址存放,指向父结点
const int N = 100;
int father[N];

基本操作

  • 初始化
for (int i = 1; i <= N; i++) {
    father[i] = i;
}
  • 查找
void findFather(int x) {
    int a = x;
    while (x != father[x]) x = father[x];

    while (a != father[a]) {
        int z = a;
        a = father[a];
        father[z] = x;
    }
    return x;
}

  • 合并
void Union(int a, int b) {
    int faA = findFather(a);
    int faB = findFather(b);
    if (faA != faB) father[faA] = faB;
}

二叉查找树(BST)

概念

  • 要么是一棵空树
  • 要么二叉查找树由根节点、左子树、右子树组成,其中左子树和右子树都是二叉查找树(左右子树可能为空),且左子树上所有节点的数据域均小于或等于根结点的数据域,右子树上所有结点的数据域均大于根节点的数据域

性质

  • 最好的平均查找长度(AVL)为$ O(log_2n) \(,最坏平均查找长度为\) O(n) $
  • 与二分查找相比,二分查找平均执行时间为$ O(log_2n) \(,但是其删除和插入结点的代价为\) O(n) $,因此当有序表是静态时,使用顺序表,采用二分查找实现查找操作,当有序表时动态时,使用二叉搜索树
  • 平均查找长度(ASL)的计算
    20220717114217

基本操作

  • 查找
void search(node* root, int x) {
    if (root == nullptr) {
        cout << "failed" << endl;
        return;
    }
    if (x == root->val) cout << root->val << endl;
    else if (x < root->val) search(root->left, x);
    else search(root->right, x);
}
  • 插入
// 注意root是引用,要修改root的值
void insert(root* &root, int x) {
    if (root == nullptr) {
        root = new node(x);
        return;
    }
    if (x == root->val) return;
    else if (x < root->val) insert(root->left, x);
    else insert(root->right, x);
}
  • 建立
node* createTree(vector<int> a) {
    node* root = nullptr;
    for (auto v : a) insert(root, v);
    return root;
}
  • 删除
    • 基本思想,找左子树最大的或者右子树最小的覆盖根节点,然后递归删除左子树或右子树中选出的结点
node* findMax(node* root) {
    while (root->right != nullptr) root = root->right;
    return root;
}

node* findMin(node* root) {
    while (root->left != nullptr) root = root->left;
    return root;
}

void deleteNode(node* &root, int x) {
    if (root == nullptr) return; // 当前root为空,不存在权值为x的结点,直接返回
    if (root->val == x) { // 找到要删除的结点,进入删除处理
        if (root->left == nullptr && root->right == nullptr) {
            delete(root);
            root = nullptr; // case1: 为叶子结点,直接删除
        }
        else if (root->left != nullptr) { // case2: 存在左孩子,找前驱pre,让前驱覆盖root,左子树中递归删除pre
            node* pre = findMin(root->left);
            root->val = pre->val;
            deleteNode(root->left, pre->val);
        } else { // case3: 不存在左子树,又不为叶子结点,那么存在右子树,找后继next,让后继覆盖root,右子树中递归删除next
            node* next = findMax(root->right);
            root->val = next->val;
            deleteNode(root->right, next->val);
        }
    } else if (x < root->val) deleteNode(root->left, x); // 左子树中查找root
    else deleteNode(root->right, x); // 右子树中查找root
}

平衡二叉树(AVL树)

概念

  • 左子树与右子树的高度之差的绝对值不超过1,其中左子树与右子树的高度只差为该节点的平衡因子

性质

  • 平均查找长度为$ O(log_2n) $
  • 平衡二叉树结点数的递推公式,$ n_h $为构造此高度的平衡二叉树所需要的最少结点数,关于该公式,递归着理解,假设根节点在n层,为了结点数最少,左子树为n - 1,右子树为n - 2,再递归地推左右子树,得到该递推公式,即根节点加左子树加右子树

\[n_0 = 0, n_1 = 1, n_2 = 2, n_h = 1 + n_{h - 1} + n_{h - 2} \]

表示

struct node {
    int val, height; // 结点的初始高度为1
    node *left, *right;

    node() {} // 重载,不然不能初始化空结点
    node(int _val) : val(_val), height(1), left(nullptr), right(nullptr) {}
};

基本操作

  • 高度
int getHeight(node* root) {
    return root == nullptr ? 0 : root->height;
}
  • 平衡因子
int getBalanceFactor(node* root) {
    return getHeight(root->left) - getHeight(root->right);
}
  • 更新树高
void updateHeight(node* root) {
    root->height = max(getHeight(root->left), getHeight(root->right)) + 1;
}
  • 查找(同BST)
void search(node* root, int x) {
    if (root == nullptr) {
        cout << "failed" << endl;
        return;
    }
    if (x == root->val) cout << root->val << endl;
    else if (x < root->val) search(root->left, x);
    else search(root->right, x);
}
  • 左旋

20220717114236

void L(node* &root) {
    node* temp = root->right;
    root->right = temp->left;
    temp->left = root;
    updateHeight(root);
    updateHeight(temp);
    root = temp;
}
  • 右旋

20220717114249

void R(node* &root) {
    node* temp = root->left;
    root->left = temp->right;
    temp->right = root;
    updateHeight(root);
    updateHeight(temp);
    root = temp;
}
  • 插入,在插入中可能导致失衡的情况分为下面四种(注意:在旋转的过程中可能出现新的不平衡的树,只要最后的树平衡即可,在递归返回时,先遇到靠近叶子结点的结点,因此调整时优先调整底部的失衡节点

20220717114328
20220717114350

树形 判断方法 调整
LL BF(root) == 2 && BF(root->left) == 1 R(root)
LR BF(root) == 2 && BF(root->left) == -1 L(root->left);R(root)
RR BF(root) == -2 && BF(root->right) == -1 L(root)
RL BF(root) == -2 && BF(root-right) == 1 R(root->right);L(root)
void insert(node* &root, int x) {
    if (root == nullptr) {
        root = new node(x);
        return;
    }
    // 递归出栈是自底向上,因此遇到的最接近的失衡结点(叶子处)的第一个结点就开始调整了,只要把最靠近失衡节点的结点进行调整,路径上的所有结点就会平衡(实际上继续出栈时仍检查了其它结点)
    if (x < root->val) {
        insert(root->left, x);
        updateHeight(root);
        if (getBalanceFactor(root) == 2) {
            if (getBalanceFactor(root->left) == 1) { // LL
                R(root);
            } else if (getBalanceFactor(root->left) == -1) { // LR
                L(root->left);
                R(root);
            }
        }
    } else {
        insert(root->right, x);
        updateHeight(root);
        if (getBalanceFactor(root) == -2) {
            if (getBalanceFactor(root->right) == -1) { // RR
                L(root);
            } else if (getBalanceFactor(root->right) == 1) { // RL
                R(root->right);
                L(root);
            }
        }
    }
}
  • 建树
node* createTree(vector<int> a) {
    node* root = nullptr;
    for (auto v : a) insert(root, v);
    return root;
}

概念

  • 堆是一棵完全二叉树,树中每个结点的值都不小于(或大于)其左右孩子的值(大顶堆和小顶堆)
  • 每个结点的值都是以它为根节点的子树的最小值,堆一般用于实现优先队列

表示

const int MAXN = 100;
// 下标从1开始,堆大小为n
int heap[MAXN], n = 10;

基本操作

以大顶堆为例

  • 向下调整(时间复杂度:$ O(log_2n) $,与左右孩子比较)
void downAdjust(int low, int high) {
    int i = low, j = i * 2;
    while (j <= high) {
        if (j + 1 <= high && heap[j + 1] > heap[j]) {
            j = j + 1;
        }
        if (heap[j] > heap[i]) {
            swap(heap[j], heap[i]);
            i = j;
            j = i * 2;
        } else break;
    }
}
  • 建堆,$ [1, \lfloor n / 2 \rfloor] \(都是非叶子结点,于是可以从\) \lfloor n / 2 \rfloor \(位置开始倒着枚举结点,每次遍历结点从\) [i, n] $范围内的调整,倒着枚举,可以保证每个节点都是以其为根节点的子树中的最大的结点
void createHeap() {
    for (int i = n / 2; i >= 1; i--) downAdjust(i, n);
}
  • 删除堆顶的最大元素
void deleteTop() {
    heap[1] = heap[n--];
    downAdjust(1, n);
}
  • 向上调整(时间复杂度:$ O(log_2n) $,与父节点比较)
void upAdjust(int low, int high) {
    int i = high, j = i / 2;
    while (j >= low) {
        if (heap[j] < heap[i]) {
            swap(heap[j], heap[i]);
            i = j; 
            j = i / 2;
        } else break;
    }
}
  • 插入元素,插入最后一个节点的后面再向上调整
void insert(int x) {
    heap[++n] = x;
    upAdjust(1, n);
}
  • 堆排序,取出堆顶元素与最后一个元素交换,然后进行一次针对堆顶元素的向下调整,如此重复,直到堆顶元素中只剩一个元素(大顶堆排升序,小顶堆排降序
void heapSort() {
    createHeap();
    for (int i = n; i > 1; i--) {
        swap(heap[i], heap[1]);
        downAdjust(1, i - 1);
    }
}

哈夫曼树

概念

  • 带权路径长度(WPL):从树的根到任意结点的路径长度(经过的边数)与该结点上的权值的乘积,注意区分WPL和构建的树的根节点的值,根节点的值不等于WPL,WPL要每一层每一层的算

\[WPL = \sum_{i - 1}^{n}w_il_i \]

  • 哈夫曼树:带权路径长度最小的二叉树,也称最优二叉树,注意哈夫曼树的树形不一定只有一种结构,但哈夫曼树没有度为1的结点
    20220717114535
  • 哈夫曼编码
    • 固定长度编码:每个字符都用相等长度的二进制位表示,可变长度编码,允许对不同字符用不等长的二进制位表示
    • 对频率高的字符赋以短编码,对频率低的字符赋以长编码,起到压缩数据的效果
    • 前缀编码,没有一个编码是另一个编码的前缀,码串可以被唯一的翻译
    • 解码,构造哈夫曼树后,左子树的边为0,右子树的边为1,向下一直到叶节点,剩下的码串继续该过程

性质

  • 每个初始结点最终都成了叶节点,且权值越小的结点到根节点的路径长度越大
  • 构造过程中新建了n - 1个结点(两两相连,连了n - 1次),因此哈夫曼树的结点总数为2n - 1

基本操作

  • 已知出现频次,要求构建哈夫曼树
// 仅模拟效果,没有构建出真正的树
int wpl(vector<int> a) {
    // c++默认为大顶堆,对于哈夫曼树,要构建小顶堆
    // priority_queue<int> q;
    // 小顶堆
    priority_queue<int, vector<int>, greater<int>> q;
    for (auto v : a) q.push(v);
    int x, y, ans = 0;
    while (q.size() > 1) {
        x = q.top();
        q.pop();
        y = q.top();
        q.pop();
        q.push(x + y);
        ans += (x + y);
    }
    return ans;
}
posted @ 2022-07-17 11:46  Patrickhao  阅读(111)  评论(0)    收藏  举报