mergesort

by Joe Chasinga

通过乔·查辛加(Joe Chasinga)

Mergesort算法的功能方法 (A functional approach to mergesort algorithm)

Algorithms are often difficult for people to understand. I believe that this is because they are most often programmed or explained in a language that encourages thinking in procedures or instructions which are not intuitive.

人们通常很难理解算法。 我相信这是因为它们通常是用一种语言来编程或解释的,这种语言会鼓励人们思考不直观的程序或指令。

Very often the meat of an algorithm (how you solve a particular problem logically without computer coding) looks very simple and understandable when described graphically. Surprisingly, however, it often does not translate well into code written in languages like Python, Java, or C++. Therefore it becomes much more difficult to understand.

当以图形方式进行描述时,算法的精髓(在没有计算机编码的情况下如何以逻辑方式解决特定问题的方法)通常看起来非常简单易懂。 但是令人惊讶的是,它通常不能很好地转换为用Python,Java或C ++等语言编写的代码。 因此,变得更加难以理解。

In other words, the algorithmic concept doesn’t map directly to how the code should be written and read.

换句话说,算法概念并不直接映射到应如何编写和读取代码

为什么算法这么难编码? (Why are algorithms so difficult to code?)

Well, we could blame it on the inner workings of early electro-mechanic computers. The early inventors of some of the most used programming languages today could never get rid of those features. Or perhaps they couldn’t help leaving their fingerprints on their inventions. Once you understand computers that well, there’s no undoing that.

好吧,我们可以将其归咎于早期机电计算机的内部运作。 当今一些最常用的编程语言的早期发明者永远都不会摆脱这些功能。 也许他们不由自主地留下了自己的指纹。 一旦您对计算机有足够的了解,就无法撤消它。

To make matters worse, on top of already micro-managing languages, somebody had to invent an API for better micro-management. They called it object-oriented programming (OOP), and added the concept of classes to programming — but I think modules and functions could handle the same things just fine, thank you very much.

更糟糕的是,除了已经存在的微管理语言之外,还必须有人发明一个API来更好地进行微管理。 他们称其为面向对象编程(OOP),并在编程中添加了类的概念-但我认为模块和函数可以很好地处理相同的事情,非常感谢。

C++ didn’t make C any better, but it did pave a way by inspiring more descendants of OOP. And all together, all these things make abstract algorithmic thinking hard for the aforementioned reasons.

C ++并没有使C变得更好,但它确实通过启发更多的OOP子孙铺平了道路。 综上所述,由于上述原因,所有这些事情使得抽象算法的思考变得困难。

案例研究:合并排序 (The case study: merge sort)

For our discussion, we will use a merge sort algorithm as a specimen. Take a look at the diagram below. If you can count and put together jigsaw puzzles, then you can probably understand how it works in a few minutes.

对于我们的讨论,我们将使用合并排序算法作为样本。 看一下下图。 如果您可以数数并把拼图拼在一起,那么您可能会在几分钟内了解它的工作原理。

The key steps of producing a merge sort are few and simple. In fact, I can explain it using my daughter’s number blocks (helpful to follow these by going back to the animated diagram for reference):

产生合并排序的关键步骤很少而且很简单。 实际上,我可以使用女儿的数字块来解释它(可以通过返回动画图作为参考来帮助遵循这些规则):

  • First, we need to keep subdividing a list of numbers (or letters, or any type of sortable values) by half until we end up with many single-element lists. A list with one element is technically sorted. This is called trivially sorted.首先,我们需要将数字列表(或字母或任何类型的可排序值)细分为一半,直到得到许多单元素列表为止。 具有一个元素的列表在技术上进行了排序。 这称为琐碎排序。
  • Then, we create a new empty list in which we could start re-arranging the elements and putting them one by one according to which one is less/smaller than the other.然后,我们创建一个新的空列表,在该列表中,我们可以开始重新排列元素,并根据其中一个元素的大小小于另一个元素将它们一一放置。
  • Then we need to “merge” each pair of lists back together, effectively reversing the subdivision steps. But this time, at every step of the way, we have to make sure that the smaller element in the pair in question is being put into the empty list first.然后,我们需要将每对列表“合并”在一起,以有效地逆转细分步骤。 但是这一次,我们必须确保将有关对中的较小元素首先放入空列表中。

For the sake of the argument, we will try to map out the above processes by making each one a subroutine (function in normal speak). The meatiest part of this algorithm is the merging, so let’s start with that first.

出于争论的目的,我们将通过使每个子程序成为一个子例程(通常来说是函数)来尝试映射上述过程。 该算法最重要的部分是合并,因此让我们首先开始。

def merge(a, b):    out = []
while (len(a) > 0 and len(b) > 0):         if (a[0] <= b[0]):            out.append(a[0])            del a[0]        else:            out.append(b[0])            del b[0]
while (len(a) > 0):        out.append(a[0])        del a[0]    while (len(b) > 0):        out.append(b[0])        del b[0]
return out

Go on and spend some time looking it over. You might notice that with imperative Python code, it is designed to be spoken out and then understood. It is very understandable in English, but not in logic.

继续并花一些时间查看一下。 您可能会注意到,使用命令式Python代码,可以说出来然后理解它。 用英语很容易理解,但是在逻辑上却不是。

我们的第一次尝试 (Our first attempt)

Here is one attempt (that you could possibly use in a whiteboarding session):

