两两比较相邻记录的元素,如果反序则交换,直到没有反序的记录。想象一下气泡往上冒的过程,在往上冒的过程比较的是相邻元素,最终会变成一个大气泡(最后一个元素是最大的,如此类推)。
def Bubble_Sort(lst):
length = len(lst)
for i in range(length,0,-1):
flag = True
for j in range(1,i):
if lst[j-1] > lst[j]:
lst[j-1], lst[j] = lst[j], lst[j-1]
flag = False
# 某一趟遍历如果没有数据交换,则说明已经排好序了,因此不用再进行迭代了
if flag:
break
return lst
需要比较 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2次,并作等数量级的记录移动。因此总的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。
通过n-i次的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个交换。
def Selection_Sort(lst):
length = len(lst)
for i in range(length-1):
min = i
for j in range(i+1,length):
if lst[min] > lst[j]:
min = j
if i != min:
lst[i],lst[min] = lst[min],lst[i] #这里才交换
return lst
比较次数依然是 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2次,但是交换次数最好是交换0次,最差的时候也就 n − 1 n-1 n−1次。但最终是比较与交换次数的总和,因此排序时间依然是 O ( n 2 ) O(n^2) O(n2)。
将一个记录插入到已经排好序的有序表中。对于每个未排序的数据(可以认为第一个元素是排好序的),在已排序的序列中从后向前扫描,找到相应位置插入。没错,这就是我们打扑克牌理牌时常用的排序手段。
def Inser_Sort(lst):
length = len(lst)
for i in range(1,length):
tmp = lst[i]
j = i
while j>0 and lst[j-1] > tmp:
lst[j] = lst[j-1]
j -= 1
lst[j] = tmp
return lst
最好的情况,即本身就是有序的,例如:{2,3,4,5,6},需比较n-1次,没有移动的记录,时间复杂度为O(n);
最坏的情况,都是逆序,此时需比较 2 + 3 + . . . + n = ( n + 2 ) ( n − 1 ) / 2 2+3+...+n=(n+2)(n-1)/2 2+3+...+n=(n+2)(n−1)/2次,记录移动次数达到最大值。因此时间复杂度也是 O ( n 2 ) O(n^2) O(n2),但是平均比较和移动的次数约为 n 2 / 4 n^2/4 n2/4次,性能比前两种要好一些。
希尔排序的实质就是分组插入排序,该方法又称缩小增量排序。
先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序(此时增量=1,就是直接插入排序了)。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的。
代码跟直接插入排序差不多:
def Shell_Sort(lst):
length = len(lst)
gap = round(length/2) #round 四舍五入 或 length//2
while gap >= 1:
for i in range(gap,length):
mark = lst[i]
j = i
while j-gap>=0 and lst[j-gap] > mark;
lst[j] = lst[j-gap]
j -= gap
lst[j] = mark
gap = round(gap/2)
return lst
希尔排序在第一轮循环后,较大的在后面而较小的在前面,也就是说基本让整个序列基本有序了。这其实就是希尔排序的精华所在,移动记录不是一步一步地挪动,而是跳跃式地移动。最终在基本有序的情况下进行直接插入排序,效率就很高了。时间复杂度为 O ( n 3 2 ) O(n^\frac {3} {2}) O(n23)。另外由于记录是跳跃式的移动,希尔排序并不是一种稳定的算法。
希尔排序是直接插入排序的改进版,那么堆排序就是选择排序的改进版。选择排序有一个缺点就是没有把每一趟的比较结果记录下来,在后一趟的比较中,有许多是比较是前一趟已经做过了,但由于前一趟没有保存比较结果,因为又重复执行这些比较操作,比较次数较多。如果可以每次做到在每次选择最小记录的同时,并根据比较结果对其他记录做出相应调整,那么效率就会高很多。
将待排序的序列构造成一个大顶堆。此时整个序列的最大值就是堆顶的根结点。将它移走(就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),将剩余的n-1个序列重新构造一个堆,这样就会得到n个元素中次大值。如此反复就得到一个有序序列。
步骤:
def Heap_Sort(lst):
#最大堆调整
def Heap_Adujust(lst,start,end):
root = lst[start]
child = 2*start + 1
while child= lst[child]: #若父节点比最大的孩子结点要大,不做处理,跳出循环
break
#父节点比最大的孩子结点要小,交换
lst[start] = lst[child]
start = child
child = 2*child + 1
lst[start] = root
length = len(lst)
first = length//2-1
#步骤一:构造最大堆
for i in range(first,-1,-1):
Heap_Adjust(lst,i,length)
#步骤二:堆排序
for i in range(length-1,-1,-1):
lst[0],lst[i] = lst[i],lst[0] #根结点与最后一个交换 ,即将最大的往最后放
Heap_Adjust(lst,0,i)
return lst
在步骤一中,构建堆的过程中,对于每个非终端节点,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n);
在正式排序时,第i次取堆顶记录重建需要用O(logi)的时间,需要取n-1次堆顶记录,因此重建堆的时间复杂度为O(nlogn)。
总体来说,堆排序时间复杂度为O(nlogn),最好,最坏和平均时间复杂度均为O(nlogn),远比冒泡、选择、直接插入的 O ( n 2 ) O(n^2) O(n2)好。由于记录的比较和交换是跳跃进行的,因此不稳定。
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
所谓的归并,是将两个或两个以上的有序文件合并成为一个新的有序文件,归并排序是把一个有n个记录的无序文件看成是由n个长度为1的有序子文件组成的文件,然后进行两两归并,如此重复,直至最后形成包含n个归并,得到n/2个长度为2或者1的有序文件,再两两归并,如此重复,直至最后形成包含n个记录的有序文件位置,这种反复将两个有序文件归并成一个有序文件的排序方法称为二路归并排序。二路归并排序的核心操作是将一堆数组中前后相邻的两个有序序列归并为一个有序序列,如下图所示:
使用非递归实现:
def Merge_Sort(lst):
#最底层的操作:将一层中 接连的两个有序的列表合并成一个有序的列表
#lfrom是源lst,lto是归并到一个新的lst,low,mid,high分别是接连两个有序列表的分段标志位
def merge(lfrom,lto,low,mid,high):
i,j,k = low,mid,low
while i
在一层中,归并一对对分段需耗费O(n)时间,由完全二叉树的深度可知,整个归并排序需进行 l o g 2 n log_{2^n} log2n(向上取整)次,因此,总的时间复杂度为O(nlogn),这也是最好最坏、平均时间性能。空间复杂度因为 lto = [None]*llen ,所以空间复杂度是O(n)。由于记录的移动是相邻的,所以算法稳定。归并排序虽占用内存,但效率高并稳定。
快速排序通常明显比同为Ο(n log n)的其他算法更快,因此常被采用,而且快排采用了分治法的思想,所以在很多笔试面试中能经常看到快排的影子。可见掌握快排的重要性。
通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
步骤:
def Quick_Sort(lst):
#三数取中间值 保证取的枢轴不会是最值
def partition(lst,low,high):
mid = low+(high-low)//2
if lst[low] > lst[high]:
lst[low],lst[high] = lst[high],lst[low]
if lst[mid] > lst[high]:
lst[mid],lst[high] = lst[high],lst[mid]
if lst[mid] > lst[low]:
lst[low],lst[mid] = lst[mid],lst[low]
key = lst[low]
while low= key:
high -= 1
lst[low] = lst[high] #通过赋值的方式,而不是交换的方式,提高性能
while low
递归树深度 l o g 2 n log_{2^n} log2n(向上取整),即仅需递归 l o g 2 n log_{2^n} log2n次,第一次需要做n次比较,第二次各自n/2次,如此类推,最优情况下时间复杂度O(nlogn);
最坏情况,序列为正序或逆序,递归树画出来就是一颗斜树,因此需要比较n(n-1)/2次,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
平均来说,时间复杂度O(nlogn);空间复杂度为O(logn)-O(n)(斜树),平均空间复杂度为O(logn)。
关键字的比较和交换是跳跃进行的,因此快排是一种不稳定算法。
对于时间复杂度来说:堆排序和归并排序发挥稳定,快速排序就像个情绪化的天才,好的时候就极佳,差的时候就不行。但对于考题简单(就是基本正序的序列),他们都算不过冒泡和插入。所以应情况而使用排序算法。
对于空间复杂度:归并排序强调马跑得快就给马喂饱,快速排序同样发挥不稳定,差的时候就是斜树O(n),好的时候就较好O(logn)(比归并好);其他算法只用一个辅助空间来进行交换。所以在乎内存使用量多少时,归并和排序就不是一个很好的选择。
对于稳定性:如果非常在乎排序的稳定性,那就选择归并比较好了。