数据结构-树

基础↓

树的定义

为了完整地建立有关树的基本概念,以下给出两种树的定义,即自由树和有根有序树。

自由树(free tree)

一棵自由树Tr可定义为一个二元组Tr=(V,E),其中V={v1,v2,…,vm}是由n(n>0)个元素组成的有限非空集合,称为顶点(vertex)集合,v:(1≤i≤n)称为顶点。E={(v:,v,)|v:,v,∈V,1≤i,j≤n}是由n一1个元素组成的序对集合,称为边集合,E中的元素(v;,v,)称为边(edge)或分支(branch)。E使得Tr成为一个连通图。

image-20231108202455869

有根树(rooted tree)

一棵有根树T,简称树,它是n(n≥0)个结点的有限集合。当n=0时,T
称为空树;否则,T是非空树。

其中,r是T的一个特殊结点,称为根(root)。T1,T2,…,Tm是除r之外其他结点构成的互不相交的m(m≥0)个子集,每个子集也是一棵树,称为根的子树(subtree)。每棵子树的根结点有且仅有一个直接前驱(即它的上层结点),但可以有0个或多个直接后继(即它的下层结点),m称为r的分支数。

image-20231108203050463

点(node)

它包含数据项及指向其他结点的分支。例如在图5.2©中的树总共
13个结点。为方便起见,每个数据项用单个字母表示。

结点的度(degree)

结点所拥有的子树棵数。例如在图5.2©所示的树中,根A的度为3,结点E的度为2,结点K,L,F,G,M,I,J的度为0。

叶结点(leaf)

即度为0的结点,又称终端结点。例如,在图5.2©所示的树中,
{K,L,F,G,M,I,J}构成树叶结点的集合。

分支结点(branch)

除叶结点外的其他结点,又称非终端结点。例如在图5.2©所示的树中,A,B,C,D,E,H就是分支结点。

子女结点(child)

若结点x有子树,则子树的根结点即为结点x的子女。例如在图5.2©所示的树中,结点A有3个子女,结点B有2个子女,结点L没有子女。

父结点(parent)

若结点x有子女,它即为子女的父结点。例如在图5.2©所示的树中,结点B,C,D,E有一个父结点,根结点A没有父结点。

兄弟结点(sibling)

同一父结点的子女互称为兄弟。例如在图5.2©所示的树
中,结点B,C,D为兄弟,结点E,F也为兄弟,但结点F,G,H不是兄弟结点。

祖先结点(ancestor)

从根结点到该结点所经分支上的所有结点。例如在图5.2©所示的树中,结点L的祖先为A,B,E。

子孙结点(descendant)

某一结点的子女,以及这些子女的子女都是该结点的子孙。例如在图5.2©所示的树中,结点B的子孙为E,F,K,L。

结点所处层次(level)

简称结点的层次,即从根到该结点所经路径上的分支条数。例如在图5.2©所示的树中,根结点在第1层,它的子女在第2层。树中任一结点的层次为它的父结点的层次加1。结点所处层次也称结点的深度。

树的深度(depth)

树中距离根结点最远的结点所处层次即为树的深度。空树的
深度为0,只有一个根结点的树的深度为1,图5.2©所示的树的深度为4。

树的高度(height)

很多数据结构教科书定义树的高度等同于树的深度,本书则从下向上定义高度。叶结点的高度为1,非叶结点的高度等于它的子女结点高度的最大值加1,这样可定义树的高度等于根结点的高度。高度与深度计算的方向不同,但数值相等。

有序树(ordered tree)

树中结点的各棵子树t1,t2…是有次序的,即为有序树。其中,t1为根的第1棵子树,t2为根的第2棵子树…。

无序树(unordered tree)

树中结点的各棵子树之间的次序是不重要的,可以互相交换位置。

森林(forest)

m(m≥0)棵树的集合。在自然界,树与森林是两个不同的概念,但在数据结构中,它们之间的差别很小。删除一棵非空树的根结点,树就变成森林(不排除空的森林);反之,若增加一个根结点,让森林中每棵树的根结点都变成它的子女,森林就成为一棵树。

二叉树的定义以及基础性质

子女

二叉树的特点是每个结点最多有两个子女,分别称为该结点的左子女和右子女。就是说,在二叉树中不存在度大于2的结点,并且二叉树的子树有左、右之分,其子树的次序不能颠倒。因此,二叉树是分支数最大不超过2的有序树。

性质
image-20231108221814874 image-20231108221823721 image-20231108221726086 image-20231108221741976 image-20231108221835718 image-20231108221843500

二叉树的储存

完全二叉树的数组储存表示

(如数组或列表)表示的二叉树遵循一定的储存规则,通常称为二叉树的数组表示或顺序存储。

在这种表示方式下,二叉树的节点按照层序遍历的顺序依次存储在数组中,同时保持二叉树的形状。具体规则如下:

  1. 根节点存储在数组的第一个位置(通常是索引 0)。

  2. 对于任意节点在数组中的索引为 i,其左子节点存储在索引 2 * i + 1 处,右子节点存储在索引 2 * i + 2 处。

  3. 如果某个节点没有左子节点或右子节点,相应的数组位置将保持为空(null)或者放置一个特殊的标记来表示该节点为空。

image-20231108223202206
一般二叉树的数组储存形式
image-20231108223625653
二叉树的链接存储形式
image-20231108223713488
struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}};
image-20231108223953492
 //把线性表示改成二叉树的节点形式
TreeNode* buildTree(const vector<int>& values, int index) {
        if (index >= values.size()) return nullptr;
        TreeNode* node = new TreeNode(values[index]);
        node->left = buildTree(values, 2 * index + 1);
        node->right = buildTree(values, 2 * index + 2);
        return node;
    }//index代表索引
// 直接定义二叉树的节点表示
class TreeNode {
public:
    int val;
    TreeNode* left;
    TreeNode* right;

    TreeNode(int value) : val(value), left(nullptr), right(nullptr) {}
};

