今夜技术人流泪,只因没看这篇【数组高频试题详解】

本文关于笔试中有关数组的算法题的解题思路,算法不仅考验智慧,同样也有做题的套路

适合人群

本文适合无算法基础,想系统学习算法的学习人,也同样适用有些基础但是做题卡壳的学习人。

不多哔哔,直接看题,建议有条件练习的,一定多自己去实现代码,算法也是一个积累的过程,从恍然大悟到熟能生巧。

努力,读书人。加油!奥利给!
今夜技术人流泪,只因没看这篇【数组高频试题详解】_第1张图片

温馨提示:本文篇幅较长

文章目录

  • 删除数组中重复出现的数
  • 数组问题的解决方案:
  • 链表问题的解决方案:
    • 链表中常见的题目:
  • 数组和链表在备战笔试的必刷题
    • 第一题:两数之和
    • 第二题:移除元素
    • 第三题:斐波那契数列求值
  • 数组
    • 1.求数组的最大子序列和
    • 1.for loop中的i++和++i有什么区别
    • 2.寻找数组重复的元素II
    • 1.两数之和II-输入有序数组
      • 做题要分析:
        • 1.第一种解法(暴力求解)
        • 2. 暴力解法的优化
  • 数组高频笔试题
    • 1.移动零(要求原地操作)
    • 2.寻找数组的中心索引位置
    • 3.搜索插入位置
    • 4.删除排序数组中重复的元素
  • 双指针技巧
    • 1.经典试题(反转数组或者反转字符串)
    • 2.数组拆分
    • 3.两数之和II-输入有序数组
    • 第三大的数
  • 种花问题
  • 三个数的最大乘积
  • 主要元素
  • 获取二维数组的行和列
  • 图片平滑器
  • 最长递增子序列
  • 连续序列
  • 零矩阵

删除数组中重复出现的数

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第2张图片

拿到这个题先分析:

  1. 给出的条件:排序好的数组
  2. 要求:原地删除重复的元素
  3. 返回移除元素数组之后的数组的长度

原地删除,不能使用额外的空间,要删除重复的元素,首先要获得数组中的元素,那一定就要遍历数组,提供的是排序,因此只需要比较互相相邻的多个元素就可以确定是否是重复的元素

class Solution {
    public int removeDuplicates(int[] nums) {
        //1.要求原地删除数组中重复的元素
        //首先的想法是如何删除数组中重复的元素---暴力法->遍历数组
        //原地删除,就是不使用额外的空间
        if (nums.length==0){
            return 0;
        }
        int i = 0;
        for (int j = 1; j < nums.length; j++) {
            if (nums[j] != nums[i]){
                i++;
                nums[i] = nums[j];
            }
        }
        return i + 1;

    }
}

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第3张图片

时间复杂度分析:使用了双指针的方式,在数组的原地重新赋值,实现了对重复元素的删除,时间复杂度:只使用了一层的for循环来遍历数组,因此时间复杂度是O(N)

数组问题的解决方案:

  1. 暴力
  2. 双指针双指针分为双层循环双指针(可以用来遍历到数组中的每一个元素和其相邻的元素,注意j的起始位置)和前后往中间推动的双指针(比如最大的盛水容器)
  3. 看清是否需要原地操作数组(有的题目会要求原地操作,这时就不能使用额外的存储空间来存储中间结果)
  4. 那么没有要求原地操作数组的题目是可以使用额外的空间来存储中间结果的
  5. 使用哈希表或者其他的数据结构可以简化时间复杂度
    实际上就是使用了哈希表等数据结构相当于在遍历数组时,将第一层的遍历结果都存储起来了,在数组遍历的第二层需要操作数组中
    具体的元素的时候就可以直接获取到数组中的元素,而不需要再一次的遍历来获取.
  6. 在解决数组问题时,一定注意数组元素的位置(数组的首元素,要操作或者要判断的最后一个元素),而且要注意该元素的前一个元素和后一个元素的位置,一般这就是要访问的点.
  7. 关于数组的访问的时间复杂度是O(1),能够实现对于元素的随机访问,但是对于查询的效率不高,平均是O(n)的时间复杂度,因此为了提高数组的查询的效率,可以使用跳表,既是在数组的基础上创建索引(但是跳表的查询是要求该数组是有序的,这一点要格外的注意).

