五大常用算法二(貪心,分治)

貪心算法

貪心算法總是作出在當前看來最好的選擇。也就是說貪心算法并不從整體最優考慮,它所作出的選擇只是在某種意義上的局部最優選擇。當然,希望貪心算法得到的最終結果也是整體最優的。雖然貪心算法不能對所有問題都得到整體最優解,但對許多問題它能產生整體最優解,如之前的Dijkstra算法,Prim算法,Kruskal算法。如果不要求絕對最佳答案,那么有時候我們使用簡單的貪婪算法生成近似的答案.

  • 貪心與動態規劃

貪心算法和動態規劃都需求最優子結構,但是貪心算法是自頂向下方式進行,就是每一步,根據策略得到一個當前最優解。傳遞到下一步,從而保證每一步都是選擇當前最優的。最后得到結果.每一步的最優解都依賴上一部的最優解.你只考慮之前已做出的選擇
而動態規劃通常自底向上解各種子問題,每一步,根據策略得到一個更小規模的問題。最后解決最小規模的問題。得到整個問題最優解.全局最優解中一定包含某個局部最優解,但不一定包含前一個局部最優解.你考慮的都是以后的子問題
經典的還是背包問題,之前的01背包問題我們采用動態規劃解決而不能用貪心.但是如果改成部分背包問題呢:
假如有三件物品,背包可裝50磅的物品,物品1重10磅,價值60元;物品2重20磅,價值100元;物品3重30磅,價值120元。你可以選擇帶走每個物品的全部或一部分,求如何選擇可以使背包所裝的價值最大?
注意到不同點是我們可以選擇帶走一部分,所以使用貪心算法十分自然地想到,先算含金量啊,先把含金量最高的都帶完,再帶含金量其次的...很容易得到解,帶走一件1,一件2,2/3件3...比較簡單代碼不寫.

  • 活動安排問題

設有n個活動的集合E = {1,2,…,n},其中每個活動都要求使用同一資源,如演講會場等,而在同一時間內只有一個活動能使用這一資源。每個活i都有一個要求使用該資源的起始時間si和一個結束時間fi,且si < fi 。如果選擇了活動i,則它在半開時間區間[si, fi)內占用資源。若區間[si, fi)與區間[sj, fj)不相交,則稱活動i與活動j是相容的。也就是說,當si >= fj或sj >= fi時,活動i與活動j相容.怎么盡可能地安排多的相容活動呢?
設待安排的11個活動的開始時間和結束時間按結束時間的非減序排列如下:

6

注意要按結束時間的早晚排列,沒排好的話,你可以回去用各種方法自己排.既然貪心么就是越早結束越好,給后面留盡可能多的空間.其次"目光短淺",從排列好的里一個個選,能選一個是一個,別管后面的...
顯然,我們選擇到了(1)1-4,(4)5-7,(8)8-11,(11)12-14
感覺不靠譜么,其實對于這個活動安排問題,貪心算法總能求得的整體最優解,即它最終所確定的相容活動集合A的規模最大。這個結論可以用數學歸納法證明。
我們還是來代碼:

  package com.fredal.structure;
import java.util.Arrays;
public class Arrange {
   public static int[] greedyArrangement(int[] start,int[] end){
       int total=start.length;
       int endtime=end[0];//選擇的所有活動的最末結束時間
       int[] arrangement=new int[total];
       arrangement[0]=1;//無腦選第一個 最早結束的那個
       int count=1;
       for(int i=0;i<total;i++){
           if(start[i]>endtime){//下一個活動開始時間晚于當前活動結束時間
               arrangement[count++]=i+1;//活動選中
               endtime=end[i];//更新結束時間
           }
       }
       return arrangement;
   }    
   public static void main(String[] args) {
       int[] start={1,3,0,5,3,5,6,8,8,2,12};
       int[] end={4,5,6,7,8,9,10,11,12,13,14};
       int[] arrangement=greedyArrangement(start, end);
       for(int i=0;i<arrangement.length;i++){
           if(arrangement[i]!=0)
               System.out.println("開始時間:"+start[arrangement[i]-1]+",結束時間:"+end[arrangement[i]-1]);
       }
   }
}

  • 哈夫曼編碼

哈夫曼編碼是廣泛地用于數據文件壓縮的十分有效的編碼方法。其壓縮率通常在20%~90%之間。哈夫曼編碼算法用字符在文件中出現的頻率表來建立一個用0,1串表示各字符的最優表示方式。一個包含100,000個字符的文件,各字符出現頻率不同,如下表所示

7

我們可以求得對于標準編碼位數需要(45+13+12+16+9+5)*3=300,而對于變長碼45×1+13×3+12×3+16×3+9×4+5×4=224,壓縮了很多...
首先要講一講前綴碼:對每一個字符規定一個0,1串作為其代碼,并要求任一字符的代碼都不是其他字符代碼的前綴。這種編碼稱為前綴碼。
我們可以用二叉樹作為前綴碼的數據結構:樹葉表示給定字符;從樹根到樹葉的路徑當作該字符的前綴碼;代碼中每一位的0或1分別作為指示某節點到左兒子或右兒子的“路標”.字符只放在樹葉上,滿二叉樹是其基本特征,你知道放法太多了,所以關鍵問題變成了怎么尋找總價值最小的完全二叉樹,即最優前綴碼.
對于該例樣本字母表的最優樹如下b,位數正是224,圖a不是完全二叉樹顯然不符合:
8

那么怎么尋找的呢,就是哈夫曼編碼干的事了.構造過程如下:
假設編碼字符集中每一字符c的頻率是f(c)。以f為鍵值的優先隊列Q用在貪心選擇時有效地確定算法當前要合并的2棵具有最小頻率的樹。一旦2棵具有最小頻率的樹合并后,產生一棵新的樹,其頻率為合并的2棵樹的頻率之和,并將新樹按順序插入優先隊列Q。經過n-1次的合并后,優先隊列中只剩下一棵樹,即所要求的樹T
9

代碼實現,這里需要使用完全二叉樹,發現之前寫的沒有特別符號要求的,就這里直接實現吧.還有優先隊列類,用之前實現過的MyHeap,有需要可以查看堆的實現.

  package com.fredal.structure;
