09_算法基本概念(Java丰介、Kotlin描述)

????本篇文章介紹一些算法里用到的基本概念背蟆。

(1)遞歸

????一個微妙的遞歸算法,代碼如下:

    public static int bad(int n) {
        if (n == 0)
            return 0;
        else
            return bad(n / 3 + 1) + n - 1;
    }

????初一看基矮,好像沒什么問題淆储。但如果n=1冠场,那么會出現bad(1) = bad(1)家浇。而bad(1)的值究竟是多少,不知道碴裙。
????這里就需要了解遞歸的兩個基本法則:
????????1)基準情形(base case)钢悲,必須總有某些基準的情形点额,它們不用遞歸就能求解;
????????2)不斷推進莺琳,遞歸調用必須總能夠朝著一個基準情形推進还棱。
????上面的代碼違反了第2)條,遞歸調用最終并沒有推進到基準情形惭等。推進到bad(1)就陷入死循環(huán)了珍手。

(2)散列

  • 散列(hashing)是一種用于以常數平均時間插入、刪除和查找的技術辞做。需要排序信息的操作則不支持琳要,如findMax、findMin等秤茅。散列是散列表的一種實現稚补;
  • 散列函數:將關鍵字映射到0到size-1這個范圍中的函數,稱為散列函數框喳;一個簡單示例:key % size 课幕;另外一個示例,Java版本:
    public static int hash(String key , int size){
        int hash = 0;
        for (int i =0 ; i < key.length() ; i++){
            hash += key.charAt(i);
        }
        return hash % size;
    }

????Kotlin版本:

    fun hash(key: String, size: Int): Int {
        var hash = 0
        for (i in 0 until key.length) {
            hash += key[i].code
        }
        return hash % size
    }
  • 散列沖突:兩個關鍵字散列到同一個值五垮;
  • 分離鏈接法:將沖突的元素保留到一個鏈表中乍惊,如Java中的HashMap、HashSet實現方式拼余;

(3)中綴表達式轉后綴表達式

????中綴表達式就是我們常見的算術表達式污桦,如 a + b * c + (d * e + f) * g。后綴表達式是一種將操作數放在前匙监,操作符放在后面的表達式凡橱,它不使用圓括號。最簡單后綴表達式如“ab+”亭姥,它對應的中綴表達式是"a+b"稼钩。中綴表達式對于人來說非常容易理解,但對于計算機來說太復雜 达罗。而后綴表達式則相反坝撑。
???? a + b * c + (d * e + f) * g 轉換為后綴表達式的結果是:abc * + de * f + q * +。
????這個轉換需要用到Stack數據結構粮揉。對中綴表達式的每個字符巡李,如果是操作數,則放到輸出中扶认;如果是操作符侨拦,則根據棧頂情況處理。
????????1)如果棧頂的操作符優(yōu)先級高辐宾,那么將它立即出棧狱从;如果兩者優(yōu)先級一樣膨蛮,也立即出棧;如果棧頂操作符優(yōu)先級低季研,則入棧敞葛。
????????2)對于圓括號 "(" 和 ")",左括號直接入棧与涡;只有遇到右括號惹谐,才移走左括號。
????后綴表達式的求值:也使用一個棧驼卖,不過處理的不是操作符豺鼻,而是操作數。對每個字符款慨,如果是操作數,就入棧桩了;如果是操作符埠戳,則從棧頂彈出2個操作數并進行計算,計算結果入棧颗圣。如此循環(huán)屁使,直到處理完表達式中的所有字符在岂。最后,棧中只剩下一個元素蛮寂,就是表達式的求值結果蔽午。

(4)樹的一些基本概念

  • 樹是一種非線性結構;
  • 樹是一些節(jié)點的集合酬蹋。這個集合可以是空集及老,如果不是空集,則由根節(jié)點和子樹組成范抓;
  • 一棵樹是N個節(jié)點和N-1條邊的集合骄恶。除了根節(jié)點外,每個節(jié)點都有一個父親匕垫;
  • 只有根節(jié)點沒有直接前驅僧鲁,其他節(jié)點有且僅有一個直接前驅;
  • 每個節(jié)點可以有任意多個直接后繼;
  • 兄弟節(jié)點:具有同一個父節(jié)點的節(jié)點悔捶;
  • 節(jié)點的度:一個節(jié)點所包含子樹的數量;
  • 樹的度:該樹所有節(jié)點中最大的度单芜;
  • 節(jié)點的層數:根節(jié)點的層數是1蜕该,依次向下。樹是一種層次結構洲鸠,每個節(jié)點都處在一定的層次上堂淡。
  • 樹的深度:樹中節(jié)點的最大層數稱為樹的深度
  • 樹的表示法:層次括號法,如( A )表示只有一個根節(jié)點A扒腕;( A( B , C ) )表示3個節(jié)點組成的樹绢淀,A是根節(jié)點,B瘾腰、C是A的直接后繼節(jié)點;以此類推;
  • 有序樹:樹中各節(jié)點的子樹是按一定次序從左向右安排的看铆;
  • 無序樹:與有序樹相反;
  • 森林:多棵互不相交的樹的集合竞慢;
  • 先序遍歷:先處理節(jié)點,再處理孩子寺谤;
  • 后序遍歷:先處理孩子变屁,再處理節(jié)點;
  • 二叉樹:每個節(jié)點闷板,子節(jié)點數不超過2遮晚;
  • 中序遍歷:先處理左孩子糜颠,再處理節(jié)點其兴,最后處理右孩子元旬。針對二叉樹而言。

