leetcode題解(陣列問題)

吳軍旗發表於2019-03-04

面試中的演算法問題,有很多並不需要複雜的資料結構支撐。就是用陣列,就能考察出很多東西了。其實,經典的排序問題,二分搜尋等等問題,就是在陣列這種最基礎的結構中處理問題的,今天主要學習常見的陣列中處理問題的方法。

陣列中的問題其實最常見。

  • 排序:選擇排序;插入排序;歸併排序;快速排序
  • 查詢:二分查詢法
  • 資料結構:棧;佇列;堆

從二分查詢法看如何寫出正確的程式

  • 建立一個基礎的框架
  • 什麼是正確的程式

二分查詢法

  • 二分查詢法的思想在1946年提出。
  • 第一個沒有bug的二分查詢法在1962年才出現。
  • 對於有序數列,才能使用二分查詢法 (排序的作用)

leetcode題解(陣列問題)

需要注意的問題

  • 宣告變數的時候,明確變數的意義,並且在書寫整個邏輯的時候,要不聽的維護住這個變數的意義。
  • 初始值問題
  • 邊界問題

    template<typename T>
    int binarySearch( T arr[], int n, T target ){
    
        int l = 0, r = n-1; // 在[l...r]的範圍裡尋找target:前閉後閉
        while( l <= r ){    // 只要還有可以查詢的內容。當 l == r時,區間[l...r]依然是有效的
            int mid = l + (r-l)/2;
            if( arr[mid] == target ) return mid;
            //mid已經判斷過了
            if( target > arr[mid] )
                l = mid + 1;  // target在[mid+1...r]中; [l...mid]一定沒有target
            else    // target < arr[mid]
                r = mid - 1;  // target在[l...mid-1]中; [mid...r]一定沒有target
        }
    
        return -1;
    }
    
複製程式碼

** 迴圈不變數。宣告不變。控制邊界。**


int l = 0, r = n-1; // 在[l...r]的範圍裡尋找target:前閉後閉

複製程式碼

改變變數定義,依然可以寫出正確的演算法


    template<typename T>
    int binarySearch( T arr[], int n, T target ){
    
        int l = 0, r = n; // target在[l...r)的範圍裡,這樣設定才能保證長度為n
        while( l < r ){    // 當 l == r時,區間[l...r)是一個無效區間 [42,43)
            int mid = l + (r-l)/2;
            if( arr[mid] == target ) return mid;
            if( target > arr[mid] )
                l = mid + 1;    // target在[mid+1...r)中; [l...mid]一定沒有target
            else// target < arr[mid]
                r = mid;        // target在[l...mid)中; [mid...r)一定沒有target
        }
    
        return -1;
    }
    
複製程式碼

注意

  • 求mid值是採用(l+r)/2容易整形溢位
  • 採用mid = l + (r-l)/2;

如何寫出正確的程式?

  • 明確變數的含義
  • 迴圈不變數
  • 小資料量除錯(4到6個資料)空集,邊界(小資料集上程式碼如何運作的)
  • 耐心找到bug,定位錯誤發生的位置。
  • 大資料量測試(效能)

LeetCode題解: Move Zeros

paste image

直觀的解題思路

leetcode題解(陣列問題)

  • 拿出非0元素
  • 將非0元素拿出來,然後空位補0

    class Solution {
    public:
        // 時間複雜度 O(n)
        // 空間複雜度 O(n) 新建立陣列
        void moveZeroes(vector<int>& nums) {
    
            vector<int> nonZeroElements;
    
            // 將vec中所有非0元素放入nonZeroElements中
            for( int i = 0 ; i < nums.size() ; i ++ )
                if( nums[i] )
                    nonZeroElements.push_back( nums[i] );
    
            // 將nonZeroElements中的所有元素依次放入到nums開始的位置
            for( int i = 0 ; i < nonZeroElements.size() ; i ++ )
                nums[i] = nonZeroElements[i];
    
            // 將nums剩餘的位置放置為0
            for( int i = nonZeroElements.size() ; i < nums.size() ; i ++ )
                nums[i] = 0;
        }
    };
    
    int main() {
    
        int arr[] = {0, 1, 0, 3, 12};
        //根據生成的資料建立vector:傳入頭指標和尾指標
        vector<int> vec(arr, arr + sizeof(arr)/sizeof(int));
    
        Solution().moveZeroes(vec);
    
        for( int i = 0 ; i < vec.size() ; i ++ )
            cout<<vec[i]<<" ";
        cout<<endl;
    
        return 0;
    }


