算法图解笔记

算法图解

第一章 算法简介

1.1 引言

算法是一组完成任务的指令。任何代码片段都可视为算法。

1.2 二分查找

对数

你可能不记得什么是对数了,但很可能记得什么是幂。\(\lg100\) 相当于问“将多少个10相乘的结果是100”。答案是两个:\(10 * 10 = 100\)。因此,\(\lg100\) = 2。

对数运算是幂运算的逆运算。

\[ 10^2 = 100 \iff lg100 = 2\\ 10^3 = 1000 \iff lg1000 = 3\\ 2^3 = 8 \iff log_2 8 = 3\\ 2^4 = 16 \iff log_2 16 = 4\\ 2^5 = 32 \iff log_2 32 = 5\\ 对数是幂运算的逆运算 \]

本书使用的大O表示法讨论运行时间时,log指的都是\(log_2\)。使用简单查找法查找元素是,在最糟糕的情况下需要查看每一个元素。因此,如果列表包含8个数字,你最多需要检查8个数字。而使用二分查找时,最多需要检查\(log_2 n\)个元素。如果列表包含8个元素,你最多需要检查3个元素,因为\(log_2 8 = 3 \iff (2^3 = 8)\) 。如果列表包含1024个元素,你最多需要检查10个元素,因为\(log_2 1024 = 10 \iff (2^{10}) = 1024)\)

仅当列表是有序的时候,二分查找才管用。

def binary_search(list, item):
    low = 0                  		# low和high用于追踪要在其中查找的列表部分
    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
        if guess < item:     		# 猜的数字小了
            low = mid + 1
    return None              		# 没有指定的元素

my_list = [1,3,5,7,9]        		# 来测试一下

print binary_search(my_list, 3)  	# => 1  
print binary_search(my_list, -1) 	# => None  
									# 在Python中,None表示空,它意味着没有找到指定的元素
1 ⭐ 练习

假设有一个包含128个名字的有序列表,你要使用二分查找在其中查找一个名字,请问最多需要几步才能找到?

答:最多需要7步,因为\(log_2{128} = 7 \iff 2^{7} = 128\)

2 ⭐ 练习

上面列表的长度翻倍后,最多需要几步?

答:最多需要8步,因为\(log_2 {256} = 8 \iff 2^{256} = 8\)

运行时间

每次介绍算法时,我们都将讨论起运行时间。一般而言,应选择效率最高的算法,以最大限度地减少运行时间或占用空间。

简单查找逐个地检查数字,如果列表包含100个数字,最多需要猜100次。如果列表包含40亿个数字,最多需要猜40亿次。换言之,最多需要猜测的次数与列表的长度相同,这被称为线性时间(linear time)

1.3 大O表示法

算法的运行时间以不同的速度增加

大O表示法指出了算法有多快。例如,假设列表包含n个元素。简单查找需要检查每个元素,因此需要执行n次操作。使用大O表示法,这个运行时间为O(n)。单位秒呢?——大O表示法指的并非以秒为单位的速度。大O表示法让你能够比较操作数,它指出了算法运行时间的增速

大O表示法指出了最糟糕情况下的运行时间。

一些常见的大O运行时间

下面按从快到慢的顺序列出了你经常会遇到的5种大O运行时间。

  • O(log n),也叫对数时间,这样的算法包括二分查找。
  • O(n),也叫线性时间,这样的算法包括简单查找。
  • O(n*log n),这样的算法包括快速排序——一种速度较快的排序算法。
  • O(\(n^2\)),这样的算法包括选择排序——一种速度较慢的排序算法。
  • \(O(n!)\),这样的算法包括旅行商问题的解决方案——一种非常慢的算法。

大O表示法,我们获得的主要启示如下:

  • 算法的速度指的并非时间,而是操作数的增速。
  • 谈论算法的速度时,我们说的是随着输入的增加,其运行时间以什么样的速度增加。
  • 算法的运行时间用大O表示法表示。
  • \(O(log_2 n) 比O(n)快\),当需要搜索的元素越多时,前者比后者快得越多。

练习

使用大O表示法给出下述各种情形的运行时间。

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

    答:\(O(log_2 n)\)

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

    答:\(O(n)\)

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

    答:\(O(n)\)

  4. 阅读电话簿中姓名以A打头的人的电话号码。这个问题比较棘手,它涉及第四章的概念。答案可能让你惊讶!

    答:\(O(n)\)

    你可能认为,我只对26个字母中的一个这样做,因此运行时间应为\(O(n/26)\)。需要牢记的一条简单规则是,大O表示法不考虑乘以、除以、加上或减去的数字。下面这些都不是正确的大O运行时间:\(O(n+26)、O(n-26)、O(n*26)、O(n/26)\),它们都应表示为\(O(n)\)

1.4 小结

  • 二分查找的速度比简单查找快得多。
  • \(O(log_2 n)比O(n)快\)。需要搜索的元素越多,前者比后者就越快得多。
  • 算法运行时间并不以秒为单位。
  • 算法的运行时间是从其增速的角度度量的。
  • 算法的运行时间用大O表示法表示。

第二章 选择排序

2.1 内存的工作原理

我想,这是一个极好的比喻。

