python算法和数据结构
算法基础
算法概念
算法(Algorithm):⼀个计算过程,解决问题的⽅法
Niklaus Wirth: “程序=数据结构+算法”

时间复杂度




时间复杂度-小结
-
时间复杂度是⽤来估计算法运⾏时间的⼀个式⼦(单位)。
-
⼀般来说,时间复杂度⾼的算法⽐复杂度低的算法慢。
-
常⻅的时间复杂度(按效率排序)
-
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n2logn)<O(n3)
-
复杂问题的时间复杂度
-
O(n!) O(2n) O(nn) …

空间复杂度
空间复杂度:⽤来评估算法内存占⽤⼤⼩的式⼦
空间复杂度的表示⽅式与时间复杂度完全⼀样
算法使⽤了⼏个变量:O(1)
算法使⽤了⻓度为n的⼀维列表:O(n)
算法使⽤了m⾏n列的⼆维列表:O(mn)
“空间换时间”
复习:递归

递归实例:汉诺塔问题





查找排序
查找
查找:在⼀些数据元素中,通过⼀定的⽅法找出与给定关键字相同的数据元素的过程。
列表查找(线性列表查找):从列表中查找指定元素
输⼊:列表、待查找元素输出:元素下标(未找到元素时⼀般返回None或-1)
内置列表查找函数:index()
顺序查找

二分查找
⼆分查找:⼜叫折半查找,从有序列表的初始候选区li[0:n]开始,通过对待查找的值与候选区中间值的⽐较,可以使候选区减少⼀半。


二分查找和线性查找速度比较
"""计算时间的装饰器"""
import time
def cal_time(func):
def wrapper(*args, **kwargs):
t1 = time.time()
result = func(*args, **kwargs)
t2 = time.time()
print("%s running time: %s secs." % (func.__name__, t2 - t1))
return result
return wrapper
"""计算时间的装饰器"""
from cal_time import *
# 线性查找
@cal_time
def linear_search(li, val):
for ind, v in enumerate(li):
if v == val:
return ind
else:
return None
# 二分查找
@cal_time
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
li = list(range(1000000))
linear_search(li, 38900)
binary_search(li, 38900)
# ---------------------run result-------------------
linear_search running time: 0.015621423721313477 secs.
binary_search running time: 0.0 secs.
排序
什么是列表排序
排序:将⼀组“⽆序”的记录序列调整为“有序”的记录序列。
列表排序:将⽆序列表变为有序列表
输⼊:列表
输出:有序列表
升序与降序
内置排序函数:sort()
常⻅排序算法介绍

常见排序算法分析
冒泡排序 (Bubble Sort)
列表每两个相邻的数,如果前⾯⽐后⾯⼤,则交换这两个数。
⼀趟排序完成后,则⽆序区减少⼀个数,有序区增加⼀个数。
代码关键点:趟、⽆序区范围

选择排序 (Select Sort)
⼀趟排序记录最⼩的数,放到第⼀个位置
再⼀趟排序记录记录列表⽆序区最⼩的数,放到第⼆个位置
……
算法关键点:有序区和⽆序区、⽆序区最⼩数的位置

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)
li = [3,4,2,1,5,6,8,7,9]
print(li)
select_sort(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)
快速排序


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)
@cal_time
def quick_sort(li):
_quick_sort(li, 0, len(li) - 1)
li = list(range(10000, 0, -1))
快速排序的效率:
快速排序的时间复杂度 O(nlogn)
快速排序的问题:
最坏情况:如果数据本身顺序与想要的排序完全相反,每次查找的时候只归位一个数据,所以最后排序的时间复杂度是O(n**2)。解决办法:第一个要排序的数随机去获取,但是不排除每次都随机获取最大的,不过几率会很小
递归:python3递归最大998层,python2是999层# 修改递归最大成熟 import sys sys.setrecursionlimit(设置上限值)
堆排序
堆排序前传-树与二叉树

⼀些概念
根节点、叶⼦节点(末端没有分支的节点)
树的深度(⾼度)
树的度 (节点的度:每个节点有几个分叉):整个树分叉最多的节点的度数
孩⼦节点/⽗节点
⼦树
二叉树

完全二叉树

⼆叉树的存储⽅式(表示⽅式)
- 链式存储⽅式
- 顺序存储⽅式(堆排序用)

子节点找父节点:(i-1)//2
堆排序的概念
堆:⼀种特殊的完全⼆叉树结构
⼤根堆:⼀棵完全⼆叉树,满⾜任⼀节点都⽐其孩⼦节点⼤
⼩根堆:⼀棵完全⼆叉树,满⾜任⼀节点都⽐其孩⼦节点⼩

堆的性质:向下调整性质
假设根节点的左右⼦树都是堆,但根节点不满⾜堆的性质可以通过⼀次向下的调整来将其变成⼀个堆。

