首页 > 其他分享 >【二叉搜索树】K型与KV型二叉搜索树简单实现

【二叉搜索树】K型与KV型二叉搜索树简单实现

时间:2024-09-02 19:21:59浏览次数:12  
标签:right cur parent 二叉 搜索 KV key root left

关于我:在这里插入图片描述


睡觉待开机:个人主页

个人专栏: 《优选算法》《C语言》《CPP》
生活的理想,就是为了理想的生活!
作者留言

PDF版免费提供倘若有需要,想拿我写的博客进行学习和交流,可以私信我将免费提供PDF版。
留下你的建议倘若你发现本文中的内容和配图有任何错误或改进建议,请直接评论或者私信。
倡导提问与交流关于本文任何不明之处,请及时评论和私信,看到即回复。


参考目录


我们先从宏观上看一看二叉搜索树究竟是个什么东西???他究竟是个什么东西。
可能很多人已经了解到他就是一个用来查找的数据结构而已,我想我下面从整体查找上能够让大家更好的对其进行定位,做到心中有数。

1.定位

我们知道,查找是日常生活中应用广泛的一种操作,对应到我们程序中来,就是在一堆数据中查找某个值呗。
那么说到查找,常见的查找有:二分查找、二叉搜索树查找、哈希查找、跳表查找、多叉树查找…(如下图)
在这里插入图片描述
那,二叉搜索树是个什么东西???

2.认识

首先,二叉搜索树又称二叉排序树

它满足下面特点,如果不满足那就不是二叉搜索树

  • 空树
  • 是二叉树且左子树所有结点的值比根小、右子树所有结点的值比根大

如果满足上述特点中的一条,那么就属于二叉搜索树
不过这里需要强调一点的是,一定要看清楚是不是左子树的所有结点的值都比根小,比如下图
在这里插入图片描述

在说完他的特点之后,我们来简单说一说他的优点和缺点。
至于优点嘛,比较适合用来查找东西,效率比较高,一般情况下能够达到O(logN)
缺点也是有的,特殊场景下,他的查找效率会退化为O(N),这样的话就直接跟遍历查找差不多了。为了说明这个情况,请看下面图例:
在这里插入图片描述

那么这个二叉搜索树能够用来干什么呢?
答案肯定可以用来查找啊,其次他的中序遍历结果就是排序升序,还可以用来查询某个值是否存在于二叉搜索树中、用一个词来查找另一个词(比如常见的中英翻译词典)、再比如说统计一篇文章中各个词出现的次数。

3.分类

从分类上来说,二叉搜索树可以分为两种:
第一种是K型二叉搜索树、另一种是KV型二叉搜索树

K型二叉搜索树:只有一个key值,这个key值要求不能重复存入二叉搜索树中,一般用来快速查找某个值是否存在于二叉搜索树中。
KV型二叉搜索树:有两个值,分别是key值和val值,key值不允许重复,val值不允许重复,一般用来key值来找另外一个val值,当然还可以用来统计一篇文章中出现了各个词出现的次数,在本篇文章中就做出示例代码。

4.二叉搜索树的常见操作

一个数据结构常见的操作就是增删查改,下面我们依次来进行介绍。

4.1插入

在这里插入图片描述
具体实现如下,我们可以这样写代码:

//插入
bool Insert(const K& key)
{
	//1.空树
	if (_root == nullptr)
	{
		_root = new Node(key);
	}
	//2.一般情况
	//2.a 寻找该插入的位置
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_val < key)//比较大往右走
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_val > key)//比较小往左走
		{
			parent = cur;
			cur = cur->_left;
		}
		else//相等,返回false
		{
			return false;//一般情况下,K型二叉搜索树不允许数据冗余
		}
	}

在这里我需要强调几点:
首先,在树中没有值的时候要进行特殊处理。
其次,要注意结点之间的链接问题,要做好链接工作。
之后 ,我们需要强调在一般的二叉搜索树中,key值不允许重复。

