使用方案查找两个排序整数列表的中位数

2021-06-06 06:46:57

我最近一直在借鉴方案的基础知识,作为通过Chris Hanson和Gerald Jay Sussman的软件设计的阅读和努力工作的一部分。计划是在20世纪70年代创建的LISP方言,由Gerald Jay Sussman和Guy L. Steele。因此,它比普通的LISP,Clojure和球拍等其他一些众所周知的方言,所有这些都是由方案的影响。具体而言,由于语言规范的极简主义,实现的实施方式显然存在很大的差异。

除了在几年前的工作规划集团的某些面条的某些面条,我没有太多的Lisps经验。所以几个月前,为了熟悉语法和一些更基本的构造(留下了几个基本的构造,我解决了来自LeetCode的一些问题,其中一个是两个阵列的中位数,哪个我现在将继续解释。

问题是:给定两个排序的整数列表,计算两个列表组合的中位值,理想地在O(日志(m + n))时间。例如:

让我们开始实现一种天真,慢速。获取单个排序列表的中位数是微不足道的:如果列表具有奇数长度,或者两个中心值的平均值,则是中央值。因此,Naïve方法是组合两个列表,对组合列表进行排序并从组合和排序的列表中获取中位数。实施可以看出这样的东西:

;;返回两个排序整数列表的中位值。 (定义(慢中位L1 L2)(设(l(l(sort(prote(附加(附加(附加)))))(如果(modulo(modelo(lence l)2)0)(/(+(list-ref l(list-ref l))(/(列表 - ref l(整数地板( - (长度L)1)2)))(列表-fiLe(整数 - 上限( - (长度l)1)2)))2)(列表-fr(整数 - 楼层(长度L)) 2)))))))

该过程开始通过合并两个列表(附加)并按升序排序新列表(排序......)。然后它检查组合列表是否具有均匀长度。如果是这样,它可以获得两个中心值并返回其平均值。如果没有,它会返回单个中心值。 (list-ref此处返回给定索引处的列表元素;整数分割舍入;整数天花板是整数分区圆形。)

求解此问题的更有效的算法基本上是二进制搜索的更复杂的变体。它比常见的二进制搜索更复杂,因为它正在同时搜索两个列表,因为每个列表中的搜索都取决于其他列表的值。

Leetcode上的突出显示的答案是漂亮的奥斯特。 (这里有一个更好的解释。)主旨是我们可以将问题重新查找一个不寻找两个排序列表中位数的问题,而是找到两个列表中的每个列表中的多少元素或者是在中位数下方。例如,在(1,3,5)(2,4)中,第一个列表贡献了两个值(一个和三个),第二个列表贡献一个(两),因为中位数是三个。如果我们有这两个子师,计算中位数是微不足道的。

因此,算法将首先解决该问题。解决这个问题的方法是递归地将两个列表划分为一半。如果每个列表的前半部分的最后一个元素小于相反列表的下半部分的第一个元素,那么我们很好 - 这两个第一个半数是达到和包括中位数的值。如果另一方面,如果一个列表的前半部分的最后一部分大于其他列表的后半部分的第一个元素,那么我们知道其他列表的前半部分位于中位数以下,但我们仍需要在第一个列表的前半部分和其他列表的下半部分仍然需要进行相同的比较,以便在中位数下方找到其余元素。

现在我们看到第一个列表的前半部分的最后一个值大于第二个列表的下半部分的第一个值:7>这意味着第二个名单的上半场(2,3,4),低于中位数,但我们仍然不知道第一个列表的前半部分和下半部分的哪个元素第二个列表是。所以我们返回()(2,3,4)(因为第二个列表贡献了三个元素)加上递归调用该过程的过程,其中两个列表(1,4,7)和(5,6)。

既没有列表都没有上半部的最后一半,其最后一个元素大于其他列表的下半部分的第一元素,其他单词4≤6和5≤7,所以我们很好地走到这里并返回(1,4)(5) (因为第一个列表贡献了两个元素和第二个列表)。

总共算法返回()(2,3,4)++(1,4)(5)=(1,4)(2,3,4,5),给我们一个(4 + 5 )/ 2 = 4.5。

我希望这有一种感觉。实际算法有点复杂,因为它需要跟踪剩余的元素数量,这些元素仍然可以构成所需的数量,元素数量小于和包括中位数(上面的示例中的六个,因为这两个列表总共有十个元素,当总数甚至时,我们需要两个中心值)。这似乎是必要的,因为在分割列表时,算法不能总是舍入或始终舍入 - 这取决于上下文。因此,除了两个列表之外,它实际上是在每个步骤的递归过程中。我在过程内调用此值期望-N。也许有一种方法可以在没有这种添加的情况下使算法工作,但我找不到它。

还有一件事。我一直在调用过程的输入列表,但实际上,这第二个实施将不是未列出而不是向量。这是因为我们将通过索引进行大量访问,这应该是向量的o(1),而是用于列表的O(n)(至少我假设是这种情况;麻省理工学院/ GNU方案参考手册并未似乎说)。

在将眼睛转向实际实现之前,我们需要编写一些实用程序。令人惊讶的是,据我所知,MIT / GNU方案中没有程序用于检查矢量是否为空(虽然有一个,但是列表)。所以让我们从那个开始:

;;返回:T如果向量是空的;否则返回:f。 (定义(矢量 - null?v)(= 0(矢量 - 长度v)))

更重要的是,虽然有一个用于获取矢量的第一个(或第八个)元素的过程,但没有用于获取矢量的最后一个元素的过程。这很简单:

;;返回向量的最后一个元素。 (定义(矢量 - 上次v)(Vector-ref v( - (矢量 - 长度v)1)))

最后,我们将实现一对方法两对向量的一对级联的过程。也就是说,我们连接两对的第一个元素,然后连接两对的第二个元素并与两个连接返回一对。以下是创建新对的过程;汽车在一对中获取第一个元素; CDR获取第二个元素。 [1]

;;返回两对向量的成对连接。 (定义(++ P1 P2)(缺点(载体 - 附加(CAR P1)(CAR P2))(矢量 - 附加(CDR P1)(CDR P2)))))))

1] => (矢量 - null?#());值:#t 1] => (矢量 - null?#(3 1 4));值:#f 1] => (矢量 - 最后#(3 1 4));值:4 1] => (++(cons#(1 2)#(3 4))(cons#(5 6)#(7 8));值:(#(1 2 5 6)。#(3 4 7 8))

;;返回两个排序整数列表的中位值。 (定义(快速中值Vector1 Vector22)(断言(+(+(载体长度向量1)(载体长度向量2))0);这个程序需要两个向量`v1'和`v2'和一个;;整数`期望-N'并确定所需的-N&#39 ;;;;;向量。它返回一对向量,每个矢量,每个值; ;载体分别构成了组成的列表;;`期望-N'最小的值。;;;;;;;;( lp#(1 3)#(4 5 6)3)=> (#(1 3)。#(4))(定义(LP v1 v2所需-n)(设*(split1(cond1((vector-null?v1)0)((向量 - null?v2)期望 - n)(否则(整数(+(+(+(载体长度V1)1)2)))))(split2( - 期望-n split1))(头部1(矢量 - 头部V1 split1))(尾部1(矢量尾V1 split1))(Head2(载体头V2分裂2))(尾部2(矢量 - 尾V2分裂2)))(COND((载体 - null?head1)(cons#()h EAD2)))((矢量 - null? head2)(缺点head1#()))((和(不是(vector-null?tail2))(>(矢量 - 第一个head1)(矢量 - 第一尾部2)))(++(cons#()head2) (LP Head1 Tail2( - 期望-N(载体长度Head2)))))))((和(Vector-Null?tail1))(>(矢量 - 上一个head2)(矢量 - 第一尾部1)))( ++(缺点head1#())(lp tail1 head2( - 期望 - n(矢量长度头1))))))(else(cons head1 head2)))))(let *((中位-idx(lambda(lambda)))(let *( n)(如果(= n 0)0(+(整数N 2)1)))))(组合 - 长度(+(向量 - 长向量1)(向量 - 长向量2)))(所需-N(中位数) -IDX组合长度))(NUMS - 中位数(LP vector1 Vector2所需-N))(组合长度 - 偶数(=(模数组合长度2)0))(Last-2(Lambda (v)(载体尾V(max 0( - (矢量长度v)2))))))(最高字(矢量 - 附加(最后二(车Nums-Le) ft-of中位数))(最后二(CDR Nums-Lefian)))))(排序(排序!最高的数字< ))(最高两次(最后两种分类))(中位数(如果组合长度为-En-op(/(+(+(+(向量 - 第一最高)(矢量 - 载体 - 最后))2)(矢量 - 最后最高二)))))中位数)))

1] => (快速中位数#(1 3)#(2));值:2 1] => (快速中位数#(1 2)#(3 4));值:5/2 1] => (快速中位数#(1 4 7 8 9)#(2 3 4 5 6));价值:9/2

此过程比前一个更快。我还没有弄清楚方案中的基准措施的好方法,但为大型清单运行它们,慢中位需要几秒钟来计算,而快速中位数是瞬间的:

1] => (定义L1(IOTA 1000000 1)); iota创建一个序列;值:l1 1] => (定义L2(IOTA 1500000 10000));值:L2 1] => (慢中位L1 L2);需要几秒钟;值:630000 1] => (定义V1(List-> Vector L1));值:V1 1] => (定义V2(列表 - > Vector L2));值:v2 1] => (v1 v2快速中值);瞬间;价值:630000

该过程总共由37行组成,评论和公用事业程序被排除在外。 [2] LeetCode的突出显示的Java和Python解决方案分别由34和24行代码组成。这些版本可能比我写的那样更好。鉴于该算法既看起来且经常比较这些子集外部的元素,否则似乎都似乎有点容易。这使得与索引更容易使用指数而不是实际分割列表,就像功能规划一样自然。但我并不遗漏有一个方案程序的可能性更加美丽,就像我想出的那样快。

这些令人困惑的名字是历史行李。汽车的内容是寄存器的地址部分的内容和登记册减量部分的CDR内容。我假设构造或类似的东西是简短的。查看Hanson& Sussman,软件设计灵活性,附录B.↩︎

实际上,这里呈现的代码由更多的行组成,因为我必须水平压缩它,以适应网站的桌面版本。 ↩︎