推广 热搜: 公司  快速  上海  中国  未来    企业  政策  教师  系统 

王争数据结构与算法之美开篇问题整理

   日期:2024-10-31     作者:caijiyuan    caijiyuan   评论:0    移动:http://keant.xrbh.cn/news/12869.html
核心提示:为什么大多数编程语言中数组从 0 而不是从 1 开始编号?从数组存储的内存模型上来看,“下标”最确切的定义应该是“

总框架

王争数据结构与算法之美开篇问题整理

  • 为什么大多数编程语言中数组从 0 而不是从 1 开始编号

从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。如果用 a 来表示数组的首地址,a[0]就是偏移为 0 的位置,也就是首地址,a[k]就表示偏移 k 个 type_size 的位置,所以计算 a[k]的内存地址只需要用这个公式
a[k]_address = base_address + k * type_size

  • 如何基于链表实现 LRU 缓存淘汰算法

维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,从链表头开始顺序遍历链表。
1.如果此数据之前已经被缓存在链表中,遍历得到这个数据对应的结点,并将其从原来的位置删除,再插入到链表的头部。
2.如果此数据没有在缓存链表中,又可以分为两种情况:如果此时缓存未满,则将此结点直接插入到链表的头部;如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
Tips 不管缓存有没有满都需要遍历一遍链表,该基于链表的实现思路缓存访问的时间复杂度为 O(n)。

  • 如何实现浏览器的前进、后退功能

1.使用两个栈X 和 Y,把首次浏览的页面依次压入栈 X
2.当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y
3.当点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中
4.当栈 X 中没有数据时,说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,说明没有页面可以点击前进按钮浏览了。

  • 线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处理?各种处理策略又是如何实现的?如何存储排队的请求