// 用于构建示例二叉树的函数
TreeNode* buildSampleTree() {
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);
    root->right->left = new TreeNode(6);
    root->right->right = new TreeNode(7);
    return root;
}
image-20231106144907287
//不要求掌握
// 解析广义表字符串并构建二叉树
TreeNode* buildTreeFromGeneralizedString(const std::string& s) {
    std::stack<TreeNode*> nodeStack;
    TreeNode* root = nullptr;
    TreeNode* currentNode = nullptr;
    bool isLeftChild = true;

    for (char c : s) {
        if (c == '(') {
            if (currentNode) {
                nodeStack.push(currentNode);
                currentNode = nullptr;
                isLeftChild = true;
            }
        } else if (c == ')') {
            if (!nodeStack.empty()) {
                currentNode = nodeStack.top();
                nodeStack.pop();
                isLeftChild = false;
            }
        } else if (c == ',') {
            isLeftChild = false;
        } else {
            currentNode = new TreeNode(c);
            if (!root) {
                root = currentNode;
            } else if (isLeftChild) {
                currentNode->left = currentNode;
            } else {
                currentNode->right = currentNode;
            }
        }
    }

    return root;
}

// 中序遍历并打印二叉树节点值
void inorderTraversal(TreeNode* root) {
    if (root) {
        inorderTraversal(root->left);
        std::cout << root->val << " ";
        inorderTraversal(root->right);
    }
}

int main() {
    std::string generalizedString = "A(B(D,E),C(F,G))";
    TreeNode* root = buildTreeFromGeneralizedString(generalizedString);

    std::cout << "Inorder Traversal: ";
    inorderTraversal(root);
    std::cout << std::endl;

    return 0;
}

image-20231106145031526

二叉树的部分算法

template<class T>
struct BinTreeNode {
    T data;
    BinTreeNode<T>* leftChild;
    BinTreeNode<T>* rightChild;

    BinTreeNode() : leftChild(nullptr), rightChild(nullptr) {}

    BinTreeNode(T x, BinTreeNode<T>* l = nullptr, BinTreeNode<T>* r = nullptr) : data(x), leftChild(l), rightChild(r) {}
};

template<class T>//BinTreeNode<T>*:表示指向 BinTreeNode 类型的指针。
void BinaryTree<T>::destroy(BinTreeNode<T>*& subTree) {//&:表示引用,它将指针本身作为引用传递给函数。在函数内部,对引用指针的任何修改都会影响到原始的指针。
    // 保护函数:若指针subTree不为空,则删除根为subTree的子树
    if (subTree != nullptr) {
        destroy(subTree->leftChild); // 递归删除subTree的左子树
        destroy(subTree->rightChild); // 递归删除subTree的右子树
        delete subTree; // 递归subTree
        subTree = nullptr;
    }
}
//如果不使用 &,而只是使用 BinTreeNode<T>* subTree,那么传递进函数的将会是一个指针的副本,而不是指针的引用。
template <class T>
BinTreeNode<T>* BinaryTree<T>::Parent(BinTreeNode<T>* subTree, BinTreeNode<T>* current) {
    // 在子树subTree中搜索结点current的父结点。若找到则函数返回父结点地址,否则函数返回NULL
    if (subTree == nullptr) {
        return nullptr;
    }

    // 根结点无父结点
    if (subTree->leftChild == current || subTree->rightChild == current) {
        return subTree;
    }

    BinTreeNode<T>* p;

    // 递归在左子树中搜索
    p = Parent(subTree->leftChild, current);
    if (p != nullptr) {
        return p;
    } else {
        // 递归在右子树中搜索
        return Parent(subTree->rightChild, current);
    }
}

二叉树的遍历

image-20231106145241966image-20231109091503447

中序遍历

中序遍历(In-order Traversal)是一种二叉树遍历算法,用于按照从左到右的顺序访问二叉树的节点。在中序遍历中,首先遍历左子树,然后访问当前节点,最后遍历右子树。这种遍历顺序通常用于获取按照升序排列的二叉搜索树(BST)中的节点。

中序遍历的核心特点是从根节点开始,然后先遍历左子树,再遍历根节点,最后遍历右子树。具体的遍历顺序是从左子树的最底部节点(最左侧的叶节点)开始,然后逐渐向上访问父节点,直到最后访问右子树的最底部节点(最右侧的叶节点)。

image-20231106150418046

#include <iostream>

// 定义二叉树节点类
class TreeNode {
public:
    int val;
    TreeNode* left;
    TreeNode* right;

    TreeNode(int value) : val(value), left(nullptr), right(nullptr) {}
};

// 中序遍历函数
void inorderTraversal(TreeNode* root) {
    if (root) {
        inorderTraversal(root->left); // 递归遍历左子树
        std::cout << root->val << " "; // 访问当前节点
        inorderTraversal(root->right); // 递归遍历右子树
    }
}

int main() {
    // 创建一个示例二叉树
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);

    std::cout << "Inorder Traversal: ";
    inorderTraversal(root); // 中序遍历并打印二叉树节点值
    std::cout << std::endl;

    return 0;
}

前序排列
image-20231106151350622
后序排列
image-20231106151412409 image-20231106151427010
template <class T>
int BinaryTree<T>::Height(BinTreeNode<T>* subTree) {
    // 保护函数:计算以subTree为根的二叉树的高度
    if (subTree == nullptr) {
        return 0; // 递归结束:空树高度为0
    } else {
        int i = Height(subTree->leftChild);
        int j = Height(subTree->rightChild);
        return (i < j) ? (j + 1) : (i + 1);
    }
}

二叉树的输出

广义表形式输出

#include <iostream>

// 定义二叉树节点类
class TreeNode {
public:
    char val;
    TreeNode* left;
    TreeNode* right;

    TreeNode(char value) : val(value), left(nullptr), right(nullptr) {}
};

// 以广义表形式输出节点类的广义表
void printGeneralizedExpression(TreeNode* root) {
    if (root != nullptr) {
        std::cout <<  root->val;
        if (root->left || root->right) {
            std::cout << "(";
            printGeneralizedExpression(root->left);
        }
        if (root->right) {
            std::cout << ",";
            printGeneralizedExpression(root->right);
        }
        std::cout << ")";
    }
}