这里给出遍历数组访问前一个元素和该元素的后一个元素的代码

   public static void main(String[] args) {
        int [] arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            for (int j = i+1; j < arr.length-1 ; j++) {

            }
        }
    }

链表问题的解决方案:

先给出链表的数据结构在Java中的实现

import java.util.Queue;
import java.util.Stack;

/**
 * @author 雷雨
 * @date 2020/9/16 19:49
 * 链表的实现
 */
public class ListNodes {
    public static void main(String[] args) {
        Stack stack = new Stack();

    }
    MyNodes head = null;
    public void add(int data){
        MyNodes newNode = new MyNodes(data);
       if (head==null){
           head=newNode;
           return;
       }
       MyNodes temp = head;
       while (temp!=null){
           temp = temp.next;
       }
       temp.next = newNode;

    }
}
class MyNodes{
    MyNodes next = null;
    int data;
    public MyNodes(int data){
        this.data =data;
    }
}

  1. 首先明确链表的特性,链表不能实现元素的随机访问,但是可以实现低时间复杂度的插入和删除,那么在算法题中如果使用到了链表,那么最最关键也就是使用到链表的添加元素和删除元素的操作.
  2. 链表的插入和删除的效率很高,都是0(1)的时间复杂度,但是对于查询访问的时间复杂度很低,平均是0(n)的时间复杂度

链表中常见的题目:

  1. 删除重复元素
  2. 反转链表

反转链表的处理思路是:

第一种思路:可以使用额外的存储空间的话,那么就创建一个新的链表,然后一直去访问原链表的尾节点将其放入新创建的链表中

第二种思路:不使用额外的存储空间的话,那就采用双指针,一个指针指向头,一个指针指向尾,每次都同时这两个结点的前后节点(用来改变链表的指针指向).

归根结底就是链表的基本操作的变形,特别去注意链表的插入和删除时的代码实现.

数组和链表在备战笔试的必刷题

第一题:两数之和

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第4张图片

做算法题,如果刚开始没有思路,先想一些比较low的作法,只要是能完成题目的要求也是可以的,然后再想办法将算法优化.往往第一个思路会给后面算法的优化提供很好的思路.

当我拿到这道题目的时候,没思路,那么就暴力法

针对该题的暴力法:就是遍历整个数组,将数组中的每个数都与数组中其他的数配对,寻找是否有相加的结果是target的组合,如果有,那么就输出他们的下标值。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        //1.比较容易想到的方法就是使用暴力法,穷举出所有的数的组合
        //缺点也很明显,就是时间复杂度比较高是n^2
        //时间复杂度比较低的算法是使用哈希表的方式
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length ; j++) {
                if (nums[i] == target - nums[j]){
                    return new int[]{i,j};
                }
            }
        }
        throw new IllegalArgumentException("No two sum solution");
    }
}

想要提升算法的能力,一定要在做题之前和做题之后分析自己算法的时间复杂度(一般情况下,不太考虑空间复杂度)

我们使用了暴力法,在遍历数组中每个元素和别的元素进行组合的时候使用了双层的for循环,因此时间复杂度是n^2,这个时间复杂度算是比较高阶的时间复杂度了,是比较low的算法。

要解决这个两数之和的算法,我们还可以使用哈希表,但是我希望做一个系统方法记录,因此哈希表的算法在之后整理了哈希表的算法总结之后再回过头来讲解这道题的解法。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer,Integer> map = new HashMap<Integer,Integer>();
        for(int i = 0; i< nums.length; ++i){
            map.add(num[i],i);
        }
        for(int j = 0;j < nums.length;++j){
            if(map.containKey(target -nums[j]) && map.get(target - nums[j]) !=j){
                return new int[] {i,map.get(target - nums[j])};
            }
        }
        throw new IllegalArgumentException("No two sum solution");
    }
}

