美文网首页
08_经验总结算法(Java、Kotlin描述)

08_经验总结算法(Java、Kotlin描述)

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

        一直以来,对算法既感到赞叹,又感到痛苦。赞叹的地方在于算法的美感,将问题抽象出来,以极为精炼的方式加以解决;有些算法还特别的巧妙,性能之高,让人叹为观止。痛苦的地方在于很多算法不好理解,记住则更难。所以,这里做一些算法记录,以供学习和查阅。部分问题可能会有多个算法实现,对此会列出它们。高效的也有,低效的也在,从对比中加深印象。
        本系列文章在文集《算法》里,分为这几篇:由经验总结的算法、算法相关的基本概念、经典排序查找算法、经典趣题算法、经典数学问题算法和业界经典算法。它们来源于一些算法手册、书籍、源码和自己的归纳总结。
        本篇文章是第一篇,主要介绍一些由经验总结的算法和结构。它们通常并不能抽象出来作为一般、常用的算法,但在实际开发中,底层经常出现它们的身影。

    (1)任意范围的对数求解算法

        在Java中,基本整型都有一定的范围,比如long类型,它的最大值是pow(2 , 64) -1 , 如果超出此范围,则数据会溢出,结果是不确定的。pow(x , y)表示x的y次方。
        而且对于计算的中间值,也会有此限制。比如一个计算的结果是在long的范围内的,但某个中间变量超出了范围,那么计算结果也是未知的。典型的比如两个big long相乘再除以另外一个big long ,期待的结果是在long范围内的,尤其是以人的视角来看。但两个big long相乘超出了long的范围,该中间值就是错误的,再除以另外一个big long,所得结果也是错误的。
        在实际场景中,有这么一个需求:对任意给定long型值x,想知道它对应的字符个数。这在将long型数值转换成对应的字符表示时就要用到。例如Java中的Long类有一个方法stringSize(),里面使用了一个边界值19(来自Java 8的源码,android 33源码中也一样)。对此没有任何的注释或说明。一开始看到这里的时候非常疑惑,为什么是19?下面就来解答这个问题。
        在数学中,这其实是对数求解问题,即:以10为底,x的对数是多少?当然,在本需求中,需要一点变动,字符个数等于x以10为底的对数取整并加1。
        一个关键的问题是如何确定上边界 N ,使得pow(10 , N) 大于Long.MAX_VALUE , 而pow(10 , N - 1)小于或等于Long.MAX_VALUE 。因为中间结果pow(10 , N) 超出了Long的范围,所以不能用long型来存储它。因此,BigInteger派上了用场。
        BigInteger表示的整数,是没有范围限制的。用它来存储整数,可以获得可靠的结果。
        Java版本算法:

    //require n>0 , 返回x = 以10为底n的对数取整加1,即使得10的x次方大于n,10的(x-1)次方小于等于n
        int getLg(long n){
            BigInteger bigN = BigInteger.valueOf(n);
            int pow = 0;
            while(powerOf(pow).compareTo(bigN)<=0){
                pow++;
            }
            return pow;
        }
    
        //require x >= 0 , 返回10的x次方的结果,用BigInteger表示
        BigInteger powerOf(int x){
            BigInteger ten = BigInteger.valueOf(10l);
            BigInteger result = BigInteger.valueOf(1l);
            for(int i =0;i<x;i++){
                result = result.multiply(ten);
            }
            return result;
        }
    

        方法getLg(long n)参数n的类型可以很方便的改为BigInteger,以突破long的限制,扩展至任意范围的整数。这里只是为了直观和方便。

        Kotlin版本:

        //require n>0 , 返回x = 以10为底n的对数取整加1,即使得10的x次方大于n,10的(x-1)次方小于等于n
        fun getLg(n: Long): Int {
            val bigN = BigInteger.valueOf(n)
            var pow = 0
            while (powerOf(pow).compareTo(bigN) <= 0) {
                pow++
            }
            return pow
        }
    
        //require x >= 0 , 返回10的x次方的结果,用BigInteger表示
        fun powerOf(x: Int): BigInteger {
            val ten = BigInteger.valueOf(10L)
            var result = BigInteger.valueOf(1L)
            for (i in 0 until x) {
                result = result.multiply(ten)
            }
            return result
        }
    

        调用getLg(Long.MAX_VALUE),获得的结果是19。也就是说,任意long的值,它对应的字符个数不会超过19 。
        将这个算法扩展一下,不限制以10为底。扩展后的Java版本:

    //require n>0 , 返回x = 以value为底n的对数取整加1,即使得value的x次方大于n,value的(x-1)次方小于等于n
        int getLgOfV(long n,int value){
            BigInteger bigN = BigInteger.valueOf(n);
            int pow = 0;
            while(powerOf(pow,value).compareTo(bigN)<=0){
                pow++;
            }
            return pow;
        }
    
        //require x >=0 , n > 0 ,返回n的x次方的结果,用BigInteger表示
        BigInteger powerOf(int x,int n){
            BigInteger nBig = BigInteger.valueOf(n);
            BigInteger result = BigInteger.valueOf(1l);
            for(int i =0;i<x;i++){
                result = result.multiply(nBig);
            }
            return result;
        }
    

        Kotlin版本:

    //require n>0 , 返回x = 以value为底n的对数取整加1,即使得value的x次方大于n,value的(x-1)次方小于等于n
        fun getLgOfV(n: Long, value: Int): Int {
            val bigN = BigInteger.valueOf(n)
            var pow = 0
            while (powerOf(pow, value).compareTo(bigN) <= 0) {
                pow++
            }
            return pow
        }
    
        //require x >=0 , n > 0 ,返回n的x次方的结果,用BigInteger表示
        fun powerOf(x: Int, n: Int): BigInteger {
            val nBig = BigInteger.valueOf(n.toLong())
            var result = BigInteger.valueOf(1L)
            for (i in 0 until x) {
                result = result.multiply(nBig)
            }
            return result
        }
    

    (2)整数转字符对应的size算法

        如果想将整数输出到屏幕,那么先要将它们转为对应的字符。在Java中,字符用char表示。将不同类型的整数(int 、long等),转成对应的字符char数组,所需数组的大小,就是本次要描述的算法。本小节是基于Java源码(Long类和Integer类)做的一些总结。
        用一个示例来更清晰地描述问题,比如 int i = 32 ; 将它转换成字符数组的结果是: char[] result = { ‘3’ , ‘2’ } ; 所需的size为2,创建char数组时,数组的大小size被赋值为2 。
        先来说明一下数值和对应字符的不同。在Java中,char是采用Unicode编码的。数值3,对应的字符是‘3’,它的Unicode编码(也叫代码点,code point)是51," char c = 51 "和" char c = ‘3’ " 这两种方式是完全等值的。从人的视角看,我们想打印数值3 ;从计算机视角看,它处理的却是51。
        言归正传,该如果来求解这个size呢?先来看看int型的处理,Java版本:

        /**
         * int型的计算,require n >= 0 。 如果n<0,则 1+ stringSize(-n) 。加1是因为减号
         */
        int stringSize(int n){
            //int最大值是21亿多,这个数组的倒数第二个约9.9亿,再加个9,就是约99.9亿,超出了int最大值
            final int[] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,99999999, 999999999, Integer.MAX_VALUE };
            for(int i = 0;;i++){
                if(n <= sizeTable[i]){
                    return i+1;
                }
            }
        }
    

        Kotlin版本:

        fun stringSize(n: Int): Int {
            //int最大值是21亿多,这个数组的倒数第二个约9.9亿,再加个9,就是约99.9亿,超出了int最大值
            val sizeTable =
                intArrayOf(9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Int.MAX_VALUE)
            var i = 0
            while (true) {
                if (n <= sizeTable[i]) {
                    return i + 1
                }
                i++
            }
        }
    

        short 、byte 的类型,是将它们转换成int后,再调用上面的算法。
        针对long型,情况有点不同,它的Java版本算法如下:

        /**
         * long型的计算,require n >= 0 
         */
        int stringSize(long n){
            long p = 10;
            for (int i=1; i<19; i++) {
                if (n < p)
                    return i;
                p = 10*p;
            }
            return 19;
        }
    

        Kotlin版本如下:

        /**
         * long型的计算,require n >= 0
         */
        fun stringSize(n: Long): Int {
            var p: Long = 10
            for (i in 1..18) {
                if (n < p) return i
                p = 10 * p
            }
            return 19
        }
    

        这里用到了值19,关于这个值的计算,可以看上面的第(5)条。
        扩展一下,int型是否也可以用这种边界值的算法呢?答案是可以的,它的边界范围是10,通过自然观察和用上面第(5)条的算法求解都可以得到结果。Java版本如下:

        /**
         * int型的计算,采用long型的类似算法
         */
        int stringSizeLg(int n){
            long p = 10;
            for (int i=1; i<10; i++) {
                if (n < p)
                    return i;
                p = 10*p;
            }
            return 10;
        }
    

        Kotlin版本如下:

       /**
        * int型的计算,采用long型的类似算法
        */
       fun stringSizeLg(n: Int): Int {
           var p: Long = 10
           for (i in 1..9) {
               if (n < p) return i
               p = 10 * p
           }
           return 10
       }
    

    (3)乘10、乘100的高性能算法

        如果想将一个值扩大10倍,再简单不过,乘以10就完事了。对计算机来说,乘以10却有不同的实现方式:乘法和移位(Shift) 。它们最底层的支持来自于电路,乘法需要乘法器电路,它比Shift方式更复杂,性能更低。
        乘法方式:

    int q , r ;
    r = q * 10  ;
    

        Shift方式:

    int q , r ;
    r = (q << 3) + (q << 1) ;
    

        在个人的mac电脑上做了一个实验,将上述两种方式分别放到一个100万次的循环中,计算它们的耗时,结果如下(以纳秒为单位):

    multiply time : 13714938
    shift time :     1668211 
    

        可以看出,性能不在一个量级。对于计算机来说,100万次的计算量算是少的,现在流行的CPU每秒能执行大约10亿次计算(数据来源于《C Primer Plus》第6版 中文版1.4节)。可以想象一下,当计算量非常大时,性能差距会有多大。
        乘100的Shift方式:

    int q , r ;
    r = (q << 6) + (q << 5) + (q << 2))
    

    (4)求商和余数的高性能算法

        通常,求商、求余的做法是这样的:q = i / 10 ; r = i % 10;
        但这种方式并未在Java源码中采用。如Integer类的getChars()方法。尤其是求余数时,没有采用求模运算符,而采用移位运算符。这其中的原因是如(7)条中所说的,Shift方式更高效。采用Shift方式的算法如下:

    int i ;
    if (i < 65536) {
    
        int q = (i * 52429) >>> (16+3); //求商
    
        int r = i - ((q << 3) + (q << 1)); //求余数,i - 10 * q 
    
    }
    

        这里对整数进行了区分,上面是小整数(小于65536)的求解算法。大整数的算法如下:

    int i ;
    if (i >= 65536) {
        int q = i / 100; //求商
    
        int r = i - ((q << 6) + (q << 5) + (q << 2)); //求余数 
    }
    

    (5)整数转字符算法

        数据准备:

    final static char[] digits = {
        '0' , '1' , '2' , '3' , '4' , '5' ,
        '6' , '7' , '8' , '9' 
    };
    
    final static char [] DigitTens = {
        '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
        '1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
        '2', '2', '2', '2', '2', '2', '2', '2', '2', '2',
        '3', '3', '3', '3', '3', '3', '3', '3', '3', '3',
        '4', '4', '4', '4', '4', '4', '4', '4', '4', '4',
        '5', '5', '5', '5', '5', '5', '5', '5', '5', '5',
        '6', '6', '6', '6', '6', '6', '6', '6', '6', '6',
        '7', '7', '7', '7', '7', '7', '7', '7', '7', '7',
        '8', '8', '8', '8', '8', '8', '8', '8', '8', '8',
        '9', '9', '9', '9', '9', '9', '9', '9', '9', '9',
        } ;
    
    final static char [] DigitOnes = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        } ;
    

        先来看int型:
            1)如果大于65536 ,则除数设置为100,所得余数小于100,占2个字符,分别存储个位和十位到适当位置。
            2)如果小于等于65536 ,则除数设置为10 ,余数占1个字符,然后将它存储到适当位置。
        如果是1),则使用数据DigitTens 和 DigitOnes ,前者方便获取十位上的字符,后者方便获取个位字符,例如,余数37 ,将它作为下标,从DigitTens中获取的值刚好是3,而从DigitOnes中获取的值刚好是7。算法如下:

        /**
         * @param i: 是需要转字符的int值,如34
         * @param index:对应的字符个数,如34对应的字符个数为2,index 的值为2 ;如果是负数,则转为相反数后加1
         * @param buf: char[]数组,存储对应的字符
         */
    static void getChars(int i, int index, char[] buf) {
        int q, r;
        int charPos = index;
        char sign = 0;
    
        if (i < 0) {
            sign = '-';
            i = -i;
        }
    
        // Generate two digits per iteration
        while (i >= 65536) {
            q = i / 100;
        // really: r = i - (q * 100);
            r = i - ((q << 6) + (q << 5) + (q << 2));
            i = q;
            buf [--charPos] = DigitOnes[r];
            buf [--charPos] = DigitTens[r];
        }
    
        // Fall thru to fast mode for smaller numbers
        // assert(i <= 65536, i);
        for (;;) {
            q = (i * 52429) >>> (16+3);
            r = i - ((q << 3) + (q << 1));  // r = i-(q*10) ...
            buf [--charPos] = digits [r];
            i = q;
            if (i == 0) break;
        }
        if (sign != 0) {
            buf [--charPos] = sign;
        }
    }
    

        这里用到了上面第(8)条的算法。
        对于long型的数据,基本思想是一样的。只是在while (i >= 65536)前再加上:

    while (i > Integer.MAX_VALUE) {
        q = i / 100;
        // really: r = i - (q * 100);
        r = (int)(i - ((q << 6) + (q << 5) + (q << 2)));
        i = q;
        buf[--charPos] = Integer.DigitOnes[r];
        buf[--charPos] = Integer.DigitTens[r];
    }
    

    (6)字符数组倒序算法

        主要思想:从中间开始,向高位、低位依次取值,并交换它们。Java版如下:

    public char[] reverse(char[] value , int count) {
       int n = count - 1;
       for (int j = (n-1) >> 1; j >= 0; j--) {
           int k = n - j;
           char cj = value[j];
           char ck = value[k];
           value[j] = ck;
           value[k] = cj;
       }
       return value;
    }
    

        Kotlin版本如下:

        fun reverse(value: CharArray, count: Int): CharArray {
            val n = count - 1
            for (j in n - 1 shr 1 downTo 0) {
                val k = n - j
                val cj = value[j]
                val ck = value[k]
                value[j] = ck
                value[k] = cj
            }
            return value
        }
    

    (7)整数转二进制字符串

        介绍三种方式,第一种递归算法,Java版本:

        /**
         * 递归:将int型转为二进制的String
         */
        public static String intToBinary(int n) {
            if (n == 0) {
                return "0";
            } else if (n == 1) {
                return "1";
            } else {
                int remain = n % 2;
                String other = intToBinary(n / 2);
                return other + remain;
            }
        }
    

        Kotlin版本:

        /**
         * 递归:将int型转为二进制的String
         */
        fun intToBinary(n: Int): String {
            return if (n == 0) {
                "0"
            } else if (n == 1) {
                "1"
            } else {
                val remain = n % 2
                val other = intToBinary(n / 2)
                other + remain
            }
        }
    

        第二种,shift循环实现,Java版本:

        /**
         * shift 循环实现,
         */
        public static String intToBinaryShift(int n){
            char[] intChars = new char[32];//Java中int型为32位
            for(int i=31;i>=0;i--,n>>=1){
                intChars[i] = (0x1 & n) == 1 ? '1':'0';
            }
            return new String(intChars);
        }
    

        这种实现开头会包含多余的零,需要注意。
        Kotlin版本:

        /**
         * shift 循环实现,
         */
        fun intToBinaryShift(n: Int): String {
            var n = n
            val intChars = CharArray(32) //Java中int型为32位
            var i = 31
            while (i >= 0) {
                intChars[i] = if (0x1 and n == 1) '1' else '0'
                i--
                n = n shr 1
            }
            return String(intChars)
        }
    

        第三种,借助Java API实现,Java版如下:

        /**
         * api 实现
         */
        public static String intToBinaryW(int n) {
            StringBuilder builder = new StringBuilder();
            while (n > 0) {
                int remain = n % 2;
                builder.append(remain);
                n = n >> 1;
            }
            return builder.reverse().toString();
        }
    

        Kotlin版:

        /**
         * api 实现
         */
        fun intToBinaryW(n: Int): String {
            var n = n
            val builder = StringBuilder()
            while (n > 0) {
                val remain = n % 2
                builder.append(remain)
                n = n shr 1
            }
            return builder.reverse().toString()
        }
    

    (8)字符转int

         将char字符转为对应的int值,例如字符'3'转为数值3。Java版如下:

        /**
         * char转int值
         */
        public static int charToInt(char c) {
            int codePoint = (int) c;//c的代码点,unicode码
            if (codePoint >= '0' && codePoint <= '9') {
                return codePoint - '0';
            }
            return -1;
        }
    

        Kotlin版:

        /**
         * char转int值
         */
        fun charToInt(c: Char): Int {
            val codePoint = c.code //c的代码点,unicode码
            return if (codePoint >= '0'.code && codePoint <= '9'.code) {
                codePoint - '0'.code
            } else -1
        }
    

    (9)文章评论结构

        由看文章、帖子及关联的评论,临时起意,设计一种结构来表示它们。
        特点:一篇文章有作者、标题、内容、发布时间等属性,一条评论有评论者、评论内容、评论时间等属性;一篇文章可以有多个评论,每条评论还可以有多个子评论;每条子评论都有父评论;评论的数量是不限制的,随时可能新增。
        设计:采用树结构,将文章作为根节点,一级评论作为它的直接后继,二级评论作为一级评论的孩子节点,以此类推;文章是一级评论的父节点,一级评论是二级评论的父节点,以此类推;文章没有父节点。
        Java版本:

    import java.util.ArrayList;
    import java.util.List;
    
    public class ArticleCommentStruct {
        String id; //编号
        String authorName; //作者名称
        String iconUrl;//图标地址
        long publishTime; //发布时间
        int type; // 0表示文章,1表示评论
        List<ArticleCommentStruct> childList; //子评论
        ArticleCommentStruct father; //父评论,如果是文章,则为空;一级评论的father是文章
        String content; //文章内容
        String title; //文章标题,只有type=0时才有值
        int hotNum;//热度值,可以用来排序
    
        public ArticleCommentStruct(){
            this(0);
        }
    
        public ArticleCommentStruct(int type){
            assert (type == 0 || type == 1);
            this.type = type;
        }
    
        /**
         * 如果是一篇文章,返回true
         * @return
         */
        public boolean isArticle(){
            return this.type == 0;
        }
    
        /**
         * 如果是一个评论,返回true
         * @return
         */
        public boolean isComment(){
            return this.type == 1;
        }
    
        /**
         * 如果是一级评论,返回true
         * @return
         */
        public boolean isFirstClassComment(){
            if (isComment() && father != null && father.isArticle()){
                return true;
            }
            return false;
        }
    
        /**
         * 添加一个子评论
         * @param child
         */
        public void addChild(ArticleCommentStruct child){
            if (childList == null){
                childList = new ArrayList<>();
            }
            childList.add(child);
        }
    
        /**
         * 创建一个类型为文章的ArticleCommentStruct对象
         * @param authorName 作者名称
         * @param content 文章内容
         * @return
         */
        public static ArticleCommentStruct makeArticle(String authorName,String content){
            ArticleCommentStruct tree = new ArticleCommentStruct();
            tree.authorName = authorName;
            tree.content = content;
            return tree;
        }
    
        /**
         * 创建一个类型为评论的ArticleCommentStruct对象
         * @param authorName 作者名称
         * @param content 评论内容
         * @return
         */
        public static ArticleCommentStruct makeComment(String authorName,String content){
            ArticleCommentStruct comment = new ArticleCommentStruct(1);
            comment.authorName = authorName;
            comment.content = content;
            return comment;
        }
    
    

        其实还可以做的更多,比如根据hotNum排序。一些将高点赞、高热度的评论置顶的做法就要用到它。这里就不再多说了。
        Kotlin版本:

    import kotlin.jvm.JvmOverloads
    import java.util.ArrayList
    
    class ArticleCommentStruct @JvmOverloads constructor(type: Int = 0) {
        var id //编号
                : String? = null
        var authorName //作者名称
                : String? = null
        var iconUrl //图标地址
                : String? = null
        var publishTime //发布时间
                : Long = 0
        var type // 0表示文章,1表示评论
                : Int
        var childList //子评论
                : MutableList<ArticleCommentStruct>? = null
        var father //父评论,如果是文章,则为空;一级评论的father是文章
                : ArticleCommentStruct? = null
        var content //文章内容
                : String? = null
        var title //文章标题,只有type=0时才有值
                : String? = null
        var hotNum //热度值,可以用来排序
                = 0
    
        init {
            assert(type == 0 || type == 1)
            this.type = type
        }
    
        /**
         * 如果是一篇文章,返回true
         * @return
         */
        val isArticle: Boolean
            get() = type == 0
    
        /**
         * 如果是一个评论,返回true
         * @return
         */
        val isComment: Boolean
            get() = type == 1
    
        /**
         * 如果是一级评论,返回true
         * @return
         */
        val isFirstClassComment: Boolean
            get() = if (isComment && father != null && father!!.isArticle) {
                true
            } else false
    
        /**
         * 添加一个子评论
         * @param child
         */
        fun addChild(child: ArticleCommentStruct) {
            if (childList == null) {
                childList = ArrayList()
            }
            childList!!.add(child)
        }
    
        companion object {
            /**
             * 创建一个类型为文章的ArticleCommentStruct对象
             * @param authorName 作者名称
             * @param content 文章内容
             * @return
             */
            fun makeArticle(authorName: String?, content: String?): ArticleCommentStruct {
                val tree = ArticleCommentStruct()
                tree.authorName = authorName
                tree.content = content
                return tree
            }
    
            /**
             * 创建一个类型为评论的ArticleCommentStruct对象
             * @param authorName 作者名称
             * @param content 评论内容
             * @return
             */
            fun makeComment(authorName: String?, content: String?): ArticleCommentStruct {
                val comment = ArticleCommentStruct(1)
                comment.authorName = authorName
                comment.content = content
                return comment
            }
        }
    }
    

         Over !

    相关文章

      网友评论

          本文标题:08_经验总结算法(Java、Kotlin描述)

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