为了能够方便的打印这棵树,我们再写一个中序遍历。因为二叉搜索树的中序遍历恰好就是有序嘛,可以方便验证我们写的程序是否正确。

4.2中序遍历

private:
	void _InOrder(Node* pcur)
	{
		if (pcur == nullptr)
		{
			return;
		}

		_InOrder(pcur->left);
		cout << pcur->_val << " ";
		_InOrder(pcur->right);
	}

public:
	void InOrder()//嵌套子函数,可以拿到_root使用
	{
		_InOrder(_root);
	}

这个地方我需要说明一点至于为什么要嵌套一层函数去调用中序遍历的原因。
这是因为要拿到私有_root成员,这样写比较方便。
当然也还有其他方法,
比如说你可以把调用中序遍历的函数设置为该类的友元函数,当然这种方法比较挫,因为本身调用函数和该类关系不大,算是一种弱关联。
还有可以学习Java一样,提供一个GetRoot函数,当然,这样也可以不失为一种好方法,只不过CPP不太习惯这种写法罢了。
再之后,把root作为缺省参数是不行的,因为访问root需要用到this作为跳板,但是this指针和缺省都属于函数参数,语法上是不支持的。
之后,就是用我这种函数嵌套的方法去处理,这种方法在CPP中是很常见的,CPP比较习惯用这种方法进行处理。

我们继续再写一个查找接口。

4.3查找

怎么实现查找接口呢?
在这里插入图片描述

bool Find(const K& key)
{
	Node* pcur = _root;

	while (pcur)
	{
		if (pcur->_val < key)
		{
			pcur = pcur->right;
		}
		else if (pcur->_val > key)
		{
			pcur = pcur->left;
		}
		else
		{
			return true;//此时找到了,返回true
		}
	}

	return false;//此时没有找到返回false
}

接下来是重头戏,删除接口。
删除接口的情况比较多,因此算是本节中重点了。

4.4删除(重点)

在这里插入图片描述
对于第一种情况,
● 找到删除的结点和其父节点
● 弄明白删除结点还有左孩子还是右孩子
● 弄明白要删除结点对于其父节点来说是左孩子还是右孩子

对于第二种情况,
● 先找到要删除的结点及其父结点
● 再找到要替换的结点(一般而言左子树的最大值或者右子树的最小值就符合条件)及其父结点
● 两者交换
● 做好链接工作并删除

但是其中有个特殊情况,就是删除的结点恰好是根,所以需要对应进行特殊处理:
对于第一种情况,
ⅰ. 把根指向下一个结点
ⅱ. 修正parent,此时parent为空,不修正会对parent进行解引用操作
在这里插入图片描述

对于第二种情况,
ⅰ. 修正parent=cur
ⅱ. 要再次判断rightMin是其父亲的左孩子还是右孩子
在这里插入图片描述
所以实现是下面这样的:

//Erase
bool Erase(const K& key)
{
   Node* parent = nullptr;
   Node* cur = _root;
   while (cur)
   {
   	if (cur->_val < key)//比较大往右走
   	{
   		parent = cur;
   		cur = cur->_right;
   	}
   	else if (cur->_val > key)//比较小往左走
   	{
   		parent = cur;
   		cur = cur->_left;
   	}
   	else//相等,找到了就开始准备删除
   	{
   		//1.只有一个孩子/没有孩子的情况
   		//左为空,父亲指向我的右
   		if (cur->_left == nullptr)
   		{
   			if (cur == _root)//删除的是根的情况
   			{
   				_root = cur->_right;
   			}
   			else//删除的不是根
   			{
   				//具体父亲的左孩子指向我的右还是右孩子指向我的右呢?
   				if (parent->_left == cur)//如果我是父亲的左孩子,那么父亲的左孩子就去继承我的右
   				{
   					parent->_left = cur->_right;
   				}
   				else//如果我是父亲的右孩子,那么父亲的右孩子就去继承我的右
   				{
   					parent->_right = cur->_right;
   				}
   			}

   			delete cur;
   		}
   		else if(cur->_right == nullptr)//右为空,父亲指向我的左
   		{
   			//具体父亲的左孩子指向我的左还是右孩子指向我的左呢?
   			if (cur == _root)//删除的是根的情况
   			{
   				_root = cur->_left;
   			}
   			else//删除的不是根
   			{
   				if (parent->_left == cur)//如果我是父亲的左孩子,那么父亲的左孩子就去继承我的左
   				{
   					parent->_left = cur->_left;
   				}
   				else//如果我是父亲的右孩子,那么父亲的右孩子就去继承我的左
   				{
   					parent->_right = cur->_left;
   				}
   			}
   			delete cur;
   		}
   		else//2.我有两个孩子的情况
   		{
   			//此时替换法删除
   			Node* rightMin = cur->_right;
   			Node* rightMinParent = cur;//坑,如果下面while循环没进去,写空的话就会被解引用了
   			while (rightMin->_left)//找合适的替换值
   			{
   				rightMinParent = rightMin;
   				rightMin = rightMin->_left;
   			}

   			//交换
   			swap(cur->_val, rightMin->_val);

   			//删除前做好链接工作
   			if (rightMinParent->_left == rightMin)
   			{
   				rightMinParent->_left = rightMin->_right;
   			}
   			else
   			{
   				rightMinParent->_right = rightMin->_right;
   			}

   			//删除结点
   			delete rightMin;
   		}//end of two children

   		return true;
   	}//end of cur->_val == key
   }

   return false;
}

5.K型二叉搜索树的代码

//K型二叉搜索树
namespace Key
{
	//结点
	template<class K>
	struct BinarySearchNode
	{
		struct BinarySearchNode* _left;
		struct BinarySearchNode* _right;
		K _val;

		BinarySearchNode(const K& key)
			:_left(nullptr)
			,_right(nullptr)
			,_val(key)
		{}
	};