使用哈希表的方式解决两数之和的问题避免了使用嵌套的for loop,使用map来判断元素是否存在的时间复杂度是O(1)的,不需要额外的遍历数组。

第二题:移除元素

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第5张图片

这个题和删除排序数组中的重复项非常的相似,可以同样使用数组+双指针的方式解答

class Solution {
    public int removeElement(int[] nums, int val) {
        int i = 0;
        for (int j = 0; j < nums.length; j++) {
            if (nums[j] != val){
                nums[i] =nums[j];
                i++;
            }
        }
        return i;

    }
}

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第6张图片

时间复杂度分析:单层for循环,O(n)的时间复杂度

第三题:斐波那契数列求值

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第7张图片

这个题是学习数据结构和算法中非常常见的一道题

拿到题,可以直接根据题目的需求,可以直接思考暴力求解的方式(因为该题目的思路,题目已经说明的非常清晰了)

class Solution {
    public int fib(int N) {

        //1.第一种方法,就是使用暴力法,递归迭代算出所以的N对应的值,直到算出N对应的fib返回
        
        if (N==0){
            return 0;
        }else if (N==1){
            return 1;
        }else {
            return fib(N-1)+fib(N-2);
        }
    }
}

时间复杂度分析:使用了递归,循环调用自己的函数,时间复杂度是N^2

思考:可优化的地方,我们在求fib的时候会发现,我们其实重复的求了很多个值的fib,而这都是耗费时间的。如果能够将求出的fib(n)的值都存储起来,在需要的时候直接访问到,那么时间的花费应该会减少

因此就有了第二种方法:记忆式的自底向上fib求值

class Solution {
    public int fib(int N) {

        /**第二种方式:记忆式的自底向上
         * 相当于每次都求出了n的fib的具体数值并存储起来(下次需要的时候就不用重复的求值),这样的算法就是
         * 降低了时间复杂度的算法,然后n一直的递增的,直到n递增到N的时候,计算出fib(N),然后返回
         * */
        if (N <= 1){
            return N;
        }
        return memoize(N);

    }
    public int memoize(int N){
        int [] cache = new int[N+1];
        cache[1] = 1;
        for (int i = 2; i <= N ; i++) {
            cache[i] = cache[i - 1] + cache[i - 2];
        }
        return cache[N];
    }
}

整体的思路:使用了一个数组将之前求出的比较小的fib(n)的值存储起来,在求fib(n)比较大的数值时,使用之前存储起来的值即可,不需要重复的求fib(n)

**时间复杂度分析:**因为要层fib(2)开始循环求值到fib(n),因此使用了一个for循环,因此复杂度应为0(n).

需要注意的点:

  1. 在解决算法问题时,往往暴力求解方法中重复求值的地方(或者有规律可循的地方)就是我们可优化算法的地方
  2. 思考如何将重复的事情优化并转化为数据结构是非常重要的。

数组

1.求数组的最大子序列和

 示例: 
//
// 输入: [-2,1,-3,4,-1,2,1,-5,4]
// 输出: 6
// 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

思路分析:(使用动态规划的方法)

这个问题的思考策略就是思考当前位置nums[i]表示当前位置i的最大子序列和是否是需要将该位置的下一个位置加进去组成新的最大子序列和。考虑的逻辑也非常好思考:就是如果加入了下一个位置的元素组成的新的最大子序列的和是小于不加入之前的最大子序列的和,那么就不加入

总的来说,就是分解子问题,子问题都有相同的解题步骤,使用循环访问到了每一个子问题,在循环中维护了一个当前的最大子序列和

代码实现

class Solution {
    public int maxSubArray(int[] nums) {
        //先看题目要求,要求O(1)的空间复杂度和O(n)的时间复杂度
        //求连续的子序列和,那么就是要看该位置加上下一个位置的值和该位置的值是增大还是减小了
        //也就是假如:  pre 代表的是当前位置的最大的子序列和
        //那么求Math.max(pre+x,x)取大的赋值给当前位置的最大子序列和也就是pre
        
		//用来维护当前位置和当前位置的前一个位置的值
        int pre = 0;
        //定义初始的最大子序列和为数组的第一个元素
        int maxAnx = nums[0];
        for( int i = 0; i = nums.length ;i++ ) {
            pre = Math.max(pre + nums[i], nums[i]);
            maxAns = Math.max(pre,maxAns);
        }
        return maxAns;

    }
}

