算法基本概念
算法: 计算过程,解决问题的方法,
时间复杂度
用来评估算法运行效率的一个东西
以上几种算法分别表示了 算法的几种时间复杂度 依次为:O(1) O(N) O(N**2) O(N**3)
时间复杂度是用来估计算法运行时间的一个式子(单位)。
一般来说,时间复杂度高的算法比复杂度低的算法慢。
常见的时间复杂度(按效率排序) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n2logn)<O(n3)
如何一眼判断时间复杂度?
循环减半的过程 —> O(logn)
几次循环就是n的几次方的复杂度
空间复杂度
空间复杂度:用来评估算法内存占用大小的一个式子
“空间换时间”
复习:递归
递归的两个特点: 1.调用自身 2. 结束条件
递归实现汉诺塔问题:
大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。 大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。 在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。 64根柱子移动完毕之日,就是世界毁灭之时。 n=2时: 1.把小圆盘从A移动到B 2.把大圆盘从A移动到C 3.把小圆盘从B移动到C n个盘子时: 1.把n-1个圆盘从A经过C移动到B 2.把第n个圆盘从A移动到C 3.把n-1个小圆盘从B经过A移动到C t = 0 def hanoi(n, A, B, C): global t if n > 0: hanoi(n-1, A, C, B) t += 1 print("%s -> %s" % (A, C)) hanoi(n-1, B, A, C) hanoi(8,'A','B','C') print(t) 汉诺塔移动次数的递推式:h(n)=2h(n-1)+1 h(64)=18446744073709551615 = 2**64 -1 假设婆罗门每秒钟搬一个盘子,则总共需要5800亿年!
汉诺塔问题
二分查找
列表查找:从列表中查找指定元素
输入:列表、待查找元素
输出:元素下标或未查找到元素
1. 顺序查找 从列表第一个元素开始,顺序进行搜索,直到找到为止。
2. 二分查找 从有序列表的候选区data[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。
def linear_search(data_set, value): for i in range(range(data_set)): if data_set[i] == value: return i return
顺序查找— O(n)
def bin_search(data_set, value): low = 0 high = len(data_set) - 1 while low <= high: mid = (low+high)//2 if data_set[mid] == value: return mid elif data_set[mid] > value: high = mid - 1 else: low = mid + 1
二分查找—O(logn)
可见二分查找顺序是比顺序查找快的
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
递归版二分查找
列表排序
排序low B三人组
冒泡排序
冒泡排序思路: 列表每两个相邻的数,如果前边的比后边的大,那么交换这两个数
代码关键点:
趟
无序区
import random from timewrap import * # 冒泡排序 @cal_time def bubble_sort(li): for i in range(len(li) - 1): # i 表示趟数 # 第 i 趟时: 无序区:(0,len(li) - i) for j in range(0, len(li) - i - 1): if li[j] > li[j+1]: li[j], li[j+1] = li[j+1], li[j]
冒泡排序一
运行时间20.29秒
import time def cal_time(func): def wrapper(*args, **kwargs): t1 = time.time() result = func(*args, **kwargs) t2 = time.time() print("%s running time: %s secs." % (func.__name__, t2-t1)) return result return wrapper import random @cal_time def bubble_sort_2(li): for i in range(len(li) - 1): # i 表示趟数 # 第 i 趟时: 无序区:(0,len(li) - i) change = False for j in range(0, len(li) - i - 1): if li[j] > li[j+1]: li[j], li[j+1] = li[j+1], li[j] change = True if not change: return li = list(range(100000)) random.shuffle(li) # print(li) bubble_sort_2(li)
冒泡排序优化–时间测试
运行时间19.68秒
时间复杂度为O(n**2)
选择排序
思路: 一趟遍历记录最小的数,放到第一个位置; 再一趟遍历记录剩余列表中最小的数,继续放置
代码关键点: 无序区 最小数的位置
@cal_time def select_sort(li): for i in range(len(li) - 1): # i 表示趟数,也表示无序区开始的位置 min_loc = i # 最小数的位置 for j in range(i + 1, len(li) - 1): if li[j] < li[min_loc]: min_loc = j li[i], li[min_loc] = li[min_loc], li[i] li = list(range(10000)) random.shuffle(li) print(li) select_sort(li) print(li)
选择排序
时间复杂度:O(n2)
运行时间7.8秒
插入排序
思路: 列表被分为有序区和无序区两个部分。最初有序区只有一个元素。 每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。如图:
代码关键点: 摸到的牌 手里的牌
import random from timewrap import * @cal_time def insert_sort(li): for i in range(1, len(li)): # i 表示无序区第一个数 tmp = li[i] # 摸到的牌 j = i - 1 # j 指向有序区最后位置 while li[j] > tmp and j >= 0: # 循环终止条件: 1. li[j] <= tmp; 2. j == -1 li[j + 1] = li[j] j -= 1 li[j + 1] = tmp li = list(range(10000)) random.shuffle(li) print(li) insert_sort(li) print(li)
插入排序
时间复杂度:O(n2) 优化空间:应用二分查找来寻找插入点(并没有什么卵用)
运行时间8.6秒
小结
有以上可以得出 运行时间:
冒泡排序 > 插入排序 > 选择排序
时间复杂度:O(n2)
空间复杂度:O(1)
NB排序三人组
快速排序
快速排序:快
快排思路: 取一个元素p(第一个元素),使元素p归位;
列表被p分成两部分,左边都比p小,右边都比p大;
递归完成排序
算法关键点: 整理 递归
import random from timewrap import * import copy import sys sys.setrecursionlimit(10000) # 第一步把列表分割两个区域函数 def partition(li, left, right): # ri = random.randint(left, right) # li[left], li[ri] = li[ri], li[left] 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 # 第2步快速排序 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) @cal_time def quick_sort(li): return _quick_sort(li, 0, len(li) - 1) @cal_time def sys_sort(li): li.sort() li = list(range(100000)) random.shuffle(li) # sys_sort(li) ---- 0.06秒 quick_sort(li) ----- 0.63秒
快速排序
由此可见快速排序效率是相当高的10万条数据仅需0.6秒
效率
快速排序的时间复杂度 为O(nlogn)
堆排序
树与二叉树:
树是一种数据结构
树是一种可以递归定义的数据结构
树是由n个节点组成的集合: 如果n=0,那这是一棵空树; 如果n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树。
一些概念 根节点、叶子节点
树的深度(高度)
树的度
孩子节点/父节点
子树
二叉树:度不超过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
——————–> i+1//2
堆:
大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大 如下图:
小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小 如下图:
堆的向下调整性质:
假设:节点的左右子树都是堆,但自身不是堆 如下
当根节点的左右子树都是堆时,可以通过一次向下的调整来将其变换成一个堆(可以理解为省长县长与村长关系 值大的能力大)
堆排序过程:
1.建立堆
2.得到堆顶元素,为最大元素
3.去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序。
4.堆顶元素为第二大元素。
5.重复步骤3,直到堆变空。
from timewrap import * import random def _sift(li, low, high): """ :param li: :param low: 堆根节点的位置 :param high: 堆最有一个节点的位置 :return: """ i = low # 父亲的位置 j = 2 * i + 1 # 孩子的位置 tmp = li[low] # 原省长 while j <= high: 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: li[i] = tmp # 省长放到对应的位置上(干部) break else: li[i] = tmp # 省长放到对应的位置上(村民/叶子节点) def sift(li, low, high): """ :param li: :param low: 堆根节点的位置 :param high: 堆最有一个节点的位置 :return: """ i = low # 父亲的位置 j = 2 * i + 1 # 孩子的位置 tmp = li[low] # 原省长 while j <= high: 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 li[i] = tmp @cal_time def heap_sort(li): n = len(li) # 1. 建堆 for i in range(n//2-1, -1, -1): sift(li, i, n-1) # 2. 挨个出数 for j in range(n-1, -1, -1): # j表示堆最后一个元素的位置 li[0], li[j] = li[j], li[0] # 堆的大小少了一个元素 (j-1) sift(li, 0, j-1) li = list(range(10000)) random.shuffle(li) heap_sort(li) print(li) # li=[2,9,7,8,5,0,1,6,4,3] # sift(li, 0, len(li)-1) # print(li)
堆排序
堆排序——内置模块
优先队列:一些元素的集合,POP操作每次执行都会从优先队列中弹出最大(或最小)的元素。
堆——优先队列
Python内置模块——heapq heapify(x) heappush(heap, item) heappop(heap)
利用heapq模块实现堆排序
import heapq, random li = [5,8,7,6,1,4,9,3,2] heapq.heapify(li) print(heapq.heappop(li)) print(heapq.heappop(li)) def heap_sort(li): heapq.heapify(li) n = len(li) new_li = [] for i in range(n): new_li.append(heapq.heappop(li)) return new_li li = list(range(10000)) random.shuffle(li) # li = heap_sort(li) # print(li) print(heapq.nlargest(100, li))
内置模块heapq实现堆排序
堆排序扩展——topK问题: 现在有n个数,设计算法找出前k大的数(k<n)
主要应用: 排行榜
解决思路:
取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数。
依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整。
遍历列表所有元素后,倒序弹出堆顶。
归并排序
假设现在的列表分两段有序,如何将其合成为一个有序列表
这种操作称为一次归并。
分解:将列表越分越小,直至分成一个元素。
终止条件:一个元素是有序的。
合并:将两个有序列表归并,列表越来越大。如下图:
import random from timewrap import * import copy import sys 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) print(li[low:high + 1]) def merge_sort(li): return _merge_sort(li, 0, len(li) - 1) li = list(range(16)) random.shuffle(li) print(li) merge_sort(li) print(li)
归并排序
时间复杂度:O(nlogn)
空间复杂度:O(n)
三种排序算法的时间复杂度都是O(nlogn)
一般情况下,就运行时间而言: 快速排序 < 归并排序 < 堆排序
三种排序算法的缺点: 快速排序:极端情况下排序效率低
归并排序:需要额外的内存开销
堆排序:在快的排序算法中相对较慢
不常用排序
希尔排序
希尔排序是一种分组插入排序算法
首先取一个整数d1=n/2,将元素分为d1个组,每组相邻量元素之间距离为d1,在各组内进行直接插入排序;
取第二个整数d2=d1/2,重复上述分组排序过程,直到di=1,即所有元素在同一组内进行直接插入排序。
希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟排序使得所有数据有序。
from cal_time import cal_time def insert_sort_gap(li, d): for i in range(d, len(li)): # i表示摸到的牌的位置 tmp = li[i] j = i - d # j是当前手里用来比较的牌 while j >= 0 and li[j] > tmp: li[j + d] = li[j] # 往后移 j -= d # 往前看 # 如果手里用来比较的牌比摸到的牌小或者j==-1 li[j + d] = tmp # 放牌 @cal_time def shell_sort(li): d = len(li) // 2 while d > 0: insert_sort_gap(li, d) d = d // 2 import random li = list(range(10000)) random.shuffle(li)
希尔排序
时间复杂度 n(logn)
运行时间0.11秒
计数
现在有一个列表,已知列表中的数范围都在0到100之间。设计算法在O(n)时间复杂度内将列表进行排序。
创建一个列表,用来统计每个数出现的次数。
import copy from timewrap import * @cal_time def count_sort(li, max_num = 100): count = [0 for i in range(max_num+1)] for num in li: count[num]+=1 li.clear() for i, val in enumerate(count): for _ in range(val): li.append(i) @cal_time def sys_sort(li): li.sort() li = [random.randint(0,100) for i in range(100000)] li1 = copy.deepcopy(li) count_sort(li) sys_sort(li1)
计数排序
运行时间0.035秒
桶排序
在计数排序中,如果元素的范围比较大(比如在1到1亿之间),如何改造算法? 桶排序(Bucket Sort):首先将元素分在不同的桶中,在对每个桶中的元素排序。
桶排序的表现取决于数据的分布。也就是需要对不同数据排序时采取不同的分桶策略。
平均情况时间复杂度:O(n+k)
最坏情况时间复杂度:O(n2k)
空间复杂度:O(nk)
import random from timewrap import * def list_to_buckets(li, iteration): """ :param li: 列表 :param iteration: 装桶是第几次迭代 :return: """ buckets = [[] for _ in range(10)] for num in li: digit = (num // (10 ** iteration)) % 10 buckets[digit].append(num) return buckets def buckets_to_list(buckets): return [num for bucket in buckets for num in bucket] # li = [] # for bucket in buckets: # for num in bucket: # li.append(num) @cal_time def radix_sort(li): maxval = max(li) # 10000 it = 0 while 10 ** it <= maxval: li = buckets_to_list(list_to_buckets(li, it)) it += 1 return li li = [random.randint(0,1000) for _ in range(100000)] radix_sort(li)
桶排序
运行时间0.33秒
基数排序
多关键字排序:加入现在有一个员工表,要求按照薪资排序,薪资相同的员工按照年龄排序。
先按照年龄进行排序,再按照薪资进行稳定的排序。
时间复杂度:O(kn)
空间复杂度:O(k+n)
(k表示数字位数)
最新评论