剑指offer刷题详细分析:part1:1题——5题

本文详细分析了剑指Offer中的前五个题目,包括二维数组查找、替换空格、从尾到头打印链表、重建二叉树以及用2个栈实现队列。分析了每道题目的解题思路、时间复杂度,并提供了代码实现。
摘要由CSDN通过智能技术生成

z

  • 剑指offer所有题目详解,可访问我的github项目:KongJetLin-offer

  • 目录

  1. Number1:二维数组查找
  2. Number2:替换空格
  3. Number3:从尾到头打印链表
  4. Number4:重建二叉树
  5. Number5:用2个栈实现队列

题目1 二维数组查找

  题目描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

  分析:如下图,我们知道,只能从左下角或者从右上角入手,输入的数大于当前数就往右走,小于当前的数就往上走
在这里插入图片描述
  我们从左上角开始遍历,代码如下:

public static boolean Find(int target, int [][] array)
    {
       //初始化左下角坐标
        int rowIndex = array.length-1;
        int colIndex = 0;

        /**
        需要注意的是,由于colIndex是在增加,而rowIndex是在减少,因此循环判断的时候,要限制的是
         colIndex<总列数,rowIndex>=0,这样才能限制角标不越界!!!不要限制错误!
         */
        while(colIndex<array[0].length && rowIndex>=0)
        {
            if(target == array[rowIndex][colIndex])
                return true;
            else if(target > array[rowIndex][colIndex])
                colIndex++;
            else
                rowIndex--;
        }
        return false;//循环后没有找到则返回false
    }

  此解法的时间复杂度为:O(m+n),其中m,n分别代表行数与列数。


题目2 替换空格

  题目描述
  请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
  题目给出的输入字符串是StringBuffer类型的,这点要注意!

  首先,对一个问题进行说明:

public static void main(String[] args)
    {
        /**
         * 如下,对于StringBuffer的replace方法: replace(int start, int end, String str)
         * 这个方法可以将单个字符的替换为多个字符,如此处将下标为5的空格" "(一个字符)替换为"%20"(3个字符),
         * 我们发现,单个替换为多个字符后,后面的字符会自动后移。
         */
        //创建字符串缓冲区对象
        StringBuffer sb = new StringBuffer();

        //添加数据
        sb.append("hello ").append("world");
        System.out.println("sb:"+sb);//sb:hello world
        System.out.println(sb.length());//11
        //替换
        sb.replace(5, 6, "%20");
        System.out.println("sb:"+sb);//sb:hello%20world
        System.out.println(sb.length());//13

        //------------------------------

        /**
         * 对于String,它的replace方法:replace(char oldChar, char newChar)
         * 也可以将单个字符的替换为多个字符,如下,将空格" "(一个字符)替换为"%20"(3个字符),
         * 我们发现,单个替换为多个字符后,后面的字符会自动后移。
         */
        String str = "hello world";
        System.out.println(str);//hello world
        System.out.println(str.length());//11

        String newStr = str.replace(" ", "%20");
        System.out.println(newStr);//hello%20world
        System.out.println(newStr.length());//13

        /*
        总结:String与StringBuffer的replace方法都可以实现将 较短的字符串 替换为 较长的字符串,
        只不过他们一个是通过索引(StringBuffer),一个是通过字符寻找(String)。
        且替换后,String与StringBuffer的长度都会自动扩大。
         */
    }

方法1:直接在StringBuffer上进行添加,不添加新的数组
  现在我们考虑怎么做替换操作。最直观的做法是从头到尾扫描字符串,每一次碰到空格字符的时候做替换。由于是把1个字符替换成3个字符,我们必须要把空格后面所有的字符都后移两个字节,否则就有两个字符被覆盖了。
  举个例子,我们从头到尾把"We are happy.“中的每一个空格替换成”%20"。为了形象起见,我们可以用一个表格来表示字符串,表格中的每个格子表示一个字符.
在这里插入图片描述
  假设字符串的长度是n,替换一个空格,移动后面的字符所需要的时间复杂度为:O(n)。如果有m个空格字符,那么替换所有空格的总的时间效率是O(n*m)。

public static String replaceSpace(StringBuffer str)
    {
        //首先寻找第一个" "的下标,如果没有找到,会返回-1
        int index = str.indexOf(" ");
        while(index != -1)
        {
            //如果找到空格,进行替换。
            // 根据上面对StringBuffer的分析,可以将一个空格字符" "替换为3个字符长度的"%20",后面的字符会自动后移。
            str.replace(index , index+1 , "%20");
            /*
            我看到有人是从index开始寻找空格,既  index = str.indexOf(" ",index);
            其实不需要这样做,因为前面第一个空格已经被替换,那么我们此时寻找当前第一个空格即可,
            这第一个空格就是之前的第二个空格。
             */
            index = str.indexOf(" ");//继续寻找空格
            
            /*
            这个方法:运行时间:22ms,占用内存:9420k
             */
        }
        return str.toString();
    }