複製程式碼

即使簡單的演算法也能進一步優化。

  • 不開闢額外空間
  • k - [0…k)中儲存所有當前遍歷過的非0元素

    class Solution {
    public:
        // 時間複雜度 O(n)
        // 空間複雜度 O(1)
        void moveZeroes(vector<int>& nums) {
    
            int k = 0; // nums中, [0...k)的元素均為非0元素
    
            // 遍歷到第i個元素後,保證[0...i]中所有非0元素
            // 都按照順序排列在[0...k)中
            for(int i = 0 ; i < nums.size() ; i ++ )
                if( nums[i] )
                    nums[k++] = nums[i];
    
            // 將nums剩餘的位置放置為0
            for( int i = k ; i < nums.size() ; i ++ )
                nums[i] = 0;
        }
    };
    
    int main() {
    
        int arr[] = {0, 1, 0, 3, 12};
        vector<int> vec(arr, arr + sizeof(arr)/sizeof(int));
    
        Solution().moveZeroes(vec);
    
        for( int i = 0 ; i < vec.size() ; i ++ )
            cout<<vec[i]<<" ";
        cout<<endl;
    
        return 0;
    }
    
複製程式碼

進一步優化

  • 非0的賦值不用操作了。

  • 非0的與0直接互換。


    class Solution {
    public:
        // 時間複雜度 O(n)
        // 空間複雜度 O(1)
        void moveZeroes(vector<int>& nums) {
    
            int k = 0; // nums中, [0...k)的元素均為非0元素
    
            // 遍歷到第i個元素後,保證[0...i]中所有非0元素
            // 都按照順序排列在[0...k)中
            // 同時, [k...i] 為0
            for(int i = 0 ; i < nums.size() ; i ++ )
                if( nums[i] )
                    swap( nums[k++] , nums[i] );
    
        }
    };
    
    
複製程式碼

** 極端情況:如果都為非0,則每個都自己和自己交換**


    class Solution {
    public:
        // 時間複雜度 O(n)
        // 空間複雜度 O(1)
        void moveZeroes(vector<int>& nums) {
    
            int k = 0; // nums中, [0...k)的元素均為非0元素
    
            // 遍歷到第i個元素後,保證[0...i]中所有非0元素
            // 都按照順序排列在[0...k)中
            // 同時, [k...i] 為0
            for(int i = 0 ; i < nums.size() ; i ++ )
                if( nums[i] )
                    //
                    if( k != i )
                        swap( nums[k++] , nums[i] );
                    else// i == k
                        k ++;
        }
    };
    
    
複製程式碼

相似題目

  • leetcode 27
  • leetcode 26
  • leetcode 80

注意的問題

  • 如何定義刪除?從陣列中去除?還是放在陣列末尾?
  • 剩餘元素的排列是否要保證原有的相對順序?
  • 是否有空間複雜度的要求? O(1)

基礎演算法思路的應用

75 Sort Colors

paste image

基數排序法


    // 時間複雜度: O(n)
    // 空間複雜度: O(k), k為元素的取值範圍
    // 對整個陣列遍歷了兩遍
    class Solution {
    public:
        void sortColors(vector<int> &nums) {
    
            int count[3] = {0};    // 存放0,1,2三個元素的頻率
            for( int i = 0 ; i < nums.size() ; i ++ ){
                assert( nums[i] >= 0 && nums[i] <= 2 );
                count[nums[i]] ++;
            }
    
            int index = 0;
            for( int i = 0 ; i < count[0] ; i ++ )
                nums[index++] = 0;
            for( int i = 0 ; i < count[1] ; i ++ )
                nums[index++] = 1;
            for( int i = 0 ; i < count[2] ; i ++ )
                nums[index++] = 2;
    
            // 小練習: 更加自使用的計數排序
        }
    };
    
    int main() {
    
        int nums[] = {2, 2, 2, 1, 1, 0};
        vector<int> vec = vector<int>( nums , nums + sizeof(nums)/sizeof(int));
    
        Solution().sortColors( vec );
        for( int i = 0 ; i < vec.size() ; i ++ )
            cout<<vec[i]<<" ";
        cout<<endl;
    
        return 0;
    }
   