1.for loop中的i++和++i有什么区别

最近在刷算法题的时候,发现了很多人的算法题解的for loop中很喜欢写成++i而不是i++,到底两者之间有什么区别呢?

特意进行了测试

这个题:

 给定一个整数数组,判断是否存在重复元素。
// 如果任意一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。
class Solution {
    public boolean containsDuplicate(int[] nums) {

        //注意在for loop中的++i和i++的循环效果都是一样的,但是++i的效率比i++更高,原因是
        //i++   取值   复制   +1   返回
        //++i   取值   加一    返回
        Arrays.sort(nums);
        for (int i = 0; i < nums.length - 1; ++i) {
            if (nums[i] == nums[i+1])return true;
        }
        return false;
    }
}

这个for loop中的i++和++i的循环效果是一样的,都是先执行,然后在加一再循环。

但是他们之间的效率是有差异的,这也就是很多大佬(大佬都很注意细节)都喜欢在for loop中写++i的原因。

    注意在for loop中的++i和i++的循环效果都是一样的,但是++i的效率比i++更高,原因是
    i++   取值   复制   +1   返回
    ++i   取值   加一    返回

2.寻找数组重复的元素II

//给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。 

思路:

本道题刚拿到题时,可能会想到暴力解法,使用双层循环访问数组中该位置的元素和该位置之前的所有元素,看是否有相同的元素,但是由于使用双层循环(一层循环控制该元素的位置一直后移,内部的循环控制了要循环访问该位置之前的每个节点),因此时间复杂度是O(n^2)的。

这样的解答超过了要求的时间。

总结:我们希望在访问元素时能在常熟的时间复杂度内实现查询,插入和删除,然后用单层循环来控制当前元素向后移,该算法的时间复杂度是O(n)的

class Solution {
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        Map<Integer,Integer> map  = new HashMap<Integer, Integer>();
        for (int i = 0; i < nums.length; i++) {
            if (map.containsKey(nums[i]))return true;
                map.put(nums[i],i);
                if (map.size()>k) map.remove(nums[i-k]);
        }
        return false;
    }
}

注意在该算法中有一个小的技巧:既然要求数组中两个元素之间的下标位置不能超过k,那么我们就动态的删除map中的元素使循环时需要判断的元素数变少

10.24程序员快乐

1.两数之和II-输入有序数组

给定一个已按照升序排列的有序数组,找到两个数使得它们相加之和等于目标数。

函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。

说明:

返回的下标值(index1 和 index2)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:

输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。

做题要分析:

  1. 分析题意和条件:
    • 有序数组(升序排列)
    • 要求得到数组中的两个元素的和与目标值相同,且index1< index2
  2. 做题思路:
    • 得到两个元素的和与目标值相同,那么首先就想到嵌套的for loop实现了数组中的每个元素的遍历和目标值比较相等时就返回
    • 使用指针可以控制使得index1
  3. 分析时间复杂度
    1. 因为使用了双层的for loop那么时间复杂度必定是O(n^2)
  4. 着手做题(分析具体的做题思路)

1.第一种解法(暴力求解)

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        for (int i = 0; i < numbers.length - 1; i++) {
            for (int j = i + 1; j < numbers.length ; j++){
                if (numbers[i] == target - numbers[j]){
                    return new int[]{i+1,j+1};
                }
            }
        }
        return new int[] {-1,-1};
    }
}

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第8张图片

这种双层for loop的解法的时间复杂度较高,在笔试和竞赛中并不占优势,不能完全展示我们的算法编程能力。

2. 暴力解法的优化

