C++ 各类树的算法

树的内容比较多,我一步步积累到这篇文章中

数据结构

最基本的二叉树,结构体中分别有基本的左孩子节点,有孩子节点,val数据值等基本内容

二叉搜索树:左孩子要比父节点小,右孩子要比父节点大。由于树构建完成后是有序的,所以插入,删除,查找都会有很大的优势。

举一个二叉搜索树的例子:当我们实现排名的数据结构的时候,我们发现,对于任意一个根节点,左子树的排名都比自己小,右子树的排名都比自己大。也就是说,比如1~10000人的排名,数据为10000个人的分数。如果想要查找我们的位置,显然很容易。除此之外,我们可以利用它计算排名。当我们进入右子树的时候,左边的分数都是比自己小的,不考虑,当我们进入左子树的时候,右子树的分数都比自己大,count右子树的节点,将会获得排名在自己后面的人数。

字典树:(字符串树)常用于存储字符串,查询效率比哈希树高。它的特点是不同的单词之间可以共享字符串。比如add和am共享a。这样如果把一个单词存储为从根节点的子节点到叶节点,节点数量会大大减少。根节点的子节点一共才26个。在这里插入图片描述

平衡二叉树
平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。一句话表述为:以树中所有结点为根的树的左右子树高度之差的绝对值不超过1。将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。

但是虽然平衡二叉树平衡度高,但是当插入或删除的时候,会由多次旋转才可以得到,改变结构。

红黑树😗*(查找效率:O(log n))**红黑树是在二叉搜索树的基础上,为节点添加颜色,并且令根节点和叶节点必须是黑色,其余点可以是黑色或者是红色。并加入两个限制。红色的点不能相邻,也就是不互相为父子节点。另一个就是任何一个节点到它所有的叶节点,黑节点数量必须相同。这就限制了最长路径不能多于最短路径的两倍。对于搜索、插入、删除有优势。

哈夫曼树:哈夫曼树是对0、1数据存储压缩的最优方式,比如我们将储存的数据的使用频率作为权值,那么权值小的在哈夫曼树中往往深度比较大,表示所占的存储大,权值大的相反。我们用无重复前缀的0、1字符串就可以结合权值构建出哈夫曼树。

B树:定义一个B树为m阶,那么根节点最少有两个子节点,每个节点最少有m/2个子节点,最多m+1个。每个节点存储数据,叶节点在同一层存储空指针。插入删除是自下而上的。

B+树:B+树常常用于磁盘,SQL查询存储等等,因为其对于查找,删除,插入具有稳定的复杂度,因为所有存储数据的节点都是叶节点,其他节点只是存储了key值。

B+树适合存储索引的原因:https://blog.csdn.net/weixin_30531261/article/details/79312676

数据结构的实现

二叉树的节点实现,和链表的实现节点很像。

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

字典树的实现,相当于一个26叉树,每一个子节点对应一个英文字母,并取isWord标识来确定是否存储过该单词,比如我们存储了“apple”,来区分我们存储过"app"

class Trie{
public:
    Trie* next[26]={nullptr};
    bool isWord;
    Trie():isWord(false){}
};

leetcode 例题

leetcode104:二叉树的最大深度
在这里插入图片描述

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if(root == nullptr){
            return 0;
        }else{
            return max(maxDepth(root->left), maxDepth(root->right)) + 1;
        }
    }
};

做树的题目最适用并且最常用的方法就是递归。但是我们如果想快速的做题目,要非常清楚一点,递归必要的是中止条件,比如该题,我做的时候没有分清是不是应该写if(root.left==nullptr && root.right==nullptr){return1}出错了以后改成上述代码。我们应该清楚是将叶节点是不是没有孩子来判断结束,还是递归到叶节点的孩子,判断其是否为空。