处理策略分两种
1.非阻塞的处理方式,直接拒绝任务请求
2.阻塞的处理方式,将请求排队,等到有空闲线程时,取出排队的请求继续处理。
用先进者先服务公平地处理每个排队的请求,所以队列这种数据结构很适合来存储排队请求。队列有基于链表和基于数组这两种实现方式。
1.基于链表的实现方式,可以实现一个支持无限排队的无界队列(unbounded queue,但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的无限排队的线程池是不合适的。
2.基于数组实现的有界队列(bounded queue,队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说,就相对更加合理。设置一个合理的队列大小非常有讲究,队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源、发挥最大性能。
Tips 除了应用在线程池请求排队的场景之外,队列还可以应用在任何有限资源池中,用于排队请求,比如数据库连接池等。对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。

  • 冒泡排序和插入排序的时间复杂度都是 O(n2),都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎

冒泡排序和插入排序的时候讲到,冒泡排序不管怎么优化,元素交换的次数是一个固定值,是原始数据的逆序度。插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度。但是,从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。

  • 如何根据年龄给 100 万用户排序

假设年龄的范围最小 1 岁,最大不超过 120 岁。遍历这 100 万用户,根据年龄将其划分到 120 个桶,再依次顺序遍历这 120 个桶中的元素即可得到按照年龄排序的 100 万用户数据。

  • 排序算法对比

在这里插入图片描述

  • 假设有 1000 万个整数数据,每个数据占 8 个字节,如何设计数据结构和算法,快速判断某个整数是否出现在这 1000 万数据中? 占用内存空间最多不要超过 100MB。

先对这 1000 万数据从小到大排序,再利用二分查找算法即可快速地查找想要的数据了。
Tips 散列表、二叉树都需要比较多的额外的内存空间,所以不适用。

  • 假设有 12 万IP 区间与归属地的对应关系,如何快速定位出一个 IP 地址的归属地

如果 IP 区间与归属地的对应关系不经常更新,可以先预处理这 12 万条数据,让其按照起始 IP 从小到大排序。将IP 地址转化为 32 位的整型数,将起始地址按照对应的整型值的大小关系从小到大进行排序。先通过二分查找,找到最后一个起始 IP 小于等于这个 IP 的 IP 区间,再检查这个 IP 是否在这个 IP 区间内,如果在就取出对应的归属地显示;如果不在就返回未查找到。

  • 为什么 Redis 要用跳表来实现有序集合,而不是红黑树

Redis 中的有序集合是通过跳表来实现的,严格点讲,其实还用到了散列表。Redis 中的有序集合支持的核心操作主要是
插入一个数据;删除一个数据;查找一个数据;按照区间查找数据(比如查找值在[100, 356]之间的数据;迭代输出有序序列。
插入、删除、查找以及迭代输出有序序列这几个操作红黑树也可以完成,且时间复杂度跟跳表一样。但按照区间来查找数据这个操作红黑树的效率没有跳表高 跳表可以做到 O(logn) 的时间复杂度定位区间的起点,再在原始链表中顺序往后遍历,非常高效。
其他原因还有跳表更容易代码实现;跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。
Tips 跳表不能完全替代红黑树,红黑树比跳表的出现要早一些,很多编程语言中的 Map 类型都是通过红黑树来实现,做业务开发时可以直接拿来用,不用费劲自己去实现,但跳表没有现成的实现,所以在开发中如果想使用跳表必须要自己实现。

  • Word 文档中单词拼写检查功能是如何实现的

常用的英文单词有 20 万个左右,假设单词的平均长度是 10 个字母,平均一个单词占用 10 个字节的内存空间,那 20 万英文单词大约占 2MB 的存储空间,就算放大 10 倍也只有 20MB。对于现在的计算机来说,这个大小完全可以放在内存里面,用散列表来存储整个英文单词词典。当用户输入某个英文单词时,拿用户输入的单词去散列表中查找。查到则说明拼写正确,没查到则说明拼写可能有误,给予提示,借助散列表可以轻松实现快速判断是否存在拼写错误。

  • 工业级的散列表应该具有哪些特性?如何设计一个工业级的散列函数

1.支持快速查询、插入、删除操作
2.内存占用合理,不能浪费过多的内存空间
3.性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况。
设计一个合适的散列函数;定义装载因子阈值并设计动态扩容策略;选择合适的散列冲突解决方法。

  • 为什么散列表和链表经常一块使用

散列表虽然支持非常高效的数据插入、删除、查找操作,但散列表中的数据都是通过散列函数打乱之后无规律存储,它无法支持按照某种顺序快速地遍历数据。如果希望按照顺序遍历散列表中的数据,需要将散列表中的数据拷贝到数组中排序再遍历。因为散列表是动态数据结构,不停地有数据的插入、删除,所以每当我们希望按顺序遍历散列表中的数据的时候,都需要先排序,效率势必会很低。为了解决这个问题,我们将散列表和链表(或者跳表)结合在一起使用。

  • 如何存储用户密码这么重要的数据

通过哈希算法对用户密码进行加密之后再存储,最好选择相对安全的加密算法,比如 SHA 等(因为 MD5 已经号称被破解了)。针对字典攻击,引入一个盐(salt)跟用户的密码组合在一起增加密码的复杂度。拿组合之后的字符串做哈希算法加密,将它存储到数据库中,进一步增加破解的难度。

  • 哈希算法的应用

安全加密 选择哈希算法的时候,要权衡安全性和计算时间来决定用哪种哈希算法
数据校验 用于校验数据的完整性和正确性
唯一标识 哈希算法可以对大数据做信息摘要,通过一个较短的二进制编码来表示很大的数据
散列函数 对哈希算法的要求非常特别,更加看重的是散列的平均性和哈希算法的执行效率
负载均衡 利用哈希算法替代映射表,可以实现一个会话粘滞的负载均衡策略
数据分片 通过哈希算法对处理的海量数据进行分片,多机分布式处理,可以突破单机资源的限制
分布式存储 利用一致性哈希算法解决缓存等分布式系统的扩容、缩容导致数据大量搬移的难题。

  • 二叉树有哪几种存储方式?什么样的二叉树适合用数组来存储

二叉树既可以用链式存储,也可以用数组顺序存储。数组顺序存储的方式比较适合完全二叉树,其他类型的二叉树用数组存储会比较浪费存储空间。

  • 使用二叉树的地方是不是都可以替换成散列表呢?有没有哪些地方是散列表做不了,必须要用二叉树来做的

1.散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,只需要中序遍历就可以在 O(n) 的时间复杂度内,输出有序的数据序列
2.散列表扩容耗时很多,且当遇到散列冲突时性能不稳定,尽管二叉查找树的性能不稳定,但在工程中,最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在 O(logn)
3.尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。
4.散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,且这个问题的解决方案比较成熟、固定。
5.为了避免过多的散列冲突,散列表装载因子不能太大,特别是基于开放寻址法解决冲突的散列表,否则会浪费一定的存储空间。
综合这几点,平衡二叉查找树在某些方面还是优于散列表的,所以,这两者的存在并不冲突,在实际的开发过程中需要结合具体的需求来选择使用哪一个。

  • 为什么工程中都喜欢用红黑树,而不是其他平衡二叉查找树呢

AVL 树是一种高度平衡的二叉树,其查找的效率非常高,但AVL 树为了维持这种高度的平衡,每次插入、删除都要做调整,比较复杂、耗时。所以对于有频繁的插入、删除操作的数据集合,使用 AVL 树的代价有点高。红黑树只做到了近似平衡,并不是严格的平衡,在维护平衡的成本上要比 AVL 树要低,红黑树的插入、删除、查找各种操作性能都比较稳定。对于工程应用来说,要面对各种异常情况,为了支撑这种工业级的应用,我们更倾向于这种性能稳定的平衡二叉查找树。

  • 在实际的软件开发中,为什么快速排序的性能要比堆排序好

1.堆排序数据访问的方式没有快速排序友好。对于快速排序来说,数据是顺序访问的。而对于堆排序来说,数据是跳着访问的。 比如,堆排序中,最重要的一个操作就是数据的堆化。比如下面这个例子,对堆顶节点进行堆化,会依次访问数组下标是 1,2,4,8 的元素,而不是像快速排序那样,局部顺序访问,所以,这样对 CPU 缓存是不友好的。
在这里插入图片描述
2.对于同样的数据,在排序过程中堆排序算法的数据交换次数要多于快速排序。对于基于比较的排序算法来说,整个排序过程就是由两个基本的操作组成的,比较和交换(或移动)。快速排序数据交换的次数不会比逆序度多。但是堆排序的第一步是建堆,建堆的过程会打乱数据原有的相对先后顺序,导致原数据的有序度降低。

  • 假设现在我们有一个包含 10 亿个搜索关键词的日志文件,如何能快速获取到热门榜 Top 10 的搜索关键词呢

处理这个问题有很多高级的解决方法,比如使用 MapReduce 等。但是如果将处理的场景限定为单机,可以使用的内存为 1GB。那这个问题该如何解决
因为用户搜索的关键词有很多可能都是重复的,所以首先统计每个搜索关键词出现的频率。通过散列表、平衡二叉查找树或者其他一些支持快速查找、插入的数据结构来记录关键词及其出现的次数,假设选用散列表,顺序扫描这 10 亿个搜索关键词。当扫描到某个关键词时去散列表中查询。如果存在将对应的次数加一;如果不存在,将它插入到散列表,并记录次数为 1。以此类推,等遍历完这 10 亿个搜索关键词之后,散列表中就存储了不重复的搜索关键词以及出现的次数,再用堆求 Top K 的方法,建立一个大小为 10 的小顶堆,遍历散列表,依次取出每个搜索关键词及对应出现的次数,与堆顶的搜索关键词对比。如果出现次数比堆顶搜索关键词的次数多,删除堆顶的关键词,将这个出现次数更多的关键词加入到堆中。以此类推,当遍历完整个散列表中的搜索关键词之后,堆中的搜索关键词就是出现次数最多的 Top 10 搜索关键词了。
上面的解决思路其实存在漏洞,10 亿的关键词很多,我们假设 10 亿条搜索关键词中不重复的有 1 亿条,如果每个搜索关键词的平均长度是 50 个字节,那存储 1 亿个关键词起码需要 5GB 的内存空间,而散列表因为要避免频繁冲突,不会选择太大的装载因子,所以消耗的内存空间就更多。而机器只有 1GB 的可用内存空间,所以无法一次性将所有的搜索关键词加入到内存中。这个时候该怎么办?相同数据经过哈希算法得到的哈希值一样,可以根据哈希算法的这个特点将 10 亿条搜索关键词先通过哈希算法分片到 10 个文件中。具体可以这样做:创建 10 个空文件 00,01,02,……,09,遍历这 10 亿个关键词,并通过某个哈希算法对其求哈希值,再将哈希值同 10 取模,得到的结果就是这个搜索关键词应该被分到的文件编号。对这 10 亿个关键词分片之后,每个文件都只有 1 亿的关键词,去除掉重复的,可能就只有 1000 万个,每个关键词平均 50 个字节,所以总的大小就是 500MB,1GB 的内存完全可以放得下。我们针对每个包含 1 亿条搜索关键词的文件,利用散列表和堆分别求出 Top 10,再把这 10 个 Top 10 放在一块,取这 100 个关键词中出现次数最多的 10 个关键词,就是这 10 亿数据中的 Top 10 最频繁的搜索关键词了。

  • 如何存储微博、微信等这些社交网络的好友关系吗

因为社交网络是一张稀疏图,使用邻接矩阵存储比较浪费存储空间。所以,这里采用邻接表来存储。不过用一个邻接表来存储这种有向图是不够的,我们去查找某个用户关注了哪些用户非常容易,但是如果要想知道某个用户都被哪些用户关注了,也就是用户的粉丝列表,是非常困难的。基于此,需要一个逆邻接表。邻接表中存储了用户的关注关系,逆邻接表中存储的是用户的被关注关系。对应到图上,邻接表中,每个顶点的链表中,存储的就是这个顶点指向的顶点,逆邻接表中,每个顶点的链表中,存储的是指向这个顶点的顶点。如果要查找某个用户关注了哪些用户,我们可以在邻接表中查找;如果要查找某个用户被哪些用户关注了,我们从逆邻接表中查找。
基础的邻接表不适合快速判断两个用户之间是否是关注与被关注的关系,可以将邻接表中的链表改为支持快速查找的动态数据结构。因为我们需要按照用户名称的首字母排序,分页来获取用户的粉丝列表或者关注列表,用跳表这种结构再合适不过。因为跳表插入、删除、查找都非常高效,时间复杂度是 O(logn),空间复杂度上稍高是 O(n)。且跳表中存储的数据本来就是有序的了,分页获取粉丝列表或关注列表非常高效。如果对于小规模的数据,比如社交网络中只有几万、几十万个用户,我们可以将整个社交关系存储在内存中,上面的解决思路是没有问题的。但是如果像微博那样有上亿的用户,数据规模太大,我们就无法全部存储在内存中了。这个时候可以通过哈希算法等数据分片方式,将邻接表存储在不同的机器上。当要查询顶点与顶点关系的时候,我们就利用同样的哈希算法,先定位顶点所在的机器,再在相应的机器上查找。另外一种解决思路是利用外部存储(比如硬盘,因为外部存储的存储空间要比内存会宽裕很多。

  • 给定一个用户,如何找出这个用户的所有三度(其中包含一度、二度和三度)好友关系

用图的广度优先搜索算法来解决,因为广度优先搜索是层层往外推进的。首先遍历与起始顶点最近的一层顶点,也就是用户的一度好友,再遍历与用户距离的边数为 2 的顶点,也就是二度好友关系,以及与用户距离的边数为 3 的顶点,也就是三度好友关系。稍加改造一下广度优先搜索代码,用一个数组来记录每个顶点与起始顶点的距离,非常容易就可以找出三度好友关系。

  • RK 算法是如何借助哈希算法来实现高效字符串匹配的

RK 算法借助哈希算法对 BF 算法进行改造,即对每个子串分别求哈希值,再拿子串的哈希值与模式串的哈希值比较,减少了比较的时间。理想情况下RK 算法的时间复杂度是 O(n),跟 BF 算法相比,效率提高了很多。其效率取决于哈希算法的设计方法,如果存在冲突的情况下,时间复杂度可能会退化。极端情况下,哈希算法大量冲突,时间复杂度退化为 O(n*m)。

  • 对于查找功能是重要功能的软件来说,比如一些文本编辑器,它们的查找功能都是用哪种算法来实现的呢?有没有比 BF 算法和 RK 算法更加高效的字符串匹配算法呢

BM 算法核心思想是利用模式串本身的特点,在模式串中某个字符与主串不能匹配的时候,将模式串往后多滑动几位,以此来减少不必要的字符比较,提高匹配的效率。BM 算法构建的规则有两类,坏字符规则和好后缀规则。好后缀规则可以独立于坏字符规则使用。因为坏字符规则的实现比较耗内存,为了节省内存,我们可以只用好后缀规则来实现 BM 算法。

  • 如何利用 Trie 树实现搜索关键词的提示功能

假设关键词库由用户的热门搜索关键词组成,将这个词库构建成一个 Trie 树。当用户输入其中某个单词的时候,把这个词作为一个前缀子串在 Trie 树中匹配。设词库里只有 hello、her、hi、how、so、see 这 6 个关键词。当用户输入了字母 h 的时候,我们就把以 h 为前缀的 hello、her、hi、how 展示在搜索提示框内。当用户继续键入字母 e 的时候,我们就把以 he 为前缀的 hello、her 展示在搜索提示框内。这就是搜索关键词提示的最基本的算法原理。

  • 如何才能实现一个高性能的敏感词过滤系统呢

AC自动机

  • 贪心、分治、回溯和动态规划算法比较

贪心、回溯、动态规划归为一类,而分治单独可以作为一类,因为它跟其他三个都不大一样。前三个算法解决问题的模型可以抽象成多阶段决策最优解模型,而分治算法解决的问题尽管大部分也是最优解问题,但大部分都不能抽象成多阶段决策模型。
回溯算法是个“万金油”。基本上能用的动态规划、贪心解决的问题,我们都可以用回溯算法解决。回溯算法相当于穷举搜索。穷举所有的情况,然后对比得到最优解。不过,回溯算法的时间复杂度非常高,是指数级别,只能用来解决小规模数据的问题。对于大规模数据的问题,用回溯算法解决的执行效率就很低了。
尽管动态规划比回溯算法高效,但是不是所有问题都可以用动态规划来解决。能用动态规划解决的问题,需要满足三个特征最优子结构、无后效性和重复子问题。在重复子问题这一点上,动态规划和分治算法的区分非常明显。分治算法要求分割成的子问题,不能有重复子问题,而动态规划正好相反,动态规划之所以高效,就是因为回溯算法实现中存在大量的重复子问题。
贪心算法实际上是动态规划算法的一种特殊情况。它解决问题起来更加高效,代码实现也更加简洁。不过,它可以解决的问题也更加有限。它能解决的问题需要满足三个条件,最优子结构、无后效性和贪心选择性(这里我们不怎么强调重复子问题)。其中,最优子结构、无后效性跟动态规划中的无异。“贪心选择性”的意思是,通过局部最优的选择,能产生全局的最优选择。每一个阶段,我们都选择当前看起来最优的决策,所有阶段的决策完成之后,最终由这些局部最优解构成全局最优解。

  • B+树特点
本文地址:http://lianchengexpo.xrbh.cn/news/12869.html    迅博思语资讯 http://lianchengexpo.xrbh.cn/ , 查看更多
 
标签: 数据结构
 
更多>同类行业资讯
0相关评论

新闻列表
企业新闻
推荐企业新闻
推荐图文
推荐行业资讯
点击排行
网站首页  |  关于我们  |  联系方式  |  使用协议  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报  |  粤ICP备2023022329号