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

2023-01-03  本文已影响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)散列

    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
    }

(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)树的一些基本概念

    二叉树的一般结构:

class BinaryNode {
    Object element;
    BinaryNode left;
    BinaryNode right;
}
  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);
    }
}

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

(5)图的一些基本概念

char vertex = new char[N] ;// 保存顶点信息
int[][] edgeWeight = new int[N][N];// 保存边的权或者连接关系
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 //图的顶点数
    }
}
    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++
        }
    } 

(6)堆

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
    }
}
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 !

上一篇下一篇

猜你喜欢

热点阅读