飞道的博客

字节跳动面试:5 亿整数的大文件,如何排序?我懵逼了

385人阅读  评论(0)

最近,面试头条,面试官一上来,就问了我这么一个问题,我一脸懵逼,决定记录一下。

问题

给你1个文件bigdata,大小4663M,5亿个数,文件中的数据随机,如下一行一个整数:


  
  1. 6196302
  2. 3557681
  3. 6121580
  4. 2039345
  5. 2095006
  6. 1746773
  7. 7934312
  8. 2016371
  9. 7123302
  10. 8790171
  11. 2966901
  12. ...
  13. 7005375

现在要对这个文件进行排序,怎么搞?

内部排序

先尝试内排,选2种排序方式。

3路快排:


  
  1. private final int cutoff = 8;
  2. public <T> void perform(Comparable<T>[] a) {
  3. perform(a, 0, a.length - 1);
  4. }
  5. private <T> int median3(Comparable<T>[] a, int x, int y, int z) {
  6. if (lessThan(a[x], a[y])) {
  7. if (lessThan(a[y], a[z])) {
  8. return y;
  9. } else if (lessThan(a[x], a[z])) {
  10. return z;
  11. } else {
  12. return x;
  13. }
  14. } else {
  15. if (lessThan(a[z], a[y])) {
  16. return y;
  17. } else if (lessThan(a[z], a[x])) {
  18. return z;
  19. } else {
  20. return x;
  21. }
  22. }
  23. }
  24. private <T> void perform(Comparable<T>[] a, int low, int high) {
  25. int n = high - low + 1;
  26. // 当序列非常小,用插入排序
  27. if (n <= cutoff) {
  28. InsertionSort insertionSort = SortFactory.createInsertionSort();
  29. insertionSort.perform(a, low, high);
  30. // 当序列中小时,使用median3
  31. } else if (n <= 100) {
  32. int m = median3(a, low, low + (n >>> 1), high);
  33. exchange(a, m, low);
  34. // 当序列比较大时,使用ninther
  35. } else {
  36. int gap = n >>> 3;
  37. int m = low + (n >>> 1);
  38. int m1 = median3(a, low, low + gap, low + (gap << 1));
  39. int m2 = median3(a, m - gap, m, m + gap);
  40. int m3 = median3(a, high - (gap << 1), high - gap, high);
  41. int ninther = median3(a, m1, m2, m3);
  42. exchange(a, ninther, low);
  43. }
  44. if (high <= low)
  45. return;
  46. // lessThan
  47. int lt = low;
  48. // greaterThan
  49. int gt = high;
  50. // 中心点
  51. Comparable<T> pivot = a[low];
  52. int i = low + 1;
  53. /*
  54. * 不变式:a[low..lt-1] 小于pivot -> 前部(first) a[lt..i-1] 等于 pivot -> 中部(middle)
  55. * a[gt+1..n-1] 大于 pivot -> 后部(final)
  56. *
  57. * a[i..gt] 待考察区域
  58. */
  59. while (i <= gt) {
  60. if (lessThan(a[i], pivot)) {
  61. // i-> ,lt ->
  62. exchange(a, lt++, i++);
  63. } else if (lessThan(pivot, a[i])) {
  64. exchange(a, i, gt--);
  65. } else {
  66. i++;
  67. }
  68. }
  69. // a[low..lt-1] < v = a[lt..gt] < a[gt+1..high].
  70. perform(a, low, lt - 1);
  71. perform(a, gt + 1, high);
  72. }

归并排序:


  
  1. /**
  2. * 小于等于这个值的时候,交给插入排序
  3. */
  4. private final int cutoff = 8;
  5. /**
  6. * 对给定的元素序列进行排序
  7. *
  8. * @param a 给定元素序列
  9. */
  10. @Override
  11. public <T> void perform(Comparable<T>[] a) {
  12. Comparable<T>[] b = a.clone();
  13. perform(b, a, 0, a.length - 1);
  14. }
  15. private <T> void perform(Comparable<T>[] src, Comparable<T>[] dest, int low, int high) {
  16. if (low >= high)
  17. return;
  18. // 小于等于cutoff的时候,交给插入排序
  19. if (high - low <= cutoff) {
  20. SortFactory.createInsertionSort().perform(dest, low, high);
  21. return;
  22. }
  23. int mid = low + ((high - low) >>> 1);
  24. perform(dest, src, low, mid);
  25. perform(dest, src, mid + 1, high);
  26. // 考虑局部有序 src[mid] <= src[mid+1]
  27. if (lessThanOrEqual(src[mid], src[mid + 1])) {
  28. System.arraycopy(src, low, dest, low, high - low + 1);
  29. }
  30. // src[low .. mid] + src[mid+1 .. high] -> dest[low .. high]
  31. merge(src, dest, low, mid, high);
  32. }
  33. private <T> void merge(Comparable<T>[] src, Comparable<T>[] dest, int low, int mid, int high) {
  34. for ( int i = low, v = low, w = mid + 1; i <= high; i++) {
  35. if (w > high || v <= mid && lessThanOrEqual(src[v], src[w])) {
  36. dest[i] = src[v++];
  37. } else {
  38. dest[i] = src[w++];
  39. }
  40. }
  41. }

数据太多,递归太深 ->栈溢出?加大Xss?

数据太多,数组太长 -> OOM?加大Xmx?

