面試中的演算法問題,有很多並不需要複雜的資料結構支撐。就是用陣列,就能考察出很多東西了。其實,經典的排序問題,二分搜尋等等問題,就是在陣列這種最基礎的結構中處理問題的,今天主要學習常見的陣列中處理問題的方法。
陣列中的問題其實最常見。
- 排序:選擇排序;插入排序;歸併排序;快速排序
- 查詢:二分查詢法
- 資料結構:棧;佇列;堆
從二分查詢法看如何寫出正確的程式
- 建立一個基礎的框架
- 什麼是正確的程式
二分查詢法
- 二分查詢法的思想在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:前閉後閉
複製程式碼
改變變數定義,依然可以寫出正確的演算法
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
直觀的解題思路
- 拿出非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
基數排序法
// 時間複雜度: 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
- 215 Kth Largest Element in an Array
雙索引技術-對撞指標
167 兩數之和 II - 輸入有序陣列
需要考慮的問題
- 如果沒有解怎樣?保證有解
- 如果有多個解怎樣?返回任意解
解法
-
最直接的思考:暴力解法。雙層遍歷,O(n^2)
- 暴力解法沒有充分利用原陣列的性質 —— 有序:有序?二分搜尋?
-
二分搜尋法
- 對於每個i, 在剩餘陣列中查詢target-nums[i]的值
- 時間複雜度為O(NlogN)
-
對撞指標
- 一般會是大於或者小於。
- 如果大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長度最小的子陣列
什麼是子陣列
- 一般不要求連續
- 而這個題目中規定了子陣列要連續這樣的特性。
- 如果沒有解怎麼辦?返回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;
}
複製程式碼
滑動視窗解
-
如果當前子陣列不到就往後再看一個
-
視窗不停向前滑動。
// 滑動視窗的思路
// 時間複雜度: 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;
}
};
複製程式碼
在滑動視窗中做記錄
無重複字元的最長子串
注意
字符集?只有字母?數字+字母?ASCII?
大小寫是否敏感?
思路
- j++如果沒有重複元素,視窗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
- 字符集範圍?英文小寫字母
- 返回的解的順序?任意。
-
76 Minimum Window Substring
- 字符集範圍
- 若沒有解? 返回“”
*若有多個解?保證只有一個解 - 什麼叫包含所有字元?S = “a”,T = “aa”
-------------------------華麗的分割線--------------------
看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。
想了解更多,歡迎關注我的微信公眾號:番茄技術小棧