通過交換操作,調整數組元素位置
問題描述:有一個長度為N的整形數組row,由0至N-1這N個數字亂序組成(每個數組出現且僅出現一次)。現在你可以對這個數組的任意兩個不同的元素進行交換。問:對於一個給定的這種數組,若要把這個數組變為從小到大排好序的操作(即,對於數組的任意下標,均有 I == row[i] 成立),最少需要進行多少次交換?
首先,舉幾個簡單的例子:
例子1:
下標 |
0 |
1 |
2 |
3 |
4 |
值 |
0 |
3 |
2 |
1 |
4 |
只需1次交換即可:把row中下標為1的元素和下標為3的元素進行交換,記為swap(row, 1, 3)。
例子2:
下標 |
0 |
1 |
2 |
3 |
4 |
值 |
0 |
2 |
1 |
4 |
3 |
需要兩次交換:
第一次:swap(row, 1, 2)
第一次交換後:
下標 |
0 |
1 |
2 |
3 |
4 |
值 |
0 |
1 |
2 |
4 |
3 |
第二次:swap(row, 3, 4)
例子3:
下標 |
0 |
1 |
2 |
3 |
4 |
值 |
0 |
4 |
2 |
1 |
3 |
需要兩次交換:
第一次:swap(row, 1, 4)
第一次交換後:
下標 |
0 |
1 |
2 |
3 |
4 |
值 |
0 |
3 |
2 |
1 |
4 |
第二次:swap(row, 1, 3)
註意,在例子3中,下標為1、3、4的三個元素的初始位置形成了一個“環”。即(接下來的話很重要),位置1上的元素本應該在位置4;位置4上的元素本應該在位置3;位置3上的元素本應該在位置1。這段很重要的話太啰嗦了,簡記如下:1-->4-->3-->1。
任何一個亂序的數組,都會包含一個或多個形如“1-->4-->3-->1”的“環”。
註意,這個“環”的開頭的結尾肯定是同一個下標,絕不會出現如下的形式:
“1-->4-->3-->……-->3”。這是因為,如果數字3出現了兩次,那就意味著原始數組row中的兩個不同的位置的元素都“本應該出現在位置3”。
所以,“通過交換的方式對數組進行排序”,其實就是“對上述的‘環’中的下標進行操作”。
下面來計算對每個“環”需要進行多少次交換。
首先定義“環”的長度如下:
“1-->4-->3-->1”的長度為3,
“1-->4-->1”的長度為2
“1-->1”的長度為1(長度為1的情況就是“該元素的處於正確位置”的情況)
對於長度為1的環,所需的交換次數是0,SWAP(1) = 0
對於長度為2的環,所需的交換次數是1,SWAP(2) = 1
對於長度為k的環,交換其中的任何兩個元素,把當前的“撕裂”為兩個更小的環,且兩個小環的長度加起來剛好等於k。例如:
對於環:
……i-->j-->k-->……r-->s-->t-->……
執行swap(row, j, s),會生成如下的兩個環(需要思考兩分鐘):
環1:……i-->j-->t-->……
環2:k-->……r-->s->k
(對於j和s在邊界的情況,上述結論也成立。)
所以,對於長度為k的環,所需的交換次數SWAP(k)=SWAP(k1) + SWAP(k2) +1,其中k1+k2=k
根據
SWAP(1) = 0,
SWAP(k)=SWAP(k1) + SWAP(k2) +1,其中k1+k2=k
可以用第二數學歸納法證明(第二數學歸納法是啥,見文末),
SWAP(n) = n - 1
註意,對長度為k的環,交換其中的任意兩個元素都可以把環撕裂為兩個小環。那麽,如果我們把第一個元素交換到“它本應出現的位置”,會怎樣呢?
對於環“1-->4-->3-->1”,下標1上的元素本應出現在位置4,所以我們執行swap(row, 1, 4),然後就把“1-->4-->3-->1”撕裂為兩個小環:
環1:“1-->3-->1”
環2:“4-->4”
環2是“已經搞定了”的狀態,接下來只需處理環1,swap(row, 1, 3)。
上述算法的直觀感覺就是,不停地把“當前位置的元素”和“它應該去的地方”的元素進行交換,這樣,“當前位置的元素”就去了“它應該去的地方”,同時,“被換過來的元素”又成了“當前位置的元素”,直到“被換過來的元素”就應該放在“當前位置”為止。
代碼如下:
int sort(int[] row) {
printArray(row);
int nSwapTimes = 0;
for (int i = 0; i < row.length; ++i) {
for (int j = row[i]; j != i; j = row[i]) {
swap(row, i, j);
++ nSwapTimes;
// 可以在每次交換後把row的當前狀態打印出來感受一下
printArray(row);
}
}
return nSwapTimes;
}
上述解法可以推廣到如下的問題:
有N對夫婦隨機坐成一排,現在要經過若幹次交換座椅,使得每對夫婦的座位都挨在一起。求最小的交換次數。座位以整形數組row表示,下標從0至2N-1。每一對夫婦都用有序數對表示:(0, 1)、(2, 3)、(2N-2, 2N-1)。數組row的第i個元素row[i]代表位置i上坐著的人。
為了解決這個問題,需要一個和row的用處剛好相反的輔助數組pos:row[i]代表位置i上坐著的人;pos[i]代表人i所在的位置。
為了方便起見,定義getPartner函數如下
int getPartner(int n) {
return (n % 2 == 1) ? (n - 1) : (n + 1);
}
這個函數返回下標n的配偶(n既可以是人也可以是座位。請思考兩分鐘,當n是座位時getPartner的含義)。
這個問題和亂序數組的排序問題只有一個區別:
亂序數組排序:下標i所在的元素的期望位置是row[i]
本問題:下標i所在元素的期望位置是getPartner(pos[getPartner(row[i])])
getPartner(pos[getPartner(row[i])])的含義:
最內層row[i],位置i上的人是誰
再加一層getPartner,此人的配偶是誰
再加一層pos,此人的配偶在row中的實際座位
再加一層getPartner,此人的期望座位(思考兩分鐘)
代碼如下:
int nResult = 0;
for (int i = 0; i < row.length; i += 2) {
for (int j = getPartner(pos[getPartner(row[i])]); j != i; j = getPartner(pos[getPartner(row[i])])) {
swap(row, i, j);
swap(pos, row[i], row[j]);
++nResult;
}
}
return nResult;
至此,結束
第一數學歸納法,若對自然數的命題P(n),滿足:
1、 P(1)成立。
2、 若P(k)成立,則P(k+1)成立
則P(n)對全體自然數成立。
第二數學歸納法,若對自然數的命題P(n),滿足:
1、 P(1)成立。
2、 若P(1),P(2),……,P(k)成立,則P(k+1)成立
則P(n)對全體自然數成立。
通過交換操作,調整數組元素位置