数据结构与算法(python)(算法部分)
文章目录
一、算法入门
1.时间、空间复杂度
时间复杂度:T(n)=O(f(n)) 估计算法运行时间的式子(单位)用最大数量级
f(n)基础操作重复执行的次数 O(1) O(n) O(log n) {O(1)、O(n2+n)}表述不正确
快速判断:
确定问题规模n
循环减半: logn
k层关于n的循环 nk
按效率排序 O(1)<O(log n)<O(n)<O(nlog n)<O(n2)<O(n2log n) <O(N3)
复杂时间复杂度 O(n!) O(2n) O(nn)
空间复杂度:S(n)=O(f(n)) 评估算法内存占用大小
使用了几个变量: O(1)
使用了长度为n的一位列表:O(n)
使用m行n列的二维列表:O(mn)
原则:"空间换时间 "
2.递归
复习:递归
特点:调用自身、结束条件。
def fun3(x):
if x>0:
print(x)
fun3(x-1)
# 输入x=3 输出 :3 2 1
#
def fun3(x):
if x>0:
fun3(x-1)
print(x)
#输入 x=3 输出 1 2 3 先执行调用函数的操作,函数无法执行时执行打印 从内向外打印
fun3(3)
递归实例:汉诺塔问题:三根柱子ABC,在一根上由上到下按从小到大放64个圆盘,按大小顺序重新摆放到另一个柱子上。
# 汉诺塔问题:
# n=2时:
# 1.小圆盘A到B
# 2.大圆盘A到C
# 3.小圆盘B到C
# n个盘子 下面一个看成一个整体,上面n-1个看成一个整体
# 1.n-1个圆盘A经过C到B(比原规模小1的原问题)
# 2.第n个圆盘A到C(一步)
# 3.n-1个圆盘B经过A到C(比原规模小1的原问题)
#递推式:h(x)=2h(x-1)+1 大约2^n
#h(64)=18446744073709551615 每秒移动一个,要5800亿年
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(2,'A','B','C')
二、查找问题
查找:在一些数据元素中,通过一定的方法找出与给定关键词相同的数据元素的过程。
列表查找(线性表查找):从列表中查找指定元素
输入:列表、待查找元素
输出:元素下标
内置列表查找函数:index() 就是线性查找
1.顺序查找
顺序查找:也叫线性查找,从列表第一个元素开始,顺序进行搜索,直到找到元素或搜索到列表最后一个元素为止。
时间复杂度o(n)
#顺序查找
def linear_search(li,val):
for ind,v in enumerate(li):
if v==val:
return ind
else:
return None
2.二分查找
二分查找:又叫折半查找,从有序列表的初始候选区 li[0:n]开始,通过对待查找的与候选区中间值的比较,可以使候选区减少一半。
时间复杂度o(log n) 但是需要先排序
#二分查找: 1-9 中查找元素3
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=[1,2,3,4,5,6,7,8,9]
print(binary_search(li,3))
三、列表排序
排序:将一组 无序 的记录序列调整为有序的记录序列
列表排序:将无序列表变为有序列表
输入:列表
输出:有序列表
升序与降序
内置排序函数:sort() 基于归并排序
常见算法排序:
冒泡排序、选择排序、插入排序
快速排序、堆排序、归并排序
希尔排序、计数排序、基数排序
1.冒泡排序
列表每两个相邻的数,如果前面的比后面的大,则交换这两个数
一趟排序完成后,无序区减少一个数,有序区增加一个数
代码关键点:趟、无序区
时间复杂度O( n2)
#冒泡排序
import random
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=Ture
if not exchange:#如果没有发生交换则表示已经排好序
return
li=[random.randint(0,100) for i in range(10)]
print(li)
bubble_sort(li)
print(li)
2.选择排序
一趟排序记录最小的数,放到第一个位置
在一趟记录列表无序区最小的数,放到第二个位置
关键点:有序区和无序区、无序区最小数的位置
新建一个列表,多一倍内存
时间复杂度O(n2) min和remove操作都需要一次遍历
#选择排序
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
li=[9,6,3,5,2,1,4,8,7]
print(select_sort_simple(li))
时间复杂度O(n2)
#改进选择排序
def select_sort(li):
for i in range(len(li)-1):
min_loc = i
for j in range(i,len(li)):
if li[j]<li[min_loc]:
min_loc = j
li[i], li[min_loc]=li[min_loc], li[i]
li=[9,6,3,5,2,1,4,8,7]
select_sort(li)
print(li)
3.插入排序
初始有序区只有一个元素,每次从无序区取一个元素插入到有序区的正确位置
时间复杂度O(n2)
#插入排序
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
return li
li=[9,6,3,5,2,1,4,8,7]
print(insert_sort(li))
4.快速排序
思路:取一个元素p,使元素p归位
列表被分成两个部分,左边都比p小,右边都比p大
递归完成排序
时间复杂度O(nlogn)
问题 :递归
最坏情况:倒序 需要 n2
#快速排序
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)
while left<right and li[left]<=tmp:
left+=1
li[right]=li[left] #把左边的值写到右边的空位
# print(li)
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)
li=[5, 3, 2, 6, 9, 7, 8, 1, 4]
print(li)
quick_sort (li, 0, len(li)-1)
print(li)
5.堆排序(补充树部分的内容)
堆排序前传:树与二叉树
树是一种数据结构,可以递归定义的数据结构
是由n个节点组成的集合:
如果n=0,则是空树
n>0,存在一个节点为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树
基本概念:根节点、叶子节点 ;树的深度(高度);树的度(有几个分叉就几个度,树的度是最多的度);
孩子节点/父节点;子树
二叉树:
完全二叉树:叶节点只能出现在最下面一层和次下层,并且最下面一层的节点都集中在该层的若干位置的二叉树。
深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
满二叉树:一个二叉树每一层的节点数都达到最大值
二叉树的存储方式:链式存储方式(后面讲数据结构时候补充) 顺序存储方式
顺序存储:父节点和左孩子 :i 到2i+1 父节点和右孩子:i到2i+2 孩子找父节点 (i-1)/2
堆:一种特殊的完全二叉树结构
大根堆:一个完全二叉树,满足任一节点都比其他孩子节点大
小根堆:一个完全二叉树,满足任一节点都比其他孩子节点小
堆的向下调整:
假设节点的左右子树都是堆,但自身不是堆,可以通过一次向下调整来将其变换成一个堆。
堆排序过程:
1.建立堆
2.得到堆顶元素为最大元素
3.去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序
4.堆顶元素为第二大元素
5.重复步骤3,直到堆变空
构造堆:
先看最后一个非叶子节点
#堆向下调整实现
def sift(li,low, high): #li:列表 low:堆的根节点位置 high:堆的最后一个元素位置
i = low #堆顶
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放到叶子节点上
堆排序:
时间复杂度O(nlogn)
时间复杂度与快排一样,实际应用快排更快一些
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
li=[i for i in range(100)]
import random
random.shuffle(li)
print(li)
heap_sort(li)
print(li)
python堆的内置模块:
import heapq
import heapq #q代表优先队列
import random
li = list(range(100))
random.shuffle(li)
print(li)
heapq.heapify(li) #建堆
heapq.heappop(li) #弹出最小元素
print(li)
堆排序:topk问题(现有n个数,设计算法得到前k大的数(k<n))
解决思路: 排序后切片O(nlogn) 冒泡排序、插入排序、选择排序 O(kn)
堆排序O(nlogk)
def sift(li,low, high): #li:列表 low:堆的根节点位置 high:堆的最后一个元素位置
i = low #堆顶
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 topk(li,k):
heap = li[0:k]
for i in range((k-2)//2,-1,-1):
sift(heap, i, k-1)
#1.建堆
for i in range(k,len(li)):
if li[i]>heap[0]:
heap[0]=li[i]
sift(heap,0, k-1)
#2.遍历
for i in range(k-1, -1, -1):
heap[0],heap[i] = heap[i], heap[0]
sift(heap,0, i-1)
#3.出数
return heap
import random
li = list(range(100))
random.shuffle(li)
print(topk(li,10))
6.归并排序
假设列表的两段有序,将其合成一个有序列表,称为一次归并。
分解:将列表越分越小,直至分成一个元素。
终止条件:一个元素是有序的。
合并:将两个有序列表合并,列表越来越大。
时间复杂度O(nlogn)
空间复杂度O(n)
def merge_sort(li,low,high):
if low < high:#至少有两个元素,递归
mid = (loe+high)//2
merge_sort(li,low,high)
merge_sort(li,mid+1,high)
merge(li,low,mid,high)
def merge(li,low,mid,high):
i = low
j = mid+1
ltmp=[]
while i<=mid and j<=high: #只要两边都有数
if li[i]<li[j]:
ltmp.append(li[i])
i+=1
else:
ltmp.append(li[j])
j+=1
print(li)
#while执行完,肯定有一部分没数了
while i<=mid:
ltmp.append(li[i])
i+=1
while j<=high:
ltmp.append(li[j])
j+=1
li[low:high+1]=ltmp
上述三种排序小结
时间复杂度都是O(nlogn)
运行时间:快速排序<归并排序<堆排序
三种算法缺点:
快速排序:极端情况下效率低
归并排序:需要额外消耗内存
堆排序:在快的排序算法中相对较慢
稳定度:一样的元素位置不变的稳定
7.希尔排序
希尔排序:一种分组插入排序算法;每趟并不是使某些元素有序,而是使整体越来越接近有序;最后一趟使得所有数据有序。
首先取一个整数 d1=n/2,将元素分为d1个组,每组相邻两元素之间的距离为d1,在各组内进行直接插入排序;
取第二个整数d2=的/2,重复上述分组排序过程,知道di=1,即所有元素在同一组进行直接插入排序。
#希尔排序
def insert_sort_gap(li,gap):
for i in range(gap,len(li)):#i表示摸到的牌下标
tmp = li[i]
j=i-gap #j指的是手里牌的下标
while j>=0 and li[j]>tmp:
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,d)
d//=2
return li
li=[9,6,3,5,2,1,4,8,7]
print(shell_sort(li))
希尔排序时间复杂度要论比较复杂,和选取的gap序列有关。
当使用 Hibbard 增量(hk=2k-1)序列时,时间复杂度为 O(n3/2)
当使用 Sedgewick 增量(hk=4k+3*2k-1+1和hk=4k-1-92k-1+1交替使用;1,5,19,41,109,209)序列时,时间复杂度为 O(n4/3)
在最佳的增量序列下,最坏情况下的时间复杂度可以达到O(nlog2n)
希尔排序在序列基本有序的情况下可以接近O(nlogn)
8.计数排序
对列表进行排序,已知列表中的数范围都在0到100之间,设计时间复杂度为O(n)的算法
#计数排序
def count_sort(li,max_count=100):
count=[0 for _ in range(max_count+1)]
for val in li:
count[val]+=1
li.clear()
for ind, val in enumerate(count):
for i in range(val):
li.append(ind)
import random
li=[random.randint(0,100) for _ in range(1000)]
print(li)
count_sort(li)
print(li)
9.桶排序
在计数排序中,如果元素的范围比较大(比如在1到1亿之间),如何改造算法?
桶排序:首先将元素分在不同的同种,在对每个桶中的元素排序。
桶排序的表现取决于数据的分布,也就是需要对不同数据排序时采取不同的分桶策略
平均情况时间复杂度O(n+k) n是输入数据的大小 k是桶的数量
最坏情况时间复杂度O(n2k) 所有数据都在一个桶中
空间复杂度O(n+k)
#桶排序
def bucket_sort(li, n=100, max_num=1000):
buckets=[[] for _ in range(n)] #创建桶
for var in li:
i = min(var//(max_num//n) , n-1) #i 表示var放到几号桶
buckets[i].append(var) #把var加到桶里边
#保持桶内的顺序
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
sorted_li = []
for buc in buckets:
sorted_li.extend(buc) #extend把一个列表加在后边
return sorted_li
import random
li=[random.randint(0,1000) for i in range(100)]
print(li)
print(bucket_sort(li))
10.基数排序
多关键字排序:假如现在有一个员工表,先按照年龄排序,再按照薪资进行稳定的排序
时间复杂度O(nk) n输入数据的大小, k为位数也就是桶个数
空间复杂度O(n+k)
def radix_sort(li):
max_num= max(li) #取最大位数
it= 0
while 10**it<=max_num: # ** 次方
buckets = [[] for _ in range(10)]
for var in li:
#取一位数 987%10 it=1 987//10%10 it=2 987//100%10 it=3
diqit=(var//10**it)%10
buckets[diqit].append(var)
#分桶完成
li.clear()
for buc in buckets:
li.extend(buc)
#把数重新写回i
it+=1
import random
li=[random.randint(0,1000) for i in range(100)]
print(li)
radix_sort(li)
print(li)
总结
算法部分的内容总结,主要包括判断算法时间、空间复杂度,简单的递归,查找算法和排序算法。