• 剑指offer学习笔记(一)


    参考书籍:《剑指offer》
    0. 注意不要抱怨的问题
    • 老板太苛刻
    • 同时太难相处
    • 加班太频繁
    • 工资太低
    1.  技术面试看点
    扎实的基础知识:一门语言+数据结构+算法设计
    高质量的代码:多考虑一下边界条件和特殊输入的处理
    清晰的思路:画图使抽象问题形象化,举例使抽象问题具体化,分解使复杂问题简单化。
    优化效率的能力:算法分析设计中充分考虑时间和空间的效率问题。

    2. 编程语言的使用

    synchronized 

    同步块大家都比较熟悉,通过 synchronized 关键字来实现,所有加上synchronized 和 块语句,在多线程访问的时候,同一时刻只能有一个线程能够用

    synchronized 修饰的方法 或者 代码块。 

    volatile

    用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新的值。volatile很容易被误用,用来进行原子性操作。


     问题描述:设计一个类,我们只能生成这个类的一个实例 ——实现Singleton模式

     懒汉式单例

    [java] 
    1. //懒汉式单例类.在第一次调用的时候实例化自己   
    2. public class Singleton {  
    3.     private Singleton() {}  
    4.     private static Singleton single=null;  
    5.     //静态工厂方法   
    6.     public static Singleton getInstance() {  
    7.          if (single == null) {    
    8.              single = new Singleton();  
    9.          }    
    10.         return single;  
    11.     }  
    12. }  

    Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。

    (事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)

    但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全,如果你第一次接触单例模式,对线程安全不是很了解,可以先跳过下面这三小条,去看饿汉式单例,等看完后面再回头考虑线程安全的问题:


    1、在getInstance方法上加同步

    [java] 
    1. public static synchronized Singleton getInstance() {  
    2.          if (single == null) {    
    3.              single = new Singleton();  
    4.          }    
    5.         return single;  
    6. }  

    2、双重检查锁定

    [java] 
    1. public static Singleton getInstance() {  
    2.         if (singleton == null) {    
    3.             synchronized (Singleton.class) {    //synchronized 锁定的关键字
    4.                if (singleton == null) {    
    5.                   singleton = new Singleton();   
    6.                }    
    7.             }    
    8.         }    
    9.         return singleton;   
    10.     }  
    3、静态内部类

    [java] 
    1. public class Singleton {    
    2.     private static class LazyHolder {    
    3.        private static final Singleton INSTANCE = new Singleton();    
    4.     }    
    5.     private Singleton (){}    
    6.     public static final Singleton getInstance() {    
    7.        return LazyHolder.INSTANCE;    
    8.     }    
    9. }    
    这种比上面1、2都好一些,既实现了线程安全,又避免了同步带来的性能影响。



    二、饿汉式单例

    [java] 
    1. //饿汉式单例类.在类初始化时,已经自行实例化   
    2. public class Singleton1 {  
    3.     private Singleton1() {}  
    4.     private static final Singleton1 single = new Singleton1();  //静态成员加载初始化
    5.     //静态工厂方法   
    6.     public static Singleton1 getInstance() {  
    7.         return single;  
    8.     }  
    9. }  
    饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。


    饿汉式和懒汉式区别
    从名字上来说,饿汉和懒汉,
    饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,
    而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。
    另外从以下两点再区分以下这两种方式:
    1、线程安全:
    饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,
    懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。
    2、资源加载和性能:
    饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,
    而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
    至于1、2、3这三种实现又有些区别,
    • 第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的;
    • 第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗;
    • 第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。

    什么是线程安全?
            如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。
    应用以下是一个单例类使用的例子,以懒汉式为例,这里为了保证线程安全,使用了双重检查锁定的方式

    [java] 
    1. public class TestSingleton {  
    2.     String name = null;  
    3.   
    4.         private TestSingleton() {  
    5.     }  
    6.   
    7.     private static volatile TestSingleton instance = null;    

    8.     public static TestSingleton getInstance() {  
    9.            //双重检查锁定
    10.            if (instance == null) {    
    11.              synchronized (TestSingleton.class) {    
    12.                 if (singleton == null) {    
    13.                    singleton = new TestSingleton();   
    14.                 }    
    15.              }    
    16.            }   
    17.            return instance;  
    18.     }  
    19.   
    20.     public String getName() {  
    21.         return name;  
    22.     }  
    23.   
    24.     public void setName(String name) {  
    25.         this.name = name;  
    26.     }  
    27.   
    28.     public void printInfo() {  
    29.         System.out.println("the name is " + name);  
    30.     }  
    31.   
    32. }  
    可以看到里面加了volatile关键字来声明单例对象,既然synchronized已经起到了多线程下原子性、有序性、可见性的作用,为什么还要加volatile呢?

    [java]
    1. public class TMain {  
    2.     public static void main(String[] args){  
    3.         TestStream ts1 = TestSingleton.getInstance();  
    4.         ts1.setName("jason");  
    5.         TestStream ts2 = TestSingleton.getInstance();  
    6.         ts2.setName("0539");  
    7.           
    8.         ts1.printInfo();  
    9.         ts2.printInfo();  
    10.           
    11.         if(ts1 == ts2){  
    12.             System.out.println("创建的是同一个实例");  
    13.         }else{  
    14.             System.out.println("创建的不是同一个实例");  
    15.         }  
    16.     }  
    17. }  



    结论:由结果可以得知单例模式为一个面向对象的应用程序提供了对象惟一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象。对于单例模式的几种实现方式,知道饿汉式和懒汉式的区别,线程安全,资源加载的时机,还有懒汉式为了实现线程安全的3种方式的细微差别。
    3. 数据结构
    3.1字符串处理

        public String replaceSpace(StringBuffer str) {


           //str.reverse();

    //     str.charAt(1);

    //     str.length();

    //     str.replace(i, i+1, "");

           char[] cs = str.toString().toCharArray();

           //System.out.println(cs.length);

           str=new StringBuffer("");

           for(int i=0;i<cs.length;i++){

               if(cs[i]!=' '){

                  str.append(cs[i]);

               }else{

                  str.append("%20");

               }

              

           }

           returnstr.toString();

        }


    3.2 链表

    从尾到头打印链表:输入一个链表,从尾到头打印链表每个节点的值。 

         /**

         递归的方式处理

         @param listNode

         @return

         */

        public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {

           ArrayList<Integer> arrayList = new ArrayList<Integer>();

           if (listNode != null) {

               if (listNode.next != null){

                  //arrayList引用指向最后的结点然后递归的添加内容到arrayList

                  arrayList = printListFromTailToHead(listNode.next);

               }

                 

               arrayList.add(listNode.val);

           }

           // System.out.println(listNode.val);

           return arrayList;

        }

        /**

         栈的方式处理

         @param

         */

        public ArrayList<Integer> printListFromTailToHeadStack(ListNode listNode) {

           ArrayList<Integer> arrayList = new ArrayList<Integer>();

           Stack<ListNode> s = new Stack<ListNode>();

           if(listNode != null){

               while(listNode.next!=null){

                  s.push(listNode);

                  listNode=listNode.next;

               }

               s.push(listNode);

               while(!s.isEmpty()){

                  arrayList.add(s.pop().val);

               }

           }

          

           return arrayList;

        }

     

    3.3 树
     
             树是一种在实际编程中经常遇到的数据结构。它的逻辑很简单:除了根节点之外每个检点都只有一个父节点,根节点没有父节点;除了叶节点之外所有的结点都有一个或者多个子节点,叶节点没有子节点。父节点和子节点之间用引用链接。
    问题描述:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
       
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    /**
         * 重建二叉树
         *
         * @param pre
         * @param in
         * @return
         */
        publicTreeNode reConstructBinaryTree(int[] pre, int[] in) {
     
            if(pre.length == 0|| pre == null|| in == null
                    || pre.length != in.length) {
                returnnull;
            else{
                returnconstructorBinaryTreeCore(pre, in, 0, pre.length - 10,
                        in.length - 1);
            }
        }
     
        privateTreeNode constructorBinaryTreeCore(int[] pre, int[] in,
                intpreStart, intpreEnd, intinStart, intinEnd) {
     
            // 前序遍历的第一个数字就是根节点
            introotValue = pre[preStart];
            TreeNode root = newTreeNode(rootValue);
            root.left = null;
            root.right = null;
     
            if(preStart == preEnd) {
                if(inStart == inEnd && pre[preStart] == in[inStart]) {
                    returnroot;
                else{
                    try{
                        thrownewException("Invalid Input!");
                    catch(Exception e) {
                        e.printStackTrace();
                    }
                }
            }
     
            // 中序遍历找到根结点的位置
            introotInorder = inStart;
            while(rootInorder <= inEnd && in[rootInorder] != rootValue) {
                rootInorder += 1;
            }
     
            if(rootInorder == inEnd && in[rootInorder] != rootValue) {
                try{
                    thrownewException("Invalid Input!");
                catch(Exception e) {
                    e.printStackTrace();
                }
            }
     
            intleftLen = rootInorder - inStart;
            intsplitPre = preStart + leftLen;
            if(leftLen > 0) {
                // 从前序和中序各自的分类位置继续递归重建二叉树
                root.left = constructorBinaryTreeCore(pre, in, preStart + 1,
                        splitPre, inStart, rootInorder - 1);
            }
            if(leftLen < inEnd - inStart) {
                root.right = constructorBinaryTreeCore(pre, in, splitPre + 1,
                        preEnd, rootInorder + 1, inEnd);
     
            }
     
            returnroot;
        }

    在程序设计的过程中,需要明确的知道在树相关的问题的解决上,递归是一个很好的分析策略。
    另外,需要充分的分析好递归边界条件和一些需要传递的参数。          

    3.4 栈和队列
     栈是一个非常常见的数据结构,在计算机领域广泛应用。栈是一种不考虑排序的数据结构,如果想在栈中获取特殊值,需要对栈做出特殊的设计。
     队列也是一种很重要的数据结构。FIFO                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
     问题描述:用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    importjava.util.Stack;
     
    publicclassSolution {
        Stack<Integer> stack1 = newStack<Integer>();
        Stack<Integer> stack2 = newStack<Integer>();
     
        publicvoidpush(intnode) {
            stack1.push(node);
        }
     
        publicintpop() {
            if(stack2.isEmpty()){
                while(!stack1.isEmpty()){
                    stack2.push(stack1.pop());
                }
            }
            if(stack2.isEmpty()){
                try{
                    thrownewException("Queue is Empty");
                catch(Exception e) {
                    e.printStackTrace();
                }
            }
             
            Integer top = stack2.pop();
            returntop;
     
        }
    }


    4 .  算法和数据操作

     二分查找、快速排序、归并排序和堆排序需要着重掌握。 
     有很多的算法都可以使用循环和递归来实现。通常来讲,使用递归的算法看起来比较简洁,但是性能不如基于循环的实现方法。
     位运算可以看作是一种特殊的算法,它是把数字表示成二进制之后对0和1的操作。由于位的运算对象是二进制数字,所以不是很直观。但是总共只有与、或、异或、坐椅、右移五种运算。

     4.1 查找和排序
     查找和排序是程序设计中最常见的算法,查找不外乎就是顺序查找、二分查找、哈希表查找和二叉排序树查找。排序比查找麻烦一些.需要熟练掌握各种排序算法的优劣。一定要对各种算法的特点烂熟于胸,能够从外存消耗、平均时间复杂度和最差时间复杂度方面比较它们的优缺点。
    旋转数组的最小数字
    题目描述
    把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减序列的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    importjava.util.ArrayList;
    publicclassSolution {
        publicintminNumberInRotateArray(int[] array) {
            intlen=array.length;
            if(array==null||len==0){
                return0;
            }
             
            intrear=0;
            inttail=len-1;
            intaimIndex=rear;
            while(array[rear]>=array[tail]){
                if(tail-rear==1){
                    aimIndex=tail;
                    break;
                }
                aimIndex=(rear+tail)/2;
                //如果三个数相等那么只能顺序查找
                if(array[rear]==array[tail]&&array[rear]==array[aimIndex]){
                    returnminInOrder(rear,tail,array);
                }
                 
                if(array[rear]<=array[aimIndex]){
                    rear=aimIndex;
                }elseif(array[tail]>array[aimIndex]){
                    tail=aimIndex;
                }
            }
             
            returnarray[aimIndex];
        }
     
        /**
         * 出现特殊情况 求解到的中间值和两端的值相等
         * @param rear
         * @param tail
         * @param array
         * @return
         */
        privateintminInOrder(intrear, inttail, int[] array) {
            intresult=array[rear];
            for(inti=rear+1;i<=tail;i++){
                if(result>array[i]){
                    result=array[i];
                }
            }
            returnresult;
        }
    }
     4.2 递归和循环
    递归的算法比较简洁,但是比较耗费空间和时间。递归有一些问题:重复计算、栈溢出、空间时间消耗。
    循环算法设计比较麻烦,边界判定比较费劲。
    题目描述:
    大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    publicclassSolution {
       publicintFibonacci(intn) {
            intf1 = 1;
            intf2 = 1;
            intresult = 0;
            if(n<=0){
                return0;
            }
            if(n <= 2) {
                return1;
            }
            for(inti = 3; i <= n; i++) {
                result = f1 + f2;
                f1 = f2;
                f2 = result;
            }
            returnresult;
     
        }
    }

    题目描述:矩形覆盖

    我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    publicclassSolution {
       publicintRectCover(inttarget) {
            if(target<=0){
                return1;
            }
            if(target<=2){
                returntarget;
            }
            intresult=0;
            intf1=1;intf2=2;
            for(inti=3;i<=target;i++){
                result=f1+f2;
                f1=f2;
                f2=result;
            }
             
             
             
            returnresult;
     
        }
    }

     4.3 位运算
      位运算是把数字用二进制表示之后,对每一位上的0或者1的运算。
      输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Solution {
        public int NumberOf1(int n) {
             
             int count = 0;
            while(n!= 0){
                count++;
                n = n & (n - 1);
             }
            return count;
        }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Solution {
        public int NumberOf1(int n) {
             
             int count = 0;
            while(n!= 0){
                count++;
                n = n & (n - 1);
             }
            return count;
        }
    }


    踏实 踏踏实实~
  • 相关阅读:
    SpringBoot应用配置常用相关视图解析器
    从Spring到SpringBoot构建WEB MVC核心配置详解
    集美大学网络15软工团队作业8分数统计
    集美大学网络15软工个人作业4分数统计
    集美大学网络15软工个人作业5分数统计
    集美大学网络15软工团队作业9分数统计
    Alpha冲刺阶段评分发布
    事后诸葛亮作业评分表
    软工网络15个人作业3-评分发布
    软工网络15个人阅读作业1-评分发布
  • 原文地址:https://www.cnblogs.com/mrzhang123/p/5365801.html
Copyright © 2020-2023  润新知