这个题目出错的地方就在于,这个递归是同时调用左孩子和右孩子,并没有区分只有一个孩子的情况。如果只有一个孩子,另外一个空节点的左孩子要做判断就会出错,应该判断空节点是否为空。所以递归的结束是判断该节点是否为空,而不是判断是否没有孩子节点。

leetcode 96:不同的二叉搜索树
在这里插入图片描述

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n+1, 0);
        dp[0] = 1;
        dp[1] = 1;
        for(int i =2; i <= n; i++){
            for(int j = 0; j < i; j++){
                dp[i] += dp[j]*dp[i-j-1];
            }
        }
        return dp[n];
    }
    
};

上述算法为什么没有用到递归呢,因为这道题目的结果和数字大小没有关系,而是和数字的个数有关系。比如你知道左子树有四个节点,你不需要知道他们的值,你只需要知道四个,他们的组合数就是确定的。那么我们从1个数,两个数到n个数,可以逐步算出结果。不需要递归算左右子树,而是用for循环谁做根节点,同时也就知道了左子树有几个数,右子树几个数,并且都比已知的个数少。

leetcode 95:不同的二叉搜索树

class Solution {
public:
    vector<TreeNode*> generateTrees(int start, int end) {
        if (start > end) {
            return { nullptr };
        }
        vector<TreeNode*> allTrees;
        // 枚举可行根节点
        for (int i = start; i <= end; i++) {
            // 获得所有可行的左子树集合
            vector<TreeNode*> leftTrees = generateTrees(start, i - 1);

            // 获得所有可行的右子树集合
            vector<TreeNode*> rightTrees = generateTrees(i + 1, end);

            // 从左子树集合中选出一棵左子树,从右子树集合中选出一棵右子树,拼接到根节点上
            for (auto& left : leftTrees) {
                for (auto& right : rightTrees) {
                    TreeNode* currTree = new TreeNode(i);
                    currTree->left = left;
                    currTree->right = right;
                    allTrees.emplace_back(currTree);
                }
            }
        }
        return allTrees;
    }

    vector<TreeNode*> generateTrees(int n) {
        if (!n) {
            return {};
        }
        return generateTrees(1, n);
    }
};

这道题目需要用到递归了,和上道题差不多的思想。有一个特点,递归的时候,很多标准答案的做法,就是做题目发现递归需要的参数和题目给的函数声明不一致,所以往往会重载函数,定义需要的参数,并且在原函数中调用最开始的范围。

leetcode 720:词典中的最长单词

class Trie{
public:
    Trie* next[26]={nullptr};
    bool isWord;
    Trie():isWord(false){}
};
class Solution {
    string ans;
public:
    string longestWord(vector<string>& words) 
    {
       Trie* root = new Trie();
       Trie* cur;
       for(auto str : words){
           cur = root;
           for(auto char_word : str){
               if(cur->next[char_word - 'a'] == nullptr){
                   cur->next[char_word - 'a'] = new Trie();
               }
               cur = cur->next[char_word - 'a'];
           }
           cur->isWord = true;
       }
       string temp;
       int i , j;
       for(int i = 0; i < 26; i++){
           temp = "";  //temp要在树中都走一遍,temp相当于从根节点到当前走的位置构成的单词
           if(root->next[i]!=nullptr && root->next[i]->isWord){dfs(root->next[i], temp, i);}//遍历根节点的26个字母,并且保证字母顺序
       }
       return ans;
    }

    void dfs(Trie* root, string &temp, int i){
        if(root == nullptr){
            return;
        }
        if(root->isWord){  //题目要求单词要由一个个字母递增出来,所以每次都要判断是否为单词,否则压栈要在条件外面
            temp.push_back('a'+i);
            if(temp.size() > ans.size()){
                ans = temp;   //ans为我们存储的当前最长单词
            }
            for(int j = 0; j < 26; j++){
                dfs(root->next[j], temp, j);  //向下一层搜索
            }
            temp.pop_back();  //返回上一级节点
        }
    }
};

首先构建一个字典树,然后通过dfs去寻找最长单词。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值