仔细分析题目,我们其实还有一个条件数组是有序,题目的条件基本上都是有用的,一定要注意该数组是一个有序的数组啊!!! 而有序的数组就特别容易就要想出二分查找,接着思路就比较明晰了。使用二分查找的特性去优化算法的查询的效率。

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        for (int i = 0; i < numbers.length ; ++i) {
            int left = i + 1;
            int right = numbers.length - 1;
            //一定要注意二分查找算法中 left是可以与right相等的情况
            while (left <= right){
                int mid  = (right - left)/2 + left;
                if (numbers[mid]  ==  target - numbers[i]){
                    return new int[] { i+1, mid +1};
                }else if(numbers[mid] < target - numbers[i]){
                    left = mid + 1;
                }else{
                    right = mid - 1;
                }
            }
        }
        return new int[] {-1,-1};
    }
}

总结:

  1. 数组中双指针使用的强化,不仅是可以一前一后,也可以是普通的双循环
  2. 二分查找的边界条件一定要记清楚

数组高频笔试题

1.移动零(要求原地操作)

思路:

创建两个指针,第一个指针用于进行循环遍历数组将不为0的位置重新赋值,实现了0的删除。第二个指针用于将之前删除的0补齐到数组的后面,可以用不为0的数值的个数来控制要补的0的个数,因为数组的长度是固定的。

    public void moveZeroes(int[] nums) {
        int index = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] != 0){
                nums[index] = nums[i];
                index = index + 1;
            }
        }
        for (int i = index; i < nums.length; i++) {
            nums[i] = 0;
        }

    }

2.寻找数组的中心索引位置

题目的意思就是找到数组的中心索引位置,中心索引位置的左边所有的数的和等于中心索引位置右边所有数的和。

思路:

本题进行一个思维的转化,就是将中心索引转化为:

可以求出总的数组的和sum

那么leftsum(左边所有数的和) = sum - arr[i] - leftsum

public int pivotIndex(int[] nums) {
        int sum = 0;
        int leftsum = 0;
        //求出了sum,数组中元素的总和
        for (int i = 0; i < nums.length; i++) {
            sum +=  nums[i];
        }
        for (int j = 0; j < nums.length ; ++j) {
            if (leftsum == sum - nums[j] - leftsum )return j;
            leftsum += nums[j];
        }
        return -1;

    }

3.搜索插入位置

题目:给定一个有序的数组和一个target值,找到数组中存在的目标值,如果没找到,那么就将该数顺序插入到数组中

思路:

给定的是一个有序的数组,那么可以直接就可以想到使用二分查找的方式进行搜索,这样的效率比遍历数组的效率高

public int searchInsert(int[] nums, int target) {
        //注意该数组是一个有序的数组
        //注意需要特别判断,因为要求插入的位置,为了不让数组越界,我们在外面特判
        //思路二分查找
        //二分查找的条件:
        //1.单调性(在一定的范围内单调)
        //2.有上限和下限(查找是在一定的范围内的)
        //3.能够通过索引位置进行访问(数组是可以通过元素的下标进行访问的)
        int left = 0;
        int right = nums.length-1;
        int mid = 0;
        if (nums.length == 0){
            return 0;
        }
        if (nums[nums.length-1] < target){
            return nums.length;
        }
        while (left < right){
            mid = left + (right - left)/2;
            if(nums[mid] < target){
                left = mid + 1;
            }else {
                right = mid;
            }
        }
        return right;
    }

4.删除排序数组中重复的元素

给定一个已经排好序的数组,删除该数组中重复的元素(要求原地删除),返回值是排序好的新的数组的长度。

思路:

  1. 同样是排好序的数组,那么就只需要判断连续的两个元素是否相同就可以保证整个数组的没有重复的元素
  2. 在判断可存在重复元素后,我们需要原地删除数组中的重复项,那么就需要创建两个数组指针用来原地删除,只有两个连续的数组元素不相同时,才进行数组的赋值,否则就跳过,相当于删除了这个重复的元素。
    public int removeDuplicates(int[] nums) {
        //1.要求原地删除数组中重复的元素
        //首先的想法是如何删除数组中重复的元素---暴力法->遍历数组
        //原地删除,就是不使用额外的空间
        if (nums.length == 0){
            return 0;
        }
        int i = 0;
        for (int j = 0; j < nums.length; j++) {
            if (nums[i] != nums[j]) {
                i++;
                nums[i] = nums[j];
            }
        }
        return i + 1;
    }