????二叉樹的一般結構:

class BinaryNode {
    Object element;
    BinaryNode left;
    BinaryNode right;
}
  • 表達式樹:樹葉是操作數朋譬,其他是操作符徙赢。從后綴表達式可以構造一個表達式樹;
  • 二叉查找樹:對于每個節(jié)點X枕屉,它的左子樹中所有項的值都小于X中的項搀擂,右子樹則相反哨颂;它的平均深度是O(log N);
  • AVL樹:帶有平衡條件的二叉查找樹威恼,每個節(jié)點的左子樹和右子樹的高度最多相差1腹备;除去插入操作外(惰性刪除)植酥,所有的樹操作都是O(log N)哎媚。插入操作可能會破壞AVL樹的特性稻据,這種特性需要“旋轉”操作來維持捻悯。插入一個新節(jié)點后今缚,如果節(jié)點A的AVL性質被破壞了,那么新節(jié)點的位置存在下列幾種情況:
  1. A的左兒子的左子樹何荚;
  2. A的左兒子的右子樹餐塘;
  3. A的右兒子的左子樹;
  4. A的右兒子的右子樹稠鼻;

????這可以歸為兩種情況候齿,一種是插入發(fā)生在“外邊”的情況周霉,即左-左俱箱、右-右,另外一種是插入發(fā)生在“內部”的情況禁漓,即左-右伶跷、右-左叭莫。第一種情況需要“單旋轉”來進行調整;第二種情況需要“雙旋轉”進行調整抵皱。
????Java實現如下:

public class AVLTree {
    //節(jié)點
    class Node {
        int data;
        Node leftChild;
        Node rightChild;
        int height;

        public Node(int data) {
            this(data, null, null);
        }

        public Node(int data, Node leftChild, Node rightChild) {
            this.data = data;
            this.leftChild = leftChild;
            this.rightChild = rightChild;
        }
    }

    int height(Node node) {
        return node == null ? -1 : node.height;
    }

    /**
     * 更新節(jié)點node的高度
     *
     * @param node
     */
    void updateHeight(Node node) {
        int leftHeight = height(node.leftChild);
        int rightHeight = height(node.rightChild);
        node.height = Math.max(leftHeight, rightHeight) + 1;
    }

    /**
     * 節(jié)點左、右子樹的高度差
     * 如果返回值為正伤为,說明右子樹較高
     *
     * @param node
     * @return
     */
    int balanceFactor(Node node) {
        return height(node.rightChild) - height(node.leftChild);
    }

    //右旋
    Node rotateRight(Node node) {
        Node left = node.leftChild;
        node.leftChild = left.rightChild;
        left.rightChild = node;
        updateHeight(left);
        updateHeight(node);
        return left;
    }

    //左旋
    Node rotateLeft(Node node) {
        Node right = node.rightChild;
        node.rightChild = right.leftChild;
        right.leftChild = node;
        updateHeight(right);
        updateHeight(node);
        return right;
    }

    //使節(jié)點node保持AVL樹特性
    Node reBalance(Node node) {
        //如果為正,說明右子樹高位衩;如果為副僚祷,說明左子樹高辙谜。絕對值如果大于1,說明AVL特性被破壞
        int balanceFactor = balanceFactor(node);
        if (balanceFactor < -1) {//左子樹高蜕琴,特性被破壞
            if (balanceFactor(node.leftChild) < 0) { //左-左情況,右旋
                node = rotateRight(node);
            } else { //左-右情況号醉,先左旋,再右旋
                node.leftChild = rotateLeft(node.rightChild);
                node = rotateRight(node);
            }
        }
        if (balanceFactor > 1) {//右子樹高
            if (balanceFactor(node.rightChild) > 0) {//右-右情況润绵,左旋
                node = rotateLeft(node);
            } else {//右-左情況,先右旋卿捎,再左旋
                node.rightChild = rotateRight(node);
                node = rotateLeft(node);
            }
        }
        return node;
    }