int main() {
    // 创建一个示例二叉树
    TreeNode* root = new TreeNode('A');
    root->left = new TreeNode('B');
    root->right = new TreeNode('C');
    root->left->left = new TreeNode('D');
    root->left->right = new TreeNode('E');

    // 以广义表形式输出二叉树
    std::cout << "Generalized Expression: ";
    printGeneralizedExpression(root);
    std::cout << std::endl;

    return 0;
}
//Generalized Expression: (A (B (D) (E)) (C))

补充知识↓

深度优先搜索(DFS)和广度优先搜索(BFS)是两种常用的图形搜索算法,用于遍历或搜索图中的节点。

深度优先搜索(DFS):

DFS 是一种递归算法,它从图中的某一起始顶点开始遍历图中的节点。DFS 探索图的深度,一直到达图中某一路径的末端,然后回溯并继续探索其他路径。在 DFS 中,使用栈或递归实现。基本思想是:从起始顶点开始,遍历其中一个相邻节点,再从该节点开始继续深度遍历,直到达到最深处,然后回溯到上一个节点,继续遍历其他节点。

广度优先搜索(BFS):

BFS 是一种迭代算法,它从图中的某一起始顶点开始,首先访问所有与起始顶点直接相连的节点,然后再逐层向外拓展遍历。在 BFS 中,使用队列实现。基本思想是:从起始顶点开始,首先访问其所有相邻节点,然后按层次顺序逐个访问相邻节点的相邻节点,直到遍历完整个图。

区别:

  1. 遍历顺序: DFS 沿着深度方向进行遍历,而 BFS 沿着广度方向进行遍历。
  2. 数据结构: DFS 一般使用栈或递归实现,而 BFS 一般使用队列实现。
  3. 适用性: DFS 适用于解决连通图中的路径查找问题,而 BFS 适用于最短路径问题以及对连通图的遍历。

这两种搜索算法在不同的场景下有不同的应用。在寻找连通图的路径或解决深度相关问题时,DFS 是一个很好的选择,而在寻找最短路径或者遍历整个连通图时,BFS 更为合适。

进阶↓

判断树是不是满二叉树或者完全二叉树

一个完全二叉树是一种特殊的二叉树,其中除了最后一层外,其他每一层都被完全填充,并且最后一层的节点都靠左排列。检验一个二叉树是否为完全二叉树可以使用广度优先搜索(BFS)。

下面是一个算法的框架,用于检验给定的二叉树是否是完全二叉树:

#include <queue>

template <class T>
bool isCompleteBinaryTree(BinTreeNode<T>* root) {
    if (root == nullptr) {
        return true; // 空树被认为是完全二叉树
    }

    std::queue<BinTreeNode<T>*> q;
    q.push(root);

    bool flag = false; // 标记,用于判断是否出现空节点

    while (!q.empty()) {
        BinTreeNode<T>* current = q.front();
        q.pop();

        // 如果当前节点为空,标记为出现空节点
        if (current == nullptr) {
            flag = true;
        } else {
            // 如果之前出现过空节点,但当前节点非空,则不是完全二叉树
            if (flag) {
                return false;
            }

            q.push(current->leftChild);
            q.push(current->rightChild);
        }
    }

    return true; // 如果能顺利通过检验,返回true
}

这个算法通过层序遍历二叉树,并检查是否出现空节点。如果出现了空节点,后续节点仍有子节点,则该树不是完全二叉树。如果遍历完成后没有出现这种情况,那么这棵树是完全二叉树。

满二叉树是一种特殊类型的二叉树,其中每个节点要么是叶子节点,要么有两个子节点。可以使用递归的方法来检查一个二叉树是否是满二叉树。

下面是一个用于检验给定二叉树是否是满二叉树的算法:

template <class T>
bool isFullBinaryTree(BinTreeNode<T>* root) {
    if (root == nullptr) {
        return true; // 空树被认为是满二叉树
    }

    // 如果是叶子节点,返回true
    if (root->leftChild == nullptr && root->rightChild == nullptr) {
        return true;
    }

    // 如果有一个子节点为空,返回false
    if (root->leftChild == nullptr || root->rightChild == nullptr) {
        return false;
    }

    // 递归检查左右子树
    return isFullBinaryTree(root->leftChild) && isFullBinaryTree(root->rightChild);
}

这个算法使用递归的方式检查给定的二叉树是否是满二叉树。它会递归检查每个节点,如果一个节点是叶子节点或者有两个子节点,就继续递归检查左右子树,如果任何一个节点不满足这两个条件,则这颗树不是满二叉树。

二叉树的前序遍历递归构建

image-20231106152252183
#include <iostream>
#include <string>

// 定义二叉树节点类
class TreeNode {
public:
    char val;
    TreeNode* left;
    TreeNode* right;

    TreeNode(char value) : val(value), left(nullptr), right(nullptr) {}
};

// 构建二叉树
TreeNode* buildTreePreorder(std::string& preorder, int& index) {
    if (index >= preorder.size()) {
        return nullptr;
    }

    char current = preorder[index++];//先使用index后++
    if (current == '#') {
        return nullptr;
    }

    TreeNode* node = new TreeNode(current);
    node->left = buildTreePreorder(preorder, index);
    node->right = buildTreePreorder(preorder, index);

    return node;
}

// 中序遍历并输出节点值
void inorderTraversal(TreeNode* root) {
    if (root) {
        inorderTraversal(root->left);
        std::cout << root->val << " ";
        inorderTraversal(root->right);
    }
}

int main() {
    std::string preorder = "AB##C##";
    int index = 0;
    TreeNode* root = buildTreePreorder(preorder, index);

    std::cout << "Inorder Traversal: ";
    inorderTraversal(root); // 中序遍历并打印二叉树节点值
    std::cout << std::endl;
    return 0;
}

利用栈进行前后中序遍历的非递归算法

image-20231106203241592
//前序遍历
#include <iostream>
#include <stack>
using namespace std;

struct TreeNode {
    int value;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int val) : value(val), left(nullptr), right(nullptr) {}
};

vector<int> preorderTraversal(TreeNode* root) {
    vector<int> result;
    stack<TreeNode*> nodeStack;

    if (root == nullptr) {
        return result;
    }

    nodeStack.push(root);

    while (!nodeStack.empty()) {
        TreeNode* node = nodeStack.top();
        nodeStack.pop();
        result.push_back(node->value);

        // 注意顺序,先将右子节点入栈,再将左子节点入栈
        if (node->right) {
            nodeStack.push(node->right);
        }
        if (node->left) {
            nodeStack.push(node->left);
        }
    }

    return result;
}