双指针技巧

1.经典试题(反转数组或者反转字符串)

使用双指针:一个指向数组(字符串)的头,一个指向数组(字符串)的尾,每次操作都互换头尾指针的值,操作完之后头指针向后移动,尾指针向前移动,实现数组(字符串)的反转。如果是字符串,那么就先转化为数组操作后,通过String.valueOf(arr)转化为字符串。

public void resverseString(char[] arr){
    int i = 0;
    int j = arr.length - 1;
    while(i < j){
        char temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
        i++;
        j--;
    }
}

总结

双指针的使用场景:当你需要从两端向中间遍历数组时

一个指针从头部开始,一个指针从尾部开始。

2.数组拆分

给定长度为 2n 的数组, 你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), ..., (an, bn) ,使得从1 到 n 的 min(ai, bi) 总和最大。 

特别要注意:拿到题之后先要分析题目,题目的最终要求是分组为n,并且数组的长度是2n,那么就是两个元素分为一组,又题目要求操作数组之后,得到 1-n的最小的元素和.

注意要操作数组的,而不是直接根据参数中得到的数组直接进行操作

public int arrayPairSum(int[] nums) {
        //这道题我的思路是直接求出相邻的两个数的最小值,然后累加出这些最小值的和
        //但是程序写出来一直出错
//        int sum = 0;
        int index  = 0;
        for (int i = 0; i < nums.length ;  i = i+2) {
            index = i + 1;
            if (nums[i] < nums[index]){
                sum += nums[i];
            }else {
                sum +=  nums[index];
            }
        }
        return sum ;
        //原因是:将题目的要求没有分析准确,题目只是给出了一个数组,要求将数组拆分
        //然后得到他的最小值的和,而不是直接在数组的基础上求最小值的和,还要进行操作

        Arrays.sort(nums);
        int sum = 0;
        for (int i = 0; i < nums.length; i += 2) {
            sum += nums[i];
        }
        return sum;

    }

3.两数之和II-输入有序数组

给定一个有序的数组和一个目标值,寻找在数组中是否存在两个数的和等于target,如果等于那么就返回一个数组,数组中的两个元素值分别是这两个数的下标

可以采用暴力法:遍历数组中的每一个元素的组合,看其是否和target相等,如果相等直接返回下标的数组。

暴力法遍历数组需要特别熟练的掌握

public int[] twoSum(int [] arr,int target){
    for(int i = 0; i < arr.length; i++){
        for(int j = i+1; j < arr.length-1; j++){
            if(arr[i] == target - arr[j]){
                return new int[] {i+1,j+1};
            }
        }
    }
    throw new IllegaArgumentException("No two sum solution");
}

第三大的数

题目,给定一个数组,找出该数组中第三大的数,如果第三大的数不存在,那么就输出最大的数

注意相同的数认为是相同大小的,比如[3,2,2,3,1],那么第三大的数是1

思路:排序数组,从后往前遍历,找到第三个不一样的数,输出即可。

public int thirdMax(int [] nums){
    Arrays.sort(nums);
    //用一个数来记录不同的数(初始化为1是因为我们比较时已经拿出了最大的数了,已经取了一个数)
    int index = 1;
    for(int i = nums.length - 1;i > 0; --i){
        if(nums[i] != nums[i-1]){
            index++;
        }
        if(index == 3){
            return nums[i-1];
        }
    }
    //没找到第三大的数就返回最大的数
    return nums[nums.length -1];
}

种花问题

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第9张图片

分析:

  1. 本身种的花不会违反规则
  2. 种植规则,其实就是必须保证要种植的位置的前一个位置是0并且后一个位置也是0,排除它的前一个位置是数组中的第一个位置,排除它的最后一个位置是数组中最后一个位置(突然有种感觉有点类似链表的概念)

设计算法:

暴力遍历,按照题目给出的算法的思路直接写出算法即可。

