图解图论介绍及应用(3):图的表示和二叉树的介绍(Airbnb的例子)

点击上方“AI公园”,关注公众号,选择加“星标“或“置顶”


作者:Vardan Grigoryan

编译:ronghuaiyang

导读

知识图谱是AI领域非常有用的一种工具,知识图谱的基础就是图论,从今天开始,给大家介绍一些图论的基础内容,今天是第3篇,图的表示和二叉树的介绍(Airbnb的例子)

树是非常有用的数据结构。你可能没有在项目中从头来实现树。但你可能已经在不知不觉中使用了它们。让我们看一个人工但有价值的例子,并尝试回答“为什么”的问题,“为什么首先使用二叉搜索树”。

正如你所注意到的,在二叉搜索树中有一个“搜索”。所以基本上,所有需要快速查找的东西,都应该放在二叉搜索树中。“应该”并不意味着必须,在编程中要记住的最重要的事情是用合适的工具来解决问题。在很多情况下,具有O(N)查找的简单链表可能比具有O(logN)查找的BST更好。

通常我们会使用BST的库来实现,很可能是c++中的std::set或std::map。然而,在本教程中,我们可以自由地重新发明我们自己的轮子。BSTs几乎可以在任何通用编程语言库中实现。你可以在你最喜欢的语言的相应文档中找到它们。用一个“现实生活中的例子”来说明,这是我们试图解决的问题—Airbnb家庭搜索。

640?wx_fmt=png

浏览Airbnb房屋搜索

我们如何在一些查询的基础上,用一堆过滤器尽可能快地搜索房子。这是一项艰巨的任务。如果我们考虑Airbnb存了400万条记录,情况就会变得更加困难。

640?wx_fmt=png

因此,当用户搜索房屋时,他们有可能“触摸”数据库中存储的400万条记录。当然,搜索结果仅限于网站主页上显示的“热门列表”,用户几乎从来没有“足够好奇”去查看数百万个列表。我没有任何关于Airbnb的分析,但是我们可以在编程中使用一个叫做“assumption”的强大工具。因此,我们假设一个用户通过查看最多1K个家庭就可以找到一个好的家庭。

这里最重要的因素是实时用户的数量,因为它对数据结构和数据库的选择以及整个项目架构都有影响。很明显,如果总共只有100个用户,那么我们可能根本不需要这么做。

相反,如果用户总数(尤其是实时用户)远远超过百万大关,那么我们必须明智地考虑每一个决策。“每一个”的使用都非常正确,这就是为什么公司在追求卓越的服务提供的同时,也在雇佣最优秀的员工。

谷歌、Facebook、Airbnb、Netflix、亚马逊(Amazon)、Twitter以及其他许多公司都要处理大量的数据,要正确地选择每秒向数百万实时用户提供数百万字节的数据,首先要雇佣正确的工程师。这就是为什么我们,程序员,要在可能的面试中与这些数据结构,算法和问题解决作斗争,因为他们所需要的是工程师有能力以最快最有效的方式解决这些大问题。

这是一个用例。用户访问主页(我们仍然在讨论Airbnb),并试图过滤出最合适的房子。我们将如何处理这个问题?(注意这个问题是后端问题,所以我们不关心前端或网络流量、http上的https或home集群上的Amazon EC2等等)。

首先,我们已经熟悉了程序员清单中最强大的工具之一(我们说的是假设而不是抽象),让我们从几个假设开始:

  • 我们处理的数据可以全部放到内存里。

  • 我们的内存足够大。

大到可以装下,嗯,多少钱?这是个好问题。存储实际数据需要多少内存。如果我们要处理400万个数据单元(同样是假设),并且如果我们知道每个单元的大小,那么我们可以很容易地得到所需的内存大小,即4M * sizeof(one_unit)。

让我们考虑一个“home”对象及其属性。实际上,我们至少需要考虑那些我们在解决问题时要处理的属性(“home”是我们的单位)。我们将在一些伪代码中将其表示为c++结构。你可以轻松地将其转换为MongoDB模式对象或任何你想要的对象。我们只讨论属性名称和类型(试着考虑在空间中使用位字段或位集)。

