剑指offer-数组\矩阵

数组总结:双指针,有序二分,分治;

1.找出数组中重复的数字。


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

重点是:使用set集合,set中不能储存重复的值

题目链接:力扣

class Solution {
    public int findRepeatNumber(int[] nums) {
        //使用set集合,set中不能储存重复的值
        Set<Integer> set = new HashSet<Integer>();
        for(int num : nums){
            if(set.contains(num)) return num;
            set.add(num);
        }
        return -1;
    }
}

2.在有本身性质的二维数组中,查找值。

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

题目链接:力扣

根据其递增的性质,将正方形立起来,像是二叉树,选取左下角作为遍历的开始。元素向上,值小于该元素,元素向右,值大于该元素

class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        //从数组的最后一行的第一个元素开始搜查找,元素所在的列以上的值小于该元素,元素右边行的值大于该元素
        //1.当遍历到元素时,该元素的值大于target时,行--;元素值小于target,列++
        int i = matrix.length-1;
        int j = 0;
        while(i>=0 && j<matrix[0].length){
            if(matrix[i][j]>target) i--;
            else if(matrix[i][j]<target) j++;
            else return true;
        }
        return false;
    }
}

3.旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为1。  

题目链接:力扣

方法一:暴力查找

class Solution {
    public int minArray(int[] numbers) {
        int min = numbers[0];
        for(int i= 1; i<numbers.length;i++){
            if(numbers[i]<min){
                min = numbers[i];
            }
        }
        return min;
    }
}

方法二:二分法

class Solution {
    public int minArray(int[] numbers) {
        int i =0;
        int j = numbers.length-1;
        //二分法:此数组从开始到旋转点是递增,从旋转点到末尾也是递增,左递增数组,右递增数组,所以用二分法
        //选取中间值和区间的最右边的值比较,
        //跳出循环时i=j
        while(i<j){
            int m = (i+j)/2;
            if(numbers[m]>numbers[j]) i = m+1;
            else if(numbers[m] <numbers[j]) j =m;
            else j--;
        }
        return numbers[j];
    }
}

 4.矩阵中的路径(DFS+剪枝)

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

题目链接:力扣

题目分析:本题是典型的矩阵搜索问题,可以使用深度优先搜索(DFS)+ 剪枝

  •  深度优先搜索:可以理解为暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
  • 剪枝: 在搜索中,遇到 这条路不可能和目标字符串匹配成功 的情况(例如:此矩阵元素和目标字符不同、此元素已被访问),则应立即返回,称之为 可行性剪枝 。

算法流程:

  • 递归参数: 当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。
  • 终止条件:

返回 false: (1) 行或列索引越界 或 (2) 当前矩阵元素与目标字符不同 或 (3) 当前矩阵元素已访问过 ( (3) 可合并至 (2) ) 。
返回 true : k = len(word) - 1 ,即字符串 word 已全部匹配。

  • 递推工作:

标记当前矩阵元素: 将 board[i][j] 修改为 空字符 '' ,代表此元素已访问过,防止之后搜索时重复访问。(当进行上下左右同时搜索时,有可能之前的元素,已经被访问过,所以要把以访问的元素置空,当递归回溯时,再修改为原来的数组值
搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至 res 。
还原当前矩阵元素: 将 board[i][j] 元素还原至初始值,即 word[k] 。

  • 返回值: 返回布尔量 res ,代表是否搜索到目标字符串。
class Solution {
    public boolean exist(char[][] board, String word) {
        char[] chars = word.toCharArray();
        //确定单词word的起始位置
        for(int i = 0;i<board.length;i++){
            for(int j=0;j<board[0].length;j++){
                if(dfs(board,i,j,0,chars)) return true;
            }
        }
        return false;

    }
    //k表示寻找第几个word中的字符

    public boolean dfs(char[][] board,int i,int j,int k,char[] chars){
        //递归回溯的条件
        if(i<0 || i == board.length || j<0 || j== board[0].length || board[i][j] != chars[k]) return false;
        if(k == chars.length-1) return true;
        //继续递归条件,说明此时board[i][j]=chars[k]
        //此时将board[i][j]做遍历标记
        board[i][j]='\0';//这表示空字符
        //继续向该元素的上下左右递归
        boolean res = dfs(board,i-1,j,k+1,chars) || dfs(board,i+1,j,k+1,chars) ||
                        dfs(board,i,j-1,k+1,chars) || dfs(board,i,j+1,k+1,chars);
        board[i][j]=chars[k];
        return res;
                        

    }
}

5.机器人的运动范围-(DFS+剪枝)

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

题目链接:力扣

此题与上题的区别:

  • 递归时指向右和下递归,不用再判断左上,每回溯一次加1;
  • 算i,j的和时,算法,圆圈加点表示求余

class Solution {
    //初始化矩阵
    int m,n;
    // int[][] arr;//定义--这个数组没用,因为已经知道长度了
    int k;
    boolean[][] value;//用于记录哪个格子被记录过
    public int movingCount(int m, int n, int k) {
        this.m = m;
        this.n = n;
        // arr = new int[m][n];//初始化
        this.k = k;
        value = new boolean[m][n];
        return dfs(0,0,0,0);
    }

    //深度优先遍历+剪枝
    public int dfs(int i,int j,int si,int sj){
        //递归回溯条件
        if(i ==m || j==n || si + sj > k || value[i][j]) return 0;//此时这个递归值为0;
        //设置ij点被遍历
        value[i][j] = true;
        return 1+dfs(i+1,j,(i+1) %10 ==0 ?si-8:si+1,sj)+dfs(i,j+1,si,(j+1) %10 ==0 ?sj-8:sj+1);
    }

    
}

6.数组中次数超过一半的数

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

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2

方法一:HashMap

方法二:排序

方法三:摩尔投票法

//方法一:哈希表
class Solution {
    int n;
    public int majorityElement(int[] nums) {
        Map<Integer,Integer> map = new HashMap<>();
        
        for(int num:nums){
            // if(map.get(num) == null) map.put(num,1);
            // else map.put(num,map.get(num)+1);
            map.put(num,map.getOrDefault(num,0)+1);
            if(map.get(num)>nums.length>>1) return num;
        }
       
       return 0;
    }
}

//方法二:排序法
class Solution {
    int x=0;//用于记录最终的x出现次数超过一半的值
    public int majorityElement(int[] nums) {
        //投票法:不同就会为负值,题目中数超过数组的长度一半,最后剩下这个数字
        int vote =0;
       
        for(int num : nums){
            if(vote == 0) x = num;
            vote += x==num ? -1:1;
        }
        return x;
    }
}

7.最小的k个数

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

题目链接:力扣

方法一:用数组自带的排序Arrays.sort(arr);

方法二: 快排的改进

方法三:堆排序(这里也是用的自带的堆排序函数)

方法一:快排
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
      //基于快速排序的,找到第k+1个最小值,那么从数组的最左边开始到k-1索引处
      //正好是确定值,基于快速排序改进,不用向左右连续递归
      if(k >= arr.length) return arr;
      quickSort(arr,k,0,arr.length-1);
      return Arrays.copyOf(arr,k);
      
    }
    public void quickSort(int[] arr,int k,int l,int r){
          int i = l;
          int j = r;
          //参考值是arr[l]
          while(i<j){
              //这里是等于,防止了死循环,这里好像必须先向再右边寻找

              while(i<j && arr[j]>=arr[l]){
                  j--;
              }
              while(i<j && arr[i]<=arr[l]){
                  i++;
              }
              swap(arr,i,j);
            }
            swap(arr,i,l);//这里防止栈溢出
            //如果划分值arr[l],l=k,此时划分完左边是最小的k个数
            if(k>i) quickSort(arr,k,i+1,r);
            if(k<i) quickSort(arr,k,l,i-1);
            return;//这里k=i时退出循环
        }

      public void swap(int[] arr,int i,int j){
          int tem = arr[i];
          arr[i] = arr[j];
          arr[j]= tem;
        }
}

8.把数组排成最小的数

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

输入: [3,30,34,5,9]
输出: "3033459"

题目链接:力扣

 解析:

此题求拼接起来的最小数字,本质是一个排序问题。设数组nums中任意两数字的字符串为x和y,

则判断顺序为(最小数字)

  • 若x+y>y+x,则x"大于"y,要求最小数,此时y应该排在x前面
  • 反之,x"小于“y,此时应该排在x的y前面。

算法流程:

1.初始化:字符串列表strs,保存各数字的字符串格式

2.应用以上”排序判断规则“,对strs执行排序;

3.返回值:拼接strs中的所有字符串,并返回