这是一种尝试(您可以在白板会话中使用):

To merge list a and b, we’ll have to first create an empty list named out for clarity (because in Python we can’t be sure it will really be “out” in the end). Then, as long as (or while) both lists are not empty, we’ll keep putting the head of both lists to a fight-off. Whichever is less than or equal to the opponent wins and gets to enter out first. The loser will have to stay and wait there for the new contestant down the line. The rematches continue on until the first while loop breaks.

要合并列表ab ,我们必须先创建一个名为空单out的透明度(因为在Python我们不能肯定这将真正成为“走出去”到底)。 然后,只要(或同时)两个列表都不为空,我们将继续将两个列表的开头进行辩论。 无论是小于或等于给对手获胜,并得到进入out第一位。 失败者将不得不留下来,在那里等待新的参赛者。 重新比赛继续进行,直到第一个while循环中断。

Now, at some point either a or b will be empty, leaving the other with one or more elements hanging. Without any contestants left in the other list, the two while loops make sure to fast track those poor elements into out so both list are exhausted. Then, when that’s all done, we return out.

现在,在某个时间点ab将为空,而另一个则悬空一个或多个元素。 在其他列表中没有任何竞争者的情况下,两个while循环可确保快速将那些不良元素快速找out从而使两个列表都用尽。 然后,当这一切都完成后,我们返回out

And this is the test cases for merge:

这是合并的测试用例:

assert(merge([1], [2]) == [1, 2])assert(merge([2], [1]) == [1, 2])assert(merge([4, 1], [3, 0, 2]) == [3, 0, 2, 4, 1])

I hope at this point it is clear to you why we end up with the result in the last case. If it isn’t, try drawing on a whiteboard or a piece of paper and simulating the explanation.

我希望在这一点上您很清楚为什么我们在最后一种情况下会得到结果。 如果不是,请尝试在白板或纸上绘图并模拟说明。

分而治之 (Divide and Conquer)

Now we will carry on with the subdivision part. This process is also known as partitioning or, in somewhat grander language, Divide and Conquer (by the way, the definition in politics is equally interesting).

现在,我们将继续进行细分部分。 这个过程也被称为分区,或者用某种更为宏大的语言来说,称为分而治之 (顺便说一句, 政治中的定义同样有趣 )。

Basically, if anything is hard to conquer or understand, you should break it down until it becomes smaller and more easily understood. Do that until the parts are unbreakable and repeat the process with the rest.

基本上,如果很难克服或理解任何内容,则应分解它,直到变得更小且更容易理解为止。 这样做直到零件牢不可破,然后对其余零件重复该过程。

def half(arr):    mid = len(arr) / 2    return arr[:mid], arr[mid:]

What the half routine does is find the middle index, slice the input list into roughly equal sublists, and return both as a pair. It only needs to do this once, since the parent function will eventually call it recursively.

half例程的工作是找到中间索引,将输入列表切成大致相等的子列表,然后将两个都成对返回。 它只需要执行一次,因为父函数最终将递归调用它。

Since we have the pieces, now we just need to put them together into a coherent scheme. This is where the water gets murky, because recursions are involved.

既然有了这些片段,那么现在我们只需要将它们放到一个连贯的方案中即可。 这是水变得暗淡的地方,因为涉及到递归。

Before going into more code, let me explain why recursions and imperative programming languages like Python do not fit together so well. I won’t go into the topic of optimization, because that does not concern today’s discussion and is not as interesting.

在讨论更多代码之前,让我解释一下为什么递归和命令式编程语言(如Python)不能很好地融合在一起。 我不会讨论优化的主题,因为它与今天的讨论无关,也不那么有趣。

One distinct irony here is that, even in a language with iterative looping like Python, it still cannot entirely avoid recursion (it might get away without recursion, but I’m sure that would make it even more bizarre). Recursion is a territory where iterative constructs, such as for and while loops, become utterly useless.

这里有一个明显的讽刺意味是,即使在像Python这样的具有迭代循环的语言中,它仍然不能完全避免递归(如果没有递归,它可能会消失,但是我敢肯定,这会使它变得更加离奇)。 递归是迭代构造(例如for和while循环)变得完全无用的领域。

Moreover, recursion is not natural in Python. It does not feel natural and transparent, but rather feels quite half-baked the way its lambda is. An attempt at voicing over recursions in Python would be like, “then we do this recursively and just pray it all works out and hits the base case in the end so it doesn’t spiral into the infinite darkness of stack overflow.” Wow, right?

而且,递归在Python中并不自然。 它感觉不到自然和透明,而是感觉像其lambda一样半生半熟。 在Python中为递归发声的尝试就像是,“然后我们递归地执行此操作,然后祈祷一切都解决了,并最终击中了基础情况,这样它就不会陷入无限的堆栈溢出黑暗之中。” 哇对吧

So here is the mergesort function:

所以这是mergesort函数:

def mergesort(arr):
if (len(arr) <= 1):        return arr
left, right = half(arr)    L = mergesort(left)    R = mergesort(right)
return merge(L, R)

Apparently, this is really clean and easy to read. But it isn’t clear what happens after the call to half , at least semantically. Because of this “non-nativity” to recursion, recursive calls are very opaque and obstructive to educational endeavors.

显然,这确实很干净而且易于阅读。 但是,至少在语义上,调用half之后会发生什么尚不清楚。 由于对递归的这种“非本土化”,因此递归调用是非常不透明的,并且阻碍了教育工作。

