算法
算法
时间复杂度
时间复杂度是用来估算算法运行时间的一个式子(单位)。
一般来说,时间复杂度高的算法比复杂度低的算法慢。
常见的时间复杂度(安效率排序):
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n2logn) < O(n3)
不常见的时间复杂度:
O(n!) < O(2n) < O(nn)
print('Hello World') # 时间复杂度O(1)
O(1) O大约 1表示一个单位
for i in range(n): print('Hello World') # 时间复杂度O(n)
for i in range(n): for j in range(n): print('Hello World') # 时间复杂度O(n2)
for i in range(n): for j in range(n): for k in range(n): print('Hello World') # 时间复杂度O(n3)
print('Hello World') print('Hello Python') print('Hello Algorithm') # 时间复杂度O(1)
for i in range(n): print('Hello World') for j in range(n): print('Hello World') # 时间复杂度O(n2) 根据打印的次数进行判断:n2 + n
for i in range(n): for j in range(i): print('Hello World') # 时间复杂度O(n2) 根据打印的次数进行判断:0 + 1 + 2 + ... + (n-1)
当两个代码的时间复杂度分别为O(n)与O(n2),谁的运行速度快?
不一定。当n比较小的时候,不一定谁比较快。一般情况下,我们研究的是n足够大的时候,n足够大,O(n)运行速度快。
当两段代码的时间复杂度都为O(n2)时,谁的运行速度快?
不一定。因为其中还牵扯到一个常数项,但是我们计算时间复杂度时,都将常数项忽略了。
当出现循环折半的时候,代码的时间复杂度就为O(logn).
while n > 1: print(n) n = n // 2 # 时间复杂度O(logn)
如何判断时间复杂度?
循环减半的过程:O(logn);
几次循环就是n的几次方的复杂度。
空间复杂度
空间复杂度:用来评估算法内存占用大小的一个式子。
使用一个变量。空间复杂度O(1)
使用了一个长度为n的列表。空间复杂度O(n)
使用了n个长度为n的列表。空间复杂度O(n2)
空间换时间
一般不考虑空间复杂度,除非代码特别复杂,会吃爆内存或者有要求时考虑。
递归
递归的两个特点:
调用自身
结束条件
假设x为3:
def func(x): if x > 0: print(x) func(x-1)
func(3) ''' 输出结果: 3 2 1 '''
递归调用图解:
def func(x): if x > 0: func(x-1) print(x)
func(3) ''' 输出结果: 1 2 3 '''
递归调用图解:
斐波那契
求斐波那契第n项
1 1 2 3 5 8 13
# 假设小规模的问题能解决的条件下,能设计步骤解决原问题
# 当n比较大时就比较慢,重复计算子问题导致的。
递归写法:
def fib(n): ''' 递归写法 ''' if n == 0 or n == 1: return 1 else: return fib(n-1) + fib(n-2) print(fib(10))
依次调用原函数,给传入的数一次减一,直至递归找到第0项或者第1项。时间复杂度O(2n)
不重复计算子问题写法:
def fib(n): ''' 不重复计算子问题 ''' res = [1, 1] for i in range(2, n+1): res.append(res[-1] + res[-2]) return res[-1] print(fib(100))
将第0项和第1项放入列表,然后一次循环,每次取列表的后两项求和存入列表,循环n次,最后取最后一项即为第n个数。时间复杂度O(n) 空间复杂度O(n)
没有空间复杂度写法:
def fib(n): if n == 0 or n == 1: return 1 a = 1 b = 1 c = 2 # 前两项之和 for i in range(n, n+1): c = a + b a = b b = c return c print(fib(100))
就是重复赋值。
汉诺塔问题
def hanoi(n, A, B, C): if n > 0: hanoi(n-1, A, C, B) print(f'{A}->{C}') hanoi(n-1, B, A, C) hanoi(3, 'A', 'B', 'C')
习题
一段有n个台阶组成的楼梯,小明从楼梯的底层向最高层前进,他可以选择一次迈一级台阶或者一次迈两级台阶。问:他有多少种不同的走法?
列表查找
列表查找
从列表中查找指定元素。
输入:列表、待查找元素
输出:元素下标或未查找到元素
顺序查找
从列表第一个元素开始,顺序进行搜索,直到找到为止。
二分查找
从有序列表的候选区data[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。
非递归写法:
def bin_search(li, val): low = 0 high = len(li) - 1 while low <= high: # 候选区有值 mid = (low + high) // 2 if li[mid] == val: return mid elif li[mid] > val: high = mid - 1 else: low = mid + 1 return -1 bin_search(li, val)
递归写法:(不推荐)
def bin_search_rec(data_set, value, low, high): if low <= high: mid = (low + high) // 2 if data_set[mid] == value: return mid elif data_set[mid] > value: return bin_search_rec(data_set, value, low, mid-1) else: return bin_search_rec(data_set, value, mid+1, high) else: return - 1 def bin_search_rec(data_set, value, low, high)
排序Low B三人组
冒泡排序
首先,列表每两个相邻的数,如果前边的比后边的大,那么交换着两个数......
def bubble_sort(li): for i in range(len(li)-i): # i表示第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] bubble_sort(li)
优化版:
def bubble_sort(li): for i in range(len(li)-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 if not exchange: break bubble_sort(li)
时间复杂度
O(n2)
空间复杂度
O(1)
选择排序
一趟遍历记录最小的数,放到第一个位置;再一趟遍历记录剩余列表(无序区)中最小的数,继续放置到无序区的第一个位置;依次重复,直到结束。
def select_sort(li): for i in range(len(li)-1): # 无序区的范围 i, n-1 min_pos = i for j in range(i+1, len(li)): if li[j] < li[min_pos]: min_pos = j li[i], li[min_pos] = li[min_pos], li[i] select_sort()
选择排序没有最好的情况,即使是排好序的列表,时间复杂度也是不变。
时间复杂度
O(n2)
空间复杂度
O(1)
插入排序
列表元素分为有序区和无序区两部分。最初有序区只有一个元素。每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。
def insert_sort(li): for i in range(1, len(li)): j = i - 1 tmp = li[i] while j >= 0 and li[j] > tmp: # 这两个条件最好不要写反了,Python中支持负索引,其他语言中不支持,就会直接报错程序挂掉 li[j+1] = li[j] j -= 1 li[j+1] = tmp insert_sort()
当列表为有序时,存在最好情况。
时间复杂度
O(n2)
空间复杂度
O(1)
NB三人组
快速排序
取一个元素P(第一个元素),使元素P归位;列表被P分成两部分,左边都比P小,右边都比P大;递归完成排序。
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)
quick_sort(li, 0, len(li)-1)
方式一
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 partition(li, left, right): tmp = li[right] i = left - 1 for j in range(left, right): if li[j] <= tmp: # 归到左半部分 i += 1 li[i], li[j] = li[j], li[i] li[right], li[i+1] = li[i+1], li[right] return i+1
优点
快
时间复杂度
O(nlogn)
最坏情况
每次都选到最大或者最小的数,超过最大递归深度。
改进
针对方式一,如果将列表改成降序,只需要将partition中的 li[right] >= tmp 改成 li[right] <= tmp和 li[left] <= tmp 改成 li[left] >= tmp即可。
堆排序
树
堆
def sift(li, low, high): ''' :param li: :param low: 堆顶 :param high: 最后一个 :return: ''' tmp = li[low] i = low j = 2 * i + 1 while j <= high: # 退出循环的第二种情况:i已经是最后一层了 if j + 1 <= high and li[j+1] > li[j]: # 右孩子存在并且大于左孩子 j += 1 if tmp < li[j]: li[i] = li[j] i = j j = 2 * i + 1 else: break # 退出循环的第一种情况:j位置比tmp小 li[i] = tmp def heap_sort(li): ''' 建堆 :param li: :return: ''' n = len(li) for low in range(n//2-1, -1, -1): sift(li, low, n-1) # 挨个输出,退休-棋子-调整 重复n次或者n-1次 for high in range(n-1, -1, -1): li[0], li[high] = li[high], li[0] sift(li, 0, high-1) heap_sort()
比快排稍微慢点,但是还是挺快的。
时间复杂度
sift() O(logn)
deap_sort() O(n)
整体时间复杂度:O(nlogn)
python内置堆排
python拥有内置的堆排序模块heapq
优先队列:一些元素的集合,POP操作每次执行都会从优先队列中弹出最大(或最小)的元素。
堆——优先队列
import heapq li = [5, 7, 9, 8, 4, 1, 6, 2, 3] heapq.heapify(li) # 建堆(小根堆) print(li) # [1, 2, 5, 3, 4, 9, 6, 8, 7] 小根堆 heapq.heapify(li, 0) # 给堆添加元素, 前边是列表,后边是要添加的元素 print(li) # [0, 1, 2, 5, 3, 4, 9, 6, 8, 7] num = heapq.heappop(li) # 最小的元素出来 print(num) # [0]
# 前n大的数
num1 = heapq.nlagest(5, [1, 2, 5, 4, 7, 8, 9, 6, 3]) # 5为多少个数,列表为待查询列表
print(num1) # [9, 8, 7, 6, 5]
# 前n小的数
num2 = heapq.nsmallgest(5,[1, 2, 5, 4, 7, 8, 9, 6, 3]) # 5为多少个数,列表为待查询列表
print(num2) # [1, 2, 3, 4, 5]
使用heapq模块实现堆排序
def heapq_sort(li): h = [] for value in li: heapqpush(h, value) return [heappop(h) for i in range(len(h))] heapq_sort()
应用
优先队列
topK问题。(n个数中找出前k大的数)
思路
取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数。依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整。遍历列表所有元素后,倒序弹出堆顶。
def topk(li, k): heap = li[0: k] # 建堆 for i in range(k // 2 - 1, -1, -1): sift(heap, i, k - 1) # 插进去调整 for i in range(k, len(li)): if li[i] > heap[0]: heap[0] = li[i] sift(heap, 0, k - 1) # 挨个出数 for i in range(k - 1, -1, -1): heap(0), heap[-1] = heap[i], heap[0] sift(heap, 0, i - 1) def sift(li, low, high): ''' :param li: :param low: 堆顶 :param high: 最后一个 :return: ''' tmp = li[low] i = low j = 2 * i + 1 while j <= high: # 退出循环的第二种情况:i已经是最后一层了 if j + 1 <= high and li[j+1] < li[j]: # 右孩子存在并且小于左孩子 j += 1 if tmp > li[j]: li[i] = li[j] i = j j = 2 * i + 1 else: break # 退出循环的第一种情况:j位置比tmp小 li[i] = tmp topk()
归并排序
假设现在的列表分两段有序(例如一半升序,一半降序),将其合成一个有序列表。这种操作称为一次归并。
一次归并
def merge(li, low, mid, high): i = low j = mid + 1 li_tmp = [] while i <= mid and j <= high: if li[i] <= li[j]: li_tmp.append(li[i]) i += 1 else: li_tmp.append(li[j]) j += 1 while i <= mid: li_tmp.append(li[i]) i += 1 while j <= high: li_tmp.append(li[i]) j += 1
for k in range(low, high + 1): li[k] = li_tmp[k - low] # li[low: high + 1] = li_tmp
使用
拿到一个完全无序的列表,如何使用归并呢?首先,将列表越分越小,直至分成一个元素。一个元素是有序的,再将两个有序列表归并,列表越来越大。
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 = [10, 4, 6, 3, 8, 2, 5, 7] merge_sort(li, 0, len(li) - 1) print(li)
时间复杂度
O(nlogn)
空间复杂度
O(n)
三种排序算法的时间复杂度都是O(nlogn)。
一般情况下,就运行时间而言:
快速排序 < 归并排序 < 堆排序
三种排序算法的缺点:
快速排序:
极端情况下排序效率低
归并排序:
需要额外的内存开销
堆排序:
在快的排序算法中相对较慢
前6种算法的简单总结
python的排序算法是Timsort。使用归并和插入算法混合实现。
没什么人用的排序
计数排序
一个列表,列表中的元素的范围都在0到100之间。设计算法在O(n)时间复杂度内将列表进行排序。
思想:
建一个列表,统计里边的每个元素的个数,统计每个数出现的次数,再根据统计的结果一次输出排序的结果。
def count_sort(li, max_num=100): count = [0 for i in range(max_num+1)] for val in li: count[val] += 1 li.clear() for i in range(len(count)): for _ in range(count[i]): li.append(i) count_sort()
时间复杂度
O(n)
希尔排序
希尔排序是一种分组插入排序算法。首先取一个整数d1=n/2,将元素分为d1个组,魅族相邻两元素之间距离为d1,在各组内进行直接插入排序;去第二个整数d2=d1/2,重复上述分组排序过程,知道di=1,即所有元素在同一组内进行直接插入排序。
希尔排序每趟并不是某些元素有序,而是使整体数据越来越接近有序;最后一趟使得所有的数据有序。
def shell_insert_sort(li, d): for i in range(d, len(li)): j = i - d tmp = li[i] while j >= 0 and li[j] > tmp: li[j + d] = li[j] j -= d li[j + d] = tmp def shell_sort(li): d = len(li) // 2 while d > 0: shell_insert_sort(li, d) d = d // 2 shell_sort()
桶排序
在技术排序中,如果元素范围比较大(比如在1到1亿之间),name这个算法就不在适用。这时可以使用桶排序。
桶排序:
首先将元素分在按不同的桶中,在对每个桶(列表)中的每个元素进行排序。
桶排序的表现取决于数据的分布。也就是需要对不同数据排序时采取不同的分桶策略。
时间复杂度
平均时间复杂度:O(n+k)
最坏情况时间复杂度:O(n2k)
空间复杂度
O(nk)
基数排序
多关键字排序:
假如现在一个员工表,要求按照薪资排序,年龄形同的员工按照年龄排序。先按照年龄进行排序,再按照薪资进行稳定的排序。
def list_to_buckets(li, i): buckets = [[] for _ in range(10)] for num in li: digit = num // (10 ** i) % 10 buckets[digit].append(num) return buckets def radix_sort(li): max_val = max(li) i = 0 while 10 ** i <= max_val: li = list_to_buckets(li, i) i += 1 return li radix_sort()
时间复杂度
O(nk)
k表示数字位数
空间复杂度
O(k + n)
k表示数字位数