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;
    }
};

總結

通過這篇文章希望可以幫助大家對算法面試中數組相關問題有更深的瞭解

這五道題也是數組中非常典型的題目,每一道題目都代表一個類型,一個思想

正在學習算法,或者在準備面試的同學,建議認真做好這五道算法面試題

如有問題,歡迎評論區留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章