排序与搜索入门
在算法中,排序和搜索作为大家最先接触到的两类算法,实质上是有共通之处的,二者所采用的方法大致上是一致的。这里不妨将二者放在一起分析,方便学习掌握。
首先我们对排序和搜索有一个大概的了解。排序,顾名思义就是按照一定的顺序将数据整理的过程;搜索则是判断一个元素是否属于一个集合的过程。可以说搜索和排序这两个过程实际上都是为了帮助我们更好的使用手里的数据。
水平有限,这里只讨论数组的排序和搜索,其他数据结构暂不讨论。下文中的集合实际上也是数组,只是为了表达方便暂时叫做集合。
顺序搜索
顺序搜索,是我们能想到的最简单也是最无脑的搜索方式。通过遍历集合中的每一个元素,来寻找符合条件的元素并输出结果。这里有两个点,遍历和符合条件:遍历,就是说要寻遍每个元素(当然,如果需求是找到一个符合条件就输出找到,那运气好第一个元素就是要找的元素,那只要寻一个元素);符合条件就是用来判断集合中的某个元素是否满足我们搜索的目的。
所以可以断言,搜索的平均和最坏情况就是搜索整个集合,而最好的情况就是搜索一次。
最好情况 | 平均情况 | 最坏情况 |
---|---|---|
O(1) | O(n) | O(n) |
利用Python代码来描述就是
def seqSearch(A, target):
for a in A:
if(a == target):
return True
return False
这里输入集合A和目标target就可以利用for循环来遍历集合A寻找和target相同的元素,找到就返回True,没有就返回False。
插入排序
插入排序的思路是很清晰的,通过将后续的元素插入前面已经排好序的数据来实现排序。由于前面的数据已经排好顺序,所以在判断插入元素的顺序时可以很简单。
def insertSort(A):
for i in range(1, len(A)):
insert(A, i, A[i])
def insert(A, pos):
i = pos - 1
while i >= 0 and A[i] > A[pos]:
i = i - 1
if i != pos-1:
move(A, pos, i + 1)
def move(A, fromPos, toPos):
value = A[fromPos]
for i in range(fromPos,toPos,-1):
A[i] = A[i-1]
A[toPos] = value
这里输入集合A,对A进行排序。insertSort作用是将集合A遍历,拎出待排序的元素A[pos]。insert进行具体的插入排序,以从小到大排序,在已经排序好的序列(A[0]~A[pos - 1])中从后向前寻找第一个小于A[pos]的元素A[i]。如果i的值没发生过变化,说明A[pos]比之前的元素都大,可以直接拎下一个元素进来排序。如果i的值发生变化,说明需要把A[pos]插入到之前排序好的序列中,这个任务由move执行。move将A[fromPos]的值存储在value中,将插入位置toPos及其以后的元素全部后移1,最后将value赋值于A[toPos]。
[17, 45, 23, 36, 40, 4, 10, 44, 35, 20, 19, 32, 3, 25, 11]
[17, 45, 23, 36, 40, 4, 10, 44, 35, 20, 19, 32, 3, 25, 11]
[17, 23, 45, 36, 40, 4, 10, 44, 35, 20, 19, 32, 3, 25, 11]
[17, 23, 36,45, 40, 4, 10, 44, 35, 20, 19, 32, 3, 25, 11]
[17, 23, 36, 40, 45, 4, 10, 44, 35, 20, 19, 32, 3, 25, 11]
[4, 17, 23, 36, 40, 45, 10, 44, 35, 20, 19, 32, 3, 25, 11]
[4, 10, 17, 23, 36, 40, 45, 44, 35, 20, 19, 32, 3, 25, 11]
[4, 10, 17, 23, 36, 40, 44, 45, 35, 20, 19, 32, 3, 25, 11]
[4, 10, 17, 23, 35, 36, 40, 44, 45, 20, 19, 32, 3, 25, 11]
[4, 10, 17, 20, 23, 35, 36, 40, 44, 45, 19, 32, 3, 25, 11]
[4, 10, 17, 19, 20, 23, 35, 36, 40, 44, 45, 32, 3, 25, 11]
[4, 10, 17, 19, 20, 23, 32, 35, 36, 40, 44, 45, 3, 25, 11]
[3, 4, 10, 17, 19, 20, 23, 32, 35, 36, 40, 44, 45, 25, 11]
[3, 4, 10, 17, 19, 20, 23, 25, 32, 35, 36, 40, 44, 45, 11]
显然,如果排序的集合本身有序,这个算法就是遍历一遍确认一下就好了,如果无序,那么每个insert还需要执行移动操作。所以有
最好情况 | 平均情况 | 最坏情况 |
---|---|---|
O(n) | O(n2) | O(n2) |
堆排序
我有意略过选择排序,直接进行堆排序。道理很简单,一是选择排序速度很慢,二是堆排序参考选择排序的思路。以从小到大排序为例,存在有n个元素集合A,找到A的最大元素,与A[n-1]交换;找到A[0]~A[n-2]的最大元素,与A[n-2]交换;以此类推。堆排序的思路和选择排序很相似,利用“堆”的特性来寻找最大值,其他和选择排序一样。
首先介绍“堆”。这里的“堆”其实就是一个二叉树,但是有两个特点:一是当且仅当深度k-1上存在2k-1个结点,深度k上才会存在叶子结点,并且节点按照从左到右添加;二是父节点的值大于等于子节点。我们把根结点的深度定义为0,每多一层深度加1,这样第k-1层最多放2k-1个结点,放满以后往第k层放,从左向右放。任意一个结点i的左右两个子结点如果存在的话分别是2i+1、2i+2,父结点存在的话就是(i-1)/2。按照这个规律,我们给出一个包含n个元素的集合A,可以知道A[n-1]是集合最后一个元素,它的父结点是A[(n-2)/2]。
利用heapify来实现一个包含递归的函数来实现堆的生成。
def heapify(A, index, max):
largest = index
left = 2 * index + 1
right = 2 * index + 2
if left < max and A[left] > A[index]:
largest = left
if right < max and A[right] > A[largest]:
largest = right
if largest != index:
A[index], A[largest] = A[largest],A[index]
heapify(A, largest, max)
这里输入一个集合A,父结点index,最大结点编号max。子结点如果存在就有left和right两个结点,如果子结点比父结点大,挑出最大的子结点来和父结点交换,此时换下来的父结点可能比孙结点的两个数小,所以还需要把换下来的父结点和孙结点来一下heapify(poor guy可能不配做孙子)。如果父结点比较大就省事了。
有了heapify就可以把整个集合堆化,用buildHeap实现。
def buildHeap(A):
for i in range(int(len(A) / 2) - 1, -1, -1):
heapify(A, i, len(A))
buildHeap把集合A变成满足堆排序所定义的堆的性质的集合。此时根节点A[0]这个祖宗必然是最大的。回想选择排序的思路:找到最大的元素扔到后面。上heapSort
def heapSort(A):
buildHeap(A)
for i in range(len(A) - 1, 0, -1):
A[0], A[i] = A[i], A[0]
heapify(A, 0, i)
heapSort每次把堆顶的数扔到集合尾部,然后把集合前部的元素再整理成堆,不断往复直到前部就剩一个元素。一个有n个元素的集合A的堆最深就是log2n,所以heapify顶天也就log2n,再考虑调用heapify的循环次数和元素个数有关,所以堆排序应该是O(nlog2n)的。
二叉搜索树搜索
在介绍二叉搜索树搜索之前,先简要介绍一下二分搜索。二分搜索是一种我们生活中常见的搜索,看过猜价格节目的人都知道,随着主持人喊出“高了”或“低了”,竞猜人也在不断报出猜测的价格。竞猜人可以使用二分搜索类似的策略,根据不断调整的上下限给出中间值,这样每次猜测都会使总的范围缩减一半。所以单独考虑二分搜索搜索的过程可知其为O(log2n)。
但是,真正我们需要搜索的数据很少有这么一个主持人孜孜不倦的告诉你高了、低了。想要实现二分搜索需要数据事先排好序。如果数据是不变的,那还好,一次操作终身享受。但是一旦数据需要不断添加、删除,维护数据的有序性将会是一件痛苦的事情。我们还知道排序一般需要O(nlog2n),所以跳过二分搜索,直接上二叉搜索树搜索吧。
先建一个二叉搜索树的类来对二叉搜索树进行描述。
class BinNode:
def __init__(self,value=None):
self.value = value
self.left = None
self. right = None
def add(self,val):
if val <= self.value:
if self.left:
self.left.add(val)
else:
self.left = BinNode(val)
else:
if self.right:
self.right.add(val)
else:
self.right = BinNode(val)
class BinTree:
def __init__(self):
self.root = None
def add(self,val):
if self.root:
self.root.add(val)
else:
self.root = BinNode(val)
def contains (self,target):
node = self.root
while node:
if target == node.value:
return True
if target < node.value:
node = node.left
if target > node.value:
node = node.right
return False
这里利用BinNode来作为树的结点,BinTree作为二叉搜索树。BinNode包含值value、左子结点left、右子结点right以及add来实现子结点的添加。这里需要满足左、父、右三个结点从大到小排序。BinTree是树本身,BinTree.add用来设置根结点或者添加结点。contains 则是二叉搜索树搜索的简单实现,思路就是二分搜索的思路。
二叉搜索树本身很简单,但是它有一个很致命的问题,那就是二叉搜索树在极端条件下会变成一个链表。试想二叉搜索树的根结点本身是最大值,而我们不断去输入数值不断减小的数据,那么每个结点都只会有左子结点或者没有子结点,n个元素有n个结点,二分搜索退化成了顺序搜索。
为了解决这个问题,需要保证二叉搜索树能够平衡,于是有了AVL树。AVL树多定义一个高度来描述树上的结点,叶子结点的高度是0,非叶子结点的高度是最大子结点高度加1,空结点的高度是-1。再考虑高度差,使每个结点的子结点的高度差绝对值不大于1。下图除了最左边的树是满足AVL树要求的,其余都是不满足的。不妨将不满足的四种情况分别根据子结点的位置定义为左左、左右、右左和右右。其中左左只要把3变成2的右节点,2取代3原来的位置就好了。右右同理。左右实际上就是左左交换1、2,右左是右右交换了2、3。
这里为了方便描述定义左旋和右旋。左旋就是将一个结点N的右子结点R变成N的父结点,N变成R的左子结点,然后R取代N原来的位置,R的子结点GL变为N的右结点;右旋就是将结点N的左子结点L变为N的父结点,N变成L的右子结点,然后L取代N的位置,L的子结点GR变为N的左结点。下图分别是左旋和右旋。
定义了左旋和右旋后就可以重新组织语言来描述不满足AVL树的情况转化为满足AVL的情况所需的操作了。借用之前定义四种情况的图中的元素,左左:3、2右旋,左右:1、2左旋后3、2右旋,右右:1、2左旋,右左:3、2右旋后1、2左旋。
class BinNode:
def __init__(self,value=None):
self.value = value
self.left = None
self.right = None
self.height = 0
def addSubTree(self, parent, val):
if parent is None:
return BinNode(val)
parent = parent.add(val)
return parent
def add(self,val):
newRoot = self
if val <= self.value:
self.left = self.addSubTree(self.left, val)
if self.heightDifference() == 2:
if val <= self.left.value:
newRoot = newRoot.rotateRight()
else:
newRoot.left = newRoot.left.rotateLeft()
newRoot = newRoot.rotateRight()
else:
self.right = self.addSubTree(self.right, val)
if self.heightDifference() == -2:
if val > self.right.value:
newRoot = newRoot.rotateLeft()
else:
newRoot.right = newRoot.right.rotateRight()
newRoot = newRoot.rotateLeft()
newRoot.computeHeight()
return newRoot
def computeHeight(self):
height = -1
if self.left:
height = max(height,self.left.height)
if self.right:
height = max(height,self.right.height)
self.height = height + 1
def heightDifference(self):
leftH = 0
rightH = 0
if self.left:
leftH = 1 + self.left.height
if self.right:
rightH = 1 + self.right.height
return leftH - rightH
def rotateLeft(self):
newRoot = self.right
grandson = newRoot.left
newRoot.left = self
self.right = grandson
self.computeHeight()
return newRoot
def rotateRight(self):
newRoot = self.left
grandson = newRoot.right
newRoot.right = self
self.left = grandson
self.computeHeight()
return newRoot
class BinTree:
def __init__(self):
self.root = None
def add(self,val):
if self.root:
self.root = self.root.add(val)
else:
self.root = BinNode(val)
def contains (self,target):
node = self.root
while node:
if target == node.value:
return True
if target < node.value:
node = node.left
elif target > node.value:
node = node.right
return False
BinTree部分基本没变化。BinNode加了一个height来描述结点的高度,add考虑高度差为±2时候根据不同状态旋转,computeHeight计算高度,heightDifference计算高度差,rotateLeft和rotateRight分别实现左旋和右旋。
前面一直强调二叉搜索树对动态数据搜索很友好,那么除了添加数据,有时候还需要删除数据。考虑二叉搜索树的特性,显然一个结点的左子树的最大值肯定没有右子结点,有的话右子结点才是最大值。这样的话,如果我们要删除的目标结点有右结点,直接用右节点来替代原来的结点;如果没有右节点,就把左子树的最大值提上来替代目标结点。这步完成以后检查一下高度差是否满足,不满足调整到满足AVL树要求。
def removeFromParent(self, parent, val):
if parent:
return parent.remove(val)
return None
def remove(self,val):
newRoot = self
if val == self.value:
if self.left is None:
return self.right
child = self.left
while child.right:
child = child.right
childKey = child.value
self.left = self.removeFromParent(self.left, childKey)
self.value = childKey
if self.heightDifference() == -2:
if self.right.heightDifference() <= 0:
newRoot = self.rotateLeft()
else:
self.right = self.right.rotateRight()
newRoot = self.rotateLeft()
elif val < self.value:
self.left = self.removeFromParent(self.left, val)
if self.heightDifference() == -2:
if self.right.heightDifference() <= 0:
newRoot = self.rotateLeft()
else:
self.right = self.right.rotateRight()
newRoot = self.rotateLeft()
else:
self.right = self.removeFromParent(self.right, val)
if self.heightDifference() == 2:
if self.left.heightDifference() >= 0:
newRoot = self.rotateRight()
else:
self.left = self.left.rotateLeft()
newRoot = self.rotateRight()
newRoot.computeHeight()
return newRoot
BinNode添加两个方法来删除结点,remove操作整个删除,removeFromParent保证删除子结点的结点。remove中如果删除的结点在左子树,那么左子树可能高度会减小,导致不平衡。此时heightDifference有且只能是-2,再根据右子树的heightDifference来判断,如果此时右子树的heightDifference≤0则类似之前insert时的“右右”,如果大于0则类似“右左”。如果删除的结点在右子树上处理的方法类似。
我们再给BinTree加一个remove来进行AVL树删除结点的操作。
def remove(self, val):
self.root = self.root.remove(val)
由于AVL树的最大层数是log2n,所以有关的搜索、插入、删除都是O(log2n)。
快速排序
快速排序是一种很受欢迎的排序,思路也很简单,就像切土豆丝,通过不断把数据分为两个部分,最后实现分而治之。但是“切土豆丝”也是需要技巧的,如何一刀对半切开,切到什么程度可以直接开始切丝都是问题。这里直接介绍利用三中值分区策略和设置最小切分长度优化的快速排序。三中值分区策略指的是在待排序集合中随机选择三个元素,三者的中值即为中枢值。所谓中枢值就是下刀的地方。设置最小切分长度指的是对于小于某个大小的集合采用其他排序方式实现排序。这里一般采用插入排序。
先按照这个思路实现快速排序quickSort。
def quickSort(A, left, right):
if left < right:
if right - left <= 7:
insertSortShort(A, left, right)
return
p = partition(A, left, right)
quickSort(A, left, p-1)
quickSort(A, p + 1, right)
quickSort就是把集合A指定的左右端进行切分,利用partition实现切分并输出中枢值,对于小于某个值(这里用7)的集合采用插入排序insertSortShort。
def partition(A, left, right):
center = int((left + right) / 2)
if A[left] > A[center]: A[left], A[center] = A[center], A[left]
if A[left] > A[right]: A[left], A[right] = A[right], A[left]
if A[center] > A[right]: A[center], A[right] = A[right], A[center]
pivot = A[center]
A[center], A[right] = A[right], A[center]
pos = left
for idx in range(left,right):
if A[idx] < pivot:
A[idx], A[pos] = A[pos], A[idx]
pos = pos + 1
A[pos], A[right] = A[right], A[pos]
return pos
partition道理上需要进行随机,但是由于集合(这里指的是从A[left]到A[right]的局部)本身排序我们就认为是随机的,所以不妨就选取集合的左中右三个元素。通过不断交换使A[center]为三个元素的中值,并存储为中枢值pivot。之后再将A[center]与A[right]交换,将中枢值先放在集合尾部。从左到右遍历这个集合,将小于中枢值pivot的元素至于集合左侧。最后将A[right]中的中枢值再换回A[pos],实现A[pos]左侧的元素都比中枢值小,返回中枢值的位置pos。
def insertSortShort(A, left, right):
for i in range(left + 1, right+1):
insertShort(A, i, A[i], left)
def insertShort(A, pos, value, left):
i = pos-1
while i >= left and A[i] > value:
i = i - 1
if i != pos - 1:
move(A, pos, i + 1)
insertSortShort其实是直接利用之前插入排序的代码修改的,添加一个left来实现堆集合局部的插入排序(若left = 0,则就是之前的插入排序)这里不加赘述。
然后我们假惺惺的加个帽子。
def qSort(A):
quickSort(A, 0, len(A)-1)
用qSort来把quickSort包起来,只需要留一个放集合A的地方。
归并排序
大部分排序算法都不需要额外的存储空间,但是这里介绍的归并算法需要O(n)的存储空间来实现O(nlog2n)的性能。
def mergeSort(A):
copy = list(A)
divideMerge(copy, A, 0, len(A))
def divideMerge(A, result, start, end):
if end - start < 2:
return
if end - start == 2:
if result[start] > result[start + 1]:
result[start], result[start + 1] = result[start + 1], result[start]
mid = (end + start) // 2
divideMerge(result, A, start, mid)
divideMerge(result, A, mid, end)
i = start
j = mid
idx = start
while idx < end:
if j >= end or (i < mid and A[i] < A[j]):
result[idx] = A[i]
i = i + 1
else:
result[idx] = A[j]
j = j + 1
idx = idx + 1
mergeSort先复制集合A,然后调用divideMerge。divideMerge通过不断将A、result对半切开直到切成元素个数不大于2的小集合,然后对小集合进行排序。排序之后将两个小集合合并成一个集合,利用的方法是利用两个子集合的初始位置i、j,以及合并之后的集合初始位置idx,不断比较A[i]、A[j]。由于两个子集合本身已经排好序,通过比较A[i]、A[j],不断把较小的元素放置在idx上,idx和较小的数的标号加一,这样归并就实现了。
我们可以知道,把一个元素个数为n的集合等分分成小于等于2的子集合需要log2n次,每次分割的归并实际都执行了n次,所以显然总体的性能时O(nlog2n)。
桶排序
桶排序顾名思义就是先按照元素个数n给出对应的桶数n,这里默认桶本身是有序的,再将元素根据一定的方式放到桶中,若桶中的元素超过一个,对桶中元素排序,再按照桶的顺序把里面的元素取出就完成了。
首先,先建一个桶的类bucket,这里使用链表的形式来存放桶中的数据。
class bucket:
def __init__(self, value = None):
self.value = value
self.next = None
def add(self, value):
if self.value is None:
self.value = value
elif self.next is None:
self.next = bucket(value)
else:
self.next.add(value)
def output(self):
temp = []
temp.append(self.value)
while self.next:
temp.append(self.next.value)
self = self.next
return temp
def sort(self):
temp = self.output()
insertSort(temp)
return temp
bucket中output用来将桶中的数据数组化,方便直接使用之前的insertSort排序。sort就是使用插入排序对桶中的数据进行排序。
def bucketSort(A):
buckets = []
idx = 0
for i in range(len(A)):
buckets.append(bucket())
for a in A:
buckets[hash(i)].add(a)
for i in range(len(A)):
temp = buckets[i].sort()
for t in temp:
if t:
A[idx] = t
idx = idx + 1
def hash(i):
return int(i/3)
bucketSort分三步,建桶,放桶,取出。取出过程中先把桶里面的数据排序,再依次取出即可。hash是为了特定需求设计的简易散列函数,这里假设待排序的数据为0~44之间任取15个数进行排序。也不妨岔开话题讲一讲如何测试排序和搜索算法。
import random
A= random.sample(range(0,45),15)
A就是0~44之间任取15个数的数组,利用random的sample可以很轻松的获取测试算法用的数据。
回到桶排序。根据概率论,可以知道当桶的数量和元素个数一致时,桶中元素个数的期望为1,方差为σ2=np(1-p)=(1-1/n),其中n为元素个数,p为一个元素进入一个特定桶的概率。又可以知道,插入排序最坏情况下为O(n2),有E(n2)=σ2(n)+E2(n)=2-1/n,即插入排序期望性能可视为常数,故桶排序为O(n)。
散列搜索
散列搜索利用的就是类似之前所使用的桶排序的原理,利用给定的桶和散列函数,将待搜索的数据通过散列函数装入不同的桶中。这里的散列函数是一个确定的函数,能够将给定的元素ei映射成一个整数hi。若有b个桶编号为0到b-1,hi要满足0≤hi<b。散列函数还需要保证当ei、ej相等时有hi、hj相等。这样原来复杂的搜索就转化为在已知的桶中寻找目标的简单问题了。
当然,之前桶排序的例子可以发现,一个桶中可能包含不止一个元素。这种情况发生时可以采用链表的方法来保存桶中数据,也可以开放定址的方法将数据以一定的方式放入空桶中,这个后续再说。
def hashCode(S, b):
hash = 0
for s in S:
hash = hash * 31 + ord(s)
return hash % b
def loadTable(A, b):
H = []
idx = 0
for i in range(0, b):
H.append(bucket())
for a in A:
h = hashCode(a, b)
H[h].add(a)
return H
def hashSearch(A, t):
numB = pow(2, int(math.log(len(A), 2)))
H = loadTable(A, numB)
h = hashCode(t, numB)
temp = H[h]
while temp:
if H[h].value == t:
return True
else:
temp = temp.next
return False
这里直接用之前桶排序的桶类bucket。首先利用loadTable来建hash表,其中hashCode是散列函数,将给定的字符串A散列化,b是桶的数量。这里将hashCode计算的结果对桶的数量b取余,方便放到桶中。hashSearch是具体执行搜索的函数,和桶排序类似思路就是建表、算值、搜索。
散列搜索还有一种方法,能够不使用链表,而是通过将冲突的散列值按照一定规律储存于其他空桶中,这就是开放地址法。首先,我们定义一个负载因子α=n/b,n是元素个数,b是桶数。当α小于1时必然存在空桶,此时对有冲突的桶实施开放地址才有意义。
class hashTable:
def __init__(self, b = 1024, deviation = 37, hashFunc = None, probFunc = None):
self.b = b
self.bins = [None] * b
self.deleted = [False] * b
if hashFunc:
self.hashFunc = hashFunc
else:
self.hashFunc = lambda value, size: hashCode(value, size)
if probFunc:
self.probFunc = probFunc
else:
self.probFunc = lambda hk, size, i: (hk + deviation * i) % size
def add(self, value):
hk = self.hashFunc(value, self.b)
ctr = 1
while ctr <= self.b:
if self.bins[hk] is None or self.deleted[hk]:
self.bins[hk] = value
self.deleted[hk] = False
return ctr
if self.bins[hk] == value and not self.deleted[hk]:
return ctr
hk = self.probFunc(hk, self.b, ctr)
ctr += 1
return -self.b
def delete(self, value):
hk = self.hashFunc(value, self.b)
ctr = 1
while ctr <=self.b:
if self.bins[hk] is None:
return -ctr
if self.bins[hk] == value and not self.deleted[hk]:
self.deleted[hk] = True
return ctr
hk = self.probFunc(hk, self.b, ctr)
ctr += 1
return -self.b
def search(self, target):
hk = self.hashFunc(target, self.b)
ctr = 1
while ctr <=self.b:
if self.bins[hk] is None:
return False
if self.bins[hk] == target:
if self.deleted[hk] == False:
return True
else:
return False
hk = self.probFunc(hk, self.b, ctr)
ctr += 1
return False
这里创建hashTable类来实现整个开放地址法。probFunc是采用开放地址法的线性探查,即当插入的元素冲突时偏移常数个地址再次尝试插入。此时新的hk = (hk + deviation * i) % size。add、delete、search,分别执行增、删、查的功能。思路很简单,不详细说了。
后记
本文简单介绍了顺序搜索、插入排序、堆排序、二叉搜索树搜索、快速排序、归并排序、桶排序、散列搜索这些常见的排序和搜索算法。只是从入门的角度将这些算法的大致思路和python代码记录在文章中,部分的代码来自互联网和算法书,充其量只是一篇班门弄斧的读书笔记。其中内容可能存在比较多的问题,多多包涵,后续增补要看情况。