树的内容比较多,我一步步积累到这篇文章中
数据结构
最基本的二叉树,结构体中分别有基本的左孩子节点,有孩子节点,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去寻找最长单词。