1. 程式人生 > >list的插入排序

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 = new
LinkedList<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就可以了。