目录
递归
如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。
要让脚本停止运行,可按Ctrl+C。
每个递归函数都有两部分:
- 递归条件(recursive case): 函数调用自己,
- 基线条件(base case): 函数不再调用自己,从而避免形成无限循环。
def countdown(i):
print i
if i <= 1: ←------基线条件
return
else: ←------递归条件
countdown(i-1)
在Python中,print是一个函数 .调用另一个函数时,当前函数暂停并处于未完成状态。该函数的所有变量的值都还在内存中。
栈有两种操作:压入和弹出。所有函数调用都进入调用栈。调用栈可能很长,这将占用大量的内存。
递归问题使用栈虽然方便,但代价是:存储详尽的信息可能占用大量的内存。两种选择:
- 重新编写代码,转而使用循环。
- 使用尾递归,并非所有的语言都支持尾递归。
快速排序
分而治之(divide and conquer,D&C):一种递归式问题解决问题的思路。D&C并非可用于解决问题的算法,而是每次递归调用都必须缩小问题的规模。提示编写涉及数组的递归函数时,基线条件通常是数组为空或只包含一个元素。
【欧几里得算法Euclidean Algorithm】
两个整数A和B的最大公约数【the Greatest Common Divisor ,GCD】 是能同时整除A 、B的最大整数。
欧几里得算法可以快速找出两个数的GCD。
找出GCD(A,B)的 欧几里得算法描述如下 :
- 如果 A = 0 那么 GCD(A,B)=B, 由于GCD(0,B)=B, 算法终止.
- 如果 B = 0 那么GCD(A,B)=A, 由于 GCD(A,0)=A, 算法终止。
- 把A写成商quotient 和余数remainder 的形式 (A = B⋅Q + R)
- 由于 GCD(A,B) = GCD(B,R),递归使用欧几里得算法找出 GCD(B,R)
D&C的工作原理:
(1) 找出简单的基线条件;
(2) 确定如何缩小问题的规模,使其符合基线条件。
1. 编写一个递归函数来计算列表包含的元素数。
2.找出二分查找算法的基线条件和递归条件
3 找出列表中最大的数字。
答案:
1.
def count(list): if list == []: return 0 return 1 + count(list[1:])
2. 二分查找的基线条件是数组只包含一个元素。如果要查找的值与这个元素相同,就找到了!否则,就说明它不在数组中。在二分查找的递归条件中,你把数组分成两半,将其中一半丢弃,并对另一半执行二分查找。
3.
def max(list): if len(list) == 2: return list[0] if list[0] > list[1] else list[1] sub_max = max(list[1:]) return list[0] if list[0] > sub_max else sub_max
快速排序
快速排序的工作原理。(可以暂时将数组的第一个元素用作基准值,不管将哪个元素用作基准值都行)
- 首先,从数组中选择一个元素,这个元素被称为基准值(pivot),
- 【分区(partitioning】将数组分成无序的两个子数组:找出比基准值小的元素、比基准值大的元素。
- 如果子数组是有序的,合并为【左边的数组 + 基准值 + 右边的数组】;
- 对子数组进行快速排序,再合并结果,就能得到一个有序数
快速排序比选择排序快得多。快速排序也使用了D&C。
基线条件为数组为空或只包含一个元素。在这种情况下,只需原样返回数组——根本就不用排序。
def quicksort(array):
if len(array) < 2:
return array
快速排序:数组基准值的选择:
将任何元素用作基准值都可行,快速排序的独特之处在于,其速度取决于选择的基准值。
对于以下数组[3,5,2,1,4],分区的各种可能方式有以下几种,【注意,这些子数组包含的元素个数都在0~4内】
归纳证明是一种证明算法行之有效的方式,它分两步:基线条件和归纳条件;在基线条件中,我证明这种算法对空数组或包含一个元素的数组管用。在归纳条件中,我证明如果快速排序对包含一个元素的数组管用,可以类推快速排序对任何长度的数组都管用。
快速排序: 完整代码
def quicksort(array):
if len(array) < 2:
return array ←------基线条件:为空或只包含一个元素的数组是“有序”的
else:
pivot = array[0] ←------递归条件
less = [i for i in array[1:] if i <= pivot] ←------由所有小于等于基准值的元素组成的子数组
greater = [i for i in array[1:] if i > pivot] ←------由所有大于基准值的元素组成的子数组
return quicksort(less) + [pivot] + quicksort(greater)
print quicksort([10, 5, 2, 3])
大O算法运行时间
思考: 最糟情况和平均情况是什么意思?
- 合并排序(merge sort)的运行时间为O(nlog n),比选择排序快得多!
- 快速排序在最糟情况下,其运行时间为O(n2)。在平均情况下的运行时间为O(n log n)。
- 相对于遇上最糟情况,快速排序遇上平均情况的可能性要大得多
from time import sleep
def print_items2(list):
for item in list:
sleep(1) # 在打印每个元素前都休眠1秒钟
print item
在大O表示法O(n)中,算法所需的固定时间量被称为常量。
通常不考虑这个常量,因为如果两种算法的大O运行时间不同,这种常量将无关紧要。
但是对快速查找和合并查找,常量的影响可能很大,快速查找的常量比合并查找小,因此如果它们的运行时间都为O(nlog n),快速查找的速度将更快。
平均情况和最糟情况
快速排序的性能高度依赖于你选择的基准值。
最佳情况也是平均情况,快速排序的平均运行时间就将为O(n log n)。
最佳情况:总是将中间的元素用作基准值: 层数为O(log n)(调用栈的高度为O(logn)),而每层需要的时间为O(n)。因此整个算法需要的时间为O(n) *O(log n) = O(n log n)。这就是。
最糟情况: 总是将第一个元素用作基准值,有O(n)层,因此该算法的运行时间为O(n) * O(n) =O(n^2)。
小结
1. D&C将问题逐步分解。使用D&C处理列表时,基线条件很可能是空数组或只包含一个元素的数组。
2. 实现快速排序时,请随机地选择用作基准值的元素。快速排序的平均运行时间为O(n log n)。
3. 大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在。
4.比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时,O(log n)的速度比O(n)快得多
散列表(hash table)
也被称为散列映射、映射、字典和关联数组。速度很快。被用于大海捞针式的查找。
散列表的内部机制:实现、冲突和散列函数、如何分析散列表的性能。
数组和链表都被直接映射到内存,但散列表更复杂,它使用散列函数来确定元素的存储位置。
栈并不能用于查找
比如用数组来实现记录商品价格,数组的每个元素包含两项内容:商品名和价格。如果将这个数组按商品名排序,就可使用二分查找在其中查找商品价格的时间将为O(log n),而散列函数查找商品价格的时间为O(1)
散列函数“将输入映射到数字”,它必须满足一些要求:
- 它必须是一致的:比如同一商品对应的价格; 散列函数总是将同样的输入映射到相同的索引。
- 它应将不同的输入映射到不同的数字/索引。
散列函数的构造
- 首先创建一个空数组,在这个数组中存储商品的价格,
- 将商品apple作为输入交给散列函数,
- 散列函数的输出为3,因此我们将苹果的价格存储到数组的索引3处
- 不断地重复这个过程,最终整个数组将填满价格。
- 查找某商品的价格: 只需将avocado作为输入交给散列函数,[散列函数准确地指出了价格的存储位置],无需在数组中查找,
- 散列函数知道数组有多大,只返回有效的索引。
Python提供的散列表实现为字典,用函数dict()来创建。Python创建散列表的快捷方式——使用一对大括号
散列表由键和值组成。在前面记录商品价格的散列表book中,键为商品名,值为商品价格。散列表将键映射到值。
将网址映射到IP地址被称为DNS解析(DNS resolution),散列表是提供这种功能的方式之一。
voted = {} # 使用散列表来检查是否重复,速度非常快
def check_voter(name):
if voted.get(name):
print("kick them out!" )
else:
voted[name] = True
print ("let them vote!")
将散列表用作缓存
缓存的工作原理:网站将数据记住,而不再重新计算。缓存具有如下两个优点:用户能够更快地看到网页;网站需要做的工作更少
缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中!
Facebook不仅缓存主页,还缓存About页面、Contact页面、Terms andConditions页面等众多其他的页面。因此,它需要将页面URL映射到页面数据。当你访问Facebook的页面时,它首先检查散列表中是否存储了该页面,是则发送缓存的数据,否则让服务器处理。
cache = {}
def get_page(url):
if cache.get(url):
return cache[url] ←------返回缓存的数据
else:
data = get_data_from_server(url)
cache[url] = data ←------先将数据保存到缓存中
return data
散列表适合用于:模拟映射关系;防止重复;缓存/记住数据,以免服务器再通过处理来生成它们。
冲突和性能
前面提到的,散列函数总是将不同的键映射到数组的不同位置。实际上,几乎不可能编写出这样的散列函数。
冲突(collision):给两个键分配的位置相同。
处理冲突最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表。
散列表中的大量元素都在链表中时,散列表的速度会很慢。有两个经验教训:
- 散列函数很重要。最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。
- 如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很好,这些链表就不会很长!
如何选择好的散列函数
在平均情况下,散列表执行各种操作的时间都为O(1)。
O(1)被称为常量时间。不管散列表多大,所需的时间都相同。
对于查找:简单查找的运行时间为线性时间;二分查找所需时间为对数时间。在散列表中查找所花费的时间为常量时间。
在平均情况下,散列表的速度确实很快。在最糟情况下,散列表所有操作的运行时间都为O(n)——线性时间。
在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速度与链表一样快,因此它兼具两者的优点!
但在最糟情况下,散列表的各种操作的速度都很慢。因此,在使用散列表时需要避免冲突,需要有:较低的填装因子;良好的散列函数。
如何实现散列表
不管你使用的是哪种编程语言,其中都内置了散列表实现。
填装因子
散列表使用数组来存储数据,因此你需要计算数组中被占用的位置数。计算公式如下:
填装因子度量的是散列表中有多少位置是空的。
假设你要在散列表中存储100种商品的价格,而该散列表包含100个位置。那么在最佳情况下,每个商品都将有自己的位置。这个散列表的填装因子为1。
填装因子大于1意味着商品数量超过了数组的位置数。一旦填装因子开始增大,你就需要在散列表中添加位置,这被称为调整长度(resizing):
- 首先创建一个更长的新数组:通常将数组增长一倍。
- 再用函数hash将所有的元素都插入到这个新的散列表中。
- 填装因子越低,发生冲突的可能性越小,散列表的性能越高。
- 一个不错的经验规则是:一旦填装因子大于0.7,就调整散列表的长度。
- 调整长度的开销很大。平均而言,即便考虑到调整长度所需的时间,散列表操作所需的时间也为O(1)。
好的散列函数
良好的散列函数让数组中的值呈均匀分布。糟糕的散列函数让值扎堆,导致大量的冲突。
散列表小结
散列表是一种功能强大的数据结构,其操作速度快,还能让你以不同的方式建立数据模型。经常使用。
可结合散列函数和数组来创建散列表。
冲突很糟糕,你应使用可以最大限度减少冲突的散列函数。
散列表的查找、插入和删除速度都非常快。散列表适合用于模拟映射关系。
一旦填装因子超过0.7,就该调整散列表的长度。
散列表可用于缓存数据(例如,在Web服务器上)。
散列表非常适合用于防止重复。