The only way to visualize this mergesort process is probably to track the changes in the sublists in every step:

可视化此mergesort过程的唯一方法可能是在每个步骤中跟踪子列表中的更改:

input: [0, 3, 1, 3, 2, 6, 5]A alias for mergesort / halfB alias for merge
## subdividing by half ...
A([0, 3, 1, 3, 2, 6, 5])              A([0, 3, 1])    A([3, 2, 6, 5])          A([0])  A([3, 1])  A([3, 2])   A([6, 5])    A([]) A([0]) A([3])  A([1]) A([3]) A([2]) A([6]) A([5])
## base case reached, start merging ...               B([], [0]) B([3], [1]) B([3], [2]) B([6], [5])          B([0], [1, 3])         B([2, 3], [5, 6])                B([0, 1, 3], [2, 3, 5, 6])                 B([0, 1, 2, 3, 3, 5, 6])
output: [0, 1, 2, 3, 3, 5, 6]

On an asymptotic side note, dividing and conquering almost always incurs a logarithmic runtime. When you keep dividing a collection into N sub-collections, whether it contains 10 or 100,000,000 items, the number of steps taken in the latter case increases at the rate of log base N.

从渐近的角度来看,划分和征服几乎总是会导致对数运行时间。 当您继续将一个集合划分为N个子集合(无论它包含10还是100,000,000个项目)时,在后一种情况下采取的步骤数以对数N的速率增加。

For instance, it takes about 3 steps to keep dividing 10 by 2 until it gets as close to 1 as it can (or exactly 3 steps to reach 1 in integer division). But it takes only about 26 steps to do the same and divide 100,000,000 by 2 until you reach 1. This fact can be expressed as follows:

例如,保持10除以2大约需要3个步骤,直到它尽可能接近1(或者整数除以1恰好需要3个步骤)。 但是,仅需大约26个步骤即可完成,将100,000,000除以2,直到达到1。这一事实可以表示为:

2^3.321928 = 102^6.643856 = 100...2^26.575425 = 100000000
or
log base 2 of 100000000 = 26.575425

The takeaway here is that we had to visualize the recursive processes in order to understand the inner workings of the algorithm — even though it looked so trivial in the animated diagram.

这里的要点是,我们必须可视化递归过程,以了解算法的内部工作原理,即使它在动画图中看起来微不足道。

Why is there a divide between the conceptual processes of the algorithm itself and the code that instructs the computer to compute such processes?

为什么算法本身的概念过程与指示计算机计算此类过程的代码之间存在鸿沟?

It’s because in a way, by using imperative languages, we are in fact still mentally enslaved by the machines.

这是因为在某种程度上,通过使用命令式语言,我们实际上仍然在精神上被机器奴役。

深入研究代码 (Diving deeper into the code)

“There’s a difference between knowing the path and walking the path.”

“知道路径和走路径是有区别的。”

― Morpheus, The Matrix

― Morpheus,矩阵

Programming is hard, we all know that. And understanding programming in a really deep way is even harder on your soul (and your career). But I would argue that, like Morpheus said, sometimes walking the path is all that matters. Being able to see clearly is one of most rewarding things in programming.

编程很难,我们都知道。 而且,以深刻的方式理解编程对您的灵魂(以及您的职业生涯)更加困难。 但是我想像莫非斯所说,有时走这条路很重要。 能够清楚地看到是编程中最有意义的事情之一。

In functional programming, the programmer (you) gets the front seat in seeing how data change recursively. This means that you have the ability to decide how the data of a certain form should be transformed to the data of another based on the snapshot of how it looks. This isn’t unlike how we have visualized the mergesort process. Let me give you a preview.

在函数式编程中,程序员(您)在看待数据如何递归更改方面处于首位。 这意味着您可以根据其外观快照决定将某种形式的数据转换为另一种形式的数据的能力。 这与我们可视化mergesort过程的方式没有什么不同。 让我给你预览。

Let’s say you want to create a base case in Python. In it, you want to return the list in question when it has only one element, and an empty list when there’s two elements. So you’d need to write something like this:

假设您要在Python中创建一个基本案例。 在其中,您要在有一个元素的情况下返回有问题的列表,而在有两个元素的情况下返回一个空列表。 因此,您需要编写如下内容:

if (len(arr) == 1):    return arrelif (len(arr) == 2):    return []

Or to make this worse but more interesting, you could try to access the first element by index 0 and the second element by index 1 and get ready to handle IndexError exception.

为了使这种情况变得更糟但更有趣,您可以尝试通过索引0访问第一个元素,并通过索引1访问第二个元素,并准备处理IndexError异常。

In a functional language like Erlang — which is what I’ll be using in this article for its dynamic type system like Python — you more or less would do something like this:

在像Erlang这样的功能语言中-这就是我将在本文中为其动态类型系统(如Python)使用的语言-您或多或少会执行以下操作:

case Arr of  [_] -> Arr;  [_,_] -> []end.

This gives you a clearer view of the state of the data. Once it’s trained enough, it requires much less cognitive power to read and comprehend what the code does than len(arr) . Just keep in mind: a programmer who doesn’t speak English might ask, “what is len?” Then you get distracted by the literal meaning of the function instead of the value of that expression.