public boolean canPlaceFlowers(int[] flowerbed, int n) {
    //用于计数符合种植规则的位置的数
    int count = 0;
    //我们从左向右遍历(其实感觉也可以从右往左遍历),所以开始的位置是0
    int i = 0;
    while(i < flowerbed.length){
        if(flowerbed[i]== 0 && (i == 0 || flowerbed[i-1] == 0) && (i == flowerbed.length -1 || flowerbed[i+1] == 0)){
            flowerbed[i] = 1;
            count++;
        }
    }
    //分析题意:这个位置是可以相等的,只要能插入n数量的花就应该是true的
    return n>=count;
}

三个数的最大乘积

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第10张图片

分析:

  1. 数组的长度大于等于3,所以一定存在三个数,而数组是可以通过下标索引值的
  2. 数组的元素的范围是-1000,1000其中含有负数,要考虑负数存在的情况
  3. 不会超过整数的范围(这个有利于我们使用整数来解答这个题)

思路:

三个数的乘积

  • 三个正数(含有至少三个正数的情况) 最大乘积是最大的三个正数的乘积或者一个最大正数和两个最小负数的乘积
  • 三个负数(全为负数的情况) 最大乘积就是最大的三个负数的乘积
  • 1个正数两个负数(正数不足3个时) 最大乘积是一个最大正数和两个最小负数的
  • 两个负数和一个正数(两个负数的绝对值大于对应的两个正数的绝对值大小) 最大乘积是一个最大正数和两个最小负数的

总结就是:最大的三个数的乘积和一个最大的数和两个最小的数的乘积比较

class Solution {
    public int maximumProduct(int[] nums) {
        //数组排序,就可以通过索引得到元素的值
        Arrays.sort(nums);
        return Math.max(nums[0] * nums[1] * nums[len],nums[len] * nums[len - 1] * nums[len - 2]);
    }
}

主要元素

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第11张图片

分析:

题目的意思其实就是要求对数组中不同的数进行计数

首先想到的就是使用Map的数据结构,因为我们不仅需要存储元素出现的次数,还需要知道这个出现次数最多的元素是哪个元素。

class Solution {
    public int majorityElement(int[] nums) {
        Map<Integer,Integer> mymap = new HashMap<Integer,Integer>();
        for(int i = 0;i < nums.length; ++i){
            map.put(nums[i],map.getOrDefault(nums[i],0)+1);
            if(map.get(nums[i]) > nums.length/2){
                return nums[i];
            }
        }
        return -1;
    }
}

image-20201026233805692

image-20201026233819100

获取二维数组的行和列

  • 获取行数
int len = nums.length;
  • 获取列数
int len2 = nums[0].length;
  • 二维数组的遍历(拓展)

对于数组int[] [] nums = new int [3] [4];

这是一个三行四列的数组,要遍历整个二维数组

public static void myMethod(int[][] nums){
    for(int i = 0; i < 3;++i){
        for(int j = 0; j < 4;++j){
            //code
        }
    }
}

思考联系:对于设计二维数组,或者是存在二维的题目,要想到使用二维数组解决,二维数组不仅能够降维,还能通过索引访问到具体的值,比如杨辉三角的问题,就可以化为二维数组的问题,通过位置的特点求值

图片平滑器

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第12张图片

// Related Topics 数组 
//  64  0

public class ImageSmoother{
    public static void main(String[] args) {
        Solution solution = new ImageSmoother().new Solution();
        
    }

//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
    public int[][] imageSmoother(int[][] M) {
        //思路:就是找到二维数组元素的相邻的格子的数的和,它的平均灰度就是再除以该元素周围的元素数
        //周围元素的范围是:除了边界以外
        //[i-1,j-1]  [i-1,j] [i-1,j+1]
        //[i,j-1]    [i,j]   [i,j+1]
        //[i+1,j-1]  [i+1,j  [i+1,j+1]

        //既然要得到这些位置的值的和,那么就要遍历整个二维数组
        //代表数组的行
        int m = M.length;
        //代表数组的列
        int n = M[0].length;
        //创建出结果数组
        int [][] ans = new int[m][n];

        //开始遍历数组求元素周围值的和
        for (int i = 0; i < m ; ++i) {
            for (int j = 0; j < n; ++j) {
                //整个变量用来计数元素周围元素的个数
                int count = 0;
                //注意这种特殊的遍历方式,直接使用有规律的范围作为遍历的范围,减少了不必要数的遍历,
                //也更加利于计算
                for (int mi = i - 1; mi <= i + 1 ; ++mi) {
                    for (int mj = j - 1; mj <= j + 1 ; ++mj) {
                        if (0 <= mi && mi < m && 0 <= mj && mj < n){
                            //这一步是该数组元素周围所以的元素都进行求和的结果
                            ans[i][j] += M[mi][mj];
                            count++;
                        }
                    }

                }
                ans[i][j] /= count;
            }

        }
        return ans;


    }
}
//leetcode submit region end(Prohibit modification and deletion)

}

