剑指刷题1--数组与矩阵

本文详细解析了《剑指Offer》中关于数组与矩阵的几道经典题目,包括JZ50数组中重复的数字、二维数组的查找、替换空格、顺时针打印矩阵以及第一个只出现一次的字符位置。解题思路涉及HashSet特性、二分查找、字符串处理和矩阵遍历等方法,旨在帮助读者掌握各种算法和技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考cyc的做题顺序。

JZ50.数组中重复的数字

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

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

Output:
2

解题思路:

1利用HashSet的特性,set存的是value,也就是数组中的元素,set要求不可重复。添加不进去就说明有重复,返回那个值

public int duplicate (int[] numbers) {
        // write code here
        HashSet<Integer> set=new HashSet<>();
        for(int i=0;i<numbers.length;i++){
               if(!set.add(numbers[i])){
            return numbers[i];
        }
     }
        return -1;
      }

2要求时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。

补充:对于这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上进行求解。在调整过程中,如果第 i 位置上已经有一个值为 i 的元素,就可以知道 i 值重复。

public int duplicate (int[] numbers) {
    for(int i=0;i<numbers.length;i++){
            while (numbers[i] !=i){
                if(numbers[i]==numbers[numbers[i]]){//交换的就是这两个元素,每次交换都要看看这两是否相等,如果相等就是重复。不相等就交换
                    return numbers[i];
                }
            swap(numbers,i,numbers[i]);//如果位置i存的数字numbers【i】不是i就一直和位置为numbers【i】存的数字交换,到位置i存的数字numbers【i】是i,意思就是位置0存的是0,但是这样有个bug就是容易陷入死循环,万一就是存的数字里面就是没有0呢
            }
        }//for循环就是遍历数组,目的是1数组下标为i存的元素也为i,2和其他位置比一下,如果存的数字一样,就是有重复
    //这样写还有一个bug就是让位置i存i是有顺序的,
    return -1;//穿的是个元素为int的数组,如果不是,返回-1
    }
     private void swap(int[] nums,int i,int j){
             int t=nums[i];
             nums[i]=nums[j];
             nums[j]=t;
         }//写了一个交换数组位置i和j元素的代码

JZ1.二维数组的查找

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

解法1:暴力查找,毫无技巧,就是挨个遍历。没有利用到题目的隐藏条件。

时间复杂度O(m*n)

空间复杂度O(1)

public boolean Find(int target, int [][] array) {
       // if(array.length==0||array[0].length==0) return false;
   for (int i=0;i<array.length;i++){
     for(int j=0;j<array[0].length;j++){
        if(array[i][j]==target){
            return true;
        }
    }
  }
    return false;
 }

解法2:二维有序数组的二分查找。【有序的数组应该想到二分法】

以左下角为例

【不能照搬一维数组的二分法,得找到合适的二分点】

public boolean Find(int target, int [][] array) {
        //求出行高和列宽
        int rows=array.length;//行高
        int cols=array[0].length;//列宽
        if(rows==0||cols==0){return false;}
        //设置初始的二分点,例如是左下角
        int row=rows-1;
        int col=0;
        while(row>=0&&col<cols){
            if(array[row][col]>target){
                //如果tar小于左下角,就往上一行
                row--;
            }else if(array[row][col]<target){
                //如果tar大于左下角,就往右一列
                col++;
            }else{
                return true;
            }
        }

return false;
    }

JZ2. 替换空格

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

思路:拆分+替换

解法1:调用String的replaceAll方法

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string字符串 
     * @return string字符串
     */
    public String replaceSpace (String s) {
        if(s==null||"".equals(s)){
            return s;
        }else{
            return s.replaceAll(" ","%20");
        }
    }

解法2:StringBuilder,StringBuffer搭配String的split函数

String 类的 split() 方法可以按指定的分割符对目标字符串进行分割,分割后的内容存放在字符串数组中。其中,str 为需要分割的目标字符串;sign 为指定的分割符,可以是任意字符串;limit 表示分割后生成的字符串的限制个数,如果不指定,则表示不限制,直到将整个目标字符串完全分割为止。

调用形式有以下两种:
str.split(String sign)
str.split(String sign,int limit)

例如,按逗号分割

String Colors=“Red,Black,White,Yellow,Blue”;

String[] arr1=Colors.split(","); //不限制元素个数,结果是red black white yellow blue;

String[] arr2=Colors.split(",",3); //限制元素个数为3,结果是red black 和 white,yellow,blue

当指定分割字符串后组成的数组长度(大于或等于 1)时,数组的前几个元素为字符串分割后的前几个字符,而最后一个元素为字符串的剩余部分。

