基本算法
算法定义
- 一个计算过程,解决问题的方法
- 有输入有输出,给规定的输入得到规定的输出
时间复杂度
- 用来估计算法运行时间的一个式子(单位)
- 一般来说,时间复杂度高的算法比复杂度低的算法慢。
- 常见复杂度效率排序
O(1)<O(logn)<O(n)
简单的判断时间复杂度?(无递归)
循环减半的过程---->O(logn)
几层循环就是n的几次方的复杂度
空间复杂度
用来估计算法内存占用大小的式子
列表查找:从列表中查找指定元素
输入:列表、待查找元素
输出:元素下标或者未查到元素
1 li = [1, 5, 6, 55, 64] 2 res = 6 in li 3 li.index(6)
顺序查找 线性查找 O(n)
从列表第一和元素开始,顺序进行搜索,直到找到为止。
1 for i in li: 2 pass
二分查找 O(logn)
从有序列表的候选区data[0:n]开始,通过对待查找的值与候选区中中间值的比较,可以使候选区减少一半。
从上图:
- 若low <= high 存在候选区
- 若low > high 不存在候选区 说明被查找的值根本不存在
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 5 def bin_search(li, val): 6 low = 0 7 high = len(li) - 1 8 while low <= high: 9 # 存在候选区 10 mid = (low + high) // 2 11 if li[mid] == val: 12 return mid 13 elif li[mid] < val: 14 # 右边 15 low = mid + 1 16 else: 17 # 左边 18 high = mid - 1 19 # 查找不到我默认返回-1 20 return -1 21 22 23 # 递归 24 def bin_search_rec(li, val, low, high): 25 if low <= high: 26 mid = (low + high) // 2 27 if li[mid] == val: 28 return mid 29 elif li[mid] > val: 30 return bin_search_rec(li, val, low, mid - 1) 31 else: 32 return bin_search_rec(li, val, mid + 1, high) 33 else: 34 return -1
low B三人组(时间O(n**2) 空间O(1))
冒泡排序 O(n**2)
- 思路:相邻两位置比较大小
1 # 装饰器 2 def timer(func): 3 def wrapper(*args, **kwargs): 4 import time 5 start_time = time.time() 6 res = func(*args, **kwargs) 7 stop_time = time.time() 8 print('运行时间%s' % (stop_time - start_time)) 9 return res 10 return wrapper
1 @timer 2 def bubble_sort(li): 3 for i in range(len(li)-1): # i表示第n趟 一共n或者n-1趟 4 exchange = False 5 for j in range(len(li)-i-1): # 第i趟 无序区[0, n-i-1] j表示箭头0~n-i-2 6 if li[j] > li[j+1]: 7 li[j], li[j+1] = li[j+1], li[j] 8 exchange = True 9 if not exchange: 10 break
选择排序O(n**2)
- 思路:一趟遍历记录最小的数,放到第一个位置,一直循环
1 def get_min_pos(li): 2 """ 3 找最小值的位置 4 :param li: 5 :return: 6 """ 7 min_pos = 0 8 for i in range(1, len(li)): 9 if li[i] < li[min_pos]: 10 min_pos = i 11 return min_pos 12 13 14 @timer 15 def select_sort(li): 16 for i in range(len(li) - 1): # n趟或者n-1趟 17 # 第i趟无序区的范围 i~最后 18 min_pos = i # min_pos更新无序区最小位置 19 for j in range(i+1, len(li)): 20 if li[j] < li[min_pos]: 21 min_pos = j 22 li[i], li[min_pos] = li[min_pos], li[i]
插入排序O(n**2)
- 思路:摸到的牌与前边的牌进行逐个比较
1 @timer 2 def insert_sort(li): 3 for i in range(1, len(li)): # i表示摸到的牌下标 4 tmp = li[i] # 摸到的牌 5 j = i - 1 6 while j >= 0 and li[j] > tmp: # 只要往后挪就循环 2个条件都得满足 7 # 如果j=-1停止挪动 如果li[j]小了 停止挪动 8 li[j+1] = li[j] 9 j -= 1 10 li[j+1] = tmp 11 12 13 # li = list(range(10000)) 14 # random.shuffle(li) 15 # insert_sort(li)
NB排序三人组
快排O(nlogn)
- 思路:
- 1.取一个元素p,使p归位
- 2.列表被P分成两部分,左边都比P小,右边都比P大
- 3.递归完成排序
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 import sys 5 import random 6 from lowB import timer 7 # 快排 O(n*logn) 8 9 10 @timer 11 def quick_sort(li): 12 _quick_sort(li, 0, len(li)-1) 13 14 15 # 这种写法没从新开辟新的空间 16 def _quick_sort(li, left, right): 17 if left < right: 18 mid = partition(li, left, right) 19 _quick_sort(li, left, mid-1) 20 _quick_sort(li, mid+1, right) 21 22 23 def partition(li, left, right): 24 tmp = li[left] 25 while left < right: 26 while left < right and li[right] >= tmp: 27 right -= 1 28 li[left] = li[right] 29 while left < right and li[left] <= tmp: 30 left += 1 31 li[right] = li[left] 32 li[left] = tmp 33 return left 34 35 36 @timer 37 def sys_sort(li): 38 li.sort() 39 40 41 li = list(range(10000)) 42 random.shuffle(li) 43 quick_sort(li) # 运行时间0.19099831581115723 44 sys_sort(li) # 运行时间0.0009989738464355469 Python内部排序快100倍 45 46 # sys.setrecursionlimit(2000)设置递归深度 47 48 49 # 开辟了新的空间 列表生成式 生成两列表占空间 思想完全相同 50 def quick_sort2(li): 51 if len(li) < 2: 52 return li 53 tmp = li[0] 54 left = [v for v in li[1:] if v <= tmp] 55 right = [v for v in li[1:] if v > tmp] 56 left = quick_sort2(left) 57 right = quick_sort2(right) 58 return left + [tmp] + right
问题:1.当列表全部倒序递归的层数就非常大 2.如果p元素只有一边有数据和p元素随机取呢
堆排序(O(nlogn))
插曲了解些数据结构
- 树是一种数据结构 可以仿照目录结构理解
- 树是一种可以递归定义的数据结构
- 上张图辅助理解
- 根节点:A
- 叶子节点:B/C等等 没孩子的节点
- 树的深度(高度):4
- 树的度:6
- 孩子节点的度:比如节点F,它的度为3
- 子树 :比如:F K L W构成一个子树 E I J P Q也是
二叉树:度不超过2的树(节点最多有两个叉)
- 满二叉树:节点全是满的,而且每个节点的孩子节点只能为2个
- 完全二叉树:最多两个孩子节点,并且是最左边一个个的少
二叉树的存储方式:链式存储、顺序存储方式
- 父节点和左孩子节点的编号下标关系:
0-----1 1-----3 2-----5 3------7 4-----9
推出关系 i------2i+1
- 父节点和右孩子节点的编号下标关系
0------2 1------4 2-----6 3-------8 4-----10
推出关系 i-------2i+2
- 孩子与父节点的编号下标关系
1------0 2------0
3------1 4------1
5------2 6------2
推出关系 左:(i-1)/2 右:(i-2)/2
总体 :(i-1)//2
堆
- 大根堆:一棵完全二叉树,满足任意节点都比孩子节点大
- 小根堆:一棵完全二叉树,满足任意节点都比孩子节点小
-----------------------------------------------------------------------------------------------------------------------
- 堆排序的三环节:
- 堆的向下调整环节性质
- 创建堆(用到性质)
- 挨个出数(用到性质)
1 # 堆的向下调整性质 2 def sift(li, low, high): 3 # li表示树 low表示树的根 high:表示最后一个子节点的位置 4 tmp = li[low] 5 i = low # 指向空的位置 6 j = 2 * i + 1 # 指向两孩子的位置 7 while j <= high: # 循环退出的第二种情况:j大于high,说明空位i没有孩子了,i是叶子节点 8 if j + 1 <= high and li[j] < li[j+1]: # 如果右孩子存在,并且右孩子比左孩子的值大,指向右孩子 9 j += 1 10 if li[j] > tmp: 11 # 省长上位 12 li[i] = li[j] 13 # 变换i与j的位置 14 i = j 15 j = 2 * i + 1 16 else: # 循环跳出的第一种情况:j位置的值比tmp小,说明两孩子都比tmp小 17 break 18 li[i] = tmp 19 20 21 @timer 22 def heap_sort(li): 23 # 创建堆 24 n = len(li) 25 # 从最后一个子树的位置一个个进行的调整 26 # 最后一个节点的位置是n-1 27 # 孩子节点的父亲孩子节点是i-1 // 2 28 for low in range(n//2-1, -1, -1): 29 sift(li, low, n-1) 30 print(li) # 画一个堆出来 31 # 挨个出数 32 for high in range(n-1, -1, -1): 33 li[0], li[high] = li[high], li[0] # 退休 棋子 34 sift(li, 0, high-1) 35 36 37 li = list(range(10)) 38 random.shuffle(li) 39 heap_sort(li) 40 print(li)
归并排序
- 归并排序之一次归并 假设现在的列表分两段有序,合成一个有序列表
1 def merge(li, low, mid, high): 2 i = low # 是第一段有序列表中第一个元素 3 j = mid + 1 # 是第二段有序列表的第一个元素 4 ltmp = [] # 创列表比较之后元素存放 5 while i <= mid and j <= high: 6 if li[i] <= li[j]: 7 ltmp.append(li[i]) 8 i += 1 9 else: 10 ltmp.append(li[j]) 11 j += 1 12 while i <= mid: 13 ltmp.append(li[i]) 14 i += 1 15 16 while j <= high: 17 ltmp.append(li[j]) 18 j += 1 19 20 li[low:high + 1] = ltmp 21 # for i in range(low, high+1): 22 # li[i] = ltmp[i-low] 23 # li = [2, 5, 7, 8, 9, 1, 3, 4, 6] 24 # merge(li, 0, 4, 8) 25 # print(li) 26 27 28 def merge_sort(li, low, high): 29 if low < high: 30 # 分解 31 mid = (low + high) // 2 32 # 递归 33 merge_sort(li, low, mid) 34 merge_sort(li, mid+1, high) 35 # 一次归并 36 merge(li, low, mid, high) 37 38 39 li = [10, 4, 6, 3, 8, 2, 5, 7] 40 merge_sort(li, 0, len(li) - 1) 41 print(li)
实例展示 不要直接合并起来再sort(nlogn) 归并一次合成下面实例就是(n)
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 5 # 如果现在两个有序列表 合并成一个 6 # def merge2list(li1, li2): 7 # li = [] 8 # i = 0 9 # j = 0 10 # while i < len(li1) and j < len(li2): 11 # if li1[i] <= li2[j]: 12 # li.append(li1[i]) 13 # i += 1 14 # else: 15 # li.append(li2[j]) 16 # j += 1 17 # while i < len(li1): 18 # li.append(li1[i]) 19 # i += 1 20 # while j < len(li2): 21 # li.append(li2[j]) 22 # j += 1 23 # return li 24 # 25 # 26 # li1 = [2, 5, 7, 8, 9] 27 # li2 = [1, 3, 4, 6] 28 # li = merge2list(li1, li2) 29 # print(li)
总结:
- 快速排序 < 归并排序 < 堆排序
三种排序算法的缺点:
- 快速排序:极端情况下排序效率低
- 归并排序:需要额外的内存开销
- 堆排序:在快的排序算法中相对较慢
补充:
希尔排序
- 是一种分组插入排序算法。
- 首先取一个整数d1=n/2,将元素分为d1个组,每组相邻量元素之间距离为d1,在各组内进行直接插入排序;
- 取第二个整数d2=d1/2,重复上述分组排序过程,直到di=1,即所有元素在同一组内进行直接插入排序。
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 import random 5 6 7 def timer(func): 8 def wrapper(*args, **kwargs): 9 import time 10 start_time = time.time() 11 res = func(*args, **kwargs) 12 stop_time = time.time() 13 print('运行时间%s' % (stop_time - start_time)) 14 return res 15 return wrapper 16 17 18 def insert_sort_gap(li, d): 19 for i in range(1, len(li)): 20 tmp = li[i] 21 j = i - d 22 while j >= 0 and li[j] > tmp: 23 li[j+d] = li[j] 24 j -= d 25 li[j+d] = tmp 26 27 28 @timer 29 def shell_sort(li): 30 d = len(li) // 2 31 while d > 0: 32 # do something 33 insert_sort_gap(li, d) 34 d = d // 2 35 36 37 li = list(range(10000)) 38 random.shuffle(li) 39 shell_sort(li) 40 # 运行时间0.11099767684936523 41 # 对插入排序确实提高了很多,但是跟NB三人组还是差距大
计数排序
- 现在有一个列表,列表中的数范围都在0到100之间,列表长度大约为100万。设计算法在O(n)时间复杂度内将列表进行排序。
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 5 # 数字不能很大 这样创列表超级大 6 def count_sort(li, max_num=50): 7 count = [0 for _ in range(max_num + 1)] 8 for val in li: 9 count[val] += 1 10 li.clear() 11 for i, v in enumerate(count): 12 # 表示i这个数出现了v次 13 for _ in range(v): 14 li.append(i) 15 # 时间复杂度O(n) 看第二层循环n是v的嘛
基数排序
- 先按照低位进行桶排序,然后按照高位再桶排序,最后挨个出数-->桶排序+关键字排序思想
1 def radix_sort(li): 2 max_num = max(li) 3 i = 0 # 最开始0位(最低位) 4 while (10 ** i <= max_num): 5 # do something 6 # 建立一个桶 7 buckets = [[] for _ in range(10)] 8 # 遍历每一个值 9 for val in li: 10 # 最低位的数字 11 digit = val // (10 ** i) % 10 12 # 把val放在桶里边 13 buckets[digit].append(val) 14 li.clear() 15 # 遍历出来每个桶 16 for bucket in buckets: 17 for val in bucket: 18 # 把每个桶里边的元素放在li列表中 19 li.append(val) 20 i += 1
下面是搞着玩的代码
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 5 # [1, 2, 3, 4, 5] 0, 1 6 # [1, 2, 3, 4, 5, 6] 0, 1, 2 7 # [1, 2, 3, 4, 5, 6, 7]0, 1, 2 8 # n // 2 -1 9 10 11 def reverse_list(li): 12 n = len(li) 13 for i in range(n // 2): 14 li[i], li[n-i-1] = li[n-i-1], li[i] 15 return li 16 17 18 print(reverse_list([1, 2, 3, 4, 5])) 19 20 21 def int2list(num): 22 li = [] 23 while num > 0: 24 li.append(num % 10) 25 num //= 10 26 li.reverse() 27 return li 28 29 30 print(int2list(12345)) 31 32 33 def reverse_int(num): 34 """ 35 123->321 12300->321 36 :param num: 37 :return: 38 """ 39 is_neg = False 40 if num < 0: 41 is_neg = True 42 num = -1 * num 43 res = 0 44 while num > 0: 45 res = res * 10 46 digit = num % 10 47 res = res + digit 48 num = num // 10 49 if is_neg: 50 res = res * -1 51 return res 52 53 54 print(reverse_int(-100)) 55 56 57 def get_digit(num, i): 58 """ 59 60 :param num: 57841 61 :param i: i为0 个位1 十位2 百位... 3 62 :return: 7 63 """ 64 return num // (10 ** i) % 10
推荐两网站不定期的练算法
算法的动画+伪代码https://visualgo.net/en
时不时的洗脑壳https://leetcode-cn.com/