紅黑樹插入焕济、刪除纷妆、驗證超詳細解釋,捉住總體思想

本文的實現(xiàn)都來自《算法導論》

所有的東西都是根據(jù)書中的思路來的晴弃,加上我自己的理解掩幢。(推薦配合書本來閱讀)。但是上鞠,這篇文章粒蜈,我不會像《算法導論》一樣逐個逐個情況地分析,我會捉住關鍵思想旗国。其實枯怖,我覺得,如果你想非常嚴謹?shù)乜疾旒t黑樹的插入能曾、刪除修復度硝,沒有什么會比《算法導論》更加好。

本文中寿冕,我假設你已經(jīng)熟悉普通二叉樹的插入蕊程、刪除算法。


修復的總體思想

  1. 自底向上:
    1.1 對于插入驼唱,新插入的結(jié)點一定在最底部藻茂,所以修復一定從最底部開始

    1.2 對于刪除,我們會看到玫恳,它的修復一定也是從最底部開始辨赐。所有刪除情況都可以轉(zhuǎn)換成在這兩種:
  2. 如果能局部(i.e. 在當前樹內(nèi))通過旋轉(zhuǎn)或改變顏色完成修復,那么就修復它京办,并結(jié)束掀序。比如上圖,如果我們能在當前樹(i.e. 以"z"為根的樹)內(nèi)解決問題惭婿,那么我們就可以結(jié)束了不恭。

  3. 如果不能叶雹,我們改變當前樹某些結(jié)點的顏色,然后向上傳遞换吧。

從這里折晦,我們就可以看出,為什么紅黑樹在插入沾瓦、刪除頻繁的場景下比AVL樹優(yōu)满着。因為對于插入的修復,紅黑樹會一直向上傳遞暴拄,然后在適當?shù)牡胤綀?zhí)行最多兩次旋轉(zhuǎn),然后結(jié)束编饺。對于刪除的修復乖篷,紅黑樹會一直向上傳遞,然后在適當?shù)牡胤綀?zhí)行最多三次旋轉(zhuǎn)透且,然后結(jié)束撕蔼。而AVL樹,它的插入修復策略也是一直向上傳遞秽誊,然后在適當?shù)牡胤綀?zhí)行旋轉(zhuǎn)鲸沮,然后結(jié)束;但是它的刪除修復策略是一邊向上傳遞锅论,一邊旋轉(zhuǎn)讼溺,直至到達根。


更深一步

有必要強調(diào)這個事實:
未做修改前最易,樹是滿足所有紅黑性質(zhì)的

  1. 刪除"z"結(jié)點后:
    "z"結(jié)點的所有組先都不滿足性質(zhì)五怒坯。亦即,我們的修復有可能要涉及"z"一直到根節(jié)點這條路徑藻懒,而且剔猿,也只會涉及這條路徑。
  2. 插入”x"結(jié)點后:
    ”x"結(jié)點位于最底部嬉荆,樹中只有”x"結(jié)點這一處可能破壞了紅黑性質(zhì)归敬,然后我們向上傳遞直至能局部修復。任意時刻鄙早,我們保證樹中只有一處地方破壞了一個紅黑性質(zhì),或是性質(zhì)四限番,或是性質(zhì)二陆爽。(這個證明在《算法導論中》,最好看一看)
  3. 紅黑性質(zhì)對每一個結(jié)點都有約束扳缕,比如性質(zhì)五:任一個結(jié)點到它的后代葉子結(jié)點的路徑上的黑結(jié)點數(shù)量都相同慌闭。在向上傳遞時别威,很重要的一件事就是:我們必須先把當前樹的所有紅黑性質(zhì)都修復好,然后再向上傳遞驴剔。然后去到上一層后省古,我們就不再需要關心下面的樹了。這是一個回溯算法的思想丧失。只不過我們用parent來回溯豺妓。

實現(xiàn)紅黑樹的順序:

  1. leftRotate(Node x) 和 rightRotate(Node y) 平衡二叉樹的左旋和右旋操作。這兩個操作在實現(xiàn)AVL樹也要用到布讹。寫代碼時琳拭,只需要把x和y當成平衡二叉樹的結(jié)點就可以了。實現(xiàn)時描验,可以參考書中的圖白嘁,非常清晰:
  2. isValidBST()驗證是否為平衡二叉樹(這個代碼通過LeetCode測試的,應該無問題)膘流,每次調(diào)用 leftRotate() 和 rightRotate()后就驗證一次絮缅。

  3. isValidRBT()根據(jù)紅黑樹的五個性質(zhì)逐一驗證。直接看代碼就可以了呼股,個人認為比較清晰耕魄。

  4. insert() 找到要插入的位置,執(zhí)行插入彭谁,然后調(diào)用isValidBST()驗證是否仍為平衡二叉樹吸奴,然后調(diào)用fixAfterInsertion(),之后再調(diào)用isValidRBT()驗證是否紅黑樹缠局。