    //插入一個新節(jié)點
    Node insertNode(int data, Node node) {
        if (node == null) {
            return new Node(data);
        }
        if (data < node.data) {
            node.leftChild = insertNode(data, node.leftChild);
        } else if (data > node.data) {
            node.rightChild = insertNode(data, node.rightChild);
        } else { //重復了
            return node;
        }

        updateHeight(node);
        return reBalance(node);
    }

    //刪除節(jié)點
    Node deleteNode(int data, Node node) {
        if (node == null) return null;
        if (data < node.data) {
            node.leftChild = deleteNode(data, node.leftChild);
        } else if (data > node.data) {
            node.rightChild = deleteNode(data, node.rightChild);
        } else if (node.leftChild == null && node.rightChild == null) {
            return null;
        } else if (node.leftChild == null) {//節(jié)點只有右孩子
            node = node.rightChild;
        } else if (node.rightChild == null) { //節(jié)點只有左孩子
            node = node.leftChild;
        } else { //節(jié)點左右孩子都有
            //找到右子樹的最小節(jié)點
            Node minNode = node.rightChild;
            while (minNode.leftChild != null) {
                minNode = minNode.leftChild;
            }
            //將最小點的值保存到node
            node.data = minNode.data;
            node.rightChild = deleteNode(minNode.data,node.rightChild); //刪除右子樹的最小點底桂,因為它已經移到node了
        }
        updateHeight(node);
        return reBalance(node);
    }
}
  • 層序遍歷:所有深度為d的節(jié)點,要在深度為d+1的節(jié)點之前處理暮顺。

  • 滿二叉樹:除了葉子節(jié)點外拖云,其他節(jié)點都有2個子節(jié)點乏苦;

  • 完全二叉樹:除最后一層外汇荐,其他各層的節(jié)點數都是2,且最后一層葉節(jié)點按照從左至右的順序連續(xù)存在油昂,只缺最后一層右側若干節(jié)點革娄;

  • B樹:當數據結構非常大,內存裝不下時冕碟,就需要用到B樹拦惋。它用來平衡cpu計算速度和磁盤訪問速度不匹配的問題。

  • M叉查找樹:有M路分支的查找樹安寺,一顆完全M叉查找樹的高度大約是log_mN 厕妖。

  • 攤還時間:當M次操作的序列總的最壞情形運行時間為O(M f(N))時,我們就說它的攤還運行時間為O(f(N))。

  • 伸展樹:從空樹開始凳枝,連續(xù)M次對樹的操作最多花費O(M logN)時間的一種二叉樹锭环。伸展樹的攤還時間是O(log N)蛾茉。它的基本想法如下:

????當一個節(jié)點被訪問后,它就要經過一系列AVL樹的旋轉被推到根上赔桌;如果一個節(jié)點很深纽竣,那么在其路徑上就存在許多相對較深的節(jié)點幽勒,通過重新構造可以減少對這些節(jié)點的進一步訪問時間。伸展樹在節(jié)點過深時朵纷,具有平衡這棵樹(某種程度上)的作用落午。在實際的許多應用中,當一個節(jié)點被訪問時瓷翻,它很可能不久再被訪問敢朱。使用伸展樹結構,可以減少查找時間洛巢。
????此外,伸展樹不要求保留高度或平衡信息茂装。根節(jié)點屋摇、左孩子和右孩子也沒有大小關系重斑,它不是查找樹坦冠。

