美文网首页
09_算法基本概念(Java、Kotlin描述)

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

作者: 刘加城 | 来源:发表于2023-01-03 17:46 被阅读0次

        本篇文章介绍一些算法里用到的基本概念。

    (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)就陷入死循环了。

    (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)如果栈顶的操作符优先级高,那么将它立即出栈;如果两者优先级一样,也立即出栈;如果栈顶操作符优先级低,则入栈。
            2)对于圆括号 "(" 和 ")",左括号直接入栈;只有遇到右括号,才移走左括号。
        后缀表达式的求值:也使用一个栈,不过处理的不是操作符,而是操作数。对每个字符,如果是操作数,就入栈;如果是操作符,则从栈顶弹出2个操作数并进行计算,计算结果入栈。如此循环,直到处理完表达式中的所有字符。最后,栈中只剩下一个元素,就是表达式的求值结果。

    (4)树的一些基本概念

    • 树是一种非线性结构;
    • 树是一些节点的集合。这个集合可以是空集,如果不是空集,则由根节点和子树组成;
    • 一棵树是N个节点和N-1条边的集合。除了根节点外,每个节点都有一个父亲;
    • 只有根节点没有直接前驱,其他节点有且仅有一个直接前驱;
    • 每个节点可以有任意多个直接后继;
    • 兄弟节点:具有同一个父节点的节点;
    • 节点的度:一个节点所包含子树的数量;
    • 树的度:该树所有节点中最大的度;
    • 节点的层数:根节点的层数是1,依次向下。树是一种层次结构,每个节点都处在一定的层次上。
    • 树的深度:树中节点的最大层数称为树的深度
    • 树的表示法:层次括号法,如( A )表示只有一个根节点A;( A( B , C ) )表示3个节点组成的树,A是根节点,B、C是A的直接后继节点;以此类推;
    • 有序树:树中各节点的子树是按一定次序从左向右安排的;
    • 无序树:与有序树相反;
    • 森林:多棵互不相交的树的集合;
    • 先序遍历:先处理节点,再处理孩子;
    • 后序遍历:先处理孩子,再处理节点;
    • 二叉树:每个节点,子节点数不超过2;
    • 中序遍历:先处理左孩子,再处理节点,最后处理右孩子。针对二叉树而言。

        二叉树的一般结构:

    class BinaryNode {
        Object element;
        BinaryNode left;
        BinaryNode right;
    }
    
    • 表达式树:树叶是操作数,其他是操作符。从后缀表达式可以构造一个表达式树;
    • 二叉查找树:对于每个节点X,它的左子树中所有项的值都小于X中的项,右子树则相反;它的平均深度是O(log N);
    • AVL树:带有平衡条件的二叉查找树,每个节点的左子树和右子树的高度最多相差1;除去插入操作外(惰性删除),所有的树操作都是O(log N)。插入操作可能会破坏AVL树的特性,这种特性需要“旋转”操作来维持。插入一个新节点后,如果节点A的AVL性质被破坏了,那么新节点的位置存在下列几种情况:
    1. A的左儿子的左子树;
    2. A的左儿子的右子树;
    3. A的右儿子的左子树;
    4. A的右儿子的右子树;

        这可以归为两种情况,一种是插入发生在“外边”的情况,即左-左、右-右,另外一种是插入发生在“内部”的情况,即左-右、右-左。第一种情况需要“单旋转”来进行调整;第二种情况需要“双旋转”进行调整。
        Java实现如下:

    public class AVLTree {
        //节点
        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;
        }
    
        /**
         * 更新节点node的高度
         *
         * @param node
         */
        void updateHeight(Node node) {
            int leftHeight = height(node.leftChild);
            int rightHeight = height(node.rightChild);
            node.height = Math.max(leftHeight, rightHeight) + 1;
        }
    
        /**
         * 节点左、右子树的高度差
         * 如果返回值为正,说明右子树较高
         *
         * @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;
        }
    
        //使节点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;
        }
    
        //插入一个新节点
        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);
        }
    
        //删除节点
        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) {//节点只有右孩子
                node = node.rightChild;
            } else if (node.rightChild == null) { //节点只有左孩子
                node = node.leftChild;
            } else { //节点左右孩子都有
                //找到右子树的最小节点
                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的节点,要在深度为d+1的节点之前处理。

    • 满二叉树:除了叶子节点外,其他节点都有2个子节点;

    • 完全二叉树:除最后一层外,其他各层的节点数都是2,且最后一层叶节点按照从左至右的顺序连续存在,只缺最后一层右侧若干节点;

    • B树:当数据结构非常大,内存装不下时,就需要用到B树。它用来平衡cpu计算速度和磁盘访问速度不匹配的问题。

    • M叉查找树:有M路分支的查找树,一颗完全M叉查找树的高度大约是log_mN 。

    • 摊还时间:当M次操作的序列总的最坏情形运行时间为O(M f(N))时,我们就说它的摊还运行时间为O(f(N))。

    • 伸展树:从空树开始,连续M次对树的操作最多花费O(M logN)时间的一种二叉树。伸展树的摊还时间是O(log N)。它的基本想法如下:

        当一个节点被访问后,它就要经过一系列AVL树的旋转被推到根上;如果一个节点很深,那么在其路径上就存在许多相对较深的节点,通过重新构造可以减少对这些节点的进一步访问时间。伸展树在节点过深时,具有平衡这棵树(某种程度上)的作用。在实际的许多应用中,当一个节点被访问时,它很可能不久再被访问。使用伸展树结构,可以减少查找时间。
        此外,伸展树不要求保留高度或平衡信息。根节点、左孩子和右孩子也没有大小关系,它不是查找树。

    (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);
    • 子图:类似于子集合,子图的顶点和边都应该是父图的顶点和边的子集;
    • 路径:路径是图结构中两个顶点之间的连线;路径上边的数量称为路径长度;两个顶点之间的路径可能有多条,路径长度可能不一样;
    • 简单路径:如果一条路径上顶点不重复出现,则称为简单路径;
    • 环:如果一条路径的第一个顶点和最后一个顶点相同,则称之为环,也称为回路;
    • 简单回路:如果第一个顶点和最后一个顶点相同,其余各点都不重复的回路称为简单回路;
    • 连通:如果图结构中,两个顶点之间有路径,则称为这两个顶点是连通的;这两个顶点可以不邻接;
    • 连通图:如果无向图中,任意两个顶点都是连通的,那么这个图便称为连通图;
    • 连通分量:无向图的极大连通子图;对于一个连通图,其连通分量有且只有一个,就是它自身;非连通图,有多个连通分量;
    • 强连通图:如果有向图中,任意两个顶点都是连通的,则称该图为强连通图;注意有向图中边具有方向性;
    • 强连通分量:有向图的极大连通子图称为该图的强连通分量;强连通图只有一个强连通分量,就是它自身;
    • 权:将边表示为某种数值,这个数值就是权;无向图中加入权,称为无向带图权;有向图中加入权,称为有向带图权;在交通图中,权可以代表路径长度;
    • 网:边上带权值的图的另一种名称;
    • 邻接矩阵:保存顶点之间关系的数组;示例如下:
    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 //图的顶点数
        }
    }
    
    • 图的遍历,深度优先算法,Java版本:
        void deepTraOne(Graph g,int n){ //从第n个顶点开始
            int i;
            g.isTrav[n] = 1; //标记该顶点已经访问过
            System.out.println(g.vertex[n]); //输出节点数据
            for (i = 0;i<g.vertexNum;i++){
                if (g.edgeWeight[n][i] != 0 && g.isTrav[i] == 0){
                    deepTraOne(g,i);
                }
            }
        }
        
        //深度优先遍历
        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]) //输出节点数据
            i = 0
            while (i < g.vertexNum) {
                if (g.edgeWeight[n][i] != 0 && g.isTrav[i] == 0) {
                    deepTraOne(g, i)
                }
                i++
            }
        }
    
        //深度优先遍历
        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)堆

    • 优先队列:某些作业应该具有优先权,支持这种特点的队列称为优先队列;
    • 优先队列是一种特殊的数据结构,至少支持两种操作:insert 和 deleteMin,即插入和删除最小者;一种简单实现是:以一个单链表为基础,在表头插入数据,并遍历该链表,以删除最小元;
    • 堆:也叫二叉堆,可以实现优先队列。堆是一颗完全二叉树,一个高为h的完全堆,有2^h到2^h ^+ ^1-1个节点,高是logN ;
    • 堆可以用数组来表示,对于数组中任一位置i(为维持特性,从下标1开始)上的元素,其左儿子在位置2i上,右儿子在位置(2i + 1)上,它的父亲则在i/2上;例如一个包含3元素的堆,A(B,C) ,A是根节点,B、C分别为左右子树,那么对应的数组array[ ]大小为3+2,a[1] = A,a[2] = B, a[3] = C ;其中a[0] 和a[4]为空,不使用。
    • 对于元素为N个的堆来说,N/2刚好是最后一个非叶节点;
    • 大根堆:在堆中,对于每个节点,它的key比左、右孩子的key大或者等,这样的堆称为大根堆;
    • 小根堆:在堆中,对于每个节点,它的key比左、右孩子的key小或者等,这样的堆称为小根堆;
    • 堆的插入:在size-1位置后,再创建一个空的可用位置,简称空穴,这样保持了堆的特性。如果放入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--){ //对每个非叶节点依次处理
                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)算法设计思想

        本小节将介绍几种算法设计思想,有:贪婪算法、分治算法、动态规划、回溯算法。

    贪婪算法:贪婪算法可谓是大名鼎鼎,使用得非常广泛。它分阶段地工作,每个阶段,可以认为所做决定是好的,而不考虑将来的后果。这常常在局部达到最优,“眼下能够拿到的就拿”。当算法终止时,如果局部最优等于全局最优,那么算法就是正确的;否则,算法得到的就是一个次最优解。代表算法有:Dijkstra算法、Prim算法和Kruskal算法,见 14_业界经典算法第(3)、(4)、(5)小节。

    分治算法:将大问题分解成若干子问题,子问题之间相互独立,从子问题的解构建原问题的解。典型的应用如快速排序、归并排序(见 10_经典排序查找算法第(5)、(7)小节),求最大子序列和算法(见 14_业界经典算法第(1)条中的第三种算法)也用到了它。

    动态规划:基本思想和分治算法类似,也是将大问题分解成若干个子问题(阶段),不同点是这些子问题不是相互独立的,后一个子问题需要用到前一个子问题的结果。将递归算法改为迭代算法时经常要用到这种思想。如斐波那契数列的迭代版本(见 《13_经典数学问题算法》第(8)小节),就是使用两个参数保留上一次的计算结果,从而将算法由指数版本改为线性版本;迭代版本实现数学递推公式(见 《13_经典数学问题算法》第(10)小节)也用到了它。

    回溯算法:很多情况下,回溯算法相当于穷举搜索的巧妙实现,它是一种试探性的算法,在尝试过程中寻找问题的解,如果不满足,就回溯到别的路径。见 《12_经典趣题算法》第(10)小节渔夫捕鱼问题。

    (8)红黑树

        红黑树(red-black树)是一种特殊的二叉查找树。对红黑树的操作在最坏情况下花费O(log N)时间。红黑树的每个节点,要么是红色,要么是黑色。它有如下性质:

    1. 每一个节点或者是黑色,或者是红色;
    2. 根节点是黑色的;
    3. 每个叶子节点是黑色的(指为null的节点);
    4. 如果一个节点是红色的,那么它的子节点必须是黑色的;
    5. 从一个节点到该节点的子孙节点所有路径包含相同数目的黑色节点。

        红黑树的基本结构和插入操作如下:

    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的父亲” 是空节点,则将y设为根节点
            } else {
                if (x.parent.left == x)
                    x.parent.left = y;    // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
                else
                    x.parent.right = y;    // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
            }
    
            // 将 “x” 设为 “y的左孩子”
            y.left = x;
            // 将 “x的父节点” 设为 “y”
            x.parent = y;
        }
    
        //右旋
        private void rightRotate(RBTNode<T> y) {
            // 设置x是当前节点的左孩子。
            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的父亲” 是空节点,则将x设为根节点
            } else {
                if (y == y.parent.right)
                    y.parent.right = x;    // 如果 y是它父节点的右孩子,则将x设为“y的父节点的右孩子”
                else
                    y.parent.left = x;    // (y是它父节点的左孩子) 将x设为“x的父节点的左孩子”
            }
    
            // 将 “y” 设为 “x的右孩子”
            x.right = y;
    
            // 将 “y的父节点” 设为 “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. 将红黑树当作一颗二叉查找树,将节点添加到二叉查找树中。
            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. 设置节点的颜色为红色
            node.color = RED;
    
            // 3. 将它重新修正为一颗二叉查找树
            insertFixUp(node);
        }
    
        /*
         * 红黑树插入修正函数
         *
         * 在向红黑树中插入节点之后(失去平衡),再调用该函数;
         * 目的是将它重新塑造成一颗红黑树。
         */
        private void insertFixUp(RBTNode<T> node) {
            RBTNode<T> parent, gparent;
    
            // 若“父节点存在,并且父节点的颜色是红色”
            while (((parent = parentOf(node))!=null) && isRed(parent)) {
                gparent = parentOf(parent);
    
                //若“父节点”是“祖父节点的左孩子”
                if (parent == gparent.left) {
                    // Case 1条件:叔叔节点是红色
                    RBTNode<T> uncle = gparent.right;
                    if ((uncle!=null) && isRed(uncle)) {
                        setBlack(uncle);
                        setBlack(parent);
                        setRed(gparent);
                        node = gparent;
                        continue;
                    }
    
                    // Case 2条件:叔叔是黑色,且当前节点是右孩子
                    if (parent.right == node) {
                        RBTNode<T> tmp;
                        leftRotate(parent);
                        tmp = parent;
                        parent = node;
                        node = tmp;
                    }
    
                    // Case 3条件:叔叔是黑色,且当前节点是左孩子。
                    setBlack(parent);
                    setRed(gparent);
                    rightRotate(gparent);
                } else {    //若“z的父节点”是“z的祖父节点的右孩子”
                    // Case 1条件:叔叔节点是红色
                    RBTNode<T> uncle = gparent.left;
                    if ((uncle!=null) && isRed(uncle)) {
                        setBlack(uncle);
                        setBlack(parent);
                        setRed(gparent);
                        node = gparent;
                        continue;
                    }
    
                    // Case 2条件:叔叔是黑色,且当前节点是左孩子
                    if (parent.left == node) {
                        RBTNode<T> tmp;
                        rightRotate(parent);
                        tmp = parent;
                        parent = node;
                        node = tmp;
                    }
    
                    // Case 3条件:叔叔是黑色,且当前节点是右孩子。
                    setBlack(parent);
                    setRed(gparent);
                    leftRotate(gparent);
                }
            }
    
            // 将根节点设为黑色
            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树是非常类似的,不同点在于需要额外处理节点的parent成员。
        红黑树有几种扩展:BB树、AA树、treep树,分别介绍如下:

    BB树全称是二叉B树(binary B-tree),它是一个带附加条件的红黑树:一个节点最多可以有一个红儿子。

    AA树:用level代替color的一种红黑树,如果节点是树叶,level = 1;如果该节点是红色的,level等于它的父节点的level;如果该节点是黑色的,level等于父节点的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树:用优先级代替color的一种红黑树,优先级满足小根堆的性质。

        结构如下:

        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 !

    相关文章

      网友评论

          本文标题:09_算法基本概念(Java、Kotlin描述)

          本文链接:https://www.haomeiwen.com/subject/ofshcdtx.html