// feel free to reorganize this struct to avoid redundant space	
// usage because of aligning factor	
// Remark 1: some of the properties could be expressed as enums,	
// bitset is chosen for as multi-value enum holder.	
// Remark 2: for most of the count values the maximum is 16	
// Remark 3: price value considered as integer,	
// int considered as 4 byte.	
// Remark 4: neighborhoods property omitted 	
// Remark 5: to avoid spatial queries, we're 	
// using only country code and city name, at this point won't consider 	
// the actual coordinates (latitude and longitude)	
struct AirbnbHome	
{	
  wstring name; // wide string	
  uint price;	
  uchar rating;	
  uint rating_count;	
  vector<string> photos; // list of photo URLs	
  string host_id;	
  uchar adults_number;	
  uchar children_number; // max is 5	
  uchar infants_number; // max is 5	
  bitset<3> home_type;	
  uchar beds_number;	
  uchar bedrooms_number;	
  uchar bathrooms_number;	
  bitset<21> accessibility;	
  bool superhost;	
  bitset<20> amenities;	
  bitset<6> facilities;	
  bitset<34> property_types;	
  bitset<32> host_languages;	
  bitset<3> house_rules;	
  ushort country_code;	
  string city;	
};

上面的结构并不完美(显然),有许多假设和/或不完整的部分。我只是查看了Airbnb的过滤器,并设计了应该存在的属性列表,以满足搜索查询。这只是一个例子。

现在我们应该计算每个“AirbnbHome”对象在内存中占用多少字节。

  • Home 名字 - name 是一个 wstring支持多语言名称/标题,这意味着每个字符将占用2个字节(如果使用其他语言,我们可能不会考虑字符大小,但在c++中 char是1字节字符, wchar是2字节字符)。快速浏览一下Airbnb的列表,我们可以假设一个home名字应该包含100个字符(虽然大多数情况下是50个字符,而不是100个字符),我们将假设100个字符作为最大值,这将导致大约200字节的内存。 uint是4个字节, uchar是1个字节, ushort是2个字节(同样,在我们的假设中)。

  • 照片 - 照片驻留在一些存储服务中,比如Amazon S3(据我所知,这个假设很可能适用于Airbnb,但是Amazon S3只是一个假设)

  • 照片URL - 我们有这些照片的URL,考虑到URL上没有标准大小限制,但实际上有一个众所周知的限制是2083字符,我们将它作为任何URL的最大大小。因此,考虑到每个home平均有5张照片,它将占用约10Kb。

  • 照片ID - 让我们重新考虑一下。通常用相同的基础URL存储服务服务内容,如 http(s)://s3.amazonaws.com/<bucket>/<object>,即有一个常见的模式来构建URL并且我们只需要存储实际的照片的ID。假设我们使用一些独特的ID生成器,它返回一个独立的20字节的字符串ID,照片对象和特定的URL模式照片就会是这样 https://s3.amazonaws.com/some-know-bucket/<unique-photo-id>。这为我们提供了良好的空间效率,因此对于存储5张照片的字符串ID,我们只需要100字节的内存。

  • Host ID - 相同的“技巧”(上图)就能完成 host_id,即主机的用户ID,需要20字节的内存(实际上我们可以为用户使用整数ID,但是考虑到MongoDB等一些数据库系统具有特定的惟一ID发生器,我们假设一个20字节长度字符串ID为某些“中值”,这些值可以适合任何的数据库系统,只需要很小的修改。Mongo的ID长度为24字节)。最后,我们取一个大小为4字节的位集,大小为32到64位的位集,大小为8字节的位集。注意我们的假设,在本例中,我们对任何表示枚举的属性使用了bitset,但是能够接受多个值,换句话说,这是一种多选题复选框。

640?wx_fmt=png

Airbnb的房子设施
  • 设施 - Airbnb每个家庭都有一个可用的设施列表,如“熨斗”、“洗衣机”、“电视”、“wifi”、“衣架”、“烟雾探测器”,甚至“笔记本电脑友好的工作空间”等等。可能有超过20个便利设施,我们坚持使用这20个只是因为这是Airbnb过滤器页面上可过滤的便利设施的数量。Bitset为我们节省了一些空间,如果我们保持适当的设施订购。例如,如果一个家庭拥有上面提到的所有设施(请查看截图中的已检查设施),我们将在位集中相应的位置置一个位。

640?wx_fmt=png

Bitset允许使用20位存储20个不同的值

例如,查看一个家庭是否有“洗衣机”:

bool HasWasher(AirbnbHome* h)	
{	
  return h->amenities[2];	
}

或者更专业一点:

const int KITCHEN = 0;	
const int HEATING = 1;	
const int WASHER = 2;	
//...	
bool HasWasher(AirbnbHome* h)	
{	
  return (h != nullptr) && h->amenities[WASHER];	
}	
	