(5)圖的一些基本概念

  • 每個數據元素之間佛玄,可以任意關聯哼蛆,構成了一個圖結構旅东;
  • 圖結構包括兩個部分:頂點Vertext和邊Edge,所有的頂點構成一個頂點集合陡舅,記為V(G)蜈出;所有的邊構成一個邊集合,記為E(G)涛酗;
  • 在數學上铡原,圖結構記為:G = (V , E) 或者 G = ( V(G) , V(E) ) ; V(G)必須是非空,V(E)可以為空商叹;
  • V(G)示例:V(G) = { V1 , V2 , V3 , V4} ;
  • E(G)示例:E(G) = { (V1, V2) , (V1 , V3) , (V2, V4) ,(V1 , V4)} ;
  • 在一個圖中燕刻,所有的邊都沒有方向性,則它是無向圖剖笙;無向圖中對頂點順序沒有要求卵洗,(V1 , V2) 和 (V2 , V1)是一樣的;
  • 在一個圖中弥咪,邊是有方向性的过蹂,則它是有向圖;頂點順序表示指向酪夷;用尖括號表示:<V1 , V2> ;
  • 頂點的度:連接頂點的邊的數量稱為度榴啸;對于無向圖,即為連接頂點的邊的數量晚岭,記為D(V);對于有向圖勋功,則分為入度ID(V)和出度OD(V)坦报;有向圖中頂點的度是入度+出度库说,D(V) = ID(V) + OD(V);
  • 鄰接頂點:一條邊的兩個頂點片择;有向圖中根據邊的方向潜的,分為起始頂點和結束頂點,也分別稱為入邊鄰接點和出邊鄰接點字管;
  • 無向完全圖:如果一個無向圖中啰挪,每兩個頂點之間都存在一條邊,那么它是無向完全圖嘲叔;
  • 有向完全圖:如果一個有向圖中亡呵,每兩個頂點之間都存在方向相反的兩條邊,那么它是有向完全圖硫戈;包含N個頂點的有向完全圖锰什,邊數是N(N-1);
  • 子圖:類似于子集合,子圖的頂點和邊都應該是父圖的頂點和邊的子集丁逝;
  • 路徑:路徑是圖結構中兩個頂點之間的連線汁胆;路徑上邊的數量稱為路徑長度;兩個頂點之間的路徑可能有多條霜幼,路徑長度可能不一樣嫩码;
  • 簡單路徑:如果一條路徑上頂點不重復出現,則稱為簡單路徑罪既;
  • 環(huán):如果一條路徑的第一個頂點和最后一個頂點相同谢谦,則稱之為環(huán),也稱為回路萝衩;
  • 簡單回路:如果第一個頂點和最后一個頂點相同回挽,其余各點都不重復的回路稱為簡單回路;
  • 連通:如果圖結構中猩谊,兩個頂點之間有路徑千劈,則稱為這兩個頂點是連通的;這兩個頂點可以不鄰接牌捷;
  • 連通圖:如果無向圖中墙牌,任意兩個頂點都是連通的,那么這個圖便稱為連通圖暗甥;
  • 連通分量:無向圖的極大連通子圖喜滨;對于一個連通圖,其連通分量有且只有一個撤防,就是它自身虽风;非連通圖,有多個連通分量;
  • 強連通圖:如果有向圖中辜膝,任意兩個頂點都是連通的无牵,則稱該圖為強連通圖;注意有向圖中邊具有方向性厂抖;
  • 強連通分量:有向圖的極大連通子圖稱為該圖的強連通分量茎毁;強連通圖只有一個強連通分量,就是它自身忱辅;
  • 權:將邊表示為某種數值七蜘,這個數值就是權;無向圖中加入權墙懂,稱為無向帶圖權橡卤;有向圖中加入權,稱為有向帶圖權垒在;在交通圖中蒜魄,權可以代表路徑長度;
  • 網:邊上帶權值的圖的另一種名稱场躯;
  • 鄰接矩陣:保存頂點之間關系的數組谈为;示例如下:
char vertex = new char[N] ;// 保存頂點信息
int[][] edgeWeight = new int[N][N];// 保存邊的權或者連接關系
  • 根據兩個頂點的編號,從鄰接矩陣中獲取值踢关,如果為0表示這兩個頂點之間沒有邊伞鲫;如果為1,則表示有邊签舞;如果大于1秕脓,表示權;
  • 對于無向圖儒搭,鄰接矩陣左下角和右上角是對稱的吠架;
  • 圖結構,Java語言描述:
public class Graph {
    static final int NUM = 20; //圖的頂點數
    char[] vertex = new char[NUM]; //頂點集合
    int gType; //圖類型搂鲫,0表示無向圖傍药;1表示有向圖
    int vertexNum;//頂點數量
    int[][] edgeWeight = new int[NUM][NUM] ; //鄰接矩陣
    int[] isTrav = new int[NUM];
}

????Kotlin版本:

class Graph {
    var vertex = CharArray(NUM) //頂點集合
    var gType //圖類型,0表示無向圖魂仍;1表示有向圖
            = 0
    var vertexNum //頂點數量
            = 0
    var edgeWeight = Array(NUM) { IntArray(NUM) } //鄰接矩陣
    var isTrav = IntArray(NUM)

    companion object {
        const val NUM = 20 //圖的頂點數
    }
}
  • 圖的遍歷拐辽,深度優(yōu)先算法,Java版本:
    void deepTraOne(Graph g,int n){ //從第n個頂點開始
        int i;
        g.isTrav[n] = 1; //標記該頂點已經訪問過
        System.out.println(g.vertex[n]); //輸出節(jié)點數據
        for (i = 0;i<g.vertexNum;i++){
            if (g.edgeWeight[n][i] != 0 && g.isTrav[i] == 0){
                deepTraOne(g,i);
            }
        }
    }
    
    //深度優(yōu)先遍歷
    void deepTraGraph(Graph g){
        int i;
        for (i =0;i<g.vertexNum;i++){
            if (g.isTrav[i] == 0){
                deepTraOne(g,i);
            }
        }
    }