堆排序的过程
1.建⽴堆。
2.得到堆顶元素,为最⼤元素
3.去掉堆顶,将堆最后⼀个元素放到堆顶,此时可通过⼀次调整重新使堆有序。
4.堆顶元素为第⼆⼤元素。
5.重复步骤3,直到堆变空。

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):
"""
构建堆
:param li:
:return:
"""
n = len(li)
for i in range((n-2)//2, -1, -1): # 父节点=(i-1)//2=(n-2)//2=n//2-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
li = [i for i in range(100)]
import random
random.shuffle(li)
print(li)
heap_sort(li)
print(li)
堆的内置模块
import heapq # q 代表有限队列
import random
li = list(range(10))
random.shuffle(li)
heapq.heapify(li) # 建堆
n = len(li)
for i in range(n): # 小根堆 取值先取最小的
print(heapq.heappop(li))
topk问题
现在有n个数,设计算法得到前k⼤的数。(k<n)
解决思路:
排序后切⽚ O(nlogn) +k
排序LowB三⼈组 O(kn)
堆排序思路 O(nlogk)
堆排序解决topk问题思路:
取列表前k个元素建⽴⼀个⼩根堆。堆顶就是⽬前第k⼤的数。
依次向后遍历原列表,对于列表中的元素,如果⼩于堆顶,则忽略该元
素;如果⼤于堆顶,则将堆顶更换为该元素,并且对堆进⾏⼀次调整;
遍历列表所有元素后,倒序弹出堆顶。
# _*_coding:utf-8_*_
# created by Alex Li on 12/3/17
# 比较排序
def sift(li, low, high):
i = low
j = 2 * i + 1
tmp = li[low]
while j <= high:
if j + 1 <= high and li[j+1] < li[j]:
j = j + 1
if li[j] < tmp:
li[i] = li[j]
i = j
j = 2 * i + 1
else:
break
li[i] = tmp
def topk(li, k):
heap = li[0:k] # 要排序的范围做成一个小根堆,确定堆的大小
# 1.建堆
for i in range((k-2)//2, -1, -1):
sift(heap, i, k-1)
# 2.找出列表右侧(根右侧剩余的列表)最大的数放在根
for i in range(k, len(li)-1):
if li[i] > heap[0]:
heap[0] = li[i]
sift(heap, 0, k-1)
# 3.出数
for i in range(k-1, -1, -1):
heap[0], heap[i] = heap[i], heap[0]
sift(heap, 0, i - 1)
return heap
import random
li = list(range(1000))
random.shuffle(li)
print(topk(li, 10))
归并排序
假设现在的列表分两段有序,如何将其合成为⼀个有序列表
这种操作称为⼀次归并。



def merge(li, low, mid, high):
"""
归并函数
:param li: 传过来的完成或者部分列表
:param low: 列表起始位置
:param high: 列表的结束位置
:param low ~ mid : 列表左半部分
:param mid+1 ~ high: 列表右半部分
:return:
"""
i = low # 初始的,让 i 等于列表的起始位置
j = mid + 1 # 列表右半部分的起始位置
tmp_list = [] # 临时列表,归并比大小时用到
while i <= mid and j <= high: # 左右两部分列表无论谁先结束,都退出循环
if li[i] < li[j]: # 如果左边i指向的元素比右边的小
tmp_list.append(li[i]) # 就把左边i指向的元素添加到临时列表中
i += 1 # 让i往往右移动一位
else: # 否则,就是右边的j指向的元素比左边i指向的元素大
tmp_list.append(li[j]) # 就把右边j指向的元素添加到临时列表中
j += 1 # 让j往右移动一位
# 当上面的循环结束后,两部分列表肯定有一边先结束,那另一边的列表剩余的元素需要手动添加到临时列表中
# 问题来了,哪边先结束呢?这里选择分别看一下
while i <= mid: # 如果左边的列表还有值,就依次添加到临时列表中
tmp_list.append(li[i])
i += 1
while j <= high: # 如果右边的列表还有值,就依次添加到临时列表中
tmp_list.append(li[j])
j += 1
# 左右两边都出数完成了,需要将临时列表写入到原来的列表中
# 临时列表是个有序列表,而原列表是无序列表,让临时列表中的元素替换到原列表中的元素
# 替换后,传过来的原列表的部分元素是有序的了
li[low:high + 1] = tmp_list
def merge_sort(li, low, high):
""" 归并排序 """
if low < high: # 至少有两个元素,就进行递归
mid = (low + high) // 2 # mid:把列表分两部分
merge_sort(li, low, mid) # 左半部分列表
merge_sort(li, mid + 1, high) # 右半部分列表
merge(li, low, mid, high) # 将左右两边的列表进行归并
li = [8, 2, 1, 5, 9, 7, 3, 4, 6]
print('before: ', li)
merge_sort(li, 0, len(li) -1)
print('after: ', li)
"""
before: [8, 2, 1, 5, 9, 7, 3, 4, 6]
after: [1, 2, 3, 4, 5, 6, 7, 8, 9]
"""
归并排序的时间复杂度
一次归并是O(n)。递归层数是多少呢?logn层。所以,归并排序的复杂度:时间复杂度:O(nlogn)。空间复杂度:O(n),需要临时列表。
NB三人组总结
三种排序算法的时间复杂度都是O(nlogn)
⼀般情况下,就运⾏时间⽽⾔:
- 快速排序 > 归并排序 > 堆排序
三种排序算法的缺点:
快速排序:极端情况下排序效率低
归并排序:需要额外的内存开销
堆排序:在快的排序算法中相对较慢

希尔排序
希尔排序(shell sort)是一种分组插入排序算法。
思路:
- 首先,取一个整数d1=n/2,将元素分为d1个组,每组相邻两个元素之间的距离是d1,在各组内进行直接插入排序。
- 取第二个整数d2=d1/2,重复上述分组排序过程,直到di=1,即所有元素都在同一组内进行直接插入排序。
希尔排序的每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟排序使所有的数有序。
其过程如下图所示:

def insert_sort_gap(li, gap):
""" 插入排序 - 希尔版 """
for i in range(gap, len(li)): # i表示摸到的牌的下标
tmp = li[i]
j = i - gap
while j >=0 and li[j] > tmp: # while循环就是从右向左找插入的位置
li[j+gap] = li[j]
j -= gap
li[j+gap] = tmp
def shell_sort(li):
""" 希尔排序 """
d = len(li) // 2
while d >= 1:
insert_sort_gap(li, gap=d)
d = d // 2
li = [8, 2, 1, 5, 9, 7, 3, 4, 6]
print('before: ', li)
shell_sort(li)
print('after: ', li)
"""
before: [8, 2, 1, 5, 9, 7, 3, 4, 6]
after: [1, 2, 3, 4, 5, 6, 7, 8, 9]
"""
但希尔排序算法的性能不高!也就比Low B三人组高点。我们来做个对比:
li = list(range(100000))
random.shuffle(li)
li1 = copy.deepcopy(li)
li2 = copy.deepcopy(li)
li3 = copy.deepcopy(li)
insert_sort(li1)
shell_sort(li2)
heap_sort(li3)
"""
li = list(range(10000))
insert_sort running: 3.434455633163452
shell_sort running: 0.03989815711975098
heap_sort running: 0.03191423416137695
li = list(range(100000))
insert_sort running: 359.0289192199707
shell_sort running: 0.7031188011169434
heap_sort running: 0.4667818546295166
"""
-
由结果可以看到,插入排序非常慢!希尔排序好了很多,但是跟NB三人组中最慢的堆排逼着还是慢了点。
那希尔排序的时间复杂度到底是多少呢?由于d的计算方式不同,希尔排序有各种不同的时间复杂度,有兴趣的可以自行查阅维基百科。
- 最好:O(n1.3)
- 最坏:O(n2)
- 平均:O(nlogn)~O(n2)
来自:https://zhuanlan.zhihu.com/p/107402632
以上这么多的算法,都属于比较排序,如果a比b大,然后怎样怎样....通过数学可以证明,比较排序算法,最快也是O(nlogn)。
计数排序
对列表进行排序,已知列表中的数范围都在0到100之间,设计时间复杂度O(n)的算法。

import random
def count_sort(li, max_value=10):
"""
计数排序
:param li: 待排序列表
:param max_value: 待排序列表中最大的数
:return:
"""
tmp_list = [0 for _ in range(max_value + 1)] # 如果最大的数是100的话,所以要+1
for value in li:
# 每个下标存储对应值出现的次数,如下标0存储0在列表中出现的次数
tmp_list[value] += 1 # 如果对应下表是0,表示该元素没有在列表中出现过,如元素 2
# print(tmp_list) # [2, 4, 0, 2, 3, 3, 3, 1, 0, 1, 1]
li.clear() # 节省空间,再将排序结果写回到原列表
for index, value in enumerate(tmp_list):
# value记录的是index的个数,如index=0;value=2,表示0在列表中出现过2次,写2个0到li就行了
for i in range(value): # 出现几次 append 几次就完了,0次的话for循环不执行
li.append(index)
# li = [random.randint(0, 10) for _ in range(20)]
# print(li)
li = [1, 1, 4, 1, 0, 4, 1, 4, 5, 7, 3, 6, 5, 9, 3, 6, 10, 5, 0, 6]
print('before:', li)
count_sort(li, max_value=10)
print('after:', li)
"""
before: [1, 1, 4, 1, 0, 4, 1, 4, 5, 7, 3, 6, 5, 9, 3, 6, 10, 5, 0, 6]
after: [0, 0, 1, 1, 1, 1, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 9, 10]
"""
代码简单,但性能很高啊,我们来跟Python内置的算法比比啊:
import time
import random
import copy
def cal_time(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
print('{} running: {}'.format(func.__name__, time.time() - start))
return res
return wrapper
@cal_time
def list_builtin_sort(li):
""" 列表内置的sort方法 """
li.sort()
@cal_time
def count_sort(li, max_value=10):
tmp_list = [0 for _ in range(max_value + 1)]
for value in li:
tmp_list[value] += 1
li.clear()
for index, value in enumerate(tmp_list):
for i in range(value):
li.append(index)
max_value = 100
li = [random.randint(0, max_value) for _ in range(10000000)]
li1 = copy.deepcopy(li)
li2 = copy.deepcopy(li)
count_sort(li1, max_value=max_value)
list_builtin_sort(li2)
"""
# Python3.6
count_sort running: 1.4821240901947021
list_builtin_sort running: 1.8745102882385254
# Python3.9
count_sort running: 1.5681896209716797
list_builtin_sort running: 0.9401049613952637
"""
是不是很有意思?我开始使用的是Python3.6的解释器,发现我们写的计数排序算法要比Python内置的列表的sort算法性能高!可喜可贺!!你(list.sort)虽然是C写的,我用Python写的,但我的算法比你好啊,所以我快!!!但快不过三秒!我用Python3.9解释器又跑了一下代码.....mmp,我开原称你为最强......
先来看计数算法的时间复杂度是多少?count_sort中,有两个大的for循环,他们整体上都分别循环一次列表,所以都是O(n)。虽然第二个for循环还嵌套了个for循环,但这个两个for循环也就是生成了一个列表,它跟上面的的for循环没啥区别,所以,计数算法的时间复杂度是:O(n)
虽然计数排序算法的性能也还可以,但这个算法的缺点也不少:
- 需要已知列表的元素大小范围,比如我们示例中,最大的元素是10或者100,也因此限制了该算法的应用场景。
- 该算法需要额外的内存空间,也就是临时列表,如果元素的范围是一个亿呢?就需要建立一个亿这么大的临时列表....
桶排序
在计数排序中,如果元素的范围比较大(比如在1到1亿之间),如何改造算法使其更优呢?
这里来介绍计数排序的优化算法——桶排序(Bucket Sort)。
桶排序的思路是:
- 首先将元素分布在不同的桶中。
- 再对每个桶中的元素进行排序。
看图理解:

def bucket_sort(li, n=5, max_num=50):
"""
桶排序
:param li: 待排序列表
:param n: 桶的个数
:param max_num: li中元素最大值
:return:
"""
buckets = [[] for _ in range(n)] # 创建桶 [[桶1], [桶2], [桶3], [桶4], [桶5]...]
for value in li: # 取出元素添加到对应的桶内
# 关键点在于:value应该插到几号桶内,要算出来
i = min(value // (max_num // n), n - 1) # i是桶号,即value应该放到几号桶内
"""
max_num // n 每个桶存储的数的范围
一个桶放多少个数,按照当前示例来说,50 // 5 = 10,一个桶放10个数,范围: 0-9;10-19;20-29;30-39;40-49
value // (max_num // n)
从li中取出的元素,应该放到几号桶呢?
如3,3 // 10 = 0 ,即应该放到0号桶内
如43, 43 // 10 = 4,即应该放到4号桶内
min(value // (max_num // n), n-1) n=5,但列表下标从0开始,所以最后一个桶的下标是 n-1 = 4
考虑到万一有元素比max_num大怎么办?比如来个50,现在最后一个桶只存储40-49这个范围内的数,但现在value是50,怎么搞?
答案就是把它放到最后一个桶内,但具体怎么搞呢?
50 // 10 = 5 ,但现在最后一个桶的下标是4,所以用min(5, 4) = 4,所以,50应该放到4号桶内
通过一番复杂的计算之后,i的值,就是value应该存放的桶号
"""
buckets[i].append(value) # 把value放到对应的桶内
# 保持桶内元素有序:冒泡算法思想
# 从右到左进行一一对比,插入到对应的位置
# 如将 1 插入到: [0, 2, 4],先将1append到列表中[0, 2, 4, 1]
# 然后让1从右到左先跟4比,1小,交换位置,再跟2比,1小,交换,再跟0比,1大,就插入到0后面
for j in range(len(buckets[i]) - 1, 0, -1):
if buckets[i][j] < buckets[i][j - 1]: # 如果右边的比左边的小,就交换
buckets[i][j], buckets[i][j - 1] = buckets[i][j - 1], buckets[i][j]
else: # 右边的比左边的大,就插入到这里就行了
break
# 现在该将各桶中元素一一取出放到列表中了
# 法1
li.clear()
for bucket in buckets:
li.extend(bucket)
# 法2
# tmp_list = []
# for i in buckets:
# tmp_list.extend(i)
# return tmp_list
li = [29, 25, 3, 49, 9, 37, 21, 43]
print('before:', li)
bucket_sort(li, n=5, max_num=50)
print('after:', li)
"""
before: [29, 25, 3, 49, 9, 37, 21, 43]
after: [3, 9, 21, 25, 29, 37, 43, 49]
"""
来看看和计数排序算法哪个快:
max_value = 10000
max_li = 100000
bucket_num = 100
li = [random.randint(0, max_value) for _ in range(max_li)]
li1 = copy.deepcopy(li)
li2 = copy.deepcopy(li)
count_sort(li1, max_value=max_value)
bucket_sort(li2, n=bucket_num, max_num=max_value)
"""
len(li) = 100000
max_value = 10000
bucket_num=100
count_sort running: 0.01595783233642578
bucket_sort running: 8.888909578323364
"""
没错,你没看错,数的范围最大值是一万,列表长度是10万,分配100个桶,结果.......说好的桶排是计数排序算法的优化版呢?负优化吧!!!
咳咳,上面那个示例,虽然姿势正确,但一看就不是老司机。经过测试,我发现桶的数量越多,性能越高:
"""
len(li) = 100000
max_value = 10000
bucket_num = 100
count_sort running: 0.01595783233642578
bucket_sort running: 8.888909578323364
len(li) = 100000
max_value = 10000
bucket_num = 1000
count_sort running: 0.015957355499267578
bucket_sort running: 0.7689692974090576
len(li) = 100000
max_value = 10000
bucket_num = 10000
count_sort running: 0.017950773239135742
bucket_sort running: 0.06984591484069824
max_value = 10000
max_li = 1000000
bucket_num = 10000
count_sort running: 0.13862895965576172
bucket_sort running: 0.740973711013794
max_value = 10000
max_li = 10000000
bucket_num = 10000
count_sort running: 1.8435328006744385
bucket_sort running: 7.941872835159302
"""
我错了,我说错话了.....桶排序只有当桶的数量、列表长度、数的最大范围都在"合适"范围内,它比计数算法快多了!否则的话,性能反而极差。但怎么找"合适"的范围......你自己多尝试吧!我战略性放弃这个算法!本来这个算法就被人戏称"没什么人用"的算法,当然,也可能是我的姿势也不对!!!
额,还是有个点需要补充的,就是桶的数量最多只能小于等于列表元素的最大值,否则报错:
"""
代码中有一步是计算每个桶存储的数的范围的,也就是 max_num // n 的结果不能为0,因为这个值要作为被除数来计算出value应该
存放到几号桶内: value // (max_num // n),来个例子:
value = 10
max_num = 100
bucket_num分别等于99,100,101
100 // 99 = 1 --> 10 // 1 = 10
100 // 100 = 1 --> 10 // 1 = 10
100 // 101 = 0 --> 10 // 0 报错ZeroDivisionError: integer division or modulo by zero
"""
桶排序——讨论
桶排序的表现取决于数据的分布,也就是需要对不同的数据排序时采取不同的分桶策略。
桶排序的时间复杂度:
- 平均情况时间复杂度:O(n+k),k是啥?说来话长啊,k是根据n和m算出来的。那m是啥?
- n:列表的长度。
- m:桶的个数。
- k:``logm * logn`,了解即可。
- 最坏情况时间复杂度:O(n2k)
空间复杂度:O(nk),这里的k是桶的长度。
基数排序
基数排序的思路也很简单,举个例:有个员工表,根据年龄进行排序,年龄相同的根据薪资进行排序,这种多关键字的排序形式。
如,现在有列表li = [32, 13, 94, 52, 17, 54, 93],要对这个列表使用基数排序算法排序,那么套路是:
- 先比较各自的个位数,然后放到对应的桶中。
- 从桶中再依次取出根据个位数排序后的列表得到:
li = [32, 52, 13, 93, 94, 54, 17]。 - 再根据十位数值放入对应的桶中。
- 再从桶中依次取出,放入列表中,得到
li = [13, 17, 32, 52, 54, 93, 94]。 - 现在这个列表就是有序的了。
下图展示了上面的相关过程:

def radix_sort(li, bucket_num=10):
""" 基数排序 """
max_num = max(li)
k = 0 # 根据 max_num 决定要入桶的次数,如 max_num 是3位数,就要循环3次: 0 1 2
while 10 ** k <= max_num: # 10 ** 0 = 1; 10 ** 1 = 10; 10 ** 2 = 100; 10 ** 3 = 1000 ......
# 1. 创建桶
buckets = [[] for _ in range(bucket_num)]
# 2. 分桶
for value in li: # 循环取值,根据当前位数进行将数写入到对应的桶中
"""
关键点在于如何从一个数中提取对应的位数,比如 987
第一次循环应该提取7;第二次提取8;第三次提取9,这个要用什么算出来
value 10 k
(987 // 10 ** 0) % 10 = 7
(987 // 10 ** 1) % 10 = 8
(987 // 10 ** 2) % 10 = 9
"""
i = (value // 10 ** k) % 10
buckets[i].append(value)
# 3. 从桶中取数,写回到原列表,3步执行完,表示根据当前位数排序完成
li.clear()
for bucket in buckets:
li.extend(bucket)
k += 1
# print('current li:', li)
li = [32, 13, 94, 52, 17, 54, 93]
print('before:', li)
radix_sort(li)
print('after:', li)
"""
before: [32, 13, 94, 52, 17, 54, 93]
current li: [32, 52, 13, 93, 94, 54, 17]
current li: [13, 17, 32, 52, 54, 93, 94]
after: [13, 17, 32, 52, 54, 93, 94]
"""
max_num = 1000000000
max_li = 1000000
li = [random.randint(0, max_num) for _ in range(max_li)]
li1 = copy.deepcopy(li)
li2 = copy.deepcopy(li)
quick_sort(li1)
radix_sort(li2)
"""
max_num = 100
max_li = 10000
quick_sort running: 0.06781792640686035
radix_sort running: 0.00797891616821289
max_num = 1000
max_li = 100000
quick_sort running: 0.7205934524536133
radix_sort running: 0.1146547794342041
max_num = 10000 # 如果列表长度不变,但最大值一直增大,快排效率不变,但基数排序效率慢慢变差
max_li = 1000000
quick_sort running: 7.680208444595337
radix_sort running: 1.9288489818572998
max_num = 100000
max_li = 1000000
quick_sort running: 5.587107419967651
radix_sort running: 2.4923593997955322
max_num = 1000000
max_li = 1000000
quick_sort running: 4.991452217102051
radix_sort running: 3.2557990550994873
max_num = 10000000
max_li = 1000000
quick_sort running: 4.583150863647461
radix_sort running: 3.395606517791748
max_num = 1000000000
max_li = 1000000
quick_sort running: 5.269375562667847
radix_sort running: 4.496855974197388
"""
可以看到,当列表最大元素越小,基数排序要比快排快,但如果列表长度不变,最大元素却越来越大,那基数排序慢慢的性能就下来了!这是怎么回事儿呢?这里就要从二者的时间复杂度来说了。
小结
基数排序的时间复杂度:O(kn),k表示while循环的次数,也就是最大的数字位数;n表示列表的长度。再来说基数排序为啥随着k变大,性能变差。
- 首先快排的时间复杂度是O(nlogn),
logn=log(2, n);而基数O(kn),k=log(10, n),结论是以10为底的对数比以2为底的对数小。 - 但如果基数排序的k越来越大.......而快排还是nlogn不变,所以,可不就慢了么。
基数排序的空间复杂度是O(k+n)。
基数排序的使用注意事项:
- 如果k值越大,则基数排序越慢,反之性能越高。
- 基数排序需要额外的内存空间,也就是要考虑桶的空间占用。
查找排序相关面试题
给两个字符串s和t,判断t是否为s的重新排列后组成的单词
s = "anagram", t = "nagaram", return true.
s = "rat", t = "car", return false.
def isAnagram(s, t):
ss = list(s)
tt = list(t)
ss.sort()
tt.sort()
return ss == tt
s = "anagram"
t = "nagaram"
print(isAnagram(s, t))
def isAnagram(s, t):
dict1 = {}
dict2 = {}
for ss in s:
dict1[ss] = dict1.get(ss, 0) + 1
for tt in t:
dict2[tt] = dict2.get(tt,0) + 1
return dict2 == dict1
s = "anagram"
t = "nagaram"
print(isAnagram(s, t))
给定⼀个m*n的⼆维列表,查找⼀个数是否存在。列表有下列特性:
每⼀⾏的列表从左到右已经排序好。
每⼀⾏第⼀个数⽐上⼀⾏最后⼀个数⼤。

def searchMatrix(self, matrix, target):
# 法一:循环(很慢) O(n*n)
# for line in matrix:
# if target in line:
# return True
# return False
"""
0 1 2 3
4 5 6 7
8 9 10 11
"""
# 法二:因为已经有序,可用二分法
h = len(matrix)
if h == 0: # 空矩阵判断,否则matrix[0]越界
return False
w = len(matrix[0])
left = 0
right = h * w - 1
while left <= right: # 二分法
mid = (left + right) // 2
# 中间值的坐标[i,j]
i = mid // w
j = mid % w
if matrix[i][j] == target:
return True
if matrix[i][j] > target:
right = mid - 1
else:
left = mid + 1
else:
return False
给定⼀个列表和⼀个整数,设计算法找到两个数的下标,使得两个数之和为给定的整数。保证肯定仅有⼀个结果。`
例如,列表[1,2,5,4]与⽬标整数3,1+2=3,结果为(0, 1)
def twoSum(self, nums: List[int], target: int) -> List[int]:
hashmap={} # 哈希表 用来存储未找到有数相加等于target的此元素
for i,num in enumerate(nums):
if hashmap.get(target - num) is not None: # 如果在哈希表找到元素和列表元素相加等于target的元素,返回俩者对应的下标
return [i,hashmap.get(target - num)]
hashmap[num] = i # 如果没有找到则将元素存到哈希表中
数据结构
数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。
简单来说,数据结构就是设计数据以何种方式组织并存储在计算机中。
比如:列表、集合与字典等都是一种数据结构。
N.Wirth: “程序=数据结构+算法”
数据结构的分类
数据结构按照其逻辑结构可分为线性结构、树结构、图结构
线性结构:数据结构中的元素存在一对一的相互关系-树结构:数据结构中的元素存在一对多的相互关系图结构:数据结构中的元素存在多对多的相互关系
列表/数组
列表(其他语言称数组)是一种基本数据类型。
关于列表的问题:
列表中的元素是如何存储的?顺序存储,是连续存储的
列表的基本操作:按下标查找、插入元素、删除元素……
这些操作的时间复杂度是多少?
扩展:Python的列表是如何实现的?
数组
通常在介绍Python的列表的时候,都会说:Python中的列表相当于别的语言中的数组...
但实际上Python中的列表和别的语言中的数组还是有些区别的,所以,我们单独的,简单来说下数组。
在C中,关于数组:
- 定义数组时,要指定数组的长度。
- 数组中的数据类型要一致。
在内存中,这里以32位系统举例。
创建一个数组,在内存中会开辟一个块连续的内存空间。

列表
Python中的列表跟数组的区别有两点很明显的区别:
- 定义列表时,无需指定列表长度。
- 举个例子:如创建一个长度为3的列表,Python解释器会创建一个比3更长的列表,留有一定的冗余度,以便后续的添加操作。问题也有,如果一直添加,留的冗余空间也会用完,那再插入新的数据之前,Python会重新创建一个列表副本,然后再允许新的数据插入,这个新列表会预留更大的冗余空间,原列表会被垃圾回收机制回收。
- 列表中的数据类型不必一致,那这一点是怎么做到的呢?这是因为列表用的是引用语义进行存储,只存元素的地址即可详情参考Python中变量的存储关系。

列表的删除和插入都是O(n)的操作,比如删除下标为0的元素,右面的元素都会往左移动。插入也类似,其他元素都要往后移动以腾出空位置进行插入操作。
栈
栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表。
栈的特点:后进先出 LIFO(last-in, first-out)
栈的概念:栈顶、栈底
栈的基本操作:
- 进栈(压栈):push
- 出栈:pop
- 取栈顶:gettop

创建栈
栈在Python中可以用列表实现:
class Stack(object):
def __init__(self):
self.stack = []
def push(self, element):
""" 入栈 """
self.stack.append(element)
def gettop(self):
""" 查栈顶元素 """
return self.stack[-1] if self.stack else None
def pop(self):
""" 出栈 """
return self.stack.pop() if self.stack else None
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.pop()) # 3
栈的应用——括号匹配问题

class Stack(object):
def __init__(self):
self.stack = []
def push(self, element):
""" 入栈 """
self.stack.append(element)
def gettop(self):
""" 查栈顶元素 """
return self.stack[-1] if self.stack else None
def pop(self):
""" 出栈 """
return self.stack.pop() if self.stack else None
def is_empty(self):
""" 如果栈为空,返回True """
return False if self.stack else True
def bracket_match(s):
stack = Stack()
match_dict = {"}": "{", "]": "[", ")": "("}
for ch in s:
# 如果不是括号就进入下一次循环
if ch not in list(match_dict.values()) + list(match_dict.keys()):
continue
if ch in match_dict.values(): # 如果是左括号
stack.push(ch)
else: # 否则 ch 就是右括号
if stack.is_empty(): # 只有右括号,少了左括号,表示匹配失败
return False
elif stack.gettop() == match_dict[ch]: # 如果栈顶和 ch 是一对,就出栈
stack.pop()
else: # 如果栈顶和 ch 不是一对,表示匹配失败
return False
# 最后如果栈为空,表示都匹配上了,否则就有匹配失败的
return True if stack.is_empty() else False
print(bracket_match("(){}[]")) # True
print(bracket_match("([){}}")) # False
print(bracket_match("(({[{(){}()[]}]}))")) # True
print(bracket_match("(2+3)/5*(3+2)")) # True
print(bracket_match("(2+3)/5*(3+2")) # False
print(bracket_match("2+3)/5*(3+2")) # False
队列
队列(Queue)是一个数据集合,仅允许在一端进行插入,另一端进行删除。
进行插入的一端称为队尾(rear),插入动作称为进队或者入队。
进行删除的一端称为队头(front),删除动作被称为出队。
队列的性质:先进先出(First-in,First-out)。

队列的实现方式——环形队列
在上图中,演示了从一个空队列到队满的情况,这里面需要注意:
- 当队列为空时,rear等于front。
- 当队满时,观察图,发现还有留有一个空位,那为啥就说队满了呢?其原因,就是如果把那个空的位置也插入值,那rear等于front了,这个时候无法判断这个队列到底是空的还是满的,所以就留一个空位来标识队满。
- front永远指向空位。
- 还有一个重点,虽然看图是个环形,但首尾如何衔接?当插入到尾部后,下一次插入怎么就能插入到队首呢?怎么算出来的?也就是如图所示,怎么样让9+1=0?答案是取余运算:
(9 + 1) % 10 = 0。
在环形队列中,当队尾指针front等于环形队列长度减一时,再前进一个位置就自动到0。
- 队首指针前进1:front = (front+1) % maxsize
- 队尾指针前进1:rear = (rear + 1) % maxsize
- 队空条件:rear == front
- 队满条件:(rear + 1) % maxsize == front
手写一个简单队列:
class Queue(object):
def __init__(self, max_size=10):
# self.max_size = max_size + 1 # 因为队满时,有个空位,队列长度为10的话,只能存9个元素,所以,这里可以加1
self.max_size = max_size + 1
self.queue = [0 for _ in range(self.max_size)]
print(self.queue, len(self.queue))
self.rear = 0 # 队尾
self.front = 0 # 队首
def push(self, element):
""" 进队列 """
if self.is_filled(): # 如果队列已满
raise ValueError('queue is filled!')
else:
self.rear = (self.rear + 1) % self.max_size
self.queue[self.rear] = element
def pop(self):
""" 出队列 """
if self.is_empty(): # 如果队列为空
raise IndexError:("queue is empty!")
else:
self.front = (self.front + 1) % self.max_size
return self.queue[self.front] # 出队后,该位置元素没有删除,但我们不用管它,后续新的元素入队会覆盖它
def is_empty(self):
""" 判断队列是否为空 """
return self.rear == self.front
def is_filled(self):
""" 判断队列是否已满 """
return (self.rear + 1) % self.max_size == self.front
@property
def get_front(self):
""" 查看队首元素 """
if self.is_empty(): # 如果队列为空
raise IndexError:("queue is empty!")
else:
return self.queue[self.front]
@property
def get_rear(self):
""" 查看队尾元素 """
if self.is_empty(): # 如果队列为空
raise IndexError:("queue is empty!")
else:
return self.queue[self.rear]
q = Queue()
for i in range(10):
q.push(i)
print(q.get_front) # 0
print(q.get_rear) # 9
双向队列
上面说的都是单向队列,其实队列还有一种队列:双向队列,也叫做双端队列,双向队列理解起来也很简单,就是两端都支持进出。
双向队列这里可以用Python内置模块实现:
from collections import deque
# 队列的创建
q = deque() # 创建一个空队列
"""
deque接收两个可选参数
deque(iterable, maxlen)
iterable: 为队列添加初始值
maxlen: 队列的长度
"""
# 这是个单向队列
q.append(1) # 从队尾添加元素
q.popleft() # 从队头出队
print(q.popleft()) # 1
# 双向队列
q.appendleft(2) # 从队头添加元素
q.pop() # 从队尾抛出元素
注意,deque模块实现的队列有个特点,就是队满了后,再添加元素,会先从另一头抛出一个元素,然后执行添加,这点跟我们手写的队列有点不一样。
利用这个特点可以做点有意思的事儿,比如模拟实现Linux的tail -f 命令,即返回文件后几行:
from collections import deque
def tail(n):
with open('test.txt', 'r', encoding='utf-8') as f:
q = deque(f, n)
return [line.strip()for line in q]
print(tail(5))
栈和队列的应用——迷宫问题

栈——深度优先搜索

基于栈实现的思路是:从起点所在的节点开始,找下一个(上下左右节点)能走的节点,当找到不能走的节点时(遇到墙或者节点已经走过),退回上一个节点寻找其他方向的能走的节点。
- 将走过的节点入栈,并且标记该节点已经走过。
- 如果当前节点的上下左右四个方向都不能走,就退回到上一个节点继续寻找....直到栈空,表示起点到终点之间没有通路。
- 关键点:
- 上下左右四个方向的坐标点如何获取?
- 如何处理没有通路的情况?
- 怎样才算找到终点?
- 找到的通路是最优(短)的通路吗?
上代码:
maze = [
# 0 1 2 3 4 5 6 7 8 9
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 0
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 1
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 2
[1, 0, 0, 0, 0, 1, 1, 0, 0, 1], # 3
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1], # 4
[1, 0, 0, 0, 1, 0, 0, 0, 0, 1], # 5
[1, 0, 1, 0, 0, 0, 1, 0, 0, 1], # 6
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1], # 7
[1, 1, 0, 0, 0, 0, 0, 0, 0, 1], # 8
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 9
]
def maze_path_stack(start_x, start_y, end_x, end_y):
"""
栈解决迷宫问题
:param start_x: 起点的x坐标
:param start_y: 起点的y坐标
:param end_x: 终点的x坐标
:param end_y: 终点的y坐标
:return: 找到通路返回True,否则返回False
"""
# 计算当前节点的下一个节点坐标,每次找下一个节点就按照 上下左右 的顺序找,且根据上下左右的顺序不同,最终的路径也会有所不同
direction_list = [
lambda x, y: (x - 1, y), # 上
lambda x, y: (x + 1, y), # 下
lambda x, y: (x, y - 1), # 左
lambda x, y: (x, y + 1), # 右
]
# 定义一个栈
stack = []
stack.append((start_x, start_y)) # 首先将起点坐标入栈,表示从起点开始找下个节点
maze[start_x][start_y] = 2 # 将起点首先标记为已经走过
while stack: # 栈空表示没有通路
current_node = stack[-1] # 取栈顶节点
# 如果当前节点坐标等于终点坐标,就表示已经找到通路了
if current_node[0] == end_x and current_node[1] == end_y:
# 栈内的各个坐标点就是起点找终点的路径
for path in stack: # 这个时候,就直接输出各个路径就完了
print(path)
return True
for d in direction_list:
next_node = d(current_node[0], current_node[1]) # 获取下一个节点的坐标
if maze[next_node[0]][next_node[1]] == 0: # 如果下一个节点是0,表示可以走
stack.append(next_node) # 将下一个节点入栈
maze[next_node[0]][next_node[1]] = 2 # 2用于标记该节点已经走过
break
else: # 如果下个节点(四个方向都找了)不能走,就退回到上一个节点,当然,也要把当前节点标记为已经走过
maze[next_node[0]][next_node[1]] = 2 # 2用于标记该节点已经走过
stack.pop() # 当前节点出栈后,栈顶元素是上一个节点坐标
else: # 如果栈空了,表示起点和终点之间没有通路
return False
maze_path_stack(1, 1, 8, 8) # 1,1 是起点; 8,8 是终点
"""最终的路径
(1, 1)
(2, 1)
(3, 1)
(4, 1)
(5, 1)
(5, 2)
(5, 3)
(6, 3)
(6, 4)
(6, 5)
(5, 5)
(4, 5)
(4, 6)
(5, 6)
(5, 7)
(4, 7)
(3, 7)
(3, 8)
(4, 8)
(5, 8)
(6, 8)
(7, 8)
(8, 8)
"""
如果你顺着输出结果捋一遍,发现得到的路径并不是最短的路径。这跟定义的下一个节点的四个方向的先后顺序有关,也跟这个算法的性质有关,基于栈实现的思想是深度优先搜索,也就是一条路走到黑。
那么有最短路径的实现么?答案是基于队列实现。
队列——广度优先搜索

基于队列的实现思路是:从起点所在的节点开始,寻找所有接下来能继续走的点,然后不断寻找,直到直到出路。
队列存储的是当前正在考虑的节点。
from collections import deque
maze = [
# 0 1 2 3 4 5 6 7 8 9
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 0
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 1
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 2
[1, 0, 0, 0, 0, 1, 1, 0, 0, 1], # 3
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1], # 4
[1, 0, 0, 0, 1, 0, 0, 0, 0, 1], # 5
[1, 0, 1, 0, 0, 0, 1, 0, 0, 1], # 6
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1], # 7
[1, 1, 0, 0, 0, 0, 0, 0, 0, 1], # 8
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 9
]
def output(tmp_list):
""" 处理临时列表,输出起点到终点的路径 """
print(
tmp_list) # [(1, 1, None), (2, 1, 0), (1, 2, 0), (1, 1, 1), (3, 1, 1), (2, 2, 1), (4, 1, 4), (3, 2, 4), (5, 1, 6), (3, 3, 7), (6, 1, 8), (5, 2, 8), (3, 4, 9), (7, 1, 10), (5, 3, 11), (2, 4, 12), (6, 3, 14), (1, 4, 15), (2, 5, 15), (6, 4, 16), (1, 5, 17), (2, 6, 18), (6, 5, 19), (1, 6, 20), (5, 5, 22), (7, 5, 22), (4, 5, 24), (5, 6, 24), (8, 5, 25), (4, 6, 26), (5, 7, 27), (8, 4, 28), (8, 6, 28), (4, 7, 29), (6, 7, 30), (5, 8, 30), (8, 3, 31), (8, 7, 32), (3, 7, 33), (4, 8, 33), (6, 8, 34), (8, 2, 36), (8, 8, 37)]
real_path = [] # 临时列表的路径需要处理,所以,real_path就是处理后的路径
current_node = tmp_list[-1]
while current_node[2] != None: # 因为起点没有上一个节点,所以,之前我们标记为None,这里遇到None就表示可以结束循环了
real_path.append(current_node[0:2]) # 将真实的节点坐标添加到real_path中
current_node = tmp_list[current_node[2]] # 找当前节点的上一个节点
real_path.append(current_node[0:2]) # 最后将起点添加到real_path中
# real_path存储的是从重点找回起点的坐标,而我们要的是从起点到终点的坐标,所以要饭转一下
print(
real_path) # [(8, 8), (8, 7), (8, 6), (8, 5), (7, 5), (6, 5), (6, 4), (6, 3), (5, 3), (5, 2), (5, 1), (4, 1), (3, 1), (2, 1), (1, 1)]
real_path.reverse()
for node in real_path:
print(node)
def maze_path_queue(start_x, start_y, end_x, end_y):
"""
栈解决迷宫问题
:param start_x: 起点的x坐标
:param start_y: 起点的y坐标
:param end_x: 终点的x坐标
:param end_y: 终点的y坐标
:return: 找到通路返回True,否则返回False
"""
# 计算当前节点的下一个节点坐标,每次找下一个节点就按照 上下左右 的顺序找,且根据上下左右的顺序不同,最终的路径也会有所不同
direction_list = [
lambda x, y: (x - 1, y), # 上
lambda x, y: (x + 1, y), # 下
lambda x, y: (x, y - 1), # 左
lambda x, y: (x, y + 1), # 右
]
# 定义一个队列,用来存储正在走的节点
q = deque()
# 定义一个列表,用来存储已经走过的节点
tmp_list = []
# 将起点坐标首先放到队列中, 第三个参数表示当前节点来自于哪个坐标点,由于这里是起点,所以标记为None
q.append((start_x, start_y, None))
while q: # 队列不为空表示有通路
# 获取当前坐标
current_node = q.popleft() # 从右边进(append),从左边出,这是单向队列,如果q.pop(),就是从右边进从右边出,这是栈了
tmp_list.append(current_node) # 将走过的节点放到临时列表中
# 如果当前节点坐标等于终点坐标,就表示已经找到通路了
if current_node[0] == end_x and current_node[1] == end_y:
# 临时列表中的各个坐标点就是起点找终点的路径
output(tmp_list)
return True
for d in direction_list: # 循环获取下个节点的上下左右的坐标
next_node = d(current_node[0], current_node[1])
if maze[next_node[0]][next_node[1]] == 0: # 如果下一个节点是0,表示可以走
# 将可以走的节点入队列,第三个参数是next_node节点来自于哪个节点,这个节点上面我们追加进tmp_list中了
q.append((next_node[0], next_node[1], len(tmp_list) - 1))
maze[next_node[0]][next_node[1]] = 2 # 2用于标记该节点已经走过
else:
return False
maze_path_queue(1, 1, 8, 8)
"""
(1, 1)
(2, 1)
(3, 1)
(4, 1)
(5, 1)
(5, 2)
(5, 3)
(6, 3)
(6, 4)
(6, 5)
(7, 5)
(8, 5)
(8, 6)
(8, 7)
(8, 8)
"""
链表
链表又分为:
- 单向链表。
- 双向链表。
- 环形链表。
线性表
无论是列表还是链表都可以抽象地称其为线性表。一个线性表是某类元素的集合,它还记录着元素之间的顺序关系,线性表是最基本的数据结构之一,应用十分广泛,根据其存储方式不同,又分为:
- 顺序表(列表),将元素顺序的存储在一块连续的存储空间中,元素间的顺序关系由它们的存储顺序自然表示。
- 链表,各个元素分布在不同的存储空间中,它们之间的关系通过"链"连起来
链表的操作
创建链表有两种方法:
- 头插法。
- 尾插法。
顾名思义,一个从头插入,一个从尾部插入。

