1. 程式人生 > >玩轉演算法面試:(三)LeetCode陣列類問題

玩轉演算法面試:(三)LeetCode陣列類問題

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

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

如何寫出正確的程式
建立一個基礎的框架,什麼是正確的程式

二分查詢法:
- 二分查詢法的思想在1946年提出。
- 第一個沒有bug的二分查詢法在1962年才出現。

對於有序數列,才能使用二分查詢法 (排序的作用)

二分查詢:前提排序

會用自然語言描述誰都會,但是具體實現就會遇到問題。

  • 邊界問題
  • 索引的指向
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:前閉後閉

main.cpp:

int main() {

    int n = pow(10,7);
    int* data = MyUtil::generateOrderedArray(n);

    clock_t startTime = clock();
    for( int i = 0 ; i < n ; i ++ )
        assert( i == binarySearch(data, n, i) );
    clock_t endTime = clock();

    cout<<"binarySearch test complete."<<endl;
    cout<<"Time cost: "<<double(endTime - startTime)/CLOCKS_PER_SEC<<" s"<<endl;
    return 0;
}

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

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

邊界量的推導。

int mid = (l+r)/2
int mid = l + (r-l)/2;

直接相加存在整型溢位的風險。

l加上範圍長度的一半。

如何寫出正確的程式?

  • 明確變數的含義
  • 迴圈不變數
  • 小資料量除錯(4到6個數據)空集,邊界。
  • 大資料量測試(效能)

耐心找到bug。定位錯誤發生的位置。小資料集上程式碼如何運作的。

LeetCode上的283 Move Zeros

https://leetcode.com/problems/move-zeroes/

公司:Facebook & Bloomberg

Given an array nums, write a function to move all 0's to the end of it while maintaining the relative order of the non-zero elements.

For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0].

Note:
You must do this in-place without making a copy of the array.
Minimize the total number of operations.

給定一個數組nums,寫一個函式,將陣列中所有的0挪到陣列的末尾,而維持其他所有非0元素的相對位置。

舉例: nums = [0, 1, 0, 3, 12],函式執行後結果為[1, 3, 12, 0, 0]

解題的模板

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        //書寫函式的邏輯
    }
};

不要糾結語言,完全可以使用自己熟悉的語言。

最直觀思路:

拿出非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;
}

執行結果:

執行結果

基本測試用例測試

accept

測試細節

21個測試用例都通過了。

圖表展示了效率之間的差異。

擊敗了97.71

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

因為非0的一定小於等於有0的。

不要開闢新空間

1直接放到0的位置。k指向空位置。
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 ++;
    }
};

27:Remove Element

Given an array and a value, remove all instances of that value in place and return the new length.
Do not allocate extra space for another array, you must do this in place with constant memory.
The order of elements can be changed. It doesn't matter what you leave beyond the new length.

Example:
Given input array nums = [3,2,2,3], val = 3
Your function should return length = 2, with the first two elements of nums being 2.

給定一個數組nums和一個數值val,將陣列中所有等於val的元素刪除,並返回剩餘的元素個數。

如nums = [3, 2, 2, 3], val = 3;
返回2,且nums中前兩個元素為2

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

26:Remove Duplicates from Sorted Array

公司:Facebook & Microsoft & Bloomberg

Given a sorted array, remove the duplicates in place such that each element appear only once and return the new length.

Do not allocate extra space for another array, you must do this in place with constant memory.

For example,
Given input array nums = [1,1,2],

Your function should return length = 2, with the first two elements of nums being 1 and 2 respectively. It doesn't matter what you leave beyond the new length.

給定一個有序陣列,對陣列中的元素去重,使得原陣列的每個元素只有一個。返回去重後陣列的長度值。

  • 如 nums = [1, 1, 2],

  • 結果應返回2,且nums的前兩個元素為1和2

  • 如何定義刪除?從陣列中去除?還是放在陣列末尾?

  • 剩餘元素的排列是否要保證原有的相對順序?

  • 是否有空間複雜度的要求? O(1)

80 Remove Duplicated from Sorted Array II

Facebook

Follow up for "Remove Duplicates":
What if duplicates are allowed at most twice?

For example,
Given sorted array nums = [1,1,1,2,2,3],

Your function should return length = 5, with the first five elements of nums being 1, 1, 2, 2 and 3. It doesn't matter what you leave beyond the new length.

給定一個有序陣列,對陣列中的元素去重,使得原陣列的每個元素最多保留兩個。返回去重後陣列的長度值。

如 nums = [1, 1, 1, 2, 2, 3],
結果應返回5,且nums的前五個元素為1, 1, 2, 2, 3

基礎演算法思路的應用

75 Sort Colors

FaceBook Microsoft PocketGems

Given an array with n objects colored red, white or blue, sort them so that objects of the same color are adjacent, with the colors in the order red, white and blue.

Here, we will use the integers 0, 1, and 2 to represent the color red, white, and blue respectively.

Note:
You are not suppose to use the library's sort function for this problem.

click to show follow up.

Follow up:
A rather straight forward solution is a two-pass algorithm using counting sort.
First, iterate the array counting number of 0's, 1's, and 2's, then overwrite array with total number of 0's, then 1's and followed by 2's.

Could you come up with an one-pass algorithm using only constant space?

給定一個有n個元素的陣列,陣列中元素的取值只有0, 1, 2三種可能。為這個陣列排序。

  • 可以使用任意一種排序演算法
  • 沒有使用上題目中給出的特殊條件

所有元素取值只有0/1/2這三種。

數出陣列中共有多少元素,然後排序。計數排序:分別統計0, 1, 2的元素個數。

統計

基數排序法

// 時間複雜度: 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;
}