int main() {
    // 创建一个二叉树
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);

    // 进行前序遍历
    vector<int> result = preorderTraversal(root);

    // 输出结果
    for (int val : result) {
        cout << val << " ";
    }
    cout << endl;

    return 0;
}

//中序遍历
template <class T>
void BinaryTree<T>::InOrder_iter() {
    LinkedStack<BinTreeNode<T>*> S;
    BinTreeNode<T>* p = root; // p是遍历指针,从根结点开始

    while (p != nullptr || !S.IsEmpty()) {
        while (p != nullptr) {
            S.Push(p); // 该子树沿途结点进栈
            p = p->leftChild; // 遍历指针进到左子女结点
        }

        if (!S.IsEmpty()) {
            S.Pop(p);
            cout << p->data; // 退栈,访问根结点
            p = p->rightChild; // 遍历指针进到右子女结点
        }
    }
}
//后序遍历
#include <iostream>
#include <stack>
using namespace std;

struct TreeNode {
    int value;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int val) : value(val), left(nullptr), right(nullptr) {}
};

vector<int> preorderTraversal(TreeNode* root) {
    vector<int> result;
    stack<TreeNode*> nodeStack;

    if (root == nullptr) {
        return result;
    }

    nodeStack.push(root);

    while (!nodeStack.empty()) {
        TreeNode* node = nodeStack.top();
        // 注意顺序,先将右子节点入栈,再将左子节点入栈
        if (node->right) {
            nodeStack.push(node->right);
        }
        if (node->left) {
            nodeStack.push(node->left);
        }
        
        nodeStack.pop();
        result.push_back(node->value);
     
    }

    return result;
}

层次序遍历二叉树的算法

层次序遍历二叉树就是从根结点开始,按层次逐层遍历;这种遍历需要使用一个先进先出的队列。在处理上一层时,将其下一层的结点直接进到队列(的队尾)。在上一层结点遍历完后,下一层结点正好处于队列的队头,可以继续访问它们。

image-20231108083605850 image-20231107204959663
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

// 定义二叉树结点
struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

// 层次序遍历二叉树并返回结果
vector<vector<int> > levelOrder(TreeNode* root) {
    vector<vector<int> > result;  // 用于存储层次遍历结果的二维向量
    if (!root) {
        return result;
    }

    queue<TreeNode*> q;  // 创建队列来辅助层次遍历
    q.push(root);  // 将根结点入队

    while (!q.empty()) {
        int levelSize = q.size();  // 当前层次的结点数量
        vector<int> levelNodes;  // 存储当前层次的结点值

        // 遍历当前层次的结点
        for (int i = 0; i < levelSize; ++i) {
            TreeNode* node = q.front();  // 取出队列中的队头结点
            q.pop();  // 出队
            levelNodes.push_back(node->val);  // 将结点值添加到当前层次的向量中

            // 将左右子结点入队
            if (node->left) {
                q.push(node->left);
            }
            if (node->right) {
                q.push(node->right);
            }
        }

        result.push_back(levelNodes);  // 将当前层次的结点值向量添加到结果中
    }

    return result;
}

int main() {
    // 创建一个二叉树
    //       1
    //      / \
    //     2   3
    //    / \
    //   4   5
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);

    // 调用层次遍历函数并获取结果
    vector<vector<int> > result = levelOrder(root);

    // 输出结果
    for (const auto& level : result) {
        for (int val : level) {
            cout << val << " ";
        }
        cout << endl;
    }

    // 释放内存,防止内存泄漏
    delete root->left->right;
    delete root->left->left;
    delete root->right;
    delete root->left;
    delete root;

    return 0;
}

//输出:
//1 
//2 3 
//4 5 

二叉树的计数

给定一个二叉树的前序遍历和中序遍历可以唯一确定一个二叉树

假定给定了一棵二叉树的前序序列( ABHFDECKG)和中序序列 (H B D F A E K C G ) ,根据前序遍历的定义,前序序列的第一个字母 A一定是树的根,又根据中序遍历的定 义 , 字 母 A 把 中 序 序 列 划 分 为 两 个 子序 列 ( ( H B D F) A ( E K C G ) ) , 这 样 可 得 到 对 二叉 树 的 第 一次近似,如图5.20(a)所示。然后,取前序序列的下一个字母B,它出现在A的左子树 中,应是 A的左子树的根,它把中序子序列(HBDF)又划分为两个子序列((H)B(DF)),
这样可得到对二叉树的第二次近似,如图5.20(b)所示 。将这个过程继续下去,最后可以得 到 如 图 5 . 2 0 ( i ) 所 示 的 二叉 树 。

image-20231109143453624
template<class T>
BinTreeNode<T>* createBinaryTree(T* VLR, T* LVR, int n) {
    // VLR 是前序序列,LVR 是中序序列,构造出的二叉树根指针由函数返回
    if (n == 0) {
        return nullptr;
    }

    int k = 0;
    while (VLR[0] != LVR[k]) {
        k++;
    }
    // 在中序序列中寻找根

    BinTreeNode<T>* t = new BinTreeNode<T>(VLR[0]);
    // 创建根结点

    t->leftChild = createBinaryTree(VLR + 1, LVR, k);
    // 从前序VLR+1开始对中序的0~k-1左(子树)序列的k个元素递归建立左子树

    t->rightChild = createBinaryTree(VLR + k + 1, LVR + k + 1, n - k - 1);
    // 从前序VLR+k+1开始对中序的k+1~n-1右(子树)序列的n-k-1个元素建立右子树

    return t;
}

有n个节点的不同二叉树个数为image-20231109151124231

例题

image-20231109152424367
#include <iostream>
#include <stack>
#include <vector>