????Kotlin版本:

    fun deepTraOne(g: Graph, n: Int) { //從第n個頂點開始
        var i: Int
        g.isTrav[n] = 1 //標記該頂點已經訪問過
        println(g.vertex[n]) //輸出節(jié)點數據
        i = 0
        while (i < g.vertexNum) {
            if (g.edgeWeight[n][i] != 0 && g.isTrav[i] == 0) {
                deepTraOne(g, i)
            }
            i++
        }
    }

    //深度優(yōu)先遍歷
    fun deepTraGraph(g: Graph) {
        var i: Int
        i = 0
        while (i < g.vertexNum) {
            if (g.isTrav[i] == 0) {
                deepTraOne(g, i)
            }
            i++
        }
    } 
  • 生成樹:滿足這3個條件的的樹稱為原圖的一顆生成樹擦酌,1)子圖的頂點與原圖相同俱诸;2)子圖的邊是原圖的子集,且這部分邊剛好將圖中所有頂點連通赊舶;3)子圖中的邊不構成回路睁搭;
  • 對于有n個頂點的連通圖赶诊,其生成樹有且只有n-1條邊。如果邊數少于此數介袜,就不可能將所有頂點連通甫何;如果多于此數出吹,則必須產生回路遇伞;
  • 最小生成樹:一個圖的生成樹可能有多個,其中權值最小的生成樹就稱為圖的最小生成樹捶牢;

(6)堆

  • 優(yōu)先隊列:某些作業(yè)應該具有優(yōu)先權鸠珠,支持這種特點的隊列稱為優(yōu)先隊列;
  • 優(yōu)先隊列是一種特殊的數據結構秋麸,至少支持兩種操作:insert 和 deleteMin渐排,即插入和刪除最小者;一種簡單實現是:以一個單鏈表為基礎灸蟆,在表頭插入數據驯耻,并遍歷該鏈表,以刪除最小元炒考;
  • 堆:也叫二叉堆可缚,可以實現優(yōu)先隊列。堆是一顆完全二叉樹斋枢,一個高為h的完全堆帘靡,有2^h到2^h ^+ ^1-1個節(jié)點,高是logN ;
  • 堆可以用數組來表示瓤帚,對于數組中任一位置i(為維持特性描姚,從下標1開始)上的元素,其左兒子在位置2i上戈次,右兒子在位置(2i + 1)上轩勘,它的父親則在i/2上;例如一個包含3元素的堆怯邪,A(B,C) 绊寻,A是根節(jié)點,B擎颖、C分別為左右子樹榛斯,那么對應的數組array[ ]大小為3+2,a[1] = A搂捧,a[2] = B, a[3] = C ;其中a[0] 和a[4]為空驮俗,不使用。
  • 對于元素為N個的堆來說允跑,N/2剛好是最后一個非葉節(jié)點王凑;
  • 大根堆:在堆中搪柑,對于每個節(jié)點,它的key比左索烹、右孩子的key大或者等工碾,這樣的堆稱為大根堆;
  • 小根堆:在堆中百姓,對于每個節(jié)點渊额,它的key比左、右孩子的key小或者等垒拢,這樣的堆稱為小根堆旬迹;
  • 堆的插入:在size-1位置后,再創(chuàng)建一個空的可用位置求类,簡稱空穴奔垦,這樣保持了堆的特性。如果放入X后尸疆,不破壞堆的特性椿猎,那么插入完成;如果破壞了寿弱,則將空穴朝著根的方向向上冒一步犯眠,直至完成。Java版本:
public class BinaryHeap {
    private static final int CAPACITY = 10;
    private int currentSize;
    private int[] heapArray;

    public void insert(int x){
        if (currentSize == heapArray.length -1){
            // 擴展大小脖捻,暫略
        }
        int hole = ++currentSize;
        for(;hole >1 && x < heapArray[hole];hole /= 2){
            heapArray[hole] = heapArray[hole/2];
        }
        heapArray[hole] = x;
    }
}

????Kotlin版本:

class BinaryHeap {
    private var currentSize = 0
    private var heapArray: IntArray
    fun insert(x: Int) {
        if (currentSize == heapArray.size - 1) {
            // 擴展大小阔逼,暫略
        }
        var hole = ++currentSize
        while (hole > 1 && x < heapArray[hole]) {
            heapArray[hole] = heapArray[hole / 2]
            hole /= 2
        }
        heapArray[hole] = x
    }

    companion object {
        private const val CAPACITY = 10
    }
}
  • 堆的構造,Java版本:
public class BinaryHeap {
    private static final int CAPACITY = 10;
    private int currentSize;
    private int[] heapArray;

    public BinaryHeap(int[] items) {
        currentSize = items.length;
        heapArray = new int[(currentSize + 2) * 11 / 10];

        int i = 1;
        for (int item:items){
            heapArray[i++] = item;
        }
        buildHeap();
    }

    private void buildHeap(){
        for (int i = currentSize/2;i>0;i--){ //對每個非葉節(jié)點依次處理
            percolateDown(i);
        }
    }