可以只掃描一遍麼?

一次三路快排

設定三個索引:zero two i

三路快排

// 時間複雜度: 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

Facebook & Microsoft & Bloomberg

給定兩個有序整型陣列nums1, nums2,將nums2的元素歸併到nums1中

215 Kth Largest Element in an Array

在一個整數序列中尋找第k大的元素
如給定陣列 [3, 2, 1, 5, 6, 4], k = 2, 結果為5
利用快排partition中,將pivot放置在了其正確的位置上的性質

利用快排partition中,將pivot放置在了其正確的位置上的性質

快排分割槽

雙索引技術 Two Pointer

167 Two Sum II - Input array is sorted

給定一個有序整型陣列和一個整數target,在其中尋找兩個元素,使其和為target。返回這兩個數的索引。
如numbers = [2, 7, 11, 15], target = 9
返回數字2,7的索引1, 2 (索引從1開始計算)

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

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

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

有序的二分搜尋

初始化的ij

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

// 時間複雜度: O(n^2)
// 空間複雜度: O(1)
class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {

        assert( numbers.size() >= 2 );
        // assert( isSorted(numbers) );

        for( int i = 0 ; i < numbers.size() ; i ++ )
            for( int j = i+1 ; j < numbers.size() ; j ++ )
                if( numbers[i] + numbers[j] == target ){
                    int res[2] = {i+1, j+1};
                    return vector<int>(res, res+2);
                }


        throw invalid_argument("the input has no solution");
    }

};

int main() {

    int nums[] = {2, 7, 11, 15};
    vector<int> vec(nums, nums + sizeof(nums)/sizeof(int));
    int target = 9;

    vector<int> res = Solution().twoSum( vec, target );
    for( int i = 0 ; i < res.size() ; i ++ )
        cout<<res[i]<<" ";
    cout<<endl;

    return 0;
}

對撞指標程式碼實現如下:

// 時間複雜度: 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");
    }

};

錯誤處理:

throw invalid_argument("the input has no solution");

三路快排也是對撞指標思路

對撞指標思路:

125 Valid Palindrome

給定一個字串,只看其中的數字和字母,忽略大小寫,判斷這個字串是否為迴文串?

“A man, a plan, a canal; Panama” - 是迴文串
“race a car” - 不是迴文串

迴文串:正著看和反著看是一樣的。

  • 空字串如何看?
  • 字元的定義?
  • 大小寫問題

344 Reverse String

給定一個字串,返回這個字串的倒序字串。

  • 如“hello”,返回”olleh”
  • 類似:翻轉一個數組

345 Reverse Vowels of a String

給定一個字串,將該字串中的母音字母翻轉
如:給出 ”hello”,返回”holle”
如:給出“leetcode”,返回“leotcede”
母音不包含y

11 Container With Most Water

給出一個非負整數陣列 a1,a2,a3,…,an;每一個整數表示一個豎立在座標軸x位置的一堵高度為ai的“牆”,選擇兩堵牆,和x軸構成的容器可以容納最多的水。

任取兩牆

盛水為最大值。

雙索引技術 Two Pointer

滑動視窗

Minimum Size Subarray Sum

給定一個整型陣列和一個數字s,找到陣列中最短的一個連續子陣列,使得連續子陣列的數字和sum >= s,返回這個最短的連續子陣列的長度值
如,給定陣列[2, 3, 1, 2, 4, 3], s = 7
答案為[4, 3],返回2

什麼叫子陣列?

一般不要求連續

而這個題目中規定了子陣列要連續這樣的特性。

  • 什麼叫子陣列
  • 如果沒有解怎麼辦?返回0

暴力解:遍歷所有的連續子陣列[i...j]
計算其和sum,驗證sum >= s
時間複雜度O(n^3)

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

視窗不停向前滑動。

// 滑動視窗的思路
// 時間複雜度: O(n)
// 空間複雜度: O(1)
class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {

        int l = 0 , r = -1; // nums[l...r]為我們的滑動視窗
        int sum = 0;
        int res = nums.size()+1;

        while( l < nums.size() ){   // 視窗的左邊界在陣列範圍內,則迴圈繼續

            if( r + 1 < nums.size() && sum < s )
                sum += nums[++r];
            else // r已經到頭 或者 sum >= s
                sum -= nums[l++];

            if( sum >= s )
                res = min(res, r-l+1);
        }

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

int main() {

    int nums[] = {2, 3, 1, 2, 4, 3};
    vector<int> vec( nums, nums + sizeof(nums)/sizeof(int) );
    int s = 7;

    cout<<Solution().minSubArrayLen(s, vec)<<endl;

    return 0;
}

另一個滑動視窗的例子

3 Longest Substring Without Repeating Characters

在一個字串中尋找沒有重複字母的最長子串,返回其長度。

  • 如”abcabcbb”,則結果為”abc”,長度為3
  • 如”bbbbb”,則結果為”b”,長度為1
  • 如”pwwkew”,則結果為”wke”,長度為3

考慮:

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

往後++

j++如果沒有重複元素。繼續加加。

i++去除重複

如何記錄重複字元?freq[256]

實現程式碼:

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

給定一個字串s和一個非空字串p,找出p中的所有是s的anagrams字串的子串,返回這些子串的起始索引。
如 s = "cbaebabacd" p = “abc”,返回[0, 6]
如 s = "abab" p = “ba”,返回[0, 1, 2]

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

76 Minimum Window Substring

給定一個字串S和T,在S中尋找最短的子串,包含T中的所有字元

  • 如 S = “ADOBECODEBANC" ; T = "ABC"
  • 結果為 "BANC"

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



作者:天涯明月笙
連結:https://www.jianshu.com/p/fa9a9701a869