OO_JAVA_表達式求導

OO_JAVA_表達式求導_第一彈

---------------------------------------------------表達式提取部分

詞法分析

​ 首先,每個表達式內部都存在不可分割的字符組,好比一個不止一位的數字,或是一個sin三角函數,這樣不能分離的字符組我稱之爲詞法單元,依照其定義,能夠將第三次做業的表達式分割成以下詞法單元:java

  • SPACE:即空格和TAB字符的組合
  • 純數字:即純粹由0-9字符集組成
  • 運算符:-、+、*、^,這些都是運算符
  • 三角函數:sin或cos內部不可存在空格
  • 左括號:(
  • 右括號:)

​ 將一串字符解析成詞法單元列表的形式,就已是一層處理了,這一步就能夠排查出一些錯誤,直接拋出異常,而後咱們獲得了一個由詞法單元組成的列表,如今就是採用遞歸遞降的方法線性解析這個列表並生成表達樹了。設計模式

語法分析

仔細閱讀指導書,能夠概括出以下定義:api

  • Num = [-|+]純數字
  • Factor = Num | x [^ Num] | sin ( Factor) [^ Num] | cos ( Factor) [^ Num] | (Expr)
  • Item = [-|+]Factor (*Factor)*
  • Expr = [-|+]Item ([-|+]Item)*

** 其中[]表示其中的字符存在或不存在皆可;()*表示可選重複0至任意次;| 表示或 **架構

除Num單元內部,其它語法單元內部能夠任意插入SPACEide

寫做分析函數的原則有兩條:函數

  1. 採用分層次的設計思路,從概括的語法定義就能夠看出,須要寫一個處理Expr的函數,一個處理Item的函數……須要子結構時調用相應函數便可。
  2. 只處理本層次須要處理的字符,遇到非法字符,返回上層調用或者拋出異常;

固然,有人會說,這樣只是寫一堆遞歸函數(o(╯□╰)o,我就是這麼寫的),沒有實行面對對象,其實,遞歸遞降也是能夠面對對象的。this

模式匹配