class Solution {
    public String minNumber(int[] nums) {
        //1.将数组转为字符数组
        String[] strs = new String[nums.length];
        for(int i =0;i<nums.length;i++){
            strs[i]=String.valueOf(nums[i]);
        }
        //2.快排
        quickSort(strs,0,strs.length-1);
        //3.将字符数组转为字符串,此时借助stringbuffer
        StringBuilder res = new StringBuilder();
        for(String num:strs){
            res.append(num);
        }
        return res.toString();

    }
    //根据本题特定的规则进行快排
    public void quickSort(String[] strs,int l,int r){
        //跳出递归的条件
        if(l>=r) return;
        int i = l;
        int j = r;
        String tem=strs[i];
        while(i<j){
            
            while(i<j && (strs[j]+strs[l]).compareTo(strs[l]+strs[j])>=0) j--;
            while(i<j && (strs[i]+strs[l]).compareTo(strs[l]+strs[i])<=0) i++;
            //交换两个值
            tem = strs[i];
            strs[i]=strs[j];
            strs[j]=tem;
            
        }
        //交换l,i值
        strs[i]=strs[l];
        strs[l]=tem;
        //向左右递归
        quickSort(strs,l,i-1);
        quickSort(strs,i+1,r);
        
    }
}

8.一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

题目链接:力扣

按位于 & ,这里要加括号 

class Solution {
    public int[] singleNumbers(int[] nums) {
        //异或规律 nums中的值异或结果是:剩下两个不同的数字异或
        int res = 0;//最后异或结果
        int m = 1;//用于移位的数
        int x =0;
        int y=0;
        //遍历数组提取出异或值
        for(int num : nums){
            res = res^num;
        }
        //寻找从右开始寻找第一个res为1的位
        while((res & m) ==0){//这里只能是判断是不是0,
            m=m<<1;//1左移
        }

        for(int num : nums){
            if((num & m)==0)  x=x^num;
            else y=y^num;
        }

        return new int[]{x,y};

    }
}

9.在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

题目链接:力扣

方法一:map

class Solution {
    public int singleNumber(int[] nums) {
        //用哈希表
        Map<Integer,Integer> map = new HashMap<>();
        for(int num:nums){
            if(map.get(num) !=null){
                 map.put(num,map.get(num)+1);
            }else{
                map.put(num,1);
            }
           

        }
        //遍历map数组
        for(Map.Entry<Integer,Integer> entry : map.entrySet()){
            if(entry.getValue() ==1) return entry.getKey();
            
        }
        return 0;
        
    }
}

 方法二:根据3个数相同,统计二进制数对应位数 1的个数

class Solution {
    public int singleNumber(int[] nums) {
        //利用所有数字出现了3次为标准
        //根据题目要求 最长是31为
        int[] counts = new int[32];
        //统计所有数字每位 1 出现的次数,
        for(int i =0;i<nums.length;i++){
            //统计nums[i]在相应位置出现的次数
            for(int j =0;j<32;j++){
                counts[j] += nums[i] & 1;//是从低位开始的
                //无符号右移
                nums[i] >>>=1; 
            }
        }
        int res =0;
        //对counts中的每个值对3取余
        for(int i =0;i<32;i++){
            res<<=1;//左移一位恢复值;
            res |= counts[31-i]%3;//高位倒着来,或运算是加入数组的1
        }
        
        return res;
    }
}

10.输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

题目链接:力扣

双指针遍历

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int r = 0;
        int i=0;
        for(; i<nums.length;i++){
            if(nums[i]>=target){
                r = i;
                break;
            }
        }
        if(i==nums.length) r=nums.length-1;
        //int r = nums.length-1;
        int l =0;
        while(r>l){
            if(nums[l]+nums[r]==target){
                return new int[]{nums[l],nums[r]};
            }else if(nums[l]+nums[r]>target){
                r--;
            }else{
                l++;
            }
        }

        return null;
    }
}

 11.分治思想:也算是动态规划,n*n矩阵,上下三角

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

题目链接:力扣

class Solution {
    public int[] constructArr(int[] a) {
        //体现了分治的思想
        if(a.length ==0) return new int[0];
        //先计算0到i-1的结果记为b[i],再乘i+1到n的连乘
        //b[i]结果是下三角
        int[] b = new int[a.length];
        b[0]=1;
        //计算下三角乘积
        for(int i=1;i<a.length;i++){
            b[i] = b[i-1] * a[i-1];
        }
        //下三角和上三角乘
        int tmp=1;
        for(int i=a.length-2;i>=0;i--){
            tmp *=a[i+1];//用一个值来统计
            b[i]=tmp *b[i];
        }
        
        return b;

    }
}

12.发现规律:最大值和最小值的差是小于5的

从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

题目链接:力扣

class Solution {
    public boolean isStraight(int[] nums) {
       //本题大小王可以看成任意的数字,最大值和最小值不会超过5;
       //排序加遍历
       //统计0的数量
       int zero = 0;
       Arrays.sort(nums);
       for(int i =0;i<nums.length-1;i++){//这里i的最大值减1
           //判断会不会重复
           if(nums[i]==nums[i+1] && nums[i] !=0 ) return false;
           if(nums[i]==0) zero++;
       }

       return nums[4]-nums[zero]<5;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值