方法2:创建一个新的StringBuffer进行替换
  我们创建一个新的StringBuffer,然后遍历之前的StringBuffer,将前一共StringBuffer的字符添加到新的StringBuffer中。如果遍历到空格,新的StringBuffer添加"%20"。

public static String replaceSpace2(StringBuffer str)
    {
        StringBuffer newSb = new StringBuffer();

        for (int i = 0; i < str.length(); i++)
        {
            char ch = str.charAt(i);
            if(ch == ' ')
            {
                newSb.append("%20");
            }
            else
            {
                newSb.append(ch);
            }
        }
        return newSb.toString();
        //运行时间:20ms,占用内存:9400k
    }

  这个方法只需要遍历原来的StringBuffer即可,时间复杂度为O(n)。

方法3:创建一个新的数组进行替换
  前面方法2是遇到空格就给新的StringBuffer添加"%20",因为StringBuffer不需要初始化长度。我们这里使用数组,数组必须初始化长度,那么我们必须先计算原来StringBuffer中空格的个数,依据个数计算数组长度来创建数组。

public static String replaceSpace3(StringBuffer str)
    {
        int length = str.length();//获取原来的StringBuffer的长度
        int spaceNum = 0;//用于保存空格个数
        //遍历获取原来StringBuffer中空格的个数
        for (int i = 0; i < length ; i++)
        {
            if(str.charAt(i) == ' ')
                spaceNum++;
        }

        int arrlen =  spaceNum*3+(length-spaceNum);//计算新数组的长度
        char[] arr = new char[arrlen];

        //遍历原来的StringBuffer,从尾到头遍历,这样就不需要创建新的变量,可以直接利用length与arrLen。
        while(length>0)
        {
            //如果遍历到的字符不是空格,将其直接添加到数组(注意添加后数组角标先减一再赋值)
            if(str.charAt(length-1) != ' ')
            {//注意,数组下标从arrlen-1开始,因此这里arrlen必须先减一再进行赋值,否则会报错
                arr[--arrlen] = str.charAt(length-1);
            }
            else
            {
                //如果是空格,向数组添加"%20"3个字符(注意添加后数组角标应该减一再赋值)
                //注意,由于是从后向前赋值,应该先复制0,再是2,再是%
                arr[--arrlen] = '0';
                arr[--arrlen] = '2';
                arr[--arrlen] = '%';
            }
            length--;//遍历一次记得将length减1,以便取到下一个字符
        }
        return new String(arr);//这里不能返回toSting,否则打印数组地址,而应该返回利用数组构造一个新的字符串

        //运行时间:20ms, 占用内存:9528k
    }

  同样,这种方法的时间复杂度是O(n)。使用数组的效率并没有比直接创建一个新的StringBuffer进行替换高。

方法4:使用String的replaceAll方法
  这种方法最简单,但是效率并没有那么高。底层应该也是将原来的StringBuffer转换为String,在装换为char数组,然后一个空格一个空格进行替换,复杂度是O(n*m)。

public static String replaceSpace4(StringBuffer str) {
        /*使用String的replaceAll方法
        replaceAll(String regex, String replacement)
          使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
         */
        return str.toString().replaceAll(" ","%20");
        //运行时间:23ms,占用内存:9368k
    }

  总结:方法2利用StringBuffer的特性较为简单,但是这个是java独有的,对于c++以及其他语言,使用方法1与方法4的时间复杂度都是O(n*m)级别,而使用第3种创建新数组的方式,时间复杂度是O(n)。


题目3 从尾到头打印链表

  题目描述:输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
  注意:ArrayList的add()方法是添加到ArrayList的尾部(ArrayList是数组实现,从尾部添加时间复杂度为 O(1) 。另外,ArrayList有下标,可以直接使用下标访问)。 因此,我们想使得ArrayList存储的是 链表从尾到头的顺序 的值,必须从链表的尾部开始add()进ArrayList。

方法1:递归
  递归的时间复杂度是O(n),代码如下:

//递归方法(分为3步骤:求解最基本问题,求解当前问题的下一个较小问题,将较小问题的解整合为当前较大问题的解)
    //方法涵义:将以listNode开头的链表的元素从尾到头赋值给ArrayList
    public ArrayList<Integer> printListFromTailToHead1(ListNode listNode) {

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

        //1、最基本问题:当遍历到链表头结点是null,不给ArrayList添加,直接返回空的ArrayList
        if(listNode == null)
            return arrayList;

        //如果链表头结点不是null

        //求解较小的问题:较小链表将其所有元素从尾到头赋值给ArrayList,返回这个ArrayList
        arrayList = printListFromTailToHead1(listNode.next);
        //将较小问题的解整合为当前较大问题的解:监听当前较大链表的头结点的元素添加到ArrayList的尾部
        arrayList.add(listNode.val);

        return arrayList;//返回添加完毕的ArrayList

        //运行时间:20ms,占用内存:8988k
    }

方法2:利用栈的方法
  使用一个栈,将链表从头到尾的元素存储到栈中,在将栈的元素存储到ArrayList中。但是这种方法会多使用一个栈的存储空间。时间复杂度依然是O(n)

//使用栈
    public ArrayList<Integer> printListFromTailToHead2(ListNode listNode)
    {
        ArrayList<Integer> arrayList = new ArrayList<Integer>();
        Stack<Integer> stack = new Stack<Integer>();

        while(listNode != null)
        {
            stack.push(listNode.val);
            listNode = listNode.next;
        }

        while(!stack.isEmpty())
        {
            arrayList.add(stack.pop());
        }
        return arrayList;
        
        //运行时间:18ms,占用内存:9148k
    }

  当然我们也可以使用数组存储,不过这种方法与使用栈存储差不多,不再赘述。


题目4 重建二叉树

  题目描述:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

  其实题目要求就是:根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

  分析:如下图,前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。然后根据中序遍历的分割结果,我们就可以知道当前根结点左子树的结点数目和右子树的结点数目,通过中序遍历算出当前结点左右子树的结点数目,然后在前序遍历中根据左右子树结点数目,确定左右子树结点的范围,这样就可以在前序遍历中找出找出左右子树的根结点。
  在左右子树中,同样根据其根结点,在中序遍历中将结果分成两部分,同样算出左右子树的结点数目,然后在前序遍历中确定左右子树的范围…
  注意!这里从前序遍历中取出结点用于构建二叉树;中序遍历用于分割确定左右子树的结点数目!

在这里插入图片描述

  • 注:此处参考 CYC2018大佬的分析

  代码如下:

HashMap<Integer , Integer> map = new HashMap();
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        if(pre == null || in == null)
            return null;
        for(int i=0 ; i<in.length ; i++)
            map.put(in[i] , i);
        return reconstruct(pre , 0 , pre.length-1 , 0);
    }

    private TreeNode reconstruct(int[] pre , int pre_left , int pre_right , int inLeft)
    {
        if(pre_left > pre_right)
            return null;

        TreeNode root = new TreeNode(pre[pre_left]);
        int inIndex = map.get(root.val);
        int inLeftLength = inIndex - inLeft;

        root.left = reconstruct(pre , pre_left+1 , pre_left+inLeftLength , inLeft);
        root.right = reconstruct(pre , pre_left+inLeftLength+1 , pre_right , inIndex+1);

        return root;
    }

题目5 用2个栈实现队列

  题目描述
  用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

  分析:入队列,我们就正常地讲元素放入第一个栈中。
在这里插入图片描述
  出队列,我们先把第一个栈里的内容按顺序搬到第二个栈里,负负得正,这样再按顺序从第二个栈出栈的时候,就成了进入第一个栈时的队列顺序,相当于出队列。

在这里插入图片描述
  代码如下:

Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();

    public void push(int node) {
        /*
            入队列,元素正常进入第一个栈。
            这里不需要判断队列是否已经满了,因为如果模拟队列满了,就是栈1满了,也会提示。
         */

        stack1.push(node);
    }

    public int pop() {
        int result = 0;
        //出队列,首先如果栈2还有之前栈1转移过来的元素,直接将元素出栈
        if(!stack2.isEmpty())
        {
            result = stack2.pop();
        }
        else
        {//如果栈1,栈2都为空,那么在运行时出栈失败,抛出运行时异常。
            if(stack1.isEmpty())
            {
                throw new RuntimeException("queue is empty");
            }
            //如果栈1不为空,将栈1所有的元素转到栈2
            while(!stack1.isEmpty())
            {
                stack2.push(stack1.pop());
            }
            //此时栈2必然有元素,将出栈元素赋予result即可
            result = stack2.pop();
        }
        return result;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值