【探索-中級演算法】顏色分類
初級解法一
首先,使用最簡單的解法,可以拆分成兩步。
第一步:掃描陣列,先把 0
放在最前面,把 1
和 2
放在最後面(即使是混淆的也沒關係)。
第二步:再在混淆的 1
和 2
中進行排序。
每一步的排序,都需要藉助兩個指標。
public void sortColors(int[] nums) {
if (nums == null || nums.length == 0) return;
int p0 = -1, px = -1;
for (int i = nums.length - 1; i >= 0; i--) {
if (nums[i] == 0 ) {
p0 = i;
break;
}
}
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
px = i;
break;
}
}
// 第一步
if (p0 != -1 && px != -1) {
while (p0 > px) {
swap(nums, p0, px);
while (nums[p0] != 0) --p0;
while (nums[px] == 0) ++px;
}
}
int p1 = -1, p2 = -1;
for (int i = p0 + 1; i < nums.length; i++) {
if (nums[i] == 2) {
p2 = i;
break;
}
}
for (int i = nums.length - 1; i >= 0; i--) {
if (nums[ i] == 1) {
p1 = i;
break;
}
}
// 第二步
if (p1 != -1 && p2 != -1) {
while (p1 > p2) {
swap(nums, p1, p2);
while (nums[p1] != 1) --p1;
while (nums[p2] != 2) ++p2;
}
}
}
public void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
這樣做簡單直觀,但是效率不是很高。
初級解法二
按照題目給的提示,首先,迭代計算出 0、1 和 2 元素的個數,然後按照0、1、2的排序,重寫當前陣列。
首先,需要明確一個概念,假設 0、1 和 2 元素的個數分別為 num1、num2、num3,則在排序之後,有如下規則:
0 所在的範圍為 [0, num1 - 1],設該範圍的小段組為 g1
1 所在的範圍為 [num1, num1 + num 2 - 1],設該範圍的小段組為 g2
2 所在的範圍為 [num1 + num 2, num1 - 1],設該範圍的小段組為 g3
而對於未排序的、混亂的陣列,對於 g1 內的非 0 元素都需要被交換成 0 元素,g2 的非 1 元素都需要交換成 1 元素,g3 的非 2 元素都需要交換成 2 元素,且有通常情況下,有如下交換規則(<-->
符號表示相互交換位置,g1_2
代表 g1 小組內的為 2 的元素):
g1_1 <--> g2_0,即 g1 小組範圍內的元素 1 直接與 g2 小組範圍內的元素 0 交換
g1_2 <--> g3_0
g2_2 <--> g3_1
即直接把 g2、g3 中的元素 0 都交換到 g1 內,其餘組內的同理。
(需要注意,這裡說的是直接,還有一種特殊情況,不能直接交換,在下面的程式碼種有註釋說明)
如圖所示,更加直觀:
具體的演算法如下 (需要注意的是幾種特殊情況):
public static void sortColors2(int[] nums) {
if (nums == null || nums.length == 0) return;
int[] counts = new int[3];
// 分別記錄各個元素的個數
for (int i = 0; i < nums.length; i++) {
counts[nums[i]]++;
}
// 三個指標,分別指向各個小組非小組對應的元素,如 g1 就指向 g1 小組內的元素 1 和 2
int g1 = (counts[0] > 0 ? 0 : -1);
int g2 = (counts[1] > 0 ? counts[0] : -1);
int g3 = (counts[2] > 0 ? nums.length - counts[2] : -1);
int f1 = g1;
int f2 = g2;
int f3 = g3;
while (true) {
if (g1 > -1 && g2 > -1 && nums[g1] == 1 && nums[g2] == 0) {
swap(nums, g1, g2);
++g1;
++g2;
} else if (g1 > -1 && g3 > -1 && nums[g1] == 2 && nums[g3] == 0) {
swap(nums, g1, g3);
++g1;
++g3;
} else if (g2 > -1 && g3 > -1 && nums[g2] == 2 && nums[g3] == 1) {
swap(nums, g2, g3);
++g2;
++g3;
} else if (g1 > -1 && g2 > -1) {
// 特殊的處理,如對於 nums = [2, 0 ,1],
// 此時不能直接把兩個元素交換到對應的小組內,即不屬於前面三種情況,
// 則先交換一下當前 g1 和 g2 指向的元素
// 這樣間接處理之後,就會使得重新符合前面三種情況之一了
swap(nums, g1, g2);
}
// 如果元素本身就在其對應的小組內,則跳過
while (g1 > -1 && g1 < nums.length && nums[g1] == 0) ++g1;
while (g2 > -1 && g2 < nums.length && nums[g2] == 1) ++g2;
while (g3 > -1 && g3 < nums.length && nums[g3] == 2) ++g3;
// 排序完成,則退出
if ((g1 == -1 || g1 - f1 == counts[0])
&& (g2 == -1 || g2 - f2 == counts[1])
&& (g3 == -1 || g3 - f3 == counts[2])) break;
}
}
初級解法三
其實,還可以從另外一個角度來看,因為只有 0、1、2
三種值,所以第一遍先計算每個值的個數,然後再按照每個值的數量遍歷陣列,依次設定對應下標的值即可。
進階解法
還是利用三個指標,p0、p1、p2
在 nums(0, m-1)
內,總是分別指向排序之後的序列的最後一個 0、1、2
元素,因此,對於 mums[m]
,需要插入到 nums(0, m-1)
中,從而形成新的有序的 nums(0, m)
序列。
而在 nums(0, m-1)
中插入 nums[m]
的時候,假設 nums[m] == 0
,則需要將原來的 p0
往後移動一位(即 ++p0
),來表示新的 0 元素需要插入的位置 nums[p0]
,而因為新插入了元素,因為在 index = p0
之後的元素需要往後移動一位。
此時就可以使用一個技巧,直接 nums[m] == 0
覆蓋到 nums[++p0]
的位置,此時被覆蓋的元素(元素值為 1)則又轉移到 nums[++p1]
的位置(相當於轉移到 1 組成序列的末尾),而又會產生被覆蓋的元素(元素值為 2),則又轉移到 nums[++p2]
的位置(相當於轉移到 2 組成序列的末尾)。
可以看演示截圖(虛線箭頭代表指標的移動,圓圈包圍的元素代表被覆蓋的元素,弧線箭頭代表轉移):
public void sortColors(int[] nums) {
int p0 = -1,p1 = -1,p2 = -1;
int m = 0;
for(; m < nums.length;m++) {
if(nums[m] == 0){
nums[++p2] = 2;
nums[++p1] = 1;
nums[++p0] = 0;
}
else if(nums[m] == 1){
nums[++p2] = 2;
nums[++p1] = 1;
}
else {
++p2;
}
}
}