递归、快速排序、散列表(哈希表)

 

目录

递归

快速排序

快速排序

快速排序:数组基准值的选择:

快速排序: 完整代码

大O算法运行时间

平均情况和最糟情况

散列表(hash table)

散列函数的构造

将散列表用作缓存

冲突和性能

如何选择好的散列函数

如何实现散列表

填装因子

好的散列函数

散列表小结


 

递归

如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。

要让脚本停止运行,可按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服务器上)。

散列表非常适合用于防止重复。

 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值