package datastru;

import java.util.Random;

//3.5 has bug

//int version
public class RedBlackTree {
    private static final boolean RED = true;
    private static final boolean BLACK = false;
    
    /**
     * sentimal, used as parent of root and leaf-node
     */
    private Node NIL = new Node(0, null, null, null, BLACK); 
    
    private Node root = NIL; //root of red black tree
    
    
    private static class Node {
        int key;
        Node left;
        Node right;
        Node parent;
        boolean color;  
        public Node(int key, Node left, Node right, Node parent, boolean color) {
            this.key = key;
            this.left = left;
            this.right = right;
            this.parent = parent;
            this.color = color;
        }
    }
    
    public void insert(int keyToInsert) {
        insert(new Node(keyToInsert, NIL, NIL, NIL, RED));
    }
    
    private void insert(Node z) {
        //x is used to traverse, and y is always the parent of x
        Node y = NIL;
        Node x = root;
        while(x != NIL) {
            y = x;
            if(z.key < x.key)
                x = x.left;
            else if(z.key > x.key)
                x = x.right;
            else
                return;
        }
        
        //let z become y's children
        z.parent = y;
        if(y == NIL)    
            /*  by the way, this indicates: 
                This is an empty red-black-tree(i.e a tree that just contains a NIL) */
            root = z;
        else if(z.key < y.key)
            y.left = z;
        else
            y.right = z;
        
        assert isValidBST();
        fixAfterInsertion(z);
        assert isValidBST();
        assert isRBT();
    }
    
