每日一课 | 数组内存及数组面试常问算法全解析

01.

数组内存及面试

大家好,我是营长,上期分享的案例上手Python数据可视化专栏七天打卡结束了,如果忘记的小伙伴可以进入公号查看往期文章哦~

本期邀请的是春晨溅雨·4位算法工程师为我们分享《数据结构算法面试全解析》专栏。

数据结构算法面试

数组内存及数组面试常问算法

本章将以 Java 语言为列,深入浅出介绍数组。

此部分主要介绍所有编程语言中基本都会涉及到的数据结构:数组。数组相信大家都会使用,但是当面试过程中闻到涉及到内存分析、数据结构对应的算法时,总会有让你猝不及防的地方,比如数组的内存模型分析?本章主要以 Java 语言为列,以阿里巴巴的面试题为主,围绕以下话题深入浅出进行讲解。

1. 数组结构

首先,什么是数组?数组好比一个“储物空间”,可以有顺序的储存同种类型的“物品”。您可以设定它的大小,也可以设定它存储“物品”的类型。

数组有一维数组,二维数组。实际上数组有点类似于我们数学中的矩阵。

如图:

图中有两个数组,数组 1 和数组 2。数组的下标从 0 开始,我们可以通过下标来获取数组中存储的元素。例如通过 TCoolNum1[0],可以获取 111,通过 TCoolNum2[2] 得到字符串“cool”。

num1 存储的即为类型为整数的“物品”,其长度为 10,数组 2 存储的类型为字符串的“物品”,数组的长度为 7,虽然该数组只存储了 3 个字符串,但是该数组在声明的时候申请长度为 7 的内存空间,尽管没有全部存储,但是占用的内存空间并不改变。

2. 数组常用方法

2.1 数组的声明和初始化
2.1.1 数组的声明

在程序中必须先声明数组,方可使用数组。

声明方法如下:

int[] TCoolNum1;
String[] TCoolNum2; //这里定义一个空数组
# or
int TCoolNum3[];
int[] TCoolNum14 = new int[];//同样也是一个空数组

注意,这里初始化的数组存储的都是 Java 中的基本数据类型。你也可以申明存储对象的数组,例如:

String[] num2
# or
String[] num2 = new String[];

如果你有一个对象名为 SomeObject,你也可以这样申明:

SomeObject num1[];
# or
SomeObject[] num2 = new SomeObject[];

2.1.2 静态初始化和动态初始化

静态初始化:

int[] TCoolNum5=new int[]{0,1,2,3,4};
#or
int[] TCoolNum6={1,2,3,4};

同学们肯定好奇为什么叫静态的,因为上面的两种方式都是程序员在初始化数组时已经为数组进行了赋值,此时 JVM 会自动识别去数组的长度,并为其分配相应的内存。

既然有静态初始化,那一定有动态初始化:

int[] TCoolNum7 = new int[7];
 #向内存空间申请长度为 7 的数组空间

如果事先知道你想要存储的数据有多长,可以直接在申明时加上长度,中括号中的 7 即代表数组的长度为 7,而此时我们并未给数组赋值,但是由于数组的类型为 int 型,属于 Java 中的基本类型,故 JVM 会给其赋初始值 0。

(面试常问)那么我们来计算一下这样一个数组在内存中究竟占了多少内存空间:一个 int 类型占用 4 个字节,我们这里申请了 7 个,即 TcoolNum7 在内存中占用了 4*7=28 字节的内存。

定义完数组,除了静态初始化时赋值,还可以用这种方式进行赋值:

int[] TCoolNum8=new int[3];
TCoolNum8[0]=0;
TCoolNum8[1]=1;
TCoolNum8[2]=2;

2.1.3 多维数组的声明和初始化(以二维数组为例)

多维数组的声明和初始化和一维数组大同小异。代码如下:

int num[][] = new int[3][3];

二维数组 a 可以看成一个三行三列的数组。如果面试官问你这样一个数组在内存中占用多少字节,你就可以举一反三了:3*3*4(字节)=36 字节,一共占用了 36 个字节。与一维数组类似,也可以以对象进行申明:

SomeObject num[][] = new SomeObject[3][3];

这里就不重复表述了。

2.2 数组常用的属性和方法

最常用的自然是数组名称 .length 来获取数组的长度,这个就不多做介绍了。

2.2.1 数组的遍历

除了获取数组的长度,在实际工作中我们经常需要对数组进行的操作就是遍历,给大家介绍两种常用的遍历方式:

int[] TCoolNum9=new int[]{0,1,2,3,4};
for (int i :TCoolNum9){
    System.out.println(i);
}

这里我们通过 for 循环,循环的是 TCoolNum9 数组中的每一个值,然后在循环中将每个值打印出来,当然我们也可以根据数组的下标进行循环:

int[] TCoolNum10=new int[]{0,1,2,3,4};
for (int i =0;i<TCoolNum10.length-1;i++){
        System.out.println(TCoolNum10[i]);
}

由于数组的下标永远都是从 0 开始,所以我们定义 i 初始值为 0,通过 TCoolNum10.length 获取数组的长度,得到该数组的下标最大值,进行循环,继而再根据 TCoolNum10[i] 的方式打印。这里的打印只是一种操作,你可以在循环里进行增删改查等等操作。

2.2.2 Java.util.Arrays 类

Java.util.Arrays 是 Java 中针对数组操作封装的类,在类中封装了一系列对数组操作的方法,如:

 int[] num2 = new int[7];
 java.util.Arrays.fill(num2,3); 
#将数组 num2 填充 int 类型元素 3

 int[] num2 = new int[7];
 java.util.Arrays.sort(num2); 
#对数组 num2 进行升序排序

 int[] num2 = new int[7];
 int[] num1 = new int[7];
 java.util.Arrays.equals(num2,num1); 
# 判断数组中元素是否相等

 java.util.Arrays.binarySearch(num2,2); # 采用二分法对排序好的数组 num2 进行查找

还有很多,需要大家在实践中学会使用。

2.3 数组的实践
数据结构的创建最终都归于应用。那么数组在编程中有哪些实际的应用呢?
2.3.1 获取数组中的最大值

假设我们有一个数组 TCoolSalary,里面存储的公司所有员工的工资,老板想知道现在公司里谁的工资最高(当然不包括老板哈),如果同学们有一定的基础,就一定知道通过 for 循环依次比较就能获得最大值。

暴力循环代码如下:

 public  static  void main(String[] args){
        int[] TCoolSalary ={10,20,3,4,2,6,54,5,45,32,87,92,6,7,5,343,5,45,45,543,365};
        int max=0;
        for (int i=0;i<TCoolSalary.length-1;i++){
            if (max<TCoolSalary[i]){
                max=TCoolSalary[i];
            }
        }
        System.out.println("薪水最高为:" +max);
    }

运行结果:

3. 数组的内存解析

Java 中的数组是用来存储同一种数据类型的数据结构,一旦初始化完成,即所占的空间就已固定下来,初始化的过程就是分配对应内存空间的过程。即使某个元素被清空,但其所在空间仍然保留,因此数组长度将不能被改变。

那么数组在内存中如何存储呢?看下图:

4. 小试牛刀

4.1 数组实战题:移除元素

题目:给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例:

给定 nums = [3,2,2,3], val = 3,函数应该返回新的长度 2,并且 nums 中的前两个元素均为 2。

你不需要考虑数组中超出新长度后面的元素。

解题思路:现在考虑数组包含很少的要删除的元素的情况。例如,num=[1,2,3,5,4],Val=4num=[1,2,3,5,4],Val=4。之前的算法会对前四个元素做不必要的复制操作。另一个例子是 num=[4,1,2,3,5],Val=4num=[4,1,2,3,5],Val=4。似乎没有必要将 [1,2,3,5][1,2,3,5] 这几个元素左移一步,因为问题描述中提到元素的顺序可以更改。

算法

当我们遇到 nums[i] = valnums[i]=val 时,我们可以将当前元素与最后一个元素进行交换,并释放最后一个元素。这实际上使数组的大小减少了 1。

请注意,被交换的最后一个元素可能是您想要移除的值。但是不要担心,在下一次迭代中,我们仍然会检查这个元素。

解题代码:

public int removeElement(int[] nums, int val) {
    int i = 0;
    int n = nums.length;
    while (i < n) {
        if (nums[i] == val) {
            nums[i] = nums[n - 1];
            // reduce array size by one
            n--;
        } else {
            i++;
        }
    }
    return n;
}

复杂度分析

时间复杂度:O(n)O(n),最多遍历 n 步。在这个方法中,赋值操作的次数等于要删除的元素的数量。因此,如果要移除的元素很少,效率会更高。

空间复杂度:O(1)O(1)。

4.2 2019 年饿了么秋招真题:搜索插入位置

题目:

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

示例 1:

输入:[1,3,5,6], 5

输出:2

解题思路:

如果该题目暴力解决的话需要 O(n)O(n) 的时间复杂度,但是如果二分的话则可以降低到 O(logn)O(logn) 的时间复杂度。

  • 整体思路和普通的二分查找几乎没有区别,先设定左侧下标 left 和右侧下标 right,再计算中间下标 mid。

  • 每次根据 nums[mid] 和 target 之间的大小进行判断,相等则直接返回下标,nums[mid] < target 则 left 右移,nums[mid] > target 则 right 左移。

  • 二分查找的思路不难理解,但是边界条件容易出错,比如 循环结束条件中 left 和 right 的关系,更新 left 和 right 位置时要不要加 1 减 1。

解题代码:

