list的插入排序
公司對入職同學的一道考試題:有大約40萬個數字(數字範圍:0~200000000000000),數字沒有重複,這些數字在一個txt檔案中,每行為一個數字。 * 這個txt檔案放在一個WEB伺服器上, 可以通過http://ip:8888/numbers.txt 下載。 * 求這些數字中,最大的100個數字之和。 * 注意:執行堆記憶體只有4M ( java -Xmx4m ) * 不允許寫臨時檔案 * 要求耗時:小於2秒 (CPU:Intel i5-4590 3.3GHz)
1. 簡單的思路是:從輸入流中按行讀取所有數字放到一個集合中,然後排序,並取前100個的和,只是這樣會比較慢。
讀取流的時間應該是省不了,但是可以改進下排序求和的過程:在插入的時候直接比較排序
- 1.1 定義一個LinkedList,第一個元素直接插入,然後
- 1.2 如果待插入的數值大於第一個元素,就將其插到列表首節點。
- 1.3. 否則如果待插入的元素小於末尾元素,就將其插到列表末尾節點。
- 1.4. 否則就挨個比較,放入比前一個節點小比後一個節點大的位置。
- 1.5. 放完之後如果列表長度大於100,就去除最後一個節點(index=100)。
這樣可以預估一下最壞的情況下需要的比較次數: 0 + 1 + 2 + 3 + ... + 99 + (400000 - 100) * 100 次
public class Demo { private static LinkedList<Long> list = newLinkedList<Long>(); public static long p() throws IOException{ String url = "http://localhost:8888/numbers.txt"; URLConnection urlconn = new URL(url).openConnection(); urlconn.connect(); HttpURLConnection httpconn =(HttpURLConnection)urlconn; int resp = httpconn.getResponseCode();if(httpconn.getResponseCode() != HttpURLConnection.HTTP_OK) { throw new IOException("連線失敗["+ resp +"],url:" + url); } BufferedReader br = null; try{ InputStreamReader input = new InputStreamReader(urlconn.getInputStream(),"UTF-8"); br = new BufferedReader(input, 4 * 1024 *1024); //4M快取 String line = ""; while((line = br.readLine()) != null){ try{ list.add(Long.parseLong(line.trim())); //最多隻插入100,減少遍歷次數 if(list.size() > 100){ list.removeLast(); } }catch(Exception e){ System.out.println("非法資料:" + line); } } }finally{ if(br != null){ br.close(); } } long result = 0; for(long i : list){ result = result + i; } return result; } private static void add(long e) { //空集合,直接插入元素 if(list.isEmpty()){ list.add(e); return; } //大於頭節點,直接插入到頭節點 if(e >= list.getFirst()){ list.addFirst(e); return; } //小於尾節點,直接插入尾節點 if(e < list.getLast()){ if(list.size() < 100){ list.addLast(e); } return; } //插入到中間節點 for (int i = 0; i < list.size(); i++) { if (e >= list.get(i + 1)) { list.add(i + 1, e); return; } } } public static void test(){ SecureRandom random = new SecureRandom(); List<Long> alist = new ArrayList<Long>(400000); for(int i = 0;i < 400000;i++){ long a = random.nextLong() % 200000000000000L; alist.add(a); } long t = System.currentTimeMillis(); for(long l : alist){ add(l); if(list.size() > 100){ list.removeLast(); } } System.out.println("插入耗時:" + (System.currentTimeMillis() - t) + "ms"); System.out.println("size:" + list.size()); long result = 0; for(long i : list){ result = result + i; } System.out.println("result:" + result); System.out.println("總耗時:" + (System.currentTimeMillis() - t) + "ms"); System.out.println(list); } public static void main(String[] args) throws IOException { test(); } }
因為這裡主要的操作是插入刪除,所以定義LinkedList例項。指給LinkedList的引用,是為了可以直接呼叫addFirst(E e),addLast(E,e)和removeLast(E,e)方法,如果指給List引用的話,呼叫List的add(int
index ,E e)和remove(int index)其實也一樣,LinkedList是連結串列實現的,本身不具有index下標屬性,為了實現List的方法,它通過判斷index是靠前還是靠後,決定從前向後還是從後向前通過引用計數。
這樣實現後,在排序上所花費的時間會有明顯減少,可以單獨測一下排花費的時間ms:16 16 15 16 16 16 15 16 16 15
2. LinkedList的add()方法在插入中間節點時,其實尋找了兩次元素的下標,get(i)和add(i,e),如果在比較得出元素應該插入的index時就直接將元素插入是不是可以更節省時間呢。
由於LinkedList的一些方法和元素不可繼承,就模擬寫了個SortLongLinkedList,添加了一個linkLongWithSort(long e)方法,對於待插入的元素,當找到位置時直接插入,而不是先記下找到的位置index,然後再呼叫add把它插入到index位置
import java.util.Iterator; import java.util.NoSuchElementException; public class SortLongLinkedList implements Iterable<Long>{ private int size = 0; private Node<Long> first; private Node<Long> last; //e < first.item && e > last.item @SuppressWarnings({ "unchecked", "rawtypes" }) public void linkLongWithSort(long e){ Node<Long> nextNode = first.next; for(;;){ if(e < nextNode.item){ nextNode = nextNode.next; continue; } Node<Long> preNode = nextNode.prev; Node<Long> newNode = new Node(preNode,e,nextNode); preNode.next = newNode; nextNode.prev = newNode; size++; break; } } public Long getFirst() { final Node<Long> f = first; if (f == null) throw new NoSuchElementException(); return f.item; } public Long getLast() { final Node<Long> l = last; if (l == null) throw new NoSuchElementException(); return l.item; } public void linkFirst(Long e) { final Node<Long> f = first; final Node<Long> newNode = new Node<Long>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; } public void linkLast(Long e) { final Node<Long> l = last; final Node<Long> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; } public Long unlinkLast() { final Long element = last.item; final Node<Long> prev = last.prev; last.item = null; last.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; return element; } public boolean isEmpty() { return size == 0; } public int size() { return size; } private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } @Override public Iterator<Long> iterator() { return new Iterator<Long>(){ Node<Long> current = first; @Override public boolean hasNext() { return current.next != last; } @Override public Long next() { long l = current.next.item; current = current.next; return l; } @Override public void remove() { // TODO Auto-generated method stub } }; } public String toString(){ StringBuilder builder = new StringBuilder(); Node<Long> n = first; builder.append(n.item); while((n = n.next) != last){ builder.append(","); builder.append(n.item); } builder.append(","); builder.append(last.item); return builder.toString(); } }
然後將使用的LinkedList改為SortLongLinkedList:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; public class Demo2 { private static SortLongLinkedList list = new SortLongLinkedList(); public static long p() throws IOException{ String url = "http://localhost:8888/numbers.txt"; URLConnection urlconn = new URL(url).openConnection(); urlconn.connect(); HttpURLConnection httpconn =(HttpURLConnection)urlconn; int resp = httpconn.getResponseCode(); if(httpconn.getResponseCode() != HttpURLConnection.HTTP_OK) { throw new IOException("連線失敗["+ resp +"],url:" + url); } BufferedReader br = null; try{ InputStreamReader input = new InputStreamReader(urlconn.getInputStream(),"UTF-8"); br = new BufferedReader(input, 4 * 1024 *1024); //4M快取 String line = ""; while((line = br.readLine()) != null){ try{ add(Long.parseLong(line.trim())); //最多隻插入100,減少遍歷次數 if(list.size() > 100){ list.unlinkLast(); } }catch(Exception e){ System.out.println("非法資料:" + line); } } }finally{ if(br != null){ br.close(); } } long result = 0; for(long i : list){ result = result + i; } return result; } private static void add(long e) { //空集合,直接插入元素 if(list.isEmpty()){ list.linkFirst(e); return; } //大於頭節點,直接插入到頭節點 if(e >= list.getFirst()){ list.linkFirst(e); return; } //小於尾節點,直接插入尾節點 if(e < list.getLast()){ if(list.size() < 100){ list.linkLast(e); } return; } //插入到中間節點 list.linkLongWithSort(e); } public static void test(){ SecureRandom random = new SecureRandom(); List<Long> alist = new ArrayList<Long>(400000); for(int i = 0;i < 400000;i++){ long a = random.nextLong() % 200000000000000L; alist.add(a); } long t = System.currentTimeMillis(); for(long l : alist){ add(l); if(list.size() > 100){ list.unlinkLast(); } } System.out.println("插入耗時:" + (System.currentTimeMillis() - t) + "ms"); System.out.println("size:" + list.size()); long result = 0; for(long i : list){ result = result + i; } System.out.println("result:" + result); System.out.println("總耗時:" + (System.currentTimeMillis() - t) + "ms"); System.out.println(list); } public static void main(String[] args) throws IOException { test(); } }
把兩個demo都測10遍,記錄插入排序得出400000個長整型數中前100個元素的耗時(ms),可以看出一些非常微小的提升。用SortLongLinkedList測的結果:14 14 14 15 15 15 15 14 14 15
3. 在插入中間節點時省去了一次獲取下標為index的元素的動作,但是在比較的時候還是順序比較的,如果將順序比較換成二分法排序插入,應該是可以更快的。
但是由於LinkedList不能直接通過下標獲取元素,如果在LinkedList上對元素個數用二分法排序反而適得其反,因為獲取中間下標元素本身就增加了尋找動作。
所以直接用ArrayList反而比較合適,只是ArrayList中摻和了陣列拷貝的成本,為了體現出陣列拷貝的開銷,這裡直接定義了一個固定長度的陣列,本質上與使用ArrayList是相同的動作。
import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class Demo3 { static Long[] array = new Long[100]; static int inserted = 0; public static void test(){ SecureRandom random = new SecureRandom(); List<Long> alist = new ArrayList<Long>(400000); for(int i = 0;i < 400000;i++){ long a = random.nextLong() % 200000000000000L; alist.add(a); } long t = System.currentTimeMillis(); for(long l : alist){ add(l); } System.out.println("插入耗時:" + (System.currentTimeMillis() - t) + "ms"); long result = 0; for(long i : array){ result = result + i; } System.out.println("result:" + result); System.out.println("總耗時:" + (System.currentTimeMillis() - t) + "ms"); System.out.println(Arrays.asList(array)); } private static void count(){ if(inserted >= 100){ return; } inserted++; } private static void add(long e){ if(array[0] == null){ array[0] = e; count(); return; } //大於頭節點,直接插入到頭節點 if(e >= array[0]){ int move = inserted; if(inserted == 100){ move = 99; } System.arraycopy(array, 0, array, 1, move); array[0] = e; count(); return; } //小於尾節點,直接插入尾節點 if(e < array[inserted - 1]){ if(inserted < 100){ array[inserted] = e; count(); } return; } middleAdd(e); } // 二分法查詢中間插入 private static void middleAdd(long e){ int left = 0; int right = inserted - 1; int middle = 0; while( right >= left){ middle = ( left + right) / 2; if(e < array[middle]){ left = middle + 1; }else if(e > array[middle]){ right = middle - 1; } } int index = middle; if(right == middle){//right < left && e <= array[middle] index = middle + 1; } int move = inserted - index; if(move > 0){ if(inserted == 100){//陣列越界 move = move - 1; } System.arraycopy(array, index, array, index + 1, move); } array[index] = e; count(); } public static void main(String[] args) { test(); } }
也紀錄了下插入排序得出400000個長整型數中前100個元素的耗時(ms),測了10組資料:11 11 11 11 13 12 11 15 12 11
這次提升比上次明顯,除了二分法插入排序,還有一個原因是少了很多java物件例項的建立,在使用List的時候,他其實會把每個元素構造成一個Node的節點例項物件,這個例項的new動作積少成多也是可觀的。這裡其實也有自動裝箱long為Long例項的過程,如果將上面的陣列的Long[] array改為long array,將看到耗時會再次降低,測的時候發現最少只要9ms就可以了。