耐心不足,没跑出来.而且要将这么大的文件读入内存,在堆中维护这么大个数据量,还有内排中不断的拷贝,对栈和堆都是很大的压力,不具备通用性。

sort命令来跑

跑了多久呢?24分钟。

为什么这么慢?

粗略的看下我们的资源:

内存 jvm-heap/stack,native-heap/stack,page-cache,block-buffer 外存 swap + 磁盘 数据量很大,函数调用很多,系统调用很多,内核/用户缓冲区拷贝很多,脏页回写很多,io-wait很高,io很繁忙,堆栈数据不断交换至swap,线程切换很多,每个环节的锁也很多。

总之,内存吃紧,问磁盘要空间,脏数据持久化过多导致cache频繁失效,引发大量回写,回写线程高,导致cpu大量时间用于上下文切换,一切,都很糟糕,所以24分钟不细看了,无法忍受。

位图法


  
  1. private BitSet bits;
  2. public void perform(String largeFileName, int total, String destLargeFileName, Castor<Integer> castor,
  3. int readerBufferSize, int writerBufferSize, boolean asc) throws IOException {
  4. System. out.println( "BitmapSort Started.");
  5. long start = System.currentTimeMillis();
  6. bits = new BitSet(total);
  7. InputPart<Integer> largeIn = PartFactory.createCharBufferedInputPart(largeFileName, readerBufferSize);
  8. OutputPart<Integer> largeOut = PartFactory.createCharBufferedOutputPart(destLargeFileName, writerBufferSize);
  9. largeOut.delete();
  10. Integer data;
  11. int off = 0;
  12. try {
  13. while ( true) {
  14. data = largeIn.read();
  15. if (data == null)
  16. break;
  17. int v = data;
  18. set(v);
  19. off++;
  20. }
  21. largeIn.close();
  22. int size = bits.size();
  23. System. out.println(String.format( "lines : %d ,bits : %d", off, size));
  24. if (asc) {
  25. for ( int i = 0; i < size; i++) {
  26. if ( get(i)) {
  27. largeOut.write(i);
  28. }
  29. }
  30. } else {
  31. for ( int i = size - 1; i >= 0; i--) {
  32. if ( get(i)) {
  33. largeOut.write(i);
  34. }
  35. }
  36. }
  37. largeOut.close();
  38. long stop = System.currentTimeMillis();
  39. long elapsed = stop - start;
  40. System. out.println(String.format( "BitmapSort Completed.elapsed : %dms", elapsed));
  41. } finally {
  42. largeIn.close();
  43. largeOut.close();
  44. }
  45. }
  46. private void set(int i) {
  47. bits. set(i);
  48. }
  49. private boolean get(int v) {
  50. return bits. get(v);
  51. }

nice! 跑了190秒,3分来钟. 以核心内存4663M/32大小的空间跑出这么个结果,而且大量时间在用于I/O,不错。

问题是,如果这个时候突然内存条坏了1、2根,或者只有极少的内存空间怎么搞?

外部排序

该外部排序上场了,外部排序干嘛的?

内存极少的情况下,利用分治策略,利用外存保存中间结果,再用多路归并来排序;

map-reduce的嫡系。

 

 

1、分

内存中维护一个极小的核心缓冲区memBuffer,将大文件bigdata按行读入,搜集到memBuffer满或者大文件读完时,对memBuffer中的数据调用内排进行排序,排序后将有序结果写入磁盘文件bigdata.xxx.part.sorted. 循环利用memBuffer直到大文件处理完毕,得到n个有序的磁盘文件:

 

2、合

现在有了n个有序的小文件,怎么合并成1个有序的大文件?把所有小文件读入内存,然后内排?(⊙o⊙)… no!

利用如下原理进行归并排序:

 

我们举个简单的例子:


  
  1. 文件 13, 6, 9
  2. 文件 22, 4, 8
  3. 文件 31, 5, 7
  4. 第一回合:
  5. 文件 1的最小值: 3 , 排在文件 1的第 1
  6. 文件 2的最小值: 2,排在文件 2的第 1
  7. 文件 3的最小值: 1,排在文件 3的第 1
  8. 那么,这 3个文件中的最小值是: min( 1, 2, 3) = 1
  9. 也就是说,最终大文件的当前最小值,是文件 123的当前最小值的最小值,绕么?
  10. 上面拿出了最小值 1,写入大文件.
  11. 第二回合:
  12. 文件 1的最小值: 3 , 排在文件 1的第 1
  13. 文件 2的最小值: 2,排在文件 2的第 1
  14. 文件 3的最小值: 5,排在文件 3的第 2
  15. 那么,这 3个文件中的最小值是: min( 5, 2, 3) = 2
  16. 2写入大文件.
  17. 也就是说,最小值属于哪个文件,那么就从哪个文件当中取下一行数据.(因为小文件内部有序,下一行数据代表了它当前的最小值)

最终的时间,跑了771秒,13分钟左右。


  
  1. less bigdata .sorted .text
  2. ...
  3. 9999966
  4. 9999967
  5. 9999968
  6. 9999969
  7. 9999970
  8. 9999971
  9. 9999972
  10. 9999973
  11. 9999974
  12. 9999975
  13. 9999976
  14. 9999977
  15. 9999978
  16. ...

转载:https://blog.csdn.net/m0_46995061/article/details/106571312
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场