Python之基础算法介绍
一、算法介绍
1、 算法是什么
算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。
2、时间复杂度
在计算机科学中,算法的时间复杂度是一个函数,它定性描述了该算法的运行时间。这是一个关于代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度(O是数量级的符号 ),简称时间复杂度。
常见时间复杂度单位:效率从上到下变低,
O(1) 简单的一次运算(常数阶)
O(n) 一次循环(线性阶)
O(n^2) 两个循环(平方阶)
O(logn) 循环减半
O(nlogn) 一个循环加一个循环减半
O(n^2logn)
O(n^3)
一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法
O(1) 常数阶 < O(logn) 对数阶 < O(n) 线性阶 < O(nlogn) < O(n^2) 平方阶 < O(n^3) < { O(2^n) < O(n!) < O(n^n)
大O推导法:
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行函数中,只保留最高阶项
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数
比如:
这是一段C的代码 #include "stdio.h" int main() { int i, j, x = 0, sum = 0, n = 100; /* 执行1次 */ for( i = 1; i <= n; i++) { sum = sum + i; /* 执行n次 */ for( j = 1; j <= n; j++) { x++; /* 执行n*n次 */ sum = sum + x; /* 执行n*n此 */ } } printf("%d", sum); /* 执行1次 */ }
分析:
执行总次数 = 1 + n + n*n + n*n + 1 = 2n2 + n + 2
根据大O推导法:
1.用常数 1 取代运行时间中的所有加法常数:执行总次数为: 2n2 + n + 1
2.在修改后的运行次数函数中,只保留最高阶项,这里的最高阶是 n 的二次方: 执行总次数为: 2n2
3.如果最高阶项存在且不是 1 ,则去除与这个项相乘的常数,这里 n 的二次方不是 1 所以要去除这个项的相乘常数:执行总次数为: n2
因此最后我们得到上面那段代码的算法时间复杂度表示为: O(n2)
3、空间复杂度
空间复杂度是用来评估算法内存占用大小的单位
空间换时间:如果需要增快算法的速度,需要的空间会更大
二、python实现常见的排序算法
前三种比较LowB,后三种比较NB
前三种时间复杂度都是O(n^2),后三种时间复杂度都是O(nlog(n))
1、冒泡(交换)排序
原理:列表中两个相邻的数,如果前一个数比后一个数大,就做交换。一共需要遍历列表的次数是len(lst)-1
时间复杂度:O(n^2)
def bubble_sort(lst): for i in range(len(lst) - 1): # 这是需要循环遍历多少次 for j in range(len(lst) - 1 - i): # 每次数组中的无序区 if lst[j] > lst[j + 1]: lst[j], lst[j + 1] = lst[j + 1], lst[j] lst = [1, 2, 44, 3, 5] bubble_sort(lst) print(lst)
优化:如果在循环的时候,有一次没有进行交换,就表示数列中的数据已经是有序的
时间复杂度:最好情况是0(n),只遍历一次,一般情况和最坏情况都是O(n^2)
def bubble_sort(lst): for i in range(len(lst)-1): # 这是需要循环遍历多少次 change = False # 做一个标志变量 for j in range(len(lst)-1-i): # 每次数组中的无序区 if lst[j] >lst[j+1]: lst[j],lst[j+1] = lst[j+1],lst[j] change = True # 每次遍历,如果进来排序的话,就会改变change的值 if not change: # 如果change没有改变,那就表示当前的序列是有序的,直接跳出循环即可 return lst = [1, 2, 44, 3, 5] bubble_sort(lst) print(lst)
2、选择排序
原理:每次遍历找到当下数组最小的数,并把它放到第一个位置,下次遍历剩下的无序区,记录剩余列表中最小的数,继续放置
时间复杂度 O(n^2)
方法一:
def select_sort(lst): for i in range(len(lst) - 1): # 当前需遍历的次数 # i为当前选中的最小数的位置 for j in range(i + 1, len(lst)): # 无序区 if lst[j] < lst[i]: # 如果有更小的数 lst[i], lst[j] = lst[j], lst[i] # 把最小的数交换到当前最小数的位置(索引) lst = [1, 2, 44, 3, 5] select_sort(lst) print(lst)
方法二:
def select_sort(lst): for i in range(len(lst) - 1): # 当前需遍历的次数 min_loc = i # 当前最小数的位置 for j in range(i + 1, len(lst)): # 无序区 if lst[j] < lst[min_loc]: # 如果有更小的数 min_loc = j # 最小数的位置改变 if min_loc != i: lst[i], lst[min_loc] = lst[min_loc], lst[i] # 把最小数和无序区第一个数交换 lst = [1, 2, 44, 3, 5] select_sort(lst) print(lst)
3、插入排序
原理:列表分为有序区和无序区,有序区是一个相对有序的序列,就是说在有序区内,是已经排序好了的,最初有序区只有一个元素,每次从无序区选择一个值,插入到有序区,直到无序区为空
时间复杂度:O(n^2)
原理图
def insert_sort(lst): for i in range(1, len(lst)): # 无序区从1开始向前跟有序区比较、插入 (有序区初始有一个值) for j in range(i, 0, -1): # 如果无序区的值小于前一个元素,交换位置 if lst[j] < lst[j - 1]: lst[j], lst[j - 1] = lst[j - 1], lst[j] else: break lst = [12, 15, 9, 20, 6, 31, 24] insert_sort(lst) print(lst)
4、快速排序
思路:取第一个元素,让它归位,就是放到一个位置,使它左边的都比它小,右边的都比它大,然后递归完成排序
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据, 然后将所有比它小的数都放到它左边,所有比它大的数都放到它右边,这个过程称为一趟快速排序。 值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。 一趟快速排序的算法是: 1)设置两个变量i、j,排序开始的时候:i=0,j=N-1 2)以第一个数组元素作为关键数据,赋值给key,即key=A[0] 3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]的值交换 4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换 5)重复第3、4步,直到i==j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值, 使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
假设一开始序列{xi}是:5,3,7,6,4,1,0,2,9,10,8。 此时,ref=5,i=1,j=11,从后往前找,第一个比5小的数是x8=2,因此序列为:[2],3,7,6,4,1,0,[5],9,10,8。# 这里的中括号不是代表数组,是我用来提示交换了的数据 此时i=1,j=8,从前往后找,第一个比5大的数是x3=7,因此序列为:2,3,[5],6,4,1,0,[7],9,10,8。 此时,i=3,j=8,从第8位往前找,第一个比5小的数是x7=0,因此:2,3,[0],6,4,1,[5],7,9,10,8。 此时,i=3,j=7,从第3位往后找,第一个比5大的数是x4=6,因此:2,3,0,[5],4,1,[6],7,9,10,8。 此时,i=4,j=7,从第7位往前找,第一个比5小的数是x6=1,因此:2,3,0,[1],4,[5],6,7,9,10,8。 此时,i=4,j=6,从第4位往后找,直到第6位才有比5大的数,这时,i=j=6,ref成为一条分界线,它之前的数都比它小,之后的数都比它大,对于前后两部分数,可以采用同样的方法来排序。
时间复杂度:O(nlog(n))
方法一:
import sys sys.setrecursionlimit(100000) # 设置默认递归次数 def partition(lst, left, right): base = lst[left] # 找一个基准 while left < right: # 从右边开始向左边遍历,大于基准的数不动,查找小于基准数的数 while left < right and lst[right] >= base: right -= 1 lst[left], lst[right] = lst[right], lst[left] # 找到小于基准的数,交换到左边 # 从左边开始向右边遍历,小于基准的数不动,查找大于基准数的数 while left < right and lst[left] < base: left += 1 lst[right], lst[left] = lst[left], lst[right] # 找到大于基准的数,交换到右边 return left # 返回right也行,都是中间值 def quick_sort(lst, left, right): if left < right: mid = partition(lst, left, right) quick_sort(lst, left, mid - 1) quick_sort(lst, mid + 1, right) lst = [2, 3, 5, 7, 1, 4, 6, 15, 5, 2, 7, 9, 10, 15, 9, 17, 12] quick_sort(lst, 0, len(lst) - 1) # 根据索引去快排 print(lst)
方法二:
def quick_sort(lst): """快速排序""" if len(lst) < 2: # 递归出口 return lst base = lst[0] # 选取基准值,可以选取第一个或最后一个元素 left, right = [], [] # 定义基准值左右两侧的列表 lst.remove(base) # 从原始数组中移除基准值 for num in lst: if num >= base: right.append(num) else: left.append(num) return quick_sort(left) + [base] + quick_sort(right) # 示例: array = [2, 3, 5, 7, 1, 4, 6, 15, 5, 2, 7, 9, 10, 15, 9, 17, 12] print(quick_sort(array))
5、归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略,分是将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"归并"在一起,即分而治之。
分治的原理:
- 将一个序列从中间位置分成两个序列
- 再把这两个子序列按照第一步继续二分下去
- 直到所有子序列的长度都为1,也就是不可以再二分为止。这时候再两两合并成一个有序序列即可。
排序原理:
- 第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针超出序列尾
- 将另一序列剩下的所有元素直接复制到合并序列
图解示例一
图解示例二
def merge_sort(lst): # 不断递归调用自己一直到拆分成成单个元素的时候就返回这个元素,不再拆分了 if len(lst) <= 1: return lst # 取拆分的中间位置 mid = len(lst) // 2 # 对拆分过后的左右再拆分 一直到只有一个元素为止 # 最后一次递归时候left和right都会接到一个元素的列表 # 最后一次递归之前的left和right会接收到排好序的子序列 left = merge_sort(lst[:mid]) right = merge_sort(lst[mid:]) # 我们对返回的两个拆分结果进行排序后合并再返回正确顺序的子列表 return merge(left, right) # 这里接收两个列表 def merge(left, right): # 从两个有顺序的列表里边依次取数据比较后放入result result = [] # 每次我们分别拿出两个列表中最小的数比较,把较小的放入result l, r = 0, 0 # 左右两个列表当前需要比较的最小数值的索引 while l < len(left) and r < len(right): # 为了保持稳定性,当遇到相等的时候优先把左侧的数放进结果列表,因为left本来也是大数列中比较靠左的 if left[l] <= right[r]: result.append(left[l]) l += 1 else: result.append(right[r]) r += 1 # while循环出来之后 说明其中一个数组已经比较完了,我们把另一个数组添加到结果数组后面 # 这里的 left[l:] right[r:] 其中一个必为空 result += left[l:] result += right[r:] return result li = [1, 5, 2, 4, 7, 5, 3, 11, 2, 1] li2 = merge_sort(li) print(li2)
6、堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。
堆是具有以下性质的完全二叉树:
- 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆
- 或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
按照堆的特点可以把堆分为大顶堆和小顶堆:
- 大顶堆:每个节点的值都大于或等于其左右孩子节点的值
- 小顶堆:每个节点的值都小于或等于其左右孩子节点的值
用简单的公式来描述一下堆的定义就是:
- 大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
- 小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序的基本思想:
- 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
- 将其与末尾元素进行交换,此时末尾就为最大值。
- 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值,
- 如此反复执行,便能得到一个有序序列了,建立最大堆时是从最后一个非叶子节点开始从下往上调整的
排序要求从小到大的话,我们需要建立最大堆,反之建立最小堆。
为什么升序要用大顶堆?
大顶堆的特点:
- 每个节点的值都大于或等于其左右孩子节点的值,我们把大顶堆构建完毕后根节点的值一定是最大的,
- 然后把根节点的和最后一个元素(也可以说最后一个节点)交换位置,那么末尾元素此时就是最大元素了
升序的排列:
- 第一步:先n个元素的无序序列,构建成大顶堆
- 第二步:将根节点与最后一个元素交换位置,(将最大元素"沉"到数组末端)
- 第三步:交换过后可能不再满足大顶堆的条件,所以需要将剩下的n-1个元素重新构建成大顶堆
- 第四步:重复第二步、第三步直到整个数组排序完成
堆的这种特性非常的有用,堆常常被当做优先队列使用,因为可以快速的访问到"最重要"的元素
图解交换过程(得到升序序列,使用大顶堆来调整)
假设数组array共有n个元素,堆排序步骤如下:
初始化最大堆:从下到上、从右到左
1. 根节点与[左右子节点的最大值]进行比较
2. 如果根节点大于[左右子节点的最大值],则不需要调整
3. 如果根节点小于[左右子节点的最大值],则把根节点的值与[左右子节点的最大值]交换
4. 子节点被交换后,记得再次检查以[被交换的子节点]为根节点的树,是否为最大堆
5. 若是,则无需处理,若不是则需要重新调整(意思就是,只要子节点的值被换了,那么以该的子节点为根节点的树可能不是最大堆,顺着下去检查)
6. [被交换的子节点]处理完毕后,像上继续检查,重复1、2、3、4、5步骤,直到初始化完成最大堆。
最大堆初始化完成后,开始排序,从上到下
1. 将最大堆的最上面的根(最大值)"沉"到最下(与最后一位元素交换值),此时就已经归位一个最大值在数组最后了
2. 已经归位的元素不去动,将剩余的(n-1)个元素去构造最大堆,此时从上到下构造,类似于初始化的[步骤5]
3. 得到(n-1)个元素的最大堆,然后循环步骤1、2直到所有元素归位完毕
代码实现
def big_endian(arr, start, end): """调整成大顶堆,初始堆时,从下往上;交换堆顶与堆尾后,从上往下调整""" root = start child = root * 2 + 1 # 左孩子 # 孩子比最后一个节点还大,也就意味着最后一个叶子节点了,就得跳出去一次循环,已经调整完毕 while child <= end: # 先将左孩子与右孩子比较,拿到两者的最大值,再跟父节点比较,这样就能得到三者间的最大值 if child + 1 <= end and arr[child] < arr[child + 1]: child += 1 # child + 1 --> 右孩子 # 父节点小于子节点直接交换值 if arr[root] < arr[child]: arr[root], arr[child] = arr[child], arr[root] # 交换之后,以交换后的子结点为根的堆可能不是大顶堆,需重新调整 root = child child = root * 2 + 1 else: break def heap_sort(arr): # 无序区大根堆排序 first = len(arr) // 2 - 1 # 最后一个有孩子的节点(//表示取整的意思) # 先初始化大顶堆 for start in range(first, -1, -1): # 从下到上,从右到左对每个节点进行调整,循环得到非叶子节点 big_endian(arr, start, len(arr) - 1) # 去调整所有的节点 # 交换堆顶与堆尾 for end in range(len(arr) - 1, 0, -1): arr[0], arr[end] = arr[end], arr[0] # 顶部尾部互换位置 big_endian(arr, 0, end - 1) # 堆长度减一(end-1),再从上往下调整成大顶堆 return arr l = [3, 1, 4, 9, 6, 7, 5, 8, 2, 10] print(heap_sort(l))