class Node:
def __init__(self, item):
self.item = item
self.next = None # 初始节点的next节点为None
class OperatorLinkList(object):
""" 单向链表的操作 """
def create_link_list_head(self, li):
""" 头插法,这里可以不管tail,因为tail一直不变 """
head = Node(li[0]) # 将列表下标为0的元素当作head
for element in li[1:]: # 因为下标为0的元素已经被当作头节点了,所以这里从下标1开始循环
node = Node(element) # 创建新的节点
node.next = head # 将新节点的next指向上个节点
head = node # 然后将head指向新节点
return head # 返回链表的head,可以通过head找到链表中的所有节点
def create_link_list_tail(self, li):
""" 尾插法,这里可以不管head,因为head一直不变 """
head = Node(li[0]) # 将列表下标为0的元素当作head
tail = head # 刚开始,尾巴和head都指向头节点
for element in li[1:]:
node = Node(element) # 创建新的节点
tail.next = node # 让当前节点的next指向新节点
tail = node # 让tail也指向新节点
return head # 返回链表的head,可以通过head找到链表中的所有节点
def get_link_list(self, head):
""" 遍历链表,通过head获取链表的所有节点 """
while head: # 当head.next不为None,就循环输出
print(head.item, end=' ')
head = head.next
print()
def insert_node(self, head, key, element):
""" 插入,head是链表的头部,key表示插入的位置,element插入的元素 """
p = Node(element)
while head:
if head.item == key:
p.next = head.next
head.next = p
head = head.next # 用于循环,从head挨个往后找,直到遇到None
def remove_node(self, head, element):
""" 删除节点,head是链表的头部,element是要删除的元素 """
while head:
# 如果当前元素的下一个节点值等于element,那就让当前节点next指向下一个节点的下一个节点
if head.next.item == element:
head.next = head.next.next
break
head = head.next
else:
print('删除的元素[{}]不存在'.format(element))
lk = OperatorLinkList()
h = lk.create_link_list_head([1, 2, 3]) # 头插法创建单向链表
t = lk.create_link_list_tail([1, 2, 3]) # 尾插法创建单向链表
lk.get_link_list(t)
lk.insert_node(t, 1, 4)
lk.get_link_list(h)
lk.get_link_list(t)
lk.remove_node(t, 2)
lk.get_link_list(t)
双链表


