算法
一、什么是算法
(1)非形式地说,算法(algorithm)就是任何定义的计算过程,该过程取某个值或值的集合作为输入并产生某个值或值的集合作为输出,这样算法就是把输入转换成输出的计算步骤的一个序列。
(2)简单来说,算法(algorithm)是一个计算过程,解决问题的方法
(3)时间复杂度:是用来估计算法运行时间的一个式子(单位)
(4)一般来说:时间复杂度高的算法比复杂度低的算法慢
(5)常见的时间复杂度(按效率排序)
![]()
(6)不常见的时间复杂度(运行速度特别慢) :O(!n)、
、![]()
(7)最常见的判断时间复杂度:
循环减半的过程--->O(logn)
几次循环就是n的几次方的复杂度
(8)空间复杂度:用来评估算法内存占用大小的一个式子,大多时候都是“空间换时间”
二、算法解决排序问题
(1)先了解递归:必须满足调用自身和有明确的结束条件
(2)函数:
def func1(x):
print(x)
func1(x-1) #没有明确的结束条件,故不是递归函数
def func2(x):
if x>0:
print(x)
func2(x+1) #没有明确的结束条件,故不是递归函数
def func3(x):
if x>0:
print(x)
func3(x-1) #有明确的结束条件,是递归函数
def func4(x):
if x>0:
print(x)
func3(x-1) #有明确的结束条件,是递归函数
递归小练习
def test(n):
if n==0:
print('我的小鲤鱼')
else:
print('抱着',end='')
test(n-1)
print('的我',end='')
test(5)
result:抱着抱着抱着抱着抱着我的小鲤鱼的我的我的我的我的我
#递归实例:汉诺塔问题 #汉诺塔 def hanoi(n,A,B,C): if n==1: print('%s->%s'%(A,C)) else: hanoi(n-1,A,C,B) print('%s->%s' % (A, C)) hanoi(n-1,B,A,C) hanoi(2,'A','B','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(2, 'A', 'B', 'C')
列表查找 (1)列表查找:从列表中查找指定元素 输入:列表、带查找元素 输出:元素下标或未查找到元素 (2)顺序查找:从列表第一个元素开始,顺序进行搜索,直到找到为止 (3)二分查找:从有序列表的候选区data[0:n]开始,通过对待查找的值 与候选区中间值比较,可以使候选区减少一半,
#二分查找 O(logn) def binary_search(li,val): low=0 high=len(li)-1 while low<=high: mid=(low+high)//2 if li[mid]>val: high=mid-1 elif li[mid]<val: low=mid+1 else: return mid else: return -1 li=list(range(0,10000,2))
# 递归版 O(logn) def bin_search_rec(li,value,low,high): if low<=high: mid=(low+high)//2 if li[mid]==value: return mid elif li[mid]>value: return bin_search_rec(li,value,low,mid-1) else: return bin_search_rec(li,value,mid+1,high) # 尾递归,会自动转换成循环 else: return
列表排序 (1)列表排序:将无序列表变为有序列表 (2)应用场景:各种榜单、各种表格、给二分查找用、给其它算法用等 (3)输入:无序列表;输出:有序列表 (4)可分为:升序排列和降序排列 (5)排序low B三人组:冒泡排序、选择排序、插入排序 (6)排序NB三人组:快速排序、堆排序、归并排序 (7)不常见的排序:基数排序、希尔排序、桶排序
冒泡排序 冒泡排序,连续遍历列表的排序算法,交换位置,直到它们出现正确的顺序
冒泡排序,有时也被称为沉没排序,是一种简单的排序算法,它可以反复遍历整个列表进行排序,
比较每一对相邻的项目,如果排序顺序错误,就将它们交换。 重复列表直到不需要交换,这表明
列表已经排序。
该算法是一种比较排序,它被命名为更小或更大的元素“冒泡”到列表的顶部。
虽然算法简单,但对于大多数问题来说,即使与插入排序相比,速度太慢并且不切实际。
如果输入通常是按照排序顺序,但偶尔会有一些乱序元素几乎到位,那么这是可行的。
![]()
伪代码 procedure bubbleSort( A : list of sortable items ) n = length(A) repeat swapped = false for i = 1 to n-1 inclusive do /* if this pair is out of order */ if A[i-1] > A[i] then /* swap them and remember something changed */ swap( A[i-1], A[i] ) swapped = true end if end for until not swapped end procedure 优化冒泡,伪代码: procedure bubbleSort( A : list of sortable items ) n = length(A) repeat swapped = false for i = 1 to n-1 inclusive do if A[i-1] > A[i] then swap(A[i-1], A[i]) swapped = true end if end for n = n - 1 until not swapped end procedure 代码关键点:趟、无序区,时间复杂度,O(n^2) 代码实现: def bobble_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] 如果冒泡排序中执行一趟而没有交换,则列表已经是有序状态,可以直接结束算法。 改进: def bobble_sort(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
选择排序 (1)一趟遍历记录最小的数,放到第一个位置; (2)再一趟遍历记录剩余列表中最小的数,继续放置,直到剩一个数 (3)代码关键点:无序区和最小数的位置
(4)代码实现:
def select_sort(li):
for i in range(len(li)-1):
# i表示趟数,也表示无序区开始的位置
min_loc=i #最小数的位置
for j in range(i,len(li)-1):
if li[j]<li[min_loc]:
min_loc=j
li[i],li[min_loc]=li[min_loc],li[i]
插入排序 (1)列表被分为有序区和无序区两个部分,最初有序区只有一个元素 (2)每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空
(3)代码关键点:摸到的牌和手里的牌,时间复杂度O(n^2)

(4)代码实现
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
排序LOW B三人组:冒泡排序、选择排序、插入排序,时间复杂度O(n^2),空间复杂度O(1)
排序NB 三人组:快速排序、堆排、归并排序
快速排序 (1)快速排序:快 (2)快排思路: 取一个元素p(第一个元素),使元素p归位; 列表被p分成两部分,左边都比p小,右边都比p大; 递归完成排序 (3)算法关键点:归位和递归
(4)快速排序的数学分析表明,平均来说,该算法时间复杂度
为O(nlogn)。在最坏的情况下,它的时间复杂度O(n^2)
尽管这种情况是罕见的。
| Class | Sorting algorithm |
|---|---|
| Worst-case performance | O(n2) |
| Best-case performance | O(n log n) (simple partition) or O(n) (three-way partition and equal keys) |
| Average performance | O(n log n) |
| Worst-case space complexity | O(n) auxiliary (naive) O(log n) auxiliary (Sedgewick 1978) |
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)
(6)代码实现第二步
def partition(li,left,right):
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
堆排序
(1)树:
- 树是一种可以递归定义的数据结构;
- 树是由n个节点组成的集合:若n=0,则是一颗空树;若n>0,那存在1个节点作为树的根节点,
其它节点可以分为m个集合,每个集合本身是一颗树;
- 结点(node):表示树中的元素,包括数据项及若干指向其子树的分支
- 结点的度(degree):结点拥有的子树数
- 根节点、叶子(leaf)节点:度为0的点(终端结点)
- 森林:指m棵不相交的树的集合
- 有序树:结点各子树从左至右有序,不能互换(左为一)
- 无序树:结点各子树可互换位置
- 双亲:上层的那个结点
- 孩子:下层结点的子树的根
- 兄弟:同一双亲下的同一层的结点(孩子之间互称兄弟)
- 堂兄弟:双亲位于同一层的结点(但并非同一双亲)
- 祖先:从根到该结点所经分支的所有结点
- 子孙:该结点下层子树的任一结点
- 结点的层次:从根到该结点的层数(根节点算第一层)
- 分支结点:度不为0的点(非终端点)
- 树的度:所有结点度中最大的值(max{各结点的度})
- 树的深度(或高度):所有结点中最大的层数(max{各结点的层次})
(2)二叉树:特殊且常用的树,是一个有序树
- 二叉树:度不超过2的树(结点最多有两个叉)
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值;
- 完全二叉树:叶结点只能出现在最下层和次下层,并且最下面一层的结点都集中在改层最左边的若干位置的二叉树
- 二叉树性质:在二叉树的第i层上至多有2(i-1)个节点(i>0)
深度为k的二叉树至多有2k-1个结点(k>0)
对于任何一棵二叉树,若度2的结点数有n2个,叶子结点数为n0,则n0=n2+1
具有n个结点的完全二叉树的深度必为(log2n)+1
如果对一棵有n个结点的完全二叉树的结点按层序编号,则对任一结点i(1<=i<=n):
1)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲parent(i)是结点(i/2);
2)如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子lchild(i)是结点2i;
3)如果2i+1>n,则结点i无右孩子;否则其右孩子rchild(i)是结点2i+1
- 二叉树的存储方式:链式存储方式和顺序存储方式(列表)
- 特点:父节点和左孩子节点的编号下标关系:i->2i+1
父节点和右孩子节点的编号下标关系:i->2i+2
(3)堆排序
- 大根堆:一棵完全二叉树,满足任一结点都比其孩子结点大
- 小根堆:一棵完全二叉树,满足任一结点都比其孩子结点小
- 堆的向下调整性质:假设,结点的左右子树都是堆,但自身不是堆,当根节点的左右子树都是堆时,可以通过一次向下的调整来将其变成一个堆。
(4)堆排序过程
- 建立堆
- 得到堆顶元素,为最大元素
- 去掉堆顶,将堆顶最后一个元素放到堆顶,此时可通过一次调整重新使堆有序
- 堆顶元素为第二大元素
- 重复第三个步骤,直到堆变空
(5)代码实现
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 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] #堆的大小少了一个元素 sift(li,0,j-1)
(6)用python内置模块实现堆排序
- 优先队列:一些元素的集合,pop操作每次执行都会从优先队列中弹出最大(或最小)的元素
- 利用heapq模块实现堆排序
import heapq 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
(7)堆排序应用--topK问题
- 现在有n个数,设计算法找出前K大的数(k<n)
- 解决方法:排序后切片、low B三人组、堆排
- 堆排思路:取列表前k个元素建立一个小根堆,堆顶就是目前第k大的数,依次向后遍历原列表
对于列表中的元素,如果小于堆顶,则忽略该元素;若大于堆顶,则将堆顶更换为该
元素,并且对堆进行一次调整,遍历列表所有元素后,倒序弹出堆顶
代码实现 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 def topk(li,k): heap=li[0:k] for i in range(k//2-1,-1,-1): sift(heap,i,k-1) for i in range(k,len(i)): if li[i]>heap[0]: heap[0]=li[i] sift(heap,0,k-1) for i in range(k-1,-1,-1): heap[0],heap[i]=heap[i],heap[0] sift(heap,0,i-1)
归并排序
- 分解:将列表越分越小,直至分成一个元素
- 终止条件:一个元素是有限的
- 合并:将两个有序列表归并,列表越来越大
| Class | Sorting algorithm |
|---|---|
| Data structure | Array |
| Worst-case performance | O(n log n) |
| Best-case performance |
O(n log n) typical, O(n) natural variant |
| Average performance | O(n log n) |
| Worst-case space complexity | О(n) total with O(n) auxiliary, O(1) auxiliary with linked lists[1] |
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)
NB三人组:三种排序算法时间复杂度都是O(nlogn)
一般情况下,就运行时间而言:快速排序<归并排序<堆排序
三种排序算法的缺点:
- 快速排序:极端情况下排序效率低
- 归并排序:需要额外的内存开销
- 堆排序:在快的排序算法中相对较慢
总结:



浙公网安备 33010602011771号