參考:http://xudacheng06.blog.163.com/blog/static/4894143320127891610158/
楊氏矩陣(Young Tableau)是一個很奇妙的資料結構,他類似於堆的結構,又類似於BST的結構,對於查詢某些元素,它優於堆;對於插入、刪除它比BST更方便。
首先介紹一下這個資料結構的定義,Young Tableau有一個m*n的矩陣,讓後有一陣列 a[k], 其中k<=m*n ,然後把a[k]中的數填入 m*n 的矩陣中,填充規則為(如圖1-1):
1. 每一行每一列都嚴格單調遞增(有其他的版本是遞減,其原理相同)。
2. 如果將a[k]中的數填完後,矩陣中仍有空間,則填入 ∞。
1 |
2 |
3 |
4 |
5 |
6 |
|
1 |
3 |
5 |
7 |
8 |
11 |
a |
4 |
6 |
9 |
14 |
15 |
19 |
b |
10 |
21 |
23 |
33 |
56 |
57 |
c |
34 |
37 |
45 |
55 |
∞ |
∞ |
d |
∞ |
∞ |
∞ |
∞ |
∞ |
∞ |
e |
圖1-1
則現在討論,這個資料結構的幾種操作,而在這些操作中,我們會發現堆排序的影子,並且這些操作具有很好的時間複雜度。
條件:一個已經建好的楊氏矩陣(Young Tableau)
操作:
【1】 在楊氏矩陣中插入元素x ---- Insert(x)
【2】 在楊氏矩陣中刪除位於 (x, y) 位置的元素---- Delete(x, y)
【3】 返回x是否在矩陣中----Find(x)
【4】 第k大數
其中1-2操作類似於堆的操作,而3操作類似於BST的操作。下面我就用我
自己的理解把這幾個操作加以解釋。
【1】 插入操作
本操作的時間複雜度為O( n + m ),其操作類似於堆排序的SIFT-UP演算法(具體演算法見《演算法設計技巧與分析》 M.H,Alsuwaiyel 著)。它的堆的結構在這裡表現為某個元素Y[x, y] 下面和右面的兩個元素Y[x, y+1] ,Y[x+1, y]均比Y[x, y]要大。於是其插入演算法可以描述為,首先把待插入元素以行為主序置於所有有效數字(除了無窮以外的數)最後面,如將x=2,放入 圖1-1 的d5的位置,然後執行類似於SIFT-UP的操作,將x與它左邊(記為x-l)及上面(記為x-u)的數進行比較,根據比較結果有三種可能的操作:
1:x-u > x 且x-u >= x-l 則將x 與 x-u進行交換
2:x-l > x 且x-l > x-u 則將x 與 x-l進行交換
3: x >= x-l 且 x > x-u 則x 不動,此時已經插入成功
(可以有其他合理的約定)
對例子插入2進行操作如圖1-2箭頭的操作方向。
1 |
2 |
3 |
4 |
5 |
6 |
|
1 |
3 |
5 |
7 |
8 |
11 |
a |
2 |
6 |
9 |
14 |
15 |
19 |
b |
4 |
10 |
21 |
23 |
33 |
57 |
c |
34 |
37 |
45 |
55 |
56 |
∞ |
d |
∞ |
∞ |
∞ |
∞ |
∞ |
∞ |
e |
圖1-2
由上述的操作知其時間複雜度最大為行列之和,即O(m+n)。
【2】 刪除操作
操作類似於插入操作,其時間複雜度也為O(m+n)。其操作類似於堆排序的SIFT-DOWN演算法。刪除演算法可以描述為,首先將刪除的位置(x, y)的數字刪除,然後調整,把楊氏矩陣中的最大值k(可以行為主序進行搜尋到最後)填入到被刪除位置,然後讓此數與其下面的數(k-d)和右面的數進行(k-r)比較,此時比較結果為兩種,因為此時元素一定會下移:
1:k-d > k-r 將k-r 和 k進行交換
2:k-d < k-r 將k-d 和 k進行交換
舉例 刪除圖1-1的a3位置的元素5如圖1-3
1 |
2 |
3 |
4 |
5 |
6 |
|
1 |
3 |
7 |
8 |
11 |
19 |
a |
4 |
6 |
9 |
14 |
15 |
57 |
b |
10 |
21 |
23 |
33 |
56 |
∞ |
c |
34 |
37 |
45 |
55 |
∞ |
∞ |
d |
∞ |
∞ |
∞ |
∞ |
∞ |
∞ |
e |
圖1-3
由操作知,刪除演算法時間複雜同樣最多為行列之和,即O(m + n)。
【3】查詢演算法
查詢演算法類似於BST二叉排序樹,不過需要在其思想上稍微進行修改,才能滿足楊氏矩陣的查詢操作,同樣利用楊氏矩陣的性質,不過此時應該換一個角度思考問題,因為與BST二叉排序樹類比,所以應該從左下角開始看起(如 圖1-1d1位置),該元素的上面的元素比本身小和右面的元素比本身大,這樣就與BST二叉排序樹具有相同的性質。所以在查詢時從左下角不斷的比較,從而最終確定所查詢元素位置。
如查詢19,比較順序如圖1-4
1 |
2 |
3 |
4 |
5 |
6 |
|
1 |
3 |
5 |
7 |
8 |
11 |
a |
4 |
6 |
9 |
14 |
15 |
19 |
b |
10 |
21 |
23 |
33 |
56 |
57 |
c |
34 |
37 |
45 |
55 |
∞ |
∞ |
d |
∞ |
∞ |
∞ |
∞ |
∞ |
∞ |
e |
圖1-4
此演算法的時間複雜度同前兩個演算法,複雜度也是O(m + n)。
【4】楊氏矩陣第K大數
1)小頂堆法
構建一個小頂堆,首先將a[0][0]加入小頂堆,然後每次從小頂堆中取出最小元素,並加入比該元素大且與之相鄰的兩個元素(對於a[0][0],則需要加入 a[0][1]和a[1][0]),直到取出第k個元素,需要注意的是,需要採用額外空間記錄某個元素是否已經加入到小頂堆以防止重複加入。
2)二分列舉+類堆查詢
首先,二分列舉找到一個數x,它比楊氏矩陣中k個數大;然後,利用類堆查詢法找到剛好小於x的元素。該演算法的時間複雜度為O((m+n)lg(mn)),但不需要額外儲存空間。程式碼如下:
int find_kth_in_YoungTableau(int** a, int m, int n, int k) { int low = a[0][0]; int high = a[m-1][n-1]; int order = 0; int mid = 0; do { mid = (low + high) >> 1; order = get_order(a, m, n, mid); if(order == k) break; else if(order > k) high = mid - 1; else low = mid + 1; } while(1); //二分列舉整,找出正好大於k的一個整數 mid int row = 0; int col = n - 1; int ret = mid; while(row <= m-1 && col >= 0) { //找出比mid小的最大數 if(a[row][col] < mid) { ret = (a[row][col] > ret) ? a[row][col] : ret; row++; } else { col--; } } return ret; } //整數k在矩陣中的排名 int get_order(int** a, int m, int n, int k) { int row, col, order; row = 0; col = n-1; order = 0; while(row <= m-1 && col >= 0) { if(a[row][col] < k) { order += col + 1; row++; }else { col--; } } return order; }
方法2舉例:要在下列楊氏矩陣中找第k大數,k=6.
1 | 3 | 5 |
2 | 4 | 8 |
6 | 9 | 10 |
首先會對mid進行列舉,重點是對列舉的mid計算其在該矩陣中小於它的有幾個數。
下面以mid=7為例,說明order=6.
首先與a[0][2]比較,a[0][2]=5<7,則order = 0+2+1=3, row = 1;
再與a[1][2]比較,a[1][2]=8>7,則col = col-1 = 1;
再與a[1][1]比較,a[1][1]=4<7,則order = 3+1+1 = 5,row = 2;
再與a[2][1]比較,a[2][1]=9>7,則col = col-1 = 0;
在於a[2][0]比較,a[2][0]=6<7,則order = 5+0+1 = 6,row = 3;
row >(m-1)=2,所以退出迴圈,返回order值等於6,即在該楊氏矩陣中比mid=7小的數一共有6個。
PS:發現如果陣列中有相同的值的話,可能無法通過列舉找到恰好比楊氏矩陣中k個數大的數。所以這種方法有其侷限性。