链表——复杂度分析
顺序表(列表/数组)与链表
- 按元素值查找
- 按下标查找
- 在某元素后插入
- 删除某元素
链表在插入和删除的操作上明显快于顺序表
链表的内存可以更灵活的分配
- 试利用链表重新实现栈和队列
链表这种链式存储的数据结构对树和图的结构有很大的启发性
哈希表
哈希表一个通过哈希函数来计算数据存 储位置的数据结构,通常支持如下操作:
- insert(key, value):插入键值对(key,value)
- get(key):如果存在键为key的键值对则返回其value,否则返回空值
- delete(key):删除键为key的键值对
直接寻址表

哈希
直接寻址表:key为k的元素放到k位置上
改进直接寻址表:哈希(Hashing)
- 构建大小为m的寻址表T
- key为k的元素放到h(k)位置上
- h(k)是一个函数,其将域U映射到表T[0,1,...,m-1]
哈希表

哈希冲突




用拉链法实现一个哈希表
class LinkList:
class Node:
def __init__(self, item=None):
self.item = item
self.next = None
class LinkListIterator:
def __init__(self, node):
self.node = node
def __next__(self):# 将链表做成一个迭代器
if self.node:
cur_node = self.node
self.node = cur_node.next
return cur_node.item
else:
raise StopIteration
def __iter__(self):
return self
def __init__(self, iterable=None):
self.head = None
self.tail = None
if iterable:
self.extend(iterable)
def append(self, obj):
s = LinkList.Node(obj)
if not self.head:
self.head = s
self.tail = s
else:
self.tail.next = s
self.tail = s
def extend(self, iterable):
for obj in iterable:
self.append(obj)
def find(self, obj):
for n in self:
if n == obj:
return True
else:
return False
def __iter__(self):
return self.LinkListIterator(self.head)
def __repr__(self):
return "<<"+", ".join(map(str, self))+">>"
# 类似于集合的结构
class HashTable:
def __init__(self, size=101):
self.size = size
self.T = [LinkList() for i in range(self.size)]
def h(self, k):
return k % self.size
def insert(self, k):
i = self.h(k)
if self.find(k):
print("Duplicated Insert.")
else:
self.T[i].append(k)
def find(self, k):
i = self.h(k)
return self.T[i].find(k)
ht = HashTable()
ht.insert(0)
ht.insert(1)
ht.insert(3)
ht.insert(102)
ht.insert(508)
#print(",".join(map(str, ht.T)))
print(ht.find(203))
哈希表的应用