public class Huffman {
   static MyHeap<Node> heap=new MyHeap<Node>();//堆類
   static class Node implements Comparable<Node>{
       private int weight;//權值 頻率
       private String value;//字符
       private Node left;
       private Node right;
       private Node parent;
       private String path;//記錄路徑
       private boolean isvisited;//是否遍歷過
       public Node(int weight, String value) {
           super();
           this.weight = weight;
           this.value = value;
       }
       public int compareTo(Node o) {
           return weight-o.weight;
       }        
   }    
   public static Node bulidHuffman(Node[] nodes){
       for(int i=0;i<nodes.length;i++){
           heap.insert(nodes[i]);
       }
       while(heap.getCurrentSize()>1){
           Node minA = heap.deleteMin();//彈出最小的兩個
           Node minB = heap.deleteMin();
           Node sumNode=new Node(minA.weight+minB.weight,minA.value+minB.value);//權值和稱為其父節點
           //維護關系
           sumNode.left=minA;
           minA.path="0";//為了方便 直接把路徑信息記這兒了
           sumNode.right=minB;
           minB.path="1";
           minA.parent=sumNode;
           minB.parent=sumNode;
           
           heap.insert(sumNode);//插入堆
           
       }
       
       return heap.findMin();//返回最后一個  相當于是完整的樹了
   }
   
   public static void printHuffman(Node node){    
       if(node.left!=null && !node.left.isvisited){//遍歷左邊
           Node left = node.left;
           left.isvisited=true;
           printHuffman(left);
       }
       
       if(node.right!=null && !node.right.isvisited){//遍歷右邊
           Node right=node.right;
           right.isvisited=true;
           printHuffman(right);
       }
       
       if(node.left==null && node.right==null){//是葉子節點
           StringBuffer sb=new StringBuffer();
           sb.append(node.path);
           System.out.print(node.value+":");
           while(node.parent!=null){
               node=node.parent;
               if(node.path!=null)
                 sb.append(node.path);//訪問父節點 獲得路徑信息
           }
           System.out.println(sb.reverse().toString());//輸出
           printHuffman(node);//遞歸 輸出下一個葉子節點的編碼
       }
   }
   
   public static void main(String[] args) {
       Node[] nodes={
               new Node(45, "a"),
               new Node(13, "b"),
               new Node(12, "c"),
               new Node(16, "d"),
               new Node(9, "e"),
               new Node(5, "f")
       };
       Node root = bulidHuffman(nodes);
       printHuffman(root);
   }
}

  • 近似裝箱問題

給定N 項物品,大小為 s1, s2, ..., sN,所有的大小都滿足 0 < si < = 1 ;問題是要把這些物品裝到最小數目的箱子中去, 已知每個箱子的容量是1個單位;下圖顯示的是對N項物品的最優裝箱方法

10

這個問題有兩種版本.第一種是聯機裝箱問題,必須將每一件物品放入一個箱子后才處理下一件物品.另外一種是脫機裝箱問題,我們做任何事情都需要等到所有的輸入數據被讀取后才進行.
我們先來考慮聯機裝箱的三種算法,第一種是下項適合算法: 當處理任一物品時,我們檢查看他是否還能裝進剛剛裝進物品的同一個箱子中去.如果能夠裝進去,那么就把它裝入該箱子,否則,就開辟一個新箱子.例子如下:
11

我們用代碼模擬:

  package com.fredal.structure;
import java.util.LinkedList;
public class BinPacking {
   static LinkedList<Box> boxes=new LinkedList<Box>();//存儲所有箱子
   static int index=1;
   
   static class Box{//箱子類
       private double remain;//剩余容量
       private LinkedList<Double> values;
       public Box(){
           remain=1;//設容量初始為1
           values=new LinkedList<Double>();//存儲箱子中的物品
       }
   }
   //下項適合算法
   public static void nextfit(double[] a){
       for(int i=0;i<a.length;i++){
           if(boxes.peek()==null)
               boxes.push(new Box());
           Box last = boxes.peek();
           if(last.remain>=a[i]){//裝的下就裝
               last.values.add(a[i]);
               last.remain-=a[i];
           }else{//裝不下就開辟新箱子
               Box nbox=new Box();
               nbox.values.add(a[i]);
               nbox.remain-=a[i];
               boxes.push(nbox);
           }
       }
       
       show(boxes);
   }
   
   //輸出顯示
   public static void show(LinkedList<Box> boxes){
       while(!boxes.isEmpty()){
           Box box = boxes.removeLast();
           System.out.print("box"+index+++":");
           while(!box.values.isEmpty()){
               Double value = box.values.removeFirst();
               System.out.print(value+" ");
           }
           System.out.println();
       }
   }
   
   public static void main(String[] args) {
       double[] a={0.2,0.5,0.4,0.7,0.1,0.3,0.8};
       nextfit(a);
   }
}

下項算法的性能是線性的,但是在實踐中顯然是不靠譜的.不需要開辟新箱子的時候開辟了新箱子.接下來講首次適合算法:依序掃描這些箱子把新的一項物品放入足夠盛下它的第一個箱子中.

12

代碼如下,注意show()函數有點變化的:

  package com.fredal.structure;
import java.util.Iterator;
import java.util.LinkedList;
public class BinPacking {    
   static LinkedList<Box> boxes=new LinkedList<Box>();//存儲所有箱子
   static int index=1;
   
   static class Box{//箱子類
       private double remain;//剩余容量
       private LinkedList<Double> values;
       public Box(){
           remain=1;//設容量初始為1
           values=new LinkedList<Double>();//存儲箱子中的物品
       }
   }
   //首次適合算法
   public static void firstfit(double[] a){
       for(int i=0;i<a.length;i++){
           boolean flag=false;
           if(boxes.peek()==null)
               boxes.add(new Box());
           Iterator<Box> it = boxes.iterator();
           while(it.hasNext()){//從頭到尾遍歷 能裝就裝
               Box box = it.next();
               if(box.remain>=a[i]){
                   box.values.add(a[i]);
                   box.remain-=a[i];
                   flag=true;
                   break;
               }
           }
           if(!flag){//全部不能裝就開辟新的
               Box nbox=new Box();
               nbox.values.add(a[i]);
               nbox.remain-=a[i];
               boxes.add(nbox);
           }
       }
       
       show(boxes);
   }
   