    private void percolateDown(int hole){
        int child;
        int tmp = heapArray[hole];

        for (;hole *2 <= currentSize;hole = child){
            child = hole *2;
            if (child != currentSize && heapArray[child +1] < heapArray[child]){
                child ++;
            }

            if (heapArray[child]<tmp){
                heapArray[hole]= heapArray[child];
            }else {
                break;
            }
        }
        heapArray[hole] = tmp;

    }

????Kotlin版本:

class BinaryHeap(items: IntArray) {
    private val currentSize: Int
    private val heapArray: IntArray

    init {
        currentSize = items.size
        heapArray = IntArray((currentSize + 2) * 11 / 10)
        var i = 1
        for (item in items) {
            heapArray[i++] = item
        }
        buildHeap()
    }

    private fun buildHeap() {
        for (i in currentSize / 2 downTo 1) {
            percolateDown(i)
        }
    }

    private fun percolateDown(hole: Int) {
        var hole = hole
        var child: Int
        val tmp = heapArray[hole]
        while (hole * 2 <= currentSize) {
            child = hole * 2
            if (child != currentSize && heapArray[child + 1] < heapArray[child]) {
                child++
            }
            if (heapArray[child] < tmp) {
                heapArray[hole] = heapArray[child]
            } else {
                break
            }
            hole = child
        }
        heapArray[hole] = tmp
    }

    companion object {
        private const val CAPACITY = 10
    }
}

(7)算法設計思想

????本小節(jié)將介紹幾種算法設計思想地沮,有:貪婪算法嗜浮、分治算法、動態(tài)規(guī)劃摩疑、回溯算法危融。

貪婪算法:貪婪算法可謂是大名鼎鼎,使用得非常廣泛雷袋。它分階段地工作吉殃,每個階段,可以認為所做決定是好的楷怒,而不考慮將來的后果蛋勺。這常常在局部達到最優(yōu),“眼下能夠拿到的就拿”鸠删。當算法終止時抱完,如果局部最優(yōu)等于全局最優(yōu),那么算法就是正確的刃泡;否則巧娱,算法得到的就是一個次最優(yōu)解碉怔。代表算法有:Dijkstra算法、Prim算法和Kruskal算法禁添,見 14_業(yè)界經典算法第(3)撮胧、(4)、(5)小節(jié)老翘。

分治算法:將大問題分解成若干子問題芹啥,子問題之間相互獨立,從子問題的解構建原問題的解酪捡。典型的應用如快速排序叁征、歸并排序(見 10_經典排序查找算法第(5)纳账、(7)小節(jié))逛薇,求最大子序列和算法(見 14_業(yè)界經典算法第(1)條中的第三種算法)也用到了它。

動態(tài)規(guī)劃:基本思想和分治算法類似疏虫,也是將大問題分解成若干個子問題(階段)永罚,不同點是這些子問題不是相互獨立的,后一個子問題需要用到前一個子問題的結果卧秘。將遞歸算法改為迭代算法時經常要用到這種思想呢袱。如斐波那契數列的迭代版本(見 《13_經典數學問題算法》第(8)小節(jié)),就是使用兩個參數保留上一次的計算結果翅敌,從而將算法由指數版本改為線性版本羞福;迭代版本實現數學遞推公式(見 《13_經典數學問題算法》第(10)小節(jié))也用到了它。

回溯算法:很多情況下蚯涮,回溯算法相當于窮舉搜索的巧妙實現治专,它是一種試探性的算法,在嘗試過程中尋找問題的解遭顶,如果不滿足张峰,就回溯到別的路徑。見 《12_經典趣題算法》第(10)小節(jié)漁夫捕魚問題棒旗。

(8)紅黑樹

????紅黑樹(red-black樹)是一種特殊的二叉查找樹喘批。對紅黑樹的操作在最壞情況下花費O(log N)時間。紅黑樹的每個節(jié)點铣揉,要么是紅色饶深,要么是黑色。它有如下性質:

  1. 每一個節(jié)點或者是黑色逛拱,或者是紅色敌厘;
  2. 根節(jié)點是黑色的;
  3. 每個葉子節(jié)點是黑色的(指為null的節(jié)點)橘券;
  4. 如果一個節(jié)點是紅色的额湘,那么它的子節(jié)點必須是黑色的卿吐;
  5. 從一個節(jié)點到該節(jié)點的子孫節(jié)點所有路徑包含相同數目的黑色節(jié)點。

????紅黑樹的基本結構和插入操作如下:

public class RBTree<T extends Comparable<T>> {

    public class RBTNode<T extends Comparable<T>> {
        boolean color;        // 顏色
        T key;                // 關鍵字(鍵值)
        RBTNode<T> left;    // 左孩子
        RBTNode<T> right;    // 右孩子
        RBTNode<T> parent;    // 父結點

        public RBTNode(T key, boolean color, RBTNode<T> parent, RBTNode<T> left, RBTNode<T> right) {
            this.key = key;
            this.color = color;
            this.parent = parent;
            this.left = left;
            this.right = right;
        }

    }