複製程式碼

可以只掃描一遍麼?

leetcode題解(陣列問題)

一次三路快排

設定三個索引:zero two i

leetcode題解(陣列問題)

三路快排


    // 時間複雜度: O(n)
    // 空間複雜度: O(1)
    // 對整個陣列只遍歷了一遍
    class Solution {
    public:
        void sortColors(vector<int> &nums) {
    
            int zero = -1;          // [0...zero] == 0
            int two = nums.size();  // [two...n-1] == 2
            for( int i = 0 ; i < two ; ){
                if( nums[i] == 1 )
                    i ++;
                else if ( nums[i] == 2 )
                    swap( nums[i] , nums[--two]);
                else{ // nums[i] == 0
                    assert( nums[i] == 0 );
                    swap( nums[++zero] , nums[i++] );
                }
            }
        }
    };
    
    int main() {
    
        int nums[] = {2, 2, 2, 1, 1, 0};
        vector<int> vec = vector<int>( nums , nums + sizeof(nums)/sizeof(int));
    
        Solution().sortColors( vec );
        for( int i = 0 ; i < vec.size() ; i ++ )
            cout<<vec[i]<<" ";
        cout<<endl;
    
        return 0;
    }
    

複製程式碼

相似題目

  • 88 Merge Sorted Array
  • 215 Kth Largest Element in an Array

雙索引技術-對撞指標

167 兩數之和 II - 輸入有序陣列

paste image

需要考慮的問題

  • 如果沒有解怎樣?保證有解
  • 如果有多個解怎樣?返回任意解

解法

  • 最直接的思考:暴力解法。雙層遍歷,O(n^2)

    • 暴力解法沒有充分利用原陣列的性質 —— 有序:有序?二分搜尋?
  • 二分搜尋法

    • 對於每個i, 在剩餘陣列中查詢target-nums[i]的值
    • 時間複雜度為O(NlogN)
      有序的二分搜尋
  • 對撞指標

初始化的ij

  • 一般會是大於或者小於。
  • 如果大i++ 小 j--
  • 兩個索引在往中間走。對撞指標。

程式碼實現


    // 時間複雜度: O(n)
    // 空間複雜度: O(1)
    class Solution {
    public:
        vector<int> twoSum(vector<int>& numbers, int target) {
    
            assert( numbers.size() >= 2 );
            // assert( isSorted(numbers) );
    
            int l = 0, r = numbers.size()-1;
            while( l < r ){
    
                if( numbers[l] + numbers[r] == target ){
                    int res[2] = {l+1, r+1};
                    return vector<int>(res, res+2);
                }
                else if( numbers[l] + numbers[r] < target )
                    l ++;
                else // numbers[l] + numbers[r] > target
                    r --;
            }
    
            throw invalid_argument("the input has no solution");
        }
    
    };
複製程式碼

相似題目

  • 125 Valid Palindrome
    • 空字串如何看?
    • 字元的定義?
    • 大小寫問題
  • 344 Reverse String
  • 345 Reverse Vowels of a String
  • 11 Container With Most Water

雙索引技術-滑動視窗

209長度最小的子陣列

paste image

什麼是子陣列

什麼叫子陣列?

  • 一般不要求連續
  • 而這個題目中規定了子陣列要連續這樣的特性。
    • 如果沒有解怎麼辦?返回0

暴力解O(N^3)

  • 計算其和sum,驗證sum >= s
  • 時間複雜度O(n^3)

程式碼實現


