1 算法基础
1.1 算法概念
1.算法(Algorithm):一个计算过程,解决问题的方法。
2. “程序=数据结构+算法”
1.2 时间复杂度
1.时间复杂度是用来估计算法运行时间的一个式子。
2. 一般来说,时间复杂度高的算法比复杂度低的算法慢。
3. 常见的时间复杂度(按效率排序)
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n2logn) < O(n3)
4. 复杂问题的时间复杂度
O(n!) O(2) O(n
) ···
5. 如何简单快速地判断算法复杂度
快速判断算法复杂度(适用于绝大多数简单情况):
- 确定问题规模n
- 循环减半过程—>logn
- k层关于n的循环—>nk
复杂情况:根据算法执行过程判断
1.3 空间复杂度
1.空间复杂度:用来评估算法内存占用大小的式子
2.空间复杂度的表示方式与时间复杂度完全一样
- 算法使用了几个变量:O(1)
- 算法使用了长度为n的一维列表:O(n)
- 算法使用了m行n列的二维列表:O(mn)
4.“空间换时间”—时间比空间重要
1.4 递归
递归的两个条件:
- 调用自身
- 结束条件
func3先打印后递归;func4先递归后打印
输入x=3,func3打印3 2 1;func4打印1 2 3
1.4.1 汉诺塔问题
n = 2时:
- 把小圆盘从A移动到B
- 把大圆盘从A移动到C
- 把小圆盘从B移动到C
n个盘子时:
- 把n-1个圆盘从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 from %s to %s" % (a,c))
hanoi(n-1,b,a,c)
hanoi(3,'A','B','C')
2 列表查找
2.1 什么是列表查找
1.查找:在一些数据元素中,通过一定的方法找出与给定关键字相同的数据元素的过程。
2.列表查找(线性表查找):从列表中查找指定元素
- 输入:列表、待查找元素
- 输出:元素下标(未找到元素时一般返回None或-1)
3.内置列表查找函数:index()
2.2 顺序查找
顺序查找,也叫线性查找,从列表第一个元素开始,顺序进行搜索,直到找到元素或搜索到列表最后一个元素为止。
def linear_search(li,val):
for ind,v in enumerate(li):
if v == val:
return ind
else:
return None
时间复杂度:O(n)
2.3二分查找
二分查找,又叫折半查找,从有序列表的初始候选区li[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。
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
时间复杂度:O(logn)
注:二分查找时间复杂度低,运行快,但是使用二分查找的是有序列表。
无序列表用顺序查找,或者排序后用二分查找。排序时间复杂度有时会更高。
有序列表用二分查找
3 列表排序
3.1 什么是列表排序
1.排序:将一组“无序”的记录序列调整为“有序”的记录序列。
2.列表排序:将无序列表变为有序列表
- 输入:列表
- 输出:有序列表
3.升序与降序
4.内痔排序函数:sort()
3.2 常见排序算法介绍
3.2.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]: #升序
if li[j] < li[j + 1]: #降序
li[j],li[j+1] = li[j+1],li[j]
#优化版:没有再进行交换说明剩下的已经排好序了,不用再进行排序
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
时间复杂度:O(n2)
3.2.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)): #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) #打印每趟排序的结果
时间复杂度:O(n2)
3.2.3 插入排序
初始时手里(有序区)只有一张牌, 每次(从无序区)摸一张牌,插入到手里已有牌的正确位置。
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
时间复杂度:O(n2)
3.2.4 快速排序
快速排序:快
快速排序思路:
1.取一个元素p(第一个元素),使元素p归位;
2.列表被p分成两部分,左边都比p小,右边都比p大;
3.递归完成排序。
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] #把右边的值写到左边空位上
while left < right and li[left] <= tmp: #从左边找比tmp大的数
left += 1 #往右走一步
li[right] = 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)
时间复杂度:O(nlogn)
3.2.5 堆排序
1.树的基础知识
树是一种数据结构,比如:目录结构。
树是一种可以递归定义的数据结构。
树是由n个节点组成的集合:
如果n=0,那这是一棵空树;
如果n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树。
一些概念:
根节点(最根上的节点)、叶子结点(没有分叉的节点)
树的深度(高度)(有多少层)
树的度(=最大的节点的度)
孩子节点/父节点(相对关系)
子树
2.二叉树的基础知识
二叉树:度不超过2的树。
每个节点最多有两个孩子节点,两个孩子节点被区分为左孩子节点和右孩子节点。
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
二叉树的存储方式(表示方式):
链式存储方式
顺序存储方式(用列表来存储)
二叉树的顺序存储方式:
父节点和左孩子节点的编号下标的关系:i—>2i+1
父节点和右孩子节点的编号下标的关系:i—>2i+2
孩子节点和父节点的编号下标的关系:i—>(i-1)//2
3.什么是堆
堆:一种特殊的完全二叉树结构。
大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大。
小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小。
堆的向下调整性质
假设根节点的左右子树都是堆,但根节点不满足堆的性质,可以通过一次向下的调整来将其变成一个堆。
4.堆排序过程
(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更大
li[i] = tmp #把tmp放到i的位置上
break
else:
li[i] = tmp #把tmp放到叶子结点上
'''
#简化版
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更大
break
li[i] = tmp #把tmp放到i的位置上(两种情况:1.tmp比li[j]大;2.j>high)
'''
def heap_sort(li):
n = len(li)
for i in range((n-2)//2,-1,-1): #知道孩子结点位置求父结点位置,孩子结点位置i = n-1,则父结点位置为(i-1)//2
# 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)
时间复杂度:O(nlogn)
5.堆的内置模块
Python内置模块——heapq
常用函数:heapify(x)、heappush(heap,item)、heappop(heap)
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=',')
6.topk问题
现在有n个数,设计算法得到前k大的数。(k<n)
解决思路:
排序后切片——O(nlogn)
冒泡、选择、插入——O(kn)
堆排序思路——O(nlogk)
(1)取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数。
(2)依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整;
(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更大
break
li[i] = tmp #把tmp放到i的位置上(两种情况:1.tmp比li[j]大;2.j>high)
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)):
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(100))
random.shuffle(li)
print(topk(li,10))
3.2.6 归并排序
假设现在的列表分两段有序,如何将其合成为一个有序列表,这种操作称为一次归并。
def merge(li,low,mid,high):
"""
:param li: 列表
:param low: 第一段有序列表的第一个数
:param mid:第一段有序列表的最后一个数
:param high:第二段有序列表的最后一个数
:return:
"""
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 执行完,肯定有一部分没数了
while i <= mid:
ltmp.append(li[i])
i += 1
while j <= high:
ltmp.append(li[j])
j += 1
li[low:high+1] = ltmp
li = [2,4,5,7,1,3,6,8]
merge(li,0,3,7)
print(li)
使用归并:
(1)分解:将列表越分越小,直至分成一个元素。
(2)终止条件:一个元素是有序的。
(3)合并:将两个有序列表归并,列表越来越大。
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)
li = list(range(100))
import random
random.shuffle(li)
merge_sort(li,0,len(li)-1)
print(li)
时间复杂度:O(nlogn)
空间复杂度:O(n)
小结
1.三种排序算法的时间复杂度都是O(nlogn).
2.一般情况下,就运行时间而言:
快速排序 < 归并排序 < 堆排序
3.三种排序算法的缺点:
(1)快速排序:极端情况下排序效率低;
(2)归并排序:需要额外的内存开销;
(3)堆排序:在快的排序算法中相对较慢。
排序方法 | 时间复杂度(最坏情况) | 时间复杂度(平均情况) | 时间复杂度(最好情况) | 空间复杂度 | 稳定性 | 代码复杂度 |
冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 | 简单 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 | 简单 |
插入排序 | O(n2) | O(n2) | O(n2) | O(1) | 稳定 | 简单 |
快速排序 | O(n2) | O(nlogn) | O(nlogn) | 平均情况:O(nlogn);最坏情况O(n) | 不稳定 | 较复杂 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 | 复杂 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 | 较复杂 |
3.2.7 希尔排序
希尔排序(Shell Sort)是一种分组插入排序算法。
(1)首先取一个整数d1=n/2,将元素分为d1个组,每组相邻量元素之间距离为d1,在各组内进行直接插入排序。
(2)取第二个整数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 #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
li = list(range(1000))
import random
random.shuffle(li)
shell_sort(li)
print(li)
希尔排序的时间复杂度讨论比较复杂,并且和选取的gap序列有关。
3.2.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)
桶排序
在计数排序中,如果元素的范围比较大(比如在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) #把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) # 把每个buc列表加到sorted_li里边
return sorted_li
import random
li = [random.randint(0,10000) for i in range(10000)]
li = bucket_sort(li)
print(li)
桶排序的表现取决于数据的分布。也就是需要对不同数据排序时采取不同的分桶策略。
平均情况时间复杂度:O(n+k)
最坏情况时间复杂度:O(n2k)
空间复杂度:O(nk)
3.2.9 基数排序
多关键字排序:加入现在有一个员工表,要求按照薪资排序,薪资相同的员工按照年龄排序。
先按照年龄进行排序,再按照薪资进行稳定的排序。
对32,13,94,52,17,54,93排序,也可以看做多关键字排序。先按照个位进行排序,再按照十位进行稳定的排序。
def radix_sort(li):
max_num = max(li) # 最大值 9->1,99->2,888->3,1000->5
it = 0
while 10 ** it <= max_num:
buckets = [[] for _ in range(10)]
for var in li:
# 987 it=1 987//10->98 98%10->8; it=2 987//100->9 9%10->9
digit = (var // 10 ** it) % 10
buckets[digit].append(var)
# 分桶完成
# 把数重新写回li
li.clear()
for buc in buckets:
li.extend(buc)
it += 1
import random
li = list(range(100000))
random.shuffle(li)
radix_sort(li)
print(li)
时间复杂度:O(kn)
空间复杂度:O(k+n)
k表示数字位数。
4 查找排序习题
1.给两个字符串s和t,判断t是否为s的重新排列后组成的单词
s = “anagram”,t = “nagaram”,return true.
s = “rat”,t = “car”,return false.
#法一
def isAnagram(self, s, t):
"""
:type s: str
:type t: str
:rtype: bool
"""
return sorted(s) == sorted(t)
#法二
def isAnagram(self, s, t):
"""
:type s: str
:type t: str
:rtype: bool
"""
dict1 = {}
dict2 = {}
for ch in s:
dict1[ch] = dict1.get(ch,0) + 1
for ch in t:
dict2[ch] = dict2.get(ch,0) + 1
return dict1 == dict2
2.给定一个m*n的二维列表,查找一个数是否存在。列表有下列特性:
每一行的列表从左到右已经排序好。
每一行第一个数比上一行最后一个数大。
[
[1, 3, 5, 7],
[10,11,16,20],
[23,30,34,50]
]
#法一
def searchMatrix(self, matrix, target):
"""
:type matrix: List[List[int]]
:type target: int
:rtype: bool
"""
for line in matrix:
if target in line:
return True
return False
#法二
def searchMatrix(self, matrix, target):
"""
:type matrix: List[List[int]]
:type target: int
:rtype: bool
"""
h = len(matrix)
if h == 0:
return False
w = len(matrix[0])
# if w == 0:
# return False
left = 0
right = h * w - 1
while left <= right:
mid = (left + right) // 2
i = mid // w #第i行
j = mid % w #第j行
if matrix[i][j] == target:
return True
elif matrix[i][j] < target:
left = mid + 1
else:
right = mid - 1
return False
3.给定一个列表和一个整数,设计算法找到两个数的下标,使得两个数之和为给定的整数。保证肯定仅有一个结果。
例如,列表[1,2,5,4]与目标整数3,1+2=3,结果为(0,1).
class Solution(object):
def binary_search(self,li,left,right,val):
while left <= right:
mid = (left + right) // 2
if li[mid][0] == val:
return mid
elif li[mid][0] < val:
left = mid + 1
else:
right = mid - 1
else:
return None
def twoSum(self, numbers, target):
"""
:type numbers: List[int]
:type target: int
:rtype: List[int]
"""
new_numbers = [[num,i] for i,num in enumerate(numbers)]
new_numbers.sort(key=lambda x:x[0])
for i in range(len(new_numbers)):
a = new_numbers[i][0]
b = target - a
if b >= a:
j = self.binary_search(new_numbers,i + 1,len(new_numbers)-1,b)
else:
j = self.binary_search(new_numbers,0,i - 1,b)
if j:
break
return sorted([new_numbers[i][1],new_numbers[j][1]])