   //輸出顯示
   public static void show(LinkedList<Box> boxes){
       while(!boxes.isEmpty()){
           Box box = boxes.removeFirst();
           System.out.print("box"+index+++":");
           while(!box.values.isEmpty()){
               Double value = box.values.removeFirst();
               System.out.print(value+" ");
           }
           System.out.println();
       }
   }
   
   public static void main(String[] args) {
       double[] a={0.2,0.5,0.4,0.7,0.1,0.3,0.8};
       firstfit(a);
   }
}

第三個是最佳適合算法,該方法不是吧一項新物品放入所發現的第一個能夠容納它的箱子,而是放到所有箱子中能夠容納它的最滿的箱子中.該算法對隨記的輸入表現的更好

13

代碼如下,注意輸出函數還是變了,并且采用了ArrayList存儲:

  package com.fredal.structure;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
public class BinPacking {
   static ArrayList<Box> boxes=new ArrayList<Box>();//存儲所有箱子
   static int index=1;    
   static class Box{//箱子類
       private double remain;//剩余容量
       private LinkedList<Double> values;
       public Box(){
           remain=1;//設容量初始為1
           values=new LinkedList<Double>();//存儲箱子中的物品
       }
   }
   //最佳適合算法
   public static void bestfit(double[] a){
       for(int i=0;i<a.length;i++){
           double remainMin=1+1;//最小的剩余容量 用于尋找最滿的箱子 初始化表示比容量大1
           int index=0;//記錄箱子編號
           if(boxes.size()==0)
               boxes.add(new Box());
           for(int j=0;j<boxes.size();j++){
               Box box=boxes.get(j);
               if(box.remain>=a[i] && box.remain<remainMin){//從頭遍歷 如果找到更滿的并且能裝下的就記錄
                   remainMin=box.remain;
                   index=j;
               }
           }
           if(remainMin<=1){//說明找到了可以裝的
               Box box = boxes.get(index);//裝進記錄好的最滿的箱子
               box.values.add(a[i]);
               box.remain-=a[i];
           }else{//找不到可以裝的就開辟新的箱子
               Box nbox=new Box();
               nbox.values.add(a[i]);
               nbox.remain-=a[i];
               boxes.add(nbox);
           }
       }
       
       show(boxes);
   }
   
   //輸出顯示
   public static void show(ArrayList<Box> boxes){
       for(int i=0;i<boxes.size();i++){
           Box box=boxes.get(i);
           System.out.print("box"+index+++":");
           while(!box.values.isEmpty()){
               Double value = box.values.removeFirst();
               System.out.print(value+" ");
           }
           System.out.println();
       }
   }
   
   public static void main(String[] args) {
       double[] a={0.2,0.5,0.4,0.7,0.1,0.3,0.8};
       bestfit(a);
   }
}

接下來是脫機算法,顯然脫機算法可以表現得更好.聯機算法的問題在于在于將大項物品裝箱困難,特別是當他們在輸入的晚期出現的時候.于是脫機算法我們可以將各項物品排序,把最大的物品放在最先,此時我們可以應用首次適合算法或最佳適合算法,分別得到“首次適合遞減算法” 和 ”最佳適合遞減算法”.
這兩種算法是差不多的,我們以首次適合遞減算法為例.

14

代碼如下,我們使用快速排序.注意java中double數有些精度問題,上面的三個算法也會出現可能無法完全裝滿的問題,我就不去改了.這里改一下

  package com.fredal.structure;
import java.util.Iterator;
import java.util.LinkedList;
public class BinPacking {    
static LinkedList<Box> boxes=new LinkedList<Box>();//存儲所有箱子
static int index=1;
static class Box{//箱子類
    private double remain;//剩余容量
    private LinkedList<Double> values;
    public Box(){
        remain=1;//設容量初始為1
        values=new LinkedList<Double>();//存儲箱子中的物品
    }
}
//首次適合遞減算法
public static void firstfit(double[] a){
    quickSort(a, 0, a.length-1);
    for(int i=0;i<a.length;i++){
        boolean flag=false;
        if(boxes.peek()==null)
            boxes.add(new Box());
        Iterator<Box> it = boxes.iterator();
        while(it.hasNext()){//從頭到尾遍歷 能裝就裝
            Box box = it.next();
            if(box.remain>a[i]||Math.abs(box.remain-a[i])<Math.pow(10, -10)){//解決一下精度
                box.values.add(a[i]);
                box.remain-=a[i];
                flag=true;
                break;
            }
        }
        if(!flag){//全部不能裝就開辟新的
            Box nbox=new Box();
            nbox.values.add(a[i]);
            nbox.remain-=a[i];
            boxes.add(nbox);
        }
    }
    show(boxes);
}
//快速排序
public static void quickSort(double[] a,int left,int right){
   if(left<right){//遞歸出口條件
       int i=left;//左指針
       int j=right;//右指針
       double x=a[left];//選擇第一個元素作為標尺
       while(i<j){
           while(i<j && a[j]<=x) j--;//從右向左找第一個大于x的數
           if(i<j) a[i++]=a[j];
           while(i<j && a[i]>x) i++;//從左向右找第一個小于等于x的數
           if(i<j) a[j--]=a[i];
       }
       a[i]=x;//插入標尺
       quickSort(a,left,i-1);//遞歸左邊
       quickSort(a, i+1, right);//遞歸右邊
   }
}

//輸出顯示
public static void show(LinkedList<Box> boxes){
    while(!boxes.isEmpty()){
        Box box = boxes.removeFirst();
        System.out.print("box"+index+++":");
        while(!box.values.isEmpty()){
            Double value = box.values.removeFirst();
            System.out.print(value+" ");
        }
        System.out.println();
    }
}
public static void main(String[] args) {
    double[] a={0.2,0.5,0.4,0.7,0.1,0.3,0.8};
    firstfit(a);
}
}

分治算法

分治法是一種很重要的算法。字面上的解釋是“分而治之”,就是把一個復雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最后子問題可以簡單的直接求解,原問題的解即子問題的解的合并。這個技巧是很多高效算法的基礎.
分治策略是:對于一個規模為n的問題,若該問題可以容易地解決(比如說規模n較小)則直接解決,否則將其分解為k個規模較小的子問題,這些子問題互相獨立且與原問題形式相同,遞歸地解這些子問題,然后將各子問題的解合并得到原問題的解。這種算法設計策略叫做分治法.
傳統上,含有兩個或以上的遞歸調用的叫做分治法.經典的例子就是快速排序,歸并排序
接下來我們選一些其他的經典例子來分析

  • 大整數乘法