    private RBTNode<T> mRoot;    // 根結點
    private static final boolean RED = false;
    private static final boolean BLACK = true;

    //左旋
    private void leftRotate(RBTNode<T> x) {
        // 設置x的右孩子為y
        RBTNode<T> y = x.right;

        // 將 “y的左孩子” 設為 “x的右孩子”锋华;
        // 如果y的左孩子非空嗡官,將 “x” 設為 “y的左孩子的父親”
        x.right = y.left;
        if (y.left != null)
            y.left.parent = x;

        // 將 “x的父親” 設為 “y的父親”
        y.parent = x.parent;

        if (x.parent == null) {
            this.mRoot = y;            // 如果 “x的父親” 是空節(jié)點,則將y設為根節(jié)點
        } else {
            if (x.parent.left == x)
                x.parent.left = y;    // 如果 x是它父節(jié)點的左孩子毯焕,則將y設為“x的父節(jié)點的左孩子”
            else
                x.parent.right = y;    // 如果 x是它父節(jié)點的左孩子衍腥,則將y設為“x的父節(jié)點的左孩子”
        }

        // 將 “x” 設為 “y的左孩子”
        y.left = x;
        // 將 “x的父節(jié)點” 設為 “y”
        x.parent = y;
    }

    //右旋
    private void rightRotate(RBTNode<T> y) {
        // 設置x是當前節(jié)點的左孩子。
        RBTNode<T> x = y.left;

        // 將 “x的右孩子” 設為 “y的左孩子”纳猫;
        // 如果"x的右孩子"不為空的話婆咸,將 “y” 設為 “x的右孩子的父親”
        y.left = x.right;
        if (x.right != null)
            x.right.parent = y;

        // 將 “y的父親” 設為 “x的父親”
        x.parent = y.parent;

        if (y.parent == null) {
            this.mRoot = x;            // 如果 “y的父親” 是空節(jié)點,則將x設為根節(jié)點
        } else {
            if (y == y.parent.right)
                y.parent.right = x;    // 如果 y是它父節(jié)點的右孩子芜辕,則將x設為“y的父節(jié)點的右孩子”
            else
                y.parent.left = x;    // (y是它父節(jié)點的左孩子) 將x設為“x的父節(jié)點的左孩子”
        }

        // 將 “y” 設為 “x的右孩子”
        x.right = y;

        // 將 “y的父節(jié)點” 設為 “x”
        y.parent = x;
    }

    public void insert(T key) {
        RBTNode<T> node=new RBTNode<T>(key,BLACK,null,null,null);

        // 如果新建結點失敗尚骄,則返回。
        if (node != null)
            insert(node);
    }

    //將結點插入到紅黑樹中
    private void insert(RBTNode<T> node) {
        int cmp;
        RBTNode<T> y = null;
        RBTNode<T> x = this.mRoot;

        // 1. 將紅黑樹當作一顆二叉查找樹侵续,將節(jié)點添加到二叉查找樹中倔丈。
        while (x != null) {
            y = x;
            cmp = node.key.compareTo(x.key);
            if (cmp < 0)
                x = x.left;
            else
                x = x.right;
        }

        node.parent = y;
        if (y != null) {
            cmp = node.key.compareTo(y.key);
            if (cmp < 0)
                y.left = node;
            else
                y.right = node;
        } else {
            this.mRoot = node;
        }
        // 2. 設置節(jié)點的顏色為紅色
        node.color = RED;

        // 3. 將它重新修正為一顆二叉查找樹
        insertFixUp(node);
    }

    /*
     * 紅黑樹插入修正函數
     *
     * 在向紅黑樹中插入節(jié)點之后(失去平衡),再調用該函數状蜗;
     * 目的是將它重新塑造成一顆紅黑樹需五。
     */
    private void insertFixUp(RBTNode<T> node) {
        RBTNode<T> parent, gparent;

        // 若“父節(jié)點存在,并且父節(jié)點的顏色是紅色”
        while (((parent = parentOf(node))!=null) && isRed(parent)) {
            gparent = parentOf(parent);

            //若“父節(jié)點”是“祖父節(jié)點的左孩子”
            if (parent == gparent.left) {
                // Case 1條件:叔叔節(jié)點是紅色
                RBTNode<T> uncle = gparent.right;
                if ((uncle!=null) && isRed(uncle)) {
                    setBlack(uncle);
                    setBlack(parent);
                    setRed(gparent);
                    node = gparent;
                    continue;
                }

                // Case 2條件:叔叔是黑色轧坎,且當前節(jié)點是右孩子
                if (parent.right == node) {
                    RBTNode<T> tmp;
                    leftRotate(parent);
                    tmp = parent;
                    parent = node;
                    node = tmp;
                }

                // Case 3條件:叔叔是黑色宏邮,且當前節(jié)點是左孩子。
                setBlack(parent);
                setRed(gparent);
                rightRotate(gparent);
            } else {    //若“z的父節(jié)點”是“z的祖父節(jié)點的右孩子”
                // Case 1條件:叔叔節(jié)點是紅色
                RBTNode<T> uncle = gparent.left;
                if ((uncle!=null) && isRed(uncle)) {
                    setBlack(uncle);
                    setBlack(parent);
                    setRed(gparent);
                    node = gparent;
                    continue;
                }

                // Case 2條件:叔叔是黑色缸血,且當前節(jié)點是左孩子
                if (parent.left == node) {
                    RBTNode<T> tmp;
                    rightRotate(parent);
                    tmp = parent;
                    parent = node;
                    node = tmp;
                }

                // Case 3條件:叔叔是黑色蜜氨,且當前節(jié)點是右孩子。
                setBlack(parent);
                setRed(gparent);
                leftRotate(gparent);
            }
        }

        // 將根節(jié)點設為黑色
        setBlack(this.mRoot);
    }