假如你去看演出,需要将东西寄存。寄存处有一个柜子,柜子有很多抽屉。

每个抽屉客房一样东西,你有两样东西要寄存,因此要了两个抽屉。

你将两样东西存放在这里。一把雨伞,和一只可爱的小兔纸玩偶。

现在你可以去看演出了。

这大致就是计算机内存的工作原理。计算机就像是很多抽屉的集合体,每个抽屉都有地址。

fe0ffeeb是一个内存单元的地址。

需要将数据存储到内存时,你请求计算机提供存储空间,计算机给你一个存储地址。需要存储多项数据时,有两种基本方式——数组和链表。

2.2 数组与链表

2.2.1 链表
2.2.2 数组

数组的特点是在计算机内存中是连续的,因此可以通过数组下标快速找到相应的内存地址的数据。

缺点:

  • 你额外请求的位置可能根本用不上,这将浪费内存。你没有使用,别人也用不了。
  • 超过数组最大容量后,还得转移。
2.2.3 术语

数组的元素带编号,编号从0而不是1开始。

元素的位置成为索引。

下面列出了常见的数组和链表操作的运行时间。

数组 链表
读取 \(O(1)\) \(O(n)\)
插入 \(O(n)\) \(O(1)\)
删除 \(O(n)\) \(O(1)\)

\(O(1) = 常量时间\)

\(O(n) = 线性时间\)

2​.​1⭐练习

假设你要编写一个记账的应用程序。

1. 卖杂货

2. 看电影

3. SFBE会费

你每天都将所有的支出记录下来,并在月底统计支出,算算当月花了多少钱。因此,你执行插入操作很多,但读取操作很少。该使用数组还是链表呢?

答:在这里,你每天都在列表中添加支出项,但每月只读取支出一次。数组的读取速度快,但插入速度慢;链表的去读速度慢,而插入速度快。由于你执行的插入操作比读取操作多,因而使用链表更合适。另外,仅当你要随机访问元素时,链表的读取速度才慢。鉴于你要读取所有的元素,在这种情况下,链表的读取速度也不慢。因此,对这个问题来说,使用链表是不错的解决方案。

2.2.4 在中间插入
2.2.5 删除
2.2⭐练习

假设你要为饭店创建一个接受顾客点菜单的应用程序。这个应用程序存储一系列点菜单。服务员添加点菜单,而厨师取出点菜单并制作菜肴。这是一个点菜单队列:服务员在队尾添加点菜单,厨师取出队列开头的点菜单并制作菜肴。

你使用数组还是链表来实现这个队列呢?(提示:链表擅长插入和删除,而数组擅长随机访问。在这个应用程序中,你要执行的是那些操作呢?)

答:使用链表。经常要执行插入操作(服务员添加点菜单),而这正是链表擅长的。不需要执行(数组擅长的)查找和随机访问操作,因为厨师总是从队列中取出第一个点菜单。

2.3⭐练习

我们来做一个思考实验。假设Facebook记录一系列用户名,每当用户试图登录Facebook时,都查找其用户名,如果找到就允许用户登录。由于经常有用户登录Facebook,因此需要执行大量的用户名查找操作。假设Facebook使用二分查找算法,而这种算法要求能够随机访问——立即获得中间的用户名。考虑到这一点,应使用数组还是链表来存储用户名呢?

答:有序数组。数组让你能够随机访问——立即获取数组中间的元素,而使用链表无法这样做。要获得链表的中间的元素,你必须从第一个元素开始,沿链表逐渐找到这个元素。

2.4⭐练习

经常有用户在Facebook注册。假设你已决定使用数组来存储用户名,在插入方面数组有何缺点?具体地说,在数组中添加新用户将出现什么情况?

答:数组的插入速度很慢。另外,要使用二分查找算法来查找用户名,数组必须是有序的。假设有一个名为AditB的用户在Facebook注册,其用户名将插入到数组到末尾,因此每次插入用户名后,你都必须对数组进行排序。

2.5⭐练习

实际上,Facebook存储用户信息时使用的既不是数组也不是链表。假设Facebook使用的是一种混合数组:链表数组。这个数组包含26个元素,每个元素都指向一个链表。例如,该数组的第一个元素指向的链表包含所有以A打头的用户,而第二个元素指向的链表包含所有以B打头的用户,以此类推。

请问,相比于数组和链表,这种混合数据结构的查找和插入速度更快还是更慢?你不必给出大O运行时间,只需指出这种新数据结构的查找和插入速度更快还是更慢。

答:查找时,其速度比数组慢,但比链表快;而插入时,其速度比数组快,但与链表相当。因此,其查找速度比数组慢。但在各方面都不比链表慢。本书后面将介绍另一种混合数据结构——散列表。这个练习应该能让你对如何使用简单数据架构构建复杂的数据结构有大致了解。

Facebook实际使用的是什么呢?很可能是十多个数据库,他们基于众多不同的数据结构:散列表、B树等。数组和链表是这些更复杂的数据结构的基石。

2.2.6 选择排序代码
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

print selectionSort([5,3,6,2,10])
posted @ 2020-08-18 23:24  accordionmaster  阅读(261)  评论(0)    收藏  举报