二叉树

生成树结构
class Node:
def __init__(self,value=None,left=None,right=None):
self.value=value
self.left=left #左子树
self.right=right #右子树
if __name__=='__main__':
root=Node('E',Node('A',right=Node('C',Node('B'),Node('D'))),Node('G',right=Node('F')))
二叉树的遍历

#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Date: 2018/3/24
from collections import deque
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
def pre_order(root):
"""前序遍历"""
if root:
print(root.data, end=',')
pre_order(root.lchild)
pre_order(root.rchild)
def in_order(root):
"""中序遍历"""
if root:
in_order(root.lchild)
print(root.data, end=',')
in_order(root.rchild)
def post_order(root):
"""后序遍历"""
if root:
post_order(root.lchild)
post_order(root.rchild)
print(root.data, end=',')
def level_order(root):
"""层次遍历"""
queue = deque()
queue.append(root)
while len(queue) > 0: # 只要队不空
node = queue.popleft()
print(node.data, end=',')
if node.lchild:
queue.append(node.lchild)
if node.rchild:
queue.append(node.rchild)
二叉搜索树
二叉查找树特性:
- 1, 左子树上所有的节点的值均小于或等于他的根节点的值
- 2, 右子数上所有的节点的值均大于或等于他的根节点的值
- 3, 左右子树也一定分别为二叉排序树
import random
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None
class BST:
def __init__(self, li=None):
self.root = None
# 将列表元素转为二叉搜索树
if li:
for val in li:
self.insert_no_rec(val)
def insert(self, node, val):
"""递归版本插入"""
if not node:
node = BiTreeNode(val)
elif val < node.data:
node.lchild = self.insert(node.lchild, val)
node.lchild.parent = node
elif val > node.data:
node.rchild = self.insert(node.rchild, val)
node.rchild.parent = node
return node
def insert_no_rec(self, val):
"""非递归插入"""
p = self.root
if not p: # 空树
self.root = BiTreeNode(val)
return
while True:
if val < p.data:
if p.lchild:
p = p.lchild
else: # 左孩子不存在
p.lchild = BiTreeNode(val)
p.lchild.parent = p
return
elif val > p.data:
if p.rchild:
p = p.rchild
else:
p.rchild = BiTreeNode(val)
p.rchild.parent = p
return
else:
return
def query(self, node, val):
"""递归查询"""
if not node:
return None
if node.data < val:
return self.query(node.rchild, val)
elif node.data > val:
return self.query(node.lchild, val)
else:
return node
def query_no_rec(self, val):
"""非递归查询"""
p = self.root
while p:
if p.data < val:
p = p.rchild
elif p.data > val:
p = p.lchild
else:
return p
return None
def pre_order(self, root):
"""前序遍历"""
if root:
print(root.data, end=',')
self.pre_order(root.lchild)
self.pre_order(root.rchild)
def in_order(self, root):
"""中序遍历:在二叉搜索树可以实现排序"""
if root:
self.in_order(root.lchild)
print(root.data, end=',')
self.in_order(root.rchild)
def post_order(self, root):
"""后序遍历"""
if root:
self.post_order(root.lchild)
self.post_order(root.rchild)
print(root.data, end=',')
def __remove_node_1(self, node):
# 删除的情况1:node是叶子节点
if not node.parent:
self.root = None
if node == node.parent.lchild: #node是它父亲的左孩子
node.parent.lchild = None
else: #右孩子
node.parent.rchild = None
def __remove_node_21(self, node):
# 删除的情况2.1:node只有一个左孩子
if not node.parent: # 根节点
self.root = node.lchild
node.lchild.parent = None
elif node == node.parent.lchild:
node.parent.lchild = node.lchild
node.lchild.parent = node.parent
else:
node.parent.rchild = node.lchild
node.lchild.parent = node.parent
def __remove_node_22(self, node):
# 删除的情况2.2:node只有一个右孩子
if not node.parent:
self.root = node.rchild
elif node == node.parent.lchild:
node.parent.lchild = node.rchild
node.rchild.parent = node.parent
else:
node.parent.rchild = node.rchild
node.rchild.parent = node.parent
def delete(self, val):
"""删除节点"""
if self.root: # 不是空树
node = self.query_no_rec(val)
if not node: # 不存在
return False
if not node.lchild and not node.rchild: #1. 叶子节点
self.__remove_node_1(node)
elif not node.rchild: # 2.1 只有一个左孩子
self.__remove_node_21(node)
elif not node.lchild: # 2.2 只有一个右孩子
self.__remove_node_22(node)
else: # 3. 两个孩子都有
min_node = node.rchild
while min_node.lchild:
min_node = min_node.lchild
node.data = min_node.data
# 替换节点之后删除min_node
if min_node.rchild: # 如果有有节点
self.__remove_node_22(min_node)
else: # 只是叶子节点
self.__remove_node_1(min_node)
tree = BST([1,4,2,5,3,8,6,9,7])
tree.in_order(tree.root) # 使用中序遍历实现排序
print("")
tree.delete(4)
tree.delete(1)
tree.delete(8)
tree.in_order(tree.root)
二叉搜索树的效率

