<<算法图解>> 学习笔记
第一章
二分查找
二分查找是一种算法, 其输入是一个有序的元素列表. 如果要查找的元素包含在列表中, 二分查找返回其位置: 否则返回 NULL
一般而言, 对于包含N个元素的列表, 用二分查找最多需要log2^N步, 而简单查找最多需要N步.
# –*– coding: utf-8 –*–
# @Time : 2019/1/4 21:53
# @Author : Damon_duan
# @FileName : binary_search.py
# @BlogsAddr : https://blog.youkuaiyun.com/Damon_duanlei
import time
import random
def binary_search(find_list, search_info):
low = 0
high = len(find_list) - 1
while low <= high:
mid = (low + high) // 2
if search_info == find_list[mid]:
return mid
if search_info > find_list[mid]:
low = mid + 1
else:
high = mid - 1
return None
def easy_search(find_list, search_info):
for i in range(len(find_list) - 1):
if search_info == find_list[i]:
return i
return None
if __name__ == '__main__':
list_a = [i for i in range(10000000)]
find_num = random.randint(0, 10000000)
print("find number is : {}".format(find_num))
t1 = time.time()
binary_search(list_a, find_num)
t2 = time.time()
easy_search(list_a, find_num)
t3 = time.time()
cost_1 = t2 - t1
cost_2 = t3 - t2
print("二分法查找花费时间:{}".format(cost_1))
print("简单查询法查找花费时间:{}".format(cost_2))
运行结果:
>>>
find number is : 8645596
二分法查找花费时间:0.0
简单查询法查找花费时间:0.3390543460845947
一些常见的大 O 运行时间
O( log n ) , 也叫对数时间, 这样的算法包括二分查找
O( n ) , 也叫线性时间, 这样的算法包括简单查找
O( n * log n ) , 这样的算法包括快速排序法
O( n^2 ) , 这样的算法包括 冒泡排序 插入排序 选择排序
O( n! ), 旅行家算法
O( log n ) 比 O( n ) 快, n 越大, 前者比后者就快的越多.
选择排序
数组和链表
链表: 链表中的元素可以储存在内存的任何地方. 链表的每个元素都存储了下一个元素的地址, 从而使一系列随机的内存地址串在一起. 只要有足够的内存空间, 就能为链表分配内存.链表的优势在插入元素,删除元素方面.
数组: 数组在内存中都是相连的,添加元素可能需要开辟新的内存空间将所有元素转移,对于数组,我们知道其中每个元素的地址. 数组的优势在查询方面.
**结论:**数组的元素都在一起, 链表的元素是分开的, 其中每个元素都存储下一个元素的地址. 数组的读取速度很快, 链表的插入和删除速度很快.
选择排序
以下代码为冒泡, 插入, 选择 排序示例
# –*– coding: utf-8 –*–
# @Time : 2019/1/4 20:24
# @Author : Damon_duanlei
# @FileName : sort_methord.py
# @BlogsAddr : https://blog.youkuaiyun.com/Damon_duanlei
import random
import copy
import time
def bubble_sort(sort_list):
for i in range(len(sort_list) - 1):
for j in range(len(sort_list) - 1 - i):
if sort_list[j] > sort_list[j + 1]:
sort_list[j], sort_list[j + 1] = sort_list[j + 1], sort_list[j]
def insert_sort(sort_list):
for i in range(1, len(sort_list)):
j = i
insert_num = sort_list[i]
while insert_num < sort_list[j - 1]:
sort_list[j] = sort_list[j - 1]
j -= 1
if j == 0:
sort_list[0] = insert_num
break
if insert_num >= sort_list[j - 1]:
sort_list[j] = insert_num
def selection_sort(sort_list):
new_list = []
while len(sort_list):
index = 0
min_num = sort_list[0]
for i in range(1, len(sort_list)):
if min_num > sort_list[i]:
min_num = sort_list[i]
index = i
new_list.append(sort_list.pop(index))
return new_list
if __name__ == '__main__':
list_a = [random.randint(0, 100) for i in range(20000)]
list_1 = copy.deepcopy(list_a)
list_2 = copy.deepcopy(list_a)
list_3 = copy.deepcopy(list_a)
t1 = time.time()
bubble_sort(list_1)
t2 = time.time()
insert_sort(list_2)
t3 = time.time()
new_list = selection_sort(list_3)
t4 = time.time()
print("冒泡排序用时: {}".format(t2 - t1))
print("插入排序用时: {}".format(t3 - t2))
print("选择排序用时: {}".format(t4 - t3))
运行结果:
>>>
冒泡排序用时: 27.151382446289062
插入排序用时: 20.87420392036438
选择排序用时: 7.898883581161499
以上三种排序方法的时间复杂度均为 O( n^2 ) 但是同样的元素排序后花费的时间出现的较大差异, 这与大 O 表示法中的常数有关, 后续部分将详细解释.
第三章 递归
递归
函数自己调用自己的过程
递归只是让解决方案更清晰, 并咩有性能上的优势. 甚至有些情况下, 使用循环的性能更好.
基线条件(递归出口)和递归条件
由于递归函数调用自己, 因此编写这样的函数很容易出错, 进而导致无线循环. 编写递归函数时, 必须告诉它何时停止递归. 正因为如此,每个递归函数都用两部分: 基线条件和递归条件. 递归条件指的是函数调用自己, 而基线条件则指的是函数不再调用自己, 从而避免形成无限循环.
栈
栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
计算机在内部使用被称为调用栈的栈.计算机如何使用调用栈,代码示例如下:
# –*– coding: utf-8 –*–
# @Time : 2019/1/6 17:05
# @Author : Damon_duanlei
# @FileName : demon_01.py
# @BlogsAddr : https://blog.youkuaiyun.com/Damon_duanlei
def greet(name):
print("hello, {} !".format(name))
greet2(name)
print("getting ready to say bye ...")
bye()
def greet2(name):
print("how are you {} ?".format(name))
def bye():
print("good bye !")
if __name__ == '__main__':
greet("Demon")
过程:
当你调用 greet(“damon”) , 计算机首先为该函数调用分配一块内存. 我们使用这些内存. 变量 name 被设置成 Damon 并存储到内存中. 每当调用函数时, 计算机都会想这样将函数调用涉及的所有变量的值存储到内存中.
接下来, 打印 hello Damon ! , 在调用greet2(“Damon”). 同样, 计算机也为这个函数调用分配一块内存.
计算机使用一个栈来表示这些内存块, 其中第二个内存块位于第一个内存块上面. 当打印 how are you Demon ? ,然后从函数调用返回.此时, 栈顶的内存块被弹出.
此时, 栈顶的内存块是函数greet的, 这意味着运行返回到了函数greet. 当调用函数greet2时, 函数 greet 只执行了一部分. 调用一个函数是, 当前函数暂停并处于未完成状态, 该函数的所有变量的值都还在内存中. 执行完greet2后, 回到函数 greet ,并从离开的地方开始接着往下执行: 首先打印 getting ready to say bye … ,在调用bye.
在栈顶添加了函数bye的内存块. 然后打印 good bye ! ,并从这个函数返回. 当回到函数 greet. 由于没有别的事情要做, 就从函数 greet 返回. 这个栈用于存储多个函数的变量, 被称为调用栈
递归调用栈
递归函数也使用调用栈! 过程和普通行数调用相同区别只是函数多次调用自己.栈在递归中扮演着重要角色. 使用栈很方便,但是也要付出代价: 存储详尽的信息可能占用大量的内存. 每个函数调用都占用一定的内存, 如果栈很高, 就意味着计算机存储了大量函数调用的信息. 在这种情况下,有两种选择: 1. 重新编写代码,转而使用循环 2.使用尾递归
以下代码为使用递归方式去斐波那锲数列
# –*– coding: utf-8 –*–
# @Time : 2019/1/6 17:39
# @Author : Damon_duanlei
# @FileName : Fibomacci.py
# @BlogsAddr : https://blog.youkuaiyun.com/Damon_duanlei
import time
def fibomacci(n):
if n == 0:
return 0
if n == 1:
return 1
ret = fibomacci(n - 1) + fibomacci(n - 2)
return ret
fibomacci_list = []
for i in range(50):
t1 = time.time()
fibomacci_list.append(fibomacci(i))
t2 = time.time()
print("第{}位数计算话费时间: {} 秒".format(i + 1, t2 - t1))
print(fibomacci_list)
当 n 取值超过 30 时,函数执行效率明显下降.
第四章 快速排序
分治法(divide and conquer, D&C )
分而治之,一中著名的递归式问题解决方法, 优秀的算法学家遇到使用任何已知算法都无法解决的问题时, 不会就此放弃, 而是尝试使用掌握的各种问题解决方法来找出解决方案. 分而治之的思路是一种通用的问题解决方法. 使用 D&C 解决问题的过程包括两个步骤.
- 找出基线条件, 这种条件必须尽可能简单
- 不断将问题分解,(或者说缩小规模) 直到符合基线条件
快速排序
快速排序是一种常用的排序算法, 比选择排序快的多. C语言标准库中的函数qsort实现就是快速排序, 快速排序也使用了 D&C.
快速排序思路
对排序算法来说, 最简单的数组是空数组或只包含一个元素的数组. 因此, 极限条件为数组为空数组或只包含一个元素(即 len(list) < 2 ). 在这种情况下, 只需原样返回数组, 根本就绪用排序
对于更长的数组. 对于包含两个元素的数组进行排序也很容易. 检查第一个元素是否比第二个元素小,如果不比第二个元素小, 就叫唤他们的位置.
当包含三个元素的数组呢? 别忘了,要使用 D&C ,因此需要将数组分解, 知道满足基线条件.
快速排序工作原理
首先, 从主组中选择一个元素, 这个元素被称为基准值(pivot)
接下来, 找出比基准值小的元素以及比基准值大的元素. 这时有了
- 一个由所有小于基准值的数字组成的子数组
- 基准值
- 一个由所有大于基准值的数组组成的子数组
这里知识进行了分区, 得到的两个子数组是无序的. 如果子数组是有序的,就可以: 左边数组 + 基准值 + 右边数组 得到一个有序数组. 那么如何对子数组排序呢? 递归的调用上面的过程直到子数组满足基线条件即数组为空数组或只有一个元素.
代码如下:
# –*– coding: utf-8 –*–
# @Time : 2019/1/12 22:04
# @Author : Damon_duanlei
# @FileName : quick_sort.py
# @BlogsAddr : https://blog.youkuaiyun.com/Damon_duanlei
import random
import time
import sys
sys.setrecursionlimit(3000) # 设置递归最大真毒为3000
def quick_sort(sort_list):
if len(sort_list) < 2:
return sort_list
else:
pivot = sort_list[0]
less = []
great = []
for i in sort_list[1:]:
if i <= pivot:
less.append(i)
else:
great.append(i)
return quick_sort(less) + [pivot] + quick_sort(great)
if __name__ == '__main__':
list_a = [random.randint(0, 1000) for i in range(10000)]
t1 = time.time()
quick_sort(list_a)
t2 = time.time()
print("快速排序用时: {}".format(t2 - t1))
运行结果:
>>>
快速排序用时: 0.019918441772460938
快速排序的独特之处在于, 其速度取决于选择的基准值. 在最糟情况下, 其运行时间为***O***(n^2). 在平均情况下, 快速排序的运行时间为**O***(n log(n))
平均情况和最糟情况
快速排序的性能高度依赖选择的基准值. 假设总是将第一个元素用作基准值, 且要处理的数组是有序的. 由于快速排序算法不检查输入数组是否有序, 因此他已然尝试对其进行排序. 这种情况下,数组并没有被分成两半,而是其中一个子数组使用为空, 这导致调用栈非常长. 而现在假设总是将中间元素用作基准值, 这种情况下,每次都将数组分成两半,不需要那么多递归调用就达到了基线条件, 因此调用栈短的多
上述示例中,第一个示例展示的事最糟情况, 而第二个示例展示的是最佳情况. 在最糟情况下, 栈长为 O( n ) ,而最佳情况下, 栈长为 O(log(n))
以下代码为前面内容给出快速排序算法代码,使用该代码对 0至 2500 有序列表排序, (因python解释器设置了递归最大深度为998, 为防止超过最大递归深度, 通过sys模块修改最大递归深度. 关于递归深度问题,感兴趣的小伙伴可以通过https://blog.youkuaiyun.com/Damon_duanlei/article/details/86098806 了解相关内容)
# –*– coding: utf-8 –*–
# @Time : 2019/1/12 22:04
# @Author : Damon_duanlei
# @FileName : quick_sort.py
# @BlogsAddr : https://blog.youkuaiyun.com/Damon_duanlei
import random
import time
import sys
sys.setrecursionlimit(3000) # 设置递归最大真毒为3000
def quick_sort(sort_list):
if len(sort_list) < 2:
return sort_list
else:
pivot = sort_list[0]
less = []
great = []
for i in sort_list[1:]:
if i <= pivot:
less.append(i)
else:
great.append(i)
return quick_sort(less) + [pivot] + quick_sort(great)
if __name__ == '__main__':
list_a = [i for i in range(2500)]
t1 = time.time()
quick_sort(list_a)
t2 = time.time()
print("快速排序用时: {}".format(t2 - t1))
运行结果:
>>>
快速排序用时: 0.26030516624450684
根据上述平均情况和最糟情况选择基准数对快速排序性能的影响对上方代码尝试做出优化,使pivot 取对应列表的中间值, 代码及运行结果如下:
# –*– coding: utf-8 –*–
# @Time : 2019/1/12 22:04
# @Author : Damon_duanlei
# @FileName : quick_sort.py
# @BlogsAddr : https://blog.youkuaiyun.com/Damon_duanlei
import random
import time
import sys
sys.setrecursionlimit(3000)
def quick_sort(sort_list):
if len(sort_list) < 2:
return sort_list
else:
pivot_index = (0 + len(sort_list) - 1)//2
pivot = sort_list[pivot_index]
less = []
great = []
for i in sort_list[1:]:
if i <= pivot:
less.append(i)
else:
great.append(i)
return quick_sort(less) + [pivot] + quick_sort(great)
if __name__ == '__main__':
list_a = [i for i in range(2500)]
t1 = time.time()
quick_sort(list_a)
t2 = time.time()
print("优化后快速排序用时: {}".format(t2 - t1))
运行结果:
>>>
优化后快速排序用时: 0.0029916763305664062
使用优化前与优化后对 10000 个 0至1000 元素排序,代码及运行结果如下:
# –*– coding: utf-8 –*–
# @Time : 2019/1/12 22:04
# @Author : Damon_duanlei
# @FileName : quick_sort.py
# @BlogsAddr : https://blog.youkuaiyun.com/Damon_duanlei
import copy
import random
import time
import sys
sys.setrecursionlimit(3000)
def quick_sort1(sort_list):
if len(sort_list) < 2:
return sort_list
else:
pivot = sort_list[0]
less = []
great = []
for i in sort_list[1:]:
if i <= pivot:
less.append(i)
else:
great.append(i)
return quick_sort1(less) + [pivot] + quick_sort1(great)
def quick_sort2(sort_list):
if len(sort_list) < 2:
return sort_list
else:
pivot_index = (len(sort_list) - 1)//2
pivot = sort_list[pivot_index]
less = []
great = []
for i in sort_list[1:]:
if i <= pivot:
less.append(i)
else:
great.append(i)
return quick_sort2(less) + [pivot] + quick_sort2(great)
if __name__ == '__main__':
list_a = [random.randint(0, 1000) for i in range(10000)]
list_1 = copy.deepcopy(list_a)
list_2 = copy.deepcopy(list_a)
t1 = time.time()
quick_sort1(list_1)
t2 = time.time()
quick_sort2(list_2)
t3 = time.time()
print("快速排序用时: {}".format(t2 - t1))
print("优化后快速排序用时: {}".format(t3 - t2))
运行结果:
快速排序用时: 0.01898193359375
优化后快速排序用时: 0.018950700759887695
结论:
优化后代码针对随机数组排序性能较优化前并无差异.但优化后的代码对有序数组(或部分有序数组) 性能远高于优化前代码.
附:
归并排序:
归并排序也是一种典型的采用 D&C 思路的算法, 归并排序平均和最糟情况下时间复杂度均为***O***(n log(n)).
代码如下:
# –*– coding: utf-8 –*–
# @Time : 2019/1/15 22:02
# @Author : Damon_duanlei
# @FileName : merge_sort.py
# @BlogsAddr : https://blog.youkuaiyun.com/Damon_duanlei
import random
def merge(list_a, list_b):
i = 0
j = 0
new_list = []
while i < len(list_a) and j < len(list_b):
if list_a[i] <= list_b[j]:
new_list.append(list_a[i])
i += 1
else:
new_list.append(list_b[j])
j += 1
new_list.extend(list_a[i:])
new_list.extend(list_b[j:])
return new_list
def merge_sort(sort_list):
if len(sort_list) <= 1:
return sort_list
else:
index = len(sort_list) // 2
left = sort_list[:index]
right = sort_list[index:]
new_list = merge(merge_sort(left), merge_sort(right))
return new_list
if __name__ == '__main__':
list_1 = [random.randint(0, 20) for i in range(50)]
list_2 = merge_sort(list_1)
print(list_1)
print(list_2)
下一章 散列表 keep going …