算法入门
课程第一部分 排序
程序等于数据结构+算法
复杂度
时间复杂度
时间复杂度排序
O(1) < O(logn)<O(n)<O(nlogn)<O(n2)<O(n2logn)<O(n3)
快速判定算法复杂度(绝大多数情况):
- 确定问题规模n
- 循环减半过程 -> logn
- k层关于n的循环->nk
复杂情况:根据算法执行过程判端
空间复杂度
空间复杂度: 用来评估算法内存占用大小的式子
递归
递归的两个特点
- 调用自身
- 结束条件
汉诺塔问题
n个盘子时:
- 把个盘子从A经过C移动到B
- 把第n个盘子从A移动到C
- 把n-1盘子从B经过A移动到C
def hanoi(n,a,b,c):
if n > 0:
hanoi(n-1,a,c,b)
print("moving form %s to %s"%(a,c))
hanoi(n-1,b,a,c)
hanoi(2,"A","B","C")
查找问题
顺序查找
def linear_search(li,val):
for ind,v in enumerate(li):
if v== val:
return ind
else:
return None
二分查找
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:
right = mid - 1
elif li(mid) < val:
left = mid + 1
return None
列表排序
冒泡排序
- 列表每两个相邻的数,如果前面比后面大,则交换这两个数
- 一趟排序完成后,则无序区减少一个数,有序区增加一个数
关键点: 趟 无序区数量
def bubble_sort(li):
for i in range(len(li)- 1):
exchange = False
for j in range(len(li) - 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
复杂度 O(n2)
选择排序
- 一趟排序记录最小的数,放在第一个位置
- 再一趟排序记录列表无序区最小的数,放到第二个位置
关键点:有序区与无序区最小值的位置
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):
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]
return li
复杂度 O(n2)
插入排序
- 初始时手里(有序区)只有一张牌
- 每次(从无序区)摸一张牌,插入到手里已有牌的正确位置
def insert_sort(li):
for i in range(1,len(li)):
tmp = li[i]
j= i -1
while li[j] > tmp and j >= 0:
li[j+1] = li[j]
j -= 1
li[j+1] = tmp
复杂度 O(n2)
快速排序
- 取一个元素p,使元素p归位
- 列表被p分成两部分,左边都比p小,右边都比p大
- 递归完成排序
def partition(li,left,right):
tmp = li[left]
while left<right:
while left < right and li[right] >= tmp:
right -= 1
li[left] = li[right]
while left < right and li[left] <= tmp:
left += 1
li[right] = li[left]
li[left] = 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)
复杂度O(nlogn)
堆排序
树与二叉树
树是一种可以递归定义的数据结构 树是由n个节点组成的集合:
- 如果n=0 ,那么这是一棵空树
- 如果n>0 ,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树 树的一些概念:
- 根节点,叶子节点
- 树的深度(高度)
- 树的度
- 孩子节点/父节点
- 子树
二叉树:度不超过2的树
满二叉树:层节点达到最大值 完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树.
二叉树的存储方式
- 链式存储方式
- 顺序存储方式
二叉树的顺序存储方式:
-
父节点和左孩子节点变化下表的关系
i -> 2i+1
-
父节点和右孩子节点的编号下标的关系
i -> 2i+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)
# 建堆完成
print(li)
时间复杂度 O(nlogn)
堆的python内部模块的使用
import heapq #q-queue 优先队列
import random
li = list(range(100))
random.shuffle(li)
print(li)
heapq.heapify(li) # 建堆
#print(li)
for i in range(len(li)):
print(heapq.heappop(li),end=" ,")
topk问题(堆排序)
有n个数,设计算法得到前k大的数(k<N)
思路: 排序后切片 O(nlogn) 排序LowB三人组 O(mn)
堆排序方法
- 取列表前k个元素建立一个小根堆.堆顶是目前第k大的数
- 依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整.
复杂度 O(mlogn)
归并排序
假设现在的列表分两段有序,如何将其合成为一个有序列表
这种操作称为一次归并
归并使用:
- 分解:将列表越分越小,直至分成一个元素
- 终止条件:一个元素是有序的
- 合并:将两个有序列表归并,列表越来越大
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
while i <= mid:
ltmp.append(li[i])
i += 1
while j <= high:
ltmp.append(li[j])
j += 1
li[low:high+1] = ltmp
def merge_sort(li,low,high):
if low<high:
mid = (low + high)//2
merge_sort(li,low,mid)
merge_sort(li,mid+1,high)
merge(li,low,mid,high)
复杂度 O(nlogn)
python内的sort方法 是基于归并排序的
NB三人组 排序总结
- 三种排序算法的时间复杂度都是O(nlogn)
- 运行时间: 快速排序<归并排序<堆排序
三种排序算法的缺点:
- 快速排序:极端情况下排序效率低
- 归并排序:需要额外的内存开销
- 堆排序:在快的排序算法中相对较慢
希尔排序
计数排序
对列表进行排序,已知列表中的数范围都在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)
复杂度 O(n)
桶排序
在计数排序中,如果元素的范围比较大(比如在1到1亿之间),如何改造算法
桶排序(Bucket Sort):首先将元素分在不同的桶中,再对每个桶中的元素排序
def bucket_sort(li,n=100,max_num=10000):
buckets = [[] for _ in range(n)]
for var in li:
i = min(var // (max_num // n) ,n-1)# i 表示var放在几号桶里
buckets[i].append(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)
return sorted_li
桶排序的表现取决于数据的分布.也就是需要对不同数据排序时采取不同的分桶策略.
平均情况时间复杂度 O(n+k) 最坏情况时间复杂度 O(n2k) 空间复杂度 O(nk)
基数排序
多关键字排序:加入现在有一个员工表,要求按照薪资排序,年龄相同的员工按照年龄排序
先按照年龄进行排序,再按照薪资进行稳定的排序
def radix_sort(li):
max_num = max(li) # 最大值
it= 0
while 10 ** it<= max_num:
buckets= [[] for _ in range(10)]
for var in li:
digit = (var // 10**it )%10
buckets[digit].append(var)
li.clear()
for buc in buckets:
li.extend(buc)
it += 1
github address:
https://github.com/chenbokaix250/ForSomeForMySelf