如何简单快速地判断算法复杂度:
递归
def func1(x):
print(x)
func1(x-1)
def func2(x):
if x>0:
print(x)
func2(x+1)
虽然上面两个函数都调用了自身,但由于没有结束条件,故不是标准地递归。
def func3(x):
if x>0:
print(x)
func3(x-1)
def func4(x):
if x>0:
func4(x-1)
print(x)
func3(3)输出为3 2 1,执行过程为先输出再调用递归函数,流程如下图所示:
func4(3)输出为1 2 3,执行过程为先调用递归函数再输出,流程如下图所示:
递归实例:汉诺塔问题
代码实现:
def hanoi(n,a,b,c):
if n>0:
hanoi(n-1,a,c,b)
print("moving from %s to %s" %(a,c))
hanoi(n-1,b,a,c)
hanoi(3,'A','B','C')
查找
顺序查找
def linear_search(li,val):
for ind,v in enumerate(li):
if v == val:
return ind
else:
return None
二分查找-实例
mid所指的5比要查找的元素3大,故right指针所指的位置变为mid-1,此时mid指向的位置变为(0+3)//2=1。
mid所指的2比要查找的元素3小,故left指针所指的位置变为mid+1,此时mid指向的位置变为(2+3)//2=2。
如果位置2的元素不是3而是4,那么right将跑到left的左边,所以当right<left时说明列表没有我们要找的元素。
代码实现:
def binary_search(li,val):
left=0
right=len(li)-1
while left<=right: # 候选区有值
mid = (left+right)//2
if li[mid]==val:
return mid
elif li[mid]>val: # 待查找的值在mid左侧
right = mid - 1
else: # li[mid]<val 待查找的值在mid右侧
left = mid + 1
else:
return None
二分查找有一个while循环,但是循环减半的,故复杂度为O(logn)
内置列表查找函数:index()属于顺序查找,因为二分查找要求列表必须是有序的,而我们的列表不一定总是有序的。先进行排序再二分查找所用的时间可能比直接用顺序查找的时间要长,所以如果遇上无序列表,我们只需要查找一次,那么可以直接使用顺序查找;如果是后面需要多次用到查找,可以先排序,后面就可以用二分查找了。
排序
常见排序算法:
冒泡排序(Bubble Sort)
先比较第0位置所指的7和它后面一个元素5,7比5大,交换两个数的位置。以此类推,完成一趟排序后,最大的元素9被放到了最后。
经过n-1趟后,列表变为有序
代码实现:
def bubble_sort(li):
for i in range(len(li)-1): # 第i趟
for j in range(len(li)-i-1):
if li[j] > li[j+1]:
li[j], li[j+1] = li[j+1], li[j]
两层循环,算法的时间复杂度为O(n^2)
冒泡排序还有可以改进的地方,如果我们传入的列表已经是有序的列表,用上面的代码还是要执行n趟。我们可以在代码中定义,如果执行一趟后没有交换任何数,说明列表已经有序,直接退出循环。
# 冒泡排序改进版
def bubble_sort(li):
for i in range(len(li)-1): # 第i趟
exchange = False
for j in range(len(li)-i-1):
if li[j] > li[j+1]:
li[j], li[j+1] = li[j+1], li[j]
exchange = True
print(li)
if not exchange:
return
选择排序(Select Sort)
select_sort_simple()实现方式可以很直观的理解选择排序的思想,但不建议使用。因为新建一个列表保存取出来的最小值增加了内存空间,并且min()和remove操作时间复杂度都是n。选择排序select_sort()有两层循环,时间复杂度为O(n^2)。
def select_sort_simple(li):
li_new = []
for i in range(len(li)):
min_val = min(li)
li_new.append(min_val)
li.remove(min_val)
return li_new
def select_sort(li):
for i in range(len(li)-1): # i是第几趟
min_loc = i
for j in range(i+1, len(li)):
if li[j] < li[min_loc]:
min_loc = j
li[i], li[min_loc] = li[min_loc], li[i]
print(li)
插入排序
def insert_sort(li):
for i in range(1, len(li)): # i表示摸到的牌的下标
tmp = li[i]
j = i - 1 # j指的是手里的牌的下标
while j>=0 and li[j] > tmp:
li[j+1] = li[j]
j-=1
li[j+1] = tmp
print(li)
li=[3,2,4,1,5,7,9,6,8]
print(li)
insert_sort(li)
j>=0结束循环是考虑到当抽出来的元素3比前面的有序元素都小,指针j会一直往左移动,当j=-1时结束循环,将抽出的元素3插到j+1位置,作为第一个元素。如果li[j]<=tmp,直接退出循环,将抽出的元素插到li[j+1]处。
快速排序
假设我们归位的过程可以用partition()函数完成,快速排序整体框架如下:
def quick_sort(data, left, right):
if left<right: # 至少两个元素
mid = partition(data, left, right)
quick_sort(data, left, mid-1)
quick_sort(data, mid+1, right)
归位完成过程图示:
将第一位元素5取出来,左边留下一个空位,从右边取一个小于5的元素放到左边的空位处。此时右边又空出来一位,改从左边取一个大于5的元素放到右边的空位处。以此类推,当左右指针重合,则将元素5放到指针所指位置。
代码实现:
def partition(li, left, right):
tmp = li[left]
while left < right:
while left < right and li[right] >= tmp: #从右面找比tmp小的数
right -= 1 # 往左走一步
li[left] = li[right] #把右边的值写到左边空位上
# print(li, 'right')
while left < right and li[left] <= tmp:
left += 1
li[right] = li[left] #把左边的值写到右边空位上
# print(li, 'left')
li[left] = tmp # 把tmp归位
return left
def _quick_sort(li, left, right):
if left<right: # 至少两个元素
mid = partition(li, left, right)
_quick_sort(li, left, mid-1)
_quick_sort(li, mid+1, right)
def quick_sort(li):
_quick_sort(li, 0, len(li)-1)
归位过程指针分别从左右两侧移到重合处,复杂度为O(n),总共有logn次递归,故时间复杂度为O(nlogn)。
最坏的情况为传入的列表是倒序的,一次归位,将最大的元素放到最右边,递归只有左边,每次递归都只有左边,不是折半分,故时间复杂度会变为O(n^2)。
python默认的递归深度是很有限的,大概是900多的样子,当递归深度超过这个值的时候,就会引发这样的一个异常:RuntimeError: maximum recursion depth exceeded
。
解决的方式是手工设置递归调用深度,方式为:
import sys
sys.setrecursionlimit(1000000) #例如这里设置为一百万
堆排序
堆排序前传——树与二叉树
树的节点就相当于列表中的元素。在列表中,我们称为元素;在树结构中,我们把树的每个元素称为节点。
- 处在树的最顶端(没有双亲)的结点叫根节点;叶子节点就是树中最底段的节点,叶子节点没有子节点。
- 定义一棵树的根节点层次为1,其他节点的层次是其父节点层次加1。一棵树中所有节点的层次的最大值称为这棵树的深度,树的深度即看树结构有多少层。
- 节点的度为其孩子节点的个数,即节点分了几个杈。一棵树中,最大的节点的度称为树的度。如上图中的树,根节点A的度最大,分了6个杈,故树的度为6。
- 以前面的树为例,E是I的父节点,I是E的孩子节点。
- 将E,I,J, P,Q单独拎出来形成一棵子树。
堆排序前传——二叉树
堆排序前传——完全二叉树
堆排序前传——二叉树的存储方式
堆排序用到的是顺序存储方式,链式存储方式会在后面的数据结构部分讲解。
顺序存储方式可以理解为以列表的形式存储。
如果知道孩子节点的位置为i,那么它的父节点位置为(i-1)//2。
堆排序——什么是堆
堆排序——堆的向下调整性质
以上图为例,假如数字代表游戏玩家水平,很明显2比9和7要菜,当不了王者,所以把2踢下来,从星耀中选出能力更强的9成为王者;把2放在9原来的位置发现,2比8和5还菜,也不配砖石段位,所以从砖石段位选出能力更强的8作为星耀玩家;接着,2比6和4菜,把6提上砖石,2只能排在铂金段位,至此,就完成了一次堆的向下调整,最终得到一个大根堆。
堆排序过程
第三步之所以要将最后一个元素放到堆顶再进行调整,是为了防止二叉树变为非完全二叉树。
向下调整函数的实现:
def sift(li, low, high):
"""
:param li: 列表
:param low: 堆的根节点位置
:param high: 堆的最后一个元素的位置
:return:
"""
i = low # i最开始指向根节点
j = 2 * i + 1 # j开始是左孩子
tmp = li[low] # 把堆顶存起来
while j <= high: # 只要j位置有数
if j + 1 <= high and li[j+1] > li[j]: # 如果右孩子有并且比较大
j = j + 1 # j指向右孩子
if li[j] > tmp:
li[i] = li[j]
i = j # 往下看一层
j = 2 * i + 1
else: # tmp更大,把tmp放到i的位置上
li[i] = tmp # 把tmp放到某一级领导位置上
break
else:
li[i] = tmp # 把tmp放到叶子节点上
从最后的根节点开始依次往前使用向下调整即可获得大根堆或者小根堆。
堆排序的实现:
def heap_sort(li):
n = len(li)
for i in range((n-2)//2, -1, -1):
# i表示建堆的时候调整的部分的根的下标
sift(li, i, n-1)
# 建堆完成了
for i in range(n-1, -1, -1):
# i 指向当前堆的最后一个元素
li[0], li[i] = li[i], li[0]
sift(li, 0, i - 1) #i-1是新的high
这里将堆的最后一个元素放到堆顶,将原来堆顶的最大值放到空出来的最后一个位置处,不需要用额外的列表来存储堆顶数据,节省了存储空间。
堆排序的时间复杂度
向下调整函数sift()每次只走过树的左边或右边,所以时间复杂度是树的深度,即O(logn),heap_sort()函数中两个循环不是嵌套的,故时间复杂度为O(n)。
堆排序的时间复杂度是O(nlogn),和快速排序的时间复杂度一样,但在实际使用中,快速排序会比堆排序要快。
堆排序——内置模块
import heapq # q->queue 优先队列
import random
li = list(range(100))
random.shuffle(li)
print(li)
heapq.heapify(li) # 建堆,默认是小根堆
n=len(li)
for i in range(n):
print(heapq.heappop(li), end=',') # heappop每次弹出堆顶元素