插入排序
排序和查找是2个重要的算法领域,两者既有区别又有联系。查找不一定要排序,但是排序必须查找。
因此对于排序算法,记住最好是边查找边排序,不要把查找和排序分开操作。比如下面这个插入排序的例子:
def insert_sort( arr ):
length = len(arr)
if length < 2:
return arr
for i in xrange( 1, length ):
for j in xrange(0, i):
if arr[i] < arr[j]: #先找出arr[i]的位置
tmp = arr[i]
for k in xrange(i, j, -1): #再重新排序
arr[k] = arr[k-1]
k -= 1
arr[j] = tmp
上面这个算法先查找后排序,这和从哪头开始查找有关。有时候查找和排序可以一起进行,下面这个插入排序,当找到curVal的位置时,排序也基本完成,只需要一条语句:arr[ curIndex ] = curVal
def insert_sort_opt( arr ):
if len(arr) < 2:
return arr
for i in xrange(1,len(arr)):
preIndex = i - 1
curIndex = i
curVal = arr[ curIndex ]
#下面的双向扫描的核心代码
while preIndex >= 0 and arr[preIndex] > curVal:
arr[ curIndex ] = arr[ preIndex ] #查找和排序一起
curIndex = preIndex
preIndex -= 1
arr[ curIndex ] = curVal
return arr
一、插入排序
https://www.cnblogs.com/lanhaicode/p/11259509.html
1、将待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列;
2、取出下一个元素,在已经排序的元素序列中从后向前扫描;
3、如果该元素(已排序)大于新元素,将该元素移到下一位置;
4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5、将新元素插入到该位置后;
6、重复步骤2~5。
复杂度分析:插入排序效率比较低,最好情况也要O(N),最坏情况还要O(n^2)。但对于小规模的数组排序而言,还是不错的。
二、快排
插入排序的变化不太多,但对于快排,可以有许多优化的方法,下面针对top-k问题来谈谈快排的优化。
问题:尽可能快地找到前K小的元素。top-k问题
https://www.cxyxiaowu.com/8647.html
核心思路:每次缩小查找的范围,最后锁定答案。将大问题拆解成小问题,通过对小问题的解决来搞定原本的大问题。
做法:每次是设置一个标杆(取其中一个元素作为标杆),然后对数组当中的元素进行调整,保证比标杆小的元素都在它的左边,比它大的都在它的右边。标杆最后在的位置就是数据有序之后它正确的位置。
复杂度分析:理想的话每次规模减半,复杂度为O(logN);最糟糕的话数组本身逆序,每次只能排除掉一个元素,复杂度升到O(n^2)。
version 1.0
# 从数组arr中选出最小的k个数
def quick_select( arr, k ):
n = len( arr )
if n <= k:
return arr
bufferArr = []
while arr:
#选择最后一个元素来作为大小判断的标杆
mark = arr.pop() #标杆随意选取,运气不好每次只能排除一个元素,时间复杂度为O(n^2)
less, greater = [], [] #需要额外的2个队列,空间复杂度上升
for x in arr:
if x <= mark :
less.append(x)
else:
greater.append(x)
if len(less) == k:
return less + bufferArr
elif len(less) < k:
bufferArr += less
k -= len(less)
arr = [mark] + greater
else:
arr = less
优化:
- 一,在空间上进行优化,即在自身数组上直接进行排序(查找的同时进行排序),不额外创建新的数组,其次是双向扫描原数组,这样比单向扫描更快。
- 二,在选择标杆上进行优化,BFPRT算法
- 判断数组元素是否大于 5 ,如果小于 5 ,对它进行插入排序,并返回数组的中位数
- 如果元素大于 5 个,对数组进行分组,每 5 个元素分成一组,允许最后一个分组元素不足 5 个。
- 对于每个分组,对它进行插入排序
- 选择出每个分组排序之后的中位数,组成新的数组
- 重复以上操作(算法思路很朴素,其实就是一个不断选择中位数的过程。
version 2.0
#对quick_select进行空间上的优化
#在原数组上直接进行快排操作,不额外增加空间,并且减少了复制数据的操作
#这里使用的是双向扫描,即交替扫描头尾2端数据,具体实现看下面代码
def quick_select_opt( arr, k ):
left = 0
right = len( arr )
if k >= (right - 0): #如果k比数组arr的size还大
return arr;
while (right - 0) != k :
mark = arr[left]
markL, markR = left, right-1
#这是双向扫描的核心代码, 务必记住
while markL < markR:
while arr[markR] > mark and markL < markR: #条件markL<markR很重要
markR -= 1
arr[markL] = arr[markR]
while arr[markL] <= mark and markL < markR:
markL += 1
arr[markR] = arr[markL]
arr[markR] = mark #写回mark
length = markR + 1 # length算式里的+1,是把mark也算进去
if length == k or length - 1 == k:
return arr[0:k] #队列是不包括最后一个元素的
elif length < k:
left = length
else:
right = length - 1
#写代码有时侯为了提高一点效率,选择跳过某些步骤或者提前某些运算,但是除非自己很有把握,不然在紧急情况下还是以稳健性为首要前提。不要为了一点点的效率而随便删减流程,因为可能会有bug,不值得。特别是上机考试的时候,如果没有充足的时间来考虑所有情况,那就更不要对流程随意删减,宁愿选择虽然复杂但是更稳健的办法。
#下面将函数quick_select_opt进行拆分,可能思路会更加清晰
#将数组arr以arr[l]为标杆划分为2部分,左边元素小于arr[l,右边大于
#arr下标范围[l,r]
#返回arr[l]的有序位置
def partion( arr, l, r ):
if l >= r:
return l
mark = arr[l]
while l < r:
while arr[r] > mark and l < r:
r -= 1
arr[l] = arr[r]
while arr[l] <= mark and l < r:
l += 1
arr[r] = arr[l]
arr[l] = mark
return l
def quick_select_opt2( arr, k ):
if len(arr) <= k:
return arr
left, right = 0, len(arr)
while (right - 0) != k:
l, r = left, right-1
length = partion( arr, l, r) + 1
if length == k or length - 1 == k:
return arr[0:k]
elif length > k:
right = length
else:
left = length
#将函数功能细分,编程的时候思路更加清晰一些,代码可读性更高。同时产生bug的几率也小了。
version 3.0
# 插入排序,快排需要用到
def insert_sort( arr ):
if len(arr) < 2:
return arr
for i in xrange(1,len(arr)):
preIndex = i - 1
curIndex = i
curVal = arr[ curIndex ]
while preIndex >= 0 and arr[preIndex] > curVal:
arr[ curIndex ] = arr[ preIndex ]
curIndex = preIndex
preIndex -= 1
arr[ curIndex ] = curVal
return arr
# 返回数组arr的一个元素下标,该元素至少比数组里3/10的元素大。
# start是数组下标的起始值,length是数组的长度
def bfprt( arr, start = None, length = None):
if start is None or length is None:
start, length = 0, len(arr)
if length <= 5:
#这里使用切片,trick
arr[start:start+length] = insert_sort( arr[start:start+length] )
return start + length // 2
idx = start
mid_num = 0
while length > 5:
arr[idx:idx+5] = insert_sort( arr[idx:idx+5] )
arr[start+mid_num], arr[idx+2] = arr[idx+2], arr[start+mid_num]
idx += 5
length -= 5
mid_num += 1
if length > 0: #剩下一些c凑不齐5个的元素
arr[idx:idx+length] = insert_sort( arr[idx:idx+length] )
arr[start+mid_num], arr[idx+length//2] = arr[idx+length//2], arr[start+mid_num]
mid_num += 1
return bfprt( arr, start, mid_num )
#划分数组arr为2部分,左边小于mark,右边大于mark
#数组arr下标范围[left,right]
def partion( arr, left, right ):
mark = arr[left]
while right > left:
while arr[right] > mark and right > left:
right -= 1
arr[left] = arr[right]
while arr[left] <= mark and right > left:
left += 1
arr[right] = arr[left]
arr[left] = mark
return left
# 最终优化
def quick_select_opt( arr, k ):
start = 0
length = len(arr)
if length <= k:
return arr
topN = length
left = start
while topN != k:
markIdx = bfprt( arr, left, topN )
arr[markIdx], arr[left] = arr[left], arr[markIdx]
markIdx = partion( arr, left, left+topN-1 )
if markIdx == k or markIdx + 1 == k:
return arr[0:k]
elif markIdx < k:
left = markIdx + 1
else:
topN = markIdx