最长递增子序列

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第13张图片

分析:

  1. 提供一个整数数组,在数组中找到递增子序列,并返回这个子序列的长度
  2. 这个数组中可能存在多个递增子序列,要求返回的是最长的子序列的长度
  3. 清楚递增子序列的概念就是 nums[i] < nums[i+1]注意是不可以相等的

思路:

遍历数组,判断子序列,并获取到子序列的值,要对子序列进行比较,取最长的子序列

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        //特殊判断
        if(nums.length == 0){
            return 0;
        }
        int count = 0;
        int maxcount = 0;
        //遍历数组并获取最长的子序列
        for(int i = 0;i < nums.length - 1; ++i){
            if(nums[i] < nums[i+1]){
                count++;
                maxcount = Math.max(count,maxcount);
            }else{
                //说明了那个递增序列结束了,让count置0,重新开始计数新的最长子序列
                count = 0;
            }
        }
        //注意返回的时候要+1,因为我们记录的是比较的次数,而子序列的长度刚好比比较次数多1。
        return maxcount + 1;
    }
}

连续序列

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第14张图片

分析:保证每次选择都是在最大的序列和的条件下选择

class Solution {
    public int maxSubArray(int[] nums) {
        for(int i  = 1; i < nums.length; ++i){
            nums[i] = Math.max(nums[i], nums[i] + nums[i - 1]);
            nums[0] = Math.max(nums[0],nums[i]);
        }
        return nums[0];
    }
}

零矩阵

今夜技术人流泪,只因没看这篇【数组高频试题详解】_第15张图片

思路:

先找到数组中0的位置,然后再修改其所在的行和所在的列为0

class Solution {
    public void setZeroes(int[][] matrix) {
        //思路:
        //遍历数组然后记录二维数组中零的位置i ,j 的值
        //然后再遍历一遍数组将0赋值
        int len = matrix[0].length;
        Set<Integer> setH = new HashSet<Integer>();
        Set<Integer> setL = new HashSet<Integer>();
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < len; j++) {
                if (matrix[i][j] == 0){
                    setH.add(i);
                    setL.add(j);
                }
            }
        }
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < len; j++) {
                if (setH.contains(i) || setL.contains(j)){
                    matrix[i][j] = 0;
                }
            }
        }
    }
}

这种解法的时间复杂度很高是O(n^2),使用了双层的for loop

ath.max(nums[0],nums[i]);
}
return nums[0];
}
}


# 零矩阵

[外链图片转存中...(img-YItuHRRb-1603848932999)]

思路:

先找到数组中0的位置,然后再修改其所在的行和所在的列为0

```java
class Solution {
    public void setZeroes(int[][] matrix) {
        //思路:
        //遍历数组然后记录二维数组中零的位置i ,j 的值
        //然后再遍历一遍数组将0赋值
        int len = matrix[0].length;
        Set setH = new HashSet();
        Set setL = new HashSet();
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < len; j++) {
                if (matrix[i][j] == 0){
                    setH.add(i);
                    setL.add(j);
                }
            }
        }
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < len; j++) {
                if (setH.contains(i) || setL.contains(j)){
                    matrix[i][j] = 0;
                }
            }
        }
    }
}

这种解法的时间复杂度很高是O(n^2),使用了双层的for loop

文章中有什么问题请各位大佬指正!每条评论我都会回复

你可能感兴趣的:(面试笔试,#,算法练习,算法,java,数组,面试,笔试)