Сортировка и слияние данных из разных файлов в один
У меня возникла задача реализовать алгоритм слияние файлов (совместно с сортировкой) в один большой файл. Задача усложняется тем, что каждый файл полностью помещается в памяти, но все сразу - нет. Я решил отсортировать каждый файл по отдельности и затем слить их в один большой, но вот как производить слияние, если все вместе они не помещаются в памяти.
Файлы содержат только числа от min(int64_t) и до max(int64_t), разделенные пробелом, например такая последовательность файлов:
test1.txt, test2.txt, test3.txt, test4.txt, test5.txt, test6.txt
должна быть слита в такой файл:
test.txt
Какой можно было бы выбрать способ или алгоритм для реализации данной задачи на С++ (готовые ф-ии использовать не буду, хочется самому написать код)?
Ответы (2 шт):
Для ускорения поможет буфер чтения. Просто читаем несколько чисел и их заносим в двумерный массив. А алгоритм слияния стандартный :
- Находим минимальное число из первых, что хранятся в буфере
- Вычёркиваем этот один элемент из буфера
Вот как реализовал буфера :
// несколько чисел на каждый файл
int64_t bufer [ filesN ] [ bufsize ] ;
// индекс первого числа
int buferindex [ filesN ] ;
// количество чисел заполнено
int bufersize [ filesN ] ;
// флаг конец файла достигнут
bool fileseof [ filesN ] ;
Функции в реализации :
читаю первое число, что находится в буфере i-го файла и записываю в указанное место памяти, возвращаю true или false
bool buffirst ( int , int64_t * ) ;
удаляю первый элемент из буфера i-го файла
void popfirst ( int ) ;
нахожу минимальное число из доступных первых, записываю результат в место памяти, удаляю этот элемент из буфера, возвращаю true или возвращаю false
bool findmin ( int64_t * ) ;
в главной функции просто нахожу минимальное число с помощью findmin , и записываю в файл
Тестировал в Си и плюсах такой код :
// gcc -std=c11 -Os main.c -o main
// g++ -x c++ -std=c++11 -Os main.c -o mainpp
# include <errno.h>
# include <string.h>
# include <stdio.h>
# include <stdlib.h>
# include <stdbool.h>
# include <stdint.h>
char const * files [ ] = { "test1.txt" , "test2.txt" , "test3.txt" ,
"test4.txt" , "test5.txt" , "test6.txt" } ;
char const fileo [ ] = "test.txt" ;
enum { filesN = sizeof ( files ) / sizeof ( files [ 0 ] ) } ;
FILE * fa [ filesN ] ;
FILE * fo ;
enum { bufsize = 12 } ;
int64_t bufer [ filesN ] [ bufsize ] ;
int buferindex [ filesN ] ;
int bufersize [ filesN ] ;
bool fileseof [ filesN ] ;
// читаю первое число, что находится в
// буфере [filei] файла и записываю в * xp , возвращаю true
// или возвращаю false
bool buffirst ( int const filei , int64_t * const xp ) {
int const ind = buferindex [ filei ] ;
if ( ind < bufersize [ filei ] ) {
* xp = bufer [ filei ] [ ind ] ;
return true ;
}
if ( fileseof [ filei ] )
return false ;
bufersize [ filei ] = 0 ;
{ int i = 0 ;
do {
if ( fscanf ( fa [ filei ] , "%ld" ,
& ( bufer [ filei ] [ i ] ) ) != 1 ) {
fileseof [ filei ] = true ;
break ;
} // if scanf
++ ( bufersize [ filei ] ) ;
++ i ;
} while ( i < bufsize ) ;
} // i
if ( bufersize [ filei ] == 0 )
return false ;
* xp = bufer [ filei ] [ 0 ] ;
buferindex [ filei ] = 0 ;
return true ;
}
// удаляю первый элемент из буфера [ bestfile ] файла
static inline void popfirst ( int const bestfile ) {
++ ( buferindex [ bestfile ] ) ;
}
// нахожу минимальное число из доступных первых,
// записываю результат в место памяти,
// удаляю этот элемент из буфера, возвращаю true
// или возвращаю false
bool findmin ( int64_t * const xp ) {
int64_t min ;
int bestfile ;
bool found = false ;
for ( int i = 0 ; i < filesN ; ++ i ) {
int64_t x ;
if ( buffirst ( i , & x ) ) {
if ( found ) {
if ( x < min ) {
min = x ;
bestfile = i ;
}
}
else {
min = x ;
bestfile = i ;
found = true ;
}
} // if buffirst
} // for i
if ( found ) {
* xp = min ;
popfirst ( bestfile ) ;
}
return found ;
}
// нахожу минимальное число с помощью findmin , и записываю в файл
int main ( ) {
for ( int i = 0 ; i < filesN ; ++ i ) {
fa [ i ] = fopen ( files [ i ] , "r" ) ;
if ( fa [ i ] == NULL ) {
fprintf(stderr,"fopen ( `%s` ) : %s\n",files [ i ],strerror(errno));
exit ( 1 ) ;
}
} // for i
fo = fopen ( fileo , "w" ) ;
if ( fo == NULL ) {
fprintf(stderr,"fopen ( `%s` ) : %s\n",fileo,strerror(errno));
exit ( 1 ) ;
}
do {
int64_t x ;
if ( findmin ( & x ) ) {
if ( fprintf ( fo , "%ld " , x ) < 0 ) {
fputs("fprintf ( fo , \"%ld \" , x )\n",stderr);
exit ( 1 ) ;
}
}
else
break ;
} while ( true ) ;
if ( fclose ( fo ) ) {
fprintf(stderr,"fclose ( `%s` ) : %s\n",fileo,strerror(errno));
exit ( 1 ) ;
}
for ( int i = 0 ; i < filesN ; ++ i ) {
if ( fclose ( fa [ i ] ) ) {
fprintf(stderr,"fclose ( `%s` ) : %s\n",files [ i ],strerror(errno));
exit ( 1 ) ;
}
} // for i
}
К сожалению, я не могу сейчас тестировать такие большие файлы. Но все же решил поделится своей идеей. Так как автор вопроса написал хочется самому написать код я не буду лишать его такой возможности, и по этой же причине прошу не минусовать мой код приведенный на JavaScript поскольку он служит псевдокодом к ответу. Т.е. в данном случае я приведу лишь свою логику, а автор вопроса если она ему понравится сможет доработать ее на плюсах или любом другом языке, который сочтет нужным. Лично мне мое решение нравится как и сам вопрос, хотя решение не идеально (иногда и только в плане размеров файлов) в остальном оно рабочее.
Примечания:
- идея алгоритма родилась из того что командой
catвlinuxиtypeвwindowsможно объединить файлы превышающие объем оперативки (я проверял наlinux) - для демонстрации вместо файлов я использую массивы таким образом алгоритм легко протестировать даже в браузере
- алгоритм гарантирует что левый(первый) файл не содержит числа превышающего любое из чисел в правом(следующем) файле
- минусом алгоритма является то что он не гарантирует размещение в памяти отдельного файла (но частично это настраивается, хотя решение не полное если файл с одинаковыми числами не влезет в память)
Основной алгоритм:
- проходим по всем файлам и сортируем их по отдельности
- после того как у нас появилсись минимумы и максимумы в отдельных файлах мы можем найти общий минимум и максимум, а так же запомнить максимальный размер файла
- далее мы создаем какое-то количество файлов (больше чем есть - лучше) в которые будем раскидывать наши числа, куда кидать число мы решаем согласно его величине и количеству файлов, таким образом меньшие числа попадут в первый файл а самые большие в последний
- после того как мы раскидали числа по файлам, файлы нужно снова отсортировать так как первоначальная сортировка была нарушена
- после того как мы отсортировали файлы нам остается только склеить их в один большой.
Пример вывода:
Отсортированные файлы:
[ 31, 42, 94, 99, 106 ] [ 6, 109, 133, 135, 141 ] [ 6, 50, 57, 118, 126 ]
{ min: 6, max: 141, maxSizeFile: 17 }
Проверка превышения размера файла: Все хорошо
Проверка сортировки: Все хорошо
[ 6, 6, 31, 42, 50, 57, 94, 99, 106, 109, 118, 126, 133, 135, 141 ]
Запуск теста на 10000 итераций
{ errors: { testOversize: 1, testEqual: 0 }}
Как видно при 10000 итерациях произошла 1 ошибка превышения размера файла, это значит что один из файлов оказался больше самого большого первоначального файла но я не измерял на сколько, возможно совсем на чуть-чуть, все дело в данных если к примеру будет идти очень много одинаковых чисел то по алгоритму они должны попасть в 1 файл и соответственно тот разростется в размерах - это единственный видимый мной минус данного подхода, возможно есть и другие которые я не вижу на данный момент, пока не придумал как это обойти.
По большей части я не стремился дать конкретный ответ, это бы заняло куда больше времени чем я уже потратил, просто решил предложить одну из идей для реализации, возможно это подтолкнет автора вопроса к своему решению.
function test(log = console.log){
let errors = {
testOversize : 0,
testEqual: 0
}
// массивы как файлы
// генерируем для примера
const a = Array.from(Array(5)).map(e => Math.floor(Math.random() * 150))
const b = Array.from(Array(5)).map(e => Math.floor(Math.random() * 150))
const c = Array.from(Array(5)).map(e => Math.floor(Math.random() * 150))
// сколько файлов создать,
// лучше больше чем есть так как числа могут совпадать
// а это значит что один файл может стать больше чем положено
const COUNT_FILES = 100
// вместимость в процентах на один файл
const FIT_FILE = (100 / COUNT_FILES)
// функция сортировки чисел
const numSort = (a, b) => { if (a === b) return 0; return Math.sign(a - b)}
///////////////////////////////////////////////////////////////////////
// 1) отсортировать файлы по отдельнсоти
const a1 = a.sort(numSort)
const b1 = b.sort(numSort)
const c1 = c.sort(numSort)
log('Отсортированные файлы:\n', a1, b1, c1)
///////////////////////////////////////////////////////////////////////
// 2) найти минимум, максимум чисел
// и максимальный размер одного файла (для теста алгоритма)
let max = -Infinity // начинаем с наименьшего
let min = Infinity // начинаем с наибольшего
let maxSizeFile = 0 // начинаем с 0
for (arr of [a1, b1, c1]){
if (min > arr[0]) min = arr[0]
if (max < arr[arr.length -1]) max = arr[arr.length -1]
sizeFile = arr.join(' ').length // получаем размер файла в байтах (для теста)
if (maxSizeFile < sizeFile) maxSizeFile = sizeFile
}
log({min, max, maxSizeFile})
///////////////////////////////////////////////////////////////////////
// 3) раскидать данные в новые файлы
files = {} // массивы как файлы в объекте (только для демонстрации)
// cоздаем нужное количество массивов (файлов)
for (let n = 0; n < COUNT_FILES; n++){
// например files['file0'] = [] file0 инициализируем пустым массивом
files[`file${n}`] = []
}
// имена файлов,
// порядок важен в первый файл меньшие числа
// в последний большие
filesKey = Object.keys(files)
for (arr of [a1, b1, c1]){
for (n of arr){
k = ((n - min) * 100) / (max - min) // нахождение позиции
f = Math.floor(k / (FIT_FILE + 1)) // индекс файла для числа
files[filesKey[f]].push(n) // записть в файл по имени (индексу)
}
}
///////////////////////////////////////////////////////////////////////
// 4) после заполнения файлов сортируем каждый из них
for (fileKey of Object.keys(files)){
files[fileKey].sort(numSort)
}
// содержимое файлов
// log(JSON.stringify(files))
// объединение файлов
// можно выполнить консольной коммандой перенаправления
// (ее можно вызвать из C++ кода)
// cat file1 file2 > newfile - для linux
// type file1 file2 > newfile - для windows
result = Object.values(files).reduce((acc, e) => [...acc, ...e], [])
// 5) на данном этапе все выполнено, осталось сделать проверки
sizes = Object.values(files).map(e => e.join(' ').length)
const testOversize = sizes.some(e => e > maxSizeFile)
if (testOversize) errors.testOversize = 1
log(testOversize
? 'Проверка превышения размера файла: Размер превышен' + sizes.some(e => e > maxSizeFile)
: 'Проверка превышения размера файла: Все хорошо')
testEqual = result.join(' ') === [...a, ...b, ...c].sort(numSort).join(' ')
if (!testEqual) errors.testEqual = 1
log(testEqual
? 'Проверка сортировки: Все хорошо'
: 'Проверка сортировки: Ошибка')
//выводим результирующий массив (файл)
log(result)
return errors
}
test()
const nope = () => {}
const iter = 10000
console.log(`Запуск теста на ${iter} итераций`)
let errors = {
testOversize : 0,
testEqual: 0
}
for(let i=0; i < iter; i++) {
const {testEqual, testOversize} = test(nope)
errors.testEqual += testEqual
errors.testOversize += testOversize
}
console.log({errors})