python算法简单了解

本文介绍了算法的基础知识,包括二分法、选择排序、递归、栈、快速排序、散列表、广度优先搜索、狄克斯特拉算法、贪婪算法、动态规划以及K最近邻算法。特别强调了算法的时间复杂度和空间效率,如二分查找的O(log n)复杂度和动态规划在解决最优化问题中的应用。此外,还提到了布隆过滤器和HyperLogLog等概率型数据结构在大数据场景下的高效使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

算法图解python

算法就是符合限制的条件下,把输入转成输出

二分法

二分查找是一种算法,其输入是一个有序的元素列表。如果要 查找的元素包含在列表中,二分查找返回其位置;否则返回null。算法复杂度O(log n)

  • 大O表示法指出了算法有多快。例如,假设列表包含n个元素。简 单查找需要检查每个元素,因此需要执行n次操作。使用大O表示法, 这个运行时间为O(n)。单位秒呢?没有——大O表示法指的并非以秒为单位的速度。大O表示法 让你能够比较操作数,它指出了算法运行时间的增速。
    • O(log n),也叫对数时间,这样的算法包括二分查找。
    • O(n),也叫线性时间,这样的算法包括简单查找。
    • O(n * log n),这样的算法包括第4章将介绍的快速排序——一种速度较快的排序算法。
    • O(n 2),这样的算法包括第2章将介绍的选择排序——一种速度较慢的排序算法。
    • O(n!),这样的算法包括接下来将介绍的旅行商问题的解决方案——一种非常慢的算法。
def binary_search(list, item):
	 """
    :param list: 传入一个列表
    :param item: 目标查找数
    :return: 返回值
    """
    low = 0
    high = len(list)-1
    while low <= high:
        mid = (low + high)//2
        guess = list[mid]
        if guess == item:
            return mid
        if guess > item:
            high = mid - 1
        else:
            low = mid + 1
    return None

在电话簿中根据名字查找电话号码。

在电话簿中根据电话号码找人。(提示:你必须查找整个电话簿。)

阅读电话簿中每个人的电话号码。

阅读电话簿中姓名以A打头的人的电话号码。这个问题比较棘手,它涉及第4章的概

念。答案可能让你感到惊讶!

选择排序

随着排序的进行,每次需要检查的元素数在逐渐减少,最后一次需要检查的元素都只有一 个。既然如此,运行时间怎么还是O(n2)呢?

这个问题问得好,这与大O表示法中的常数相关。 你说得没错,并非每次都需要检查n个元素。第一次需要检查n个元素,但随后检查的元素 数依次为n -1, n – 2, …, 2和1。平均每次检查的元素数为1/2 × n,因此运行时间为O(n × 1/2 × n)。 但大O表示法省略诸如1/2这样的常数(有关这方面的完整讨论,请参阅第4章),因此简单地写 作O(n × n)或O(n2)。

def findSmallest(arr): 
   smallest = arr[0]
   smallest_index = 0 
   for i in range(1, len(arr)): 
   	if arr[i] < smallest: 
           smallest = arr[i] 
           smallest_index = i 
   return smallest_index

def selectionSort(arr):
   newArr = [] 
   for i in range(len(arr)): 
       smallest = findSmallest(arr)
       newArr.append(arr.pop(smallest)) 
   return newArr

递归

汉诺塔问题

汉诺塔问题是一个古老的游戏。游戏的目的是将左方柱子上的盘子搬到右方的柱子。

游戏的规则有三条:

(1)一次搬一只盘子。

(2)每根柱子只有最上面的盘子可被搬动。

(3)大盘子不可置于小盘子的上方。

  • 递归
def func(n):
    """

    :param n: 汉诺塔问题中有几个盘子
    :return: 进行的次数
    """
    if n == 1:
        return 1
    return 2*func(n-1)+1
  • 非递归
def hannoi(n):
     """
    :param n: 汉诺塔问题中有几个盘子
    :return: 进行的次数
    """

    L = [0] * n
    for i in range(1, 2**n):
        b_i = bin(i)
        j = len(b_i) - b_i.rfind('1') - 1
        L[j] = ((L[j]+1)%3 if j%2 == 0 else (L[j]+2)%3)
    return i
def hannoi(n):
     """
    :param n: 汉诺塔问题中有几个盘子
    :return: 进行的次数
    """
        
    for i in range(1, 2**n):
        b_i = bin(i)
    return i

