性能比较
往往评价一个排序算法的好坏往往可以从下边几个方面入手:
(1)时间复杂度:即从序列的初始状态到经过排序算法的变换移位等操作变到最终排序好的结果状态的过程所花费的时间度量。
(2)空间复杂度:就是从序列的初始状态经过排序移位变换的过程一直到最终的状态所花费的空间开销。
(3)稳定性:稳定性是不管考虑时间和空间必须要考虑的问题,往往也是非常重要的影响选择的因素。
(4)使用场景:排序算法有很多,不同种类的排序算法适合不同种类的情景,可能有时候需要节省空间对时间要求没那么多,反之,有时候则是希望多考虑一些时间,对空间要求没那么高,总之一般都会必须从某一方面做出抉择。
插入排序
插入排序算法是基于某序列已经有序排列的情况下,通过一次插入一个元素的方式按照原有排序方式增加元素。这种比较是从该有序序列的最末端开始执行,即要插入序列中的元素最先和有序序列中最大的元素比较,若其大于该最大元素,则可直接插入最大元素的后面即可,否则再向前一位比较查找直至找到应该插入的位置为止。插入排序的基本思想是,每次将1个待排序的记录按其关键字大小插入到前面已经排好序的子序列中,寻找最适当的位置,直至全部记录插入完毕。执行过程中,若遇到和插入元素相等的位置,则将要插人的元素放在该相等元素的后面,因此插入该元素后并未改变原序列的前后顺序。插入排序是一种稳定的排序方法。
直接插入排序
将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过。
因此,直接插入排序可以用两个循环完成:
第一层循环:遍历待比较的所有数组元素
第二层循环:将本轮选择的元素(selected)与已经排好序的元素(ordered)相比较。
如果:selected > ordered,那么将二者交换
【列表举例】
L = [9,1,2,5,7,4,8,6,3,5]
# print('排序前',L)
【直接插入排序】
# 插入排序
# 直接插入排序
def insertsort(L):
# 遍历数组中的所有元素,0号索引元素默认已排序,因此从1开始
for i in range(1,len(L)):
# 将该元素与已排好的前序数组依次比较,如果小,则交换:
for j in range(i-1,-1,-1):
if L[j]>L[j+1]:
L[j],L[j+1] = L[j+1],L[j]
# print('第{}次'.format(i),L)
return L
# print('排序后',insertsort(L))
【输出结果】
排序前 [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
第1次 [1, 9, 2, 5, 7, 4, 8, 6, 3, 5]
第2次 [1, 2, 9, 5, 7, 4, 8, 6, 3, 5]
第3次 [1, 2, 5, 9, 7, 4, 8, 6, 3, 5]
第4次 [1, 2, 5, 7, 9, 4, 8, 6, 3, 5]
第5次 [1, 2, 4, 5, 7, 9, 8, 6, 3, 5]
第6次 [1, 2, 4, 5, 7, 8, 9, 6, 3, 5]
第7次 [1, 2, 4, 5, 6, 7, 8, 9, 3, 5]
第8次 [1, 2, 3, 4, 5, 6, 7, 8, 9, 5]
第9次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
排序后 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
希尔排序
希尔排序又称“缩小增量排序”,该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某 个“增量”的元素组成的)分别进行直接插入排序,然后依次**缩减增量(折半)**再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上有较大提高。
希尔排序的总体实现应该由三个循环完成:
第一层循环:将gap依次折半,对序列进行分组,直到gap=1
第二、三层循环:也即直接插入排序所需要的两次循环。具体描述见上。
【希尔排序】
#希尔排序
def insertshell(L):
#初始化gap值,此处利用序列长度的一般为其赋值
gap = int(len(L)/2)
#第一层循环:依次改变gap值对列表进行分组
# count = 0
while (gap >= 1):
#利用直接插入排序的思想对分组数据进行排序
#从gap开始
for x in range(gap,len(L)):
#从x-gap开始与选定元素开始倒序比较,每个比较元素之间间隔gap
for i in range(x-gap,-1,-gap):
#如果该组当中两个元素满足交换条件,则进行交换
if L[i] > L[i+gap]:
L[i+gap],L[i] = L[i],L[i+gap]
#条件折半
gap = int(gap/2)
# count += 1
# print('第{}次'.format(count),L)
return L
# print('排序后',insertshell(L))
【输出结果】
排序前 [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
第1次 [4, 1, 2, 3, 5, 9, 8, 6, 5, 7]
第2次 [2, 1, 4, 3, 5, 6, 5, 7, 8, 9]
第3次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
排序后 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
选择排序
选择排序算法的基本思路是为每一个位置选择当前最小的元素。选择排序的基本思想是,基于直接选择排序和堆排序这两种基本的简单排序方法。首先从第1个位置开始对全部元素进行选择,选出全部元素中最小的给该位置,再对第2个位置进行选择,在剩余元素中选择最小的给该位置即可;以此类推,重复进行“最小元素”的选择,直至完成第(n-1)个位置的元素选择,则第n个位置就只剩唯一的最大元素,此时不需再进行选择。
使用这种排序时,要注意其中一个不同于冒泡法的细节。举例说明:序列58539.我们知道第一遍选择第1个元素“5”会和元素“3”交换,那么原序列中的两个相同元素“5”之间的前后相对顺序就发生了改变。因此,我们说选择排序不是稳定的排序算法,它在计算过程中会破坏稳定性。
直接选择排序
选择排序的第一趟处理是从数据序列所有n个数据中选择一个最小的数据作为有序序列中的第1个元素并将它定位在第一号存储位置。第二趟处理从数据序列的n-1个数据中选择一个第二小的元素作为有序序列中的第2个元素并将它定位在第二号存储位置。依此类推,当第n-1趟处理从数据序列的剩下的2个元素中选择一个较小的元素作为有序序列中的最后第2个元素并将它定位在倒数第二号存储位置,至此,整个的排序处理过程就已完成。
简单选择排序的基本思想:比较+交换。
从待排序序列中,找到关键字最小的元素;
如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
从余下的 N - 1 个元素中,找出关键字最小的元素,重复(1)、(2)步,直到排序结束。
直接选择排序也是通过两层循环实现。
第一层循环:依次遍历序列当中的每一个元素
第二层循环:将遍历得到的当前元素依次与余下的元素进行比较,符合最小元素的条件,则交换。
【直接选择排序】
# 直接选择排序
def selectsort(L):
#依次遍历序列中的每一个元素
for i in range(0,len(L)):
#将当前位置的元素定义此轮循环当中的最小值
minimum = L[i]
#将该元素与剩下的元素依次比较寻找最小元素
for j in range(i+1,len(L)):
if L[j] < minimum:
L[j],minimum = minimum,L[j]
#将比较后得到的真正的最小值赋值给当前位置
L[i] = minimum
# print('第{}次'.format(i),L)
return L
# print('排序后',selectsort(L))
【输出结果】
排序前 [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
第0次 [1, 9, 2, 5, 7, 4, 8, 6, 3, 5]
第1次 [1, 2, 9, 5, 7, 4, 8, 6, 3, 5]
第2次 [1, 2, 3, 9, 7, 5, 8, 6, 4, 5]
第3次 [1, 2, 3, 4, 9, 7, 8, 6, 5, 5]
第4次 [1, 2, 3, 4, 5, 9, 8, 7, 6, 5]
第5次 [1, 2, 3, 4, 5, 5, 9, 8, 7, 6]
第6次 [1, 2, 3, 4, 5, 5, 6, 9, 8, 7]
第7次 [1, 2, 3, 4, 5, 5, 6, 7, 9, 8]
第8次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
第9次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
排序后 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
堆排序
-
堆的概念
堆:本质是一种数组对象。特别重要的一点性质:任意的叶子节点小于(或大于)它所有的父节点。对此,又分为大顶堆和小顶堆,大顶堆要求节点的元素都要大于其孩子,小顶堆要求节点元素都小于其左右孩子,两者对左右孩子的大小关系不做任何要求。
利用堆排序,就是基于大顶堆或者小顶堆的一种排序方法。下面,我们通过大顶堆来实现。 -
基本思想:
堆排序可以按照以下步骤来完成:-
首先将序列构建称为大顶堆; (这样满足了大顶堆那条性质:位于根节点的元素一定是当前序列的最大值)
-
取出当前大顶堆的根节点,将其与序列末尾元素进行交换;
(此时:序列末尾的元素为已排序的最大值;由于交换了元素,当前位于根节点的堆并不一定满足大顶堆的性质) -
对交换后的n-1个序列元素进行调整,使其满足大顶堆的性质;
-
重复2.3步骤,直至堆中只有1个元素为止
-
#堆排序
#获取左右叶子节点
def LEFT(i):
return 2*i + 1
def RIGHT(i):
return 2*i + 2
#调整大顶堆
#L:待调整序列 length: 序列长度 i:需要调整的结点
def adjustmaxheap(L,length,i):
#定义一个int值保存当前序列最大值的下标
largest = i
#执行循环操作:两个任务:1 寻找最大值的下标;2.最大值与父节点交换
while (1):
#获得序列左右叶子节点的下标
left,right = LEFT(i),RIGHT(i)
#当左叶子节点的下标小于序列长度 并且 左叶子节点的值大于父节点时,将左叶子节点的下标赋值给largest
if (left < length) and (L[left] > L[i]):
largest = left
# print('左叶子节点')
else:
largest = i
#当右叶子节点的下标小于序列长度 并且 右叶子节点的值大于父节点时,将右叶子节点的下标值赋值给largest
if (right < length) and (L[right] > L[largest]):
largest = right
# print('右叶子节点')
#如果largest不等于i 说明当前的父节点不是最大值,需要交换值
if (largest != i):
temp = L[i]
L[i] = L[largest]
L[largest] = temp
i = largest
# print(largest)
continue
else:
break
#建立大顶堆
def build_max_heap(L):
length = len(L)
for x in range((int)((length-1)/2),-1,-1):
adjustmaxheap(L,length,x)
#堆排序
def heap_sort(L):
#先建立大顶堆,保证最大值位于根节点;并且父节点的值大于叶子结点
build_max_heap(L)
#i:当前堆中序列的长度.初始化为序列的长度
i = len(L)
#执行循环:1. 每次取出堆顶元素置于序列的最后(len-1,len-2,len-3...)
# 2. 调整堆,使其继续满足大顶堆的性质,注意实时修改堆中序列的长度
while (i > 0):
L[i-1],L[0] = L[0],L[i-1]
#堆中序列长度减1
i = i-1
#调整大顶堆
adjustmaxheap(L,i,0)
# print ('第{}次'.format(i),L)
return L
# print('排序后',heap_sort(L))
【输出结果】
排序前 [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
第1次 [8, 7, 4, 6, 5, 1, 2, 5, 3, 9]
第2次 [7, 6, 4, 5, 5, 1, 2, 3, 8, 9]
第3次 [6, 5, 4, 3, 5, 1, 2, 7, 8, 9]
第4次 [5, 5, 4, 3, 2, 1, 6, 7, 8, 9]
第5次 [5, 3, 4, 1, 2, 5, 6, 7, 8, 9]
第6次 [4, 3, 2, 1, 5, 5, 6, 7, 8, 9]
第7次 [3, 1, 2, 4, 5, 5, 6, 7, 8, 9]
第8次 [2, 1, 3, 4, 5, 5, 6, 7, 8, 9]
第9次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
第10次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
排序后 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
交换排序
冒泡排序
冒泡排序算法是把较小的元素往前调或者把较大的元素往后调。这种方法主要是通过对相邻两个元素进行大小的比较,根据比较结果和算法规则对该二元素的位置进行交换,这样逐个依次进行比较和交换,就能达到排序目的。冒泡排序的基本思想是,首先将第1个和第2个记录的关键字比较大小,如果是逆序的,就将这两个记录进行交换,再对第2个和第3个记录的关键字进行比较,依次类推,重复进行上述计算,直至完成第(n一1)个和第n个记录的关键字之间的比较,此后,再按照上述过程进行第2次、第3次排序,直至整个序列有序为止。
排序过程中要特别注意的是,当相邻两个元素大小一致时,这一步操作就不需要交换位置,因此也说明冒泡排序是一种严格的稳定排序算法,它不改变序列中相同元素之间的相对位置关系。
冒泡排序思路:
将序列当中的左右元素,依次比较,保证右边的元素始终大于左边的元素;
( 第一轮结束后,序列最后一个元素一定是当前序列的最大值;)
对序列当中剩下的n-1个元素再次执行步骤1。
对于长度为n的序列,一共需要执行n-1轮比较
(利用while循环可以减少执行次数)
# 冒泡排序
def bubble_sort(L):
length = len(L)
#序列长度为length,需要执行length-1轮交换
for x in range(1,length):
#对于每一轮交换,都将序列当中的左右元素进行比较
#每轮交换当中,由于序列最后的元素一定是最大的,因此每轮循环到序列未排序的位置即可
for i in range(0,length-x):
if L[i] > L[i+1]:
L[i],L[i+1] = L[i+1],L[i]
# print('第{}次'.format(x),L)
return L
# print ('排序后',bubble_sort(L))
【输出结果】
排序前 [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
第1次 [1, 2, 5, 7, 4, 8, 6, 3, 5, 9]
第2次 [1, 2, 5, 4, 7, 6, 3, 5, 8, 9]
第3次 [1, 2, 4, 5, 6, 3, 5, 7, 8, 9]
第4次 [1, 2, 4, 5, 3, 5, 6, 7, 8, 9]
第5次 [1, 2, 4, 3, 5, 5, 6, 7, 8, 9]
第6次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
第7次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
第8次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
第9次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
排序后 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
快速排序
快速排序的基本思想是:通过一趟排序算法把所需要排序的序列的元素分割成两大块,其中,一部分的元素都要小于或等于另外一部分的序列元素,然后仍根据该种方法对划分后的这两块序列的元素分别再次实行快速排序算法,排序实现的整个过程可以是递归的来进行调用,最终能够实现将所需排序的无序序列元素变为一个有序的序列。
快速排序的基本思想:挖坑填数+分治法
从序列当中选择一个基准数(pivot)
1.在这里我们选择序列当中第一个数最为基准数
2.将序列当中的所有数依次遍历,比基准数大的位于其右侧,比基准数小的位于其左侧
3.重复步骤1.2,直到所有子集当中只有一个元素为止。
用伪代码描述如下:
1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中
#快速排序
#L:待排序的序列;start排序的开始index,end序列末尾的index
#对于长度为length的序列:start = 0;end = length-1
def quick_sort(L,start,end):
if start < end:
i , j , pivot = start , end , L[start]
while i < j:
#从右开始向左寻找第一个小于pivot的值
while (i < j) and (L[j] >= pivot):
j = j-1
#将小于pivot的值移到左边
if (i < j):
L[i] = L[j]
i = i+1
#从左开始向右寻找第一个大于pivot的值
while (i < j) and (L[i] < pivot):
i = i+1
#将大于pivot的值移到右边
if (i < j):
L[j] = L[i]
j = j-1
#循环结束后,说明 i=j,此时左边的值全都小于pivot,右边的值全都大于pivot
#pivot的位置移动正确,那么此时只需对左右两侧的序列调用此函数进一步排序即可
#递归调用函数:依次对左侧序列:从0 ~ i-1 右侧序列:从i+1 ~ end
L[i] = pivot
#左侧序列继续排序
quick_sort(L,start,i-1)
#右侧序列继续排序
quick_sort(L,i+1,end)
# global count
# count += 1
# print ('第{}次'.format(count),L)
return L
# count = 0
# print('排序后',quick_sort(L,0,len(L)-1))
【输出结果】
排序前 [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
第1次 [1, 2, 3, 4, 5, 7, 8, 6, 5, 9]
第2次 [1, 2, 3, 4, 5, 7, 8, 6, 5, 9]
第3次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
第4次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
第5次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
第6次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
排序后 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
归并排序
归并排序算法就是把序列递归划分成为一个个短序列,以其中只有1个元素的直接序列或者只有2个元素的序列作为短序列的递归出口,再将全部有序的短序列按照一定的规则进行排序为长序列。
归并排序融合了分治策略,即将含有n个记录的初始序列中的每个记录均视为长度为1的子序列,再将这n个子序列两两合并得到n/2个长度为2(当凡为奇数时会出现长度为l的情况)的有序子序列;将上述步骤重复操作,直至得到1个长度为n的有序长序列。
需要注意的是,在进行元素比较和交换时,若两个元素大小相等则不必刻意交换位置,因此该算法不会破坏序列的稳定性,即归并排序也是稳定的排序算法。
- 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个典型的应用。它的基本操作是:将已有的子序列合并,达到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
归并排序其实要做两件事: 分解----将序列每次折半拆分 合并----将划分后的序列段两两排序合并
因此,归并排序实际上就是两个操作,拆分+合并 如何合并?
L[first…mid]为第一段,L[mid+1…last]为第二段,并且两端已经有序,现在我们要将两端合成达到L[first…last]并且也有序。
首先依次从第一段与第二段中取出元素比较,将较小的元素赋值给temp[]
重复执行上一步,当某一段赋值结束,则将另一段剩下的元素赋值给temp[]
此时将temp[]中的元素复制给L[],则得到的L[first…last]有序 如何分解?
在这里,我们采用递归的方法,首先将待排序列分成A,B两组;然后重复对A、B序列
分组;直到分组后组内只有一个元素,此时我们认为组内所有元素有序,则分组结束。
# 归并排序
# 这是合并的函数
# 将序列L[first...mid]与序列L[mid+1...last]进行合并
def mergearray(L,first,mid,last,temp):
#对i,j,k分别进行赋值
i,j,k = first,mid+1,0
#当左右两边都有数时进行比较,取较小的数
while (i <= mid) and (j <= last):
if L[i] <= L[j]:
temp[k] = L[i]
i = i+1
k = k+1
else:
temp[k] = L[j]
j = j+1
k = k+1
#如果左边序列还有数
while (i <= mid):
temp[k] = L[i]
i = i+1
k = k+1
#如果右边序列还有数
while (j <= last):
temp[k] = L[j]
j = j+1
k = k+1
#将temp当中该段有序元素赋值给L待排序列使之部分有序
for x in range(0,k):
L[first+x] = temp[x]
# 输出验证
# global count
# count += 1
# print('第{}次'.format(count), L)
# 这是分组的函数
def merge_sort(L,first,last,temp):
if first < last:
mid = (int)((first + last) / 2)
#使左边序列有序
merge_sort(L,first,mid,temp)
#使右边序列有序
merge_sort(L,mid+1,last,temp)
#将两个有序序列合并
mergearray(L,first,mid,last,temp)
# 归并排序的函数
def merge_sort_array(L):
#声明一个长度为len(L)的空列表
temp = len(L)*[None]
#调用归并排序
merge_sort(L,0,len(L)-1,temp)
return L
count = 0
# print('排序后',merge_sort_array(L))
【输出结果】
排序前 [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
第1次 [1, 9, 2, 5, 7, 4, 8, 6, 3, 5]
第2次 [1, 2, 9, 5, 7, 4, 8, 6, 3, 5]
第3次 [1, 2, 9, 5, 7, 4, 8, 6, 3, 5]
第4次 [1, 2, 5, 7, 9, 4, 8, 6, 3, 5]
第5次 [1, 2, 5, 7, 9, 4, 8, 6, 3, 5]
第6次 [1, 2, 5, 7, 9, 4, 6, 8, 3, 5]
第7次 [1, 2, 5, 7, 9, 4, 6, 8, 3, 5]
第8次 [1, 2, 5, 7, 9, 3, 4, 5, 6, 8]
第9次 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
排序后 [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
基数排序
基数排序:通过序列中各个元素的值,对排序的N个元素进行若干趟的“分配”与“收集”来实现排序。
分配:我们将L[i]中的元素取出,首先确定其个位上的数字,根据该数字分配到与之序号相同的桶中
收集:当序列中所有的元素都分配到对应的桶中,再按照顺序依次将桶中的元素收集形成新的一个待排序列L[ ]
对新形成的序列L[]重复执行分配和收集元素中的十位、百位…直到分配完该序列中的最高位,则排序结束
根据上述“基数排序”的展示,我们可以清楚的看到整个实现的过程
#基数排序
#确定排序的次数
#排序的顺序跟序列中最大数的位数相关
def radix_sort_nums(L):
maxNum = L[0]
#寻找序列中的最大数
for x in L:
if maxNum < x:
maxNum = x
#确定序列中的最大元素的位数
times = 0
while (maxNum > 0):
maxNum = (int)(maxNum/10)
times = times+1
return times
#找到num从低到高第pos位的数据
def get_num_pos(num,pos):
return ((int)(num/(10**(pos-1))))%10
#基数排序
def radix_sort(L):
count = 10*[None] #存放各个桶的数据统计个数
bucket = len(L)*[None] #暂时存放排序结果
#从低位到高位依次执行循环
for pos in range(1,radix_sort_nums(L)+1):
#置空各个桶的数据统计
for x in range(0,10):
count[x] = 0
#统计当前该位(个位,十位,百位....)的元素数目
for x in range(0,len(L)):
#统计各个桶将要装进去的元素个数
j = get_num_pos(int(L[x]),pos)
count[j] = count[j]+1
#count[i]表示第i个桶的右边界索引
for x in range(1,10):
count[x] = count[x] + count[x-1]
#将数据依次装入桶中
for x in range(len(L)-1,-1,-1):
#求出元素第K位的数字
j = get_num_pos(L[x],pos)
#放入对应的桶中,count[j]-1是第j个桶的右边界索引
bucket[count[j]-1] = L[x]
#对应桶的装入数据索引-1
count[j] = count[j]-1
# 将已分配好的桶中数据再倒出来,此时已是对应当前位数有序的表
for x in range(0,len(L)):
L[x] = bucket[x]
return L
print('排序后',radix_sort(L))