一、前言
这篇博客主要针对排序
进行下总结记录。
在排序的过程中,根据元素是否完全在内存中,可将排序分为两类:内部排序
和外部排序
。见下图:
而内部排序算法的性能主要是由其时间复杂度
和空间复杂度
影响的,所以会简单记录这两个指标。
一下将主要针对内部排序进行记录,外部排序捎带一提。
二、主要内容
2.1、内部排序
2.1.1、插入排序
直接插入排序
:
算法描述:设有序序列L[1…i-1],带插入元素L(i),无序序列L[i+1…n]。
①查找出L(i)在L[1…i-1]中的插入位置k
②将L[k…i-1]中的所有元素全部后移一个位置
③将L(i)复制到L(k)
code:
def InsertSort(array):
n = len(array)
for i in range(1,n):
flag = array[i]
key = i
while(array[key-1]>flag) and key>0:
array[key] = array[key-1]
key -= 1
array[key] = flag
return array
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2),主要是比较次数和移动次数。
空间复杂度:
O
(
1
)
O(1)
O(1)
稳定性:稳定
折半插入排序
:
算法描述:相对于直接插入,即首先找到带插入元素的位置,然后统一的移动带插入元素位置之后的所有元素。
code:
def Half_insortsort(array):
n = len(array)
for i in range(1,n):
flag = array[i]
low = 0
high = i-1
while(low<=high):
mid = (low+high)//2
if array[mid]>flag:
high = mid - 1
else:
low = mid + 1
# high = low - 1
for j in range(i,low,-1):
array[j] = array[j-1]
# j = i
# while(j>high+1):
# array[j] = array[j-1]
# j -= 1
array[high+1] = flag
return array
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2),减少了比较的次数(比较:
O
(
n
l
o
g
(
n
)
)
O(nlog(n))
O(nlog(n))),移动次数不变
空间复杂度:
O
(
1
)
O(1)
O(1)
稳定性:稳定
希尔排序
:
算法描述:也称缩小增量排序。顾名思义,即将待排序表分成若干个形如L[i,i+d,i+2d,…,i+kd]的“特殊”子表。具体步骤如下:
①首先取一个小于n的步长d1(一般取n/2),然后将表分成d1组,所有组距为d1的位于同一组,然后在各组中进行插入排序
②然后去第二个步长d2(d2<d1,一般取d1/2),重复上述步骤,一直到组距为1
code:
def Shell_Sort(array):
n = len(array)
dk = n//2
while (dk > 0):
for j in range(dk,n):
#flag = array[dk]
while j-dk>=0 and array[j-dk]>array[j]:
array[j],array[j-dk] = array[j-dk],array[j]
j -= dk
dk = dk//2
return array
时间复杂度:分析比较困难,设计数学上尚未解决的难题。n在特定范围,复杂度为
O
(
n
1.3
)
O(n^{1.3})
O(n1.3),最坏的情况下为
O
(
n
2
)
O(n^2)
O(n2)。
空间复杂度:
O
(
1
)
O(1)
O(1)
稳定性:不稳定
2.1.2、交换排序
冒泡排序
:
算法描述:
①比较相邻的元素。如果第一个比第二个大(小),则交换他们两个。
②对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这样,最大(小)的元素将会位于最后。
③针对所有的元素重复以上的步骤,除了最后已经选出的有序元素。
④持续每次对越来越少的元素(无序元素)重复上面的步骤,直到没有任何一对数字需要比较,则序列最终有序。
code:
def BubbleSort(array):
n = len(array)
for i in range(0,n):
for j in range(0,n-1-i):
if array[j]>array[j+1]:
array[j],array[j+1] = array[j+1],array[j]
return array
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2),
空间复杂度:
O
(
1
)
O(1)
O(1)
稳定性:稳定
快速排序
:
算法描述:是建立在冒泡排序上的一个分而治之的典型应用。基本思想如下:
①在待排序表L[1…n]中任取一个pivot作为基准
②通过一趟排序将待排序表划分为单独的两部分:小于pivot的L[1…k]和大于pivot的L[k…n]
③然后分别对两个字表重复上述过程,直到每部分只有一个元素或者为空为止。
code:
def QuickSort(array):
def partition(array,low,high):
pivot = array[low]
while low<high:
while low<high and array[high]>=pivot:
high -= 1
array[low] = array[high] # 比基准小的交换到前面
while low<high and array[low]<=pivot:
low += 1
array[high] = array[low] # 比基准大交换到后面
array[low] = pivot # 基准值的正确位置,也可以为 nums[high] = pivot
return low
#return low # 返回基准值的索引,也可以为 return high
#递归
def qsort(array,low,high):
if low<high:
pivotpos = partition(array,low,high)
qsort(array,low,pivotpos-1)
qsort(array,pivotpos+1,high)
return array
result = qsort(array,0,len(array)-1)
return result
时间复杂度:快排的运行时间和划分是否对称有很大关系。基准值若能把数据分为平均的两块,划分次数O(logn),每次划分遍历比较一遍O(n),时间复杂度O(nlogn)。
空间复杂度:由于快排是递归的,需借助一个递归工作栈来保存每层必要的信息,而其深度便是递归调用的最大深度。最好的情况为
O
(
l
o
g
2
n
)
O(log_2^{n})
O(log2n),最坏的情况为
O
(
n
)
O(n)
O(n),平均情况为
O
(
l
o
g
2
n
)
O(log_2^{n})
O(log2n)。
稳定性:不稳定。
2.1.3、选择排序
简单选择排序
:
算法描述:假设待排序表为L[1…n]。
第i趟排序选出待排序表L[i…n]中最小(大)的元素然后与L[i]交换即可。这样,每趟可以确定一个元素的最终位置。
code:
def SelectSort(array):
n= len(array)
for i in range(0,n-1):
min = i
for j in range(i+1,n):
if array[j]<array[min]:
min = j
array[i],array[min] = array[min],array[i]
return array
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)
稳定性:不稳定
堆排序
:
算法描述:堆排序是一种树形选择排序方法,排序过程中将L[1…n]视为一颗完全二叉树。分两种情况:
①大顶堆。即
L
(
i
)
>
=
L
(
2
i
)
且
L
[
i
]
>
=
L
[
2
i
+
1
]
L(i)>=L(2i)且L[i]>=L[2i+1]
L(i)>=L(2i)且L[i]>=L[2i+1]
②小顶堆。即
L
(
i
)
<
=
L
(
2
i
)
且
L
[
i
]
<
=
L
[
2
i
+
1
]
L(i)<=L(2i)且L[i]<=L[2i+1]
L(i)<=L(2i)且L[i]<=L[2i+1] (1<=i<=floor(n/2))
堆排序的关键是构造初始堆,对于初始序列建堆,是一个反复筛选的过程。n个结点的完全二叉树,最后一个节点是第floor(n/2)个结点的孩子。对第floor(n/2)个结点为根的子树进行筛选(对于大顶堆,若根结点的关键词小于左右孩子中的关键字较大者,则交换),使该子树成为堆。之后依次对floor(n/2)~1为根的字数进行筛选,看结点值是否大于其左右子结点的值,若大于,则将其交换,此时,可能会破坏下一级的堆,则继续采用上述方法构造下一级的堆直到以该节点的子树成为堆为止。
构造完成后,则按照根结点-右结点-左结点从根遍历输出即可。
code:
def HeapSort(array):
def AdjustDown(array,k,end):
flag = array[k]
i = 2*k + 1
while i<=end:
if i<end and array[i]<array[i+1]:
i += 1
if flag > array[i]:
break
else:
array[k] = array[i]
k = i
i = 2*i + 1
array[k] = flag
def BuildMaxHeap(array,length):
i = length//2 - 1 ###
if length<=1:
return array
while i>=0: ###
AdjustDown(array,i,length)
i -= 1
BuildMaxHeap(array,len(array)-1)
i = len(array) - 1
while i >= 0:
array[0],array[i] = array[i],array[0]
AdjustDown(array,0,i-1)
i -= 1
return array
时间复杂度:建堆的时间复杂度为
O
(
n
)
O(n)
O(n),每次调整的时间复杂度为
O
(
h
)
O(h)
O(h),所以在最好,最坏, 平均的情况下,堆排序的时间复杂度均为
O
(
n
l
o
g
2
n
)
O(nlog_2^n)
O(nlog2n)
空间复杂度:
O
(
1
)
O(1)
O(1)
稳定性:不稳定
2.1.4、归并排序
算法描述:假设待排序表有n个记录。
可以视为待排序表有n个有序的子表,每个子表长度为1,然后两两归并,得到ceil(n/2)个长度为2或者1的子表,然后一一对子表进行排序,然后再两两子表归并,并对得到的子表进行排序,依次重复。
code:
def MergeSort(array):
if len(array)<=1:
return array
def merge(left,right):
i = j = 0
results = list()
while i<len(left) and j<len(right):
if left[i]<right[j]:
results.append(left[i])
i += 1
else:
results.append(right[j])
j += 1
results = results+left[i:]+right[j:]
return results
mid = len(array)//2
left = MergeSort(array[:mid])
right = MergeSort(array[mid:])
result = merge(left,right)
return result
时间复杂度:
O
(
n
l
o
g
2
n
)
O(nlog_2^n)
O(nlog2n)
空间复杂度:
O
(
n
)
O(n)
O(n)
稳定性:不稳定
2.1.5、基数排序
算法描述:一种特殊的排序方法,不基于比较进行排序,而采用多关键字进行排序,借助“分配”和“收集”两种操作对单逻辑关键词进行排序。又分为最高位优先MSD
和最低位优先LSD
。
code:
def RadixSort(array):
N = 10
M = 1
mostbit = len(str(max(array)))
buckets = [[] for row in range(N)]
while mostbit:
for i in range(len(array)):
buckets[array[i]//M % N].append(array[i])
j = 0
for bucket in buckets:
while bucket: ###
array[j] = bucket.pop(0)
j += 1
M *= 10
mostbit -= 1
return array
空间复杂度:一趟排序需要的辅助存储空间为r(r个队列),所以空间复杂度为
O
(
r
)
O(r)
O(r)
时间复杂度:需要进行n趟收集和分配,一趟分配需要
O
(
n
)
O(n)
O(n),一趟收集需要
O
(
r
)
O(r)
O(r),所以时间复杂度为
O
(
d
(
r
+
n
)
)
O(d(r+n))
O(d(r+n))
稳定性:稳定
Ps:这部分也记录下计数排序
和桶排序
,因为均属于线性时间复杂度的排序。
计数排序
:
算法描述:计数排序要求输入数据的范围在 [0,N-1] 之间,则可以开辟一个大小为 N 的数组空间。所以说应用比较局限。采用的方法是将输入的数据值转化为键存储在该数组空间中,数组中的元素为该元素出现的个数。
code:
def ConutingSort(array):
bucket = [0]*(max(array)+1)
for num in array:
bucket[num] += 1
i = 0
for j in range(len(bucket)):
while bucket[j]>0:
array[i] = j
i += 1
bucket[j] -= 1
return array
时间复杂度:O(n+k)
空间复杂度:O(n+k)
稳定性:稳定
桶排序
:
算法描述:桶排序实际上是计数排序的升级版。桶排序先用一定的函数关系将数据划分到不同有序的区域(桶)内,然后子数据分别在桶内排序,之后顺次输出。当输入的数据可以均匀的分配到每一个桶中,算法效率最快;当输入的数据被分配到了同一个桶中,算法效率最慢。
code:
def BucketSort(array):
n=len(array)
big=max(array)
num=big//10+1
bucket=[]
buckets=[[] for i in range(0,num)]
for i in array: #划分桶
buckets[i//10].append(i)
for i in buckets: #桶内排序
bucket=HeapSort(i) #随便采用一种排序方法
arr=[]
for i in buckets:
if isinstance(i, list):
for j in i:
arr.append(j)
else:
arr.append(i)
for i in range(0,n):
array[i]=arr[i]
return array
时间复杂度:假设n个数据,划分为k个桶,桶内采用快速排序,时间复杂度为O(n)+O(k * n/k * log(n/k))=O(n)+O( n*(log(n)-log(k)) ),
空间复杂度:k越大,时间复杂度越接近O(n),当然空间复杂度O(n+k)会越大,这是空间与时间的平衡
稳定性:稳定
2.2、外部排序
在实际应用中,经常需要对大文件进行排序,因为文件中的记录很多、信息量庞大,无法将整个文件复制进内存进行排序。因此,需要将待排序的记录存储在外存上,排序是再把数据一部分一部分的调入内存中进行排序。在排序过程中需要多次进行内存和外存的之间的交换,对外存中的记录进行排序后的结果仍然放入原有文件中。上述排序方法就称为外部排序
。
由于外存设备的不同,通常可分为磁盘文件排序
和磁带文件排序
两大类。可知,磁盘
是直接存取设备,磁带
是顺序存取设备。
由于磁盘读/写的机械动作所需时间远远超过内存运行时间,因此外部排序过程中时间代价主要考虑访问磁盘的次数,即I/O次数
。
外部排序常用的三种排序方法:
① 多路平衡归并与失败树
② 置换选择排序算法
③ 最佳归并树
具体内容不做过多记录。
三、结尾
最后,对时间复杂度,空间复杂度,稳定性做个总结。见下图: