大数据算法:对5亿数据进行排序
0.前言:
在大數據研究的路上,我們總要對一些很大的數據進行各種各樣的操作。比如說對數據排序,比如說對數據統計,比如說對數據計算。而在大量的數據面前,我們總是束手無策,因為我們無法在限定時間的情況下,在效率上做到讓人滿意,也無法在限定空間的情況下,能夠快速解決問題。可能我們在一些日常的開發過程中,沒有遇到過這些問題。不過,現在是時候來考慮一下這樣的問題了。因為,現在正值大數據的時代。
在本文中我會用三種方法,從兩個方面來說明如何解決對5億數據進行排序工作。
1.版本說明
商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
本文作者:Q-WHai
發表日期: 2015年10月24日
本文鏈接:https://blog.csdn.net/lemon_tree12138/article/details/48783535
來源:CSDN
更多內容:分類 >> 算法與數學
2.思路分析:
? 拿到這樣的一個問題,你的第一感覺是什么?冒泡排序?選擇排序?插入排序?堆排?還是快排?可能你的想法是我的內存不夠。的確,這么大的一個數據量,我們的內存的確不夠。因為單是5億的整數數據就有3.7個G(別說你是壕,內存大著呢)。既然內存不夠,那么我們要怎么來解決呢?
? 要知道我們一步做不了的事,兩步總能做到。那么我們就來嘗試第一步做一些,剩下的一些就等會再來搞定吧。基于這樣的思路,就有下面的一個解題方法——分治!
1.分治——根據數據存在文件中的位置分裂文件到批量小文件中
? 相對于樸素的排序,這是一種比較穩妥的解決方法。因為數據量太大了!我們不得不將大事化小,小事化了。
? 這里我們的做法是每次讀取待排序文件的10000個數據,把這10000個數據進行快速排序,再寫到一個小文件bigdata.part.i.sorted中。這樣我們就得到了50000個已排序好的小文件了。
? 在有已排序小文件的基礎上,我只要每次拿到這些文件中當前位置的最小值就OK了。再把這些值依次寫入bigdata.sorted中。
2.分治——根據數據自身大小分裂文件到批量小文件中
? 按照數據位置進行分裂大文件也可以。不過這樣就導致了一個問題,在把小文件合并成大文件的時候并不那么高效。那么,這里我們就有了另一種思路:我們先把文件中的數據按照大小把到不同的文件中。再對這些不同的文件進行排序。這樣我們可以直接按文件的字典序輸出即可。
3.字典樹
? 關于字典樹的基本使用,大家可以參見本人的另一篇博客:《數據結構:字典樹的基本使用》
? 基于《數據結構:字典樹的基本使用》這篇博客中對字典序的講解,我們知道我們要做就是對字典樹進行廣度優先搜索。
?
3.結構設計圖:
??
?
4.代碼分析:
0.分治
(0)分割大文件
? 此步對大文件的分割是按序進行的,這樣我們就可以確保數據的離散化,不會讓一個小文件中的數據很多,一個小文件的數據很少。
?
public static void splitBigFile2PartBySerial(String filePath, String partPath) throws IOException {File file = new File(filePath);FileInputStream inputStream = new FileInputStream(file);BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));StringBuffer buffer = new StringBuffer();String readerLine = "";int line = 0;while ((readerLine = reader.readLine()) != null) {buffer.append(readerLine + " ");if (++line % Config.PART_NUMBER_COUNT == 0) {sortStringBuffer(buffer);int splitLine = line / Config.PART_NUMBER_COUNT;write(partPath.replace("xxx", "" + splitLine), buffer.toString());buffer.setLength(0);System.out.println("SPLIT: " + splitLine);}}reader.close();}?
(1)排序
?
? 即使是已經切割成小份的了,不過每個小文件中的數據集仍然有50000個。因為50000個數據也不是一個小數據,在排序的過程中,也會有一些講究,所有這里我們使用的是快排。如下:
?
public static void sortStringBuffer(StringBuffer buffer) {String[] numberTexts = buffer.toString().split(" ");buffer.setLength(0);int[] numbers = new int[numberTexts.length];for (int i = 0; i < numberTexts.length; i++) {numbers[i] = Integer.parseInt(numberTexts[i]);}int[] sorted = QKSort.quickSort(numbers);for (int i = 0; i < sorted.length; i++) {buffer.append(sorted[i] + "\n");}}?
(2)合并
?
? 在合并的時候,我們要明確一個問題。雖然我們的單個小文件已經有序,不過我們還并不知道整體的順序。比如:
? 文件1:1 2 4 6 9 34 288
? 文件2:4 5 6 87 99 104 135
? 上面的兩個文件雖然每個文件內部已經有序,不過整體來說,是無序的。對于在單個文件有序的基礎上,我們可以做一些事情。我們可以把每個文件中的數據看成是一個隊列,我們總是從隊列的首部開始進行出隊(因為隊列的頭部總是最小的數)。這樣,我們就把問題轉化成從N個小文件中依次比較,得到最小的結果并記入文件(當然,我們不可以生成一個數就寫一次文件,這樣太低效了,我們可以使用一個變量緩存這此"最小值",在累計到一定數量之后再一次性寫入。再清空變量,循環反復,直到文件全部寫入完畢)。
?
public static void mergeSorted(String dirPath) throws NumberFormatException, IOException {long t = System.currentTimeMillis();File dirFile = new File(dirPath);File[] partFiles = dirFile.listFiles();FileInputStream[] inputStreams = new FileInputStream[partFiles.length];BufferedReader[] readers = new BufferedReader[partFiles.length];int[] minNumbers = new int[partFiles.length];for (int i = 0; i < partFiles.length; i++) {inputStreams[i] = new FileInputStream(partFiles[i]);readers[i] = new BufferedReader(new InputStreamReader(inputStreams[i]));minNumbers[i] = Integer.parseInt(readers[i].readLine());}int numberCount = Config.TOTAL_NUMBER_COUNT;while (true) {int index = Tools.minNumberIndex(minNumbers);System.out.println(minNumbers[index]);write(Config.BIGDATA_NUMBER_FILEPATH_SORTED, minNumbers[index] + "\n");minNumbers[index] = Integer.parseInt(readers[index].readLine());if (numberCount-- <= 0) {break;}}System.err.println("TIME: " + (System.currentTimeMillis() - t));for (int i = 0; i < partFiles.length; i++) {inputStreams[i].close();readers[i].close();}}注:這里關于分治的算法,我就只提供一種實現過程了。可能從上面的說明中,大家也意識到了一個問題,如果我們把大文件中的數據按照數值大小化分到不同的小文件中。這樣會有一個很致命的問題,那就是可能我們的小文件會出現兩極分化的局面,即有一部分文件中的數據很少,有一部分小文件中的數據很多。所以,這里我就不再提供實現過程,在上面有所說明,只是想說我們在解決問題的時候,可能會有很多不同的想法,這些想法都很好,只是有時我們需要一個最優的來提升逼格(^_^)。
?
1.字典樹
? 因為我們知道字典樹是可以壓縮數據量的一種數據結構,尤其是針對那么使用的字符串個數有限(比如:'0','1','2','3','4','5','6','7','8','9'),并整體數量很多的情況。因為我們可以可以讓同一個字符重復使用多次。比如:"123456789"和"123456780"其實只使用了'0'-'9'這10個字符而已。關于字典樹的實現,我想是很簡單的一種方法。如果你還是感覺有些朦朧和模糊的話,就請參見本人的另一篇博客《數據結構:字典樹的基本使用》,在那一篇博客中,我有很詳細地介紹對字典樹的各種基本操作及說明。
? 這里我還是貼出一部分關鍵的代碼,和大家一起學習吧。代碼如下:
(0)將數據記入文件
?
public static void sortTrie(String filePath) throws IOException {File file = new File(filePath);FileInputStream inputStream = new FileInputStream(file);BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));TrieTree tree = new TrieTree("sorting");String readerLine = "";int line = 0;while ((readerLine = reader.readLine()) != null) {tree.insert(readerLine);if (++line % Config.PART_NUMBER_COUNT == 0) {System.out.println("LINE: " + line);}}System.out.println("文件讀取完畢");writeTrieTree2File(Config.BIGDATA_NUMBER_FILEPATH_SORTED, tree);reader.close();}?
(1)對字典樹進行廣度優先搜索
?
?
public static void sortNumberOrder(String filePath, Node node) throws IOException {Queue<Node> queuing = new LinkedList<Node>();queuing.offer(node);while (!queuing.isEmpty()) {Node currentNode = queuing.poll();if (currentNode.isEnd()) {buffer.append(getNodePath(currentNode) + "\n");if (++index % 50000 == 0) {write(filePath, buffer.toString());}}Node[] children = currentNode.getChildren();for (Node sonNode : children) {if (sonNode != null) {queuing.offer(sonNode);}}}}/*** 獲得某一節點的上層節點,即前綴字符串* @param node* @return*/public static String getNodePath(Node node) {StringBuffer path = new StringBuffer();Node currentNode = node;while (currentNode.getParent() != null) {path.append(currentNode.getName());currentNode = currentNode.getParent();}return path.reverse().toString();}?
5.小結:
?
在大數據的探索還遠不止于此。還有很多東西等著我們去了解,去發現,以及創造。
而對于大量數據的問題,我們可以利用分治來化解它的大,從而可以更方便地去觀察全局。也可以使用我們已經學習過的一些數據結構及算法來求解問題。不過隨著我們不斷地學習,不斷地探索,我們可能會感覺到自己學習的一些固有的數據結構和算法并不能完全地解決我們遇到的問題,那么就需要我們的創造力了。
總結
以上是生活随笔為你收集整理的大数据算法:对5亿数据进行排序的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构:字典树的基本使用
- 下一篇: Trie树进阶:Double-Array