	//二叉搜索树
	template<class K>
	class BSTree
	{
		typedef struct BinarySearchNode<K> Node;
	private:
		Node* _root = nullptr;
	public:
		//插入
		bool Insert(const K& key)
		{
			//1.空树
			if (_root == nullptr)
			{
				_root = new Node(key);
			}
			//2.一般情况
			//2.a 寻找该插入的位置
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_val < key)//比较大往右走
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_val > key)//比较小往左走
				{
					parent = cur;
					cur = cur->_left;
				}
				else//相等,返回false
				{
					return false;//一般情况下,K型二叉搜索树不允许数据冗余
				}
			}

			//既不是空树,也不是相同的数字
			//2.b链接
			Node* newnode = new Node(key);
			if (parent->_val > key)
			{
				parent->_left = newnode;
			}
			else
			{
				parent->_right = newnode;
			}
			cur = parent = newnode = nullptr;

			return true;
		}// end of Insert

		//中序遍历对外调用接口
		void InOrder()
		{
			_InOrder(_root);
		}//end of InOrder

		//查找
		bool Find(const K& key)
		{
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_val < key)//比较大往右走
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_val > key)//比较小往左走
				{
					parent = cur;
					cur = cur->_left;
				}
				else//相等,找到了返回true
				{
					return true;
				}
			}

			return false;
		}

		//Modify
		//注:二叉搜索树不允许去修改,后面KV型可以修改V值

		//Erase
		bool Erase(const K& key)
		{
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_val < key)//比较大往右走
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_val > key)//比较小往左走
				{
					parent = cur;
					cur = cur->_left;
				}
				else//相等,找到了就开始准备删除
				{
					//1.只有一个孩子/没有孩子的情况
					//左为空,父亲指向我的右
					if (cur->_left == nullptr)
					{
						if (cur == _root)//删除的是根的情况
						{
							_root = cur->_right;
						}
						else//删除的不是根
						{
							//具体父亲的左孩子指向我的右还是右孩子指向我的右呢?
							if (parent->_left == cur)//如果我是父亲的左孩子,那么父亲的左孩子就去继承我的右
							{
								parent->_left = cur->_right;
							}
							else//如果我是父亲的右孩子,那么父亲的右孩子就去继承我的右
							{
								parent->_right = cur->_right;
							}
						}

						delete cur;
					}
					else if(cur->_right == nullptr)//右为空,父亲指向我的左
					{
						//具体父亲的左孩子指向我的左还是右孩子指向我的左呢?
						if (cur == _root)//删除的是根的情况
						{
							_root = cur->_left;
						}
						else//删除的不是根
						{
							if (parent->_left == cur)//如果我是父亲的左孩子,那么父亲的左孩子就去继承我的左
							{
								parent->_left = cur->_left;
							}
							else//如果我是父亲的右孩子,那么父亲的右孩子就去继承我的左
							{
								parent->_right = cur->_left;
							}
						}
						delete cur;
					}
					else//2.我有两个孩子的情况
					{
						//此时替换法删除
						Node* rightMin = cur->_right;
						Node* rightMinParent = cur;//坑,如果下面while循环没进去,写空的话就会被解引用了
						while (rightMin->_left)//找合适的替换值
						{
							rightMinParent = rightMin;
							rightMin = rightMin->_left;
						}

						//交换
						swap(cur->_val, rightMin->_val);

						//删除前做好链接工作
						if (rightMinParent->_left == rightMin)
						{
							rightMinParent->_left = rightMin->_right;
						}
						else
						{
							rightMinParent->_right = rightMin->_right;
						}

						//删除结点
						delete rightMin;
					}//end of two children

					return true;
				}//end of cur->_val == key
			}

			return false;
		}


	private:
		//中序遍历的主要逻辑,这里为了封闭性考虑,不把中序遍历的核心接口暴露出去
		//中序遍历
		void _InOrder(const Node* root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->_left);
			cout << (*root)._val << " ";
			_InOrder(root->_right);
		}//end of _InOrder
		
	}; //end of class BSTree

	//BSTree专用的测试接口
	void BSTreeTest()
	{
		int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
		BSTree<int> tree;

		for (const auto& e : a)
		{
			tree.Insert(e);
		}
		tree.InOrder();

		cout << endl << endl;
		cout << tree.Find(1) << endl;
		cout << tree.Find(100) << endl;
		for (const auto& e : a)
		{
			tree.Erase(e);
			tree.InOrder();
			cout << endl;
		}

	}//end of BSTreeTest
}//end of namespace Key

6.KV型二叉搜索树的代码及其应用示例

namespace KV
{
	//结点
	template<class K, class V>
	struct BinarySearchTreeNode
	{
		struct BinarySearchTreeNode<K, V>* _left;
		struct BinarySearchTreeNode<K, V>* _right;
		K _key;
		V _val;

		BinarySearchTreeNode(const K& key, const V& val)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _val(val)
		{}
	};

	//树
	template <class K, class V>
	class BinarySearchTree
	{
		typedef struct BinarySearchTreeNode<K, V> Node;
	private:
		Node* _root = nullptr;
	public:
		bool Insert(const K& key, const V& val)
		{
			//空树情况
			if (_root == nullptr)
			{
				Node* newnode = new Node(key, val);
				_root = newnode;
				newnode = nullptr;

				return true;
			}

			//一般情况
			//找出要插入位置的父结点
			Node* parent = nullptr, *cur = _root;
			while (cur)
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else//相等情况返回false
				{
					cout << key << ":该种情况被终止" << endl;
					return false;
				}
			}
			//链接
			Node* newnode = new Node(key, val);
			if (parent->_key > key)
			{
				parent->_left = newnode;
			}
			else
			{
				parent->_right = newnode;
			}
			newnode = cur = parent = nullptr;

			return true;
		}//end of Insert

		void InOrder()
		{
			_InOrder(_root);
		}
	private:
		void _InOrder(const Node* root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << " " << root->_val << " ";
			_InOrder(root->_right);
		}//end of _InOrder

	public:
		bool Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else
				{
					return true;
				}
			}

			return false;
		}//end of Find

		bool Erase(const K& key)
		{
			Node* cur = _root, * parent = nullptr;
			while (cur)
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					//找到了相同的值,准备删除
					
					if (cur->_left == nullptr)//要删除的左子树为空,那么父亲结点接过他的右子树就行
					{
						if (parent == nullptr)
						{
							parent = cur;
							_root = cur->_right;
						}

						if (parent->_left == cur)
						{
							parent->_left = cur->_right;
						}
						else
						{
							parent->_right = cur->_right;
						}
					}
					else if (cur->_right == nullptr)//要删除的右子树为空,那么父亲结点接过他的左子树就行
					{
						if (parent == nullptr)
						{
							parent = cur;
							_root = cur->_left;
						}

						if (parent->_left == cur)
						{
							parent->_left = cur->_left;
						}
						else
						{
							parent->_right = cur->_left;
						}
					}
					else
					{
						//这种情况下需要替换法删除
						Node* leftMax = cur->_left;
						Node* leftMaxParent = cur;
						while (leftMax->_right)
						{
							leftMaxParent = leftMax;
							leftMax = leftMax->_right;
						}

						//交换
						swap(leftMax->_key, cur->_key);
						swap(leftMax->_val, cur->_val);

						//链接并删除
						if (leftMaxParent->_left == leftMax)
						{
							leftMaxParent->_left = leftMax->_left;
						}
						else
						{
							leftMaxParent->_right = leftMax->_left;
						}
						delete leftMax;
					}

					return true;
				}
			}//end of while

			return false;
		}//end of Erase

	};//end of class BinarySearchTree

	void Test()
	{
		BinarySearchTree<int, string> tree;
		int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
		string str[] = { "8", "3", "1", "10", "6", "4", "7", "14", "13" };
		for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++)
		{
			tree.Insert(a[i], str[i]);
			tree.InOrder();
			cout << endl;
		}

		cout << tree.Find(1) << endl;
		cout << tree.Find(100) << endl;
		cout << tree.Find(109) << endl;
		cout << "-------------------------------" << endl;
		for (auto e : a)
		{
			tree.Erase(e);
			cout << endl;
			tree.InOrder();
		}
	}
}//end of namespace KV

然后他的一个应用是用一个词搜索对应的另一个词,还有就是统计一篇文章中各个词出现了多少次。

