剑指offer刷题笔记

本文记录了《剑指offer》刷题笔记,包括数组中重复数字、二维数组查找、替换空格、从尾到头打印链表、重建二叉树等题目,详细解析解题思路,并附带相关代码实现。
摘要由CSDN通过智能技术生成

剑指offer刷题笔记

本文题解和图片来自CS-Notes
因为我在看题解时并不能一下就看懂,所以记录一下自己在做题和看题解时的思路过程
在原题解的基础上做了一定的修改

3. 数组中重复的数字

题目描述


在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。

Input:
{2, 3, 1, 0, 2, 5}
Output:
2

审题:
在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。这说明数组的元素值与数组的长度有关,比如数组长为2,那么数组里的元素最小为0,最大为1。

解题思路


对于这种数组元素在[0,n-1]范围内的问题,可以将值为i的元素调整到第i个位置上进行求解。那怎么调整呢?

例如{2, 3, 1, 0, 2, 5}

第一次调整,遍历第一个元素2,判断元素值2是否与下标0相等,不是,则将它与以此元素值为下标的元素交换,也就是将arr[0]与arr[2]交换,即arr[i]与arr[arr[i]]交换,arr[arr[i]]指的就是以当前遍历元素的值为下标的元素。现在这个元素就是1
那么得到的是{1,3,2,0,2,5}

第二次调整,此时下标为1,注意:此时数组已经不是最初的数组,是上一次得到的数组;这次的元素是3,应该与下标为3的元素交换,即与0交换,{1,0,2,3,2,5}

第三次调整, 这次是2,下标也是2,所以不必交换

第四次调整,下标是3,值也为3,不必交换

第五次调整,下标是4,值为2,应该与下标为2的交换,但是此时我们知道下标为2的元素已经是2了,所以在这一步就可以得到重复的数字!

那怎么得到它呢,在上一个判断的基础上再进行判断,如果当前元素要交换到的位置上的元素与当前元素相等,那么它就是一个重复元素。代码表示为arr[i] == arr[arr[i]]。

public boolean duplicate(int numbers[],int length,int [] duplication) {
           if(numbers == null || length == 0)
               return false;
           for(int i = 0;i<length;i++){
               while(numbers[i] != i){
                   if(numbers[i] == numbers[numbers[i]]){
                       duplication[0] = numbers[i];
                       return true;
                   }
                   swap(numbers,i,numbers[i]);
               }
           }
        return false;
    }
    private void swap(int[] arr,int i,int j){
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }

值得注意的是,给出的方法返回值是布尔值而不是重复元素的值,需要返回的元素值传给了数组duplication,而且数组长度作为了一个单独的值传入。另外,把元素的交换放在了判断是否重复之后,减少了不必要的交换。

4.二维数组中的查找

题目描述


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

依照题意,此二维数组可以举例如下图:

在这里插入图片描述

解题思路


该二维数组中的一个数,小于它的数一定在其左边或上边,大于它的数一定在其右边或下边,为了缩小查找范围,我们从右上角或左下角开始找。为什么呢?如果从右上角开始找,那小于它的数一定在其左边,因为上边已经没有数字了,大于它的数一定在其下边,因为右边已经没有数字了,同理,左下角也是一样。

    public boolean Find(int target, int [][] array) {
        if(array == null || array.length == 0 || array[0].length == 0)
            return false;
     int rows = array.length,cols = array[0].length;
     //行数为二维数组的长,列数为二维数组中一行的长,这里每一行长度都相等
     int r = 0,c = cols - 1;
     while(r <=rows - 1 && c >= 0){
         if(array[r][c] > target)
         //如果当前数大于目标值,就往左找
             c--;
         else if(array[r][c] < target)
         //如果当前数小于目标值,就往下找
             r++;
         else
             return true;
     }
        return false;
   }

5.替换空格

题目描述


请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

解题思路


先看简单的情况:
Input:
“A B”