void FindLongestPath(BinTreeNode<T>* root) {
    if (root == nullptr) {
        return;
    }

    std::stack<BinTreeNode<T>*> nodeStack;
    std::vector<BinTreeNode<T>*> longestPath;
    BinTreeNode<T>* prev = nullptr;

    while (root || !nodeStack.empty()) {
        while (root) {
            nodeStack.push(root);
            root = root->leftChild;
        }

        root = nodeStack.top();

        if (root->rightChild == nullptr || root->rightChild == prev) {
            if (root->leftChild == nullptr && root->rightChild == nullptr && nodeStack.size() > longestPath.size()) {
                longestPath.clear();
                longestPath.assign(nodeStack.size(), nullptr);
                std::copy(nodeStack.cbegin(), nodeStack.cend(), longestPath.begin());
            }

            prev = root;
            nodeStack.pop();
            root = nullptr;
        } else {
            root = root->rightChild;
        }
    }

    // 输出最长路径上的节点值
    std::cout << "Longest Path: ";
    for (const auto& node : longestPath) {
        std::cout << node->data << " ";
    }
}

线索二叉树↓

定义

线索二叉树是对普通二叉树的一种改进,它在节点的指针上加上了线索(thread),以便更高效地执行遍历操作,尤其是中序遍历。在线索二叉树中,部分节点的左右指针指向的不再是其左右子节点,而是指向中序遍历下的前驱和后继节点。左孩子就指向左孩子,如果没有左孩子,就指向前驱。

线索二叉树的定义包括两种类型的线索:前序线索和中序线索。

image-20231109155514861

1. 前序线索二叉树:

在前序线索二叉树中,每个节点的左右指针被修改为指向其前驱和后继节点。其中:

  • 每个节点的 leftChild 指针,如果该节点有左子节点,则指向其左子节点;否则,指向其在前序遍历下的前驱节点。
  • 每个节点的 rightChild 指针,如果该节点有右子节点,则指向其右子节点;否则,指向其在前序遍历下的后继节点。

2. 中序线索二叉树:

在中序线索二叉树中,每个节点的左右指针被修改为指向其前驱和后继节点。其中:

  • 每个节点的 leftChild 指针,如果该节点有左子节点,则指向其左子节点;否则,指向其在中序遍历下的前驱节点。
  • 每个节点的 rightChild 指针,如果该节点有右子节点,则指向其右子节点;否则,指向其在中序遍历下的后继节点。

线索二叉树的设计允许在不使用递归或栈的情况下,执行更快速的中序遍历。通过这些线索,可以在节点之间直接跳转,提高了遍历操作的效率。

template <class T>
struct ThreadNode {
    // 线索二叉树的结点类
    int ltag, rtag; // 线索标志
    ThreadNode<T> *leftChild, *rightChild; // 线索或子女指针
    T data; // 结点中所包含的数据

    ThreadNode(T item) : data(item), leftChild(nullptr), rightChild(nullptr), ltag(0), rtag(0) {
        // 构造函数
    }
};

//获取中序序列下第一个节点
    template <class T>
    class ThreadTree {
    public:
        ThreadNode<T>* First(ThreadNode<T> t) {
            // 函数返回以t为根的中序线索二叉树中中序序列下的第一个结点
            ThreadNode<T>* p = t;
            while (p->ltag == 0) {
                p = p->leftChild;
            }
            // 最左下结点(不一定是叶结点)
            return p;
        }
    
//获取中序序列的下一个节点
        template <class T>
        ThreadNode<T>* ThreadTree<T>::Next(ThreadNode<T>* t) {
            ThreadNode<T>* p = t->rightChild;
            if (t->rtag == 0) {
                return First(p); // 在右子树中找中序下第一个结点
            } else {
                return p; // rtag == 1,直接返回后继线索
            }
        }

//获取中序序列中的最后一个节点
        template <class T>
        ThreadNode<T>* ThreadTree<T>::Last(ThreadNode<T>* t) {
            // 函数返回以 * t 为根的中序线索二叉树中中序序列下的最后一个结点
            ThreadNode<T>* p = t;
            while (p->rtag == 0) {
                p = p->rightChild; // 最右下结点(不一定是叶结点)
            }
            return p;
        }

//获取中序序列状态下前驱节点
        template <class T>
        ThreadNode<T>* ThreadTree<T>::Prior(ThreadNode<T>* t) {
            // 返回中序线索二叉树中结点t在中序下的前驱结点
            ThreadNode<T>* p = t->leftChild;
            if (t->ltag == 0) {
                return Last(p);//p!不是last(t)
            } else {
                return p;
            }
        }

//输出中序遍历
        template <class T>
        void ThreadTree<T>::Inorder(ThreadNode<T>* t) {
            ThreadNode<T>* p;
            for (p = First(t); p != NULL; p = Next(p)) {
                cout << p->data;
            }
            cout << endl;
        }

        template <class T>
        void ThreadTree<T>::createInThread() {
            ThreadNode<T> *pre = NULL; // 前驱结点指针

            if (root != NULL) {
                // 非空二叉树,线索化
                createInThread(root, pre);
                pre->rightChild = NULL; // 后处理中序最后一个结点
                pre->rtag = 1;
            }
        } 
    
};

利用中序遍历对二叉树进行中序线索化

    template <class T>
    void ThreadTree<T>::createInThread(ThreadNode<T>* p, ThreadNode<T>*& pre) {
        // 通过中序遍历,对二叉树进行线索化
        if (p == NULL) return;

        createInThread(p->leftChild, pre); // 递归,左子树线索化

        if (p->leftChild == NULL) {
            // 建立当前结点的前驱线索
            p->leftChild = pre;
            p->ltag = 1;
        }

        if (pre != NULL && pre->rightChild == NULL) {
            // 建立前驱结点的后继线索
            pre->rightChild = p;
            pre->rtag = 1;
        }
        pre = p; // 前驱跟上,当前指针向前遍历

        createInThread(p->rightChild, pre); // 递归,右子树线索化
    }

   

利用中序线索二叉树进行前序遍历

template <class T>
void ThreadTree<T>::PreOrder(ThreadNode<T> p) {
    // Pre-order traversal on the threaded binary tree

    while (p != NULL) {
        cout << p->data;  // Visit the root node

        if (p->ltag == 0) {
            p = p->leftChild;  // If it has a left child, go to the left child
        } else if (p->rtag == 0) {
            p = p->rightChild; // If it has a right child, go to the right child
        } else {
                while (p != NULL && p->rtag == 1) {
                    p = p->rightChild; // Traverse along the right thread until a node with a right child is found
                                        }
                if (p != NULL) {
                    p = p->rightChild; // If there is a right child, move to the right child
                               }
               }
    }
}