AVL树

AVL插入

# 右旋
def rotate_right(self, p, c):
s2 = c.rchild
p.lchild = s2
if s2:
s2.parent = p
c.rchild = p
p.parent = c
# 更新p、c俩个节点的平衡因子
p.bf = 0
c.bf = 0
return c

# 左旋
def rotate_left(self, p, c):
s2 = c.lchild
p.rchild = s2
if s2:
s2.parent = p
c.lchild = p
p.parent = c
# 更新p、c俩个节点的平衡因子
p.bf = 0
c.bf = 0
return c

# 右旋-左旋
def rotate_right_left(self, p, c):
g = c.lchild
s3 = g.rchild
c.lchild = s3
if s3:
s3.parent = c
g.rchild = c
c.parent = g
s2 = g.lchild
p.rchild = s2
if s2:
s2.parent = p
g.lchild = p
p.parent = g
# 更新bf
if g.bf > 0: # 插在s3上
p.bf = -1
c.bf = 0
elif g.bf < 0: # 插在s2上
p.bf = 0
c.bf = 1
else: # s1、s2、s3、s4都是空,则插入的是g
p.bf = 0
c.bf = 0
return g

# 左旋-右旋
def rotate_left_right(self, p, c):
g = c.rchild
s2 = g.lchild
c.rchild = s2
if s2:
s2.parent = c
g.lchild = c
c.parent = g
s3 = g.rchild
p.lchild = s3
if s3:
s3.parent = p
g.rchild = p
p.parent = g
# 更新bf
if g.bf < 0:
p.bf = 1
c.bf = 0
elif g.bf > 0:
p.bf = 0
c.bf = -1
else:
p.bf = 0
c.bf = 0
return g
AVL树代码:
from bst import BiTreeNode, BST
class AVLNode(BiTreeNode):
def __init__(self, data):
BiTreeNode.__init__(self, data)
self.bf = 0 # balance factor 平衡因子 (不能超过1)
class AVLTree(BST):
def __init__(self, li=None):
BST.__init__(self, li)
def rotate_left(self, p, c):
s2 = c.lchild
p.rchild = s2
if s2:
s2.parent = p
c.lchild = p
p.parent = c
# 更新p、c俩个节点的平衡因子
p.bf = 0
c.bf = 0
return c
def rotate_right(self, p, c):
s2 = c.rchild
p.lchild = s2
if s2:
s2.parent = p
c.rchild = p
p.parent = c
p.bf = 0
c.bf = 0
return c
def rotate_right_left(self, p, c):
g = c.lchild
s3 = g.rchild
c.lchild = s3
if s3:
s3.parent = c
g.rchild = c
c.parent = g
s2 = g.lchild
p.rchild = s2
if s2:
s2.parent = p
g.lchild = p
p.parent = g
# 更新bf
if g.bf > 0:
p.bf = -1
c.bf = 0
elif g.bf < 0:
p.bf = 0
c.bf = 1
else: # 插入的是g
p.bf = 0
c.bf = 0
return g
def rotate_left_right(self, p, c):
g = c.rchild
s2 = g.lchild
c.rchild = s2
if s2:
s2.parent = c
g.lchild = c
c.parent = g
s3 = g.rchild
p.lchild = s3
if s3:
s3.parent = p
g.rchild = p
p.parent = g
# 更新bf
if g.bf < 0:
p.bf = 1
c.bf = 0
elif g.bf > 0:
p.bf = 0
c.bf = -1
else:
p.bf = 0
c.bf = 0
return g
def insert_no_rec(self, val):
# 1. 和BST一样,插入
p = self.root
if not p: # 空树
self.root = AVLNode(val)
return
while True:
if val < p.data:
if p.lchild:
p = p.lchild
else: # 左孩子不存在
p.lchild = AVLNode(val)
p.lchild.parent = p
node = p.lchild # node 存储的就是插入的节点
break
elif val > p.data:
if p.rchild:
p = p.rchild
else:
p.rchild = AVLNode(val)
p.rchild.parent = p
node = p.rchild
break
else: # val == p.data
return
# 2. 更新balance factor
while node.parent: # node.parent不空
if node.parent.lchild == node: # 传递是从左子树来的,左子树更沉了
#更新node.parent的bf -= 1
if node.parent.bf < 0: # 原来node.parent.bf == -1, 更新后变成-2
# 做旋转
# 看node哪边沉
g = node.parent.parent # 为了连接旋转之后的子树
x = node.parent # 旋转前的子树的根
if node.bf > 0:
n = self.rotate_left_right(node.parent, node)
else:
n = self.rotate_right(node.parent, node)
# 记得:把n和g连起来
elif node.parent.bf > 0: # 原来node.parent.bf = 1,更新之后变成0
node.parent.bf = 0
break
else: # 原来node.parent.bf = 0,更新之后变成-1
node.parent.bf = -1
node = node.parent
continue
else: # 传递是从右子树来的,右子树更沉了
#更新node.parent.bf += 1
if node.parent.bf > 0: # 原来node.parent.bf == 1, 更新后变成2
# 做旋转
# 看node哪边沉
g = node.parent.parent # 为了连接旋转之后的子树
x = node.parent # 旋转前的子树的根
if node.bf < 0: # node.bf = 1
n = self.rotate_right_left(node.parent, node)
else: # node.bf = -1
n = self.rotate_left(node.parent, node)
# 记得连起来
elif node.parent.bf < 0: # 原来node.parent.bf = -1,更新之后变成0
node.parent.bf = 0
break
else: # 原来node.parent.bf = 0,更新之后变成1
node.parent.bf = 1
node = node.parent
continue
# 链接旋转后的子树
n.parent = g
if g: # g不是空
if x == g.lchild:
g.lchild = n
else:
g.rchild = n
break
else:
self.root = n
break
tree = AVLTree([9,8,7,6,5,4,3,2,1])
tree.pre_order(tree.root)
print("")
tree.in_order(tree.root)
二叉搜索树扩展应用——B树