設有兩個大整數相乘,X=61438521,Y=94736407.那么XY=5820464730934047.易知我么的算法需要O(N2)即O(82)次操作.
如果我們把X和Y都拆成兩半,由最高幾位和最低幾位組成.那么XL=6143,XR=8521,YL=9473,YR=6470.于是X=XL*10^4+XR,Y=YL*10^4+YR.可以得到
XY=XL*YL*10^8+(XL*YR+XR*YL)*10^4+XRYR
顯然這個式子就是由4個乘法組成的,每一個都是原問題的一半,而108,104的乘法只是添一些0,于是可以得到遞歸:T(N)=4T(N/2)+O(N)..
我們按照主定理(相關資料查閱維基百科),可以求得算法復雜度仍然是O(N2).并沒有改進這個問題.
觀察XL*YR+XR*YL,可以分解為(XL-XR)(YR-YL)+XL*YL+XR*YR,我們僅需要算前面一項,后面的兩項已經計算過了.于是得到了T(N)=3T(N/2)+O(N).,按照主定理,可得T(N)=O(N^1.59).當然對于每一個乘積我們還可以繼續遞歸下去,一般到四位數就不用遞歸了.
我們還是采取代碼來模擬過程:

  package com.fredal.structure;
public class BigMultiply {
   //大整數相乘
   public static String multiply(String x,String y){
       int flag1=0;//x的符號位
       int flag2=0;//y的符號位
       if(x.charAt(0)=='-'){//處理符號
           x=x.substring(1);//先把符號位截掉
           flag1=1;
       }
       if(y.charAt(0)=='-'){
           y=y.substring(1);
           flag2=1;
       }
       
       String flag=(flag1^flag2)==1?"-":"";//相乘即異或之后符號位
       
       if(x.length()<y.length())//保證x的位數更大
           return flag+multiply(y, x);
   
       if(x.length()<=4)
           return flag+Integer.parseInt(x)*Integer.parseInt(y);//少于等于四位數直接計算了
       
       if(x.length()%2==0){//x位數是偶數 就把y補成和x一樣長
           while(x.length()>y.length())
               y="0"+y;
       }else{//x位數不是偶數 就先把x補成偶數 再把y補成和x一樣長
           x="0"+x;
           while(x.length()>y.length())
               y="0"+y;
       }
       
       String xl=x.substring(0,x.length()/2);
       String xr=x.substring(x.length()/2);
       String yl=y.substring(0,y.length()/2);
       String yr=y.substring(y.length()/2);
       
       String D1=minus(xl, xr);//xl-xr
       String D2=minus(yr, yl);//yr-yl
       
       String xlyl=multiply(xl, yl);//xl*yl
       String xryr=multiply(xr, yr);//xr*yr
       
       String D3=add(multiply(D1, D2)+"", add(xlyl, xryr));//D1*D2+Xl*Yl+Xr*Yr
       return flag+add(shift(xlyl, x.length()),add(shift(D3, x.length()/2),xryr));//Xl*Yl*10^n+D3*10^(n/2)+Xr*Yr
   }
   
   //大數相減 帶符號處理
   public static String minus(String x,String y){
       int large=compare(x, y);
       String flag=large>=0?"":"-";//加上符號
       if (large==0)
           return "0";
       else if(large>0)//轉化成大的減小的
           return minusBigNum(x,y);
       else 
           return flag+minusBigNum(y,x);
   }
   
   //大數相減
   private static String minusBigNum(String x,String y){//大數減小數
       int len=x.length();
       while(len>y.length())
           y="0"+y;
       StringBuilder result=new StringBuilder();
       int flag=0;//表示是否進位
       for(int i=len-1;i>=0;i--){
           int xs=Integer.parseInt(String.valueOf(x.charAt(i)));
           int ys=Integer.parseInt(String.valueOf(y.charAt(i)));
           if(xs+flag>=ys){//別忘了把flag加上
               result.append(xs-ys+flag);
               flag=0;
           }else{
               result.append(10+xs-ys+flag);
               flag=-1;
           }
       }
       return clearZero(result.reverse().toString());
   }
   
   //大數相加
   public static String add(String x,String y){
       if(x.charAt(0)=='-'){//先處理符號
           x=x.substring(1);
           if(y.charAt(0)=='-'){
               y=y.substring(1);
               return "-"+add(x,y);
           }else
               return minus(y, x);             
       }
       
       if(y.charAt(0)=='-'){
           y=y.substring(1);
           return minus(x, y);
       }
       if(x.length()<y.length())
           return add(y, x);//保證x的位數更大
       
       int len=x.length();
       
       while(len>y.length())//補位使位數相等
           y="0"+y;
       
       StringBuilder result=new StringBuilder();
       int flag=0;//表示是否進位
       for(int i=len-1;i>=0;i--){
           int xs=Integer.parseInt(String.valueOf(x.charAt(i)));
           int ys=Integer.parseInt(String.valueOf(y.charAt(i)));
           if(xs+ys+flag>9){//別忘了把flag加上
               result.append(xs+ys-10+flag);
               flag=1;
           }else{
               result.append(xs+ys+flag);
               flag=0;
           }
       }
       if(flag!=0)
           result.append(1);
       return clearZero(result.reverse().toString());
   }
   
   //計算10n次方的 就是后面加0
   public static String shift(String x,int n){
       for(int i=0;i<n;i++){
           x+="0";
       }
       return x;
   }
   
   //消除0
   private static String clearZero(String str){
       int i=0;
       while(i<str.length()&&str.charAt(i)=='0'){
           i++;
       }
       return str.substring(i);
   }
   
   //比較兩個數的大小
   private static int compare(String x,String y){
       if(x.length()>y.length())
           return 1;
       else if(x.length()<y.length())
           return -1;
       else{
           int index = 0;
           while (index < x.length() && x.charAt(index) == y.charAt(index))
               index++;
           if (index == x.length())
               return 0;
           else {
               return x.charAt(index) > y.charAt(index)? 1 : -1;
           }
       }
   }
   
