• 算法扩展


    (以下算法出自  算法爱好者  ,由本人精简,拓展学习。版权所有http://www.cnblogs.com/ytlds/

    1、最小栈的实现

      实现一个栈,带有出栈(POP),入栈(PUSH),取最小元素(getMin)三个方法,保证方法时间复杂度为O(1)

      步骤:①创建2个栈A、B,B用来辅助A

         ②第一个元素进栈时,元素下标进入栈B,此时这个元素就是最小元素

         ③当有新元素入栈时,比较该元素与栈A中的最小值,若比其小,将其下标存入栈B

         ④不管入栈出栈,只需取栈B顶的下标

    2、判断2的乘方

      实现一个方法,判断正整数是否是2的乘方,要求性能尽可能高(提示:2的乘方数2、4、8、16......,其二进制中只有一位1,(10B、100B、1000B,再将2的乘方数减去1转成二进制,全是(1B、11B、111B...)))

      步骤:①将2的乘方数与其减1做与运算

         ②结果为0,则为2的乘方

    public  static  boolean  method(int  num){
      return  (num&(num-1))==0;      
    }

    3、找出缺失的整数

      一个无序数组里有若干个正整数,范围从1到100,其中99个整数都出现了偶数次,只有一个整数出现了奇数次(比如1,1,2,2,3,3,4,5,5),如何找到这个出现奇数次的整数

      步骤:遍历整个数组,依次做异或运算。由于异或在位运算时相同为0,不同为1,因此所有出现偶数次的整数都会相互抵消变成0,只有唯一出现奇数次的整数会被留下。

      扩展:一个无序数组里有若干个正整数,范围从1到100,其中98个整数都出现了偶数次,只有两个整数出现了奇数次(比如1,1,2,2,3,4,5,5),如何找到这个出现奇数次的整数

      步骤:遍历整个数组,依次做异或运算。由于数组存在两个出现奇数次的整数,所以最终异或的结果,等同于这两个整数的异或结果。这个结果中,至少会有一个二进制位是1(如果都是0,说明两个数相等,和题目不符)。

        eg:如果最终异或的结果是5,转换成二进制是00000101。此时我们可以选择任意一个是1的二进制位来分析,比如末位。把两个奇数次出现的整数命名为A和B,如果末位是1,说明A和B转为二进制的末位不同,必定其中一个整数的末位是1,另一个整数的末位是0。根据这个结论,我们可以把原数组按照二进制的末位不同,分成两部分,一部分的末位是1,一部分的末位是0。由于A和B的末位不同,所以A在其中一部分,B在其中一部分,绝不会出现A和B在同一部分,另一部分没有的情况。这样我们的问题又回归到了上一题的情况,按照原先的异或解法,从每一部分中找出唯一的奇数次整数即可。

    4、求两个整数的最大公约数

      拓展:

      辗转相除法(前提:两个正整数a和b(a>b),它们的最大公约数等于a除以b的余数c和b之间的最大公约数。比如10和25,25除以10商2余5,那么10和25的最大公约数,等同于10和5的最大公约数)

    public static int getNum(int A, int B){
        int result = 1;
        if(A > B)
            result = gcd(A,B);
        else
            result = gcd(B,A);
            return result;
    }
    
    private static int gcd(int a, int b){
        if(a%b == 0)
            return b;
        else
            return gcd(b, a%b);
    }

      问题:整数过大,a%b取模性能会比较低

      更相减损术(前提:两个正整数a和b(a>b),它们的最大公约数等于a-b的差值c和较小数b的最大公约数比如10和25,25减去10的差是15,那么10和25的最大公约数,等同于10和15的最大公约数)

    public static int gcd(int A, int B){
        if(A == B)
            return  A;
        if(A < B)
            return gcd(B - A, A);
        else
            return gcd(A - B, B);
    }

      问题:两个整数过大求差运算次数较多

      步骤:结合两种算法,通过移位运算

    public static int gcd(int A, int B){
            if (A == B)
                return  A;
            if (A > B)
                return  gcd(B , A);
            else
                if ((!A&1) && (!B&1))
                    return gcd(A>>1, B>>1) << 1;
                else if ((!A&1) && (B&1))
                    return gcd(A>>1, B);
                else if ((A&1) && (!B&1))
                    return gcd(A, B>>1);
                else
                    return gcd(A, A-B);
    }

    5、动态规划(核心:最优子结构、边界、状态转移方程式)

      有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法

      提示:0~9级走法有X种,0~8级走法有Y种,那么F(10) = F(9) + F(8),最后依次递归

    public int getCon(int n){
        if (n < 1)
            return 0;
        if (n == 1)
            return 1;
        if (n == 2)
            return 2;
        return getCon(n-1) + getCon(n-2);
    }

      简单递归的时间复杂度较高,可以使用备用录算法

    public int getCon(int n, HashMap<Integer, Integer> map){
        if (n < 1)
            return 0;
        if (n == 1)
            return 1;
        if (n == 2)
            return 2;
        if (map.contains(n)){
            return map.get(n);
        }else {
            int value = getCon(n-1,map) + getCon(n-2,map);
            map.put(n, value);
            return value;
        }
    }

      备用录算法中哈希表中存入多个结果,继续优化(逆转方向)

    public int getCon(int n){
        if (n < 1)
            return 0;
        if (n == 1)
            return 1;
        if (n == 2)
            return 2;
        int a=1, b=2, temp=0;
        // 每一个结果都只需要用到前面的两个结果
        for (int i=3; i<n; i++){
            temp = a + b;
            a = b;
            b = temp;
        }
        return temp;
    }

      有一个国家发现了5座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人数也不同。参与挖矿工人的总数是10人。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半金矿。要求用程序求解出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿

      提示:分析最优子结构(第五个金矿可以不挖可以挖,所以有两个最优子结构。不挖时即4个金矿10个工人,挖时即4个金矿[10-第5金矿工人]),那么5个金矿最优选择也有两个了

         把金矿数量设为N,工人设为M,金矿黄金量设为数组G[],金矿用工量设为P[],那么存在的最优关系就是F(5,10) = MAX( F(4,10) , F(4,10-P[4])+G[4] )。

         边界:若工人数量不够挖一座金矿,则

          N=1,W>=P[0],F(N,W) = G[0];

          N=1,W<P[0],F(N,W) = 0;

         得到状态转移方程式

           F(n,w) = 0    (n<=1, w<p[0]);

           F(n,w) = g[0]     (n==1, w>=p[0]);

           F(n,w) = F(n-1,w)    (n>1, w<p[n-1])  

           F(n,w) = max(F(n-1,w),  F(n-1,w-p[n-1])+g[n-1])    (n>1, w>=p[n-1])

         实现方法有简单递归、备忘录算法、动态规划(自底向上递推)

    int getMostGold(int n, int w, int[] g, int[] p){
        int[] preResult = new int[p.length];
        int[] results = new int[p.length];
    
        for (int i=0; i<=n; i++){
            if (i < p[0])
                preResult[i] = 0;
            else
                preResult[i] = g[0];
        }
    
        for (int i=0; i<n; i++){
            for (int j=0; j<=w; j++){
                if (j<p[i]){
                    results[j] = preResult[j];
                }else {
                    results[j] = Math.max(preResult[j], preResult[j-p[i]] + g[i]);
                }
            }
            preResult = results;
        }
        return  results[n];
    }

    6、跳跃表——基于有序链表的扩展

      更快查找到一个有序链表的某节点

      步骤:

      取出链表的一层关键节点作为索引,然后再取出二层的关键节点作为索引,最后只剩连个关键节点即可。所以要插入的新节点就需要逐步的去和索引比较确定范围,最后插入即可

      

      问题:插入新节点之后,索引会变的不够用。最终采取随机方式"提拔"上索引。

      

      插入的步骤:

    1. 新节点和各层索引节点逐一比较,确定原链表的插入位置。O(logN)

    2. 把索引插入到原链表。O(1)

    3. 利用抛硬币的随机方式,决定新节点是否提升为上一级索引。结果为“正”则提升并继续抛硬币,结果为“负”则停止。O(logN)

      删除的步骤:

      在索引层找到要删除的节点,那么删除每一层的相同节点

      

    1. 自上而下,查找第一次出现节点的索引,并逐层找到每一层对应的节点。O(logN)

    2. 删除每一层查找到的节点,如果该层只剩下1个节点,删除整个一层(原链表除外)。O(logN)

    7、B-树——MySQL数据库索引主要基于B+树和Hash表

      首先数据库索引都是存储在磁盘上,当数据量大响应的索引大小也增加

      一个m阶的B树有如下几个特征:

        1.根结点至少有两个子女。

        2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m

        3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m

        4.所有的叶子结点都位于同一层。

        5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

      

      这颗树中,看(2,6)节点。该节点满足所有特征:

      

       具体的B-树的插入和删除本人尚不能用言语表达~~遗憾!!!~~

      应用:B-树主要应用于文件系统以及部分数据库索引,比如非关系型数据库MongoDB。而大部分关系型数据库,比如MySQL,则使用B+树作为索引

    8、B+树

      基于B-树的一种变体,有着比B-树更高的查询性能。

      一个m阶的B+树具有如下几个特征:

        1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。

        2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

        3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

       

      B+树相比B-树的优势有三个:①、单一节点存储更多元素,使查询IO次数更少。②、所有查询都要查找到叶子节点,查询性能稳定。3、所有叶子节点形成有序链表,范围查询简便。

      B+树的插入和删除类似B-树。

     9、Base64算法

      Base64算法只支持64个【可打印字符】。Base64可把原来ASCII码的控制字符甚至ASCII码之外的字符转换成可打印的6bit字符(原来的字节码是8bit),如下图:8bit字符串[Man]编码成Base64的[TWFu]

      

      如果有多余的bit位,补0即可,没有匹配的8bit字符,则使用[=]字符填充,如下图:8bit字符串[M]编码成Base64的[TQ==]

      

    10、 MD5算法

      算法过程分为四步:处理原文、设置初始值、循环加工、拼接结果

      具体算法过程请自行参考代码......

    11、SHA算法

      分类:SHA-1算法(已淘汰,可破解)、SHA-2、SHA-3(已问世)

      SHA-2算法又分为多个版本:(信息摘要越长,破解难度越大。但同时耗费性能和占用空间也越高)

        SHA-256:可以生成长度256bit的信息摘要。

        SHA-224:SHA-256的“阉割版”,可以生成长度224bit的信息摘要。

        SHA-512:可以生成长度512bit的信息摘要。

        SHA-384:SHA-512的“阉割版”,可以生成长度384bit的信息摘要。

    12、AES算法

      在Java的java.crypto包有很好包装,具体的密钥、填充方式、工作模式暂且不讨论。

            public static void main(String[] args) {
            String content = "test";
            String password = "123456";
            // 加密
            byte[] encry = encrypt(content,password);
            System.out.println(encry);
            // 解密
            System.out.println(new String(decrypt(encry,password)));
        }
    
        public static byte[] encrypt(String content, String password) {
            KeyGenerator kgen = null;
            try {
                kgen = KeyGenerator.getInstance("AES");
                kgen.init(128, new SecureRandom(password.getBytes()));
                SecretKey secretKey = kgen.generateKey();
                byte[] enCodeFormat = secretKey.getEncoded();
                SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
                Cipher cipher = Cipher.getInstance("AES");// 创建密码器
                cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化
                byte[] byteContent = content.getBytes("utf-8");
                byte[] result = cipher.doFinal(byteContent);
                return result;//加密
            } catch (NoSuchAlgorithmException | InvalidKeyException
                    | NoSuchPaddingException | BadPaddingException
                    | UnsupportedEncodingException | IllegalBlockSizeException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        /**
         * 解密
         *
         * @param content  待解密内容
         * @param password 解密密钥
         * @return
         */
        public static byte[] decrypt(byte[] content, String password) {
            KeyGenerator kgen = null;
            try {
                kgen = KeyGenerator.getInstance("AES");
                kgen.init(128, new SecureRandom(password.getBytes()));
                SecretKey secretKey = kgen.generateKey();
                byte[] enCodeFormat = secretKey.getEncoded();
                SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
                Cipher cipher = Cipher.getInstance("AES");// 创建密码器
                cipher.init(Cipher.DECRYPT_MODE, key);// 初始化
                byte[] result = cipher.doFinal(content);
                return result; // 解密
            } catch (NoSuchAlgorithmException | BadPaddingException
                    | IllegalBlockSizeException | NoSuchPaddingException
                    | InvalidKeyException e) {
                e.printStackTrace();
            }
            return null;
    
        }    

    13、红黑树——是一种自平衡的二叉树

      二叉树的特征:

        1.左子树上所有结点的值均小于或等于它的根结点的值。

        2.右子树上所有结点的值均大于或等于它的根结点的值。

        3.左、右子树也分别为二叉排序树

        

      红黑树除了符合二叉树的基本特征外,还符合下列特性:

        1.节点是红色或黑色。

        2.根节点是黑色。

        3.每个叶子节点都是黑色的空节点(NIL节点)。

        4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

        5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

         

        红黑树从根到叶子的最长路径不会超过最短路径的2倍。当插入或删除节点时破坏规则,需要调整。其中经过具体的旋转(左右旋转)、变色使红黑树重新符合规则。

        应用:JDK的集合类TreeMap和TreeSet底层就是红黑树实现。在Java8中,连HashMap也用到红黑树。

    14、HashMap

      HashMap是一个用于存储Key-Value键值对的集合,每个键值对也叫Entry。这些Entry分散存储在一个数组中,这个数组就是HashMap的主干。

      HashMap数组每个元素初始值都是NULL,我们最常用的方法是Get和Put

      Put的原理:

        HashMap数组的每个元素不止是一个Entry对象,也是一个链表的头节点。每个Entry对象通过Next指针指向它的下个Entry节点。当新插入的Entry映射到冲突的数组位置时,只需插入对应链表(头插法,不插入尾部)。

        

      Get的原理:

        首先使用输入的key做一次Hash映射,得到对应index。此时就会找到对应的Entry链表,再在链表中依次找到key值。

      问题:HashMap默认的初始长度是多少?

      答:初始长度为16,每次自动扩展或是手动初始化时,长度必须是2的幂

        选择16的原因是服务于从key映射到index的Hash算法(因为从Key映射到HashMap数组的位置,会用到一个Hash函数:index = Hash("apple"))。为了实现一个尽量均匀分布的Hash函数,我们通过利用Key的HashCode值来做位运算。

        获得如下Hash函数(Length是HashMap长度):index =  HashCode(Key) &  (Length - 1);

    15、高并发的HashMap

      问题:高并发情况下,为什么HashMap可能出现死锁?Java8中,HashMap的结构有怎样的优化?

      ReHash:是HashMap在扩容时候的一个步骤。当经过多个元素插入,Key映射位置发生冲突几率会逐渐提高。此时HashMap扩展长度,进行Resize。

      Resize的条件是:HashMap.Size   >=  Capacity * LoadFactor

      1.Capacity:HashMap的当前长度。上一期曾经说过,HashMap的长度是2的幂。

      2.LoadFactor:HashMap负载因子,默认值为0.75f。

      Resize步骤:

      1.扩容:创建一个新的Entry空数组,长度是原数组的2倍。

      2.ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。

      答:以上操作在单线程执行并无问题,在多线程环境,会形成死循环。(具体形成原因请查看原漫画!!!)

      通常高并发情况下,通常采用另一个集合类ConcurrentHashMap。这个集合类兼顾了线程安全和性能。

    16、ConcurrentHashMap

      ConcurrentHashMap的结构:

      

      Segment本身相当于一个HashMap对象,ConcurrentHashMap集合中有2的N次方个,共同保存在一个名为segment的数组中。可以说,ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。

      优势:采用了【锁分段技术】,每个Segment就好比一个自治区,读写操作高度自治,互不影响。

      以下是几种并发读写的情形:

        case1:不同Segment的并发写入(不同Segment的写入是可并发执行的)

        

        case2:同一Segment的一写一读(是可以并发执行的)

        

        case3:同一Segment的并发写入(Segment写入需要上锁,对同一Segment的并发写入会被阻塞)

         

        ConcurrentHashMap当中每个Segment各自有锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。

      Get方法:

        1.为输入的Key做Hash运算,得到hash值。

        2.通过hash值,定位到对应的Segment对象

        3.再次通过hash值,定位到Segment当中数组的具体位置。

      Put方法:

        1.为输入的Key做Hash运算,得到hash值。

        2.通过hash值,定位到对应的Segment对象

        3.获取可重入锁

        4.再次通过hash值,定位到Segment当中数组的具体位置。

        5.插入或覆盖HashEntry对象。

        6.释放锁。

       在统计ConcurrentHashMap的Size()时,怎么保证一致性?

      1.遍历所有的Segment。

      2.把Segment的元素数量累加起来。

      3.把Segment的修改次数累加起来。

      4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。

      5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。

      6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。

      7.释放锁,统计结束。

    17、单例模式

      (由于本人以前接触过单例模式,博客中也有描述,就不过多介绍)

      下面代码是懒汉式代码,基本是线程安全(【反射】的方式仍可以构建多个实例对象):

    public class Singleton {
        private Singleton() {}  //私有构造函数
       private volatile static Singleton instance = null;  //单例对象
       //静态工厂方法
       public static Singleton getInstance() {
           if (instance == null) {      //双重检测机制
           synchronized (this){  //同步锁
             if (instance == null) {     //双重检测机制
               instance = new Singleton();
                 }
              }
           }
           return instance;
        }
    }

      其中关于volatile关键字阻止了变量访问前后的指令重排,保证指令执行顺序。也可以保证线程访问的变量值是主内存中的最新值。

      利用反射打破单例模式约束

    //获得构造器
    Constructor con = Singleton.class.getDeclaredConstructor();
    //设置为可访问
    con.setAccessible(true);
    //构造两个不同的对象
    Singleton singleton1 = (Singleton)con.newInstance();
    Singleton singleton2 = (Singleton)con.newInstance();
    //验证是否是不同对象
    System.out.println(singleton1.equals(singleton2));

      防止反射构建对象,只需要用枚举实现单例模式即可,因为JVM会阻止反射获取枚举类的私有构造方法。

    enum Singleton{  
        INSTANCE;  
    }  
      
    public class SingletonTest {  
        public static void main(String[] args) {  
            Singleton s=Singleton.INSTANCE;  
            Singleton s2=Singleton.INSTANCE;  
            System.out.println(s==s2);  
        }  
    } 
    enum Singleton2{  
        INSTANCE{  
            @Override  
            protected void read() {  
                System.out.println("read");  
            }  
      
            @Override  
            protected void write() {  
                System.out.println("write");  
            }  
        };  
        protected abstract void read();  
        protected abstract void write();  
    }  
    public class SingletonTest2 {  
      
        public static void main(String[] args) {  
            Singleton2 s=Singleton2.INSTANCE;  
            s.read();  
        }  
      
    }  

      以上是两种枚举实现单例模式的方式(仅做参考)。但是对于原来的单例方式,想要序列化,但是又想反序列化,则必须实现readResolve方法

    class SingletonB implements Serializable {
    
        private static SingletonB instence = new SingletonB();
    
        private SingletonB() {
        }
    
        public static SingletonB getInstance() {
            return instence;
        }
    
        // 不添加该方法则会出现 反序列化时出现多个实例的问题
        public Object readResolve() {
            return instence;
        }
    }

    18、排序算法

      ①、桶排序(浪费空间,小数不好排序)

      考试分数:5分、2分、7分、5分、3分、8分(总分10分),排序?

      答:新建一个长度为11的数组(长度就是0~10分),考试的分数如果为5分,则在a[5]上加1,当所有考试分数都循环完后,数组的值为0、0、1、1、0、2、0、1、1、0、0,可以看出数组的值即为分数出现的个数,然后for循环输出 a[i]次 数组的下标就是排序后的分数。

      ②、冒泡排序

      将【12、35、99、18、76】从大到小排序

      答:比较【12】和【35】大小,大的往前,所以交换它们,新结构为【35、12、99、18、76】,继续比较第二、三位【12】和【99】,最终当比较4次之后,最小【12】到最后一位。重新开始从第一、二位比较【35】和【99】,最后循环结束。一般需要循环a[n].length - 1次即可成功。

      ③、快速排序

      对“6  1  2 7  9  3  4  5 10  8”排序

      答:以“6”为基准,设置两个变量 i 、 j ,分别指向首尾元素。让 j 先往左移, i 往右移,当 j 指向比基准小的数时stop,当 i 指向比基准大的数时stop,然后交换这两个值,但是 i 、 j 不动,继续按原方向移动,满足条件就交换。当 i 、 j 指向同一个数时stop,此时交换基准“6”和这个数,第一次排序就好了(左边比“6”小,右边比“6”大)。同样的,将“6”的左右两边按刚刚的方法再排序,一直递归下去,就会得到从小到大排序的一组数字了。

    public static int Partition(int[] a,int p,int r){  
      int x=a[r-1];  
      int i=p-1;  
      int temp;  
      for(int j=p;j<=r-1;j++){  
        if(a[j-1]<=x){  
          // 交换(a[j-1],a[i-1]);  
          i++;  
          temp=a[j-1];  
          a[j-1]=a[i-1];  
          a[i-1]=temp;  
        }  
      }  
      //交换(a[r-1,a[i+1-1]);  
      temp=a[r-1];  
      a[r-1]=a[i+1-1];  
      a[i+1-1]=temp;  
      return i+1;  
    }  
    public static void QuickSort(int[] a,int p,int r){  
      if(p<r){  
        int q=Partition(a,p,r);  
        QuickSort(a,p,q-1);  
        QuickSort(a,q+1,r);  
      }  
    }  
    //main方法中将数组传入排序方法中处理,之后打印新的数组  
    public static void main(String[] stra){  
      int[] a={7,10,3,5,4,6,2,8,1,9};  
      QuickSort(a,1,10);  
      for (int i=0;i<a.length;i++)  
      System.out.println(a[i]);  
    }
  • 相关阅读:
    学习进度表
    mysql实现跨库查询
    jmeter分布式(1台Windows,一台Mac,亲测可用互相使用)
    解决appium 连接真机Android 9启动报错.....shell "ps 'uiautomator'
    使用fiddler抓包修改请求/返回的数据
    adb 获取当前界面activity
    使用adb 命令获取APP包名
    jmeter实现登录并设置token为全局变量
    python3 SystemError: Parent module '' not loaded, cannot perform relative import
    adb 运行提示error: cannot connect to daemon
  • 原文地址:https://www.cnblogs.com/ytlds/p/8550141.html
Copyright © 2020-2023  润新知