算法进阶
贪心算法
贪心算法,又称贪婪算法,指的是,在对问题求解时,总是做出在当前看来是最好的选择。
也就是说,贪心算法不从整体最优进行考虑,而是只考虑当前(局部)的最优解。
贪心算法并不保证会得到最优解,但是在某些问题上,贪心算法的解就是最优解,这也就意味着,遇到相关问题时,我们要会判断解决当前问题,能否使用贪心算法来解决问题。
找零问题
假设商店老板需要找零n元钱,钱币的面额有:100、50、20、5、1元,且各个面额钱数量很多,总之能找开!
那么,问题来了:如何找零使得所需钱币的数量最少?
实现:
def greed(t, n):
"""
贪心算法实现找零问题
:param t: 不同面额的金钱列表,这个列表需要降序排序,要从最大面额的开始找
:param n: n元钱,表示要对n元钱找零
:return: 找零结果,根据传过来的值可以预期找零应该是 3张10的;1张50的,1张20的,1张5块的;一张1块的。
"""
t.sort(reverse=True) # 降序排序
m = [0 for _ in t]
for i, money in enumerate(t):
"""
思路是:
第一次循环,先用100的找
m[i] = n // money 表示 376整除100等于3,表示100的需要找三张
n %= money 找完100的,还剩多少呢?还剩76
第二次循环:
用76整除50等于1,也就是50的需要1张,零头还剩26块
第三次循环:
用26整除20等于1,表示20的也需要1张,零头还剩6块
第三次循环:
用6整除5等于1,表示5块的需要1张,零头还剩1
第四次循环:
1整除1等于1,表示1块需要1张,此时n就等于0,表示找开了,循环也结束了
"""
m[i] = n // money
n %= money
# print(m, n, money)
return m, n
t = [100, 50, 20, 5, 1]
print(greed(t, 376)) # ([3, 1, 1, 1, 1], 0) # 返回的m表示找零结果是3张10的;1张50的,1张20的,1张5块的;一张1块的,而 n 值0表示找开了
# print(greed(t, 376.5)) # ([3.0, 1.0, 1.0, 1.0, 1.0], 0.5),0.5表示省5毛钱找不开,因为我们的t中只定义到了元,没有定义角
背包问题
一个小偷在某个商店发现有n个商品,第i个商品价值vi,重wi千克,他希望拿走的价值尽量高,但他的背包最多只能容纳W千克的东西。
问题来了,他应该如何分配,才能让拿走的商品价值最高?
背包问题还可以进一步细分:
0-1背包:有n种商品,且每种商品只有1个。这也就意味着,对于一个商品,小偷要么把它完整拿走,要么留下,不能只拿走部分,或者把一个商品拿走多次,比如商品为金条。- 分数背包:对于一个商品,小偷可以拿走其中任意一部分,比如商品为金砂。
思考:对于上面两种背包来说,贪心算法是否都能得到最优解?为什么?
首先来分析分数背包。
现有(价值v;重量千克):
- 金砂:v1=120;w1=10kg
- 银砂:v1=100;w1=20kg
- 铜砂:v1=80;w1=30kg
- 背包容量:W=50kg
根据常识,我们优先拿走所有的金砂,其次是所有银砂,如果背包还有空的话,再把铜砂也拿走。所以,结果就是金砂拿走10kg;银砂拿走20kg;此时背包容量还剩20kg,所以拿走铜砂20kg,背包满了,走人....
再来分析0-1背包。
现有(价值v;重量千克),如一根金条重10kg,价值120:
- 一根金条:v1=120;w1=10kg
- 一根银条:v1=100;w1=20kg
- 一根铜条:v1=80;w1=30kg
- 背包容量:W=50kg
根据常识,我们还是优先拿金条,然后是银条,此时背包容量还剩20kg,铜条这里就装不下了,此时的价值是120+100=220。
这背包不满啊,我们再尝试其他的组合,拿银条和铜条,二者加一块正好是50kg,那价值几何?100+80=180。
还剩最后的组合,拿金条和铜条,其价值120+80=200。
这么一算,贪心算法就不一定适用了,因为该算法得到的结果不一定是最优解(背包不一定是满的)。
综合考虑,贪心算法只能解决分数背包的问题。来看下代码示例吧。
示例:
# 金砂 银砂 铜砂
goods = [(120, 10), (100, 20), (80, 30)] # 每个商品元组(价值,重量)
goods.sort(key=lambda x: x[0] / x[1], reverse=True) # 根据单价降序排序,因为要从价值最高的开始拿,尽可能的多拿,所以要将最有价值的排在首位
def fractional_backpack(goods, w):
"""
分数背包实现
:param goods: 商品列表
:param w: 背包重量
:return:
m: 存储每种商品拿多少,它是介于0~1之间数字,1表示拿走当前商品的所有,零点几表示拿走一部分
"""
m = [0 for _ in range(goods.__len__())]
total_v = 0 # 拿走商品的总价值
for i, (price, weight) in enumerate(goods):
if w >= weight: # 表示背包容量大于当前商品的重量,可以将这个商品都拿走
m[i] = 1 # 拿走所有商品
w -= weight # 背包容量要减去拿走商品的重量
total_v += price # 拿走当前商品的总价值
else: # 表示当前背包容量不足以拿走当前完整的商品,只能拿走一部分,这一部分是多少,是需要计算的
m[i] = round(w / weight, 2) # 当前背包的容量除以当前商品的重量,也就是拿走一部分
w = 0 # 拿走的这一部分商品,正好填满背包的容量,也就是背包满了
total_v += m[i] * price # 部分商品的价值
break # 背包满了,推出就行了
return total_v, m
print(fractional_backpack(goods, 50)) # (273.6, [1, 1, 0.67])
数字拼接问题
有n个非负整数,将其按照字符串拼接的方式拼接为一个整数,如何拼接可以使拼接出的整数最大?
如32, 94, 128, 1286, 6, 71,这几个值比较判断......最终得到的结果是94716321286128。
问题:字符串类型的数字如何比较大小?
比较两个字符串类型的整数大小,利用的是其在ASCII编码的位置来决定大小的。
字符串的编码比较机制就是从字符串第一位开始比较,如果相等,再比较第二位,以此类推,得出较大的字符串。
对于长度不同的字符串,前面的都相等,后面的有字符的大:
print('6' > '32') # True
print('96' > '87') # True
print('128' > '1276') # True
print('128' > '1286') # False
坑
上面的比较逻辑都没问题,但如果用于作为数字拼接问题的解决算法,就存在问题了。
首先,对于长度相等的没问题。但对于长度不相等的肯定有问题了,如下面的两组数字进行拼接:
a, b = "128" "1286" # b>a拼接结果如下:
b + a = "1286128" # 拼接结果要比a+b大
a + b = "1281286"
# 上面这种情况没问题
# 但下面这种情况就有问题了
c, d = "728" "7286" # d>c拼接结果
d + c = "7286728" # d>c的拼接结果要比c>d大
c + d = "7287286"
对于上面存在的问题,怎么解决呢?也好办,我们将两组字符串进行拼接后比较:
# before
c, d = '728', '7286'
r = c + d if c > d else d + c
print(r) # 7286728
# after
c, d = '728', '7286'
r = c + d if c + d > d + c else d + c
print(r) # 7287286
按照优化后的思路实现代码:
from functools import cmp_to_key
def my_cmp(x, y):
""" 对比两个值返回对比结果 """
if x + y < y + x:
return 1
elif x + y > y + x:
return -1
else: # 相等
return 0
def number_join(li):
"""
数字拼问题
:param li: 带拼接的数字列表
:return: 拼接后的字符串
"""
# 首先,将列表中的整数都转换为字符串,方便后后续比较
li = list(map(str, li))
# 然后对列表进行降序排序,将最大的放到最前面
# 这一步关键就是根据值的大小进行交换,如: ["32", "94"] --> ["94", "32"]
li.sort(key=cmp_to_key(my_cmp))
# 上面一步执行完,得到的降序列表就是待拼接前的列表了
# print(li) # ['94', '71', '6', '32', '1286', '128']
# return ''.join(li) # 94716321286128
return ','.join(li) # 为了便于观察,这里以逗号分进行分割
li = [32, 94, 128, 1286, 6, 71]
print(number_join(li)) # 94,71,6,32,1286,128
上面使用的是内置排序算法,你也可以使用其他的排序算法来完成排序。
参考:
活动选择问题
假设有n个活动要举行,这些活动要占用同一片场地,但场地在同一时刻只能举办一个活动。
每个活动都有一个开始时间si和结束时间fi(这里的时间以整数表示)。
问题:如下图,有11场活动,如何安排才能使这个场地能够举办的活动最多?
贪心结论:最先结束的活动一定是最优解的一部分。
证明:假设活动a是所有活动中最先结束的活动,活动b是最优解中最先结束的活动。
- 如果
a=b,结论成立。 - 如果
a!=b,则活动b的结束时间一定晚于活动a的结束时间,此时用活动a替换掉最优解中的活动b,活动a一定不与最优解中的其他活动时间重叠,因此替换后的活动a也是最优解。
代码示例:
activities = [
(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)
] # 元组(活动开始时间,活动结束时间)
def activity_selection(a):
"""
活动选择问题
:param a: 所有活动列表
:return m: 选择出的活动列表
"""
# 首先根据最先结束的活动进行降序排序活动列表
a.sort(key=lambda x: x[1])
m = [a[0]] # 将最先结束的活动先放到m中
for i in range(1, len(a)): # 最先结束的活动放到了m中,这里直接从第二个开始循环,挨着看每个活动跟m中的最后一个活动是否冲突
# 在循环中判断,当前活动和m中的最后一个活动进行时间对比
# 如果当前活动的开始时间小于m中的最后一个活动的结束时间,表示冲突了,这个当前活动就被抛弃掉
# 如果当前活动的开始时间大于等于m中的最后一个活动的结束时间,表示不冲突,可以将这个活动添加到m中
# 也允许这种活动:8-10点,10-12点,即允许在结束的时间点,开始下一场活动
if a[i][0] >= m[-1][1]:
m.append(a[i])
return m
print(activity_selection(activities)) # [(1, 4), (5, 7), (8, 11), (12, 16)]
动态规划
从斐波那契数列看动态规划
斐波那契数列: F**n = F**n−1 + F**n−2
练习:使⽤递归和⾮递归的⽅法来求解斐波那契数列的第n项
# 子问题的重复计算
def fibnacci(n):
if n == 1 or n == 2:
return 1
else:
return fibnacci(n-1) + fibnacci(n-2)
"""
f(5) = f(4)+f(3)
f(4) = f(3)+f(2)
f(3) = f(2)+f(1)
f(3) = f(2)+f(1) 递归会有很多重复计算
"""
# 动态规划(DP)的思想 = 递推式 + 重复子问题
def fibnacci_no_recurision(n):
f = [0,1,1]
if n > 2:
for i in range(n-2):
num = f[-1] + f[-2]
f.append(num)
return f[n]
print(fibnacci_no_recurision(100))
钢条切割问题
- 问题
某公司出售钢条,出售价格与钢条长度之间的关系如下表:
钢条切割问题:现有一段长度为n的钢条和上面的价格表,求切割钢条方案,使得总收益最大。
- 思路
思考: 长度为n的钢条的不同切割方案有几种?
有 2 n − 1 2^{n-1} 2n−1种,因为有 n − 1 n-1 n−1个可以切割的地方,每个位置都有切与不切两种选择,所以是 2 n − 1 2^{n-1} 2n−1种,但是这种方法不太合适,因为如果n太大的时候,切割方案会指数爆炸,效率不高。
2.1 最优子结构
昨天有讲动态规划,动态规划(DP)的思想 = 最优子结构(递推式) + 重复子问题,那么我们可以看看这道题目的最优子结构:
可以将求解规模为n的原问题,划分为规模更小的子问题:完成一次切割后,可以将产生的两段钢条看成两个独立的钢条切个问题。
组合两个子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大的,构成原问题的最优解。
钢条切割满足最优子结构:问题的最优解由相关子问题的最优解组合而成,这些子问题可以独立求解。
最优子结构(大的问题切割为小的子问题,如果子问题有最优解并且这些最优解解能算大问题的解,即最优子结构)
2.2 递推式
我们再来看看这道问题的递推式:
设长度为n的钢条切割后最优收益值为rn,可以得出递推式:
r n = m a x ( p n , r 1 + r n − 1 , r 2 + r n − 2 , . . . , r n − 1 十 r 1 ) r_n=max(p_n,r_1 +r_{n-1},r_2+r_{n-2},...,r_{n-1}十r_1) rn=max(pn,r1+rn−1,r2+rn−2,...,rn−1十r1)
第一个参数 p n p_n pn表示不切割,其他 n − 1 n-1 n−1个参数分别表示另外 n − 1 n-1 n−1种不同切割方案,对方案 i = 1 , 2... n − 1 i=1,2...n-1 i=1,2...n−1,将钢条切割为长度为 i i i和 n − i n-i n−i两段,方案i的收益为切割两段的最优收益之和,考察所有的 i i i,选择其中收益最大的方案!
- 代码
从递推式,我们想到可以用递归求解!有结束条件,又是调用自身
# 价格列表,下标即为长度
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
def cut_rod_recurision_1(p, n):
if n == 0:
return 0
else:
res = p[n]
for i in range(1, n):
res = max(res, cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n - i))
# 递归2次,所以慢
return res
print(cut_rod_recurision_1(p, 10))
结果为: 30,即当钢条长度n=10时,最大总收益为30!
- 思路改进
昨天的博客里说到了,当n变大时,递归的效率其实也不高,那么有什么方法可以进行改进呢?我们想,上面的递推式,我们取的是 m a x ( r i + r n − i ) max(r_i +r_{n-i}) max(ri+rn−i),即算当i确定时,就要进行两次递归,那么是不是可以减少递归的次数呢?
这里我们就进行改进:
从钢条的左边切割下长度为i的一段,只对右边剩下的一段继续进行切割,左边的不再切割
递推式简化为 r n = max 1 < j ≤ m ( p i + r n − i ) r_n=\max\limits_{1<j\leq m}(p_i+r_{n-i}) rn=1<j≤mmax(pi+rn−i)
不做切割的方案就可以描述为:左边一段长度为n,收益为 p n p_n pn,剩余一段长度为0,收益为 r 0 r_0 r0=0。
这样对于每一个i,我们就减少了递归的次数,增加了效率!
- 代码改进
# 简化后的算法
def cut_rod_recurision_2(p, n):
if n == 0:
return 0
else:
res = 0
for i in range(1, n+1):
res = max(res, p[i] + cut_rod_recurision_2(p, n-i)) # 递归一次,所以快
return res
- 思路再改进
其实我们思路改进后,还是用到了递归,只不过是减少了递归的使用次数而已,所以当n过大时,算法还是无法承受,效率不高。我们之前的两种方法,改进前以及改进后,都用到了递归,递推式分别为:
r n = m a x ( p n , r 1 + r n − 1 , r 2 + r n − 2 , . . . , r n − 1 十 r 1 ) r_n=max(p_n,r_1 +r_{n-1},r_2+r_{n-2},...,r_{n-1}十r_1) rn=max(pn,r1+rn−1,r2+rn−2,...,rn−1十r1)
r n = max 1 < j ≤ m ( p i + r n − i ) r_n=\max\limits_{1<j\leq m}(p_i+r_{n-i}) rn=1<j≤mmax(pi+rn−i)
可以发现,这两种方法都是自上而下的切割方法。什么叫做自上而下的切割方法呢?就是把长度为n的进行切割为两段,再分别计算两段的最优价值,就这样,一步一步切割,一步一步计算,最后到不能切割为止,得到结果!时间复杂度为 O ( 2 n ) O(2^n) O(2n)!
那这里,我们就提出了 动态规划(DP)的思路 :
自底向上的方法: 从短到长,把长度从0~n的钢条最优价格求出来,因为每次求出长度后都用列表储存,所以计算后面的长度时,直接从列表里取出相应切割方案对应的价值,相加即可,不需要递归,大大增加了效率!
- 代码再改进
def cut_rod_dp(p, n):
r = [0]
for i in range(1, n+1): # 下标i对应的数字就是长度为n=i的钢条的最优价格,所以i从1开始,把长度从1~n的钢条最优价格求出来
res = 0
for j in range(1, i+1): # 求当n确定时,利用递推式求出此时的最优价格
res = max(res, p[j] + r[i - j])
r.append(res)
return r[n]
思路很简单,代码也很简单!
当然,有了最大价值,我们还需要知道切割方案:
# 重构解
def cut_rod_extend(p, n):
r = [0]
s = [0]
for i in range(1, n+1): # 下标i对应的数字就是长度为n=i的钢条的最优价格,所以i从1开始
res_r = 0 # 记录价格的最大值
res_s = 0 # 记录价格最大值对应方案的左边不切割部分的长度
for j in range(1, i+1):
if p[j] + r[i - j] > res_r:
res_r = p[j] + r[i - j]
res_s = j
r.append(res_r)
s.append(res_s)
return r[n], s
# 获取切割方案
def cut_rod_solution(p, n):
r, s = cut_rod_extend(p, n)
ans = []
while n > 0:
ans.append(s[n])
n -= s[n]
return ans
所有代码为:
# 自底向上的方法
def cut_rod_dp(p, n):
r = [0]
for i in range(1, n+1): # 下标i对应的数字就是长度为n=i的钢条的最优价格,所以i从1开始,把长度从1~n的钢条最优价格求出来
res = 0
for j in range(1, i+1): # 求当n确定时,利用递推式求出此时的最优价格
res = max(res, p[j] + r[i - j])
r.append(res)
return r[n]
# print(c2(p, 20))
# print(cut_rod_dp(p, 20))
# 重构解
def cut_rod_extend(p, n):
r = [0]
s = [0]
for i in range(1, n+1): # 下标i对应的数字就是长度为n=i的钢条的最优价格,所以i从1开始
res_r = 0 # 记录价格的最大值
res_s = 0 # 记录价格最大值对应方案的左边不切割部分的长度
for j in range(1, i+1):
if p[j] + r[i - j] > res_r:
res_r = p[j] + r[i - j]
res_s = j
r.append(res_r)
s.append(res_s)
return r[n], s
# 获取切割方案
def cut_rod_solution(p, n):
r, s = cut_rod_extend(p, n)
ans = []
while n > 0:
ans.append(s[n])
n -= s[n]
return ans
r, s = cut_rod_extend(p, 20)
print(s)
print(cut_rod_dp(p, 20))
结果为:
[0, 1, 2, 3, 2, 2, 6, 1, 2, 3, 10] # 切割方案
30 # 最大价值
钢条切割问题——自底向上递归实现
时间复杂度O(n^2)
- 总结
钢条切割问题——动态规划解法
递归算法由于重复求解相同子问题,效率极低
动态规划的思想:
每个子问题只求解一次,保存求解结果
之后需要此问题时,只需查找保存的结果
动态规划问题关键特征
什么问题可以使用动态规划方法?
最优问题
最优子结构
原问题的最优解中涉及多少个子问题
在确定最优解使用哪些子问题时,需要考虑多少种选择
重叠子问题
最⻓公共子序列
⼀个序列的⼦序列是在该序列中删去若⼲元素后得 到的序列。
- 例:“ABCD”和“BDF”都是“ABCDEFG”的⼦序列
最⻓公共⼦序列(LCS)问题:给定两个序列X和Y,求X和Y⻓度最⼤的公共⼦序列。
- 例:X="ABBCBDE" Y="DBBCDB" LCS(X,Y)="BBCD"
应⽤场景:字符串相似度⽐对
思考:暴⼒穷举法的时间复杂度是多少?
- O(2^(m+n))