    private void fixAfterInsertion(Node z) {
        
        /* 紅黑樹有5個性質(zhì)
           一奄抽、每個結(jié)點非紅即黑
           二、根節(jié)點是黑色
           三甩鳄、葉子結(jié)點是黑色
           四逞度、紅結(jié)點的兩個子節(jié)點都是黑節(jié)點
           五、對每個結(jié)點妙啃,從它到它的后代葉子結(jié)點的所有簡單路徑上档泽,黑節(jié)點的數(shù)量都相同
         */
        
        /*
            z為新插入的結(jié)點,它是紅色的揖赴,它的兩個孩子都是葉子節(jié)點(NIL)
            所以馆匿,如果此時紅黑樹性質(zhì)被破壞,只有兩種可能:
                1. z就是根節(jié)點燥滑,這破壞了性質(zhì)二
                2. z和z.parent都是紅色渐北,這破壞了性質(zhì)四
        */
        
        /*
            z在這個函數(shù)中的語義:
                就像下面的invariants(3)提到的,整個修復過程铭拧,紅黑樹只有一處違反了紅黑性質(zhì)
                且這一處赃蛛,要么違反的是性質(zhì)二恃锉,要么違反的是性質(zhì)四。(如果你迷糊了呕臂,只要這樣想:
                未插入之前破托,這是一顆紅黑樹,插入時在最底部進行的歧蒋,那么當然此時只可能在一處違反紅黑性質(zhì)土砂,
                然后,我們的修復策略每次都會將這一處修復谜洽,然后向上傳遞萝映,直至能夠在某個地方完全修復,
                或一直傳遞到頂部)
                
                所以我們只需關注:z, z.parent, z的uncle
         */
        
        /*  How fix strategy works?
            在情況一中:為什么能夠只通過更換顏色來修復 "z和z.parent都是紅色"
            在情況二和情況三中:我們?yōu)槭裁茨軌蛲ㄟ^旋轉(zhuǎn)來修復 "z和z.parent都是紅色"
            
            the shared answer: 
            對于z的兩個子樹阐虚、z.parent的一個子樹序臂、uncle的兩個子樹,
            它們的根都是黑色敌呈,而且它們都具有相同的黑高
         */
        
        /*
            如果進入了循環(huán)(也就是 z.parent.color == RED 為true)贸宏,
            那么我們在循環(huán)體中修復 "z和z.parent都是紅色" 造寝,
                a)如果是通過情況二或情況三修復的磕洪,那么直接退出循環(huán)。
                b)如果是通過情況一修復的诫龙,將z上移兩層析显。新的z將會是紅色,
                    那么我們在下一次循環(huán)就看下:新的z和它的父親是否都是紅色签赃,
                    是就繼續(xù)執(zhí)行修復谷异,否就退出循環(huán)。(i.e. 向上傳遞锦聊。歹嘹。。)
                
            invariants: 
                1. z是紅色的
                2.  如果z.parent為根節(jié)點孔庭,那么z.parent是黑色的(也就是性質(zhì)二沒有被破壞)
                3. 如果有任何紅黑性質(zhì)被破壞尺上,則至多只有一條被破壞,或是性質(zhì)二圆到,或是性質(zhì)四怎抛。
                      3.1如果是性質(zhì)二,那么就是:z.parent為根節(jié)點芽淡,且z.parent是紅色的马绝,出現(xiàn)這種情況的原因是
                              經(jīng)過一次或多次向上傳遞(i.e. 執(zhí)行一次或多次循環(huán)體的情況一),z從紅黑樹的底部傳到最頂部了
                      3.2如果是性質(zhì)四挣菲,那么就是:z和z.parent都是紅色的富稻,這正正就是while循環(huán)體要修復的
            
            這三個invariants在:
                第一次循環(huán)開始之前都成立掷邦、(初始)
                在循環(huán)的每次迭代后(i.e. 每次循環(huán)體執(zhí)行完畢后)成立、(保持)
                在循環(huán)終止后也成立(終止) 這能推導出一個有用的性質(zhì)
                                         /
                                        /
                                       \/
                            invariants(3)指出要么是性質(zhì)二不成立唉窃,要么是性質(zhì)四不成立耙饰,
                            現(xiàn)在z的父親為黑色,那么性質(zhì)四肯定成立纹份,性質(zhì)二有可能不成立
                            具體場景看最后一行代碼
         */
        while(z.parent.color == RED) {
            //當在循環(huán)體中引用z的爺爺時苟跪,由invariants(2)可知,z的爺爺必定存在(i.e. 不為NIL)
            
            if(z.parent == z.parent.parent.left) {
                Node uncle = z.parent.parent.right;
                if(uncle.color == RED) {    //case 1
                    uncle.color = z.parent.color = BLACK;
                    z.parent.parent.color = RED;
                    z = z.parent.parent;    //向上傳遞
                    
                    //case 1結(jié)束后有可能退出循環(huán)蔓涧,也有可能繼續(xù)循環(huán)
                }
                else {  
                    if(z == z.parent.right) {   //case 2件已,轉(zhuǎn)變?yōu)閏ase3
                        z = z.parent;
                        leftRotate(z);
                    }
                    //case 3:
                    z.parent.color = BLACK;
                    z.parent.parent.color = RED;
                    rightRotate(z.parent.parent);
                    
                    //case 3結(jié)束后必定退出循環(huán)
                    assert z.parent.color == BLACK;
                }
            }
            else { //與上面代碼基本一樣,對稱
                if(z.parent == z.parent.parent.right) {
                    Node uncle = z.parent.parent.left;
                    if(uncle.color == RED) {    
                        uncle.color = z.parent.color = BLACK;
                        z.parent.parent.color = RED;
                        z = z.parent.parent;    
                    }
                    else {  
                        if(z == z.parent.left) {    
                            z = z.parent;
                            rightRotate(z);
                        }
                        z.parent.parent.color = RED;
                        z.parent.color = BLACK;
                        leftRotate(z.parent.parent);
                        assert z.parent.color == BLACK;
                    }
                }
            }
        }
        
        /*
            經(jīng)過若干次迭代后元暴,如果z是根節(jié)點篷扩,且它是紅色,
            那么z的父親(i.e. NIL)就是黑色茉盏,循環(huán)終止鉴未。此時性質(zhì)二不成立。
            現(xiàn)在修復它:
         */
        root.color = BLACK;
    }
    
    public void delete(int keyToDelete) {
        Node x = root;
        while(x != NIL) {
            if(x.key < keyToDelete)
                x = x.right;
            else if(x.key > keyToDelete)
                x = x.left;
            else {
                delete(x);
                break;
            }
        }
    }
    
    private Node successor(Node t) {
        if(t.left == NIL)
            return t;
        
        return successor(t.left);
    }