先拆分,把字符串按字符拆分放到数组;然后遍历数组中的每个元素也就是每个字符,如果是空格,就替换【在stringbuilder后面追加%20】;如果不是,就追加该字符;最后输出整个StringBuilder。
public String replaceSpace (String s) {
      if(s==null||"".equals(s)){
          return s;
      }
        StringBuilder sb=new StringBuilder();
        String[] strs=s.split("");
        for(String str:strs){
            if (" ".equals(str)){
                sb.append("%20");
            }else{
                sb.append(str);
            }
        }
        return sb.toString();
    }

解法3;利用char和字符串之间的转换s.charAt(i)和new String(c, 0, c_index);

public String replaceSpace (String s) {
        int length = s.length();
        char[] c = new char[length * 3];//为替换留空间
        int c_index = 0;
        for (int i = 0; i < length; i++) {
        //i用来取字符串,c_index是char的下标。
            // 若为空格
            if (s.charAt(i) == ' ') {
                c[c_index++] = '%';
                c[c_index++] = '2';
                c[c_index++] = '0';
                // 若不为空格
            }else {
                c[c_index++] = s.charAt(i);
            }
        }
        // 字符数组转字符串
        return new String(c, 0, c_index);
    }

JZ19.顺时针打印矩阵

题目;输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下2 X 2矩阵:

输入[[1,2],[3,4]]

返回值[1,2,4,3]

思路:就是不断地收缩矩阵的边界;定义四个变量代表范围,up、down、left、right;

  1. 准备一个数组去存这些数字

  2. 向右走存入整行的值,当存入后,该行再也不会被遍历,代表上边界的 up 加一,同时判断是否和代表下边界的 down 交错

  3. 向下走存入整列的值,当存入后,该列再也不会被遍历,代表右边界的 right 减一,同时判断是否和代表左边界的 left 交错

  4. 向左走存入整行的值,当存入后,该行再也不会被遍历,代表下边界的 down 减一,同时判断是否和代表上边界的 up 交错

  5. 向上走存入整列的值,当存入后,该列再也不会被遍历,代表左边界的 left 加一,同时判断是否和代表右边界的 right 交错

    public ArrayList<Integer> printMatrix(int [][] matrix) {
            ArrayList<Integer> list=new ArrayList<>();
            if(matrix==null||matrix.length==0||matrix[0].length==0){
                  return list;
            }
                //定义四个变量,不断收缩范围,准备一个数组去装这些数字
                int up=0;
                int down=matrix.length-1;
                int left=0;
                int right=matrix[0].length-1;
            while (true){
                //先打印最上面一行,逐个逼近,按照向右,向下,向左和向上
                for(int i=left;i<=right;i++){
                    list.add(matrix[up][i]);
                }
                //上面这一层打印结束,up加1,到了下一行,之后进行判断,再进行打印列
                up++;
                if(up>down){
                    break;
                }
                for(int i=up;i<=down;i++){
                    list.add(matrix[i][right]);
                }
                right--;
                if(left>right){
                    break;
                }
                //打印最下面的行
                for(int i=right;i>=left;i--){
                    list.add(matrix[down][i]);
                }
                down--;
                if(down<up){
                    break;
                }
                //打印左列
                for(int i=down;i>=up;i--){
                    list.add(matrix[i][left]);
                }
                left++;
                if(left>right){
                    break;
                }
            }
            return list;
        }
    

    注意:这四个变量要动起来

    时间复杂度:O(n)
    空间复杂度:O(1)

    修改代码进一步对while的条件进行修改,也就是越界条件;然后在while里面打印下行和左列的时候要加个判断,避免重复访问。

    public ArrayList<Integer> printMatrix(int [][] matrix) {
            ArrayList<Integer> list=new ArrayList<>();
            if(matrix==null||matrix.length==0||matrix[0].length==0){
                  return list;
            }
                //定义四个变量,不断收缩范围,准备一个数组去装这些数字
                int up=0;
                int down=matrix.length-1;
                int left=0;
                int right=matrix[0].length-1;
            while (left<=right&&up<=down){
                //先打印最上面一行,逐个逼近,按照向右,向下,向左和向上
                for(int i=left;i<=right;i++){
                    list.add(matrix[up][i]);
                }
                for(int i=up+1;i<=down;i++){
                    list.add(matrix[i][right]);
                }
                //打印最下面的行,为了避免重复
                if(up<down){
                for(int i=right-1;i>=left;i--){
                    list.add(matrix[down][i]);
                }
               }
                //打印左列,为了避免重复
                if(left<right){
                for(int i=down-1;i>=up+1;i--){
                    list.add(matrix[i][left]);
                }
               }
                up++;
                right--;
                down--;
                left++;
            }
            return list;
        }
    

