正如封面一样所说的一样 “像小说一样有趣的算法入门书”,其面的插画一样正诠释了其有趣性。 读完这本书,所以个人觉得这本书适合小白作为 “课外读物”一样去阅读,不适合专业的或者想系统学习的人将其作为核心读物。下面是自己读这本书的一些简记,不全面,但基本是本书的结构,仅供参考。
算法是一组完成任务的指令。
大 O 表示法是一种特殊的表示法,表示了算法的速度, 表示了最糟糕情况下的运行时间。假设一个列表包含 n n n 个元素,需要从元素中找到目标元素,则最简单的方式是一个一个找,则需要执行 n n n 次操作。使用大 O 表示法,这个的运行时间为 O ( n ) O(n) O(n) (实际上并不能完全将大 O 运行时间等价于操作数,但这个例子足够)。这个数值没有单位,指出了算法运行时间的增速。
常见的大 O 运行时间,由快到慢
输入:一种有序的元素列表
输出:如果需要查找元素在列表中,返回其位置;否则返回null
大 O 表示的运行时间为 O ( log n ) O(\log n) O(logn)
## 在列表中参照元素
def binary_search(bucket, item):
low = 0
high = len(bucket) - 1
while low <= high:
mid = (low + high) // 2
guess = bucket[mid]
if guess == item:
return mid
if guess > item:
high = mid - 1
print('guess number {:2d} is bigger'.format(guess))
else:
low = mid + 1
print('guess number {:2d} is smaller'.format(guess))
return None
if __name__ == '__main__':
l = [1, 3, 5, 6, 9, 15, 48]
r = binary_search(l, 5)
print(r)
链表:每个元素都存储了下一个元素的地址,从而使一系列随机的内存串起来,其插入数据方便。需要读取所有元素时,链表的效率很高;但当读取链表中某一个元素时比较低,这是因为读取这个元素,必须读取之前的元素,只能顺序访问。
数组:一个连起来的内存存储元素,即插入新元素时,重新分配,支持随机访问。
每次从列表中选择一个最小的元素放入一个新的列表,以此类推,取出目标列表的所有元素。
时间复杂度为 O ( n 2 ) O(n^2) 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):
new_arr = []
for i in range(len(arr)):
smallest_index = findSmallest(arr)
new_arr.append(arr.pop(smallest_index))
return new_arr
if __name__ == '__main__':
arr = [2, 1, 0, 3, 7, 45, 10]
new_arr = selectionSort(arr)
print('sorted : ', new_arr)
一种优雅的问题解决方法
# 计算阶乘
def factorial(num):
return 1 if num == 1 else num * factorial(num-1)
分而治之(divide and conquer,D&C),一种著名的递归式问题解决方法,快速排序的基本思想就是 D&C。
使用 D&C 解决问题的步骤:
# 4.1 计算列表中元素之和
def sum1(arr):
return arr[0] if len(arr) == 1 else arr.pop(0) + s(arr)
if __name__ == '__main__':
arr = [2, 4, 6, 8]
sum1(arr)
# 4.2 编写一个递归函数来计算列表包含元素数
def countList(arr):
return 0 if arr == [] else 1 + countList(arr[:-1])
if __name__ == '__main__':
arr = [2, 4, 6, 8]
nums = countList(arr)
print(nums)
# 4.3 找出列表中的最大值
def findMax(arr):
if arr == []:
return 0
else:
return arr[0] if arr[0] > findMax(arr[1:]) else findMax(arr[1:])
if __name__ == '__main__':
array = [2, 3, 1]
print(findMax(array))
def quickSort(arr):
if len(arr) < 2:
return arr
else:
pivot =arr[0]
less = [i for i in arr[1:] if i <= pivot]
greater = [i for i in arr[1:] if i > pivot]
return quickSort(less) + [pivot] + quickSort(greater)
# return quickSort(greater) + [pivot] + quickSort(less) ## 由大到小的顺序
if __name__ == '__main__':
arr = [1, 2, -9, 18, -7, -7]
print('Sorted:', quickSort(arr))
快速排序的速度取决于选择的基准值,在最糟糕的情况下,其运行时间为 O ( n 2 ) O(n^2) O(n2), 在平均情况下,快速排序的运行时间为 O ( n log n ) O(n \log n) O(nlogn)。
合并排序的运行时间恒为 O ( n log n ) O(n \log n) O(nlogn) ,如果快速排序的运行时间也为 O ( n log n ) O(n \log n) O(nlogn),然而由于大 O 记法忽略了常量的原因,快速排序算法比合并排序快。
数组和链表都被直接映射到内存,但散列表更为复杂,它使用散列函数来确定元素的存储位置。
Python提供的散列表实现为字典
散列表适用场景:
模拟映射关系
防止重复
缓存数据,以免服务器再通过处理来生成它们
冲突:给两个键分配的位置相同
在平均情况下,散列表执行各种操作的时间都为 O ( 1 ) O(1) O(1), O ( 1 ) O(1) O(1) 被称为常量时间。
填装因子度量的是散列表中有多少位置是空的,公式表示为: 散 列 表 包 含 的 元 素 数 位 置 总 数 \frac{散列表包含的元素数}{位置总数} 位置总数散列表包含的元素数。
图模拟一组连接,由节点和边组成。
可解的问题类型:
队列支持的操作:入队和出队
队列是一种先进先出(First In First Out, FIFO)的数据结构, 而栈是一种后进先出(Last In First Out, LIFO) 的数据结构。
from collections import deque
def person_is_seller(name):
return name[-1] == 'y'
def bfs(graph):
search_queue = deque()
search_queue += graph["you"]
searched = []
while search_queue:
person = search_queue.popleft()
if not person in searched:
if person_is_seller(person):
print(person +" is seller")
return True
else:
search_queue += graph[person]
searched.append(person)
return False
if __name__ == '__main__':
graph = {}
graph['you'] = ["alice","bob","claire"]
graph['bob'] = ["anuj","peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["thom","jooy"]
graph["anuj"] = []
graph["peggy"] = []
graph["thom"] = []
graph["jooy"] = []
flag = bfs(graph)
广度优先搜索的运行时间为 O ( 人 数 + 边 数 ) O(人数+边数) O(人数+边数)
要计算非加权图中的最短路径,可以使用广度优先搜索算法;如若解决的是加权图,则可用狄克斯特拉算法。狄克斯特拉算法包含4个步骤:
狄克斯特拉算法算法只适用于有向无环图
贪婪算法:每步都采取最优的做法。
# 电台覆盖问题
states_needed = set(['mt', 'wa', 'or', 'id', 'nv', 'ut', 'ca', 'az'])
stations = {}
stations['kone'] = set(['id', 'nv', 'ut'])
stations['ktwo'] = set(['wa', 'id', 'mt'])
stations['kthree'] = set(['or', 'nv', 'ca'])
stations['kfour'] = set(['nv', 'ut'])
stations['kfive'] = set(['ca', 'az'])
final_stations = []
while states_needed:
best_station = None
states_covered = set()
for station, stayes_for_station in stations.items():
covered = states_needed & stayes_for_station
if len(covered) > len(states_covered):
best_station = station
states_covered = covered
final_stations.append(best_station)
states_needed -= states_covered
final_stations
涉及 “所有组合” 的问题通常是 NP完全问题。
如果问题可转换为集合覆盖问题或旅行商问题,那它坑爹是 NP 完全问题
动态规划解决的问题的条件是是每个子问题是离散的,即相互之间不依赖
费曼算法(Feynman algorithm)算法的步骤:
# Longest Common Substring
def longest_common_substring(str1, str2):
cell = np.zeros(shape=(len(str1), len(str2)))
for idx_a, a in enumerate(str1):
for idx_b, b in enumerate(str2):
if a == b:
cell[idx_a][idx_b] = cell[idx_a-1][idx_b-1] + 1
return cell
if __name__ == '__main__':
import numpy as np
str1 = 'fosh'
str2 = 'forh'
cell = longest_common_substring(str1, str2)
print(cell)
# Longest Common Subsequence
def logest_common_subsequence(str1, str2):
cell = np.zeros(shape=(len(str1), len(str2)))
for idx_a, a in enumerate(str1):
for idx_b, b in enumerate(str2):
if a == b:
cell[idx_a][idx_b] = cell[idx_a-1][idx_b-1] + 1
else:
cell[idx_a][idx_b] = max(cell[idx_a-1][idx_b], cell[idx_a][idx_b-1])
return cell
if __name__ == '__main__':
import numpy as np
str1 = 'fosh'
str2 = 'fish'
cell = logest_common_subsequence(str1, str2)
print(cell)