    private void setRed(RBTNode<T> node) {
        node.color = RED;
    }

    private void setBlack(RBTNode<T> node) {
        node.color = BLACK;
    }

    private boolean isRed(RBTNode<T> node) {
        return node.color == RED;
    }

    private RBTNode<T> parentOf(RBTNode<T> node) {
        return node.parent;
    }
}

????這其中的左旋属百、右旋和AVL樹是非常類似的记劝,不同點在于需要額外處理節(jié)點的parent成員。
????紅黑樹有幾種擴展:BB樹族扰、AA樹厌丑、treep樹,分別介紹如下:

BB樹全稱是二叉B樹(binary B-tree)渔呵,它是一個帶附加條件的紅黑樹:一個節(jié)點最多可以有一個紅兒子怒竿。

AA樹:用level代替color的一種紅黑樹,如果節(jié)點是樹葉扩氢,level = 1耕驰;如果該節(jié)點是紅色的,level等于它的父節(jié)點的level录豺;如果該節(jié)點是黑色的朦肘,level等于父節(jié)點的level減1饭弓。

????結構如下:

    class AANode<T> {
        T element;
        AANode<T> left;
        AANode<T> right;
        int level;
        
        AANode(T element,AANode<T> left,AANode<T> right){
            this.element = element;
            this.left = left;
            this.right = right;
            level = 1;
        }
    }

treep樹:用優(yōu)先級代替color的一種紅黑樹,優(yōu)先級滿足小根堆的性質媒抠。

????結構如下:

    class TreepNode<T>{
        T element;
        TreepNode<T> left;
        TreepNode<T> right;
        int priority;
        
        TreepNode(T element,TreepNode<T> left,TreepNode<T> right){
            this.element = element;
            this.left = left;
            this.right = right;
            priority = random.nextInt();
        }
        
        static Random random = new Random();
    }

???? Over !

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
禁止轉載弟断,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末趴生,一起剝皮案震驚了整個濱河市阀趴,隨后出現的幾起案子,更是在濱河造成了極大的恐慌苍匆,老刑警劉巖刘急,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異浸踩,居然都是意外死亡叔汁,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門民轴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來攻柠,“玉大人,你說我怎么就攤上這事后裸。” “怎么了冒滩?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵微驶,是天一觀的道長。 經常有香客問我开睡,道長因苹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任篇恒,我火速辦了婚禮扶檐,結果婚禮上,老公的妹妹穿的比我還像新娘胁艰。我一直安慰自己款筑,他們只是感情好,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布腾么。 她就那樣靜靜地躺著奈梳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪解虱。 梳的紋絲不亂的頭發(fā)上攘须,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機與錄音殴泰,去河邊找鬼于宙。 笑死浮驳,一個胖子當著我的面吹牛,可吹牛的內容都是我干的捞魁。 我是一名探鬼主播抹恳,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼署驻!你這毒婦竟也來了奋献?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤旺上,失蹤者是張志新(化名)和其女友劉穎瓶蚂,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體宣吱,經...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡窃这,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了征候。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杭攻。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖疤坝,靈堂內的尸體忽然破棺而出兆解,到底是詐尸還是另有隱情,我是刑警寧澤跑揉,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布锅睛,位于F島的核電站,受9級特大地震影響历谍,放射性物質發(fā)生泄漏现拒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一望侈、第九天 我趴在偏房一處隱蔽的房頂上張望印蔬。 院中可真熱鬧,春花似錦脱衙、人聲如沸侥猬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陵究。三九已至,卻和暖如春奥帘,著一層夾襖步出監(jiān)牢的瞬間铜邮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留松蒜,地道東北人扔茅。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像秸苗,于是被迫代替她去往敵國和親召娜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內容