JZ34.第一个只出现一次的字符位置

题目:在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)

思路:HashMap

看到题目后可以发现,因为他的字符串长度是有限的,而且我们需要找到第一个只出现一次的字符,我们可以首先考虑用Hashmap这样的数据结构来把所有出现过的字符和他们对应的出现次数记录下来。接着,因为我们需要找到第一个只出现一次的字符,所以我们还需要把整个输入的字符串再遍历一遍,然后遍历时我们检测当前字符的出现次数在我们的hashmap里是不是1,如果我们一旦遇到符合条件的字符,直接返回他的位置,因为我们只要第一个符合条件的字符。还有一个注意的点就是区分大小写,因为我们这里是直接存储的字符,所以我们并没有改变他的大小写,所以不用进行额外的操作。

但是如果题目产生变形,不需要区分大小写,我们可以在存储时把所有的字符都变成大写或者小写,放进hashmap的keyset里,然后找的时候也进行相应的变形,就可以了

解法1:所以本题分为两步:

  1. 创建一个hashmap,然后遍历整个字符串一遍,记录下每个字符出现的次数【key存字符,value存次数】

  2. 再次遍历整个字符串,根据我们前面存储的hashmap找哪个字符只出现过一次,直接返回他的位置。

    遍历字符串 for(int i=0;i<str.length();i++)

    str.charAt(i)可以取出字符串中i位置的字符。

    map可以通过get函数,得到key对应的value值。

    **map.keySet()可以获得map的key的集合。**集合包含什么用contains

    import java.util.*;//这句要写,不然hashmap会飘红
    public class Solution {
        public int FirstNotRepeatingChar(String str) {
            HashMap<Character,Integer> map=new HashMap<>();
            //遍历字符串,把每个字符放到map的key,在统计次数,把值放到map的value
            for(int i=0;i<str.length();i++){
                if(!map.keySet().contains(str.charAt(i))){
                    map.put(str.charAt(i),1);
                }else{
                    map.put(str.charAt(i),map.get(str.charAt(i))+1);
                }
            }
            //这样就把每个字符和对应出现的次数存下来了。
            //再遍历整个字符串,看哪个字符首先在map中的value为1
            for(int i=0;i<str.length();i++){
                if(map.get(str.charAt(i))==1){
                    return i;
                }
            }
            return -1;
        }
    }
    

    解法2;考虑的字符范围有限,可以用整型数组替代HashMap。ASCII 码只有 128 个字符,因此可以使用长度为 128 的整型数组来存储每个字符出现的次数。

    import java.util.*;
    public class Solution {
        public int FirstNotRepeatingChar(String str) {
        int[] cnts = new int[128];
            //数组中存的数字是次数,靠字符转换成的整型作为下标,每个字符对应唯一的下标,ascii码。
        for (int i = 0; i < str.length(); i++){
            cnts[str.charAt(i)]++;
        }
        for (int i = 0; i < str.length(); i++){
            if (cnts[str.charAt(i)] == 1)
                return i;
        }
        return -1;
        }
    

    时间复杂度:O(2n), 需要遍历两次字符串
    空间复杂度:O(n)

    解法3:以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。

    这个的思想还是哈希,只不过每个位置的值为0或1,节省空间。

    str.toCharArray()将字符串转为字符数组

    具体过程:

    1. 初始化:bitset<128> b1表示只出现1次, b2表示出现2次以上
    2. 遍历字符串,第一次出现,b1[ch] = 1
    3. 如果第二次出现,b2[ch] = 1
    4. 最后,找出第一个b1[ch] == 1 && b2[ch] == 0的字符
    import java.util.*;
    public class Solution {
        public int FirstNotRepeatingChar(String str) {
        BitSet bs1 = new BitSet(128);//bs1表示出现1次
        BitSet bs2 = new BitSet(128);//bs2表示出现2次以上
            //if条件为1,才执行
            //遍历字符串,通过get字符可以获得该字符对应的是0还是1,1意味着出现;0意味着没出现
            //第一次出现的话,bs1就set函数,自动设置值为1;第二次出现bs2就set
        for (char c : str.toCharArray()) {
            if (!bs1.get(c) && !bs2.get(c))
                bs1.set(c);     // 0 0 -> 1 0之前没出现,现在第一次出现
            else if (bs1.get(c) && !bs2.get(c))
                bs2.set(c);     // 1 0 -> 1 1。之前出现了一次,字符第二次出现
        }//每个c在bs1和bs2都有值
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (bs1.get(c) && !bs2.get(c))  // 1 0,表示出现了一次
                return i;
        }
        return -1;
        }
    }
    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值