我对算法很感兴趣,这次介绍的煎饼排序问题是在很多算法课程上都介绍过的经典例子。如果这是你第一次接触这个问题,我非常建议你在阅读时先独立思考解决方法。下面我们开始,希望大家喜欢咯。
- 煎饼排序: 维基百科给出的释义煎饼排序是数学上的一个问题的一种通俗叫法:对一堆无序的煎饼以大小排序,铲子可以在任意位置伸进去并且把上面的煎饼都翻转过来。
通俗点说,我们有一个锅铲和一堆煎饼,我们的目标是将煎饼按照大小排序,大的在下面。我们唯一的办法是让锅铲从一个地方伸进去,并且把上面所有的煎饼翻下来。举个栗子,一开始的煎饼是这样子的:
我们决定在这里铲入:
红色箭头代表插入位置,蓝色的表示新的煎饼堆底。
就是这样!
- 煎饼排序算法
(在你往下看之前,我建议你先自己想想解决办法。)
我现在讲的不是最好的办法,但是却是最直观最容易解释的。我选这种方法是为了向人们展示有些算法是非常容易并且直观的。我希望大家看了以后都能来尝试一下算法。通常,计算机专业会把普通人都吓跑,因为它一开始看上去太令人紧张。在这篇文章最后我将贴上更快的算法的链接。
- 将问题分解开来:
- 我们需要将煎饼排序,初始的形状可能是任意的。
- 我们只能对一部分煎饼进行翻转。
- 如果想让某一块特定的煎饼在最下面,需要先把它翻到最上面。
- 因此想要排好一块煎饼就需要先翻一下把它翻到顶上再把它翻到下面才行。
- 凭直觉想出来的算法
- 把未排序的煎饼中最大(或者顺序在最后)的煎饼翻下去(需要两步)。
- 重复第一步。
既然我们有了算法,那么我们就来思考一下看它正确与否吧:
- 只有一个煎饼的时候正确吗?——正确。
- 两个煎饼,大的在上,正确吗?——正确,我们翻一下就行了。
- 如果有三个煎饼,顺序从上到下依次是:中、大、小,正确吗?正确,我们先把大的翻上去,现在从上到下依次是大、中、小;然后我们再整个翻一下,顺序就变成了小、中、大。
既然在这几种简单情况下都是正确的,那么我们不妨用Python写出来吧~(欢迎在Github上Fork我的代码。)
# Sorts Pancakes def sortPancakes(stack): sorted_index = 10 for i in reversed(range(len(stack))): stack = flip(stack, findLargestPancake(stack, i)) print("Flip Up", stack) stack = flip(stack, i) print("Flip Down", stack) return stack # All of the pancakes are sorted after index # Returns the index of largest unsorted pancake def findLargestPancake(stack, index): largest_pancake = stack[index] largest_index = index; for i in range(index): if stack[i] > largest_pancake: largest_pancake = stack[i] largest_index = i print "" print("Insert Spatula in index", largest_index, "Size", largest_pancake) return largest_index # Slide spatula under pancake at index and flip to top def flip(stack, index): newStack = stack[:(index + 1)] newStack.reverse() newStack += stack[(index + 1):] return newStack stack = [1, 4, 5, 2, 3, 8, 6, 7, 9, 0] print"\nUnsorted:" print stack print "\nIterating:" stack = sortPancakes(stack) print "\nSorted:" print stack print ""
我们运行一下程序:
Unsorted: [1, 4, 5, 2, 3, 8, 6, 7, 9, 0] Iterating: (‘Insert Spatula in index’, 8, ‘Size’, 9) (‘Flip Up’, [9, 7, 6, 8, 3, 2, 5, 4, 1, 0]) (‘Flip Down’, [0, 1, 4, 5, 2, 3, 8, 6, 7, 9]) (‘Insert Spatula in index’, 6, ‘Size’, 8) (‘Flip Up’, [8, 3, 2, 5, 4, 1, 0, 6, 7, 9]) (‘Flip Down’, [7, 6, 0, 1, 4, 5, 2, 3, 8, 9]) (‘Insert Spatula in index’, 0, ‘Size’, 7) (‘Flip Up’, [7, 6, 0, 1, 4, 5, 2, 3, 8, 9]) (‘Flip Down’, [3, 2, 5, 4, 1, 0, 6, 7, 8, 9]) (‘Insert Spatula in index’, 6, ‘Size’, 6) (‘Flip Up’, [6, 0, 1, 4, 5, 2, 3, 7, 8, 9]) (‘Flip Down’, [3, 2, 5, 4, 1, 0, 6, 7, 8, 9]) (‘Insert Spatula in index’, 2, ‘Size’, 5) (‘Flip Up’, [5, 2, 3, 4, 1, 0, 6, 7, 8, 9]) (‘Flip Down’, [0, 1, 4, 3, 2, 5, 6, 7, 8, 9]) (‘Insert Spatula in index’, 2, ‘Size’, 4) (‘Flip Up’, [4, 1, 0, 3, 2, 5, 6, 7, 8, 9]) (‘Flip Down’, [2, 3, 0, 1, 4, 5, 6, 7, 8, 9]) (‘Insert Spatula in index’, 1, ‘Size’, 3) (‘Flip Up’, [3, 2, 0, 1, 4, 5, 6, 7, 8, 9]) (‘Flip Down’, [1, 0, 2, 3, 4, 5, 6, 7, 8, 9]) (‘Insert Spatula in index’, 2, ‘Size’, 2) (‘Flip Up’, [2, 0, 1, 3, 4, 5, 6, 7, 8, 9]) (‘Flip Down’, [1, 0, 2, 3, 4, 5, 6, 7, 8, 9]) (‘Insert Spatula in index’, 0, ‘Size’, 1) (‘Flip Up’, [1, 0, 2, 3, 4, 5, 6, 7, 8, 9]) (‘Flip Down’, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) (‘Insert Spatula in index’, 0, ‘Size’, 0) (‘Flip Up’, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) (‘Flip Down’, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) Pancake Sort Completed! Sorted:<em> </em>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
成功了!就是这么简单!
- 计算运行时间
计算一个算法的运行时间是非常重要的。这可以让你知道问题的复杂程度和规模。在计算机领域我们经常用大O表示法(Big-O Notation),它保证了程序的运行时间一定在这之内。(如果你想不起来或者根本没听说过这个名称,建议你移步维基。)
分析这个算法的运行时间还是比较直观的。可以想象,最差的情况是,这堆煎饼是交替的最小和最大。比如[0, 9, 1, 8, 2, 7, 3, 6, 5, 4]。我们需要先把9翻上去,再翻到底下。然后是8、7、6等等。每一个都需要两步,所以总的步数最大是2n-3。n是总的煎饼个数,“-3”是因为当倒数第二个归位了之后倒数第一个的位置自然也就正确了,并且倒数第二个只需要一下。有些Reddit的评论给我提了建议:为了避免混淆,我们在这里要说清楚我们忽略了”搜索时间“,现在计算的都是翻转所需要的时间。
- 运行时间(翻转所需要的):O(n)。
- 内存大小:O(n)。
然而,我们还没考虑每次搜索最大的煎饼所需要的时间。在我上面的代码中,如果要在翻转之前先找到最大的煎饼,我们必须搜索整个(还未排好序的)煎饼堆。这就使得我们最差的搜索时间成为了n乘以n。因为我们必须把每个都找一遍才能确定哪个是最大的,并且一共有n轮搜索。因此:
- 运行时间:O(n*n)。
- 内存大小:O(n)。
还需要说明的一点是,我的程序使用了不多余n的内存:
# Slide spatula under pancake at index and flip to top def flip(stack, index): newStack = stack[:(index + 1)] newStack.reverse() newStack += stack[(index + 1):] return newStack
我的解法需要O(n+k)的内存,k表示第一次翻转的大小,最大不多余n-1。因此我们的解答需要2n-1或者O(n)的内存。如果我们想避免这些开销我们可以原地翻转,在数组中挨个交换,但是这样代码就不好懂了。
- 结论
以上介绍的算法不是最快的。如果你还想进一步了解这个问题,我建议你读一下这篇Bill Gates和Christos Papadimitriou写的论文,以及Chitturi,Fahle,Meng以及一些其他人写的这篇论文。如果你喜欢这篇文章的话,可以来读一下我写的关于计数排序的文章。
之后我还会推出一个更难的”烧焦的煎饼问题“:
每块被翻到过最底下的煎饼被烧焦了,这个排序必须使得每个煎饼的烧焦的那一面在下。
这个解法将会在我的邮件列表中发给大家,或者你可以看这个更新。