编写递归函数时,必须告诉它何时停止递归。正因为如此,每个递归函数都有两部分:基线 条件(base case)和递归条件(recursive case)。递归条件指的是函数调用自己,而基线条件则 指的是函数不再调用自己,从而避免形成无限循环。

你可将待办事 项添加到该清单的任何地方,还可删除任何一个待办事项。一叠便条要简 单得多:插入的待办事项放在清单的最前面;读取待办事项时,你只读取 最上面的那个,并将其删除。因此这个待办事项清单只有两种操作:压入 (插入)和弹出(删除并读取)。这种数据结构称为栈。

  • 递归指的是调用自己的函数。

  • 每个递归函数都有两个条件:基线条件和递归条件。

  • 栈有两种操作:压入和弹出。

  • 所有函数调用都进入调用栈。

  • 调用栈可能很长,这将占用大量的内存。

快速排序

  • 分而治之

D&C并不那么容易掌握,我将通过三个示例来介绍。首先, 介绍一个直观的示例;然后,介绍一个代码示例,它不那么好看, 但可能更容易理解;最后,详细介绍快速排序——一种使用D&C 的排序算法。

使用D&C解决问题的过程包括两个步骤。

(1) 找出基线条件,这种条件必须尽可能简单。

(2) 不断将问题分解(或者说缩小规模),直到符合基线条件。

归纳证明

刚才你大致见识了归纳证明!归纳证明是一种证明算法行之有效的方式,它分两步:基线 条件和归纳条件。是不是有点似曾相识的感觉?例如,假设我要证明我能爬到梯子的最上面。

递归条件是这样的:如果我站在一个横档上,就能将脚放到下一个横档上。换言之,如果我站 在第二个横档上,就能爬到第三个横档。这就是归纳条件。而基线条件是这样的,即我已经站 在第一个横档上。因此,通过每次爬一个横档,我就能爬到梯子最顶端。

对于快速排序,可使用类似的推理。在基线条件中,我证明这种算法对空数组或包含一个 元素的数组管用。在归纳条件中,我证明如果快速排序对包含一个元素的数组管用,对包含两 个元素的数组也将管用;如果它对包含两个元素的数组管用,对包含三个元素的数组也将管用, 以此类推。因此,我可以说,快速排序对任何长度的数组都管用。这里不再深入讨论归纳证明, 但它很有趣,并与D&C协同发挥作用。

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) 

散列表

散列函数准确地指出了价格的存储位置,你根本不用查找!之所以能够这样,具体原因如下。

  • 散列函数总是将同样的输入映射到相同的索引。每次你输入avocado,得到的都是同一个 数字。因此,你可首先使用它来确定将鳄梨的价格存储在什么地方,并在以后使用它来 确定鳄梨的价格存储在什么地方。

  • 散列函数将不同的输入映射到不同的索引。avocado映射到索引4,milk映射到索引0。每 种商品都映射到数组的不同位置,让你能够将其价格存储到这里。

  • 散列函数知道数组有多大,只返回有效的索引。如果数组包含5个元素,散列函数就不会 返回无效索引100。

散列表可能是最有用的,也被称为散列映射、映射、字典和 关联数组。散列表的速度很快!还记得第2章关于数组和链表的讨论吗?你可以立即获取数组中 的元素,而散列表也使用数组来存储数据,因此其获取元素的速度与数组一样快。

这里总结一下,散列表适合用于:

  • 模拟映射关系;

  • 防止重复;

  • 缓存/记住数据,以免服务器再通过处理来生成它们。

  • 散列函数很重要。前面的散列函数将所有的键都映射到一个位置,而最理想的情况是, 散列函数将键均匀地映射到散列表的不同位置。

  • 如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很 好,这些链表就不会很长!

你几乎根本不用自己去实现散列表,因为你使用的编程语言提供了散列表实现。你可使用 Python提供的散列表,并假定能够获得平均情况下的性能:常量时间。

散列表是一种功能强大的数据结构,其操作速度快,还能让你以不同的方式建立数据模型。

你可能很快会发现自己经常在使用它。

  • 你可以结合散列函数和数组来创建散列表。

  • 冲突很糟糕,你应使用可以最大限度减少冲突的散列函数。

  • 散列表的查找、插入和删除速度都非常快。

  • 散列表适合用于模拟映射关系。

  • 一旦填装因子超过0.7,就该调整散列表的长度。

  • 散列表可用于缓存数据(例如,在Web服务器上)。

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

