給定一個整數陣列 nums,按要求返回一個新陣列 counts。陣列 counts 有該性質: counts[i]
的值是 nums[i]
右側小於 nums[i]
的元素的數量。
示例:
輸入: [5,2,6,1] 輸出:[2,1,1,0] //解釋:
5 的右側有 2 個更小的元素 (2 和 1). 2 的右側僅有 1 個更小的元素 (1). 6 的右側有 1 個更小的元素 (1). 1 的右側有 0 個更小的元素.
1. 暴力模擬法
暴力模擬法思路非常簡單,就是每次都從末尾找比num[i]小的數並計數,然後放到結果陣列即可。
1 vector<int> countSmaller(vector<int>& nums) { 2 int n=nums.size(); 3 vector<int>ans(n,0); 4 int t; 5 for(int i=n-2;i>=0;i--){ 6 t=0; 7 for(int j=n-1;j>i;j--){ 8 if(nums[j]<nums[i]) 9 t++; 10 } 11 ans[i]=t; 12 } 13 return ans; 14 }
但這樣帶來的演算法效率會非常的低,原因在於每次尋找比num[i]小的數都需要從末尾遍歷一次,所以時間複雜度為O(n^2)
2. 模擬法+查詢
我們從暴力模擬法為起點進一步優化,我們看到每次我們都要從末尾遍歷相同的元素,實際上我們可以建立一個保持排序的陣列sorted_num。
這個陣列代表:在nums[i]之後所有的數,並且已經排好序。
每次在nums陣列出現新的需要判斷的數就要插入到這個sorted_num,然後在這個數通過二分查詢到下界(可以用STL自帶的lower_bound()) 減去sorted_num.begin()就是比nums[i]小的元素個數了。
1 class Solution { 2 public: 3 vector<int> countSmaller(vector<int>& nums) { 4 vector<int> sorted_num; 5 /*建立一個保持排序的陣列*/ 6 vector<int> res; 7 /*用於儲存結果的陣列*/ 8 for(int i=nums.size()-1;i>=0;i--){ 9 auto iter = lower_bound(sorted_num.begin(),sorted_num.end(),nums[i]); 10 /*通過lower_bound()二分尋找下界,返回一個迭代器(也就是相對於sorted_num的index)*/ 11 int pos = iter-sorted_num.begin(); 12 /* 13 通過排序陣列的二分查詢性質,我們可以知道iter-sorted_num.begin()(可以理解成sorted_num陣列的起始位置)就是 14 題目需要的比nums[i]小的數字個數 15 */ 16 sorted_num.insert(iter,nums[i]); 17 /*這時nums[i]已經使用完了,需要給以後的數字拿來判斷 18 插入後要保持sorted_num排序,所以nums[i]插入到iter位置*/ 19 res.push_back(pos); 20 } 21 reverse(res.begin(),res.end()); 22 /*一路上都是倒序插入的,所以在最後要逆轉陣列*/ 23 return res; 24 } 25 };
未使用的STL的lower_bound()的模擬法加查詢程式碼,因為減少了函式呼叫,效率會高很多
class Solution { public: vector<int> countSmaller(vector<int>& nums) { vector<int>t,res(nums.size());
/*初始化t,res*/ for(int i=nums.size()-1;i>=0;i--){ int left=0,right=t.size();
/*下面是一個在t陣列裡二分查詢的過程*/ while (left<right){ int mid=left+(right-left)/2; if(t[mid]>=nums[i]){ right= mid; } else{ left=mid+1; } } res[i]=right; t.insert(t.begin()+right,nums[i]); } return res; } };
3. 記憶化+排序
還有個很第二種方法比較類似的方法,就是用STL的pari記錄:沒有排序前每個num[i]對應的下標i,pair<int,int>:nums[i]->i。
記錄完之後進行排序。 然後利用已排序的性質進行查詢,程式碼有些複雜且用到了一些位操作的知識,比較難想到,但也是一種非常好的方法。
1 const int MAXN = 100007; 2 3 int cnt[MAXN],n; 4 /*陣列cnt[i]用於記錄出現元素nums[i]的個數*/ 5 class Solution { 6 public: 7 inline void add(int k) 8 { 9 for(; k <= n; k += -k&k) cnt[k] += 1; 10 } 11 12 int sum(int k) 13 { 14 int res = 0; 15 for(; k; k -= -k&k) res += cnt[k]; 16 return res; 17 } 18 19 vector<int> countSmaller(vector<int>& nums) { 20 n = nums.size(); 21 vector<int> ans(n); 22 vector<pair<int, int>> A; 23 /*建立一個從陣列內容到未排序前索引的對映A*/ 24 for(int i = 0; i < n; i ++) { 25 A.push_back({nums[i], i+1}); 26 } 27 memset(cnt, 0, sizeof(cnt)); 28 sort(A.begin(), A.end()); 29 /*進行排序*/ 30 for(int i = 0; i < n; i ++) { 31 int id = A[i].second; 32 int t = sum(n) - sum(id); 33 ans[id-1] = t; 34 add(id); 35 } 36 return ans; 37 } 38 };
4. 二叉搜尋樹
作為一個經常刷leetcode的人來說,剛開始看到這種方法簡直是跪著看完的。建立一個二叉搜尋樹,每個樹的結點都有變數val這個結點的值和變數count記錄小於該結點的個數。
因為count的最後一個值的結果一定是0,所有先把0放入count陣列,並建立一個以val為nums[i-1]的單獨結點樹。
逆序讀nums[i]並建立二叉搜素樹,首先排除nums.size()==0的情況,每讀取一個nums[i],先去搜尋樹尋找這個nums[i]對應的答案,
找到答案之後返回給引用引數count_small,再把nums[i]這個值作為新的結點的val插入。
最後需要將樹的結點delete以防記憶體浪費。
程式碼有詳細註釋。時間複雜度為O(nlogn)
1 struct BSTNode{ 2 int val; 3 int count; 4 BSTNode *left; 5 BSTNode *right; 6 BSTNode(int x) 7 : val(x) 8 , left(NULL) 9 , right(NULL) 10 , count(0) 11 {} 12 }; 13 /*建立一個二分查詢樹,每個樹結點有四個值,分別是: 14 int val;這個結點代表的值val 15 int count;這個val代表的次數也就是在nums陣列種比val小的數的個數 16 left 左子樹指標 17 right 右子樹指標 18 一個建構函式,建構函式如上定義 19 */ 20 21 void BST_insert(BSTNode *node,BSTNode *insert_node,int &count_small) 22 { 23 if(node->val >= insert_node->val) 24 { 25 /*插入的結點更小,被比較結點(即node)的count++,然後插入到左子樹(如果不為空)*/ 26 node->count++; 27 if(node->left) 28 { 29 BST_insert(node->left,insert_node,count_small); 30 } 31 else 32 { 33 /*左子樹為空,插入結點就作為當前結點的左孩子*/ 34 node->left = insert_node; 35 } 36 } 37 else{ 38 /*插入的結點更大,需要在右子樹(如果不為空)繼續找*/ 39 count_small += node->count + 1; 40 if(node->right) 41 { 42 BST_insert(node->right,insert_node,count_small); 43 } 44 else 45 { 46 /*當前右子樹為空,插入結點作為當前結點右孩子*/ 47 node->right = insert_node; 48 } 49 } 50 } 51 /*count_small作為一個引用的引數,在遞迴尋找子樹的時候作為一個“類似全域性變數”的存在*/ 52 53 54 class Solution { 55 public: 56 vector<int> countSmaller(vector<int>& nums) { 57 int n=nums.size(); 58 /*如果陣列為空返回空值*/ 59 if(n==0)return {}; 60 vector<int> count; 61 count.push_back(0); 62 /*建立一個二叉搜素樹*/ 63 BSTNode* node=new BSTNode(nums[n-1]); 64 int count_small; 65 for(int i = 1;i < n;i++) 66 { 67 count_small = 0; 68 BST_insert(node,new BSTNode(nums[n-i-1]),count_small); 69 count.push_back(count_small); 70 } 71 /*最後不要忘記刪除樹節點*/ 72 delete node; 73 reverse(count.begin(),count.end()); 74 /*push_back的時候是逆序的,此時只要將count陣列reverse即可*/ 75 return count; 76 } 77 };
5. 利用歸併排序
這題比較“官方的”解法,因為這一道題我是在分治演算法中找到的,所以我認為這一題出題的目的就是考查使用歸併排序過程中求解滿足條件的“點對”。程式碼直接引用了leetcode上的答案,
因為我覺得其註釋已經非常完全了。這裡我引用了一道我做別的題時寫的題解,和下面這個程式碼思路是一樣的。
在歸併排序的過程中利用陣列間已經有的大小關係,算出所需的解
每一次merge都將一個陣列分成 left(即nums[l]~nums[mid]) 和right(即nums[mid+1]~nums[r])兩個區間
其中:
1. left、right兩個區間中滿足條件的對數已經在上一次二分中算出
2. 每次二分只需要算出left 和 right 之間滿足條件的對數即可
對right中的每個元素,用findl,findr指標在left區間中劃定連續的範圍即可
題目要求的點對要符合以下條件:
i<j 且nums[i]>nums[j]
由歸併排序升序的性質,可以知道
nums[index]<=nums[mid](index<=mid)
nums[index]>=nums[mid](index>=mid)
我們只需要遍歷right中的數並在left區間找到對應範圍,這個範圍只需要滿足第二個條件nums[i]>nums[j]即可,因為第一個條件是一定滿足了的。(i<mid<j)
步驟:
1. 利用歸併排序遞迴求解兩個子區間內部對數並進行歸併排序,此時left和right為遞增區間
2. 排序的同時遍歷 在right區間中的數(設為nums[i]),left區間找到滿足這個條件的範圍findl~findr,findr~findl即為左區間和nums[i]匹配的點個數。
時間複雜度:O(nlogn)
class Solution { public: vector<int> countSmaller(vector<int>& nums) { vector<int>count;//儲存結果 vector<pair<int,int> > num;//關聯每個數和它的序號 for(int i =0;i<nums.size();++i) { count.push_back(0); num.push_back(make_pair(nums[i],i));//儲存每個數和它在原陣列中的序號,以免在排序過程中打亂順序 } merge_sort(num,count); return count; } //歸併排序 void merge_sort(vector<pair<int,int> >& vec, vector<int>& count) { if(vec.size()<2) return; int mid = vec.size()/2; vector<pair<int,int> > sub_vec1; vector<pair<int,int> > sub_vec2; for(int i =0;i<mid;++i) sub_vec1.push_back(vec[i]); for(int i =mid;i< vec.size();++i) sub_vec2.push_back(vec[i]); merge_sort(sub_vec1,count); merge_sort(sub_vec2,count); vec.clear(); merge(sub_vec1,sub_vec2,vec,count); } //合併兩陣列 void merge(vector<pair<int,int> >& sub_vec1,vector<pair<int,int> >& sub_vec2, vector<pair<int,int> >& vec, vector<int>& count) { int i =0; int j =0; while(i < sub_vec1.size() && j < sub_vec2.size()) { if(sub_vec1[i].first <= sub_vec2[j].first ) { vec.push_back(sub_vec1[i]); count[sub_vec1[i].second] += j;//這句話和下面註釋的地方就是這道題和歸併排序的主要不同之處 i++; }else{ vec.push_back(sub_vec2[j]); j++; } } for(;i<sub_vec1.size();++i) { vec.push_back(sub_vec1[i]); count[sub_vec1[i].second] += j;// -。- } for(;j<sub_vec2.size();++j) { vec.push_back(sub_vec2[j]); } } };
6. 使用樹狀陣列
這種解法比較少見,速度卻是驚人的快。
樹狀陣列中getSum(index)
作用是求原始陣列nums中小於或等於當前index的數的和,此處用來統計逆序數,可以把getSum(x)看作是nums中小於x的數的個數的和。
以題目中的測試資料[5, 2, 6, 1]
為例:
- 初始狀態陣列c中元素全部置為0
- 插入5時,大於等於5的所有記錄的值+1,此時比5小的數的個數為0;
- 插入2時,大於等於2的所有記錄的值+1,此時比2小的數的個數為0,比5小的數的個數為1;
- 插入6時,大於等於6的所有記錄的值+1,此時比6小的數的個數為2(
getSum(6-1)=2
),比2小的數的個數為0,比5小的數的個數為1(getSum(5-1)=1
); - 插入1時,大於等於1的所有記錄的值+1,此時比1小的數的個數為0,比6小的數的個數為3(
getSum(6-1)=3
),比2小的數的個數為1(getSum(2-1)=1
),比5小的數的個數為2(getSum(5-1)=2
);
可以看出當把每個數都遍歷一遍後,陣列arr中分別記錄了比每個數小的數的個數之和,題目要求每個數右側比它小的數的個數,因此用總數減掉每個數左側比它小的數的個數就可得到。
從插入元素的過程可以看出,每次插入一個元素時,陣列arr中記錄的值剛好是插入當前元素之前比該元素小的元素的個數(即當前元素左側比它小的元素個數),因此只需要在插入一個數之前做一次求和,並用一個陣列記錄該值即可。
1 class Solution { 2 public: 3 int *arr, n; 4 int lowbit(int x){ 5 return x&(-x); 6 } 7 void update(int pos, int delta){ 8 while (pos <= n){ 9 arr[pos] += delta; 10 pos += lowbit(pos); 11 } 12 } 13 int getSum(int pos){ 14 int ret = 0; 15 while (pos){ 16 ret += arr[pos]; 17 pos -= lowbit(pos); 18 } 19 return ret; 20 } 21 vector<int> countSmaller(vector<int>& nums) { 22 n = nums.size(); 23 vector<int> ret(n); 24 if (n == 0){ 25 return ret; 26 } 27 int minn = 999999999, maxx = -999999999; 28 for (int i=0;i<n;++i){ 29 maxx = max(maxx, nums[i]); 30 minn = min(minn, nums[i]); 31 } 32 n = maxx - minn + 2; 33 arr = new int[n]; 34 memset(arr, 0, sizeof(int)*n); 35 for (int i=nums.size()-1;i>=0;--i){ 36 ret[i] = getSum(nums[i] - minn); 37 update(nums[i]-minn+1, 1); 38 } 39 return ret; 40 } 41 };