算法基础
说到算法,那什么是算法?
算法(Algorithm):一个计算过程,解决问题的方法。
跟算法相关的一些概念,比如:时间复杂度和空间复杂度。
时间复杂度是用来估算一个算法的运行效率的。通常用O表示。比如,print("xxxx")一句代码的时间复杂度是O(1),循环n次,就是O(n)。
的时间复杂度是O(logn),n的指数
常见的时间复杂度(按效率排序)
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n2logn)<O(n3)
不常见的时间复杂度(看看就好)
O(n!) O(2n) O(nn) …
如何一眼判断时间复杂度?
循环减半的过程-->O(logn)
几次循环就是n的几次方的复杂度
空间复杂度是用来评估一个算法内存占用大小的。
递归的示例:
# 终端输出:抱着抱着抱着我的小鲤鱼的我的我的我 def func(n): if n==1: print("我的小鲤鱼",end='') else: print("抱着",end='') func(n-1) print("的我",end='') func(4)
递归解决汉诺塔问题
def hanoi(n,A='A',B='B',C='C'): if n>0: hanoi(n-1,A,C,B) print("%s->%s"%(A,C)) hanoi(n-1,B,A,C) hanoi(3)
备注:汉诺塔的个数中移动的次数 2**n-1
面试题:
一段有n个台阶组成的楼梯,小明从楼梯的最底层向最高处前进,它可以选择一次迈一级台阶或者一次迈两级台阶,问:他有多少种不同的走法?
# 思路:可以使用递归从最后的情况算起,最后一步分为两种情况,跨一级台阶和跨两级台阶 # def steps(n): if n==1:return 1 elif n==2:return 2 return steps(n-1)+steps(n-2) print(steps(5))
def func(n): if n==1:return 1 if n==2:return 2 if n==3:return 4 return func(n-1)+func(n-2)+func(n-2) # 2**(n-1) print(func(8))
备注:这种迈一级或者两级台阶的情况类似于菲波那切数列
算法
有序列表的二分查找
def linear_search(li,aim,start=0,end=None): if end==None: end = len(li)-1 if start <= end: mid = (start+end)//2 if li[mid] > aim: return linear_search(li,aim,start=start,end=mid-1) elif li[mid] < aim: return linear_search(li,aim,start=mid+1,end=end) else: return mid else: return None li = [1,3,5,7,9,11,12,13] print(linear_search(li,11))
二分查找的时间复杂度是O(logn) ,局限就是,必须是有序列表,备注:二分查找的关键词:候选区。
排序
排序lowB三人组:冒泡排序、选择排序、插入排序
排序NB三人组:快排(快速排序)、堆排序、归并排序
使用较少的排序:基数排序、希尔排序、桶排序
列表的冒泡排序
# 冒泡的思想是,如果后一个元素比前一个元素小,则交换这两个元素的位置, # 一趟循环后,最后一个元素肯定是所有元素中最大的 def bubble_sort(li): # 最后一趟肯定只有一个元素,并且这个元素是最小的,所以只需要n-1趟 n = len(li)-1 # i 表示第几趟 for i in range(n): # 第1趟 无序区元素n-1 第2趟 无序区元素n-2 # 第3趟 无序区元素n-3 第i趟 无序区元素n-i # j 表示 无序区的第几个元素 for j in range(n-i): # 如果前一个数大于后一个数,交换这两个数的位置 if li[j] > li[j+1]: li[j],li[j+1] = li[j+1],li[j] bubble_sort(li)
冒泡的时间复杂度是O(n**2)
冒泡排序的优化:
如果冒泡排序中执行一趟没有交换,则证明列表是有序的,可以直接结束算法
def bubble_sort(li): # 最后一趟肯定只有一个元素,并且这个元素是最小的,所以只需要n-1趟 n = len(li)-1 # i 表示第几趟 exchange = False for i in range(n): # 第1趟 无序区元素n-1 第2趟 无序区元素n-2 # 第3趟 无序区元素n-3 第i趟 无序区元素n-i # j 表示 无序区的第几个元素 for j in range(n-i): # 如果前一个数大于后一个数,交换这两个数的位置 if li[j] > li[j+1]: li[j],li[j+1] = li[j+1],li[j] # 发生交换,就将exchange改为True exchange = True # 每趟结束后做一个判断,如果为False就退出 if not exchange: break bubble_sort(li)
选择排序
# 选择排序的思路就是,每一趟遍历找到最小的数,放到第一个位置, # 再次遍历剩余列表中最小的数,继续放置 # 难点: 如何找到这个最小数 # 方法:标记一个位置比如无序区的第一个位置的数最小,遍历无序区, # 如果后一个数比标记的最小数小,就将这个数置换为最小数 # 每趟结束后,将最小数与这个标记点交换位置 def select_sort(li): n = len(li) # i 表示趟数,也表示无序区的第一个数 for i in range(n-1): # 标记一个最小数的位置 min_loc = i # 循环无序区,不用跟自身比,所以可以从i+1开始 for j in range(i+1,n): # 如果后一个数比标记的数小,就置换这个数为最小的数 if li[j]< li[min_loc]: min_loc = j # 循环一遍后,将这个最小数的位置与标记位互换 li[min_loc],li[i] = li[i],li[min_loc] select_sort(li)
选择排序的时间复杂度是O(n**2),但是选择排序比冒泡排序快,因为没有冒泡两个数交换的次数多,但两者都在一个数量级上
插入排序
# 插入排序的思路:列表被分为有序区和无序区两个部分,最初的有序区只有一个元素, # 就是列表的第一个元素,每次从无序区中选择一个元素,与有序区的元素比较, # 插入到有序区的位置 def insert_sort(li): # i 表示趟数,但是从1开始是便于记录当前无序区的第一个元素 for i in range(1,len(li)): temp = li[i] # 有序区的最后一个元素 j = i-1 while j >= 0 and li[j] > temp: # 如果用于比较的元素j 没有值时(无序区的第一个元素比有序区的所有的元素都小时) # 并且 比无序区大时,将这个元素后移 # 将有序区用于比较的元素比较。第一次进来时是有序区最有一个元素 li[j+1] = li[j] # 最往前一位继续比较 j -= 1 # 否则,就将这个元素放置于用于比较元素的后面 li[j+1] = temp insert_sort(li) # 方式二 def insert_sourt_2(li): for i in range(1,len(li)): temp = li[i] for j in range(i-1,-1,-1): if li[j] > temp: li[j+1] = li[j] else: break insert_sourt_2(li)
插入排序的时间复杂度是O(n**2),lowB三人组中冒泡排序最慢
可优化点:将有序列表部分按照二分查找来寻找插入点,但是并不能提升时间复杂度。
因为使用二分查找找到插入点确实快,但是,找到后,我们还有将这个元素挪动到这个插入点,这就是一个O(n)。
快速排序
# 快速排序的思路:取一个元素(第一个元素),让这个元素归位,归位的方式是左边的元素比它小 # 右边的元素比它大,这样列表被这个元素分成了两部分,在将这部分递归完成同样的 # 递归的结束条件: 类似于二分,定义一个left和right 当left>=right时,表示这部分还有一个元素或没有元素 def partition(li,left,right): # 先将这个数取出来 temp = li[left] while left < right: # 从右边取出比temp小的数,并将这个数放在左边left位置 while left < right and li[right] >= temp: right -= 1 li[left] = li[right] # 从左边找出比temp大的数,放到右边right位置 while left < right and li[left] <= temp: left += 1 li[right] = li[left] # 当left>=right时,表示此时temp已归位,并将这个元素放到left或right li[left] = temp # 将位置返回 return left 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) quick_sort(li,0,len(li)-1)
快排的时间复杂度是O(n*logn),缺点是递归深度的限制。
快排的最坏情况是排序的元素是降序的,这样的时间复杂度是O(n**2),如何避免,随机化
import random def partition(li,left,right): # 随机化,从这些元素中随机取一个,与left元素换位 rand = random.randint(left,right) li[left],li[rand] = li[rand],li[left] # 先将这个数取出来 temp = li[left] while left < right: # 从右边取出比temp小的数,并将这个数放在左边left位置 while left < right and li[right] >= temp: right -= 1 li[left] = li[right] # 从左边找出比temp大的数,放到右边right位置 while left < right and li[left] <= temp: left += 1 li[right] = li[left] # 当left>=right时,表示此时temp已归位,并将这个元素放到left或right li[left] = temp # 将位置返回 return left 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) quick_sort(li,0,len(li)-1)
如果想将列表进行降序排序,应该修改哪些符号?
很简单,将判断left,right对应元素与temp的比较的符号反过来。
import random def partition(li,left,right): # 随机化,从这些元素中随机取一个,与left元素换位 rand = random.randint(left,right) li[left],li[rand] = li[rand],li[left] # 先将这个数取出来 temp = li[left] while left < right: # 从右边取出比temp小的数,并将这个数放在左边left位置 while left < right and li[right] <= temp: right -= 1 li[left] = li[right] # 从左边找出比temp大的数,放到右边right位置 while left < right and li[left] >= temp: left += 1 li[right] = li[left] # 当left>=right时,表示此时temp已归位,并将这个元素放到left或right li[left] = temp # 将位置返回 return left 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) quick_sort(li,0,len(li)-1)
堆排序
要理解堆排序,就需要理解堆的概念,而理解堆,就需要二叉树的概念,而理解二叉树,就需要理解树的概念。
树是一种数据结构。比如:目录结构。
树是一种可以递归定义的数据结构,是由n个节点组成的集合。
备注:树的度表示这个树结构最大的节点数(最大的杈数)
二叉树是指度不超过2的树(节点最多有两个杈)
一个二叉树,如果每一层的节点数都达到最大的值(2个),则这个二叉树就是满二叉树。
叶节点只能出现在最下层和次下层,并且最下层一层的节点都集中在该层的最左边的二叉树就是完全二叉树。
二叉树的存储方式分为链式存储方式和顺序存储方式。
二叉树顺序存储在列表中时,
父节点与左孩子节点的索引下标的关系是:父节点索引下标 i,左孩子节点索引为 2i+1
父节点与右孩子的节点的索引下标关系为: i ---> 2i +2
从子节点找到父节点的索引关系: 子为i 父节点为 (i-1) // 2
堆是一种特殊的完全二叉树,满足任意节点都比其孩子节点大的是大根堆,满足任意 节点都比其孩子下的是小根堆。
大根堆实现正序排序
备注:堆排序的时间复杂度是O(n*logn),堆排序是NB三人组中最慢的,快排最快。
堆可以实现优先队列(优先队列是指每次都会取出最大或者最小的元素)
Python提供了内置的堆排序队列的模块 heapq,内部使用小根堆的方式。
方法: heapq.heapify(list) 提供一个列表,将这个列表构建成一个小根堆 heapq.heappush(heap,item) 添加一个元素后重新调整这个堆
heapq.heappop(heap) 取出堆顶元素 heapq.nlargest(n,list) 可以将列表中最大的前n个数取出来,常用于排行榜单前多少
heapq.nsmallest(n,list) 将列表中最小的前n个数取出来。
现有n个数,设计算法找出前k大的数 topK问题
解决方法:1,排序后切片 利用快排排序然后取出后K个 ,时间复杂度 O(nlogn)
2,利用冒泡排序,每趟取出一个最大数,K趟就取出前K大,时间复杂度 O(kn)
3,利用选择排序,每趟从无序区选出一个最大的,K趟,时间复杂度也是 O(kn)
4,利用堆排序。思路:取出列表中前K个元素建立一个小根堆。堆顶就是列表 中k个数中的最小值,依次向后遍历原列表,如果列表中的元素比堆顶小,则 忽略该元素,如果比堆顶大,则将堆顶更换为该元素,并对堆进行调整,遍 历完列表后,堆中就是前K大的元素,并依次取出堆顶。
这个方法的时间复杂度是 O(nlogk) ,是最快的方法。
def sift(li,low,high): # low 是待调整的堆的根节点位置,high 是堆的最后节点的位置,用来判断是否越界 i = low # 表示当前节点的左孩子 j = 2 * i + 1 temp = li[i] while j <= high : # 如果右孩子存在并且右孩子大于左孩子 j+1<=high 简写为 j<high if j < high and li[j+1] < li[j]: j += 1 # 此时的j 表示右孩子 if li[j] < temp: li[i] = li[j] i = j j = 2*i+1 else: break # 将调整好的temp写入 li[i] = temp def topk(li,k): temp = li[0:k] for i in range((k-2)//2,-1,-1): sift(temp,i,k-1) for j in range(k,len(li)): if li[j] > temp[0]: temp[0] = li[j] sift(temp,0,k-1) for i in range(k-1,-1,-1): temp[0],temp[i] = temp[i],temp[0] sift(temp,0,i-1) print(temp) topk(li,10)
def sift(li,low,high): # low 是待调整的堆的根节点位置,high 是堆的最后节点的位置,用来判断是否越界 i = low # 表示当前节点的左孩子 j = 2 * i + 1 temp = li[i] while j <= high : # 如果右孩子存在并且右孩子大于左孩子 j+1<=high 简写为 j<high if j < high and li[j+1] < li[j]: j += 1 # 此时的j 表示右孩子 if li[j] < temp: li[i] = li[j] i = j j = 2*i+1 else: break # 将调整好的temp写入 li[i] = temp def heap_sort(li): # 构造堆 思路:从最后一个元素的父节点开始调整堆(从最后一个子树) # 列表长度为li ,最后一个元素索引为 n-1 则父节点索引为 (n-1-1)//2 n = len(li) for i in range((n-2)//2,-1,-1): # 从最后一个节点的父节点开始 # 难点 high的取值 我们可以将整个堆的最后一个元素作为防止越界的值,虽然不够精确,但是足够 sift(li,i,n-1) # 构造好堆后,我们需要取堆顶取出,放到一个新容器中,同时要将堆的最后一个元素移到堆顶, # 所以取巧,我们可以倒序的循环这个列表,将取出的堆顶(调整后的列表的第一个元素) # 与堆的最后一个元素换位(将li[0]与li[i]换位),可以节省重新构建一个列表的空间 for j in range(n-1,-1,-1): # 将堆顶取出,同时将堆的最后一个元素移到堆顶 li[0],li[j] = li[j],li[0] # 取出堆顶后调整堆 sift(li,0,j-1) heap_sort(li)
归并排序
归并排序是有条件的,归并排序用于将两段有序列表,合并成一个有序的列表。
def merge(li,low,mid,high): # low 为这个列表的起始值,mid为这个列表的分水岭(第一个有序列表的结束),high为这个列表的结束 # 创建一个新列表 li_temp = [] # 第一个有序列表的第一个元素为 low,另一个有序列表的第一个元素为 mid+1 i = low j = mid +1 # 当一个有序列表中的元素没有可比较的时候,退出 while i <= mid and j <= high: if li[i] > li[j]: li_temp.append(li[j]) j += 1 else: li_temp.append(li[i]) i += 1 while i <= mid: li_temp.append(li[i]) i += 1 while j <= high: li_temp.append(li[j]) j += 1 li[low:high+1] = li_temp # lis = [1,2,3,5,6,7,2,4,6,8,9] # merge(lis,0,5,10) l1 = [2,3,4,5,6,7] l2 = [1,3,5,6,7,8,9] l3 = l1 + l2 merge(l3,0,len(l1)-1,len(l3)-1) print(l3)
无序列表归并怎么用?
# 归并排序的思路:两个有序列表组成的列表,依次比较两个有序列表的元素,哪一个列表的元素小,就将这个元素放入一个新的列表中 # 直到一个列表中的元素为空,然后将另一个列表中剩余的元素全部放入新列表中,这就是一次归并 # 对于无序的列表,如何使用归并,思路:将这个列表无限分解,直到分解为一个元素就是有序的, #终止条件,直到剩余一个元素或者没有元素就是有序的,再将这些元素依次归并为一个大的有序列表,然后直到所有的元素合并完 def merge(li,low,mid,high): # low 为这个列表的起始值,mid为这个列表的分水岭(第一个有序列表的结束),high为这个列表的结束 # 创建一个新列表 li_temp = [] # 第一个有序列表的第一个元素为 low,另一个有序列表的第一个元素为 mid+1 i = low j = mid +1 # 当一个有序列表中的元素没有可比较的时候,退出 while i <= mid and j <= high: # 哪个有序列表中的元素小就将这个元素添加到这个新列表中 if li[i] > li[j]: li_temp.append(li[j]) j += 1 else: li_temp.append(li[i]) i += 1 # 循环列表中剩余的元素,全部添加到这个新列表中 while i <= mid: li_temp.append(li[i]) i += 1 while j <= high: li_temp.append(li[j]) j += 1 # 将这个新列表赋值给原列表 li[low:high+1] = li_temp 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)
备注:归并排序的时间复杂度是O(nlogn)。空间复杂度是O(n)
NB三人组中,三个的时间复杂度都是O(nlogn),快排最快,堆排最慢,归并居中。
关于稳定性:排序前后,对于相同的多个数,前后相对位置不发生改变,比较稳定的是:冒泡排序、插入排序、归并排序。
备注:python中的sort排序采用的是混合的稳定的排序,从归并和插入继承而来。
希尔排序
希尔排序是一种改进了的插入排序。是一种分组插入排序算法,希尔排序每趟并使某些元素有序,而是使整体数据越来越接近有序,最后一趟排序使得所有数据有序。
# 希尔排序 思路:首先,取一个整数d1 = n(列表的长度),整除2 d1 = n//2 ,将元素分成d1个组,每组和相邻元素距离是d1,在各组 # 内进行直接插入排序,再去第二个整数,在d2 = d1//2,重复上述动作,直到dn=1,即所有元素在同一组内进行直接插入排序。 def insert_sort_gap(li,d): for i in range(1,len(li)): temp = li[i] # 731 j = i-d while j >=0 and li[j] >temp: li[j+d] = li[j] j -=d li[j+d] = temp def shell_sort(li): d = len(li)//2 while d > 0 : insert_sort_gap(li,d) d //= 2 shell_sort(li)
计数排序
# 计数排序是线性排序 ,是有使用局限性的,适用于一定范围内的整数 def count_sort(li,max_num): # max_num 代指这个列表中的最大数 min_num 代指这个列表中的最小数 # 生成一个最小数和最大数之间包含所有整数的一个列表,每个数初始为0个 count = [0 for i in range(max_num+1)] # 循环增列表,统计这个数出现的次数 for num in li: count[num] +=1 # 循环这个统计的列表,k为对应的 j = 0 for k,v in enumerate(count): for m in range(v): li[j] = k j += 1 count_sort(li,999)
备注:计数排序的时间复杂度为O(n),但是局限性很大,数字的范围不能太大,而且数字不能是小数和负数。还必须是整数。
桶排序
桶排序适用用于范围比较大的计数排序。但是局限性就是数据的分布要均匀,如果数据集中于某一个桶中,就会很慢。
桶排序的思路是将元素分在不同的桶中,再对每个桶中的元素排序。
基数排序
基数排序基于多关键字排序。对列表中的元素先按照一个特征排序,再对相同特征的按照另一个特征排序,优先按照低级别的关键字排序,比如,对于整数,先按照个位排序,个位相同的在按照十位排序,依次类推。
def list_to_buckets(li,base,iteration): buckets = [[] for i in range(base)] for number in li: # 对一个数取其某一位上对应的数字,方法 :先对这个数对10的几次方取余,在将这个结果对10取余 # 个位 十的0次方,十位,十的一次方... (num//10**n)//10 digit = (number //(base**iteration))%base buckets[digit].append(number) return buckets def buckets_to_list(buckets): return [x for bucket in buckets for x in bucket] def radix_sort(li,base=10): maxval = max(li) it = 0 while base**it <= maxval: li = buckets_to_list(list_to_buckets(li,base,it)) it+=1 return li ret = radix_sort(li)
基数排序的时间复杂度是O(kn)。