一、算法
1. 什么是算法
算法就是为了解决某一个问题而采取的具体有效的操作步骤
算法的复杂度,表示代码的运行效率,用一个大写的O加括号来表示,比如O(1),O(n)
认为算法的复杂度是渐进的,即对于一个大小为n的输入,如果他的运算时间为n3+5n+9,那么他的渐进时间复杂度是n3
mark:我们可以把所有的算法想象为一本“菜谱”,特定的算法比如菜谱中的的一道“老醋花生米”的制作流程,只要按照菜谱的要求制作老醋花生米,那么谁都可以做出一道好吃的老醋花生米。so,这个做菜的步骤就可以理解为:“解决问题的步骤”
2. 算法的意义:
假设计算机无限快,并且计算机存储容器是免费的,我们还需要各种乱七八糟的算法吗?如果计算机无限快,那么对于某一个问题来说,任何一个都可以解决他的正确方法都可以的!
当然,计算机可以做到很快,但是不能做到无限快,存储也可以很便宜但是不能做到免费。
那么问题就来了。
效率:解决同一个问题的各种不同算法的效率常常相差非常大,这种效率上的差距的影响往往比硬件和软件方面的差距还要大。
3. 如何选择算法:
第一首先要保证算法的正确性
一个算法对其每一个输入的实例,都能输出正确的结果并停止,则称它是正确的,我们说一个正确的算法解决了给定的计算问题。不正确的算法对于某些输入来说,可能根本不会停止,或者停止时给出的不是预期的结果。然而,与人们对不正确算法的看法想反,如果这些算法的错误率可以得到控制的话,它们有时候也是有用的。但是一般而言,我们还是仅关注正确的算法!
第二分析算法的时间复杂度
算法的时间复杂度反映了程序执行时间随输入规模增长而增长的量级,在很大程度上能很好反映出算法的好坏。
二、时间复杂度
1、什么是时间复杂度
一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
2、时间复杂度的计算方法
一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试因为该方法有两个缺陷:
- 想要对设计的算法的运行性能进行测评,必须先依据算法编写相应的程序并实际运行。
- 所得时间的统计计算依赖于计算机的硬件、软件等环境因素,有时候容易掩盖算法的本身优势。
所以只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。
一般情况下,算法的基本操作重复执行的次数是模块n的某一个函数f(n),因此,算法的时间复杂度记做:T(n)=O(f(n))。随着模块n的增大,算法执行的时间的增长率和f(n)的增长率成正比,所以f(n)越小,算法的时间复杂度越低,算法的效率越高。
在计算时间复杂度的时候,先找出算法的基本操作,然后根据相应的各语句确定它的执行次数,再找出T(n)的同数量级(它的同数量级有以下:1,Log2n ,n ,nLog2n ,n的平方,n的三次方,2的n次方,n!),找出后,f(n)=该数量级,若T(n)/f(n)求极限可得到一常数c,则时间复杂度T(n)=O(f(n))。
3、常见的时间复杂度
常见的算法时间复杂度由小到大依次为:
Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<…<Ο(2n)<Ο(n!)
求解算法的时间复杂度的具体步骤:
- 找出算法中的基本语句,算法中执行最多的那条语句是基本语句,通常是最内层循环的循环体。
- 计算基本语句的执行次数的量级,保证最高次幂正确即可查看他的增长率。
- 用大O几号表示算法的时间性能
如果算法中包含镶套的循环,则基本语句通常是最内层的循环体,如果算法中包并列的循环,则将并列的循环时间复杂度相加,例如:
#!/usr/bin/env python #-*- coding:utf-8 -*- __author__ = 'luotianshuai' n = 100 for i in range(n): print(i) for i in range(n): ##每循i里的一个元素,for循环内部嵌套的for循环就整个循环一次 for q in range(n): print(q)
第一个for循环的时间复杂度为Ο(n),第二个for循环的时间复杂度为Ο(n2),则整个算法的时间复杂度为Ο(n+n2)=Ο(n2)。
Ο(1)表示基本语句的执行次数是一个常数,一般来说,只要算法中不存在循环语句,其时间复杂度就是Ο(1)。
其中Ο(log2n)、Ο(n)、 Ο(nlog2n)、Ο(n2)和Ο(n3)称为多项式时间,而Ο(2n)和Ο(n!)称为指数时间,计算机科学家普遍认为前者(即多项式时间复杂度的算法)是有效算法,把这类问题称为P(Polynomial,多项式)类问题,而把后者(即指数时间复杂度的算法)称为NP(Non-Deterministic Polynomial, 非确定多项式)问题。在选择算法的时候,优先选择前者!
OK我懂对于没有算法基础的同学,看起算法来也很头疼,但是这个是基础和重点,不会算法的开发不是一个合格的开发并且包括语言记得基础也是需要好好整理的!加油吧~~ 咱们在一起看下时间复杂度的详细说明吧
三、常见的时间复杂度示例
1、O(1)
#O(1) n = 100 sum = (1+n) * n/2 #执行一次 sum_1 = (n/2) - 10 #执行一次 sum_2 = n*4 - 10 + 8 /2 #执行一次
这个算法的运行次数函数是f(n)=3。根据我们推导大O阶的方法,第一步就是把常数项3改为1。在保留最高阶项时发现,它根本没有最高阶项,所以这个算法的时间复杂度为O(1)。
并且:如果算法的执行时间不随着问题规模n的增长而增加,及时算法中有上千条语句,其执行的时间也不过是一个较大的常数。此类算法的时间复杂度记作O(1)
2、O(n2)
n = 100 for i in range(n): #执行了n次 for q in range(n): #执行了n2 print(q) #执行了n2
解:T(n)=2n2+n+1 =O(n2)
一般情况下,对进循环语句只需考虑循环体中语句的执行次数,忽略该语句中步长加1、终值判别、控制转移等成分,当有若干个循环语句时,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度f(n)决定的。
3、O(n)
#O(n) n =100 a = 0 #执行一次 b = 1#执行一次 for i in range(n): #执行n次 s = a +b #执行n-1次 b =a #执行n-1次 a =s #执行n-1次
解:T(n)=2+n+3(n-1)=4n-1=O(n)
4、Ο(n3)
#O(n3) n = 100 for i in range(n):#执行了n次 for q in range(n):#执行了n^2 for e in range(n):#执行了n^3 print(e)#执行了n^3
简单点来去最大值是:Ο(n3)
5、常用的算法的时间复杂度和空间复杂度
排序法 | 平均时间 | 最差情况 | 稳定度 | 额外空间 | 备注 |
冒泡排序 | Ο(n2) | Ο(n2) | 稳定 | O(1) | n小时较好 |
交换排序 | Ο(n2) | Ο(n2) | 不稳定 | O(1) | n小时较好 |
选择排序 | Ο(n2) | Ο(n2) | 不稳定 | O(1) | n小时较好 |
插入排序 | Ο(n2) | Ο(n2) | 稳定 | O(1) | 大部分已排序时较好 |
快速排序 | Ο(nlogn) | Ο(n2) | 不稳定 | Ο(nlogn) | n较大时较好 |
希尔排序(SHELL) | Ο(log2n) | Ο(ns) 1<s<2 | 不稳定 | O(1) | s是所选分组 |
归并排序 | Ο(log2n) | Ο(log2n) | 稳定 | O(1) | n大时较好 |
堆排序 | Ο(log2n) | Ο(log2n) | 不稳定 | O(1) | n大时较好 |
基数排序 | Ο(logRB) | Ο(logRB) | 稳定 | O(N) | B是真数(0-9) R是基数(个十百) |
四、递归
递归就是在函数中调用本身,大多数情况下,这会给计算机增加压力,但是有时又很有用,比如下面的例子:
汉诺塔游戏
把A柱的盘子,移动到C柱上,最少需要移动几次,大盘子只能在小盘子下面
递归实现:
def hanoi(x, a, b, c): # 所有的盘子从 a 移到 c if x > 0: hanoi(x-1, a, c, b) # step1:除了下面最大的,剩余的盘子 从 a 移到 b print('%s->%s' % (a, c)) # step2:最大的盘子从 a 移到 c hanoi(x-1, b, a, c) # step3: 把剩余的盘子 从 b 移到 c hanoi(10, 'A', 'B', 'C') #计算次数 def h(x): num = 1 for i in range(x-1): num = 2*num +1 print(num) h(10)
用递归打印斐波那契数列
def fei(n): if n == 0: return 0 elif n == 1: return 1 else: return fei(n-1)+fei(n-2)
你会发现,即使n只有几十的时候,你的计算机内存使用量已经飙升了
其实,如果结合生成器,你会发现不管n有多大,都不会出现卡顿,但这是生成器的特性,本篇博客不重点介绍
# 结合生成器 def fei(n): pre,cur = 0,1 while n >=0: yield pre n -= 1 pre,cur = cur,pre+cur for i in fei(400000): print(i)
台阶问题
有n个台阶,可以一次走上1个阶台,2个台阶,3个台阶。请问n个台阶,有几种走法。
def func(n): if n == 1: return 1 elif n == 2: return 2 elif n == 3: return 3 return func(n - 1) + func(n - 2) + func(n - 3)
关于递归次数,Python中有个限制,可以通过sys模块来修改
import sys sys.setrecursionlimit(1000000)
五、查找
1.顺序查找
这个没的说,就是for循环呗,时间复杂度O(n)
def linear_search(data_set, value): for i in range(len(data_set)): if data_set[i] == value: return i return
2.二分查找(大前提是要查找的数据是有序的)
时间复杂度O(logn)
就是一半一半的查找,看目标值在左边一半还是右边一半,然后替换左端点或者右端点,继续判断
1 import time 2 def cal_time(func): #装饰器 3 def wrapper(*args, **kwargs): 4 t1 = time.time() 5 result = func(*args, **kwargs) 6 t2 = time.time() 7 print("%-20s cost time: %f" % (func.__name__, t2 - t1)) 8 return result 9 return wrapper 10 11 @cal_time 12 def bin_search(data_set,val): 13 #low 和high代表下标 最小下标,最大下标 14 low=0 15 high=len(data_set)-1 16 while low <=high:# 只有当low小于High的时候证明中间有数 17 mid=(low+high)//2 18 if data_set[mid]==val: 19 return mid #返回他的下标 20 elif data_set[mid]>val: 21 high=mid-1 22 else: 23 low=mid+1 24 return # return null证明没有找到 25 data_set = list(range(100000000)) 26 print(bin_search(data_set, 999999))
非递归版本:
def binary_serach(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 else: return None
递归版本的二分查找
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 None
六、排序实例
排序算法是在更复杂的算法中的是一个构建基础,所以先看下常用的排序。
速度慢的三个:
1.冒泡排序
需求:
请按照从小到大对列表,进行排序==》:[69, 471, 106, 66, 149, 983, 160, 57, 792, 489, 764, 589, 909, 535, 972, 188, 866, 56, 243, 619]
思路:相邻两个值进行比较,将较大的值放在右侧,依次比较!
原理图:
原理分析:
列表中有5个元素两两进行比较,如果左边的值比右边的值大,就用中间值进行循环替换!
既然这样,我们还可以用一个循环把上面的循环进行在次循环,用表达式构造出内部循环!
代码实现:
#!/usr/bin/env python #-*- coding:utf-8 -*- __author__ = 'luotianshuai' import random maopao_list = [13, 22, 6, 99, 11] ''' 原理分析: 列表中有5个元素两两进行比较,如果左边的值比右边的值大,就用中间值进行循环替换! 既然这样,我们还可以用一个循环把上面的循环进行在次循环,用表达式构造出内部循环! ''' def handler(array): for i in range(len(array)): for j in range(len(array)-1-i): ''' 这里为什么要减1,我们看下如果里面有5个元素我们需要循环几次?最后一个值和谁对比呢?对吧!所以需要减1 这里为什么减i?,这个i是循环的下标,如果我们循环了一次之后最后一只值已经是最大的了还有必要再进行一次对比吗?没有必要~ ''' print('left:%d' % array[j],'right:%d' % array[j+1]) if array[j] > array[j+1]: tmp = array[j] array[j] = array[j+1] array[j+1] = tmp if __name__ == '__main__': handler(maopao_list) print(maopao_list)
时间复杂度说明看下他的代码复杂度会随着N的增大而成指数型增长,并且根据判断他时间复杂度为Ο(n2)
原理就是,列表相邻的两个数,如果前边的比后边的小,那么交换顺序,经过一次排序后,最大的数就到了列表最前面
代码:
def bubble_sort(li): for j in range(len(li)-1): for i in range(1, len(li)): if li[i] > li[i-1]: li[i], li[i-1] = li[i-1], li[i] return li
冒泡排序的最差情况,即每次都交互顺序的情况,时间复杂度是O(n2)
存在一个最好情况就是列表本来就是排好序的,所以可以加一个优化,加一个标志位,如果没有出现交换顺序的情况,那就直接return
经过实际测试,发现经过优化过的版本要比没优化过的版本执行速度更快,有时随着数据源数量级增长,速度会成指数倍提高。
# 优化版本的冒泡 def bubble_sort_opt(li): for j in range(len(li)-1): flag = False for i in range(1, len(li)): if li[i] > li[i-1]: li[i], li[i-1] = li[i-1], li[i] flag = True if not flag: return li return li
2.插入排序
需求:
请按照从小到大对列表,进行排序==》:[69, 471, 106, 66, 149, 983, 160, 57, 792, 489, 764, 589, 909, 535, 972, 188, 866, 56, 243, 619]
思路:
一个列表默认分为左侧为排序好的,我们拿第一个元素举例,他左边的全是排序好的,他右侧是没有排序好的,如果右侧的元素小于左侧排序好的列表的元素就把他插入到合适的位置
原理图:
代码实现:
#!/usr/bin/env python #-*- coding:utf-8 -*- __author__ = 'luotianshuai' import random import time chaoru_list = [69, 471, 106, 66, 149, 983, 160, 57, 792, 489, 764, 589, 909, 535, 972, 188, 866, 56, 243, 619] def handler(array): for i in range(1,len(array)): position = i #刚开始往左边走的第一个位置 current_val = array[i] #先把当前值存下来 while position > 0 and current_val < array[position -1]: ''' 这里为什么用while循环,咱们在判断左边的值得时候知道他有多少个值吗?不知道,所以用while循环 什么时候停下来呢?当左边没有值得时候,或者当他大于左边的值得时候! ''' array[position] = array[position - 1] #如果whille条件成立把当前的值替换为他上一个值 ''' 比如一个列表: [3,2,4,1] 现在循环到 1了,他前面的元素已经循环完了 [2,3,4] 1 首先我们记录下当前这个position的值 = 1 [2,3,4,4] 这样,就出一个位置了 在对比前面的3,1比3小 [2,3,3,4] 在替换一下他们的值 在对比2 [2,2,3,4] 最后while不执行了在进行替换'array[position] = current_val #把值替换' ''' position -= 1 #当上面的条件都不成立的时候{左边没有值/左边的值不比自己的值小} array[position] = current_val #把值替换 if __name__ == '__main__': handler(chaoru_list) print(chaoru_list) ''' array = []#[69, 471, 106, 66, 149, 983, 160, 57, 792, 489, 764, 589, 909, 535, 972, 188, 866, 56, 243, 619] old_time = time.time() for i in range(50000): array.append(random.randrange(1000000)) handler(array) print(array) print('Cost time is :',time.time() - old_time) '''
原理:把列表分为有序区和无序区两个部分。最初有序区只有一个元素。然后每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。
1 def insert_sort(li): 2 for i in range(1,len(li)): 3 tmp = li[i] 4 j = i - 1 5 while j >= 0 and tmp < li[j]: # 找到一个合适的位置插进去 6 li[j+1] = li[j] 7 j -= 1 8 li[j+1] = tmp 9 return li
时间复杂度是O(n2)
3.选择排序
需求:
请按照从小到大对列表,进行排序==》:[69, 471, 106, 66, 149, 983, 160, 57, 792, 489, 764, 589, 909, 535, 972, 188, 866, 56, 243, 619]
思路:
第一次,从列表最左边开始元素为array[0],往右循环,从右边元素中找到小于array[0]的元素进行交换,直到右边循环完之后。
第二次,左边第一个元素现在是最小的了,就从array[1],和剩下的array[1:-1]内进行对比,依次进行对比!
对比:
他和冒泡排序的区别就是,冒泡排序是相邻的两两做对比,但是选择排序是左侧的“对比元素”和右侧的列表内值做对比!
原理图:
代码实现:
#!/usr/bin/env python #-*- coding:utf-8 -*- __author__ = 'luotianshuai' xuanze_list = [13, 22, 6, 99, 11] print(range(len(xuanze_list))) def handler(array): for i in range(len(array)): ''' 循环整个列表 ''' for j in range(i,len(array)): ''' 这里的小循环里,循环也是整个列表但是他的起始值是i,当这一个小循环完了之后最前面的肯定是已经排序好的 第二次的时候这个值是循环的第几次的值比如第二次是1,那么循环的起始值就是array[1] ''' if array[i] > array[j]: temp = array[i] array[i] = array[j] array[j] = temp # print(array) if __name__ == '__main__': handler(xuanze_list) print(xuanze_list)
选择排序代码优化:
#!/usr/bin/env python #-*- coding:utf-8 -*- __author__ = 'luotianshuai' import random import time def handler(array): for i in range(len(array)): smallest_index = i #假设默认第一个值最小 for j in range(i,len(array)): if array[smallest_index] > array[j]: smallest_index = j #如果找到更小的,记录更小元素的下标 ''' 小的循环结束后在交换,这样整个小循环就之前的选择排序来说,少了很多的替换过程,就只替换了一次!提升了速度 ''' tmp = array[i] array[i] = array[smallest_index] array[smallest_index] = tmp if __name__ == '__main__': array = [] old_time = time.time() for i in range(50000): array.append(random.randrange(1000000)) handler(array) print(array) print('Cost time is :',time.time() - old_time)
原理:遍历列表一遍,拿到最小的值放到列表第一个位置,再找到剩余列表中最小的值,放到第二个位置。。。。
def select_sort(li): for i in range(len(li)-1): min_loc = i # 假设当前最小的值的索引就是i for j in range(i+1,len(li)): if li[j] < li[min_loc]: min_loc = j if min_loc != i: # min_loc 值如果发生过交换,表示最小的值的下标不是i,而是min_loc li[i],li[min_loc] = li[min_loc],li[i] return li
时间复杂度是O(n2)
速度快的几种排序:
4.快速排序(快排)
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动.他的时间复杂度是:O(nlogn) ~Ο(n2)
排序示例:
假设用户输入了如下数组:
创建变量i=0(指向第一个数据)[i所在位置红色小旗子], j=5(指向最后一个数据)[j所在位置蓝色小旗子], k=6(赋值为第一个数据的值)。
i=0 j=3 k=6
i=2 j=3 k=6
![](https://i-blog.csdnimg.cn/blog_migrate/dcbc0421e4117d31644d7abebb8b46f5.png)
如果i和j没有碰头的话,就递加i找大的,还没有,就再递减j找小的,如此反复,不断循环。注意判断和寻找是同时进行的。
代码实现:
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author:luotianshuai import random import time def quick_sort(array,start,end): if start >= end: return k = array[start] left_flag = start right_flag = end while left_flag < right_flag: ''' left_flag = start 默认为0 right_flag = end 默认为传来的列表总长度 当left_flag 小与right_flag的时候成立,说明左右两边的小旗子还没有碰头(为相同的值) ''' #右边旗子 while left_flag < right_flag and array[right_flag] > k:#代表要继续往左一移动小旗子 right_flag -= 1 ''' 如果上面的循环停止说明找到右边比左边的值小的数了,需要进行替换 ''' tmp = array[left_flag] array[left_flag] = array[right_flag] array[right_flag] = tmp #左边旗子 while left_flag < right_flag and array[left_flag] <= k: #如果没有找到比当前的值大的,left_flag 就+=1 left_flag += 1 ''' 如果上面的循环停止说明找到当前段左边比右边大的值,进行替换 ''' tmp = array[left_flag] array[left_flag] = array[right_flag] array[right_flag] = tmp #进行递归把问题分半 quick_sort(array,start,left_flag-1) quick_sort(array,left_flag+1,end) if __name__ == '__main__': array = [] # [69, 471, 106, 66, 149, 983, 160, 57, 792, 489, 764, 589, 909, 535, 972, 188, 866, 56, 243, 619] start_time = time.time() for i in range(50000): array.append(random.randrange(1000000)) quick_sort(array,0,len(array)-1) end_time = time.time() print(array) print(start_time,end_time) cost_time = end_time - start_time print('Cost time is :%d' % cost_time)
原理:让指定的元素归位,所谓归位,就是放到他应该放的位置(左边的元素比他小,右边的元素比他大),然后对每个元素归位,就完成了排序
可以参考这个动图来理解下面的代码
代码:
# 归位函数 def partition(data, left, right): # 左右分别指向两端的元素 tmp = data[left] # 把左边第一个元素赋值给tmp,此时left指向空 while left < right: # 左右两个指针不重合,就继续 while left < right and data[right] >= tmp: # right指向的元素大于tmp,则不交换 right -= 1 # right 向左移动一位 data[left] = data[right] # 如果right指向的元素小于tmp,就放到左边现在为空的位置 while left < right and data[left] <= tmp: # 如果left指向的元素小于tmp,则不交换 left += 1 # left向右移动一位 data[right] = data[left] # 如果left指向的元素大于tmp,就交换到右边 data[left] = tmp # 最后把最开始拿出来的那个值,放到左右重合的那个位置 return left # 最后返回这个位置 # 写好归位函数后,就可以递归调用这个函数,实现排序 def quick_sort(data, left, right): if left < right: mid = partition(data, left, right) # 找到指定元素的位置 quick_sort(data, left, mid - 1) # 对左边元素排序 quick_sort(data, mid + 1, right) # 对右边元素排序 return data
正常的情况,快排的复杂度是O(nlogn)
快排存在一个最坏情况,就是每次归位,都不能把列表分成两部分,此时复杂度就是O(n2)了,如果要避免设计成这种最坏情况,可以在取第一个数的时候不要取第一个了,而是取一个列表中的随机数
5.归并排序
原理:列表分成两段有序,然后分解成每个元素后,再合并成一个有序列表,这种操作就叫做一次归并
应用到排序就是,把列表分成一个元素一个元素的,一个元素当然是有序的,将有序列表一个一个合并,最终合并成一个有序的列表
图示:
代码:
def merge(li, left, mid, right): # 一次归并过程,把从mid分开的两个有序列表合并成一个有序列表 i = left j = mid + 1 ltmp = [] # 两个列表的元素依次比较,按从大到小的顺序放到一个临时的空列表中 while i <= mid and j <= right: 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 <= right: ltmp.append(li[j]) j += 1 li[left:right+1] = ltmp return li def _merge_sort(li, left, right): # 细分到一个列表中只有一个元素的情况,对每一次都调用merge函数变成有序的列表 if left < right: mid = (left+right)//2 _merge_sort(li, left, mid) _merge_sort(li, mid+1, right) merge(li, left, mid, right) return li def merge_sort(li): return(_merge_sort(li, 0, len(li)-1))
照例,时间复杂度是O(nlogn)
特殊的,归并排序还有一个O(n)的空间复杂度
6.堆排序
把这个放到最后,是因为这个是最麻烦的,把最麻烦的放到最后,是一种对工作负责的表现
如果要说堆排序,首先得先把‘树’搞明白
树
树是一种数据结构;
树是由n个节点组成的集合; -->如果n为0,那这是一颗空树,如果n>0,那么那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树。
一些可能会用到的概念:
根节点:树的第一个节点,没有父节点的节点
叶子节点:不带分叉的节点
树的深度(高度):就是分了多少层
孩子节点、父节点:节点与节点之间的关系
图示:
二叉树
然后在树的基础上,有一个二叉树,二叉树就是每个节点最多有两个子节点的树结构,比如这个:
满二叉树:除了叶子节点,所有节点都有两个孩子,并且所有叶子节点深度都一样
完全二叉树:是有满二叉树引申而来,假设二叉树深度为k,那么除了第k层,之前的每一层的节点数都达到最大,即没有空的位置,而且第k层的子节点也都集中在左子树上(顺序)
二叉树的存储方式
有链式存储和顺序存储的方式(列表),本篇只讨论顺序存储的方式
思考:
父节点和左孩子节点的编号下标有什么关系? 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.构造堆
2.得到堆顶元素,就是最大的元素
3.去掉堆顶,将堆的最后一个元素放到堆顶,此时可以通过一次调整重新使堆有序
4.堆顶元素为第二大元素
5.重复步骤3,直到堆为空
其中构造堆的过程:
挨个出数的过程:
代码:
def sift(li, left, right): # left和right 表示了元素的范围,是根节点到右节点的范围,然后比较根和两个孩子的大小(注意我们在代码上限制只取两个孩子的位置),把大的放到堆顶 # 和两个孩子的大小没关系,因为我们只需要拿堆顶的元素就行了 # 构造堆 i = left # 当作根节点 j = 2 * i + 1 # 上面提到过的父节点与左子树根节点的编号下标的关系 tmp = li[left] while j <= right: if j+1 <= right and li[j] < li[j+1]: # 找到两个孩子中比较大的那个 j = j + 1 if tmp < li[j]: # 如果孩子中比较大的那个比根节点大,就交换 li[i] = li[j] i = j # 把交换了的那个节点当作根节点,循环上面的操作 j = 2 * i + 1 else: break li[i] = tmp # 如果上面发生交换,现在的i就是最后一层符合条件(不用换)的根节点, def heap_sort(li): n = len(li) for i in range(n//2-1, -1, -1): # 建立堆 n//2-1 是为了拿到最后一个子树的根节点的编号,然后往前走,最后走到根节点0//2 -1 = -1 sift(li, i, n-1) # 固定的把最后一个值的位置当作right,因为right只是为了判断递归不要超出当前树,所以最后一个值可以满足 # 如果每遍历一个树,就找到它的右孩子,太麻烦了 for i in range(n-1, -1, -1): # 挨个出数 li[0], li[i] = li[i],li[0] # 把堆顶与最后一个数交换,为了节省空间,否则还可以新建一个列表,把堆顶(最大数)放到新列表中 sift(li, 0, i-1) # 此时的列表,应该排除最后一个已经排好序的,放置最大值的位置,所以i-1
时间复杂度也是O(nlogn)
来扩展一下,如果要取一个列表的top10,就是取列表的前十大的数,怎么做?
可以用堆来实现,取堆的前十个数,构造成一个小根堆,然后依次遍历列表后面的数,如果比堆顶小,则忽略,如果比堆顶大,则将堆顶替换成改元素,然后进行一次向下调整,最终这个小根堆就是top10
其实Python自带一个heapq模块,就是帮我们对堆进行操作的
heapq模块
队列中的每个元素都有优先级,优先级最高的元素优先得到服务(操作),这就是优先队列,而优先队列通常用堆来实现
如果用heapq模块来实现堆排序,就简单多了:
import heapq def heapq_sort(li): h = [] for value in li: heapq.heappush(h,value) return [heapq.heappop(h) for i in range(len(h))]
而想取top10 ,直接一个方法就行了
heapq.nlargest(10,li)
这三种速度快的排序方式就说完了,其中,快排是速度最快的,即使这样,也不如Python自带的sort快
再来介绍两种排序,希尔排序和计数排序
7.希尔排序
希尔排序是一种分组插入排序的算法
思路:
首先取一个整数d1=n/2,将元素分为d1个组,每组相邻量元素之间距离为d1,在各组内进行直接插入排序;
取第二个整数d2=d1/2,重复上述分组排序过程,直到di=1,即所有元素在同一组内进行直接插入排序。
希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟排序使得所有数据有序。
图示:
代码:
def shell_sort(li):
gap = int(len(li)//2) # 初始把列表分成 gap个组,但是每组最多就两个元素,第一组可能有三个元素
while gap >0:
for i in range(gap,len(li)):
tmp = li[i]
j = i - gap
while j>0 and tmp<li[j]: # 对每一组的每一个数,都和他前面的那个数比较,小的在前面
li[j+gap] = li[j]
j -= gap
li[j+gap] = tmp
gap = int(gap//2) # Python3中地板除也是float类型
return li
通过diamante也能看出来,其实希尔排序和插入排序是非常相像的,插入排序就可以看做是固定间隔为1的希尔排序,希尔排序就是把插入排序分了个组,同一个组内,相邻两个数之间不是相差1,而是相差gap
时间复杂度:O((1+t)n),其中t是个大于0小于1的数,取决于gap的取法,当gap=len(li)//2的时候,t大约等于0.3
8.计数排序
需求:有一个列表,列表中的数都在0到100之间(整数),列表长度大约是100万,设计算法在O(n)时间复杂度内将列表进行排序
分析:列表长度很大,但是数据量很少,会有大量的重复数据。可以考虑对这100个数进行排序
代码:
def count_sort(li): count = [0 for i in range(101)] # 根据原题,0-100的整数 for i in li: count[i] += 1 i = 0 for num,m in enumerate(count): # enumerate函数将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。 for j in range(m): li[i] = num i += 1
七、算法练习
1. 练习1
排序-赠品1:
现在有一个列表,列表中的数范围都在0-100之间,列表长度大学为100万。设计算法在O(n)时间复杂度内将列表进行排序。
2. 练习2
排序-赠品2:
现在有n个数(n>10000),设计算法,按大小顺序得到前10大的数。---应用场景:榜单TOP 10
3. 练习3
堆的应用(了解)
解决思路:
1.取列表前10个元素建立一个小根堆。堆顶就是目前第10大的数。
2.依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行依次调整;
3.遍历列表所有元素后,倒序弹出堆顶。
4. 练习4
算法--习题1
给定一个列表和一个整数,设计算法找到两个数的下标,使得两个数之和为给定的整数。保证肯定仅有一个结果。
例如,列表[1, 2, 5, 4]与目标整数3, 1 + 2 = 3,结果(0, 1)。
https://leetcode.com/problemset/all/ 刷一些算法题
5. 练习5
算法--习题2
1. 给定一个升序列表和一个整数,返回该整数在列表中的下标范围。
2. 例如:列表[1,2,3,3,3,4,4,5],若查找3,则返回(2, 4);若查找1,则返回[0, 0]。