前言

为了能够在面试回答中优雅而不失体面回答面试考点,该文章借鉴了不同平台对知识点的描述

  • 如有侵权请联系我
  • 文章的不足和错误请指正,好的建议也不要吝啬,我都会采纳并更正

字节跳动Java面试题精选——算法与数据结构【含答案】,点赞,转发,收藏一气呵成!

金九银十虽已过去大半,但还有金三银四呀,最新整理一些算法与数据结构以及答案总结,希望对想进BAT的同学有帮助。

正文——算法与数据结构

1.如何判断链表是否有环

方法1:快慢指针法 2.设两个工作指针p、q,p总是向前走,但q每次都从头开始走,对于每个节点,看p走的步数是否和q一样。比如p从A走到D,用了4步,而q则用了14步。因而步数不等,出现矛盾,存在环。

2.红黑树

二叉搜索树:(Binary Search Tree又名:二叉查找树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉搜索树。

红黑树是一棵二叉搜索树,它在每个结点上增加一个存储位来表示结点的颜色,可以是RED或BLACK。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树没有一条路径会比其他路径长出2倍,所以红黑树是近似平衡的,使得红黑树的查找、插入、删除等操作的时间复杂度最坏为O(log n),但需要注意到在红黑树上执行插入或删除后将不在满足红黑树性质,恢复红黑树的属性需要少量(O(log n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操 作时间仍可以保持为 O(log n) 次。具体如何保证?引出红黑树的5个性质。

红黑树的5个性质:满足以下五个性质的二叉搜索树

  • 每个结点或是红色的或是黑色的
  • 根结点是黑色的
  • 每个叶结点是黑色的
  • 如果一个结点是红色的,则它的两个子结点是黑色的
  • 对于每个结点,从该结点到其后代叶结点的简单路径上,均包含相同数目的黑色结点

插入操作

由于性质的约束,插入的结点都是红色的。插入时性质1、3始终保持。破坏性质2当且仅当当前插入结点为根节点。变一下颜色即可。如果是破坏性质4或5,则需要旋转和变色来继续满足红黑树的性质。下面说一说插入的几种情况,约定当前插入结点为N,其父结点为P,叔叔为U,祖父为G

情形1:树空,直接插入违反性质1,将红色改黑。

情形2:N的父结点为黑,不必修改,直接插入

从情形3开始的情形假定N结点的父结点P为红色,所以存在G,并且G为黑色。且N存在一个叔叔结点U,尽管U可能为叶结点。

情形3:P为红,U为红(G结点一定存在且为黑)这里不论P是G的左孩子还是右孩子;不论N是P的左孩子还是右孩子。

首先把P、U改黑,G改红,并以G作为一个新插入的红结点重新进行各种情况的检查,若一路检索至根节点还未结束,则将根结点变黑。

情形4:P为红,U为黑或不存在(G结点一定存在且为黑),且P为G的左孩子,N为P的左孩子(或者P为G的右孩子,N为P的右孩子,保证同向的)。 P、G右旋并将P、G变相反色。因为P取代之前黑G的位置,所以P变黑可以理解,而G变红是为了不违反性质5。

情形5:P为红,U为黑或不存在,且P为G的左孩子,N为P的右孩子(或P为G的右孩子,N为P的左孩子,保证是反向的),对N,P进行一次左旋转换为情形4

删除操作比插入复杂一些,但最多不超过三次旋转可以让红黑树恢复平衡。

其他

  • 黑高从某个结点x出发(不含x)到达一个叶结点的任意一条简单路径上的黑色结点个数称为该结点的黑高。红黑树的黑高为其根结点的黑高。
  • 一个具有n个内部结点的红黑树的高度h<=2lg(n+1)
  • 结点的属性(五元组):color key left right p
  • 动态集合操作最坏时间复杂度为O(lgn)

3.数据库索引的实现

数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。

数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。

B树

为了描述B-Tree,首先定义一条数据记录为一个二元组[key, data],key为记录的键值,对于不同数据记录,key是互不相同的;data为数据记录。那么B-Tree是满足下列条件的数据结构:

  • d为大于1的一个正整数,称为B-Tree的度。用来表示每个结点包含的关键字个数的上界和下界。可以证明h<=logd((N+1)/2)
  • h为一个正整数,称为B-Tree的高度。
  • 每个非叶子节点由n-1个key和n个指针组成,其中d<=n<=2d。
  • 每个叶子节点最少包含一个key和两个指针,最多包含2d-1个key和2d个指针,叶节点的指针均为null 。
  • 所有叶节点具有相同的深度,等于树高h。
  • key和指针互相间隔,节点两端是指针。
  • 一个节点中的key从左到右非递减排列。
  • 所有节点组成树结构。
  • 每个指针要么为null,要么指向另外一个节点。
  • 如果某个指针在节点node最左边且不为null,则其指向节点的所有key小于v(key1),其中v(key1)为node的第一个key的值。
  • 如果某个指针在节点node最右边且不为null,则其指向节点的所有key大于v(keym),其中v(keym)为node的最后一个key的值。
  • 如果某个指针在节点node的左右相邻key分别是keyi和keyi+1且不为null,则其指向节点的所有key小于v(keyi+1)且大于v(keyi)。

由于B-Tree的特性,在B-Tree中按key检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的data,否则对相应区间的指针指向的节点递归进行查找,直到找到节点或找到null指针,前者查找成功,后者查找失败。

一个度为d的B-Tree,设其索引N个key,则其树高h的上限为logd((N+1)/2),检索一个key,其查找节点个数的渐进复杂度为O(logdN)。从这点可以看出,B-Tree是一个非常有效率的索引数据结构。

B+树

B-Tree有许多变种,其中最常见的是B+Tree,例如MySQL就普遍使用B+Tree实现其索引结构。

B+树是B树的变形,它把所有的data都放在叶子结点中,只将关键字和子女指针保存于内结点,内结点完全是索引的功能。

与B-Tree相比,B+Tree有以下不同点:

  • 每个节点的指针上限为2d而不是2d+1。
  • 内节点不存储data,只存储key;叶子节点存储data不存储指针。

一般在数据库系统或文件系统中使用的B+Tree结构都在经典B+Tree的基础上进行了优化,增加了顺序访问指针。

在B+Tree的每个叶子节点增加一个指向相邻叶子节点的指针

例如图4中如果要查询key为从18到49的所有数据记录,当找到18后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。

为什么B树(B+树)

一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。

这涉及到磁盘存取原理、局部性原理和磁盘预读。

先从B-Tree分析,根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:

  • 每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O
  • B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)

综上所述,用B-Tree作为索引结构效率是非常高的。

而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。

至于B+Tree为什么更适合外存索引,原因和内节点出度d有关。

由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,拥有更好的性能。

4.一致性Hash

第一:简单介绍 一致性哈希算法是分布式系统中常用的算法。比如,一个分布式的存储系统,要将对象存储到具体的节点上,如果采用普通的hash方法,将数据映射到具体的节点上,如key%N,N是机器节点数

  • 考虑到比如一个服务器down掉,服务器结点N变为N-1,映射公式必须变为key%(N-1)
  • 访问量加重,需要添加服务器结点,N变为N+1,映射公式变为hash(object)%(N+1)

当出现1,2的情况意味着我们的映射都将无效,对服务器来说将是一场灾难,尤其是对缓存服务器来说,因为缓存服务器映射的失效,洪水般的访问都将冲向后台服务器。

第二点:hash算法的单调性

Hash 算法的一个衡量指标是单调性,单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。

consistent hash 也是一种hash 算法,简单的说,在移除 / 添加一个结点时,它能够尽可能小的改变已存在的映射关系,尽可能的满足单调性的要求。

第三点:将对象和服务器结点分别映射到环型空间

通常的一致性哈希做法是将 value 映射到一个 32 位的 key 值,也即是 0~2^32-1 次方的数值空间;我们可以将这个空间想象成一个首( 0 )尾( 2^32-1 )相接的圆环。

我们可以通过hash函数将我们的key映射到环型空间中,同时根据相同的哈希算法把服务器也映射到环型空间中,顺便提一下服务器或者某个计算节点的 hash 计算,一般的方法可以使用机器的 IP 地址或者机器名作为 hash 输入。

第四点:将对象映射到服务器

在这个环形空间中,如果沿着顺时针方向从对象的 key 值出发,直到遇见一个 服务器结点,那么就将该对象存储在这个服务器结点上,因为对象和服务器的hash 值是固定的,因此这个 cache 必然是唯一和确定的。

这时候考察某个服务器down机或者需要添加服务器结点,也就是移除和添加的操作,我们只需要几个对象的映射。

第五点:虚拟结点

Hash 算法的另一个指标是平衡性 (Balance)。平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。

对于上述的做法,可能导致某些对象都映射到某个服务器,使得分布不平衡。为此可以采用“虚拟结点”的做法。

“虚拟结点”( virtual node )是实际节点在 hash 空间的复制品,一实际结点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。引入“虚拟结点”会让我们的映射分布更为平衡一些。

  • 引入“虚拟结点”前: Hash(“192.168.1.1”);
  • 引入“虚拟结点”后: Hash(“192.168.1.1#1”); Hash(“192.168.1.1#2”);