一、算法介绍
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 = , sum = , n = ; /* 执行1次 */
for( i = ; i <= n; i++)
{
sum = sum + i; /* 执行n次 */
for( j = ; 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): # 当前需遍历的次数
min_loc = i # 当前最小数的位置
for j in range(i + 1, len(lst)): # 无序区
if lst[j] < lst[min_loc]: # 如果有更小的数
lst[min_loc], lst[j] = lst[j], lst[min_loc] # 把最小的数交换到当前最小数的位置(索引) 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开始遍历表示无序区从1开始,有序区初始有一个值
tmp = lst[i] # tmp表示拿到的无序区的第一张牌
j = i - 1 # j表示有序区的最后一个值
while j >= 0 and lst[j] > tmp: # 当有序区有值,并且有序区的值比无序区拿到的值大就一直循环
lst[j + 1] = lst[j] # 有序区的值往后移
j -= 1 # 找到上一个有序区的值,然后再循环
lst[j + 1] = tmp # 跳出循环之后,只有j+1的位置是空的,要把当下无序区的值放到j+1的位置 lst = [1, 2, 44, 3, 5]
insert_sort(lst)
print(lst)
4、快速排序
思路:取第一个元素,让它归位,就是放到一个位置,使它左边的都比它小,右边的都比它大,然后递归完成排序
时间复杂度:O(nlog(n))
import sys
sys.setrecursionlimit(100000) # 设置默认递归次数 def partition(lst, left, right):
tmp = lst[left] # 找一个基准
while left < right:
# 从右边开始向左边遍历,大于基准的数不动,查找小于基准数的数赋值到左边
while left < right and lst[right] >= tmp:
right -= 1
lst[left] = lst[right] # 找到小于基准的数,赋值到左边 # 从左边开始向右边遍历,小于基准的数不动,查找大于基准数的数赋值到左边
while left < right and lst[left] <= tmp:
left += 1
lst[right] = lst[left] # 找到大于基准的数,赋值到右边 lst[left] = tmp # 归位的元素
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 = [5, 1, 6, 7, 7, 4, 2, 3, 6]
quick_sort(lst, 0, len(lst) - 1)
print(lst)
5、归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略,分是将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"归并"在一起,即分而治之。
def merge_sort(li):
# 不断递归调用自己一直到拆分成成单个元素的时候就返回这个元素,不再拆分了
if len(li) == 1:
return li # 取拆分的中间位置
mid = len(li) // 2
# 拆分过后左右两侧子串
left = li[:mid]
right = li[mid:] # 对拆分过后的左右再拆分 一直到只有一个元素为止
# 最后一次递归时候ll和lr都会接到一个元素的列表
# 最后一次递归之前的ll和rl会接收到排好序的子序列
ll = merge_sort(left)
rl = merge_sort(right) # 我们对返回的两个拆分结果进行排序后合并再返回正确顺序的子列表
# 这里我们调用拎一个函数帮助我们按顺序合并ll和lr
return merge(ll, rl) # 这里接收两个列表
def merge(left, right):
# 从两个有顺序的列表里边依次取数据比较后放入result
# 每次我们分别拿出两个列表中最小的数比较,把较小的放入result
result = []
while len(left) > 0 and len(right) > 0:
# 为了保持稳定性,当遇到相等的时候优先把左侧的数放进结果列表,因为left本来也是大数列中比较靠左的
if left[0] <= right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
# while循环出来之后 说明其中一个数组没有数据了,我们把另一个数组添加到结果数组后面
result += left
result += right
return result li = [1, 5, 2, 4, 7, 5, 3, 2, 1]
li2 = merge_sort(li)
print(li2)
6、堆排序
1.堆是一个完全二叉树
2.完全二叉树即是:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数(2层),第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
3.堆满足两个性质: 堆的每一个父节点数值都大于(或小于)其子节点,堆的每个左子树和右子树也是一个堆。
4.堆分为最小堆和最大堆。最大堆就是每个父节点的数值要大于孩子节点,最小堆就是每个父节点的数值要小于孩子节点。排序要求从小到大的话,我们需要建立最大堆,反之建立最小堆。
5.堆的存储一般用数组来实现。假如父节点的数组下标为i的话,那么其左右节点的下标分别为:(2*i+1)和 (2*i+2)。如果孩子节点的下标为j的话,那么其父节点的下标为(j-1)/2。
完全二叉树中,假如有n个元素,那么在堆中最后一个父节点位置为(n/2-1)。
def swap(a, b): # 将a,b交换
a, b = b, a
return a, b def sift_down(array, start, end):
"""
调整成大顶堆,初始堆时,从下往上;交换堆顶与堆尾后,从上往下调整
:param array: 列表的引用
:param start: 父结点
:param end: 结束的下标
:return: 无
"""
while True: # 当列表第一个是以下标0开始,结点下标为i,左孩子则为2*i+1,右孩子下标则为2*i+2;
# 若下标以1开始,左孩子则为2*i,右孩子则为2*i+1
left_child = 2 * start + 1 # 左孩子的结点下标
# 当结点的右孩子存在,且大于结点的左孩子时
if left_child > end:
break if left_child + 1 <= end and array[left_child + 1] > array[left_child]:
left_child += 1
if array[left_child] > array[start]: # 当左右孩子的最大值大于父结点时,则交换
array[left_child], array[start] = swap(array[left_child], array[start]) start = left_child # 交换之后以交换子结点为根的堆可能不是大顶堆,需重新调整
else: # 若父结点大于左右孩子,则退出循环
break print(">>", array) def heap_sort(array): # 堆排序
# 先初始化大顶堆
first = len(array) // 2 - 1 # 最后一个有孩子的节点(//表示取整的意思)
# 第一个结点的下标为0,很多博客&课本教材是从下标1开始,无所谓吧,你随意
for i in range(first, -1, -1): # 从最后一个有孩子的节点开始往上调整
print(array[i])
sift_down(array, i, len(array) - 1) # 初始化大顶堆 print("初始化大顶堆结果:", array)
# 交换堆顶与堆尾
for head_end in range(len(array) - 1, 0, -1): # start stop step
array[head_end], array[0] = swap(array[head_end], array[0]) # 交换堆顶与堆尾
sift_down(array, 0, head_end - 1) # 堆长度减一(head_end-1),再从上往下调整成大顶堆 array = [1, 1, 16, 7, 2, 3, 20, 3, 17, 8]
heap_sort(array)
print("堆排序最终结果:", array)
三、查找算法
1、二分查找
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
def search(lst, num):
left = 0
right = len(lst) - 1
while left <= right: # 循环条件
mid = (left + right) // 2 # 获取中间位置,数字的索引
if num < lst[mid]: # 如果查询数字比中间数字小,那就去中间数的左边找,
right = mid - 1 # 调整右边边界
elif num > lst[mid]: # 如果查询数字比中间数字大,那么就去中间数的右边找
left = mid + 1 # 调整左边边界
elif num == lst[mid]:
print('找到的数:%s, 索引是:%s' %(lst[mid], mid))
return mid # 如果查询数字刚好为中间值,返回该值得索引
print('没有找到该值')
return -1 # 如果循环结束,左边大于了右边,代表没有找到 lst = [1, 3, 4, 8, 11, 12, 13, 15, 17, 20, 21, 27, 42, 43, 49, 51, 52, 57, 58, 59, 62, 69, 71, 73, 74, 80, 83, 84, 89, 96, 100, 111]
search(lst, 1) # 找到的数:1, 索引是:0
search(lst, 51) # 找到的数:51, 索引是:15
search(lst, 96) # 找到的数:96, 索引是:29
search(lst, 120) # 没有找到该值
def search(lst, num, left=None, right=None):
left = left if left else 0
right = len(lst)-1 if right is None else right
mid = (right + left) // 2
if left > right: # 循环结束条件
print('没有找到该值')
return None
elif num < lst[mid]:
return search(lst, num, left, mid - 1)
elif num > lst[mid]:
return search(lst, num, mid + 1, right)
elif lst[mid] == num:
print('找到的数:%s, 索引是:%s' % (lst[mid], mid))
return mid lst = [1, 3, 4, 8, 11, 12, 13, 15, 17, 20, 21, 27, 42, 43, 49, 51, 52, 57, 58, 59, 62, 69, 71, 73, 74, 80, 83, 84, 89, 96, 100, 111]
search(lst, 1) # 找到的数:1, 索引是:0
search(lst, 51) # 找到的数:51, 索引是:15
search(lst, 96) # 找到的数:96, 索引是:29
search(lst, 120) # 没有找到该值