广度优先搜索

广度优先搜索让你能够找出两样东西之间的最短距离,不过最短距离的含义有很多!使用广

度优先搜索可以:

  • 编写国际跳棋AI,计算最少走多少步就可获胜;

  • 编写拼写检查器,计算最少编辑多少个地方就可将错拼的单词改成正确的单词,如将 READED改为READER需要编辑一个地方;

  • 根据你的人际关系网络找到关系最近的医生。

队列

队列是一种先进先出(First In First Out,FIFO)的数据结构,而栈是一种后进先出(Last In First Out,LIFO)的数据结构。

队列的工作原理与现实生活中的队列完全相同。 假设你与朋友一起在公交车站排队,如果你排在他前面,你将先上车。队列的工作原理与此相同。队列类 似于栈,你不能随机地访问队列中的元素。

队列只支持两种操作:入队和出队。

from collections import deque

def search(name): 
    search_queue = deque() 
    search_queue += graph[name] 
    searched = [] 
    while search_queue: 
        person = search_queue.popleft() 
        if not person in searched:
            if person_is_seller(person): 
            	print(person + " is a mango seller!" )
        		return True 
            else: 
                search_queue += graph[person] 
                searched.append(person)
    return False 
search("you")

如果你在你的整个人际关系网中搜索芒果销售商,就意味着你将沿每条边前行(记住,边是 从一个人到另一个人的箭头或连接),因此运行时间至少为O(边数)。

你还使用了一个队列,其中包含要检查的每个人。将一个人添加到队列需要的时间是固定的, 即为O(1),因此对每个人都这样做需要的总时间为O(人数)。所以,广度优先搜索的运行时间为 O(人数 + 边数),这通常写作O(V + E),其中V为顶点(vertice)数,E为边数。

从某种程度上说,这种列表是有序的。如果任务A依赖于任务B,在列表中任务A就必须在任 务B后面。这被称为拓扑排序,使用它可根据图创建一个有序列表。假设你正在规划一场婚 礼,并有一个很大的图,其中充斥着需要做的事情,但却不知道要从哪里开始。这时就可使 用拓扑排序来创建一个有序的任务列表。

狄克斯特拉算法

如果你 要找出最快的路径,该如何办呢?为此,可使用另一种算法——狄克斯特拉 算法(Dijkstra’s algorithm)。

狄克斯特拉算法包含4个步骤。

(1) 找出“最便宜”的节点,即可在最短时间内到达的节点。

(2) 更新该节点的邻居的开销,其含义将稍后介绍。

(3) 重复这个过程,直到对图中的每个节点都这样做了。

(4) 计算最终路径。

在无向图中,每条边都是一个环。狄克斯特拉算法只适用于有向无环图(directed acyclic graph,DAG)。

这就是狄克斯特拉算法背后的关键理念:找出图中最便宜的节点,并确保没有到该节点的更 便宜的路径!

不能将狄克斯特拉算法用于包含负权边的图。在包含 负权边的图中,要找出最短路径,可使用另一种算法——贝尔曼福德算法(Bellman-Ford algorithm)。本书不介绍这种算法,你可以在网上找到其详尽的说明。

def find_lowest_cost_node(costs): 
    lowest_cost = float("inf") 
    lowest_cost_node = None 
    for node in costs:
        cost = costs[node] 
        if cost < lowest_cost and node not in processed:
        	lowest_cost = cost
        	lowest_cost_node = node 
    return lowest_cost_node


node = find_lowest_cost_node(costs)
while node is not None:
    cost = costs[node] 
    neighbors = graph[node] 
    for n in neighbors.keys(): 
    	new_cost = cost + neighbors[n] 
        if costs[n] > new_cost:
            costs[n] = new_cost
            parents[n] = node
	processed.append(node)
	node = find_lowest_cost_node(costs)

贪婪算法

是贪婪算法的优点—— 简单易行!

贪婪算法很简单:每步都采取最优的做法。在这个示例中,你每次都选择结束最早的 课。用专业术语说,就是你每步都选择局部最优解,最终得到的就是全局最优解。

