前言
排序是算法的入门知识,其经典思想可以用在许多算法中,在实际应用中是相当常见的一类。记得在本科的数据结构课上就有讲过几个经典的排序算法,现在来好好地回顾下。
在回顾之前,了解一个概念,这个概念也是我刚刚了解的。(手动扶额-。-)
排序算法稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
这个稳定的概念有什么用呢?不管,先记着再说啦。
一、冒泡排序(BubbleSort)
两个相邻的数比较大小,较大的数下沉,较小的冒起来。
过程为:
- 比较相邻的两个数据,如果第二个数小,就交换位置;
- 从前往后两两比较,一直到比较最后面两个数据。最终最大的数被交换到末尾位置,这样第一个最大数的位置就排好了;
- 继续重复上述过程,依次将第2、3…n-1个最大的数排好位置。
当然也可以反着来,从后面往前比较,先排好最小的数到数列到开头的位置。
python代码实现:
def bubble_sort(list):
l = len(list)
for j in range(len(list)-1): #单纯地设定循环次数,标识趟次
for i in range(l-1):
if list[i] > list[i+1]:
list[i],list[i+1] = list[i+1],list[i]
else:
pass
l = l - 1 #减少比较次数,最后n位的最大数已经排好,不需要再进行比较了
return list
搬运来的动图,特别直观:
1.1鸡尾酒排序
这是一种冒泡排序的改进算法,可以称之为双向冒泡排序:
- 先对数组从左往右进行升序的冒泡排序;
- 再对数组进行从右往左的降序冒泡排序;
- 循环往复,不断缩小没有排序的数组范围。
def cocktail_sort(list):
l = len(list)
start = 0
end = l - 1
flag = True #标志上一轮循环是否有交换,若无,则表示排序已经完成,无需继续循环(冒泡排序优化点)
while flag:
flag = False
for i in range(start,end,1):
if list[i] > list[i+1]:
list[i],list[i+1] = list[i+1],list[i]
flag = True
else:
pass
end = end - 1
for i in range(end,start,-1):
if list[i] < list[i-1]:
list[i],list[i-1] = list[i-1],list[i]
flag = True
else:
pass
start = start + 1
return list
再搬运一张鸡尾酒排序动图:
二、 选择排序(SelctionSort)
选择排序十分简单直观,步骤如下:
- 在序列中找到最小(大)元素,存放在序列的起始位置;
- 再从剩余的未排序序列种继续寻找最小(大)的元素,存放在已排序序列的后一位;
- 重复到第n-1次,完成排序。
def selection_sort(list):
l = len(list)
for i in range(l-1):
_index = list.index(min(list[i:l])) #也可以再套一层循环,逐一比较出最小值
list[i],list[_index] = list[_index],list[i]
感谢网上的动图:
三、插入排序(InsertionSort)
对于未排序数据,在已排序序列中从后向前扫描,找到相应位置插入。十分类似我们打扑克时抓牌的过程。
- 从第一个元素开始,单个元素当然是可以认为已排序;
- 取出下一个元素,与已排序序列从后向前扫描比较,直到找到已排序元素小于或等于新元素的位置;
- 将新元素插入到找到的位置后一个的位置;
- 继续取下一个元素重复步骤。
def insertion_sort(lists):
l = len(lists)
for i in range(1,l): #大循环,开始依次抽取元素进行插入
_tmp = lists[i]
for j in list(range(i))[::-1]:
if _tmp < lists[j]: #比前一个小就继续前进,被比较元素向后移一位
lists[j+1] = lists[j]
if j == 0: #比较到队首了,说明临时值是最小的
lists[j] = _tmp
else:
lists[j+1] = _tmp
break
return lists
继续搬运:
3.1希尔排序
插入排序对那些基本有序的序列排序效率高,但对于乱序的序列,移动次数非常多导致效率较低。所以就有了插入排序的改进版——希尔排序。希尔排序会优先比较距离较远的元素,又称之为缩小增量排序。
- 设定步长,按步长将原序列分为若干子序列,分别对子序列进行插入排序;
- 逐渐减小步长,重复步骤1。直至步长为1,此时序列基本有序,最后进行一次插入排序。
可以看出,希尔排序可以在一开始就对距离较远的元素进行换位、排序。避免了插入排序中,一个元素在往前比较过程中大量元素被逐一移动的过程。希尔排序的关键就是步长的选择,看到一种说法说步长用质数是个不错的选择;也有一说用序列[1,4,13,40,121,364…]后一元素是前一元素的3倍+1;当然还有更加简单粗暴的len/2,然后依次除以2直至1.
希尔排序动图展示(这里展示的步长分别为5、2、1):
代码略,本质上就是按照步长进行多次插入排序。
四、快速排序(Quicksort)
快速排序简称快排,这个排序算法就厉害了,听说面试官特别喜欢考,所以重点来了,同志们。
- 从序列中取出一个值作为基准;
- 把所有比基准值小的摆放在基准的左边,比基准大的摆在基准的右边(相等的数任意一边)。这就完成了一次分区操作;
- 对左右两个子序列继续做这样的分区操作,直至每个区间只有一个数。这是一个递归过程。
失败的一次尝试:
def quick_sort(arr):
lefti = 0
righti = len(arr) - 1
if righti == -1:
return 0
else:
x = arr[0] #x即为基准
count = 0
k = 0
while lefti < righti and count < 2:
for i in range(righti,lefti,-1):
if righti - lefti == 1:
count += 1
if arr[i] < x and count < 2:
arr[lefti] = arr[i]
righti = i
k = i
lefti += 1
break
for j in range(lefti,righti,1):
if righti - lefti == 1:
count += 1
if arr[j] > x and count < 2:
arr[righti] = arr[j]
lefti = j
k = j
righti -= 1
break
arr[k] = x
quick_sort(arr[:k])
quick_sort(arr[k+1:])
排序过程没问题,只是迭代时修改的不是原数组,而是新的被拆分的数组,导致只有第一层循环的元素交换被保留。无奈还是参照下别的代码学习一哈把,coding能力还是有待加强。
参照资料后的改进版:
def quick_sort(arr,left,right):
if left >= right:
return
low = left
high = right
key = arr[low] #取序列的第一个值为基准
while left < right:
while left < right and arr[right] >= key: #外层循环里已经有left<right但内层循环里仍然需要,因为要保证left和rigth最终会合相等,而不能让left在自增过程中超过right
right -= 1
arr[left] = arr[right]
while left < right and arr[left] <= key:
left += 1
arr[right] = arr[left]
arr[left] = key #此时left和right已经相等
quick_sort(arr,low,left-1)
quick_sort(arr,left+1,high)
大神秀技巧版(一行代码实现):
quick_sort = lambda array: array if len(array) <= 1 else quick_sort([item for item in array[1:] if item <= array[0]]) + [array[0]] + quick_sort([item for item in array[1:] if item > array[0]])
这个lambda真的很精妙,用两个列表生成式拼接出完整列表,所有小于等于arr[0]的放左边,所有大于arr[0]的放右边。劣势是占用了新的内存空间,常规版的快排是in-palce的,都是原地操作。
动图演示:
五、归并排序(MergeSort)
将两个有序序列合并的方法很简单,比较2个序列的第一个数,谁小就取谁,取出后删除对应序列中的这个数。继续比较直至所有元素都被取出。将两个有序序列合并的过程称为2-路归并。归并排序就基于此:
- 把待排序的序列一分为二,分出两个子序列;
- 继续将子序列分裂直至子序列中只有一个元素,一个元素自然就算排序完成;
- 一路分裂一路归并,最终获得完整序列。
def merge(arrX,arrY): #合并排序算法
i,j = 0,0
arrN = []
while i < len(arrX) and j < len(arrY):
if arrX[i] < arrY[j]:
arrN.append(arrX[i])
i += 1
else:
arrN.append(arrY[j])
j += 1
if i == len(arrX):
arrN.extend(arrY[j:])
if j == len(arrY):
arrN.extend(arrX[i:])
return arrN
def merge_sort(arr): #迭代过程
l = len(arr)
if l <= 1:
return arr
else:
X = merge_sort(arr[:round(l/2)])
Y = merge_sort(arr[round(l/2):])
return merge(X,Y)
动图演示:
六、计数排序(CountingSort)
计数排序不是基于比较的排序算法,它依靠一个辅助数组来实现,将输入的数据转化为键存储在专门准备的数组空间中,计数排序要求输入的数据必须是正整数,且最好不要过大。计数排序是用来排序0到100之间的数字且重复项比较多的最好的算法。
- 找到待排序序列种的最大值(也可以找出最小值,建立中间数组时节省一定的空间);
- 统计序列中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有计数从低到高向上累加,数组C[i]中的值会变成所有小于等于i的元素个数;
- 从原序列反向填充目标数组:将每个元素i填入新数组的第C[i]项,每放一个元素C[i]减1.
def counting_sort(arr):
m = max(arr)
c = [0 for i in range(m+1)]
for i in arr:
c[i] += 1 #c[i]表示在原序列arr中值为i的元素有几个
for j in range(1,m+1):
c[j] = c[j] + c[j-1] #c[j]表示在原arr序列中最后一个值为j的元素排第几位,或者表示小于等于j的元素个数
res = [None for i in range(len(arr))]
for r in range(len(arr)-1,-1,-1):
res[c[arr[r]]-1] = arr[r] #因为索引是从0开始的,需要-1
c[arr[r]] -= 1 #放置好一个元素,就需要在c数组中去掉一个元素,最后c数组会变成全0的数组
return res
依然是动图伺候:
后话
其实还是有一些排序算法没有涉及,比如桶排序、基数排序、堆排序。毕竟不是专业的程序员,就不继续深究了。文中展示的所有代码都是本人手写并运行验证通过的,可放心复制粘贴食用。
关于复杂度也可以一张图说明:
还记得开头讲过的稳定性么,开写这篇博文时并不明白其作用。实现了这几个排序算法后也自己琢磨明白了一点。稳定的好处是:从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。
也就是说,实际应用中我们遇到的排序不是简单地针对一个数组或者一个序列,而是有很多维度的。我们针对其中一个维度进行稳定排序,原先其他维度的先后顺序不会被改变。这才是稳定性的意义所在。
最后感谢来自他人博客的动图,转载声明:
来源于一像素的博客:https://www.cnblogs.com/onepixel/articles/7674659.html