算法:談談數據結構【數組】那些事

導讀: 數據是最基本的數據結構,能解決不少問題,好比常見的C_n^m,A_n^m求解,使用數組來解決重複遞歸過程,動態規劃使用數組記錄最近解過程當中的各個步驟的解。今天咱們用幾個常見的面試題來談一談算法——數據結構【數組】那些事java

背景

數組是最多見的也是常常使用的數據結構,大多數語言的封裝類型(List/Map)都含有數組的身影。咱們知道數組時一塊連續的內存區域,訪問某個數據時用數組下標訪問,當擴容和刪除時略顯麻煩.面試

那咱們怎麼才能用好數組呢?怎麼能快速使用數組這種簡單的數據結構來解決複雜的問題呢?算法

下面咱們從一到題來引入講解數組解一些常見的算法題.數組

給定一個整數數組 nums ,找到一個具備最大和的連續子數組(子數組最少包含一個元素),返回其最大和。數據結構

示例: 輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。函數

最大子序列求和的解法

在個人思惟裏,若是一個問題,你以前沒作過,沒有系統的學習過此類通用算法解決,那麼暴力就是能想到的第一種解法思路。post

下面咱們來看一下暴力解題法,怎麼去作?首先咱們能夠想到學習

列舉 數組中的全部可能的連續區間/子集?而後把連續區間相加,求和,判斷最大.優化

解法一: 時間複雜度 O(n^3)

public int maxSubArray(int[] nums) {
        int max = Integer.MIN_VALUE;
        int sum;
        //使用兩個for循環來列舉出全部的連續區間/子集
        for (int i = 0; i < nums.length; i++) {
            for (int j = i; j < nums.length; j++) {
                sum = 0;
                //列舉出來的子集,在經過一個for循環相加
                for (int k = i; k <= j; k++) {
                    sum += nums[k];
                }
                if (sum > max) {
                    max = sum;
                }
            }
        }
        return max;
    }
複製代碼

解法二: 時間複雜度 O(n^2)

其實咱們在列舉的過程當中就能夠把和相加起來,進行判斷,全部有了解法二網站

public int maxSubArray(int[] nums) {
        int max = Integer.MIN_VALUE;
        int sum;
        for (int i = 0; i < nums.length; i++) {
            sum = 0;
            for (int j = i; j < nums.length; j++) {
            //列舉子序列求和的過程當中解決此問題
                sum += nums[j];
                if (sum > max)
                    max = sum;
            }
        }
        return max;
    }
複製代碼

解法三:動態規劃 時間複雜度O(n)

看了上述的暴力解題法,咱們發現一些能夠優化的東西,由於他有重複的子結構性質,那就是每次相加都從開始的那個元素去相加,那咱們能夠從動態規劃的角度想一下這個問題。

設sum[i]爲以第i個元素結尾且和最大的連續子數組,那咱們能夠把上述問題描述爲這樣的一個公式

sum[i] 
= max(sum[i-1] + nums[i], nums[i])

把公式化解一下就是這樣的.

sum[i]=\begin{cases}
nums[i] & if(sum[i-1] <= 0) \\
sum[i-1] + nums[i] & if(sum[i-1]> 0)
\end{cases}

那麼算法能夠描述爲

public int maxSubArray(int[] nums) {
        int sums[] = new int[nums.length];
        sums[0] = nums[0];
        int max = nums[0];

        for (int i = 1; i < nums.length; i++) {
            if (sums[i-1] > 0) sums[i] += nums[i];
            else sums[i] = nums[i];
            if (max < sums[i]) max = sums[i];
        }
        return max;
    }
複製代碼

這樣算法的空間複雜度爲O(n),若是原數據數組nums能夠被更改,那麼咱們的sums數組能夠省略.

public int maxSubArray(int[] nums) {
        int currnetValue = nums[0];
        int max = nums[0];
        for (int i = 1; i < nums.length; i++) {
            if (currnetValue > 0) currnetValue += nums[i];
            else currnetValue = nums[i];
            if (max < currnetValue) max = currnetValue;
        }
        return max;
    }
複製代碼

n層for循環求解 C_n^m

A_n^m 排列問題和組合問題很類似(經典問題有全排列),這裏列舉一道 C_n^m相關的題

給定一個無重複元素的數組candidates和一個目標數target,找出candidates中全部可使數字和爲target的組合。
candidates中的數字能夠無限制重複被選取。

說明: 全部數字(包括target)都是正整數。 解集不能包含重複的組合。
示例1: 輸入: candidates = [2,3,6,7], target = 7, 所求解集爲: [ [7], [2,2,3] ]

示例2: 輸入: candidates = [2,3,5], target = 8, 所求解集爲: [ [2,2,2,2], [2,3,3], [3,5] ]

解法

按個人思路來,第一就去想暴力解法,而後再去想辦法優化(若是你以前作過這題,或者直接能想到什麼動態規劃算法,能夠直接使用,不要在暴力。)

想講一個暴力通用解法模型,就是本節所說的n層for循環求解 C_n^m. 這能夠解決不少與組合相關的問題,如密碼暴力破解等.

給你一個字符串,列舉裏面全部可能的三位密碼,(字符串裏面字母都不相同,不考慮密碼順序)

這樣咱們能很簡單的想到一個算法

int len = str.length();
    for(int i = 0;i < len - 2;i++){
        for(int j = i +1;j<len - 1;j++){
            for(int k = j+1;k<len;k++){
                String password = ""+ str.charAt(i) + str.charAt(j) + str.charAt(k);
            }
        }
    }
