給定乙個無序的陣列 nums,將它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的順序。
示例 1:
輸入: nums = [1, 5, 1, 1, 6, 4]
輸出: 乙個可能的答案是 [1, 4, 1, 5, 1, 6]
示例 2:
輸入: nums = [1, 3, 2, 2, 3, 1]
輸出: 乙個可能的答案是 [2, 3, 1, 3, 1, 2]
說明:
你可以假設所有輸入都會得到有效的結果。
高階:你能用 o(n) 時間複雜度和 / 或原地 o(1) 額外空間來實現嗎?
這道題給了我們乙個無序陣列,讓我們排序成擺動陣列,滿足nums[0] < nums[1] > nums[2] < nums[3]...,並給了我們例子。我們可以先給陣列排序,然後在做調整。調整的方法是找到陣列的中間的數,相當於把有序陣列從中間分成兩部分,然後從前半段的末尾取乙個,在從後半的末尾去乙個,這樣保證了第乙個數小於第二個數,然後從前半段取倒數第二個,從後半段取倒數第二個,這保證了第二個數大於第三個數,且第三個數小於第四個數,以此類推直至都取完,參見**如下:
// o(n) space
class solution
}};
上一解法之所以時間複雜度為o(nlogn),是因為使用了排序。但回顧解法1,我們發現,我們實際上並不關心a和b內部的元素順序,只需要滿足a和b長度相同(或相差1),且a中的元素小於等於b中的元素,且r出現在a的頭部和b的尾部即可。實際上,由於a和b長度相同(或相差1),所以r實際上是原陣列的中位數,下文改用mid來表示。因此,我們第一步其實不需要進行排序,而只需要找到中位數即可。而尋找中位數可以用快速選擇演算法實現,時間複雜度為o(n)。
該演算法與快速排序演算法類似,在一次遞迴呼叫中,首先進行partition過程,即利用乙個元素將原陣列劃分為兩個子陣列,然後將這一元素放在兩個陣列之間。兩者區別在於快速排序接下來需要對左右兩個子陣列進行遞迴,而快速選擇只需要對一側子陣列進行遞迴,所以快速選擇的時間複雜度為o(n)。詳細原理可以參考有關資料,此處不做贅述。
在c++中,可以用stl的nth_element()函式進行快速選擇,這一函式的效果是將陣列中第n小的元素放在陣列的第n個位置,同時保證其左側元素不大於自身,右側元素不小於自身。
找到中位數後,我們需要利用3-way-partition演算法將中位數放在陣列中部,同時將小於中位數的數放在左側,大於中位數的數放在右側。該演算法與快速排序的partition過程也很類似,只需要在快速排序的partition過程的基礎上,新增乙個指標k用於定位大數:
int i = 0, j = 0, k = nums.size() - 1;
while(j < k)
else if(nums[j] < mid)
else
}
在這一過程中,指標j和k從左右兩側同時出發相向而行,每次要麼j移動一步,要麼k移動一步,直到相遇為止。這一過程的時間複雜度顯然為o(n)。
至此,原陣列被分為3個部分,左側為小於中位數的數,中間為中位數,右側為大於中位數的數。之後的做法就與解法1相同了:我們只需要將陣列從中間等分為2個部分,然後反序,穿插,即可得到最終結果。以下為完整實現:
class solution
else if(nums[j] < mid)
else
}if(nums.size() % 2) ++midptr;
vectortmp1(nums.begin(), midptr);
vectortmp2(midptr, nums.end());
for(int i = 0; i < tmp1.size(); ++i)
for(int i = 0; i < tmp2.size(); ++i)
}};
快速選擇過程也可以手動實現,以下為手動實現的完整**:
class solution
else if(nums[j] < mid)
else
}if(nums.size() % 2) ++midptr;
vectortmp1(nums.begin(), midptr);
vectortmp2(midptr, nums.end());
for(int i = 0; i < tmp1.size(); ++i)
for(int i = 0; i < tmp2.size(); ++i)
}private:
void quickselect(vector&nums, int begin, int end, int n)
else
}if(i - 1 > n)
else if(i <= n)
}};
由於省略了排序過程,且快速選擇和3-way-partition的時間複雜度都為o(n),所以這一解法時間複雜度為o(n)。和解法1相同,解法2也需要儲存a陣列和b陣列,所以空間複雜度不變,仍未o(n)。
接下來,我們思考如何簡化空間複雜度。上文提到,解法1和2之所以空間複雜度為o(n),是因為最後一步穿插之前,需要儲存a和b。在這裡我們使用所謂的虛位址的方法來省略穿插的步驟,或者說將穿插融入之前的步驟,即在3-way-partiton(或排序)的過程中順便完成穿插,由此來省略儲存a和b的步驟。「位址」是一種抽象的概念,在本題中位址就是陣列的索引。
btw,由於虛位址較為抽象,需要讀者有一定的數學基礎和抽象思維能力,如果實在理解不了沒有關係,解法2已經是足夠優秀的解法。
如果讀者學習過作業系統,可以利用作業系統中的實體地址空間和邏輯位址空間的概念來理解。簡單來說,這一方法就是將陣列從原本的空間對映到乙個虛擬的空間,虛擬空間中的索引和真實空間的索引存在某種對映關係。在本題中,我們需要建立一種對映關係來描述「分割」和「穿插」的過程,建立這一對映關係後,我們可以利用虛擬位址訪問元素,在虛擬空間中對陣列進行3-way-partition或排序,使陣列在虛擬空間中滿足某一空間關係。完成後,陣列在真實空間中的空間結構就是我們最終需要的空間結構。
在某些場景下,可能對映關係很簡潔,有些場景下,對映關係可能很複雜。而如果對映關係太複雜,程式設計時將會及其繁瑣容易出錯。在本題中,想建立乙個簡潔的對映,有必要對前面的3-way-partition進行一定的修改,我們不再將小數排在左邊,大數排在右邊,而是將大數排在左邊,小數排在右邊,在這種情況下我們可以用乙個非常簡潔的公式來描述對映關係:#define a(i) nums[(1+2(i)) % (n|1)],i是虛擬位址,(1+2(i)) % (n|1)是實際位址。其中n為陣列長度,『|』為按位或,如果n為偶數,(n|1)為n+1,如果n為奇數,(n|1)仍為n。
accessing a(0) actually accesses nums[1].
accessing a(1) actually accesses nums[3].
accessing a(2) actually accesses nums[5].
accessing a(3) actually accesses nums[7].
accessing a(4) actually accesses nums[9].
accessing a(5) actually accesses nums[0].
accessing a(6) actually accesses nums[2].
accessing a(7) actually accesses nums[4].
accessing a(8) actually accesses nums[6].
accessing a(9) actually accesses nums[8].
以下為完整**:
class solution
}};
時間複雜度與解法2相同,為o(n),空間複雜度為o(1)。
當然,也可以在解法1中利用虛位址方法,即利用虛位址對nums進行排序,那麼時間複雜度為o(nlogn),空間複雜度為o(1)。
先排序,再插空
class solution:
def wigglesort(self, nums: list[int]) -> none:
nums.sort(reverse=true)
mid = len(nums) // 2
nums[1::2],nums[0::2] = nums[:mid], nums[mid:]
LeetCode 324 擺動排序 II
給定乙個無序的陣列nums,將它重新排列成nums 0 nums 1 nums 2 nums 3 的順序。示例 1 輸入 nums 1,5,1,1,6,4 輸出 乙個可能的答案是 1,4,1,5,1,6 示例 2 輸入 nums 1,3,2,2,3,1 輸出 乙個可能的答案是 2,3,1,3,1,2...
leetcode 324 擺動排序 II
思路 將給定陣列排序並逆序 拆分處理後的陣列,得到兩個陣列 將乙個陣列插入至另乙個陣列。4,6,5,5 1.排序 逆序 6,5,5,4 2.拆分 6,5 5,4 3.合併 5,4,6,5 4.result 5,6,4,5 def resolution nums length len nums if ...
leetcode 324 擺動排序 II
給定乙個無序的陣列 nums,將它重新排列成 nums 0 nums 1 nums 2 nums 3 的順序。示例 1 輸入 nums 1,5,1,1,6,4 輸出 乙個可能的答案是 1,4,1,5,1,6 示例 2 輸入 nums 1,3,2,2,3,1 輸出 乙個可能的答案是 2,3,1,3,1...