这使您可以更清楚地了解数据状态。 一旦经过足够的培训,与len(arr)相比,它需要更少的认知能力来阅读和理解代码的作用。 请记住:不说英语的程序员可能会问:“伦是什么?” 然后,您会因函数的字面意思而不是该表达式的值而分心。

However, this comes with a price: you don’t have the luxury of a looping construct. A language like Erlang is recursion-native. Almost every meaningful Erlang program will make use of rigorous recursive function calls. And that’s why it is mapped more closely to the algorithmic concepts which usually consist of recursion.

但是,这是有代价的:您没有循环构造的奢侈。 像Erlang这样的语言是递归本机的。 几乎每个有意义的Erlang程序都将使用严格的递归函数调用。 这就是为什么将它更紧密地映射到通常由递归组成的算法概念的原因。

Let’s try to retrace our steps in producing mergesort, but this time in Erlang, starting with the merge function.

让我们尝试追溯生成合并排序的步骤,但是这次是在Erlang中,从merge功能开始。

merge([], [], Acc) -> Acc;merge([], [H | T], Acc) -> [H | merge([], T, Acc)];merge([H | T], [], Acc) -> [H | merge(T, [], Acc)];merge([Ha | Ta], [Hb | Tb], Acc) ->  case Ha =< Hb of    true  -> [Ha | merge(Ta, [Hb | Tb], Acc)];    false -> [Hb | merge([Ha | Ta], Tb, Acc)]  end.

What an abomination! Definitely not an improvement in terms of readability, you think. Yes, Erlang admittedly won’t win any prizes for beautiful language. In fact, many functional languages can look like gibberish to the untrained eyes.

真是可恶! 您认为绝对不是可读性上的改进。 是的,Erlang不会因为美丽的语言而赢得任何奖项。 实际上,许多功能语言对于未经训练的人来说看起来像胡言乱语。

But let’s give it a chance. We will go through each step like we did before, and perhaps in the end some of us will see the light. But before we go on, for those of you who are not familiar with Erlang, these are some points worth noting:

但是,让我们有机会。 我们将像以前一样经历每个步骤,也许最终我们中的某些人会看到曙光。 但是在我们继续之前,对于那些不熟悉Erlang的人来说,以下几点值得注意:

  • Each block of merge is considered a function clause of the same function. They are separated by ;. When an expression ends in Erlang, it ends with a period (.). It’s a convention to separate a function into several clauses for different cases. For instance, merge([], [], Acc) -> Acc; clause maps the case where the first two arguments are empty lists to the value of the last argument.

    每个merge块都被视为同一函数的一个函数子句。 他们被分开; 。 当表达式以Erlang结尾时,它以句点( . )结尾。 按照惯例,将函数分成几个子句以适应不同的情况。 例如, merge([], [], Acc) -> A cc; 子句将前两个参数为空列表的情况映射到最后一个参数的值。

  • Arity plays an important role in Erlang. Two functions with the same name and arity are considered the same function. Otherwise, they aren’t. For example, merge/1 and merge/3 (how functions and their arity are addressed in Erlang) are two different functions.

    Arity在Erlang中扮演重要角色。 具有相同名称和别名的两个功能被视为相同功能。 否则,事实并非如此。 例如, merge/1merge/3 (如何在Erlang中解决功能及其Arity)是两个不同的功能。

  • Erlang uses rigorous pattern matching (This is used in many other functional languages, but especially in Erlang). Since values in pure functional languages are immutable, it is safe to bind variables in a similar shape of data to the existing one with a matched shape. Here is a trivial example:

    Erlang使用严格的模式匹配 (在许多其他功能语言中使用,尤其是在Erlang中)。 由于纯函数语言中的值是不可变的,因此将具有相似数据形状的变量绑定到具有匹配形状的现有变量是安全的。 这是一个简单的示例:

{X, Y} = {0.5, 0.13}.X.  %% 0.5Y.  %% 0.13
[A, B, C | _] = [alice, jane, bob, kent, ollie].[A, B, C].  %% [alice, jane, bob]
  • Note that we will seldom talk about returning values when we work with Erlang functions, because they don’t really “return” anything per se. It maps an input value to a new value. This isn’t the same as outputting or returning it from the function. The function application itself is the value. For instance, if Add(N1, N2) -> N1+N2., Add(1, 2) is 3. There’s no way for it to return anything other than 3, hence we can say it is 3. This is why you could easily do add_one = add(1) and then add_one(2) is 3, add_one(5) is 6, and so on.

    请注意,在使用Erlang函数时,我们很少谈论返回值,因为它们实际上并不真正“返回”任何东西。 它将输入值映射到新值。 这与从函数输出或返回它不同。 功能应用程序本身就是价值。 例如,如果Add(N1, N2) -> N1+ N2 ., Add(1, 1,2)为3。除了3之外,它无法返回其他任何东西,因此我们可以说它是3。这就是为什么您可以轻松地do add_one = add (1),并且en add_one (2)为3, add_one (5)为6,依此类推。

For those who are interested, see referential transparency. To make this point clearer and risking redundancy, here is something to think about:

对于那些感兴趣的人,请参阅参考透明 。 为了使这一点更加清楚并冒着冗余的风险,请考虑以下几点:

when f(x) is a function with one arity, and the mapping is f(x) ->; x , then it's conclusive that f(1) -&gt; 1, f(2) -> 2, f(3.1416) -> 3.1416, and f("foo") -> "foo".