bool HasWasherAndKitchen(AirbnbHome* h)	
{	
  return (h != nullptr) && h->amenities[WASHER] && h->amenities[KITCHEN];	
}	
	
bool HasAllAmenities(AirbnbHome* h, const std::vector<int>& amenities)	
{	
  bool has = (h != nullptr);	
  for (const auto a : amenities) {	
    has &= h->amenities[a];	
  }	
  return has;	
}

你可以尽可能地改进代码(并修复编译错误)。我们只是想在这个问题上下文中强调位集背后的思想。

  • 房子的规则,房子的类型 - 同样的想法(我们在便利设施领域实现的)适用于“House rules”、“Home Type”和其他类型。

  • 国家代码,城市名称 - 最后,国家代码和城市名称。正如上面代码注释中提到的(参见注释),我们将不存储纬度和经度以避免地理空间查询(另一篇文章的主题)。相反,我们保存国家代码和城市名称以缩小搜索范围(为了简单起见,省略街道)。国家代码可以表示为2个字符、3个字符或3位数字,我们保存为一个数字表示,并将使用一个 ushort。幸运的是,城市比国家多,所以我们不能使用“城市代码”(尽管我们可以为内部使用创建一个)。相反,我们将存储实际的城市名,保留平均50个字节为一个城市的名字。对于特殊的情况如:Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu(85个字母的城市 名字),我们最好使用一个额外的布尔变量,它表明这是特定的超长名字的城市(不要试图发音)。记住字符串和向量的内存开销。我们将在结构的最终大小上添加额外的32个字节(以防万一)。我们还假设我们在64位系统上工作,尽管我们为 int和 short选择了非常紧凑的值。

// Note the comments	
struct AirbnbHome	
{	
  wstring name; // 200 bytes	
  uint price; // 4 bytes	
  uchar rating; // 1 byte	
  uint rating_count; // 4 bytes	
  vector<string> photos; // 100 bytes	
  string host_id; // 20 bytes	
  uchar adults_number; // 1 byte	
  uchar children_number; // 1 byte	
  uchar infants_number; // 1 byte	
  bitset<3> home_type; // 4 bytes	
  uchar beds_number; // 1 byte	
  uchar bedrooms_number; // 1 byte	
  uchar bathrooms_number; // 1 byte	
  bitset<21> accessibility; // 4 bytes	
  bool superhost; // 1 byte	
  bitset<20> amenities; // 4 bytes	
  bitset<6> facilities; // 4 bytes	
  bitset<34> property_types; // 8 bytes	
  bitset<32> host_languages; // 4 bytes, correct me if I'm wrong	
  bitset<3> house_rules; // 4 bytes	
  ushort country_code; // 2 bytes	
  string city; // 50 bytes	
};

因此,420+字节的再加上32字节,452字节,考虑到有些人可能痴迷于对齐,我们将其四舍五入为500字节。因此,每个“home”对象最多占用500字节,对于所有的home清单,500字节×400万=1.86GB ~ 2GB。似乎是可信的。我们在构造结构的时候做了很多假设,这样节省内存就更容易了,不管我们要用这些数据做什么,我们至少需要2GB的内存。

现在是最困难的部分。为这个问题选择正确的数据结构(尽可能高效地过滤清单)并不是最困难的任务。(对我来说)最难的任务是通过一堆过滤器来搜索列表。如果只有一个搜索键值(一个过滤器),我们会很容易地解决它。假设用户唯一关心的是价格,所以我们只需要找到价格在提供范围内下降的“AirbnbHome”对象。如果我们用二叉搜索树来求它,它看起来是这样的。

640?wx_fmt=png

如果你想象一下这四百万个对象,这棵树长得非常非常大。顺便说一下,内存开销也会增长,因为我们使用BST来存储对象。由于每个父树节点都有两个指向其左子节点和右子节点的附加指针,因此每个子指针的附加字节数为8(假设是64位系统)。对于400万个节点,它的总和为~62 Mb,与2Gb的对象数据相比,这看起来相当小,尽管我们不能轻易“忽略”它。

到目前为止,最后一张图中的树显示,任何项都可以很容易地在O(logN)时间复杂度中找到。

算法复杂度—让我们快速地进行说明,在大多数情况下,找到算法的O复杂度是比较容易的。首先要注意的是,我们总是考虑最坏的情况,即一个算法产生一个积极结果(解决问题)的最大操作数。

假设一个数组中有100个元素,顺序没有排序。需要多少次比较才能找到一个元素(同时考虑到可能缺少所需的元素)?它将进行多达100次比较,因为我们应该将每个元素的值与我们要查找的值进行比较。尽管元素可能是数组中的第一个元素,但我们只考虑最坏的情况(元素要么丢失,要么位于数组的最后一个位置)。