   public static void main(String[] args) {
       System.out.println(multiply("-61438521", "94736407"));
       System.out.println(multiply("-3124234254543411432432422238221342421",
               "-2423442342342342342342342323423445345699"));
               
   }
}

算法并不復雜,但是處理符號什么的還是比較麻煩的,而且加法減法啥的都要自己去實現.

  • Strassen矩陣乘法

兩個矩陣的乘法學過線性代數的都知道怎么求,一般來說復雜度為O(N^3).直接給出標準的算法

  package com.fredal.structure;
public class MartixMultiply {
   public static int[][] multiply(int[][] a, int[][] b) {
       int n = a.length;
       int[][] c = new int[n][n];

       for (int i = 0; i < n; i++)
           // 初始化
           for (int j = 0; j < n; j++)
               c[i][j] = 0;

       for (int i = 0; i < n; i++)
           for (int j = 0; j < n; j++)
               for (int k = 0; k < n; k++)
                   c[i][j] += a[i][k] * b[k][j];

       return c;
   }

   public static void main(String[] args) {
       int[][] a = { { 1, 2 }, { 3, 4 } };
       int[][] b = { { 3, 4 }, { 7, 2 } };
       int[][] c = multiply(a, b);

       System.out.println(c[0][0] + " " + c[0][1] + " " + c[1][0] + " "
               + c[1][1]);
   }
}

Strassen提出了算法打破了O(N^3)的屏障.用到分治算法,把矩陣分為4塊.

15
16

其中:
17

可以得到遞推關系T(N)=7T(N/2)+O(N2),依據主定理得到解T(N)=O(N^2.81).
這兒不做出證明,顯然這用到了分治法的思想,我們用代碼模擬

  package com.fredal.structure;
public class MartixMultiply {
   public static int[][] StrassenMultiply(int[][] a, int[][] b) {
       int[][] result = new int[a.length][b.length];
       if (a.length == 2)
           return multiply(a, b);// 如果是2階的 就結束遞歸 用傳統方法
       // a的四個子矩陣
       int[][] A00 = divide(a, 1);
       int[][] A01 = divide(a, 2);
       int[][] A10 = divide(a, 3);
       int[][] A11 = divide(a, 4);
       // b的四個子矩陣
       int[][] B00 = divide(b, 1);
       int[][] B01 = divide(b, 2);
       int[][] B10 = divide(b, 3);
       int[][] B11 = divide(b, 4);

       int[][] m1 = StrassenMultiply(addArrays(A00, A11), addArrays(B00, B11));
       int[][] m2 = StrassenMultiply(addArrays(A10, A11), B00);
       int[][] m3 = StrassenMultiply(A00, subArrays(B01, B11));
       int[][] m4 = StrassenMultiply(A11, subArrays(B10, B00));
       int[][] m5 = StrassenMultiply(addArrays(A00, A01), B11);
       int[][] m6 = StrassenMultiply(subArrays(A10, A00), addArrays(B00, B01));
       int[][] m7 = StrassenMultiply(subArrays(A01, A11), addArrays(B10, B11));

       int[][] C00 = addArrays(m7, subArrays(addArrays(m1, m4), m5));// m1+m4-m5+m7
       int[][] C01 = addArrays(m3, m5); // m3+m5
       int[][] C10 = addArrays(m2, m4); // m2+m4
       int[][] C11 = addArrays(m6, subArrays(addArrays(m1, m3), m2));// m1+m3-m2+m6

       // 將四個矩陣合并起來
       Merge(result, C00, 1);
       Merge(result, C01, 2);
       Merge(result, C10, 3);
       Merge(result, C11, 4);

       return result;
   }

   // /分割得到子矩陣
   private static int[][] divide(int[][] a, int flag) {
       int[][] result = new int[a.length / 2][a.length / 2];
       switch (flag) {
       case 1:
           for (int i = 0; i < a.length / 2; i++)
               for (int j = 0; j < a.length / 2; j++)
                   result[i][j] = a[i][j];
           break;
       case 2:
           for (int i = 0; i < a.length / 2; i++)
               for (int j = a.length / 2; j < a.length; j++)
                   result[i][j - a.length / 2] = a[i][j];
           break;
       case 3:
           for (int i = a.length / 2; i < a.length; i++)
               for (int j = 0; j < a.length / 2; j++)
                   result[i - a.length / 2][j] = a[i][j];
           break;
       case 4:
           for (int i = a.length / 2; i < a.length; i++)
               for (int j = a.length / 2; j < a.length; j++)
                   result[i - a.length / 2][j - a.length / 2] = a[i][j];
           break;
       }
       return result;
   }

   // 矩陣加法
   private static int[][] addArrays(int[][] a, int[][] b) {
       int[][] result = new int[a.length][a.length];
       for (int i = 0; i < result.length; i++) {
           for (int j = 0; j < result.length; j++) {
               result[i][j] = a[i][j] + b[i][j];
           }
       }
       return result;
   }

   // 矩陣減法
   private static int[][] subArrays(int[][] a, int[][] b) {
       int[][] result = new int[a.length][a.length];
       for (int i = 0; i < result.length; i++) {
           for (int j = 0; j < result.length; j++) {
               result[i][j] = a[i][j] - b[i][j];
           }
       }
       return result;
   }

   // 將b復制到a的指定位置
   private static void Merge(int[][] a, int[][] b, int flag) {
       switch (flag) {
       case 1:
           for (int i = 0; i < a.length / 2; i++)
               for (int j = 0; j < a.length / 2; j++)
                   a[i][j] = b[i][j];
           break;
       case 2:
           for (int i = 0; i < a.length / 2; i++)
               for (int j = a.length / 2; j < a.length; j++)
                   a[i][j] = b[i][j - a.length / 2];
           break;
       case 3:
           for (int i = a.length / 2; i < a.length; i++)
               for (int j = 0; j < a.length / 2; j++)
                   a[i][j] = b[i - a.length / 2][j];
           break;
       case 4:
           for (int i = a.length / 2; i < a.length; i++)
               for (int j = a.length / 2; j < a.length; j++)
                   a[i][j] = b[i - a.length / 2][j - a.length / 2];
           break;
       }
   }