Output:
“A%20B”

可以看到字符串的长度发生了变化,比之前多了两个字符的长度,于是先在尾部填充任意字符,使得字符串的长度等于替换后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。

令p1指向字符串原来的末尾位置,p2指向字符串填充字符后的末尾位置。p1和p2从后向前遍历,当p1遍历到非空格字符时,令p2指向的位置填充相同的字符,当p1遍历到空格字符时,就令p2指向的位置依次填充02%(因为是逆向遍历,所以是逆序填充)。

从后向前遍历是为了在改变p2所指向的内容时,不会影响到p1遍历原来字符串的内容。

public String replaceSpace(StringBuffer str) {
        int p1 = str.length() - 1;
        for(int i = 0; i <= p1; i++){
            //对字符串遍历,当遇到空格就在末尾追加两个空格字符
            if(str.charAt(i) == ' ')
                str.append("  ");
        }
        int p2 = str.length() - 1;
        //现在是追加后的字符串,令p2指向它的末尾
        while(p1 >= 0 && p2 > p1){
            char c = str.charAt(p1--);
            if(c == ' '){
                //如果p1遇到空格,p2就把当前指向的字符替换掉
                str.setCharAt(p2--,'0');
                str.setCharAt(p2--,'2');
                str.setCharAt(p2--,'%');
            }else{
                str.setCharAt(p2--,c);
            }
                
        }
        return str.toString();
    }

在这里插入图片描述

6.从尾到头打印链表

题目描述


输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。
在这里插入图片描述

解题思路


使用递归

要逆序打印链表 1->2->3(3,2,1),可以先逆序打印链表 2->3(3,2),最后再打印第一个节点 1。而链表 2->3 可以看成一个新的链表,要逆序打印该链表可以继续使用求解函数,也就是在求解函数中调用自己,这就是递归函数。

//传入节点1
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
//新建一个list
    ArrayList<Integer> ret = new ArrayList<>();
    //每一次调用递归都创建一个list,用来存储下一个节点调用递归方法返回的list和当前节点的值
    if (listNode != null) {
    	//下一个节点是2,2再调用方法,返回一个list,放入当前list
        ret.addAll(printListFromTailToHead(listNode.next));
        //存入该节点的值
        ret.add(listNode.val);
    }
    return ret;
}

用addAll是因为这个递归方法返回的是一个list集合,而add传入的是一个int值

为什么不先存入当前节点值再存入下一个list呢?因为arraylist是存取有序的,存入顺序与取出顺序一致,而我们需要逆序打印链表,那么越靠前的元素就越在后面输出,例如1->2->3,先存入的应该是2->3这个链表,再存入1的值,这样取出的时候就是先取出2->3,再取出1。

另外,使用递归必须有一个基准条件和一个不断向基准条件推进的过程
这里的基准条件是listNode == null ,不断推进的过程是printListFromTailToHead(listNode.next)

使用头插法

使用头插法可以得到一个逆序的链表。

头结点和第一个节点的区别:

  • 头结点是在头插法中使用的一个额外节点,这个节点不存储值;
  • 第一个节点就是链表的第一个真正存储值的节点,称为首节点
    在这里插入图片描述
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    // 头插法构建逆序链表
    ListNode head = new ListNode(-1);
    //创建头结点,值设为1
    while (listNode != null) {
        ListNode memo = listNode.next;
        //保存传入节点的下一个节点,防止丢失
        listNode.next = head.next;
        //设置当前节点的下一个节点为首节点
        head.next = listNode;
        //把首节点设为当前节点,此时之前的首节点变为当前节点的下一个
        listNode = memo;
        //转到下一个节点继续操作
    }
    // 构建 ArrayList
    ArrayList<Integer> ret = new ArrayList<>();
    head = head.next;
    while (head != null) {
        ret.add(head.val);
        head = head.next;
    }
    return ret;
}

