Java算法总结

字符串

1.字符串翻转

给定一个字符串,长度为n,要求把字符串前面的m个字符移动到字符串的尾部,如把字符串“abcdef”前面的2个字符’a’和’b’(m=2)移动到字符串的尾部,使得原字符串变成字符串“cdefab”。
要求:请写一个函数完成此功能,要求对长度为n的字符串操作的时间复杂度为 O(n),空间复杂度为 O(1)。

常规思路:
1)取前面第i(i >= 1 && i <= m)个字符,然后将后面的m-1个字符移动到前面,字符串第n个位置放第i个字符;
2)i加1,然后重复步骤1,直到i==m时旋转完成。

则上面的例子经过下面两步骤可以完成:
步骤一:ab|cdef -> bcdef|a
步骤二:b|cdefa -> cdef|ab
针对长度为n的字符串来说,假设需要移动m个字符到字符串的尾部,那么总共需要 m x n 次操作,同时设立一个变量保存第一个字符,这样,时间复杂度为O(m * n),空间复杂度为O(1)。

反转法
思路:字符串分成XY两部分,我们可以先对X、Y反转,然后对XY进行一次反转的到YX。这样,我们可以达到题目时间复杂度和空间复杂度的限定条件。
实例:
步骤一:ab|cdef -> ba|fedc
步骤二:ba|fedc -> cdef|ab

算法实现:

/**
     * 字符串翻转
     * 先局部翻转,然后整体翻转
     */
    public static  void rotateStr(char[] strArray, int m){
      if(strArray == null) return;
      if(m < 0 || m >= strArray.length) return;
      revertPartition(strArray, 0, m);
      revertPartition(strArray, m+1, strArray.length -1);
      revertPartition(strArray, 0, strArray.length -1);

    }

    public static void revertPartition(char[] strArray, int start, int end){
        while(start < end){
           char tmp = strArray[start];
           strArray[start++] = strArray[end];
           strArray[end--] = tmp;
        }
    }

测试代码:

public static void main(String[] args){
        char[] strArray = {'a', 'b', 'c', 'd','e','f'};
        rotateStr(strArray, 1);
        System.out.println(String.valueOf(strArray));
        System.exit(0);
    }

说明:测试代码中,m是取值范围是 0 到 n-1,n是字符数组的长度。

题目延伸:句子中单词的翻转,如输入i am a student,输出student a am i。
思路:先对句子中的每个单词进行翻转,然后对整个句子做一次翻转。
算法实现:

 /**
     * 句子翻转
     * 先局部翻转单词,然后整体翻转句子
     */
    public static  void rotateSentence(char[] sentence){
        if(sentence == null) return;
        int start = 0, end = 0;
        for(;end<= sentence.length && start < sentence.length; end++){
            //如果end遍历到空格或者达到句子末尾,则找到一个单词并进行翻转
           if(( end < sentence.length && sentence[end] == 32) || end == sentence.length){
               revertPartition(sentence, start, end-1);
               start =  end + 1;
           }
        }
        //整个句子进行翻转
        revertPartition(sentence, 0, sentence.length -1);

    }

测试代码:

public static void main(String[] args){
        char[] sentence = {'i', ' ', 'a', 'm', ' ', 'a',' ','s','t','u','d','e','n','t'};
        rotateSentence(sentence);
        System.out.println(String.valueOf(sentence));
        System.exit(0);
    }

2.字符串包含

给定两个分别由字母组成的字符串A和字符串B,字符串B的长度比字符串A短。请问,如何最快地判断字符串B中所有字母是否都在字符串A里?
下面,我们给出两种较忧的解法:
计数排序:
假设有一个仅由字母组成字串,让每个字母与一个素数对应,从2开始,往后类推,A对应2,B对应3,C对应5……。遍历第一个字串,把每个字母对应素数相乘,最终,会得到一个整数。
利用上面字母和素数的对应关系,对第二个字符串进行遍历,用上面的整数除以该字母对应的素数。如果有余数,则字符串B的字母不都在字符串A中;如果整个过程中没有余数,则字符串B的字母都在字符串A中。

算法实现:

   //此方法只有理论意义,因为整数乘积很大,有溢出风险
    public static boolean contain(char[] a, char[] b) {
        int[] p = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101};
        int multi = 1;
        for (int i = 0; i < a.length; ++i) {
            int item = p[a[i] - 'a'];
            if (multi % item != 0) {
                multi *= item;
            }
        }
        for (int i = 0; i < b.length; ++i) {
            int item = p[b[i] - 'a'];
            if (multi % item != 0) {
                return false;
            }
        }
        return true;
    }

位运算:
我们可以用26个bit位分别来表示26个字母,对字符串A,用位运算计算出一个“签名”,遍历字符串B中的字母,看在A中是否存在。

算法实现:

public static boolean bitContain(char[] a, char[] b) {
        int hash = 0;
        for (int i = 0; i < a.length; i++) {
            hash |= 1 << (a[i] - 'a');
        }

        for (int i = 0; i < b.length; i++) {
            if ((hash & 1 << (b[i] - 'a')) == 0) {
                return false;
            }
        }
        return true;
    }

位运算的扩展:给定一个int类型的整数 ,输出它的二进制数中 1的个数

算法实现:

  /**
     * 计算一个整数二进制表示的1的个数,假设整数的位数为n,1的个数为m
     *
     * 思想:通过移位来计算1的个数
     * 
     * 时间复杂度:O(n)
     */
    public static  int countOne1(int num){
       int count = 0;
       while(num > 0){
           if((num&1) == 1){
               count ++;
           }
           num >>= 1;
       }
       return  count;

    }


    /**
     * 思想: a&= (a-1)运算会让a的1的总个数减1,因此不停做a&(a-1)来统一1的个数
     * @param num
     * @return
     *时间复杂度:O(m)
     */
    public static int countOne2(int num){
        int count = 0;
        while (num != 0){
            num &= (num-1);
            count ++;
        }
        return count;
    }

2.字符串全排列

输入一个字符串(不存在相同的字符),打印出该字符串中字符的所有排列。
递归实现:
从字符串中依次选出每一个元素,作为排列的第一个元素,然后对剩余的字符串进行全排列,如此递归处理,从而得到字符串的全排列。
例子:输出abc的全排列
1)a固定在第一个位置,求b、c的排列,得到bc,cb,整个字符串的全排列有abc,acb。

2)对于原始字符串abc,a和b交换位置得到字符串bac,b固定在第一个位置,求a、c的排列,得到ac,ca,整个字符串的全排列有bac,bac。

3)对于原始字符串abc,a和c交换位置得到字符串cab,c固定在第一个位置,求a、b的排列,得到ab,ba,整个字符串的全排列有cab,cba。

算法实现:

 //字符串的全排列
    public static void perSort(char[] arrayStr, int from, int to) {
        if (to <= 1) {
            System.out.println(String.valueOf(arrayStr));
            return;
        }
        if(from == to){
            System.out.println(String.valueOf(arrayStr));
        }
        else {
            for (int i = from; i <= to; i++) {
                swap(arrayStr, i, from);
                perSort(arrayStr, from + 1, to);
                swap(arrayStr, i, from);
            }
        }
    }

上述递归算法的时间复杂度是O(n!)

数组

1. 给定一个长度为n的数组,输出最小的k个数。

下面我们给出三种解题思路:
1)输出最小的k个数,我们很容易想到对数组序列进行一次排序,然后输出前k个元素,假设排序用的快排,则时间复杂度是O(nlogn + k)

2)题目没有要求最小的k个数有序,也没要求最后n-k个数有序。既然这样,就没有必要对所有元素进行排序。我们知道选择排序是选择最小的元素放到第一个位置,次小的元素放到第二个位置……n小的元素放到第n个位置。
这里,我们要找的是最小的k个数,所以只需要进行k次选择,然后输出前k个数就可以,时间复杂度是O(nk)。

3)快排的原理是先选择一个基准元素,经过一次partition后,基准元素前面的元素都是比它大的,后面的元素都是比它小的。我们可以根据快排的这一特性来解这道题,假设经过一次partition后,基准元素在数组中的位置是middle,接下来可以分以下三种情况考量:

情况一:k – 1 = middle,说明middle正是我们要找的第k小的元素,且从0到k-1是序列最小的k个数;
情况二:k – 1 < middle,说明我们要找的第k小的元素在数列的0到middle-1中,我们需要递归地进行一次划分查找;
情况三:k – 1 > middle,说明我们要找的第k小的元素在数列的middle+1到n-1中,我们需要递归地进行一次划分查找。

算法实现

 /**
     * 给定一个整形数组序列,找到最小的n个数
     */
public static  void findMinK(int[] element, int start, int end, int k){
        if(end <= k -1) return;
        if(start < end){
            int middle = partition(element, start, end);
            if(k-1 == middle) print(element, k);
            else if(k-1 < middle) findMinK(element, start, middle -1, k);
            else findMinK(element, middle +1 , end, k);
        }
    }


public static int partition(int[] element, int low, int high) {
        int baseElement = element[low];

        while (low < high) {
            while (low < high && baseElement <= element[high]) high--;
            element[low] = element[high];
            while (low < high && baseElement >= element[low]) low++;
            element[high] = element[low];
        }
        element[low] = baseElement;
        return low;
    }

2.数组序列中有一个数字出现的次数超过了数组长度的一半,请找出这个数字。

思路一:
如果数组序列无序,那么我们可以先把数组中所有这些数字先进行排序(至于排序方法可选取最常用的快速排序)。一个数字在数组中的出现次数超过了一半,那么在已排好序的数组索引的N/2处(从零开始编号),就一定是这个数字。因此,排完序后直接输出数组中的第N/2处的数字即可,这个数字即是整个数组中出现次数超过一半的数字,总的时间复杂度是 O(nlogn)*。

思路二:
我们能不能换种思路考虑上面这道题,对于这个数组序列,我们每次去掉两个不同的数,直到最后剩下的数就是我们想找的数。其实,我们完全不用每次去掉两个数,我们只需声明两个变量num,time,其中num分别表示当前遍历的某个数和这个数出现的次数:

1)num初始化为第一个数,time初始化为1;

2)遍历下一个数,如果下一个数和num相等,则time加1,不等,time减1;

3)如果time等于0,则num初始化下一个待遍历的数,同时time初始化为1,直到遍历完成,num并是我们要找的数。

算法实现:

public static void findHalfNum(int[] element) {
        if (element.length < 1) return;
        int num = element[0], time = 1;
        for (int i = 0; i < element.length; i++) {
            if (time == 0) {
                num = element[i];
                time = 1;
            }
            if (num == element[i]) {
                time++;
            } else {
                time--;
            }
        }
        System.out.println("数组中超过一半的数是:" +  num);
    }

上述算法的时间复杂度是:O(n)

二叉树

1.二叉树反转

给你一个二叉树,将二叉树的左右子树进行交换(不使用递归)。
递归算法:

/**
     * 递归实现
     * @param curNode:输入节点
     */
    public static void revertRecursion(Node curNode) {
        if (curNode == null) return;
        //当前节点非空,交换左右子树
        swap(curNode);
        revertRecursion(curNode.left);
        revertRecursion(curNode.right);
    }

非递归算法,用Stack实现:

public static void revertStack(Node root){
       Stack<Node> nodeStack = new Stack<>();
       if(root != null) nodeStack.push(root);
       while(!nodeStack.isEmpty()){
           Node curNode = nodeStack.pop();
           swap(curNode);
           if(curNode.left != null) nodeStack.push(curNode.left);
           if(curNode.right != null) nodeStack.push(curNode.right);
       }
    }

附:Node类、swap方法。
Node类:

public static class Node{
        public Node left;
        public Node right;
        public Object data;
    }

swap方法:

public static  void swap(Node curNode){
        Node tmp = curNode.left;
        curNode.left = curNode.right;
        curNode.right = tmp;
    }

单例模式

单例模式最优方案: 线程安全,并且效率高,代码如下:

public class Singleton {
    //使用volatile保证了多线程访问时instance变量的可见性
    private volatile static Singleton instance;
    // 定义一个私有构造方法
    private Singleton() {

    }
    public static Singleton getInstance() {
        // 对象实例化时与否判断(不使用同步代码块,instance不等于null时,直接返回对象,提高运行效率)
        if (instance == null) {
            //同步代码块(对象未初始化时,使用同步代码块,保证多线程访问时对象在第一次创建后,不再重复被创建)
            synchronized (Singleton.class) {
                //未初始化,则初始instance变量
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

上面的volatile关键字(禁止指令重排)只在JDK1.5以后有效,有一种静态内部类法写法,能够实现延迟加载并保证线程安全。静态内部类单例的写法避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。

public class Singleton {
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
     
    private Singleton(){}
         
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}
    原文作者:湘北南
    原文地址: https://www.jianshu.com/p/116ecec4c151#comments
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