    private void delete(Node z) {
        assert z != NIL;
        
        if(z.left != NIL && z.right != NIL) {
            Node successor = successor(z.right);
            z.key = successor.key;
            
            //not recursion, just an way to turn if-case to else-case
            //and more importantly, highlight the semantic
            delete(successor);  
        }
        else {
            Node x = (z.left != NIL) ? z.left : z.right; //NOTE that, x may be NIL
            boolean needToFixUp = z.color == BLACK;
            
            //make the subTree rooted at 'x' be the child of z.parent
            x.parent = z.parent;
            if(z.parent == NIL)
                root = x;
            else {
                if(z.parent.left == z)
                    z.parent.left = x;
                else
                    z.parent.right = x;
            }
            
            //now, x is replacement of the deleted Node
            if(needToFixUp)
                fixAfterDeletion(x);
        }
    }

    private void fixAfterDeletion(Node x) {
        /*  
            只考慮x為左孩子的情況鸠姨,x是右孩子的情況對稱铜秆。。讶迁。
            
         */
    }

    //treat x as a binary-search-tree node, left rotate it
    private void leftRotate(Node x) {
        assert x.right != NIL;
        Node y = x.right;
        
        Node subTree = y.left;
        x.right = subTree;
        if(subTree != NIL)
            subTree.parent = x;
        
        Node p = x.parent;
        y.parent = p;
        if(p == NIL)
            root = y;
        else if(p.left == x)
            p.left = y;
        else
            p.right = y;
        
        x.parent = y;
        y.left = x;
        
        assert isValidBST();
    }
    
    //treat y as a binary-search-tree node, right rotate it
    private void rightRotate(Node y) {
        assert y.left != NIL;
        Node x = y.left;
        
        Node subTree = x.right;
        y.left = subTree;
        if(subTree != NIL)
            subTree.parent = y;
        
        Node p = y.parent;
        x.parent = p;
        if(p == NIL)
            root = x;
        else if(p.left == y)
            p.left = x;
        else
            p.right = x;
        
        x.right = y;
        y.parent = x;
        
        assert isValidBST();
    }
    
    /* Debug methods: */
    private boolean isRBT() {
        if(root == NIL) //empty
            return true;

        return root.color == BLACK && isRBT(root);
    }
    
    private boolean isRBT(Node t) {
        Node tp = t.parent, tl = t.left, tr = t.right;  
        
        if (tp != NIL && t != tp.left && t != tp.right)
            return false;
        
        if (tl != NIL && (tl.parent != t || tl.key > t.key))
            return false;
        
        if (tr != NIL && (tr.parent != t || tr.key < t.key))
            return false;
        
        if (t.color == RED && tl != NIL && tl.color == RED) 
            return false;
        if (t.color == RED && tr != NIL && tr.color == RED)
            return false;
        
        if(!hasSameNumberOfBlackNodeInAllPaths(t))
            return false;
        
        //recursive check
        if (tl != NIL && !isRBT(tl))
            return false;
        if (tr != NIL && !isRBT(tr))
            return false;
        
        return true;
    }
    
    private int expectedBlackNodeCount;
    private boolean hasSameNumberOfBlackNodeInAllPaths(Node t) {
        //for convenience, when computing how many black node in a path:
            //root(t) is included
            //leaf(NIL) is included
        expectedBlackNodeCount = 1;     //there must be a leaf node in any path
        Node x = t;
        while(x != NIL) {
            if(x.color == BLACK)
                expectedBlackNodeCount ++;
            x = x.left;
        }
        return backTrack(t, 0);
    }

    private boolean backTrack(Node t, int count) {
        if(t == NIL)
            return count + 1 == expectedBlackNodeCount;
        
        if(t.color == BLACK)
            count ++;
        
        return backTrack(t.left, count) &&
               backTrack(t.right, count);
    }

    private boolean isValidBST(Node root, Integer lowerBound, Integer uppperBound) {
        if(root == NIL)
            return true;

        boolean isRootInRange = (lowerBound == null || root.key > lowerBound) &&
                                (uppperBound == null || root.key < uppperBound);
        if(!isRootInRange)
            return false;

        return isValidBST(root.left, lowerBound, root.key) &&
                isValidBST(root.right, root.key, uppperBound);
    }

    private boolean isValidBST() {      
        return isValidBST(root, null, null);
    }
    
    /*some tests*/
    private static void insertAscendly() {
        RedBlackTree rbt = new RedBlackTree();
        
        for(int i = 1; i < 1000; i ++)
            rbt.insert(i);
    }
    private static void insertDescendly() {
        RedBlackTree rbt = new RedBlackTree();
        
        for(int i = 1000; i >= 0; i --)
            rbt.insert(i);
    }
    private static void insertRandomly() {
        RedBlackTree rbt = new RedBlackTree();
        
        Random ra =new Random();
        for (int i = 0 ;i < 1000; i++) {
            rbt.insert(ra.nextInt(10000000));
        }
    }
    