在把首节点设为当前节点之前,需要把当前节点的下一个节点设为首节点,因为需要断开当前节点与旧链表的联系,与新链表连接。

使用栈

栈具有后进先出的特点,在遍历链表时将值按顺序放入栈中,最后出栈的顺序即为逆序。在这里插入图片描述

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    Stack<Integer> stack = new Stack<>();
    while (listNode != null) {
        stack.add(listNode.val);
        listNode = listNode.next;
    }
    ArrayList<Integer> ret = new ArrayList<>();
    while (!stack.isEmpty())
        ret.add(stack.pop());
    return ret;
}

7.重建二叉树

题目描述


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

解题思路


1 找到根节点
2 划分左子树和右子树
前序遍历的结果中,第一个结点一定是根结点,然后在中序遍历的结果中查找这个根结点,根结点左边的就是左子树,根结点右边的就是右子树,递归构造出左、右子树即可

每棵子树的根节点肯定是pre子数组的首元素,所以每次新建一个子树的根节点

递归思想,每次将左右两颗子树当成新的子树进行处理,中序的左右子树索引很好找,前序的开始结束索引通过计算中序中左右子树的大小来计算,然后递归求解,直到startPre>endPre||startIn>endIn说明子树整理完毕。方法每次返回左子树或右子树的根节点

startpre+1,指的是前序遍历左子树的开始位置,即前序遍历根节点的下一个节点。i-startin,i指的是中序遍历根节点的位置,减去中序遍历开始位置即左子树的长度。所以i-startIn+starpre是前序遍历左子树结束的位置。
i-starin是在计算当前节点左子树节点个数;i-starin+starpre是确定当前节点左子树在pre里的位置;根据中序遍历+前序遍历的特点,pre[starpre+1]就是当前节点的左子节点,pre[i-starin+starpre+1]就是当前节点的右子节点;巧妙的是那个判定条件,越界即左右子节点不存在

根据题目给出的前序遍历、中序遍历数组,首先找出根节点,然后再根据中序遍历找到左子树和右子树的长度,分别构造出左右子树的前序遍历和中序遍历序列,最后分别对左右子树采取递归,递归跳出的条件是序列长度为1.

public class Solution {
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        TreeNode root=reConstructBinaryTree(pre,0,pre.length-1,in,0,in.length-1);
        //重载方法,因为需要递归控制子数组的开始和结束索引
        return root;
    }
    private TreeNode reConstructBinaryTree(int [] pre,int startPre,int endPre,int [] in,int startIn,int endIn) {
         
        if(startPre>endPre||startIn>endIn)//递归终止条件
            return null;
        TreeNode root=new TreeNode(pre[startPre]);
        //每次都把左右两棵子树当成新的子树处理
        for(int i=startIn;i<=endIn;i++){
        //在中序遍历数组中遍历查找根节点
            if(in[i]==pre[startPre]){
            //找到根节点在中序遍历的位置,那么左边就是左子树,右边就是右子树
                root.left=reConstructBinaryTree(pre,startPre+1,startPre+i-startIn,in,startIn,i-1);
                //startPre+1传入下一个要比较的先序遍历数组元素
                // i-startIn i就是根节点的位置,减去中序遍历开始的位置,就是左子树的长度
                //加上startPre,就和startPre+1构成一个先序遍历的子数组,里面包含的就是左子树的前序遍历序列
                //startIn是中序遍历开始的位置,i-1是根节点前面一个,左子树的中序遍历序列
                root.right=reConstructBinaryTree(pre,i-startIn+startPre+1,endPre,in,i+1,endIn);
                //上面已经知道,i-startIn就是左子树的长度,加上startPre+1,那么这就是右子树的开始索引
                //endPre是先序遍历的结束索引,那么这个子数组就是右子树的先序遍历序列
                //i+1就是根节点后面一个,那么这个子数组就就是右子树的中序遍历序列
                      break;//如果在中序数组中找到了直接跳出循环
            }
       }
        return root;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值