排序算法性能分析及优化方案
这是我在做排序算法的实验时一些自己的探索,在代码后面,欢迎交流,可能有些网络资源,我忘了出处了,侵删
快排
def getData():
"""
:return: data整形数组
"""
with open("number.txt", "r") as f:
string = f.read()
sdata = string.split(",")
data = [int(i) for i in sdata]
return data
def quickSort(left, right,data):
if(left >= right):
return
i = left
j = right
base = data[i]
while(i < j):
while(data[j] >= base and i < j):
j -= 1
while(data[i] <= base and i < j):
i += 1
if(i < j):
data[i],data[j] = data[j], data[i]
data[left] = data[i]
data[i] = base
quickSort(left, i-1, data)
quickSort(i+1, right, data)
if __name__ == '__main__':
import time
data = getData()
start = time.perf_counter()
quickSort(0, len(data)-1, data)
end = time.perf_counter()
print(end - start)
start = time.perf_counter()
data2 = getData()
data2.sort()
end = time.perf_counter()
print(end - start)
归并
import time
import numpy as np
def merge(leftArr, rightArr):
c = []
high = low = 0
while low < len(leftArr) and high < len(rightArr):
if leftArr[low] < rightArr[high]:
c.append(leftArr[low])
low += 1
else:
c.append(rightArr[high])
high += 1
if low == len(leftArr):
for i in rightArr[high:]:
c.append(i)
else:
for i in leftArr[low:]:
c.append(i)
return c
def merge_sort(lists):
if len(lists) <= 1:
return lists
middle = int(len(lists)/2)
left = merge_sort(lists[:middle])
right = merge_sort(lists[middle:])
return merge(left, right)
def getData():
"""
:return: data整形数组
"""
data = np.loadtxt("data.txt")
return data
if __name__ == '__main__':
a = getData()
start = time.perf_counter()
data = merge_sort(a)
end = time.perf_counter()
print(end - start)
data = np.array(data)
np.savetxt("merout.txt", data, fmt="%d")
快排和归并的性能对比
算法 | 归并 | 快排 |
---|---|---|
最优时间复杂度 | n* log2n | n * log2n |
平均时间复杂度 | n * log2n | n * log2n |
最坏时间复杂度 | n * log2n | n2 |
对10w
个基本无序整数排序的结果显示
- 快排的平均用时为0.23 s
- 归并的平均用时为0.45 s
结果分析
- 实验所用数据为-109/109 之间随即生成的
10w
个数据,处于基本无需状态,所以这里快排是平均时间复杂度的耗时 - 归并排序尽管比较的次数相对于快排少,但是由于归并排序需要使用辅助数组存储有序排列部分,所以存在大量的赋值操作拉低了时间效率
- 辅助数组也需要更多的内存空间
归并算法的痛点
- 对比的时间很快,但是完成赋值操作占用了大量时间
- 需要大量的额外 空间O(n)
针对归并算法的优化
- 反思现在使用的归并排序算法的分治和数组拷贝在时间上是串行的,而现在的
cpu
都是多核多线程,没必要把所有问题的处理都积攒到一个过程中,可以采用多线程策略 - 需要的辅助空间较大,可以将待排序文件分割,分块分区进行排序
多线程
横向优化
这里所谓的横向就是将不同块交给不同的线程,由一个线程完成从分到治的一整个流程
- 将不同块交给不同的线程,分区处理,在处理上并行操作
- 将上图四个部分,交给四个线程,得到的解决方案流程图如下:
纵向优化
纵向优化是指为耗时久的复制操作分配多一些的计算资源,从而更好地提升算法效率
- ”分“的过程所需资源相对较少,所以少分配线程,“治”的过程开销较大,多分配线程
- 两个红框交给两个线程完成,四个绿框交给四个线程完成,这其中需要注意对于临界资源的调用顺序,此方案的流程如下:
解决大文件排序问题
- 分块处理
- 内存映射文件
- 系统来管理所有的文件缓存操作。不需要分配任何内存,也不需要将文件数据加载到内存,并且不必将数据重新写入文件或者释放任何内存块
内存映射文件,是由一个文件到一块内存的映射。Win32提供了允许应用程序把文件映射到一个进程的函数 (CreateFileMapping)。内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。
- 内存映射文件的优势
- 无需执行直接的文件I/O(读写)。
- 创建于内存中的数据结构可保存于文件中,以便将来由同一个或其他程序来使用。
- 方便高效的内存中算法(排序、搜索树、字符串处理等)可以用来处理文件数据,即使文件可能要比可用的物理内存大得多。
- 文件处理的性能通常要比使用
ReadFile()
和WriteFile()
这些文件访问函数快的多。 - 无需处理缓冲区及其所包含的文件数据。操作系统不仅会承担这些艰苦的工作,而且会做得即高效又高度可靠。
- 多个进程可以通过将他们的虚拟地址空间映射到同一个文件或分页文件的形式来共享内存。
- 无需消耗分页文件空间。
内存映射的基本流程
在对大数据文件进行排序时先创建文件映射对象,然后在文件映射对象上创建视图,然后通过多线程技术让每个线程去排序自己分配到的数据,然后在通过合并函数合并各个线程排序号的数据。
对横向优化的实现
在我使用第一种线程分配技术和内存映射文件多200w
个文件进行排序时,受到个人PC机性能的影响,优化性能不明显,但是网路上有使用这种方法成功的案例,其时间的确缩短到了单线程的1/4,这证明思路是可行的,只是要根据自己的计算机属性来进行线程调度,这引发了我对线程数设置过多会导致怎样结果的探索。
//文件视图
pRecords = MapViewOfFile(mHandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
if (NULL == pRecords)
printf("Failure to map input file.\n");
CloseHandle (mHandle);
/* 创建排序线程,并初始化各个线程处理记录块的参数 */
lowRecordNum = 0;
for (iTh = 0; iTh < numFiles; iTh++) {
threadArg[iTh].iTh = iTh;
threadArg[iTh].lowRecord = pRecords + lowRecordNum; //记录块的初始 位置
threadArg[iTh].highRecord = pRecords + (lowRecordNum + nRecTh);//记录块的最后位置
lowRecordNum += nRecTh;
pThreadHandle[iTh] =CreateThread(NULL, 0, SortThread, &threadArg[iTh], CREATE_SUSPENDED, NULL);
}
//处理多余数据的线程配置处理记录块的参数
if(overFiles==(numFiles+1))
{
threadArg[numFiles].iTh = numFiles;
threadArg[numFiles].lowRecord = pRecords + lowRecordNum; //记录块的初始 位置
threadArg[numFiles].highRecord = pRecords + (lowRecordNum + overRecTh);//记录块的最后位置
pThreadHandle[numFiles] =CreateThread(NULL, 0, OverSortThread, &threadArg[numFiles], CREATE_SUSPENDED, NULL);
}
start = clock();//计时
/* 唤醒所有线程,除了处理多余数据的线程 */
for (iTh = 0; iTh < numFiles; iTh++)
ResumeThread (pThreadHandle[iTh]);
/* 等待排序线程合并完成*/
printf("正在排序请等待..........\n");
WaitForSingleObject (pThreadHandle[0], INFINITE);
if( overFiles==(numFiles+1) )
{
// 唤醒处理多余数据的线程
ResumeThread (pThreadHandle[numFiles]);
WaitForSingleObject (pThreadHandle[numFiles], INFINITE);
//有多余的记录数时,在主线程中在排序一次,因为已经大部分数据已经排序好了,所以花费时间少
}
/* if( overFlage == TRUE)
qsort (pRecords, nRec, RECSIZE, KeyCompare);
*/
finish = clock();//完成排序所花的时间
线程并不是越多越好
线程数到达一定数目,性能就上不去了,这是因为线程增多之后,内核线程固定开销部分的增加跟由于数据量减少得到的性能提升抵消了
- 线程的生命周期开销非常高
- 消耗过多的CPU资源
- 如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。
- 降低稳定性
- 上下文切换开销
基本有序问题
在实验中我还探索了归并算法和快排解决基本有序问题的性能,针对2w
基本有序的数据排序得到的时间如下:
merg sort
用时 0.11 s- quick sort 无法得到结果
- 在对
2w
个基本有序的数据排序时,递归的深度为2w
,超过了python的处理能力 - 个人计算机无法处理如此大的栈深度
- 在对
处理基本有序的问题
-
递归深度可能为n,但如果合理设计算法,去掉冗余,log2n的递归深度一般是不会导致栈溢出的。即使是TB级别的数据,递归深度也不过是40个栈帧
-
处理基本有序问题,不要选择快排
-
可以尝试
Timsort
, 它作为性能优越的工业级排序算法,在处理有序问题上有很多值得我借鉴的地方-
Timsort是结合了合并排序(merge sort)和插入排序(insertion sort)而得出的排序算法,它在现实中有很好的效率
-
TimSort
算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这些 run 序列,每次拿一个 run 出来按规则进行合并。每次合并会将两个 run合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的 run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。
-
- 针对
Timsort
的测试,使用10w
基本无序数据- 快排的平均用时为0.23 s
- 归并的平均用时为0.45 s
timesort
平均用时为0.02s