    private static void testDeleteWithoutFixUp() {
        RedBlackTree rbt = new RedBlackTree();
        
        Random ra =new Random();
        int[] keys = new int[1000];
        for (int i = 0 ;i < 1000; i++) {
            keys[i] = ra.nextInt(10000000);
            rbt.insert(keys[i]);
        }
        for(int i = 0; i < 100; i ++) {
            rbt.delete(keys[i]);
        }
        assert rbt.isValidBST();
        
        rbt = new RedBlackTree();
        for(int i = 1000; i >= 0; i --)
            rbt.insert(i);
        for(int i = 1000; i >= 0; i --)
            rbt.delete(i);
        assert rbt.isValidBST();
        
        rbt = new RedBlackTree();
        for(int i = 0; i < 1000; i ++)
            rbt.insert(i);
        for(int i = 0; i < 1000; i ++)
            rbt.delete(i);
        assert rbt.isValidBST();
    }
    
    public static void main(String[] args) {
        insertAscendly();
        insertDescendly();
        insertRandomly();
    }   
}

刪除算法后續(xù)更新

end

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末连茧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子巍糯,更是在濱河造成了極大的恐慌啸驯,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祟峦,死亡現(xiàn)場離奇詭異罚斗,居然都是意外死亡,警方通過查閱死者的電腦和手機宅楞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門针姿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人咱筛,你說我怎么就攤上這事搓幌∧宦” “怎么了犁钟?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵蛉迹,是天一觀的道長失驶。 經(jīng)常有香客問我减宣,道長洲愤,這世上最難降的妖魔是什么娶吞? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任火脉,我火速辦了婚禮,結(jié)果婚禮上堂污,老公的妹妹穿的比我還像新娘家肯。我一直安慰自己,他們只是感情好盟猖,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布讨衣。 她就那樣靜靜地躺著,像睡著了一般式镐。 火紅的嫁衣襯著肌膚如雪反镇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天娘汞,我揣著相機與錄音歹茶,去河邊找鬼。 笑死你弦,一個胖子當著我的面吹牛惊豺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播禽作,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼尸昧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了领迈?” 一聲冷哼從身側(cè)響起彻磁,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤碍沐,失蹤者是張志新(化名)和其女友劉穎狸捅,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體累提,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡尘喝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了斋陪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片朽褪。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖无虚,靈堂內(nèi)的尸體忽然破棺而出缔赠,到底是詐尸還是另有隱情,我是刑警寧澤友题,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布嗤堰,位于F島的核電站,受9級特大地震影響度宦,放射性物質(zhì)發(fā)生泄漏踢匣。R本人自食惡果不足惜告匠,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望离唬。 院中可真熱鬧后专,春花似錦、人聲如沸输莺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嫂用。三九已至建瘫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間尸折,已是汗流浹背啰脚。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留实夹,地道東北人橄浓。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像亮航,于是被迫代替她去往敵國和親荸实。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 紅黑樹 紅黑樹也是一種自平衡的二叉搜索樹以前也叫做平衡二叉B樹(Symmetric Binary B-tree) ...
    ducktobey閱讀 768評論 0 1
  • 之前在公司組內(nèi)分享了紅黑樹的工作原理缴淋,今天把它整理下發(fā)出來准给,希望能對大家有所幫助,對自己也算是一個知識點的總結(jié)重抖。 ...
    JasonGaoH閱讀 996評論 1 14
  • 之前在公司組內(nèi)分享了紅黑樹的工作原理钟沛,今天把它整理下發(fā)出來畔规,希望能對大家有所幫助,對自己也算是一個知識點的總結(jié)恨统。 ...
    Java旺閱讀 491評論 0 3
  • 目錄 1叁扫、什么是樹 2、相關術(shù)語 3畜埋、二叉樹 3.1莫绣、二叉樹的類型 3.2、二叉樹的性質(zhì) 3.3悠鞍、二叉樹的結(jié)構(gòu) 3...
    我哈啊哈啊哈閱讀 2,548評論 0 10
  • 目錄 0.樹0.1 一般樹的定義0.2 二叉樹的定義 1.查找樹ADT 2.查找樹的實現(xiàn)2.1 二叉查找樹2.2 ...
    王偵閱讀 7,208評論 0 3