640?wx_fmt=png

计算算法复杂度的目的是找到一个输入大小和操作数量之间的依赖关系,例如上面的数组元素为100,,操作数量也是100,如果数组元素(输入)的数量将增加到1423,找到任何元素的操作数量也将增加到1423(最坏的情况下)。所以在这种情况下,输入和操作数之间的关系很清楚,这就是所谓的线性,操作数的增长和数组输入的增长一样多。增长。这是复杂度的关键点,我们说在一个未排序数组中搜索一个元素需要O(N)个时间来强调搜索它的过程将需要N个操作(甚至N个操作乘以某个常数,比如3N)。另一方面,访问数组中的任何元素都需要一个常数时间,即O(1)。这是因为数组的结构。它是一个连续的数据结构,包含相同类型的元素,因此“跳转”到特定元素只需要计算它相对于数组第一个元素的相对位置。

640?wx_fmt=png

有一件事很清楚。二叉搜索树保持节点的有序。那么在二叉搜索树中搜索一个元素的算法复杂度是多少呢?我们应该计算找到一个元素所需的操作数量(在最坏的情况下)。

请看上面的插图。当我们从根开始搜索时,第一个比较可能导致三种情况:

  1. 找到节点。

  2. 如果所需元素小于节点的值,则比较将继续到节点的左子树

  3. 如果我们搜索的值大于节点的值,则比较将继续到节点的右子树。

在每一步中,我们将需要考虑的节点数量减少一半。在BST中找到一个元素所需的操作数(即比较次数)等于树的高度。树的高度是最长路径上的节点数。这里是4。高是log~2~N + 1,如图所示。所以搜索的复杂度是O(log~2~N + 1) = O(log~2~N)这意味着在最坏的情况下,在400万个节点中搜索某些内容需要进行log~2~4000000 = ~22次比较。

回到树 - 元素在二叉搜索树中的访问时间是O(logN)。为什么不使用哈希表呢?哈希表具有固定的访问时间,这使得几乎在任何地方都可以使用哈希表。

640?wx_fmt=png

在这个问题上我们必须考虑到一个重要的要求。我们必须能够进行范围搜索,例如价格在80美元到162美元之间的房屋。对于BST,只需遍历树并保存计数器,就可以轻松获得范围内的所有节点。对于散列表来说,它有点昂贵,因此在本例中使用BSTs是合理的。

尽管还有另一个地方,它引导我们重新思考哈希表。分布。房价不会“永远”上涨,大多数房子的价格都在同一区间。看这张截屏,柱状图向我们展示了真实的价格,数百万的房子在相同的范围内(+/- 18 ~212美元),他们有相同的平均价格。简单数组可以起到很好的作用。假设数组的索引是价格,值是房屋列表,我们可以在常数时间内访问任意价格范围(嗯,几乎是常数)。下面是它的样子(非常抽象):

640?wx_fmt=png

就像哈希表一样,我们通过每套房子的价格来访问它们。所有价格相同的房屋都归入一个单独的BST。如果我们存储home id而不是上面定义的整个对象( AirbnbHome结构体),它还将为我们节省一些空间。最可能的情况是将所有homes完整对象保存在一个哈希表中,将home ID映射到完整home对象,并存储另一个哈希表(或者更好的是数组),它使用home ID来映射价格。

因此,当用户请求一个价格范围时,我们从价格表中获取home ID,将结果缩减到固定的大小(即分页,通常在一个页面上显示10 - 30个条目),使用每个home ID获取完整的home对象。

只要记住另一件事(在背景中考虑)。平衡对于BST非常重要,因为它是在O(logN)中执行树操作的唯一保证。按顺序插入元素时,不平衡BST的问题很明显。最终,这个树变成了一个链表,这显然导致了线性时间操作。先不管这个,假设所有的树都是完全平衡的。再看一遍上面的图。每个数组元素代表一棵大树。如果我们把图改成这样

640?wx_fmt=png

更像一个图了

它类似于一个“更真实”的图。这个插图表示了最隐蔽的数据结构和图,这将带我们进入下一节。

640?wx_fmt=png— END—

英文原文:https://medium.com/free-code-camp/i-dont-understand-graph-theory-1c96572a1401

640?wx_fmt=jpeg

请长按或扫描二维码关注本公众号

喜欢的话,请给我个好看吧640?wx_fmt=gif


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值