class Solution {
    public int searchInsert(int[] nums, int target) {
        int n = nums.length;
        int left = 0, right = n - 1, ans = n;
        while (left <= right) {
            int mid = ((right - left) >> 1) + left;
            if (target <= nums[mid]) {
                ans = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return ans;
    }
}

复杂度分析

时间复杂度:O(logn)O(logn),其中 n 为数组的长度。二分查找所需的时间复杂度为 O(logn)O(logn)。

空间复杂度:O(1)O(1)。

4.3 2019 年今日头条秋招真题:盛最多的水

题目:给定 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器,且 n 的值至少为 2。

解题思路:

首先做题之前我们需要找到每一题存在的规律,本题通过图片不难发现,我们要获得最大区域是由两限度和 X 轴香乘获得,但是受限于两边较短的那个长度,故我们可以总结一下:两线段距离越远,得到的面积就越大,同时线段较短的那个长度越大面积越大。

我们在由线段长度构成的数组中使用两个指针,一个初始化指向数组的下标 0,另一个指向数组的末尾。每一次我们都会通过计算数组之间下标的长度与两侧线段较短的相乘得到区域面积,通过变量 maxarea 来持续存储到目前为止所获得的最大面积。并将较短的指针往另一侧移动一步。为什么要移动较短的那个指针呢?因为在循环时,往另一端移动则 x 轴上的距离变短了,如果移动的是较长的指针,则区域面积一定会变小,但是移动较短的指针有可能会使 maxarea 变大,这也是不用遍历所有可能的原因。

解题代码:

public class Solution {
    public int maxArea(int[] height) {
        int maxarea = 0, l = 0, r = height.length - 1;
        while (l < r) {
            maxarea = Math.max(maxarea, Math.min(height[l], height[r]) * (r - l));
            if (height[l] < height[r])
                l++;
            else
                r--;
        }
        return maxarea;
    }
}

复杂度分析

时间复杂度:O(n),一次扫描。空间复杂度:O(1),使用恒定的空间。

4.4 2019 年百度搜索部秋招真题:两数之和

题目:

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9

所以返回 [0, 1]

解题思路:

为了对运行时间复杂度进行优化,我们需要一种更有效的方法来检查数组中是否存在目标元素。如果存在,我们需要找出它的索引。保持数组中的每个元素与其索引相互对应的最好方法是什么?哈希表。

通过以空间换取速度的方式,我们可以将查找时间从 O(n)O(n) 降低到 O(1)O(1)。哈希表正是为此目的而构建的,它支持以 近似 恒定的时间进行快速查找。我用“近似”来描述,是因为一旦出现冲突,查找用时可能会退化到 O(n)O(n)。但只要你仔细地挑选哈希函数,在哈希表中进行查找的用时应当被摊销为 O(1)O(1)。

一个简单的实现使用了两次迭代。在第一次迭代中,我们将每个元素的值和它的索引添加到表中。然后,在第二次迭代中,我们将检查每个元素所对应的目标元素(target - nums[i]target−nums[i])是否存在于表中。注意,该目标元素不能是 nums[i]nums[i] 本身!

解题代码:

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

4.5 2019 年百度搜索部秋招真题:三数之和(二数之和进阶版)

题目:

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a、b、c,使得 a+b+c=0?找出所有满足条件且不重复的三元组。注意:答案中不可以包含重复的三元组。

示例:

给定数组 nums = [-1, 0, 1, 2, -1, -4],满足要求的三元组集合为:

[
  [-1, 0, 1],
  [-1, -1, 2]
]

解题思路:

首先我们的思路依旧是简化,对于一个无顺序的数组进行升序排序,从小到大排列。我们定义 i、l、r 分别指向数组的第一个值,i 值后面的值和数组最后一个值。以 i++ 循环遍历,相当于先确定三个数中的一个数 num[i],再加上 num[l]、num[r],如果 sum 小于 0,则将 l 向右移动一格;相反,如果 sum>0,则将 r 向左移动一格;如果等于 0,则放入结果集中。最后注意去重就可以了。

解题代码:

class Solution {
    public static List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> ans = new ArrayList();
        int len = nums.length;
        if(nums == null || len < 3) return ans;
        Arrays.sort(nums); // 排序
        for (int i = 0; i < len ; i++) {
            if(nums[i] > 0) break; // 如果当前数字大于 0,则三数之和一定大于 0,所以结束循环
            if(i > 0 && nums[i] == nums[i-1]) continue; // 去重
            int L = i+1;
            int R = len-1;
            while(L < R){
                int sum = nums[i] + nums[L] + nums[R];
                if(sum == 0){
                    ans.add(Arrays.asList(nums[i],nums[L],nums[R]));
                    while (L<R && nums[L] == nums[L+1]) L++; // 去重
                    while (L<R && nums[R] == nums[R-1]) R--; // 去重
                    L++;
                    R--;
                }
                else if (sum < 0) L++;
                else if (sum > 0) R--;
            }
        }        
        return ans;
    }
}

复杂度分析

时间复杂度:O(n2)O(n2),其中 n 为数组长度。

今日内容有get吗,欢迎各位留言讨论!

下期预告:单向链表、双向链表和循环链表图文解析

以上专栏均来自CSDN GitChat专栏《数据结构算法面试全解析》,作者春晨溅雨·4位算法工程师,专栏详情可识别下方二维码查看哦!

了解更多详情

可识别下方二维码

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值