   // 常規做法
   public static int[][] multiply(int[][] a, int[][] b) {
       int n = a.length;
       int[][] c = new int[n][n];

       for (int i = 0; i < n; i++)
           // Initialization
           for (int j = 0; j < n; j++)
               c[i][j] = 0;

       for (int i = 0; i < n; i++)
           for (int j = 0; j < n; j++)
               for (int k = 0; k < n; k++)
                   c[i][j] += a[i][k] * b[k][j];

       return c;
   }

   public static void main(String[] args) {
       int[][] a = { { 1, 2, 6, 7 }, { 3, 4, 5, 4 }, { 5, 8, 3, 8 },
               { -6, 4, 3, 9 } };
       int[][] b = { { 3, 4, 9, 0 }, { 7, 2, -5, -6 }, { 0, 7, -4, 6 },
               { -6, 3, -5, 4 } };
       int[][] c = multiply(a, b);

       System.out.println(c[0][0] + " " + c[0][1] + " " + c[1][0] + " "
               + c[1][1]);
   }
}

  • 最近點對問題

這個問題真的很有意思,給定空間上的n個節點S={(xi,yi)},如何查找這n個點對中最近的點對的距離?
我們都知道兩點間距離:((xi-xj)2+(yi-yj)2)1/2
那么如果使用暴力搜索,需要兩兩檢測,需要花費O(N2).
我們可以采用分治法的思想.首先我們假設這些點都已經按照x坐標排序過,那么可以在中間畫一條線,把點集分為Pl和Pr,那么最近的一對點要么都在Pl中,要么都在Pr中,要么分別在Pl和pr中,把這三個距離分別用dl,dr,dc表示.

18

顯然我們可以遞歸第計算dl和dr,關鍵是計算dc.令δ=min(dl,dr),顯然我們可以做出雙道帶,來縮小考慮的范圍
19

對于在帶中的,我們可以通過兩層循環蠻力計算,但畢竟還是不好,最壞的情況是說有點都可能在這帶狀區域里.那么考慮對y坐標排序.如果pi和pj的y坐標差大于δ,那么可以直接break跳出內循環.如對于p3來說,我們只需要考慮p4和p5,之后的break就行了.
20

還有一點可優化的是,其實對于任意的點pi,最多有7個點需要被考慮.
21

如圖所示,這是點最多的情況.因為如果在上圖的情況中隨便再添一個點,比如在左邊,那么這個點距離左邊其他四個點的距離肯定有小于δ的(最均勻矩形中心,距離小于δ).那么只要距離一個點小于δ.那么與之前的假設在Pl中最短的距離為δ矛盾了!所以我們只需考慮最壞情況,某個角上是pi,那么其余還有7個點要考慮,得證.
接下來就容易了,但是還有要注意的是,不能每次遞歸都去排序x,y坐標.可以保留兩個表,一個x坐標排序的表,一個y坐標排序的表.這個算法是O(NlogN)的.
還是來實現吧:

  package com.fredal.structure;
