BAT程序员手把手带你学算法-数组篇(理论知识剖析+5道经典面试题目)


笔者先后在BAT中的两家工作,在我面试候选人的时候,发现很多同学简历看上去很优秀,各种框架各种经验,但是一面试发现对数据的基本操作都不太熟悉

只能说在准备面试的过程中,对最基础的数据结构都没有好好准备

这里结合自己的面试思路,从面试必备的理论知识到五道精选的面试题目,来给大家讲解一下。

数组是非常基础的数据结构,在面试中,数组的题目一般在思维上都不难,主要是考察对代码的掌控能力

也就是说,想法很简单,但实现起来 可能就不是那么回事了

这篇文章我将讲解面试中必考的数组理论知识,再给出五道精选面试题目

带大家一起分析每一道经典题目的思路,同时给出了每一道题目的暴力解法和更优解法的配有详细注释的代码

通过这篇文章可以帮助大家对算法面试中数组的相关知识有一个全面的了解

接下来先介绍面试中必考的数组理论知识

必须掌握的数组理论知识

数组是存放在连续内存空间上的相同类型数据的集合。 数组可以方便的通过下表索引的方式获取到下表下对应的数据。

举一个字符数组的例子,如图所示:
在这里插入图片描述

需要两点注意的是

  • 数组下表都是从0开始的。
  • 数组内存空间的地址是连续的

正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址,

例如删除下表为3的元素,需要对下表为3的元素后面的所有元素都要做移动操作,如图所示:
在这里插入图片描述

那么二维数组直接上图,大家应该就知道怎么回事了
在这里插入图片描述

那么这里要请同学思考一下,二维数组在内存的空间地址是连续的么?

我们来举一个例子,例如: int[][] rating = new int[3][4]; , 这个二维数据在内存空间可不是一个 3*4 的连续地址空间

看了下图,就应该明白了:
在这里插入图片描述

这个二维数据在内存中不是 3*4 的连续地址空间,而是四条连续的地址空间组成!

接下来,我从leetcode中给大家总结了五道数组相关的经典面试题目

五道数组经典面试题目

第一道:搜索插入位置

leetcode 编号35
在这里插入图片描述
这道题目呢,考察的数据的基本操作,思路很简单,但是在通过率在简单题里并不高,不要轻敌

可以使用暴力解法,通过这道题目,如果准求更优的算法,建议试一试用二分法,来解决这道题目

暴力解法时间复杂度:O(n)
二分法时间复杂度:O(logn)

二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力

代码详细讲解:

// 暴力解法 
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        for (int i = 0; i < nums.size(); i++) {
        // 分别处理如下三种情况
        // 目标值在数组所有元素之前
        // 目标值等于数组中某一个元素
        // 目标值插入数组中的位置
            if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果
                return i;
            }
        }
        // 目标值在数组所有元素之后的情况
        return nums.size(); // 如果target是最大的,或者 nums为空,则返回nums的长度
    }
};
// 二分解法
// 时间复杂度:O(logn)
// 空间复杂度:O(1)
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0;
        int right = n - 1; // 我们定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle;
            }
        }
        // 分别处理如下四种情况
        // 目标值在数组所有元素之前  [0, -1]
        // 目标值等于数组中某一个元素  return middle;
        // 目标值插入数组中的位置 [left, right],return  right + 1
        // 目标值在数组所有元素之后的情况 [left, right], return right + 1
        return right + 1;
    }
};

第二道:移除元素

leetcode 编号27
在这里插入图片描述

在这道题目中,我们只要理解数组在内存中的结构,就知道数据中的元素只能被覆盖掉,而能直接删掉

所以这里题目中说的移除元素,其实是覆盖掉某一个元素

那么暴力的解法,很简单,两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组

很明显暴力解法时间复杂度是O(n), 然后尝试一个更优解,快慢指针法,时间复杂度可以做到O(n)

快慢指针法是解决数据问题中常见操作,头一个接触这个算法 还是有点懵的,

建议通过这道题目了解一下快慢指针法

代码详细讲解:

// 暴力解法
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int size = nums.size();
        for (int i = 0; i < size; i++) {
            if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
                for (int j = i + 1; j < size; j++) {
                    nums[j - 1] = nums[j];
                }
                i--; // 因为下表i以后的数值都向前移动了一位,所以i也向前移动一位
                size--;// 此时数组的大小-1
            }
        }
        return size;

    }
};
// 快慢指针解法
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slowIndex = 0; // index为 慢指针
        for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {  // i 为快指针
            if (val != nums[fastIndex]) { //将快指针对应的数值赋值给慢指针对应的数值
                nums[slowIndex++] = nums[fastIndex]; 注意这里是slowIndex++ 而不是slowIndex--
            }
        }
        return slowIndex;
    }
};

第三道:删除排序数组中的重复项

leetcode 编号26

在这里插入图片描述

这道题目是 编号27的延伸, 做过了27题之后,再过这道题,一定会对快慢指针法有一个深刻的理解

代码详细讲解:

// 快慢指针解法
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        if (nums.empty()) return 0; // 别忘记空数组的判断
        int slowIndex = 0;
        for (int fastIndex = 0; fastIndex < (nums.size() - 1); fastIndex++){
            if(nums[fastIndex] != nums[fastIndex + 1]) { // 发现和后一个不相同
                nums[++slowIndex] = nums[fastIndex + 1]; //slowIndex = 0 的数据一定是不重复的,所以直接 ++slowIndex
            }
        }
        return slowIndex + 1; //别忘了slowIndex是从0开始的,所以返回slowIndex + 1
    }
};

第四道:长度最小的子数组

leetcode 编号209

在这里插入图片描述

这道题目暴力是也可以的,时间复杂度为O(n^2)

其实也是通过一个快指针和慢指针来实现一个滑动窗口,最终得到长度最小的子数组,时间复杂度为O(n)

建议通过这道题目了解一下滑动窗口的思想

代码详细讲解:

// 暴力解法
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int result = INT32_MAX; // 最终的结果
        int sum = 0; // 子序列的数值之和
        int subLength = 0; // 子序列的长度
        for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
            sum = 0;
            for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
                sum += nums[j];
                if (sum >= s) { // 一旦发现子序列和超过了s,更新result
                    subLength = j - i + 1; // 取子序列的长度
                    // result取 result和subLength最小的那个
                    result = result < subLength ? result : subLength;
                    break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
                }
            }
        }
        // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
        return result == INT32_MAX ? 0 : result;
    }
};
// 滑动窗口
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int result = INT32_MAX;
        int sum = 0; // 滑动窗口数值之和
        int i = 0; // 滑动窗口起始位置
        int subLength = 0; // 滑动窗口的长度
        for (int j = 0; j < nums.size(); j++) {
            sum += nums[j];
            // 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
            while (sum >= s) {
                subLength = (j - i + 1); // 取子序列的长度
                // result取 result和subLength最小的那个
                result = result < subLength ? result : subLength;
                sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
            }
        }
        // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
        return result == INT32_MAX ? 0 : result;
    }
};

第五道:螺旋矩阵

leetcode 编号59

在这里插入图片描述

这是一道模拟题,就是模拟螺旋矩阵

这道题绝对是面试中的常客,特别是笔试的时候

而且这道题很多同学就算做过,过一段时间,还是做这道题目 ,还是做不好。

解题的关键在于在循环遍历的时候需要定义好自己的循环不变量

这道题目是数组面试题中最常见的一个类型之一

代码详细讲解:

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
        int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
        int loop = n / 2; // 每个圈循环几次
        int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(3, 3)
        int count = 1; // 用来计数
        int offset = 1; // 每一圈循环,需要偏移的位置
        int i,j;
        while (loop --) {
            i = startx;
            j = starty;

            // 下面开始的四个for就是模拟转了一圈
            // 模拟填充上行从左到右(左闭右开)
            for (j = starty; j < starty + n - offset; j++) {
                res[startx][j] = count++;
            }
            // 模拟填充右列从上到下(左闭右开)
            for (i = startx; i < startx + n - offset; i++) {
                res[i][j] = count++;
            }
            // 模拟填充下行从右到左(左闭右开)
            for (; j > starty; j--) {
                res[i][j] = count++;
            }
            // 模拟填充左列从下到上(左闭右开)
            for (; i > startx; i--) {
                res[i][j] = count++;
            }

            // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
            startx++;
            starty++;

            // offset 控制每一圈,遍历的长度
            offset += 2;
        }

        // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
        if (n % 2) {
            res[mid][mid] = count;
        }
        return res;
    }
};

总结

通过这篇文章希望可以帮助大家对算法面试中数组相关问题有更深的了解

这五道题也是数组中非常典型的题目,每一道题目都代表一个类型,一个思想

正在学习算法,或者在准备面试的同学,建议认真做好这五道算法面试题

如有问题,欢迎评论区留言。

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读