编码技巧 --- 内存有限下合并大文件

2023-10-22 17:00:24 浏览数 (1)

引言

今天看书看到了这样一个问题:

「假设有10个接口访问的日志,每个日志的大小为300M,每个文件里的日志都是按照时间戳从小到大排序的。现在我们希望将这10个较小的日志文件,合并为一个大文件,合并之后的文件依旧按照时间戳从小到大排序,如果处理上述任务的机器只有1G内存,那么该如何将这10个日志文件合并?」

一般来说,如果机器内存足够大,可以直接将所有数据全部加载到内存,然后整合到一个集合后进行排序后输出一个大文件。但并不建议这样操作,这样无节制的使用内存,可能会导致性能下降甚至程序崩溃。

思路

那我们如何在有限条件下处理这样的有序多文件合并为有序大文件呢?先想想C#是如何读取大文件的?

C#处理大文件的方法是使用流(Stream)而不是一次性将整个文件加载到内存中。

代码语言:javascript复制
int bufferSize = 1024 * 1024; // 使用1MB的缓冲区

byte[] buffer = new byte[bufferSize];

try
{
    using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
    {
        int bytesRead;

        while ((bytesRead = fileStream.Read(buffer, 0, bufferSize)) > 0)
        {
            // do something... 
        }

    }
}
catch
{

}

那我们要多个有序文件合并成一个文件,就反过来,我们从每个文件中取出最小的数据,然后分多路依次合并到目标文件中。这其实就是「归并排序中的 Merge()函数的处理思路」。想仔细了解可以看一下数据结构与算法 --- 排序算法(二)

实现

可以将文件看作数组,那问题就变成了多个有序数组合并为一个有序数组。

用C#代码实现如下:

代码语言:javascript复制
public static void Main()
{
    //这里暂时只使用3组数据
    int[][] sortedArrays = new int[][]
    {
        new int[] { 1, 4, 7, 10, 13 },
        new int[] { 2, 5, 8, 11, 14 },
        new int[] { 3, 6, 9, 12, 15 },

    };

    int[] mergedArray = MergeSortedArrays(sortedArrays);
    Console.WriteLine("合并后的有序数组:");
    foreach (int num in mergedArray)
    {
        Console.Write(num   " ");
    }
}

private static int[] MergeSortedArrays(int[][] arrays)
{
    int totalLength = 0;
    foreach (int[] arr in arrays)
    {
        totalLength  = arr.Length;
    }

    int[] mergedArray = new int[totalLength];
    int[] currentIndex = new int[arrays.Length];

    for (int i = 0; i < totalLength; i  )
    {
        int minIndex = -1;
        int minValue = int.MaxValue;

        // 在所有数组中选择最小值
        for (int j = 0; j < arrays.Length; j  )
        {
            if (currentIndex[j] < arrays[j].Length && arrays[j][currentIndex[j]] < minValue)
            {
                minValue = arrays[j][currentIndex[j]];
                minIndex = j;
            }
        }

        // 将最小值放入合并后的数组
        mergedArray[i] = minValue;
        currentIndex[minIndex]  ;
    }

    return mergedArray;
}

在上面的示例代码中,我们模拟创建了3个有序数组(sortedArrays),然后调用 MergeSortedArrays 方法来将这些有序数组合并为一个有序数组。在 MergeSortedArrays 方法中,我们使用了一个辅助数组 currentIndex 来记录每个有序数组当前的索引位置。然后,我们依次从所有数组中选择最小值,将其放入合并后的数组中,并更新对应数组的索引。重复这个过程直到合并后的数组填满,即得到了合并后的有序数组。

上述代码执行结果:

代码语言:javascript复制
合并后的有序数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

那么如果换成日志文件,为了解决内存条件限制,则可以为每个小文件及最终的排序文件,都前置一个内存缓存(数组),在读取数据时,一次性读取一批数据到内存(如同文章开头的示例),同理,写入数据时,先写数据到内存,等内存满了之后,在一次性地将内存中的数据写入到最终的排序文件中。

至于为什么要等到内存满了才写入,是因为磁盘的读写速度远慢于内存的读写速度,等到内存满了在写入,能够充分利用内存,节省执行时间,提高效率,但是还是需要注意尺度,避免程序直接崩溃

0 人点赞