import java.util.ArrayList;
import java.util.Set;
import java.util.TreeSet;
public class Distance {
   static final int NUM=10;//使用窮舉法的點數   
   public static void main(String[] args) {
       Set<Point> testData = new TreeSet<Point>();         
       java.util.Random random = new java.util.Random();  
       for(int i = 0;i < 100000;i++){  
           int x = random.nextInt(100000);  
           int y = random.nextInt(100000);  
           testData.add(new Point(x, y));  
       } 
       Point [] points = new Point[testData.size()];  
       points = (Point[]) testData.toArray(points);
       Point[] result=new Point[2];
       long startTime=System.currentTimeMillis();   //獲取開始時間
       result=findpair(points);
       long endTime=System.currentTimeMillis(); //獲取結束時間
       System.out.println("最近點: ("+result[0].getX()+","+result[0].getY()
               +")--("+result[1].getX()+","+result[1].getY()+")");
       System.out.println("距離為: "+distance(result[0], result[1]));
       System.out.println("分治法運行時間:"+(endTime-startTime)+"ms");
       long startTime2=System.currentTimeMillis();   //獲取開始時間
       result=bong(points);
       long endTime2=System.currentTimeMillis(); //獲取結束時間
       System.out.println("窮舉法運行時間:"+(endTime2-startTime2)+"ms");
   }    
   //尋找最近點對
   public static Point[] findpair(Point[] p){
       Point[] result=new Point[2];
       if(p.length<NUM)
           return bong(p);
       //開始畫線了  求所有點在x坐標的中位數
       int minX = (int) Double.POSITIVE_INFINITY; 
       int maxX = (int) Double.NEGATIVE_INFINITY;
       for(int i = 0; i < p.length; i++){  
           if(p[i].getX() < minX)  
               minX = (int) p[i].getX();  
           if(p[i].getX() > maxX)  
               maxX = (int) p[i].getX();  
       }  
       int midX = (minX + maxX)/2;
       //把以midx為界劃分出的點分成兩組放到兩個表
       ArrayList<Point> L1=new ArrayList<Point>();
       ArrayList<Point> L2=new ArrayList<Point>();
       for(int i = 0; i < p.length; i++){  
           if(p[i].getX() <= midX)       
               L1.add(p[i]);  
           if(p[i].getX() > midX)  
               L2.add(p[i]);  
       } 
       //按x坐標排序
       Point [] p1 = new Point[L1.size()];  
       Point [] p2 = new Point[L2.size()];           
       L1.toArray(p1);  
       L2.toArray(p2);  
       mergeSort(p1, "x");     //按X坐標升序排列  
       mergeSort(p2, "x");     //按X坐標升序排列 
       //遞歸求p1,p2中最近的兩個點
       Point[] result1 = new Point[2];  
       result1 = findpair(p1);
       Point[] result2 = new Point[2];  
       result2 = findpair(p2);
       //求二者中的最小值
       if (distance(result1[0], result1[1])<distance(result2[0], result2[1])) {
           result=result1;
       }else {
           result=result2;
       }
       double distance=Math.min(distance(result1[0], result1[1]),distance(result2[0], result2[1]));
       //開始劃分帶了  在兩個子集中找哪些距離劃分線小于d的保存
       ArrayList<Point> L3 = new ArrayList<Point>();   
       for(int i = 0; i < p1.length; i++){  
           if(midX - p1[i].getX() < distance)  
               L3.add(p1[i]);  
       }  
       for(int i = 0; i < p2.length; i++){  
           if(p2[i].getX() - midX < distance){  
               L3.add(p2[i]);  
           }  
       } 
       //將得到的按照y升序排列
       Point [] p3 = new Point [L3.size()];  
       L3.toArray(p3);            
       mergeSort(p3, "y");  
       //然后開始優化的窮舉 即比較之后的7個點
       if (p3.length<NUM) {
           Point[] temp= bong(p3);
           if (distance(temp[0], temp[1])<distance&&distance(temp[0], temp[1])!=0) {
               result=temp;
           }
       }else {
           for(int i=0;i<p3.length-7;i++){
               double tempd;
               for(int j=1;j<8;j++){
                   if (i+j>=p3.length) {
                       break;
                   }else {
                       tempd=distance(p3[i], p3[i+j]);
                       if (tempd<distance&tempd!=0) {
                           result[0]=p3[i];
                           result[1]=p3[i+j];
                       }
                   }                 
               }
           }
       }
       return result;
   }    
   //歸并排序
   private static void mergeSort(Point[] p, String flag) {
       Point[] result = new Point[p.length];  
       mergeSort(p, result, 0, p.length - 1, flag); 
   }
   private static void mergeSort(Point[] a, Point [] result, int left, int right, String flag){  
       if(left < right){  
           int center = (left + right) >> 1;  
           //分治  
           mergeSort(a, result, left, center, flag);  
           mergeSort(a, result, center + 1, right, flag);  
           //合并  
           merge(a, result, left, center + 1, right, flag);  
       }  
     } 
   private static void merge(Point [] a, Point [] result, int leftPos, int rightPos, int rightEnd, String flag){  
       int leftEnd = rightPos - 1;  
       int numOfElements = rightEnd - leftPos + 1;  
         
       int tmpPos = leftPos;       //游標變量, 另兩個游標變量分別是leftPos 和 rightPos            
       while(leftPos <= leftEnd && rightPos <= rightEnd){  
           if(flag.equals("x")){  
               if(a[leftPos].getX() <= a[rightPos].getX())  
                   result[tmpPos++] = a[leftPos++];  
               else  
                   result[tmpPos++] = a[rightPos++];  
           }else if(flag.equals("y")){  
               if(a[leftPos].getY() <= a[rightPos].getY())  
                   result[tmpPos++] = a[leftPos++];  
               else  
                   result[tmpPos++] = a[rightPos++];  
           }else  
               throw new RuntimeException();  
       }       
       while(leftPos <= leftEnd)  
           result[tmpPos++] = a[leftPos++];  
       while(rightPos <= rightEnd)  
           result[tmpPos++] = a[rightPos++];         
       //將排好序的段落拷貝到原數組中  
       System.arraycopy(result, rightEnd-numOfElements+1, a, rightEnd-numOfElements+1, numOfElements);  
   }      
   //窮舉法
   private static Point[] bong(Point[] p){
       Point[] result=new Point[2];
       if (p.length<=1) {
           result[0]=new Point(Double.MIN_VALUE, Double.MIN_VALUE);
           result[1]=new Point(Double.MAX_VALUE, Double.MAX_VALUE);
           return result;
       }else{
           double min=distance(p[0], p[1]);
           int start=0;
           int end=1;
           for (int i = 0; i < p.length; i++) {
               for (int j = i+1; j < p.length; j++) {
                   if (distance(p[i], p[j])<min&&distance(p[i], p[j])!=0) {
                       min=distance(p[i], p[j]);
                       start=i;
                       end=j;
                   }
               }
           }
           result[0]=p[start];
           result[1]=p[end];
           return result;
       }
   }    
   //計算距離
   private static double distance(Point p1,Point p2){
       return Math.sqrt((p1.getX()-p2.getX())*(p1.getX()-p2.getX())+(p1.getY()-p2.getY())*(p1.getY()-p2.getY()));
   }
   //用一個類來表示點
   static class Point implements  Cloneable,Comparable<Point>{
       double x,y;
       public Point(double x, double y) {
           super();
           this.x = x;
           this.y = y;
       }
       public double getX() {
           return x;
       }
       public double getY() {
           return y;
       }
       public int compareTo(Point o) {  
           if(x == o.getX() && y == o.getY())  
               return 0;  
           else   
               return 1;  
       }
       public boolean equals(Object p) {
           // TODO 自動生成的方法存根
           if (this.x==((Point) p).getX()&&this.y==((Point) p).getY()) {
               return true;
           }else {
               return false;
           }
       }
   }    
}

我們采用了隨機數產生器隨機產生點,并統計了分治法與純粹暴力搜索的運行時間,差距相當明顯(這個程序當規模特別大的時候還是會棧溢出,為啥)

22

泊松分酒問題

講這道題純粹就是比較好玩,就記錄一下.泊松分酒是很著名的一道題,講的是假設某人有12品脫的啤酒一瓶,想從中倒出六品脫,但是恰巧身邊沒有6品脫的容器,僅有一個8品脫和一個5品脫的容器,怎樣倒才能將啤酒分為兩個6品脫呢?
我們用代碼模擬很簡單的就得到了答案

  package com.fredal.structure; import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
public class Oil {
    static class Status{
         static int[] full={12,8,5};//滿的狀態
         int[] bottle=new int[3];//瓶子的狀態
         Status from;//從哪個狀態來的
         
         public Status(int a,int b,int c){
             bottle[0]=a;
             bottle[1]=b;
             bottle[2]=c;
         }
         
         //獲取某種狀態開始下一步的所有的狀態
         public Set opreation(){
             Set res=new HashSet();
             
             //開始倒酒
             for(int i=0;i<bottle.length;i++){
                 for(int j=0;j<bottle.length;j++){
                     if(i==j) continue; //不倒自己
                     if(bottle[i]==0) continue;//自己是空的 不倒
                     if(bottle[j]==full[j]) continue;//對方是滿的 不倒
                     
                     Status t=new Status(bottle[0], bottle[1], bottle[2]);
                     t.from=this;//從自己這個狀態開始變化
                     
                     //真的開始倒酒了                     t.bottle[j]+=t.bottle[i];
                     t.bottle[i]=0;
                     if(t.bottle[j]>full[j]){//裝不下了
                         t.bottle[i]=t.bottle[j]-full[j];//滿的倒回去
                         t.bottle[j]=full[j];
                     }              
                     res.add(t);
                 }
             }
             return res;
         }
         