例如:要求a="ABCBDAB"与b="BDCABA"的LCS:
- 由于最后⼀位"B"≠"A":
- 因此LCS(a,b)应该来源于LCS(a[:-1],b)与LCS(a,b[:-1])中更⼤的那⼀个
具体算法如下图:

坐标[7,6]是最长公共子序列的个数,代码输出:
def lcs_length(x, y):
m = len(x)
n = len(y)
c = [[0 for _ in range(n+1)] for _ in range(m+1)] # 构建矩阵
for i in range(1, m+1):
for j in range(1, n+1):
if x[i-1] == y[j-1]: # i j 位置上的字符匹配的时候,来自于左上方+1
c[i][j] = c[i-1][j-1] + 1
else:
c[i][j] = max(c[i-1][j], c[i][j-1])
return c[m][n]
记录回溯位置: 1 左上方 2 上方 3 左方
def lcs(x, y):
m = len(x)
n = len(y)
c = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
b = [[0 for _ in range(n + 1)] for _ in range(m + 1)] # 记录回溯位置: 1 左上方 2 上方 3 左方
for i in range(1, m+1):
for j in range(1, n+1):
if x[i-1] == y[j-1]: # i j 位置上的字符匹配的时候,来自于左上方+1
c[i][j] = c[i-1][j-1] + 1
b[i][j] = 1
elif c[i-1][j] > c[i][j-1]: # 来自于上方
c[i][j] = c[i-1][j]
b[i][j] = 2
else:
c[i][j] = c[i][j-1]
b[i][j] = 3
return c[m][n], b
def lcs_trackback(x, y):
"""回溯"""
c, b = lcs(x, y)
i = len(x)
j = len(y)
res = []
while i > 0 and j > 0:
if b[i][j] == 1: # 来自左上方=>匹配
res.append(x[i-1])
i -= 1
j -= 1
elif b[i][j] == 2: # 来自于上方=>不匹配
i -= 1
else: # ==3 来自于左方=>不匹配
j -= 1
return "".join(reversed(res))
print(lcs_trackback("ABCBDAB", "BDCABA"))
应用:利⽤欧⼏⾥得算法实现⼀个分数类,⽀持分数的四则运算。
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Date: 2018/9/9
class Fraction:
def __init__(self, a, b):
self.a = a
self.b = b
x = self.gcd(a,b)
self.a /= x
self.b /= x
def gcd(self, a, b):
"""计算最大公约数"""
while b > 0:
r = a % b
a = b
b = r
return a
def zgs(self, a, b):
"""计算最小公倍数"""
# 12 16 -> 4
# 3*4*4=48
x = self.gcd(a, b)
return a * b / x
def __add__(self, other):
"""计算分数的加法"""
a = self.a
b = self.b
c = other.a
d = other.b
fenmu = self.zgs(b, d)
fenzi = a * fenmu / b + c * fenmu / d
return Fraction(fenzi, fenmu)
def __str__(self):
"""求最简分数"""
return "%d/%d" % (self.a, self.b)
a = Fraction(1,3)
b = Fraction(1,2)
print(a+b) # 5/6
RSA加密算法简介
密码与加密
传统密码:加密算法是秘密的
现代密码系统:加密算法是公开的,密钥是秘密的
-
对称加密
-
⾮对称加密

RSA加密算法过程
-
随机选取两个质数p和q
-
计算n=pq
-
选取⼀个与φ(n)互质的⼩奇数e,φ(n)=(p-1)(q-1)
-
对模φ(n),计算e的乘法逆元d,即满⾜ (e*d) mod φ(n) = 1
-
公钥(e, n) 私钥(d, n)
加密过程:c = (m^e) mod n
解密过程:m = (c^d) mod n



浙公网安备 33010602011771号