《算法图解》笔记
C1 算法简介
1. 二分查找
如果数据的组织是有序的,那么从中间开始进行查找,每次判断可以排除一半的数据,可以极大的节省资源。对于包含n个元素的列表,用二分查找最多需要log2n步,而简单查找最多需要n步。
def binary_search(list, item):
low = 0
high = len(list)—1
while low <= high:
mid = (low + high)
guess = list[mid]
if guess == item:
return mid
if guess > item:
high = mid - 1
else:
low = mid + 1
return None
2. 大 O 表示法
大O表示法是一种特殊的表示法,指出了算法的速度有多快(大O表示法表示的是最慢的情形)。
- 假设列表包含n个元素。简单查找需要检查每个元素,因此需要执行n次操作,这个运行时间为O(n)。
- 二分查找需要执行log n次操作,使用大O表示法,这个运行时间为O(log n)。
算法的速度指的并非时间,而是操作数的增速,随着输入的增加,其运行时间将以什么样的速度增加。
C2 选择排序
数组和链表
- 数组:占用连续地址的内存空间,如果内存不够大,是无法创建数组的。此外,数组也无法增加或减少数量。
- 链表:可以存储在内存的任意空间,链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。这样的存在一个问题,当我们要查找第n个元素的内容时,必须从第一个元素开始逐个读取,效率很低。
效率
数组 | 链表 | |
---|---|---|
读取 | O(1) | O(n) |
插入 | O(n) | O(1) |
删除 | O(n) | O(1) |
- 数组读取时只需要读出对于地址的元素,但插入和删除时必须将元素后移。
- 链表读取时需逐个读取,插入和删除时只需要修改对应元素指向的地址即可。
选择排序
遍历所有数据,找到最大或最小的那个,存入新列表的第一个位置,再找出第二打或小的放入新列表,依此类推。
C3 递归
递归是指:在函数的定义中使用函数自身的方法。
每个递归函数都有两部分:
-
基线条件(base case)
-
递归条件(recursive case):定义如何停止,避免死循环。
栈
栈是只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。
使用栈虽然很方便,但是也要付出代价:存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。
C4 快速排序
分而治之
工作原理:
- 找出基线条件,这种条件必须尽可能简单。
- 不断将问题分解(缩小规模),直到符合基线条件。
快速排序
快速排序使用了分而治之的思想。
- 基准条件:只包含一个元素的序列;
- 问题分解:找到一个基准值,将大于它的放一边,小于的放另一边;
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(n log n),快速排序的性能高度依赖于基准值的选择。
最糟情况
基准值总是最边缘的元素,其中一个子数组始终为空,这导致调用栈非常长,最终效率为O(nlogn)
最佳情况
基准值总是中间的元素,因为你每次都将数组分成两半,所以不需要那么多递归调用,很快就到达了基线条件,因此调用栈短得多,最终效率为O(logn)。
(在调用栈的每层都涉及O(n)个元素)
值得注意的是,最佳情况也是平均情况。只要你每次都随机地选择一个数组元素作为基准值,快速排序的平均运行时间就将为O(nlog n)。
C5 散列表(哈希表)
散列函数
散列函数是这样的函数,即无论你给它什么数据,它都还你一个数字。
- 它必须是一致的。例如,假设你输入apple时得到的是4,那么每次输入apple时,得到的都必须为4。如果不是这样,散列表将毫无用处。
- 它应将不同的输入映射到不同的数字。例如,如果一个散列函数不管输入是什么都返回1,它就不是好的散列函数。最理想的情况是,将不同的输入映射到不同的数字。
散列表(哈希表)
散列表由键和值组成,是能够通过给定的键(关键字)的值直接访问到具体对应的值的一个数据结构。
冲突
给两个键分配的位置相同就会产生冲突。
处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表,但这样做会降低性能。如果链条很长,性能就会急速下降。
最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。
性能
在平均情况下,散列表执行各种操作的时间都为O(1),查找速度与数组一样快,而插入和删除速度与链表一样快。
(O(1)被称为常量时间。你以前没有见过常量时间,它并不意味着马上,而是说不管散列表多大,所需的时间都相同。)
但在最糟情况下,散列表的各种操作的速度都很慢。
散列表平均情况 | 散列表最糟情况 | 数组 | 链表 | |
---|---|---|---|---|
读取 | O(1) | O(n) | O(1) | O(n) |
插入 | O(1) | O(n) | O(n) | O(1) |
删除 | O(1) | O(n) | O(n) | O(1) |
因此,在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要有:
-
较低的填装因子
-
良好的散列函数
填装因子
填装因子 = 散列表存储的元素数 / 位置数
填装因子越低,发生冲突的可能性越小,散列表的性能越高。所以一旦填装因子开始增大,你就需要在散列表中添加位置,这被称为调整长度。一个不错的经验规则是:一旦填装因子大于0.7,就调整散列表的长度。
广度优先搜索
解决最短路径问题的算法被称为广度优先搜索。
图简介
图由节点和边组成,模拟一组连接,一个节点可能与众多节点直接相连,这些节点被称为邻居。
广度优先搜索
广度优先搜索是一种用于图的查找算法,可帮助回答两类问题。
-
从节点A出发,有前往节点B的路径吗?
-
从节点A出发,前往节点B的哪条路径最短?
查找最短路径
在广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,即先检查一度关系,再检查二度关系。
注意,只有按添加顺序查找时,才能实现这样的目的。有一个可实现这种目的的数据结构,那就是队列(queue)。
队列
队列的工作原理与现实生活中的队列完全相同。
队列只支持两种操作:入队和出队。
队列是一种先进先出(First In First Out,FIFO)的数据结构,而栈是一种后进先出(Last In First Out,LIFO)的数据结构。
实现图
图由多个节点组成,每个节点都与邻近节点相连。
而使用散列表可以实现图结构。
实现算法
需求:从你的关系网中寻找芒果商人
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
对于检查过的人,务必不要再去检查,否则可能导致无限循环。
运行时间
- 你将沿着每一条边前行,所以有O(边数)
- 你还使用了一个队列,其中包含要检查的每个人。将一个人添加到队列需要的时间是固定的,即为O(1),因此对每个人都这样做需要的总时间为O(人数)。
广度优先搜索的运行时间为O(人数 + 边数),这通常写作O(V + E),其中V为顶点(vertice)数,E为边数。