首先須要一個Parser接口,實現以下(不必定)函數(爲清晰起見,寫的很簡單atom

public interface Parser {
    public Atom toAtom();

    public Integer getIndex();
    
    public void addParser(Parser parser);
}

分別是一個轉換函數,一個獲取新遍歷下標的函數,一個添加子Parser的函數。架構設計

根據不一樣層級Parser的須要,完成相應函數的重寫(Override)便可,舉個栗子:設計

public class ExprParser implements Parser {
    ArrayList<Parser> itemList;
    // Integer endIndex; //

    private ExprParser() {
        itemList = new ArrayList<>();
    }

    @Override
    public Atom toAtom() {
        Atom root = new Atom(Atom.Type.PLUS, BigInteger.ZERO);
        for (Parser parser:itemList) {
            root.addChild(parser.toAtom());
        }
        return root;
    }
    
    @Override
    public Integer getIndex() {
        return endIndex;
    }

    public static Parser newParser(String string) {
        Parser parser = new ExprParser();
        for (int i = 0; i < string.length();) {
            if (string.charAt(i) == '-' || string.charAt(i) == '+' ||
                    string.charAt(i) != ' ' && string.charAt(i) != '\t') {
                Parser itemParser = ItemParser.newParser(string.substring(i));
                parser.addParser(itemParser);
                i = itemParser.getIndex();
            } else {
                i += 1;
            }
        }
        return parser;
    }

    @Override
    public void addParser(Parser parser) {
        itemList.add(parser);
    }
}

依舊格式簡易,可是隻是爲了說明怎麼重寫這樣的函數,Atom即最後生成的表達樹的節點類或者接口,隨我的意。

另外沒有顯式拋出異常,真正寫時須要在返回parser時檢查item List是否爲空,若爲空,則顯式拋出異常,進行異常處理。

如今說明一下,這個newParser工廠函數爲何這麼寫,爲了清楚起見,我把Expr的定義從新拉過來:

Expr = [-|+]Item ([-|+]Item)*

這個語法定義告訴咱們Expr線性掃描,檢查是否有減號或是加號,而後須要獲取一個Item,這時調用ItemParser的工廠函數便可,格式異常由不一樣層級的Parser實現類的工廠函數負責拋出,只要語法分析合理,這樣寫做是簡單無誤的。

以此類推,相信讀懂了上述抽象的思惟方式,寫出Item和Factor的Parser對你易如反掌!

OO_JAVA_表達式求導_第二彈

------------------------------------------------表達樹的構建

整體來講,按照設計模式上能夠分爲兩類:本次實驗仍是建議第二種方式(雖然我用了第一種,,::>_<::)

  1. extends 繼承,採起一個父類實現所需方法子類重寫方法,可是爲了統一填充到ArrayList或者HashMap中,必須採起子類cast回父類的方式,這樣子類不能有本身的屬性,不然沒法cast;
  2. implements 實現,採起定義共通的接口,全部節點類重寫接口中的方法,如求導方法、化簡方法、toString方法。

在第三次做業中,我採用了繼承的方案實現樹形結構節點的構造,Atom父類實現了很是多的函數,如今的視角來看,是很不合理的,由於Atom負擔了全部子類的函數,臃腫累贅,層次是分明瞭,只有父類Atom和子類各類Atom,可是在工程管理和邏輯架構設計上,仍是有所問題的。

下面是個人Atom類圖關係,其它類都繼承自Atom很複雜把。
Atom類圖關係

OO_JAVA_表達式求導_第三彈

-----------------------------如何將函數式思惟帶入化簡函數

要實現化簡合併,最好先完成兩個準備工做(一個可選)

  1. expand:展開(可選),有時會碰見123*(x+123)這樣的狀況,*與+沒有按照優先級組合,而是須要根據乘法分配率展開這樣的項;
  2. flat Map:平鋪,在處理字符串生成表達樹以及求導表達樹返回新表達樹的過程當中,會產生不少如1*(1*(1*x))連乘項或連加項嵌套的結果,平鋪能夠將之展開成爲1*1*1*x的形式,利於下一步的分類和合並。

首先從宏觀的角度抽象地理解化簡,是怎麼一回事,一共兩步

  1. classification:分類,將能化簡的規整在一塊兒,不能化簡的單獨存放,最終結果是一個List<List<Atom>>,每個List<Atom>存放的是能夠合併的同類項;
  2. merge:合併,對不一樣的可合併List<Atom>進行合併處理,再新建一個父對象,將合併結果依次填回父對象的子節點中,返回父對象。

如今簡單說明一下Java8引入的Stream API和lambda表達式(其實是Functional Interface)

舉個栗子,如今咱們有一串數字在列表origin中,咱們要對他們都平個方,生成一個新列表,正常作法以下:

List<Integer> after = new ArrayList<>();
for (Integer integer: origin) {
    after.add(integer * integer);
}

可是有了Stream API和lambda表達式,就能夠這麼寫:

List<Integer> after = origin.stream().map(e -> e * e).collect(toList());

Stream流的主要功能有filter(篩選),map(映射),flatMap(平鋪映射),collect(收集返回列表或Map或其它)。

須要填充函數接口的參數的來源有兩種:

  1. 方法引用,好比Integer::add,就是對Integer的加法方法的一個引用,內秉實際上是函數接口
  2. lambda表達式,基本形式爲(參數列表)->返回值 或 (參數列表)->{語句; return 返回值;}組成。

如今問題來了,爲何要這麼寫呢?何時要寫成哪種形式呢?

  • 這兩種寫法的主要區別在於邏輯的分離性與可控性:前者循環方式,循環流程徹底由你掌控,可操做性極大,同時腦力負擔也大一些;後者將循環體隱藏在Stream API的內部,你不須要在控制循環的方方面面,能夠一層層地分離邏輯,減輕腦力負擔,高效清晰地編寫代碼。
  • 在循環體內部流程很是簡單時,好比上述栗子,你選擇哪種都沒有什麼問題,無非是代碼行數和內部執行效率的區別,可是,在具化到我要講的classification過程時,也就是能夠儘可能分離循環體內部操做時,選擇Stream API能夠清晰地簡單地生成新列表,下面就對此說明一下吧。

Classification能夠分爲四步:

  1. 實現一個函數(知足Predicate接口定義),也就是一個泛化意義上的相等函數,用來判斷兩項可否合併;
  2. 根據先前實現的接口,採用Stream的filter過濾器API,對每一個元素,過濾出與之相等(可合併)的元素組成列表;
  3. 依照先前產生的列表,通通插入到Map中,爲保證key的惟一,採用每個列表的toString方法結果做爲key;
  4. 將上述生成的Map的values取出,返回一個列表的列表,實現classification的功能。

上述過程就是一個邏輯不斷分離的過程,對應到相加項和相乘項,惟一不一樣的就是Predicate接口,也是這麼思惟的意義所在。

這裏把後三步的合併加法與合併乘法共通的代碼貼出來:

public List<Atom> filter(Atom atom, Predicate<Atom> predicate) {
    return this.children.stream()
        .filter(predicate)
        .collect(Collectors.toList());
}

public ArrayList<List<Atom>> classify(Predicate<Atom> predicate) {
    Map<String, List<Atom>> map = new HashMap<>();
    for (Atom atom : children) {
        ArrayList<Atom> list = (ArrayList<Atom>) this.filter(atom, predicate);
        map.put(list.toString(), list);
    }
    return new ArrayList<>(map.values());
}

與上述思惟相似,Merge也可分步進行:

  1. 先merge全部子節點從新填充到新root節點中;
  2. 使用classification,獲取新root節點分類後的子節點列表的列表;
  3. 建立新root節點,迭代前一步獲取的列表,使用合併同類項函數(同類項列表=>合併後的項),將節點依次插入新的root節點中,返回。

繼續拆分,上述第三步採用Stream的思路還能夠拆分:

  1. 產生Stream<List<Atom>>流;
  2. map映射List<Atom>到Atom,這裏交由另外一個函數接口負責;
  3. filter篩選須要添加至最終列表的項,這裏依然能夠分離出去交由另外一個函數接口負責;
  4. 最後collect函數收集流中的元素,返回List<Atom>,若爲空,返回特定值。

這裏舉個例子就把合併相加項的filter調用的函數接口寫一下吧:

Predicate<Atom> addPredicate = e -> !(
        e.getType().equals(Atom.Type.CONSTANT) && e.getValue().equals(BigInteger.ZERO)
);

上述函數接口代表,若是元素e是一個常數0了話,返回false不然返回true。

綜上所述,若是你理解了分離邏輯的操做後,寫出清晰簡潔的代碼應該是易如反掌把!

相關文章
相關標籤/搜索