while states_needed: 
    best_station = None 
    states_covered = set() 
    for station, states in stations.items(): 
        covered = states_needed & states 
        if len(covered) > len(states_covered): 
            best_station = station 
            states_covered = covered 
            
states_needed -= states_covered 
final_stations.add(best_station)

简言之, 没办法判断问题是不是NP完全问题,但还是有一些蛛丝马迹可循的。

  • 元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会变得非常慢。

  • 涉及“所有组合”的问题通常是NP完全问题。

  • 不能将问题分成小问题,必须考虑各种可能的情况。这可能是NP完全问题。

  • 如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是NP完全问题。

  • 如果问题涉及集合(如广播台集合)且难以解决,它可能就是NP完全问题。

  • 如果问题可转换为集合覆盖问题或旅行商问题,那它肯定是NP完全问题。

动态规划

动态规划都有哪些实际应用呢?

  • 生物学家根据最长公共序列来确定DNA链的相似性,进而判断度两种动物或疾病有多相 似。最长公共序列还被用来寻找多发性硬化症治疗方案。

  • 你使用过诸如git diff等命令吗?它们指出两个文件的差异,也是使用动态规划实现的。

  • 前面讨论了字符串的相似程度。编辑距离(levenshtein distance)指出了两个字符串的相 似程度,也是使用动态规划计算得到的。编辑距离算法的用途很多,从拼写检查到判断 用户上传的资料是否是盗版,都在其中。

  • 你使用过诸如Microsoft Word等具有断字功能的应用程序吗?它们如何确定在什么地方断 字以确保行长一致呢?使用动态规划!

K最近邻算法

KNN算法虽然简单却很有用!要对东西进行分类时,可首先尝试这种算法。下面来看一个更 真实的例子。

但愿通过阅读本章,你对KNN和机器学习的各种用途能有大致的认识!机器学习是个很有趣 的领域,只要下定决心,你就能很深入地了解它。

  • KNN用于分类和回归,需要考虑最近的邻居。

  • 分类就是编组。

  • 回归就是预测结果(如数字)。

  • 特征抽取意味着将物品(如水果或用户)转换为一系列可比较的数字。

  • 能否挑选合适的特征事关KNN算法的成败。

二叉树

在二叉查找树中查找节点时,平均运行时间为 O(log n),但在最糟的情况下所需时间为O(n);而在有序数组中查找时,即便是在最糟情况下所 需的时间也只有O(log n),因此你可能认为有序数组比二叉查找树更佳。然而,二叉查找树的插 入和删除操作的速度要快得多。

并行算法

归并函数可能令人迷惑,其理念是将很多项归并为一项。映射是将一个数组转换为另一个数组。

布隆过滤器和 HyperLogLog

布隆过滤器

布隆过滤器提供了解决之道。布隆过滤器是一种概率型数据结构,它提供的答案有可能不对, 但很可能是正确的。为判断网页以前是否已搜集,可不使用散列表,而使用布隆过滤器。使用散 列表时,答案绝对可靠,而使用布隆过滤器时,答案却是很可能是正确的。

  • 可能出现错报的情况,即Google可能指出“这个网站已搜集”,但实际上并没有搜集。
  • 不可能出现漏报的情况,即如果布隆过滤器说“这个网站未搜集”,就肯定未搜集。

布隆过滤器的优点在于占用的存储空间很少。使用散列表时,必须存储Google搜集过的所有 URL,但使用布隆过滤器时不用这样做。布隆过滤器非常适合用于不要求答案绝对准确的情况, 前面所有的示例都是这样的。对bit.ly而言,这样说完全可行:“我们认为这个网站可能是恶意的, 请倍加小心。”

HyperLogLog

HyperLogLog是一种类似于布隆过滤器的算法。如果Google要计算用户执行的不同搜索的数 量,或者Amazon要计算当天用户浏览的不同商品的数量,要回答这些问题,需要耗用大量的空 间!对Google来说,必须有一个日志,其中包含用户执行的不同搜索。有用户执行搜索时,Google 必须判断该搜索是否包含在日志中:如果答案是否定的,就必须将其加入到日志中。即便只记录 一天的搜索,这种日志也大得不得了!

HyperLogLog近似地计算集合中不同的元素数,与布隆过滤器一样,它不能给出准确的答案, 但也八九不离十,而占用的内存空间却少得多。 面临海量数据且只要求答案八九不离十时,可考虑使用概率型算法!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值