From 27c6856422ec3f409d05482ba182e3790ad20cf9 Mon Sep 17 00:00:00 2001 From: wizardforcel <562826179@qq.com> Date: Mon, 7 Aug 2017 17:04:37 +0800 Subject: [PATCH] ex16 --- ex16.md | 258 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 ex16.md diff --git a/ex16.md b/ex16.md new file mode 100644 index 0000000..ac4fdd3 --- /dev/null +++ b/ex16.md @@ -0,0 +1,258 @@ +# 练习 16:冒泡、快速和归并排序 + +> 原文:[Exercise 16: Bubble, Quick, and Merge Sort](https://learncodethehardway.org/more-python-book/ex16.html) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +你现在将尝试为你的`DoubleLinkedList`数据结构实现排序算法。对于这些描述,我将使用“数字列表”来表示随机的事物列表。这可能是一堆扑克牌,一张纸上的数字,名称列表或其他任何可以排序的东西。当你尝试排序数字列表时,通常有三个备选方案: + +> 冒泡排序 + +> 如果你对排序一无所知,这是你最可能尝试的方式。它仅仅涉及遍历列表,并交换你找到的任何乱序偶对。你不断遍历列表,交换偶对,直到你没有交换任何东西。很容易理解,但是特别慢。 + +> 归并排序 + +> 这种排序算法将列表分成两半,然后是四个部分,直到它不能再分割为止。然后,它将这些返回的东西合并,但是在合并它时,通过检查每个部分的顺序,以正确的顺序进行操作。这是一个聪明的算法,在链表上工作得很好,但在固定大小的数组上并不是很好,因为你需要某种`Queue`来跟踪部分。 + +> 快速排序 + +> 这类似于合并排序,因为它是一种“分治”算法,但它的原理是交换分割点周围的元素,而不是将列表拆分合并在一起。在最简单的形式中,你可以选择从下界到上界的范围和分割点。然后,交换分割点上方的大于它的元素,和下方的小于它的它元素。然后你选择一个新的下界,上界和分割点,它们在这个新的无序列表里面,再执行一次。它将列表分成更小的块,但它不会像归并排序一样拆分它们。 + +## 挑战练习 + +本练习的目的是,学习如何基于“伪代码”描述或“p-code”的实现算法。你将使用我告诉你的参考文献(主要是维基百科)研究算法,然后使用伪代码实现它们。在这个练习的视频中,我会在这里快速完成前两个,更细节的东西留作练习。那么你的工作就是自己实现快速排序算法。首先,我们查看维基百科中[冒泡排序](https://en.wikipedia.org/wiki/Bubble_sort)的描述,来开始: + +``` +procedure bubbleSort( A : list of sortable items ) + n = length(A) + repeat + swapped = false + for i = 1 to n-1 inclusive do + /* 如果这个偶对是乱序的 */ + if A[i-1] > A[i] then + /* 交换它们并且记住 */ + swap( A[i-1], A[i] ) + swapped = true + end if + end for + until not swapped +end procedure +``` + +你会发现,因为伪代码只是对算法的松散描述,它最终在不同书籍,作者和维基百科的页面之间截然不同。它假设你可以阅读这种“类编程语言”,并将其翻译成你想要的内容。有时这种语言看起来像是一种叫做 Algol 的旧语言,其他的时候它会像格式不正确的 JavaScript 或者 Python 一样。你只需要尝试猜测它的意思,然后将其翻译成你需要的。这是我对这个特定的伪代码的最初实现: + +```py +def bubble_sort(numbers): + """Sorts a list of numbers using bubble sort.""" + while True: + # 最开始假设它是有序的 + is_sorted = True + # 一次比较两个,跳过头部 + node = numbers.begin.next + while node: + # 遍历并将当前节点与上一个比较 + if node.prev.value > node.value: + # 如果上一个更大,我们需要交换 + node.prev.value, node.value = node.value, node.prev.value + # 这表示我们需要再次扫描 + is_sorted = False + node = node.next + + # 它在顶部重置过,但是如果我们没有交换,那么它是有序的 + if is_sorted: break +``` + +我在这里添加了其他注释,以便你可以学习并跟踪它,将我在此处完成的内容与伪代码进行比较。你还应该看到,维基百科页面正在使用的数据结构,与`DoubleLinkedList`完全不同。维基百科的代码假设在数组或列表结构上实现函数。你必须将下面这行: + +``` +if A[i-1] > A[i] then +``` + +使用`DoubleLinkedList`翻译为 Python: + +```py +if node.prev.value > node.value: +``` + +我们不能轻易地随机访问`DoubleLinkedList`,所以我们必须将这些数组索引操作转换为`.next`和`.prev`。在循环中,我们还必须注意`next`或`prev`属性是否是`None`。这种转换需要大量的翻译,学习和猜测你正在阅读的伪代码的语义。 + +### 学习冒泡排序 + +你现在应该花时间研究这个`bubble_sort`Python 代码,看看我如何翻译它。确保观看我实时的视频,并获得更多的透视。你还应该绘制在不同类型的列表(已排序,随机,重复等)上运行的图表。一旦你了解我是如何做到的,为此研究`pytest`和`merge_sort`算法: + +```py +import sorting +from dllist import DoubleLinkedList +from random import randint + +max_numbers = 30 + +def random_list(count): + numbers = DoubleLinkedList() + for i in range(count, 0, -1): + numbers.shift(randint(0, 10000)) + return numbers + + +def is_sorted(numbers): + node = numbers.begin + while node and node.next: + if node.value > node.next.value: + return False + else: + node = node.next + + return True + + +def test_bubble_sort(): + numbers = random_list(max_numbers) + + sorting.bubble_sort(numbers) + + assert is_sorted(numbers) + + +def test_merge_sort(): + numbers = random_list(max_numbers) + + sorting.merge_sort(numbers) + + assert is_sorted(numbers) +``` + +这个测试代码的一个重要部分是,我正在使用`random.randint`函数生成随机数据进行测试。这个测试不会测试许多边界情况,但这是一个开始,我们将在以后进行改进。记住,你没有实现`sort.merge_sort`,所以你可以不写这个测试函数,或者现在注释它。 + +一旦你进行了测试,并且写完了这个代码,再次研究维基百科页面,然后在尝试`merge_sort`之前,尝试一些其他的`bubble_sort`版本。 + +### 归并排序 + +我还没准备好让你自己实现它。我将再次对`merge_sort`函数重复此过程,但是这次我想让你尝试,从归并排序的维基百科页面 上的伪代码中实现该算法,然后再查看我怎么做。有几个建议的实现,但我使用“自顶向下”的版本: + +```py +function merge_sort(list m) + if length of m ≤ 1 then + return m + + var left := empty list + var right := empty list + for each x with index i in m do + if i < (length of m)/2 then + add x to left + else + add x to right + + left := merge_sort(left) + right := merge_sort(right) + + return merge(left, right) + +function merge(left, right) + var result := empty list + + while left is not empty and right is not empty do + if first(left) ≤ first(right) then + append first(left) to result + left := rest(left) + else + append first(right) to result + right := rest(right) + + while left is not empty do + append first(left) to result + left := rest(left) + while right is not empty do + append first(right) to result + right := rest(right) + return result +``` + +为`test_merge_sort`编写剩余测试用例函数,然后在这个实现上进行尝试。我会给你一个线索,当仅仅使用第一个`DoubleLinkedListNode`时,该算法效果最好。你也可能需要一种方法,从给定的节点计算节点数。这是`DoubleLinkedList`不能做的事情。 + +### 归并排序作弊模式 + +如果你尝试了一段时间并且需要作弊,这是我所做的: + +```py +def count(node): + count = 0 + + while node: + node = node.next + count += 1 + + return count + + +def merge_sort(numbers): + numbers.begin = merge_node(numbers.begin) + + # horrible way to get the end + node = numbers.begin + while node.next: + node = node.next + numbers.end = node + + +def merge_node(start): + """Sorts a list of numbers using merge sort.""" + if start.next == None: + return start + + mid = count(start) // 2 + + # scan to the middle + scanner = start + for i in range(0, mid-1): + scanner = scanner.next + + # set mid node right after the scan point + mid_node = scanner.next + # break at the mid point + scanner.next = None + mid_node.prev = None + + merged_left = merge_node(start) + merged_right = merge_node(mid_node) + + return merge(merged_left, merged_right) + + + +def merge(left, right): + """Performs the merge of two lists.""" + result = None + + if left == None: return right + if right == None: return left + + if left.value > right.value: + result = right + result.next = merge(left, right.next) + else: + result = left + result.next = merge(left.next, right) + + result.next.prev = result + return result +``` + +在尝试实现它时,我将使用此代码作为“备忘单”来快速获取线索。你还会看到,我在视频中尝试从头开始重新实现此代码,因此你可以看到我努力解决你可能遇到过的相同问题。 + +### 快速排序 + +最后,轮到你尝试实现`quick_sort`并创建`test_quicksort`测试用例。我建议你首先使用 Python 的普通列表类型实现简单的快速排序。这将有助于你更好地理解它。然后,使用简单的 Python 代码,并使其处理`DoubleLinkedList`(的头节点)。记住要把你的时间花费在这里,显然还要在你的`test_quicksort`里进行大量的调试和测试。 + +## 深入学习 + ++ 这些实现在性能上绝对不是最好的。尝试写一些丧心病狂的测试来证明这一点。你可能需要将一个很大的列表传给算法。使用你的研究来找出病态(绝对最差)的情况。例如,当你把一个有序的列表给`quick_sort`时会发生什么? ++ 不要实现任何改进,但研究你可以对这些算法执行的,各种改进方法。 ++ 查找其他排序算法并尝试实现它们。 ++ 它们还可以在`SingleLinkedList`上工作吗?`Queue`和`Stack`呢?它们很实用吗? ++ 了解这些算法的理论速度。你会看到`O(n^2)`或`O(nlogn)`的引用,这是一种说法,在最坏的情况下,这些算法的性能很差。为算法确定“大O”超出了本书的范围,但我们将在练习 18 中简要讨论这些度量。 ++ 我将这些实现为一个单独的模块,但是将它们作为函数,添加到`DoubleLinkedList`更简单吗?如果你这样做,那么你需要将该代码复制到可以处理的其他数据结构上吗?我们没有这样的设计方案,如何使这些排序算法处理任何“类似链表的数据结构”。 ++ 再也不要使用气泡排序。我把它包含在这里,因为你经常遇到坏的代码,并且我们会在练习 19 中提高其性能。