树和森林↓

定义

森林是一种图的特殊类型,它由若干棵不相交的树(无环的连通图)组成。每一棵树称为森林的一颗树。在森林中,树与树之间没有边相连,即森林是由多棵树组成的非连通图。

树的父指针表示方式

它以一组连续的存储单元来存放树中的结点,每个结点有两个域:一个是data域,用来存放数据元素;另一个是parent域,用来存放指示其父结点位置的指针。树中结点的存放顺序一般不做特殊要求,但为了操作实现的方便,有时也会规定结点的存放顺序。例如,可以规定按树的前序次序存放树中的各个结点,或规定按树的层次次序安排所有结点。

image-20231110143243332

树的子女链表表示方法

为树中每个结点设置一个子女链表,并将这些结点的数据和对应子女链表的头指针放在一个向量中,就构成了子女链表表示。在这种表示中,有n个结点就有n个子女链表(叶结点的子女链表为空链表)。例如,对于图5.34(a)给出的树,其子女链表表示如图5.35(a)所示。在图5.35©中用虚线箭头标出子女链表中指针的指向。

image-20231110143556901

子女-兄弟链表表示法

它的每个结点由3个域组成:image-20231110144042055

image-20231110144058456
template <class T>
class TreeNode {
public:
    T data;
    TreeNode* firstChild;   // 指向第一个子节点
    TreeNode* nextSibling;  // 指向下一个兄弟节点

    TreeNode(T value) : data(value), firstChild(nullptr), nextSibling(nullptr) {}
};

template <class T>
class Tree {
private:
    TreeNode<T>* root;  // 树的根节点

public:
    Tree() : root(nullptr) {}

    // 在指定节点下添加子节点
    void addChild(TreeNode<T>* parent, T value) {
        TreeNode<T>* newNode = new TreeNode<T>(value);
        if (parent->firstChild == nullptr) {
            parent->firstChild = newNode;
        } else {
            TreeNode<T>* sibling = parent->firstChild;
            while (sibling->nextSibling != nullptr) {
                sibling = sibling->nextSibling;
            }
            sibling->nextSibling = newNode;
        }
    }

    // 获取树的根节点
    TreeNode<T>* getRoot() const {
        return root;
    }

    // 设置树的根节点
    void setRoot(TreeNode<T>* node) {
        root = node;
    }
    
    template <class T>
	TreeNode<T>* Find(TreeNode<T>* p, T value) {
    // 函数返回根为 *p 的子树中值为 value 的节点的地址

    // 如果当前节点的值等于目标值,则直接返回当前节点的地址,即找到了目标节点
    if (p->data == value) {
        return p;
    } else {
        // 否则,在当前节点的各个子树中进行搜索
        TreeNode<T>* q;
        TreeNode<T>* s;

        // 遍历当前节点的子节点
        for (q = p->firstChild; q != NULL; q = q->nextSibling) {
            // 递归调用 Find 函数,在子树中搜索目标值
            s = Find(q, value);

            // 如果在子树中找到了目标值,则返回找到的节点的地址
            if (s != NULL && s->data == value) {
                return s;
            }
        }

        // 如果在当前节点的所有子树中都没有找到目标值,则返回 NULL
        return NULL;
    }
}

};

树、森林与二叉树之间的相互转换

树的子女-兄弟链表表示是一种二叉链表结构,它可对应到一棵二叉树。当然,它们在语义上是不同的,在树的子女-兄弟链表表示中,结点的左指针指示它的第一个子女结点的地址,右指针指示它的兄弟结点的地址,但二叉树的子树不包含这些信息。

image-20231110145629716

由于根结点没有兄弟,所以树转换为二叉树后,二叉树的根的右子树一定为空。将一个森林转换为一棵二叉树的方法:先将森林中的每一棵树转换为二叉树,再将第一棵树的根作为转换后的二叉树的根,第一棵树的左子树作为转换后二叉树根的左子树,第二棵树作为转换后二叉树的右子树,第三棵树作为转换后二叉树根的右子树的右子树,以此类推,森林就可以转换为一棵二叉树.

image-20231110145706718 image-20231110150025474

DFS和BFS↓

树的遍历方式有两种,即深度优先遍历和广度优先遍历。

树的深度优先遍历

树的深度优先遍历通常有两种遍历次序:先根次序(前序)遍历和后根次序(后序)遍历。因为一般的树没有硬性规定子树的先后次序,所以只能人为地假设第一棵子树T1、第二棵子树T2…