複製代碼

那要是如今列舉100位密碼呢?不可能寫這樣的循環了吧?

通常解法,是使用遞歸來替代n層for循環,通常解法模型,僞代碼以下

void find(int start,int count,int passwordLen,int array[]){
    if(count == passwordLen){
        //獲得結果
        return;
    }
    for(int i = start;i<array.length;i++){
        find(i + 1,count+1,passwordLen,array);
    }
}
複製代碼

那麼上面那道題,使用這個模型來解決代碼以下:

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> listAll = new ArrayList<>();
        List<Integer> currentList = new ArrayList<>();
        Arrays.sort(candidates);
        find(listAll,currentList,candidates,target,0);
        return listAll;
    }
    
    
    private void find(List<List<Integer>> listAll,List<Integer> currentList,int[] candidates,int target,int currentIndex) {
        if(target < 0) return;
        if(target == 0){
            listAll.add(currentList);
        }else{
            for(int i = currentIndex;i<candidates.length && candidates[i]<=target;i++){
                List<Integer> list=new ArrayList<>(currentList);
                list.add(candidates[i]);
                find(listAll,list,candidates,target - candidates[i],i);
                //currentList.remove(currentList.size()-1);
            }
        }
    }
}
複製代碼

爬樓梯

假設你正在爬樓梯。須要 n 階你才能到達樓頂。 每次你能夠爬 1 或 2 個臺階。你有多少種不一樣的方法能夠爬到樓頂呢?

若是你以前見過這題,那應該知道這是一個斐波那契數列(類似的問題還有不少,如兔子繁殖問題等都是斐波那契數列),能夠描述爲

f(n) = f(n-1)+ f(n-2)

那麼求這個公式怎麼解呢?咱們能夠想到一個遞歸過程,以下

public int fib(int n) {
    if(n <= 2) return 1;
    else{
        return fib(n-1) + fib(n-2);
    }
}
複製代碼

咱們知道遞歸過程當中會有不少重複計算,這裏列舉一張法f(5)的計算過程,以下

那咱們怎麼解決這個問題呢,使用一個數組來保存結果,避免重複計算,(也被成爲備忘錄法則),這裏其實已是動態規劃了。

public int fib(int n) {
        if(n <= 2) return 1;
        if(save[n] > 0){
            return save[n];
        }else{
            save[n] = save[n-1]+save[n-2];
            return save[n];
        }
}
複製代碼

咱們知道遞歸有一個函數棧的調用過程,那咱們能夠嘗試把遞歸去了

public int fib(int n) {
        save[0] = 1;
        save[1] = 1;
        for(int i = 2;i < n;i++){
            save[i] = save[i-1]+ save[i-2];
        }
        return save[n-1];
    }
}
複製代碼

其實這個若是隻是一次計算f(n)的結果,不用使用數組,能夠優化爲

int v1 = 1,v2=1;
        for(int i = 2;i < n;i++){
            int temp = v1 + v2;
            v1 = v2;
            v2 = temp;
        }
        return v2;
    }
複製代碼

拓展:看完上述內容,你應該能輕鬆解決,若是樓梯不是一次只能上1或者2層,他能上任意層,怎麼解決這個問題呢?歡迎留言區評論

關於二維數組

二維數組是在一維數組的基礎上,增長了縱向。那二維數組通常的暴力解法和一維數組基本相似。常見的二維數組經典題目有 【n皇后問題】【走迷宮】 對應的回溯算法,也是暴力算法的一種,有一個剪枝,優化過程。 在這裏舉一個二維數組例子。

一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲「Start」 )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲「Finish」)。 問總共有多少條不一樣的路徑?

有了上面的暴力解題思路過程,而且此題也有一些重疊子問題,那咱們這題,直接考慮動態規劃解決. 能夠想到遞歸函數爲

sum[i][j]=\begin{cases}
1 & if(i == 0 || j == 0) \\
sum[i-1][j] + sum[i][j-1] & if(i > 0 \&\& j > 0)
\end{cases}

根據遞歸公式代碼,也能輕鬆寫出

class Solution {
    public int uniquePaths(int m, int n) {
        int sum[][] = new int[m][n];
        for(int i = 0;i<m;i++){
            sum[i][0] = 1;
        }
         for(int i = 0;i<n;i++){
            sum[0][i] = 1;
        }
        for(int i = 1;i<m;i++){
            for(int j = 1;j<n;j++){
                sum[i][j] = sum[i-1][j] + sum[i][j-1];
            }
        }
        return sum[m-1][n-1];
    }
}
複製代碼

總結

本文主要介紹了一些數組的算法,一些解題思路,從問題的暴力解法開始(通用的暴力解法:兩個for循環列舉,n層for循環等),逐步優化,一些能想到的優化方式,節省空間,時間。到後面一些能夠用動態規劃解決的問題,(此處沒有列舉一些回溯常看法法,可是思路都是從暴力解法開始,逐步剪枝,優化的過程)

文末

由於本人技術有限,文中若有錯誤之處,還請大佬指教,感謝.

文中題目來源 LeetCode
若是你正在準備面試,或者你喜歡算法,刷題推薦LeetCode,挺不錯的網站.

若是你對圖片,Canvas感興趣,推薦閱讀

Android:讓你的「女神」逆襲,代碼擼彩妝(畫妝)
Flutter PIP(畫中畫)效果的實現

相關文章
相關標籤/搜索