f(x)是具有一个Arity的函数,并且映射为f(x) -> ; x,则它是at f(1) - &g t; 1, f(2的定论th t; 1, f(2 t; 1, f(2 ) -> 2, f(3.1416) -> 3.1416, and f("fo o”)->“ foo”。

This may look like a no-brainer, but in an impure function there's no such guaranteed mapping:

这看起来很容易,但是在一个不纯函数中,没有这样保证的映射:

a = 1

a = 1

a = 1def add_to_a(b):

a = 1 def add_to_a(b):

a = 1def add_to_a(b): return b + a

a = 1 def add_to_a(b): return b + a

Now a might as well be anything before add_to_a gets called. Thus in Python, you could write a pure version of the above as:

现在, a之前很可能会成为什么add_to_a被调用。 因此,在Python中,您可以将上述代码的纯文本编写为:

def add(a, b):

def add(a, b):

def add(a, b): return a + b

def add(a, b): return a + b

or lambda a, b: a + b .

lambda a, b: a + b

Now it’s time to bumble into the unknown.

现在是时候进入未知世界了。

与Erlang一起前进 (Forging ahead with Erlang)

merge([], [], Acc) -> Acc;

The first clause of the merge/3 function means that when the first two arguments are empty lists, map the entire expression to (or “return”) the third argument Acc.

merge/3函数的第一子句意味着,当前两个参数为空列表时,将整个表达式映射到(或“返回”)第三个参数Acc

Interestingly, in a pure function, there’s no way of retaining and mutating state outside of itself. We can only work with what we have received as inputs into the function, transform it, then feed the new state into another function’s argument (most often this is another recursive call to itself).

有趣的是,在一个纯函数中,没有办法在其外部保持和改变状态。 我们只能使用我们作为函数输入收到的东西,对其进行转换,然后将新状态馈入另一个函数的参数中(通常这是对自身的另一个递归调用)。

Here, Acc stands for accumulator, which you can think of as a state container. In the case of merge/3, Acc is a list that starts empty. But as the recursive calls get on, it accumulates values from the first two lists using the logic we program (which we will talk about next).

在这里, Acc代表累加器,您可以将其视为状态容器。 在merge/3的情况下, Acc是一个以空开头的列表。 但是随着递归调用的进行,它使用我们编程的逻辑(将在下面讨论)从前两个列表中累积值。

This process of exhausting a value to build up another value is collectively known as reduction. Therefore, in this case it we can conclude that since the first two lists are exhausted (empty), Acc must be ripe for pick up.

用尽一个值来建立另一个值的过程统称为减少。 因此,在这种情况下,我们可以得出结论,由于前两个列表已用尽(为空),因此Acc必须可以使用。

merge([], [H | T], Acc) -> [H | merge([], T, Acc)];

The second clause matches the case when the first list is already empty, but there’s still at least one more element in the second list. [H | T] means a list has a head element H which cons onto another list T. In Erlang, a list is a linked list, and the head has a pointer to the rest of the list. So a list of [1, 2, 3, 4] can be thought of as:

第二个子句与第一个列表已经为空的情况匹配,但是第二个列表中至少还有一个元素。 [H | T] [H | T]表示一个列表的头元素H 约束在另一个列表 T 。 在Erlang中,列表是一个链接列表,并且头部具有指向列表其余部分的指针。 因此, [1, 2, 3, 4]可以认为是:

%% match A, B, C, and D to 1, 2, 3, and 4, respectively
[A | [B | [C | [D | []]]]] = [1, 2, 3, 4].

In this case, as you can see in the conning example, T can just be an empty tail list. So in this second case, we map it to a value of a new list in which the H element of the second list is conned onto the recursive result of calling merge/3 when T is the second argument.

在这种情况下,如您在精简示例中所看到的, T只能是一个空的尾列表。 因此,在第二种情况下,我们将其映射到新列表的值,其中,当T是第二个参数时,第二个列表的H元素被限制在调用merge/3的递归结果上。

merge([H | T], [], Acc) -> [H | merge(T, [], Acc)];

The third case is just a flip side of the second case. It matches the case when the first list is not empty, but the second is. This clause maps to a value in a similar pattern, except it calls merge/3 with the tail of the first list as the first argument and keeps the second list empty.

第三种情况只是第二种情况的反面。 它匹配第一个列表不为空但第二个列表为空的情况。 该子句以相似的模式映射到一个值,除了它以第一个列表的尾部作为第一个参数调用merge/3并保持第二个列表为空。

merge([Ha | Ta], [Hb | Tb], Acc) ->  case Ha =< Hb of    true  -> [Ha | merge(Ta, [Hb | Tb], Acc)];    false -> [Hb | merge([Ha | Ta], Tb, Acc)]  end.

Let’s begin with the meat of merge/3 first. This clause matches the case when the first and second arguments are non-empty lists. In this case, we enter a case … of clause (equivalent to switch case in other languages) to test if the head element of the first list (Ha) is less than or equal to the head element of the second list (Hb).

让我们先从merge/3开始。 此子句与第一个和第二个自变量为非空列表时的情况匹配。 在这种情况下,我们输入子句的case … of (相当于其他语言的switch case),以测试第一个列表( Ha )的头元素是否小于或等于第二个列表( Hb )的头元素。

If that is true, we con Ha onto the resulting list of the next recursive call to merge with the tail list of the previous first list (Ta) as the new first argument. We keep the second and third arguments the same.

如果是这样,我们将Ha在下一个递归调用的结果列表上,并与先前的第一个列表( Ta )的尾部列表合并为新的第一个参数。 我们保持第二个和第三个参数相同。

These clauses constitute to a single function, merge/3. You can imagine that it could have been a single function clause. We could use complex case … of and/or if conditional plus pattern-matching to weed out each case and map it to the right result. That would have made it more chaotic, when you can easily read each case the function is matching quite nicely on separate lines.

这些子句构成单个功能merge/3 。 您可以想象它可能是单个函数子句。 我们可以使用...和/或条件匹配和模式匹配的复杂案例来剔除每种情况,并将其映射到正确的结果。 当您可以轻松阅读每种情况时,该函数在单独的行上可以很好地匹配,这会使它变得更加混乱。

However, things got a little hairy for the subdividing operation, which needs two functions: half/1 and half/3.

但是,细分操作有些麻烦,它需要两个功能: half/1half/3

half([]) -> {[], []};half([X]) -> {[X], []};half([X,Y]) -> {[X], [Y]};half(L) ->  Len = length(L),  half(L, {0, Len}, {[], []}).
half([], _, {Acc1, Acc2}) ->  {lists:reverse(Acc1), lists:reverse(Acc2)};half([X], _, {Acc1, Acc2}) ->  {lists:reverse(Acc1), lists:reverse([X | Acc2])};half([H|T], {Cnt, Len}, {Acc1, Acc2}) ->  case Cnt >= (Len div 2) of      true -> half(T, {Cnt + 1, Len}, {Acc1, [H|Acc2]});      false -> half(T, {Cnt + 1, Len}, {[H|Acc1], Acc2})  end.

This is where you’ll miss Python and its destructive nature. In a pure functional language, lists are linked lists. When you work with them, there’s no looking back. There’s no logic that says “I want to divide a list in half, so I’m going to get the middle index, and slice it into two left and right portions.”

这是您会想念Python及其破坏性的地方。 在纯功能语言中,列表是链接列表。 当您与他们一起工作时,便不会回头。 有没有逻辑,说:“我要除以2的列表,所以我会得到中间指标,并切片成左右两个部分。”

If your mind is set in working with linked lists, you’re more along the lines of “I can only go forward through the list, working with a few elements at a time. I need to create two empty lists and keep count of how many items I’ve retrieve from the source list and put into the first one so I know when it’s time to switch to another bucket. All the aforementioned needs to be passed in as arguments in the recursive calls.” Whew!

如果您决定使用链接列表,那么您将更像“我只能遍历列表,一次处理几个元素”。 我需要创建两个空列表,并统计从源列表中检索到的第一个条目的数量,以便我知道何时该切换到另一个存储桶。 所有上述所有内容都需要在递归调用中作为参数传递。” ew!

In other words, cutting a list in half can be compared to chopping a block of cheese with a knife in the middle of it. On the other hand, a functional comparison for doing so is like pouring coffee into two cups equally — you just need to know when it’s time to stop pouring into the first one and move on to the second one.

换句话说,将清单切成两半可以比作中间用刀切成块的干酪。 另一方面,这样做的功能比较就像将咖啡均匀地倒入两杯中一样-您只需要知道何时该停止倒入第一个杯子并转到第二个杯子。

The half/1 function, although it isn’t really necessary, is there for convenience.

half/1函数虽然不是真正必需的,但它还是为了方便起见。

half([]) -> {[], []};half([X]) -> {[X], []};half([X,Y]) -> {[X], [Y]};half(L) ->  Len = length(L),  half(L, {0, Len}, {[], []}).

By now, you should get the sense of what each Erlang function clause is doing. The new bracket pairs here represent tuples in Erlang. Yes, we are returning a left and right value pair, like in the Python version. The half/1 function is here to handle simple, explicit base cases which don’t warrant the worthiness of passing in other arguments.

到目前为止,您应该了解每个Erlang函数子句在做什么。 这里的新括号对表示Erlang中的元组。 是的,我们返回的是左值和右值对,就像在Python版本中一样。 half/1函数在这里用于处理简单的显式基本情况,这些情况不保证传递其他参数的价值。

However, take note of the last case when the argument has a list with more than two elements. (Note: those with less than or equal to two elements are already handled by the first three clauses.) It simply computes the following:

但是,请注意当参数的列表包含两个以上元素时的最后一种情况。 (注意:少于或等于两个元素的元素已经由前三个子句处理。)它仅计算以下内容:

  • the length of the list L and calls half/3 with L as the first argument

    列表L的长度,并以L作为第一个参数调用half/3

  • a pair of counter variables and list’s length, which will be used to signal the switching from list one to list two一对计数器变量和列表的长度,用于指示从列表一切换到列表二
  • and of course, a pair of empty lists to fill the elements from L in.

    当然还有一对空列表来填充L in中的元素。

half([], _, {Acc1, Acc2}) ->  {lists:reverse(Acc1), lists:reverse(Acc2)};

half/3 looks like a mess, but only to the untrained eyes. The first clause matches a pattern when the source list is drained. In this case, the second pair of counter and length won’t matter since it’s already the end. We simply know that Acc1 and Acc2 are ripe for yielding. But wait, what’s with the reversing of both?

half/3看起来像是一团糟,但仅限于未经训练的眼睛。 当源列表耗尽时,第一个子句与模式匹配。 在这种情况下,第二对计数器和长度无关紧要,因为它已经结束了。 我们仅知道Acc1Acc2已经成熟。 但是,等等,两者的反转又如何呢?

Appending an element to a linked list is a very slow operation. It runs O(N) times for every append, because it needs to create a new list, copy the existing one onto it, and create a pointer to the new element and assign it to the last element. It’s like redoing the whole list. Couple this with recursions and you are bound for disaster.

将元素附加到链表是很慢的操作。 它需要为每个追加运行O(N)次,因为它需要创建一个新列表,将现有列表复制到该列表上,并创建一个指向新元素的指针并将其分配给最后一个元素。 就像重做整个列表一样。 再加上递归,您将注定要遭受灾难。

The only good way to add something to a linked list is to prepend it at the head. Then all it needs to do is create a memory for that new value and give it a reference to the head of the linked list. A simple O(1) operation. So even though we could concatenate lists using ++ like [1, 2, 3] ++ [4], we rarely want to do it this way, especially with recursions.

向链接列表添加内容的唯一好方法是将其放在开头。 然后,它要做的就是为该新值创建一个内存,并为其提供对链接列表开头的引用。 一个简单的O(1)操作。 因此,即使我们可以使用[1, 2, 3] ++ [4]类的++来连接列表,我们也很少想这样做,特别是在递归的情况下。

The technique here is to reverse the source list first, then con an element onto it like [4 | [3, 2, 1]] , and reverse them again to get the right result. This may sound terrible, but reversing a list and reversing it back is an O(2N) operation, which is O(N). But in between, conning elements onto the list takes only O(1), so it basically costs no extra runtime.

这里的技术是先反转源列表,然后将元素像[4 | [3, 2, 1]] [4 | [3, 2, 1]]并再次反转它们以获得正确的结果。 这听起来很糟糕,但是反转列表并将其反转是O(2N)操作,即O(N)。 但是在这两者之间,将元素精简到列表仅需要O(1),因此基本上不需要额外的运行时间。

half([H|T], {Cnt, Len}, {Acc1, Acc2}) ->  case Cnt >= (Len div 2) of      true -> half(T, {Cnt + 1, Len}, {Acc1, [H|Acc2]});      false -> half(T, {Cnt + 1, Len}, {[H|Acc1], Acc2})  end.

Getting back to half/3. The second clause, the meat of the function, does exactly the same thing as the coffee pouring metaphor we visited earlier. Since the source list is still “emitting” data, we want to keep track of the time we have been pouring values from it into the first coffee cup Acc1.

回到half/3 。 第二个子句,即函数的实质,与我们之前访问过的咖啡浇注隐喻的功能完全相同。 由于源列表仍在“发出”数据,因此我们希望跟踪将值从其中倒入第一个咖啡杯Acc1

Remember that in half/1’s last clause, we calculated the length of the original list? That is the Len variable here, and it stays the same throughout all the calls. It’s there so that we can compare Cnt counter to it divided by 2 to see if we have come to the middle of the source list and should switch to filling up Acc2 . That is where the case … of comes in.

还记得在half/1的最后一个子句中,我们计算了原始列表的长度吗? 这就是这里的Len变量,并且在所有调用中都保持不变。 在那里,我们可以将Cnt计数器与其除以2的值进行比较,以查看是否已到达源列表的中间,并且应该切换为填充Acc2 。 就是这种case … of

Now, let’s put them all together in mergesort/1 . This should be as simple as the Python version, and can be easily compared.

现在,让我们将它们放到mergesort/1 。 这应该与Python版本一样简单,并且可以轻松进行比较。

mergesort([A]) -> [A];mergesort([A, B]) ->  case A =< B of      true -> [A,B];      false -> [B,A]  end;mergesort(L) ->  {Left, Right} = half(L),  merge(mergesort(Left), mergesort(Right), []).

而已! (That’s it!)

At this point, either you think this is a novel and useful way of thinking about a problem, or you find it just plain confusing. But I hope you got something out of this programming approach that helps shine new light on how we can think about algorithms.

在这一点上,您要么认为这是一种解决问题的新颖且有用的方法,要么就会发现它令人困惑。 但是,我希望您能从这种编程方法中学到一些东西,这有助于使我们对如何思考算法有新的认识。

更新资料 (Update)

The Python implementation of merge function isn’t efficient because in each while loop the first element in the list is removed. Although this is a common pattern in functional languages like Erlang, in Python it is very costly to remove or insert an element anywhere other than the last position because unlike a list in Erlang which is a linked list which is very efficient to remove or add element at the head of the list, Python list behaves like an array which has to reposition all other elements when one is removed or added, incurring a O(n) runtime.

merge功能的Python实现效率不高,因为在每个while循环中,列表中的第一个元素均被删除。 尽管在Erlang等功能语言中这是常见的模式,但是在Python中,删除或插入除最后位置以外的其他位置的元素非常昂贵,因为与Erlang中的列表不同的是,链接列表非常有效地删除或添加元素在列表的顶部,Python列表的行为就像一个数组,当删除或添加一个元素时,它必须重新定位所有其他元素,从而导致O(n)运行时。

The better way is to sacrifice little space to define a counter variable for each list which can be incremented and used to access the current element of the source list without the need to remove the top-most element at all.

更好的方法是牺牲很少的空间为每个列表定义一个计数器变量,该计数器变量可以递增并用于访问源列表的当前元素,而根本不需要删除最顶层的元素。

def merge(a, b):    out = []
ai = 0    bi = 0
while (ai <= len(a) - 1 and bi <= len(b) - 1):         if (a[ai] <= b[bi]):            out.append(a[ai])            ai += 1        else:            out.append(b[bi])                        bi += 1
while (ai <= len(a) - 1):        out.append(a[ai])        ai += 1
while (bi <= len(b) - 1):        out.append(b[bi])        bi += 1
return out

翻译自: https://www.freecodecamp.org/news/a-functional-approach-to-merge-sort-and-algorithms-in-general-bbc12457eeb0/

mergesort

mergesort_Mergesort算法的功能方法相关推荐

  1. 神经网络模型提升算法性能的方法

    转自:https://machinelearningmastery.com/improve-deep-learning-performance/(英文原文) PS:找了好久,CSDN都分类为转载,但是 ...

  2. c语言检测正弦波波峰波谷,一种基于波峰波谷检测的计步算法的制作方法

    本发明涉及计步器算法领域,具体是一种基于波峰波谷检测的计步算法. 背景技术: 当今社会,健康越来越受到人们的重视,步行作为人类活动中最基础.最常见.最重要的运动形式,使得深入研究计步算法有着重要的意义 ...

  3. AI一分钟 | 网信办暂停快手、火山小视频算法推荐功能;无需人类司机,加州将允许自动驾驶汽车接送乘客

     整理 | 费棋 一分钟AI 据加州公共事业监管机构表示,它们将允许自动驾驶汽车接送乘客,且无需配备人类司机. 有知情人士透露,亚马逊公司正在考虑是否通过 Alexa 语音助手提供个人对个人的支付 ...

  4. python画简单的图形的代码-Python实现画图软件功能方法详解

    概述 虽然Python的强项在人工智能,数据处理方面,但是对于日常简单的应用,Python也提供了非常友好的支持(如:Tkinter),本文主要一个简单的画图小软件,简述Python在GUI(图形用户 ...

  5. LZW算法PHP实现方法 lzw_decompress php

    LZW算法PHP实现方法 lzw_decompress php 博客分类: Php / Pear / Mysql / Node.js LZW算法简介 字符串和编码的对应关系是在压缩过程中动态生成的,并 ...

  6. 数据结构与算法--解决问题的方法-顺时针打印矩阵

    顺时针打印矩阵 题目输入一个矩阵,按照从外向里顺时针的顺序依次打印每一个数字.例如下案例: 如上图矩阵,顺时针打印:1,2,3,4,8,12,16,15,14,13,9,5,6,7,1,10 以上问题 ...

  7. 数据结构与算法--解决问题的方法- 二叉树的的镜像

    解决问题的思路 工作中遇到的问题可能用到的数据结构由很多,并且各种数据结构都不简单,我们不可能光凭借想象就能得到问题的解法,因此画图是在家具问题过程中用来帮助自己分析,推理的常用手段.很多问题比较抽象 ...

  8. ie浏览器安全使用网银支付功能方法

    ie浏览器安全使用网银支付功能方法 ie浏览器怎么安全使用网银支付功能?每次使用我们在使用网银支付时,我们如果不放心自己银行卡的安全.我们在每次支付时候可以选择"清除SSL状态" ...

  9. python可以实现的功能_Python 实现某个功能每隔一段时间被执行一次的功能方法...

    本人在做项目的时候遇到一个问题: 某个函数需要在每个小时的 3 分钟时候被执行一次,我希望我 15:45 启动程序,过了18 分钟在 16:03 这个函数被执行一次,下一次过 60 分钟在 17:03 ...

最新文章

  1. php 编辑器中使用短代码,php-在WooCommerce短代码输出中更改标记
  2. 一个jstack/jmap等不能用的case
  3. PHP 循环删除无限分类子节点
  4. Java开发必须熟悉的Linux命令总结
  5. 深入理解Presto
  6. iPhone 13全系价格曝光:顶配售价将达新高
  7. android 开发 - 结束所有activity
  8. gcn在图像上的应用_使用图卷积网络(GCN)做图像分割
  9. php首字母 大写 数组去重复
  10. iPhone开发知识和项目
  11. 显示MSSQL SQL语句执行的时间
  12. 附032.Kubernetes实现蓝绿发布
  13. OneNote同步错误记录
  14. 【JavaFx 构建ProAdmin UI界面】
  15. json oracle 导入,JsonToOracle(Json导入Oracle工具)
  16. 【工业视觉-CCD相机和CMOS相机成像的本质区别】
  17. SSL 1231 容易的网络游戏
  18. 作者:​孙少陵(1972-),男,中移(苏州)软件技术有限公司高级工程师、副总经理。...
  19. Day01:基础入门-概念名词
  20. Halcon学习(1)初识Halcon HDevelop

热门文章

  1. 阿里P8亲自教你!mysql列转行
  2. scrapy框架的理解
  3. linux 单用户密码修改
  4. PPPOE拨号上网流程及密码窃取具体实现
  5. booth算法实现乘法器
  6. Nagios:企业级系统监控方案
  7. 打开editor的接口讨论
  8. 操作系统04进程同步与通信
  9. 【左偏树】【P3261】 [JLOI2015]城池攻占
  10. 海南首例供港造血干细胞志愿者启程赴广东捐献