image-20231110153940792
先根次序(前序)遍历
image-20231110150436361
template <class T>
void Tree<T>::PreOrder(TreeNode<T> *p) {
    // 先根次序遍历并输出以 *p 为根的树

    // 当树非空时
    if (p != NULL) {
        // 输出根结点数据
        cout << p->data;

        // 遍历以 p 为根的子树,沿子女的兄弟链递归遍历它的子树
        for (p = p->firstChild; p != NULL; p = p->nextSibling) {
            PreOrder(p);l3eelkq1w3ednl`12ehio
        }
    }
}


后根次序(后序)遍历
image-20231110153931257
template <class T>
void Tree<T>::PostOrder(TreeNode<T> *p) {
    // 以指针 p 为根,按后根次序遍历树

    // 当树非空时
    if (p != NULL) {
        // 从根结点的第一个子节点开始沿根子女的兄弟链遍历
        TreeNode<T> *q = p->firstChild;

        while (q != NULL) {
            // 后根遍历各子树,递归调用 PostOrder
            PostOrder(q);
            q = q->nextSibling;
        }

        // 最后访问根结点,输出根结点数据
        cout << p->data;
    }
}

树的广度优先遍历

层次遍历

堆(Heap)↓

优先队列

优先队列(Priority Queue)是一个抽象数据类型,它类似于队列(Queue),但其中的元素具有优先级。这些元素不是按照插入的顺序出队,而是按照其优先级出队。在优先队列中,元素的处理顺序是由其优先级而不是插入顺序决定的。

最小优先队列和最大优先队列是优先队列的两种常见变体:

  1. 最小优先队列:在最小优先队列中,拥有最高优先级的元素(即最小的元素)先被处理。当你从最小优先队列中取出元素时,你会得到最小的元素。
  2. 最大优先队列:在最大优先队列中,拥有最高优先级的元素(即最大的元素)先被处理。当你从最大优先队列中取出元素时,你会得到最大的元素。

这两种优先队列的主要区别在于它们所强调的元素的优先级。最小优先队列强调的是最小元素优先,而最大优先队列强调的是最大元素优先。

在C++的标准模板库(STL)中,std::priority_queue 默认是最大优先队列(即默认以最大值优先),但你可以通过提供自定义的比较器来创建最小优先队列。

//最大优先队列(不要求掌握)
#include <queue>
template <typename T, typename Container = std::vector<T>, typename Compare = std::less<typename Container::value_type>>
class PriorityQueue {
public:
    // 构造函数
    PriorityQueue() : pq() {}

    // 插入元素
    void push(const T& value) {
        pq.push(value);
    }

    // 删除顶部元素
    void pop() {
        pq.pop();
    }

    // 获取顶部元素
    T top() const {
        return pq.top();
    }

    // 检查队列是否为空
    bool empty() const {
        return pq.empty();
    }

    // 获取队列大小
    size_t size() const {
        return pq.size();
    }

private:
    std::priority_queue<T, Container, Compare> pq;
};

//最小优先队列(掌握)
#include <vector>
#include <functional> // For std::greater
#include <stdexcept> // For exceptions

template <typename T>
class MinPriorityQueue {
public:
    // 构造函数
    MinPriorityQueue() : heap() {}

    // 插入元素
    void push(const T& value) {
        heap.push_back(value);
        heapifyUp(heap.size() - 1);
    }

    // 删除顶部元素
    void pop() {
        if (empty()) {
            throw std::out_of_range("Priority queue is empty");
        }
        std::swap(heap[0], heap[heap.size() - 1]);
        heap.pop_back();
        heapifyDown(0);
    }

    // 获取顶部元素
    T top() const {
        if (empty()) {
            throw std::out_of_range("Priority queue is empty");
        }
        return heap[0];
    }

    // 检查队列是否为空
    bool empty() const {
        return heap.empty();
    }

    // 获取队列大小
    size_t size() const {
        return heap.size();
    }

private:
    std::vector<T> heap;

    void heapifyUp(size_t index) {
        while (index > 0) {
            size_t parent = (index - 1) / 2;
            if (heap[parent] > heap[index]) {
                std::swap(heap[parent], heap[index]);
                index = parent;
            } else {
                break;
            }
        }
    }

    void heapifyDown(size_t index) {
        size_t left, right, smallest;
        while (2 * index + 1 < heap.size()) {
            left = 2 * index + 1;
            right = 2 * index + 2;
            smallest = index;

            if (left < heap.size() && heap[left] < heap[smallest]) {
                smallest = left;
            }
            if (right < heap.size() && heap[right] < heap[smallest]) {
                smallest = right;
            }

            if (smallest != index) {
                std::swap(heap[index], heap[smallest]);
                index = smallest;
            } else {
                break;
            }
        }
    }
};




堆的定义

假定在各个数据记录(或元素)中存在一个能够标识数据记录(或元素)的数据项,并将依据该数据项对数据进行组织,则可称此数据项为关键码(key)。

"堆"是一种特殊的树形数据结构,用于实现优先队列,每次出队列的是优先权最高的元素(一定是完全二叉树)。

  1. 最大堆(Max Heap):在最大堆中,每个节点的值都大于或等于其子节点的值。根节点是整个堆中的最大值。
  2. 最小堆(Min Heap):在最小堆中,每个节点的值都小于或等于其子节点的值。根节点是整个堆中的最小值。

image-20231110155734548image-20231110161616145

最小优先队列(Min Priority Queue)中的元素通常是按照优先级(或者说权重)进行存储的。在队列中,具有较小优先级的元素被认为更高优先级,因此在出队时,应该先出队较小优先级的元素。

通常,最小优先队列的实现使用一种数据结构来确保在队列中总是能够快速找到具有最小优先级的元素。常见的实现方式包括二叉堆、斐波那契堆等。

以二叉堆为例:

在二叉堆中,元素被组织成一棵二叉树,并满足堆的性质:任意节点的值小于(或等于)其子节点的值。最小元素通常位于堆的根节点。

存储方式可以采用数组,将堆按照层次遍历的顺序存储在数组中。对于节点 i,其左子节点为 2i+1,右子节点为 2i+2。

举个简单的例子,考虑以下最小优先队列:

Priority:  8   10   5   3   1
Element:   A    B   C   D   E

在这个例子中,元素 A 具有最高的优先级(8 最小),元素 E 具有最低的优先级(1 最大)。这样的队列在存储时可能会按照数组 [8, 10, 5, 3, 1] 存储。在二叉堆中,对应的树形结构如下:

         1
       /   \
      3     5
     / \
    10   8

这个二叉堆满足最小堆的性质,即树中的任意节点的值小于(或等于)其子节点的值。这样,堆的根节点(1)即为最小的元素。

在实际实现中,插入和删除最小元素的操作会保持堆的性质,确保队列中始终能够快速找到最小优先级的元素。

shiftdown

用于在把一个元素放在堆中适当的位置,默认下面是调好的

template <class T, class E>
void MinHeap<T, E>::siftDown(int start, int m) {
    // 从结点 start 开始到 m 为止,自上向下比较,如果子女的值小于双亲的值,则值小的上浮,继续向下层比较,将一个集合局部调整为最小堆

    int i = start;
    int j = 2 * i + 1; // j 是 i 的左子女位置
    Element<T, E> temp = heap[i];

    while (j <= m) {
        // 检查是否到最后位置

        if (j < m && heap[j] > heap[j + 1]) {
            j++;
            // 让 j 指向两子女中的小者
            // 在 Element 声明中定义 ">" 重载函数
        }

        if (temp <= heap[j]) {
            break;
            // 小则不做调整
        } else {
            heap[i] = heap[j];
            i = j;
            j = 2 * j + 1;
            // 否则小者上移,i, j 下降
        }
    }

    heap[i] = temp;
    // 回放 temp 中暂存的元素
}

shiftup

template <class T, class E>
void MinHeap<T, E>::siftUp(int start) {
    // 从结点 start 开始到结点 0 为止,从下向上比较
    // 如果子女的值小于父结点的值,相互交换,重新调整为最小堆
    // 关键码比较符 "<=" 在 Element 中定义

    int j = start;
    int i = (j - 1) / 2;
    Element<T, E> temp = heap[j];

    while (j > 0) {
        // 沿父结点路径向上直达根

        if (heap[i] <= temp) {
            break; // 父结点值小,不需要调整
        } else {
            heap[j] = heap[i];
            j = i;
            i = (i - 1) / 2;
        }
        // 父结点值大,进行调整
    }

    heap[j] = temp; // 将原始位置的元素值回送
}

插入和删除

template <class T, class E>
bool MinHeap<T, E>::Insert(Element<T, E> x) {
    // 共有函数:将 x 插入最小堆中

    if (IsFull()) {
        cerr << "Heap Full!" << endl;
        return false; // 堆满,插入失败
    }

    heap[currentSize] = x; // 将元素 x 插入堆的最后位置
    siftUp(currentSize); // 向上调整,以保持最小堆的性质
    currentSize++; // 堆计数加1

    return true; // 插入成功
}

template <class T, class E>
bool MinHeap<T, E>::RemoveMin(Element<T, E>& x) {
    // 删除最小元素,并将其值存储到 x 中

    if (!currentSize) {
        cout << "Heap empty" << endl;
        return false; // 堆空,返回 false
    }

    x = heap[0]; // 将最小元素的值存储到 x 中
    heap[0] = heap[currentSize - 1]; // 将最后一个元素填补到根结点
    currentSize--; // 堆计数减1

    siftDown(0, currentSize - 1); // 从根结点向下调整为堆

    return true; // 返回最小元素删除成功
}


Huffman树↓

基本原理

Huffman 树以字符出现的频率来构建一个最优的前缀编码。在 Huffman 树中,出现频率高的字符将具有较短的编码,而出现频率低的字符将具有较长的编码,以实现更高效的数据压缩。

路径长度

外部路径长度(External Path Length)

对于一颗有 n 个叶子节点的树,树的外部路径长度是指从根节点到每个叶子节点的路径长度之和。也就是说,外部路径长度是根节点到每个叶子节点路径长度之和。

内部路径长度(Internal Path Length)

树的内部路径长度是指树中每对节点之间的路径长度之和。它是从根节点到每个节点的路径长度之和,但不包括树的所有叶子节点。

路径长度(Path Length)

PL = EPL + IPL

image-20231108085636740
带权路径长度(Weighted Path Length)

带权路径长度(Weighted Path Length)是树中各路径长度与路径上边权重之和的乘积。在这个概念中,树中每条路径的长度由路径上所有边的权重之和确定。

带权路径长度的计算

在带权路径长度中,每个节点之间的路径长度不仅取决于节点的层次关系(深度),还受到树中路径上边的权重影响。对于带权路径长度的计算,考虑以下几点:

  1. 树的边赋权: 在带权路径长度中,树的每条边都被赋予一个权重(或成本)。
  2. 路径长度的计算: 从树的根节点到叶子节点的路径长度是由沿着路径的所有边的权重之和确定的。
  3. 权重和路径长度的乘积: 对于每条路径,其长度(路径上边的权重之和)与路径的长度(边数)的乘积即为带权路径长度。

带权路径长度也与 Huffman 树密切相关。在 Huffman 树中,每个字符的编码长度乘以其出现频率即为该字符在编码过程中的带权路径长度。Huffman 树以最小的带权路径长度构建出最优的编码方案,以实现数据的高效压缩。

image-20231108090328410

构建

  1. 统计字符频率:遍历要编码的数据(如文本),统计每个字符出现的频率。
  2. 构建最小堆:将每个字符及其频率作为节点构建成一颗森林(即初始时每个节点视为一颗独立的树),并根据频率构建最小堆。
  3. 构建 Huffman 树:从最小堆中取出频率最低的两棵树(两个节点),合并为一棵新树,新树的根节点频率为两个节点频率之和。重复此过程,直至最小堆中只剩下一棵树,即 Huffman 树。
  4. 生成编码:从根节点开始遍历 Huffman 树,左子树赋值为 0,右子树赋值为 1,从根节点到每个叶子节点的路径就是对应字符的 Huffman 编码。
#include <iostream>
#include <queue>
#include <vector>

// Huffman 树节点
struct Node {
    char data; // 字符
    unsigned frequency; // 频率
    Node* left;
    Node* right;

    Node(char data, unsigned frequency) : data(data), frequency(frequency), left(nullptr), right(nullptr) {}
};

// 用于最小堆的比较器
struct Compare {
    bool operator()(Node* a, Node* b) {
        return a->frequency > b->frequency;
    }
};

// 生成 Huffman 树
Node* buildHuffmanTree(std::vector<char>& data, std::vector<unsigned>& frequency) {
    std::priority_queue<Node*, std::vector<Node*>, Compare> minHeap;

    for (size_t i = 0; i < data.size(); ++i) {
        Node* newNode = new Node(data[i], frequency[i]);
        minHeap.push(newNode);
    }

    while (minHeap.size() != 1) {
        Node* left = minHeap.top();
        minHeap.pop();

        Node* right = minHeap.top();
        minHeap.pop();

        Node* parent = new Node('$', left->frequency + right->frequency);
        parent->left = left;
        parent->right = right;

        minHeap.push(parent);
    }

    return minHeap.top();
}

// 打印 Huffman 树
void printHuffmanCodes(Node* root, std::string code) {
    if (root == nullptr) {
        return;
    }

    if (root->data != '$') {
        std::cout << root->data << " : " << code << std::endl;
    }

    printHuffmanCodes(root->left, code + "0");
    printHuffmanCodes(root->right, code + "1");
}

int main() {
    std::vector<char> data = {'a', 'b', 'c', 'd', 'e', 'f'};
    std::vector<unsigned> frequency = {5, 9, 12, 13, 16, 45};

    Node* root = buildHuffmanTree(data, frequency);

    std::cout << "Huffman Codes are:\n";
    printHuffmanCodes(root, "");

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值