前段时间鼓起勇气和老板说了一下以后想从事机器学习方向的工作,所以最好能有一份不错的实习,希望如果我有好的机会他可以让我去,没想到老板非常通情达理,说人还是要追寻自己感兴趣的东西,忙完这阵你就去吧。所以最近开始疯狂地投实习生简历,各家春招都去投了试试。那天第一次面试去了网易,面试官感觉很年轻,也挺有耐心的,前面机器学习部分基本都没什么问题,最后说那写写代码吧,先来个快排吧,当时感觉有点懵,说了一句我不是计算机专业的,我可以陈述这个算法的原理但是快速地写可能写不出来,现在想想真是个败笔……后来面试官为了给我个台阶下,让我写了个二分查找,这个确实简单,但自己还是写糊了,再后来面试官出去转了一圈回来说二轮面试官有事,今天就先到这儿吧,就这样挂了第一家……
回来以后,痛定思痛,一些基本的排序算法啊什么的还是要随手就能写啊,所以今儿就把几种最常见的排序算法用Python实现以下,防止后面再出现上面的那种遗憾。
===========================================================================
1插入排序
第一个数自然有序不用动,从第二个位置开始,依次向前扫描,如果前面一个数大于该数则交换两者位置,直到首尾置或者前面一个数小于该数,这样有序数组的长度不断增长,直到最后一个位置排序就完成了。
对于插入排序,除了首尾置每个位置要扫描一次,每次扫描的平均长度n/2,所以整体算法的平均复杂度O(n2),假如数组本来就有序,那么只需要n-1次比较即可。由于是两两之间进行比较,不会产生新的逆序,算法是稳定的。
Python实现如下
def insert_sort(lst):
for i in range(1,len(lst)):
j=i
num=lst[j]
while j>0 and lst[j-1]>num:
lst[j]=lst[j-1]
j-=1
lst[j]=num
2 选择排序
从第一个位置开始整体扫描数组一遍,找到最小的数把它移动到首尾置,然后首尾置+1,再次从首尾置出发找到最小的数移动到当前的首尾置上,如此进行下去直到首尾置和末位置重合即完成了排序。
选择排序时间复杂度不受数组本来分布的影响,稳定的是O(n2),因为每次从一个位置开始都要扫描到数组的尾部。选择排序把最小的数移动到当前首尾置,如果是数组采用交换的话有可能会破坏算法稳定性,因为在最小值之前可能存在于首元素相等的元素,但如果使用链表实现,也就是不交换两者的位置,二是直接把最小的元素插到当前首尾置之前这样算法就又是稳定的了。
Python实现如下
def select_sort(lst):
for i in range(len(lst)):
index=i
for j in range(i,len(lst)):
if lst[j]<lst[index]:
index=j
lst[i],lst[index]=lst[index],lst[i]
3 堆排序
选择排序的主要缺点就在于它每次都要从头到尾整体比较依次,整个排序过程下来有很多重复的操作。这里可以利用堆这种数据结构来实现更加高效的查找。也就是说数组来了,先建堆,从第一个非叶子节点开始直到根节点,将每个子树都调节成堆,需要注意的是有的子树调整完了会对其再下一层子树产生影响。建堆完成之后,堆顶存储的就是数组中的最大值,将其交换到最后一个位置然后就忽略它,再调整当前堆,再把堆顶元素换下来,如此一直进行下去就完成了排序。
对于堆排而言,首先建堆的时间是O(n),然后每次选择一个元素的时间不超过O(log(n)),整体的复杂度就是O(n*log(n))。堆排调整元素的时候其移动的路劲和自然顺序交叉,所以它是明显不稳定的。
Python实现如下
def adjust(lst,root,end):
left=2*root+1
right=2*root+2
larger=root
if left<end and lst[left]>lst[larger]:
larger=left
if right<end and lst[right]>lst[larger]:
larger=right
if larger!=root:
lst[root],lst[larger]=lst[larger],lst[root]
adjust(lst,larger,end)
def build_heap(lst):
loc=len(lst)//2-1
while loc>=0:
adjust(lst,loc,len(lst))
loc-=1
def heap_sort(lst):
build_heap(lst)
for i in range(len(lst)-1,-1,-1):
lst[0],lst[i]=lst[i],lst[0]
adjust(lst,0,i)
4冒泡排序
冒泡排序就是从第一个元素开始,如果其大于下一个元素则交换两个元素的位置,这样一遍扫描下来就将最大的元素移动到了数组的尾部,第二趟忽略第一次找到的那个最大的再从头开始扫描,再次找到最大的将其沉到当前的尾部,如此反复进行下去就可以完成排序。
如果依照上面的算法,冒泡排序每次都需要扫描n趟,每趟扫描的元素数量从n开始递减。整体复杂度是O(n2)。当然还可以做一些优化,比如某一趟扫描中没有发现任何一个逆序那就可以直接终止扫描,数组已经有序了,比如一个已经排好序的数组,只需要一趟扫描,n次比较就可以完成。
Python实现如下
def bubble_sort(lst):
for i in range(len(lst)):
flag=True
for j in range(1,len(lst)-i):
if lst[j-1]>lst[j]:
lst[j-1],lst[j]=lst[j],lst[j-1]
flag=False
if flag:
break
5 快速排序
终于到了我的心头之痛……快排的原理其实挺简单的,首先选择一个元素作为标准,然后将数组中比它小的元素移动到它的左边去,比它大的则移动到右边,然后再分别对左边和右边的数组递归使用快排。这里最主要的一个问题就是如何将数组中的小于标准的移动到左边,大于标准的移动到右边。通常我们选取首元素作为标准,那么相当于首尾置就空了,先末位置开始往前扫描找到第一个小于标准的数将其移动到首尾置,这下后面又剩下一个空位置,我们再从第二个元素开始往后扫描找到第一个大于标准的数将其移动到后面的空位置上,就这样反复进行,直到这两个位置相交,再将标准移动到小于标准元素的最后一个位置。当然我们也可以采用另一种简单一点的方式,分别用两个坐标表示小于标准元素和大于标准元素的最后一个位置,然后从标准后的元素开始扫描,如果该元素小于标准,则小于标准元素的最后位置坐标+1,交换那个位置和当前位置的元素,扫描到数组尾部,再交换标准元素和小于标准元素的最后一个位置。
快排的效率和元素划分的关系很大,如果每次都是划分成相等的两段那么复杂度大概在O(n*log(n)),如果每次划分只是将标准划分出去,那么需要的复杂度就是O(n2)。但是抽象地看,快排这种划分其实就是二叉树,其平均高度应该是O(og(n)),那么整体的平均复杂度应该是O(n*log(n))。快排明显是不稳定的,因为在依据标准划分成两部分的时候很有可能产生新的逆序,所以对于快排而言即使原序列非常接近有序也不会使其变得更加高效。
Python实现如下
def quick_sort(lst,l=0,r=len(lst)-1):
if l>=r:
return
i=l
j=r
pivot=lst[i]
while i<j:
while i<j and lst[j]>=pivot:
j-=1
if i<j:
lst[i],lst[j]=lst[j],lst[i]
i+=1
while i<j and lst[i]<=pivot:
i+=1
if i<j:
lst[i],lst[j]=lst[j],lst[i]
j-=1
lst[i]=pivot
quick_sort(lst,l,i-1)
quick_sort(lst,i+1,r)
def quick_sort1(lst,start=0,end=len(lst)-1):
if start>=end:
return
pivot=lst[start]
i=start
for j in range(start+1,end+1):
if lst[j]<pivot:
i+=1
lst[i],lst[j]=lst[j],lst[i]
lst[start],lst[i]=lst[i],lst[start]
quick_sort1(lst,start,i-1)
quick_sort1(lst,i+1,end)
6 归并排序
感觉比较快的排序都用了递归的思想,线性扫描的时间实在是太慢了。归并排序的想法就是每次按照一定的长度对序列进行划分,每个子序列是有序的,下一步子序列长度*2,两两合并成有序,依次进行下去。需要注意的部分就是序列的长度不一定是2的幂,所以最后剩下来的部分需要单独考虑一下,如果其小于一个序列长度,直接添加在归并完的序列后面,如果其大于一个序列长度,则将其划分成一个序列和一个短序列进行合并。
归并的复杂度很明显是O(n*log(n))。对于稳定性而言,我们只要注意处理遇到相同值的时候将前面的值排在前面,那么归并排序算法就是稳定的。
Python实现如下
def merge(lfrom,lto,low,mid,high):
i,j,k=low,mid,low
while i<mid and j<high:
if lfrom[i]<=lfrom[j]:
lto[k]=lfrom[i]
i+=1
else:
lto[k]=lfrom[j]
j+=1
k+=1
while i <mid:
lto[k]=lfrom[i]
i+=1
k+=1
while j <high:
lto[k]=lfrom[j]
k+=1
j+=1
def merge_pass(lfrom,lto,llen,slen):
i=0
while i+2*slen<llen:
merge(lfrom,lto,i,i+slen,i+2*slen)
i+=2*slen
if i+slen<llen:
merge(lfrom,lto,i,i+slen,llen)
else:
for j in range(i,llen):
lto[j]=lfrom[j]
def merge_sort(lst):
slen,llen=1,len(lst)
templst=[None]*llen
while slen<llen:
merge_pass(lst,templst,llen,slen)
slen*=2
merge_pass(templst,lst,llen,slen)
slen*=2
=====================================================
完成了这些算法之后,我们用一个例子来看看这些算法的运行时间分别是多少,看看是否和复杂度分析的一致。
随机在1到10000生成10000个数,然后分别用Python自带的sorted方法,插入排序,选择排序,堆排序,两种快排和归并排序方法进行排序并统计排序时间,重复进行10次取平均值,代码如下
def test(sort,lst):
start=time.time()
sort(lst)
end=time.time()
return end-start
time_rec=[]
for i in range(10):
lst=[random.randint(0,10000) for x in range(10000)]
func=[sorted,insert_sort,select_sort,heap_sort,quick_sort,quick_sort1,merge_sort]
for j,fun in enumerate(func):
dt=test(fun,copy.deepcopy(lst))
print j,dt
time_rec.append(dt)
average_time=[]
for start in range(7):
average_time.append(sum(time_rec[start::7])/10)
最后的结果
Sorted:0.00280003547668 Insert_sort:3.81039996147
Select_sort:3.1276999712 Heap_sort:0.0629999876022
Quick_sort:0.0250000476837 Quick_sort1:0.0212999820709
Merge_sort:0.0320999860764
插入和选择排序的确慢得令人发指,然后两种快排和归并排序比其高两个量级,系统自带的sorted方法则又快了一个量级。算一算,10000/(log210000)差不多是752,这样高两个量级也是合情合理啊。但是还是系统自带的厉害啊,它用的是蒂姆排序,它是一种混成式的方法,也就是结合了简单排序和复杂排序各自的优点,这里我就不去详细讨论了。
希望找实习可以顺利一点啊~~~~