void TestBSTree3()
{
// 输入单词,查找单词对应的中文翻译
BSTree<string, string> dict;
dict.Insert("string", "字符串");
dict.Insert("tree", "树");
dict.Insert("left", "左边、剩余");
dict.Insert("right", "右边");
dict.Insert("sort", "排序");
// 插入词库中所有单词
string str;
while (cin>>str)
{
BSTreeNode<string, string>* ret = dict.Find(str);
if (ret == nullptr)
{
cout << "单词拼写错误,词库中没有这个单词:" <<str <<endl;
}
else
{
cout << str << "中文翻译:" << ret->_value << endl;
}
}
}
void TestBSTree4()
{
// 统计水果出现的次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
BSTree<string, int> countTree;
for (const auto& str : arr)
{
// 先查找水果在不在搜索树中
// 1、不在,说明水果第一次出现,则插入<水果, 1>
// 2、在,则查找到的节点中水果对应的次数++
//BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
}


好的,如果本篇文章对你有帮助,不妨点个赞~谢谢。
在这里插入图片描述


EOF

标签:right,cur,parent,二叉,搜索,KV,key,root,left
From: https://blog.csdn.net/2302_79031646/article/details/141826575

相关文章

  • Java二叉树的遍历以及最大深度问题
    Java学习+面试指南:https://javaxiaobear.cn1、树的相关概念1、树的基本定义树是我们计算机中非常重要的一种数据结构,同时使用树这种数据结构,可以描述现实生活中的很多事物,例如家谱、单位的组织架构、等等。树是由n(n>=1)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为......
  • 算法与数据结构——二叉树
    二叉树二叉树(binarytree)是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。structTreeNode{ intval; //节点值 TreeNode*left; //左子结点指......
  • Java 实现二叉树展平为链表
    Java实现二叉树展平为链表前言问题背景解决方案代码实现代码分析结论使用原地算法(O(1)空间复杂度)将二叉树展平为链表问题描述解决方案代码实现代码分析优化思路结论前言在处理二叉树数据结构时,有时需要将其转换成一种特殊的形态,即链表。这种转换可以简化某些......
  • 使用zig语言制作简单博客网站(七)右边栏分类点击和文章搜索
    分类点击事件处理后端代码sqlite数据库article表新增cate_name字段CREATETABLEarticle(idINTEGERPRIMARYKEYAUTOINCREMENT,titleVARCHAR(1024)NOTNULL,descriptionVARCHAR(1024),contentTEXTNOTNUL......
  • R语言 Pubmed搜索 相关文章数
    #Gene_Filter_Pubmed.R基因在Pubmed搜索使用R语言,搜索在pubmed中,发表的SCI文章的数量;(用来判断自己所研究课题,潜在同类文章数量;)工程目录结构:项目目录结构:rm(list=ls())gc()options(stringsAsFactors=F) ##加载自定义R包source("./R_Function/R_Packages_Manage.......
  • Python语言 Google学术搜索 相关文章数
    使用Python语言,搜索在Google学术中,发表的SCI文章的数量;(用来判断自己所研究课题,潜在同类文章数量;)工程目录结构:项目目录结构:importsysimporttimefromseleniumimportwebdriverfromselenium.webdriver.chrome.optionsimportOptionsimportreimportwarningsfroms......
  • 折腾 Quickwit,Rust 编写的分布式搜索引擎-官方配置详解
    Nodeconfiguration(节点配置)节点配置允许您为集群中的各个节点自定义和优化设置。它被分为几个部分:常规配置设置:共享的顶级属性Storage(存储)设置:在storage部分定义https://quickwit.io/docs/configuration/node-config#storage-configurationMetastore(元存储)设置:在metastore......
  • 折腾 Quickwit,Rust 编写的分布式搜索引擎-官方教程
    快速上手在本快速入门指南中,我们将安装Quickwit,创建一个索引,添加文档,最后执行搜索查询。本指南中使用的所有Quickwit命令都在CLI参考文档中进行了记录。https://quickwit.io/docs/main-branch/reference/cli使用Quickwit安装程序安装QuickwitQuickwit安装程序会自动为您......
  • 折腾 Quickwit,Rust 编写的分布式搜索引擎 - 从不同的来源摄取数据
    摄取API在这节教程中,我们将介绍如何使用IngestAPI向Quickwit发送数据。要跟随这节教程,您需要有一个本地的Quickwit实例正在运行。https://quickwit.io/docs/get-started/installation要启动它,请在终端中运行./quickwitrun。创建索引首先,我们创建一个无模式的索引。#Crea......
  • 折腾 Quickwit,Rust 编写的分布式搜索引擎 - 可观测性之日志管理
    Quickwit从底层构建,旨在高效地索引非结构化数据,并在云存储上轻松搜索这些数据。此外,Quickwit开箱即支持OpenTelemetrygRPC和HTTP(仅protobuf)协议,并提供了一个RESTAPI,可以接收任何JSON格式的日志。这让Quickwit成为了日志的理想选择!.https://quickwit.io/docs/guides/......