int minSubArrayLen(int s, vector<int>& nums) {

        assert(s > 0);

        int res = nums.size() + 1;
        for(int l = 0 ; l < nums.size() ; l ++)
            for(int r = l ; r < nums.size() ; r ++){
                int sum = 0;
                for(int i = l ; i <= r ; i ++)
                    sum += nums[i];
                if(sum >= s)
                    res = min(res, r - l + 1);
            }

        if(res == nums.size() + 1)
            return 0;

        return res;
    }
複製程式碼

暴力解的優化O(N^2)


int minSubArrayLen(int s, vector<int>& nums) {

        assert(s > 0);

        // sums[i]存放nums[0...i-1]的和
        vector<int> sums(nums.size() + 1, 0);
        for(int i = 1 ; i <= nums.size() ; i ++)
            sums[i] = sums[i-1] + nums[i-1];

        int res = nums.size() + 1;
        for(int l = 0 ; l < nums.size() ; l ++)
            for(int r = l ; r < nums.size() ; r ++){
                // 使用sums[r+1] - sums[l] 快速獲得nums[l...r]的和
                if(sums[r+1] - sums[l] >= s)
                    res = min(res, r - l + 1);
            }

        if(res == nums.size() + 1)
            return 0;

        return res;
    }
    
複製程式碼

滑動視窗解

leetcode題解(陣列問題)

  • 如果當前子陣列不到就往後再看一個

  • 視窗不停向前滑動。


// 滑動視窗的思路
// 時間複雜度: O(n)
// 空間複雜度: O(1)
class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        //nums[l...r]為我們的滑動視窗
        int l = 0, r = -1;
        int sum = 0;
        int res = nums.size() + 1;
        while(l < nums.size()){
            if(r+1 < nums.size() && sum < s){
                r++;
                sum += nums[r];
            }else{
                sum -= nums[l];
                l++;
            }
            
            if(sum >= s){
                res = min(res, r - l + 1);
            }
        }
        
        if(res == nums.size() + 1)
            return 0;
        return res;
    }
};

複製程式碼

在滑動視窗中做記錄

無重複字元的最長子串

paste image

注意

字符集?只有字母?數字+字母?ASCII?
大小寫是否敏感?

思路

往後++

  • j++如果沒有重複元素,視窗j繼續往後
  • 如果有重複元素,i++去除重複
  • freq[256]記錄視窗中的元素

leetcode題解(陣列問題)

實現程式碼


    class Solution {
    public:
        int lengthOfLongestSubstring(string s) {
    
            int freq[256] = {0};
    
            int l = 0, r = -1; //滑動視窗為s[l...r]
            int res = 0;
    
            // 整個迴圈從 l == 0; r == -1 這個空視窗開始
            // 到l == s.size(); r == s.size()-1 這個空視窗截止
            // 在每次迴圈裡逐漸改變視窗, 維護freq, 並記錄當前視窗中是否找到了一個新的最優值
            while( l < s.size() ){
    
                if( r + 1 < s.size() && freq[s[r+1]] == 0 )
                    freq[s[++r]] ++;
                else    //r已經到頭 || freq[s[r+1]] == 1
                    freq[s[l++]] --;
    
                res = max( res , r-l+1);
            }
    
            return res;
        }
    };
    
    int main() {
    
        cout << Solution().lengthOfLongestSubstring( "abcabcbb" )<<endl;
        cout << Solution().lengthOfLongestSubstring( "bbbbb" )<<endl;
        cout << Solution().lengthOfLongestSubstring( "pwwkew" )<<endl;
        cout << Solution().lengthOfLongestSubstring( "" )<<endl;
    
        return 0;
    }
    
複製程式碼

相似題目

  • 438 Find All Anagrams in a String

    • 字符集範圍?英文小寫字母
    • 返回的解的順序?任意。
  • 76 Minimum Window Substring

    • 字符集範圍
    • 若沒有解? 返回“”
      *若有多個解?保證只有一個解
    • 什麼叫包含所有字元?S = “a”,T = “aa”

-------------------------華麗的分割線--------------------

看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。

個人部落格番茄技術小棧掘金主頁

想了解更多,歡迎關注我的微信公眾號:番茄技術小棧

番茄技術小棧

相關文章