         //是否含有某種狀態
         public boolean has2(int x){
            int index=0;
            if (bottle[0]==x) index++;
            if (bottle[1]==x) index++;
            if (bottle[2]==x) index++;
            return index==2?true:false;
         }
         
         public Status getFrom() {
            return from;
        }
         
         public String toString(){
                return "<" + bottle[0] + "," + bottle[1] + "," + bottle[2] + ">";
        }
         
        public int hashCode() {
            return 100;
        }
        
        public boolean equals(Object obj) {
            Status x=(Status)obj;
            return bottle[0]==x.bottle[0]&&bottle[1]==x.bottle[1]&&bottle[2]==x.bottle[2];
        }
    }
    
    public static void main(String[] args) {
        Set<Status> all=new HashSet<Status>();//存放所有結果狀態
        all.add(new Status(12, 0, 0));
        
        for(;;){
            Set newset=new HashSet();
            
            for(Status x:all){//所有上一種狀態產生所有下一種狀態
                Set t = x.opreation();
                newset.addAll(t);
            }
            
            if(all.containsAll(newset)) break;//出口
            all.addAll(newset);
        }
        
        LinkedList<Status> list=new LinkedList<Status>();//存放有6的一溜
        
        for(Status k:all){
            if(k.has2(6)){
                while(k!=null){    
                    list.push(k);
                    k=k.getFrom();//從終止狀態開始往上追溯
                }
            }
        }   
        //輸出
        while(!list.isEmpty()){
            System.out.println(list.pop());
        }
    }    
}

這個解法找到的其實是最優解,至于為什么呢,其實利用set的方法十分巧妙,結果集set里隨著一次次的分酒一次次地擴增,當第一次出現含有兩個6的狀態的時候,再往前追溯,步驟是最少的!因為這個我們想要的狀態是第一次出現.
假如我們每次都打印出all集合,可以知道,當第一次找到含有兩個6狀態的時候程序并沒有結束,因為還沒有找到所有的狀態.
而后面的狀態再進行分酒時,仍有可能產生兩個6的狀態,但是想要加入set集合的時候就行不通了,所以此程序只輸出最早加入的那一個解,并且是最優的.
當然這種算法并不能輸出所有的解,如果要得到所有的解,我們可以采用以下算法,這種算法借鑒了圖的深度搜索(DFS)以及回溯的技巧,需要注意的是,和8皇后問題一樣,需要回溯的時機有兩個,出錯的時候和找到某一組解的時候.

  package com.fredal.structure;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Oil {
    int[] full = new int[3]; //滿狀態 容量
    int[] bottle = new int[3]; //瓶子的狀態
    int target = 0; //目標
    List<int[]> res = new ArrayList<int[]>();//存放結果
    
    public void opreation(int[] bottle) {
        for(int i=0;i<3;i++) {
            for(int j=1;j<3;j++){//每個瓶子都不往自己倒 總共6種可能性
                int[] temp = bottle.clone();//每次循環都創建臨時數組 
                int to=(i+j)%3;//(i+j)%3 是除每種i瓶子外其他兩個瓶子的序號,即要倒的目標
                if(temp[i]==0) continue;//自己是空的 不倒
                if(temp[to]==full[to]) continue;//對方是滿的 不倒
                
                //開始倒酒
                temp[to]+=temp[i];
                temp[i]=0;
                if(temp[to]>full[to]){//裝不下了
                    temp[i]=temp[to]-full[to];//滿出來的部分倒回去
                    temp[to]=full[to];
                }
                
                if(had(temp)) continue;//檢測是否已經存在相同狀態,防止重復

                res.add(temp);//添加到結果鏈表
                if(has2(temp))    return;//如果找到有兩個想要的狀態的結果就返回
                opreation(temp);//繼續下一次分酒
                res.remove(res.size()-1); //回溯 仔細體會
            }
        }
    }

    //是否以及含有狀態
    private boolean had(int[] bottlex) {
        for(int[] e:res)
            if(e[0]==bottlex[0]&&e[1]==bottlex[1]&&e[2]==bottlex[2]) return true;
        return false;
    }

    //檢測找到結果
    private boolean has2(int[] bottle) {
        int index=0;
        for(int i=0;i<bottle.length;i++)        
            if(bottle[i]==target) index++;        
        if(index==2){
            show(res);//輸出
            res.remove(res.size()-1);//回溯
            return true;
        }
        return false;
    }
    //打印
    private void show(List<int[]> res) {
        for(int[] e:res) {
            System.out.println(e[0] + "," + e[1] + "," + e[2]);
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Oil o = new Oil();
        Scanner scanner = new Scanner(System.in);
        String s =""; 
        if(scanner.hasNext()) {
            s = scanner.nextLine();
        }
        String[] data = s.split(",");
        int[] d = new int[data.length];
        for(int i=0;i<data.length;i++){
            d[i] = Integer.parseInt(data[i]);
        }
        o.full = new int[]{d[0],d[1],d[2]};
        o.bottle = new int[]{d[3],d[4],d[5]};
        o.target = d[6];
        o.res.add(new int[]{d[3],d[4],d[5]});//添加初始狀態
        o.opreation(o.bottle);
    }
    
}

顯然,按照深度搜索并不能有效地找到最優解.上面兩種算法都是比較巧的,我也比較喜歡.
如果要同時找到所有解和最優解,用圖的廣度搜索(BFS)會很方便,這也是網上采用的最多的,代碼到處都有,就不寫了.
更多內容與相關下載請查看擴展閱讀

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 回溯算法 回溯法:也稱為試探法,它并不考慮問題規模的大小,而是從問題的最明顯的最小規模開始逐步求解出可能的答案,并...
    fredal閱讀 13,728評論 0 89
  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,769評論 0 33
  • 【程序1】 題目:古典問題:有一對兔子,從出生后第3個月起每個月都生一對兔子,小兔子長到第三個月后每個月又生一對兔...
    葉總韓閱讀 5,165評論 0 41
  • Java經典問題算法大全 /*【程序1】 題目:古典問題:有一對兔子,從出生后第3個月起每個月都生一對兔子,小兔子...
    趙宇_阿特奇閱讀 1,908評論 0 2
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,765評論 18 399