標籤:

CodyNote009:索引定址系列探討(Part.1)

CodyNote009:索引定址系列探討(Part.1)

來自專欄 Cody習題4 人贊了文章

馬良,祁彬彬


序言

MATLAB的索引概念相當於數據的邏輯和脈絡,是矢量化代碼的精神與魂魄。矩陣和數組元素的索引控制方法,吸引無數MATLAB編程者沉迷其中,幾乎所有合格的MATLAB編程教材,都會用一定篇幅,提及索引的重要和靈活性。

不過潛水國內BBS多年,個人感覺能把索引用好的人,和龐大的MATLAB使用人群基數相比,仍屬鳳毛麟角,能給我留下深刻印象、可稱傳世的代碼寥寥無幾。其中當然有客觀原因,比如:

  1. 作為科研工具之一,MATLAB使用者所能投入的時間與定時定期輸出成果間的窘迫比例;
  2. 各高校本科講述MATLAB編程的教師水準...港真,實在良莠不齊,經常:「編程課不講編程」,手拿半通不通、翻譯help的教材,一本正經,一絲不苟對著PPT念函數,弄些2D曲線隨便plot糊弄完事兒的怪現象屢見不鮮,一些對矢量化編程存有好奇心的學生,有力無處使,茫茫然不知怎麼找到恰當的提高切入點;
  3. 最後,講述索引定址也是出力不討好的噁心事兒,牽扯知識點繁雜不易分類不說,同時也缺乏優秀案例,能階梯式、有系統地藉由代碼欣賞和實際訓練,激發出矢量化代碼和自身矩陣思維能力立體形式、綜合提高的熱情,很容易把實打實的索引定址講解,弄成一鍋激情四射、卻找不到幾條幹貨的心靈雞湯。

呃...說起這雞湯...

我主動自污一條:在我眼裡,索引是牽著矩陣元素和數據變化的那根線,需要我們指揮那根線,讓它在正確的時間,出現在正確的地方。達此目的,需要總結、累積和持續訓練,才能讓看似呆板的函數工具「活」起來,變得立體而情緒豐富,並最終為問題的高效解決服務。

為此,我和彬彬打算做一個以索引控制為主題的系列文章,以多個Cody實例的多種求解代碼方案,重新探討和認識MATLAB多變而靈活的索引控制技巧。不過題目的內涵豐富,題量大,篇幅方面估計比較兇殘。所以分成3-4塊單獨發(現在還不清楚能寫幾篇)。

這是第1篇。

索引應用基礎

最基本的二維矩陣索引定址

【鏈接】:ww2.mathworks.cn/matlab

【原題】:A is a matrix of any size and dimension. Each element of matrix A belongs to the set S of natural numbers up to N. B is a vector of N-length, defining output value for each of the elements of set S. Function takes each element of A, looks up table B and finds corresponding output value to return output matrix C having same size as that of A, made of output elements.

e.g.

A = [ 8 9 5 9; 7 8 13 19; 15 12 13 6; 15 14 3 11; 3 15 2 4]

and

B = [20 18 16 14 12 10 8 6 4 2 0 -2 -4 -6 -8 -10 -12 -14 -16 -18 -20]

expected output is,

C = [ 6 4 12 4; 8 6 -4 -16; -8 -2 -4 10; -8 -6 16 0; 16 -8 18 14]

【解釋】:題干寫得神頭鬼腦不好理解!重新組織語言說明要求:已知輸入A和B,A是二維矩陣,B為1×N數組,要求返回A的同維矩陣C,C中每個元素存在於B數組,A提供對應索引。例如:B中第A(1)=8位置處的數值為6,即:C(1)=B(A(1))=B(8)=6。

Test

Code Input

1

%%A = [17 24 1 8 15; 23 5 7 14 16; 4 6 13 20 22; 10 12 19 21 3; 11 18 25 2 9];B = [1 1 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 ...6 6 6 6 6 7 7 7 7 7 7 7 7];y_correct = [ 4 5 1 3 4 5 2 3 4 4 2 2 4 4 5 3 3 4 5 2 3 4 5 1 3];assert(isequal(mapmat(A,B),y_correct));

【分析】:結果輸出矩陣維度跟A、數值跟B,C中每個元素A和B各佔一半貢獻,A指明方向,B實錘元素值。

高維索引循環

不熟悉MATLAB中的矢量化索引定址,可以按A的維度二重循環,逐個元素賦值:

function ans = mapmat(a,b)[m,n]=size(a);zeros(m,n); for i=1:m for j=1:n ans(i,j)=b(a(i,j)); end endend

低維索引循環

也可以按矩陣A的低維索引賦值:

function ans = mapmat(a,b)zeros(size(a)); for i=1:numel(a) ans(i)=b(a(i)); endend

關於高維和低維索引的問題,我和彬彬的書里打了個比喻:二者關係好比家庭住址定位的多種方式:可以用經緯度標註,可以用某地標建築以東以南多少米,也可以用街道門牌號碼…等等。不同的地址解釋方式,有其特定的使用場合,但結果等效。高維和低維索引同理:都表示數組中某個或者某幾個位置的元素,只是定址表達方式不同。比較前面兩段短代碼,前者2重循環,循環體內按ans(i,j)賦值;後者1重循環,循環體內按ans(i)賦值;前者用行、列號交叉定位檢索數據;後者通過元素順序進行定位(默認逐列),比如一個4×4的矩陣:

>> a=randi(10,4)a = 8 7 10 8 8 2 4 3 3 2 6 6 7 5 3 7

第2行,第3列的元素a(2,3)=4,同樣,按「列優先元素抽取和排列順序」的約定,等效低維索引地址是a(2*4+2)=a(10)=4

>> a(2,3),a(10)ans = 4ans = 4

兩種索引對元素的提取返回結果等效,換句話說索引映射到了同一索引位置的元素,二者間可通過sub2ind/ind2sub換算,仍以4×4矩陣為例:

>> t=sub2ind(size(a),2,3)t = 10>> [i,j]=ind2sub(size(a),10)i = 2j = 3

高維索引比較直觀,行、列、層的空間關係容易想像,好像家庭住址的街道門牌號碼,低維索引好比經緯度數據,實效而略抽象,得對MATLAB的元素排序方式有一定的了解。

直接索引

不過這個問題既不要高維也不要低維索引,倆字母加一對括弧就夠了:

function ans = mapmat(a,b)B(A);end

上述索引控制示例,暗示了MATLAB索引使用的兩個基礎規則:

(1). 索引維度決定輸出變數維度(一對一);

(2). 索引號可重複使用(一對多)。

這兩點是MATLAB索引變換的關鍵所在:原問題數組B是1維數組,但卻跟隨索引維度屬性,A有多大,對B索引後的返回結果也有多大;A的索引序列不需要全都不同,這對自定義各種序列擴展,具有重要意義,如:

>> idx=min(1:10,flip(1:10))idx = 1 2 3 4 5 5 4 3 2 1>> a=abcdea = abcde>> a(idx)ans = abcdeedcba

變數idx通過min函數構造索引號「鏡像」序列,應用到字元型變數a上,映射構成變數a的鏡像。再提一句:索引構造有諸多小技巧,前面利用min函數構造「鏡像」索引、間接控制元素的變換順序即為其中的一個,再舉一例加深印象:

給定兩字母完成填充擴展。如:給定字母』a』和』f』,要求按字母順序輸出』abcdef』;給定字母』t』和』m』,按字母排序順序t本應在後m在前,自動逆序擴展為』tsrqponm』。可以用判斷流程寫出「標配」代碼:

function ans=alphabet(a,b)if a>=b flip(b:a);else a:b;endend

或稍微簡化一下去掉else:

function ans = alphabet(a,b) a:b; if isempty(ans) fliplr(b:a); endend

順便注意字元也能比大小完成邏輯判斷,比如第1個小程序中的a>=b;字元同樣也能進行冒號操作完成序列擴展,比如的2個程序中的fliplr(b:a)。

而上述2個程序中的if完全可避免:

function ans = alphabet(a,b) a:sign(b-a+.8):b;end

sign命令針對正數、0和負數分別得到1、0、-1,屬常見常用函數,在這裡起到巧妙判定順、逆序的作用,如b>=a,即要求正向序列,sign結果可能是0或1,sign內+0.8的作用就是讓運算結果至少不能為0,即使a=b,a:1:b的結果也保證正確;如b<a,b-a+.8<0,sign(...)=-1,取值0.8目的是避免0的出現,但不是唯一的取值,只要小於1,不能等於1因為a=』b』,b=』a』時,結果變成0,序列填充出錯。當然,序列構造方法多樣,並不唯一,還可以構造(-1)^n來實現填充:

function ans = alphabet(a,b) a:(-1)^(a>b):b;end

索引構造中有許多類似技巧,尤其結合邏輯數組,很多平時看似不起眼、不常用的函數,都能發揮出人意料的巧妙作用,在效率不變的前提下,大幅簡化代碼。

矩陣元素控制

Draw a 「X」

【鏈接】:ww2.mathworks.cn/matlab

【題目】:Given n as input Draw a X in a n-by-n matrix. example:

n=3y=[1 0 1 0 1 0 1 0 1]n=4y=[1 0 0 1 0 1 1 0 0 1 1 0 1 0 0 1]

【解釋】:要求生成」X」型的矩陣,即正反對角線元素為1,其餘為零的0-1矩陣,也就是矩陣中間畫個叉。這題思路很多,但在MATLAB中,對角線元素索引不是特別方便,循環可能多幾個步驟(本節最下方),需要通過一些特定的操作,構造出與形狀接近的矩陣再修改編輯。

正對角線容易通過函數eye(n)實現,反對角線則翻轉eye(n):

>> eye(n)+flipud(eye(n));

新問題又來了:n=4沒問題,兩條對角線不相交,可n=3時正反對角線元素相加,中間元素從1變為2,怎麼辦?用find函數找到2所在索引位置再重新賦值就很笨了,因此邏輯索引正式登場,連續兩次取「反」操作:

>> ~~(eye(n)+flipud(eye(n)));

兩個波浪號都不能省,第1次取反操作讓所有0元素變成1,1變成0,第二次重新變回來,那麼原來的2就再次成為1。

前面取「反」操作,接下來用「或」操作:

>> eye(n)|flip(eye(n))

對2個同維矩陣的「或」操作,每個對位元素比較,如果2個比較的元素其中之一為1,則輸出矩陣同維索引位置處返回1:

or(eye(n),flip(eye(n)))

函數選擇因人而異,flip還可以改成fliplr、flipud、rot90...等。而改用循環的話就比較繁瑣:

function y = X(n) y = zeros(n); for i = 1:n y(i,i) = 1; y(n-i+1,i) = 1; endend

Draw a 「Z」

【鏈接】:ww2.mathworks.cn/matlab

【原題】:Given n as input, generate a n-by-n matrix like Z by 0 and 1 .

Example:

n=5ans=[1 1 1 1 1 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 1 1 1 1 1]

【解釋】:用「1」畫Z字,大小由行列維度決定。反對角線如同前一題,可再度翻轉eye(n);最後處理上下兩個邊界。以5×5輸出為例,先eye(n)構造反對角線元素,形成輸出矩陣雛形:

>> flip(eye(5))ans = 0 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 0

再單獨用索引對上下兩條邊賦值:

>> ans([1 end],:)=1ans = 1 1 1 1 1 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 1 1 1 1 1

這對矩陣多索引位置元素數值相同的賦值操作極度好用,那麼拓展一下:如果賦值元素的數值不同呢?

這是個好問題!按常規筆畫順序,Z中每個元素數字次第加1,怎麼寫代碼?

以至少能構成「Z」的n取值為下限,假設n×n方陣(n>=3)。

首先,Z中元素數量是n*3-2,所以n×n方陣中的「Z」字,其中應依次填充1:n*3-2的數字;其次,按列排布形式,把「Z」顛倒個兒,填充起來更方便:

function ans=ZvarElem(n)eye(n);ans(:,[1 end])=1;ans(ans|0)=[flip(1:n) n+1:2*n-2 flip(2*n-1:3*n-2)];rot90(ans,-1);end

函數體第3條語句,「ans|0」作用相當於logical函數,把0-1陣換成邏輯索引,等式右側是Z所需元素填充順序,調用示例:

>> ZvarElem(5)ans = 1 2 3 4 5 0 0 0 6 0 0 0 7 0 0 0 8 0 0 0 9 10 11 12 13>> ZvarElem(4)ans = 1 2 3 4 0 0 5 0 0 6 0 0 7 8 9 10

總結:次第加1的問題是寫稿子時臨時想出來的,代碼ZvarElem純屬現編,雖然現炒現賣可能考慮不周、代碼勢必存在優化空間,不過第3行還算有料,1行之內出現2種常用的矩陣索引控制形式:左側邏輯索引和右側絕對索引。二者的意思可以用個比喻加深理解,一列隊伍a(維度1×n)點名,教官想讓自左至右第2、4、6...號學員向前一步出列,此時可以直接點編號名:

>> a(2:2:end)

或者提供一個條件(被2整除),讓學員先報數,再讓從第2個人開始,隔一個出列:

>> a(~mod(a,2))

邏輯索引控制數據的方式有時具有無可比擬的優勢,在簡單遞增序列或許還和「點名式」絕對索引難分軒轅,有些問題,比如在隨機數序列中,選擇偶數元素(不是偶數索引),邏輯索引就有較大的優勢:

>> a=randi(10,1,12)a = 9 10 2 10 7 1 3 6 10 10 2 10>> a(~mod(a,2))ans =10 2 10 6 10 10 2 10

那是不是可以下結論說邏輯索引一定優於直接索引?未必!比如構造一些規律性很強的擴展,先做點名式精準索引控制,再統一映射至序列的做法更簡單,比如給定1×4隨機整數序列b:

>> b=randi(10,1,4)b = 2 5 10 8

要求讓序列b以波浪起伏式擴展,也就是[2 5 10 8 10 5 2 5 10...]擴展3次,此時不大方便用邏輯索引構造,注意到一個循環節是2*numel(b)-1=7個元素,那麼先控制索引擴展,然後統一用b映射就比較好:

>> min(1:7,7:-1:1)ans = 1 2 3 4 3 2 1>> repmat(b(ans(1:end-1)),1,3)ans = 2 5 10 8 10 5 2 5 10 8 10 5 2 5 10 8 10 5

熟練的話repmat能複製元素,也同樣可以複製索引,這個問題中,repmat能里能外:

>> min(1:7,7:-1:1);>> b(repmat(ans(1:end-1),1,3))ans = 2 5 10 8 10 5 2 5 10 8 10 5 2 5 10 8 10 5

邏輯開關式索引和精準點名式的絕對索引,選擇依問題特徵而定,沒有定式;此外,邏輯索引和絕對索引又緊密聯繫,並且可通過一定方式互換,比如:

>> a=1:11a = 1 2 3 4 5 6 7 8 9 10 11>> b=mod(a,2)b = 1 0 1 0 1 0 1 0 1 0 1>> find(b)ans = 1 3 5 7 9 11>> a(1:2:end)ans = 1 3 5 7 9 11

除了前面所說的構造序列賦值,還可通過稀疏矩陣命令sparse做索引序列的精準構造:

function ans=ZvarElem(n)spdiags(sparse([1:n,n-1:-1:2,1:n],1:3*n-2,1:1:3*n-2));end

精準構造是因sparse的特殊性,這是真正的點名式元素操作,例如:

>> a=magic(6)a = 35 1 6 26 19 24 3 32 7 21 23 25 31 9 2 22 27 20 8 28 33 17 10 15 30 5 34 12 14 16 4 36 29 13 18 11>> a(logical(sparse([1 3],[2 4],1,size(a,1),size(a,2))))ans = 1 22>> a([1 3],[2 4])ans = 1 26 9 22

加了sparse,索引第(1,2)和(3,4)這2個元素,直接用索引,則變成第(1,2)、(1,4)、(2,3)和(2,4)這4個元素。

Problem 43682. Pairwise column flip

嘗試了元素的索引,下面討論整列換位操作。

【鏈接】:ww2.mathworks.cn/matlab

【題目】:Given matrix M_in, flip every pair of columns. So if M_in is

[1 2 3 4; 1 2 3 4]

then M_out is

[2 1 4 3 2 1 4 3]

Note: if M_in has odd number of columns, the last column should remain unchanged. So if M_in is

[17 24 1 8 15 23 5 7 14 16 4 6 13 20 22 10 12 19 21 3 11 18 25 2 9]

then M_out is

[24 17 8 1 15 5 23 14 7 16 6 4 20 13 22 12 10 21 19 3 18 11 2 25 9]

【分析】:兩兩換列操作,但注意當列數為2n的形式,則兩兩互換的對位列恰好相等,但列數為2n+1的形式,要保留最後一列原位置不變。for循環的代碼如下:

function x = flip_columns(x) for k = 2:2:size(x,2) x(:,k-1:k) = x(:,[k k-1]); endend

輸入基礎上直接換列,返回輸出矩陣,奇偶不同的列數在使用for循環時其實不受影響,因為循環節的結束用步長值2做彈性控制,總列數為奇數時末尾天然不能到達。注意看循環體內那句代碼,意味著換列本質上是在互換列索引號。fliplr、flip命令也可達到同樣效果:

function x = flip_columns(x) for i=1:2:length(x)-1 x(:,i:i+1)=fliplr(x(:,i:i+1)); endend

前兩種循環體一採用偶數列索引,一採用奇數列索引-1,效果相同,再看遍歷全部列號的循環寫法:

function y = flip_columns(x) b = size(x,2); for k = 1:b y(:,k) = x(:,min(k-(-1)^k,b)); endend

循環遍歷的是全部列索引編號,把列號更換的判斷放進循環體,自行構造以-1的乘方為基礎的邏輯索引進行控制,這又是用min做對位比較,以k=1:5為例:

k=1,min(1-(-1)^1,5)=2,故:y(:,1)=x(:,2);

k=2,min(2-(-1)^2,5)=1,故:y(:,2)=x(:,1);

...

k=4,min(4-(-1)^4,5)=3,故:y(:,4)=x(:,3);

k=5,min(5-(-1)^5,5)=5,故:y(:,5)=x(:,5)。

由此例:min函數對位比較,交換控制序列和列號最大值,其目的是防止最後的列編號數值溢出——精妙的構思!這段代碼出自Cody高手yurenchu的手筆,的確不同凡響!

再看看另一種完全不同的構思方式:

function ans = flip_columns(x)1:size(x,2);reshape([[ans(2:2:end) zeros(1,mod(ans(end),2))];ans(1:2:end)],1,[]);x(:,ans(ans>0));end

這棄用循環,將原列索引分奇偶排成兩行重構,如奇偶列數不同補0湊齊,索引引用時,用選擇大於0的邏輯索引重排。

下面又是yurenchu代碼,他的演算法水平令人欽佩,這是用線性代數中,矩陣乘法控制列編號交換的方法,我認為這種方法雖然都在線性代數中學過,但在具體問題中實現構造的意圖卻並不容易:

function ans = flip_columns(x) e = eye(size(x,2)); [x fliplr(x)]*kron(e, str2num([0 1; 1 0]))*vertcat(e,0*e);end

以x=magic(5)為例:第1行構造同維單位陣。

第2行是關鍵,裡面構造了3個矩陣的連乘形式,第1個是用輸入和輸入自身的翻轉陣,構造5×10維度擴維形式:

>> [x fliplr(x)]ans = 17 24 1 8 15 15 8 1 24 17 23 5 7 14 16 16 14 7 5 23 4 6 13 20 22 22 20 13 6 4 10 12 19 21 3 3 21 19 12 10 11 18 25 2 9 9 2 25 18 11

第2個是列交換索引號矩陣的擴維:

>> kron(e, str2num([0 1; 1 0]))ans = 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0

得到10×10列號更換序列,如果讓第1和第2兩個矩陣相乘,發現兩個列元素正好被兩兩互換,這是線性代數的基礎知識,不再贅述,可得到的結果是擴大的5×10矩陣:

>> [x fliplr(x)]*kron(e, str2num([0 1; 1 0]))ans = 24 17 8 1 15 15 1 8 17 24 5 23 14 7 16 16 7 14 23 5 6 4 20 13 22 22 13 20 4 6 12 10 21 19 3 3 19 21 10 12 18 11 2 25 9 9 25 2 11 18

還得再給它縮回原來的維度,做到這一步,可選擇的辦法比較多,容易想到的是x(1:end/2),但yurenchu不愛走尋常路——既然選擇了矩陣乘法,索性一條路走到黑,再構造10×5矩陣降維!於是有了最後一個用於縮維構造的矩陣vertcat(e,0*e)。

>> vertcat(e,0*e)ans = 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

同時注意同維0矩陣的寫法沒用zeros,而是0*e。

yurenchu好像並不滿意前述演算法,發現先擴維,再縮維雖然可行,但還能不能在做矩陣連乘時不擴維呢?他想到用blkdiag構造的方案:

function ans = flip_columns(x) b = size(x,2); x*blkdiag(kron(eye(floor(b/2)), str2num([0 1; 1 0])), ones(mod(b,2)))end

先說說blkdiag,這是MATLAB基本工具箱命令:

>> which blkdiagC:Program FilesMATLABR2018a oolboxmatlabelmatlkdiag.m

把小的矩陣塊,沿主對角線方向集成的命令,比如:

>> blkdiag(magic(4),randi(10,3),nan(2))ans = 16 2 3 13 0 0 0 0 0 5 11 10 8 0 0 0 0 0 9 7 6 12 0 0 0 0 0 4 14 15 1 0 0 0 0 0 0 0 0 0 5 6 3 0 0 0 0 0 0 4 6 8 0 0 0 0 0 0 9 10 8 0 0 0 0 0 0 0 0 0 NaN NaN 0 0 0 0 0 0 0 NaN NaN

注意到,小矩陣塊維度可以不同。下面說說yurenchu代碼的意思:

第1行獲取輸入矩陣列總數;

第2行有講究,首先blkdiag內部的第1個小矩陣塊,也就是kron(...),用於構造小矩陣塊[0 1;1 0]的主對角線擴維,顯然,子矩陣內部的子矩陣「[0 1;1 0]」用於互換某兩個同行元素,用法同前;blkdiag命令第2輸入參數又補了個ones(mod(b,2)),用於預防因輸入不同,可能出現、也可能不出現最後一列的意外情況,如果是偶數,例如b=8,則mod(b,2)=0,而0維的ones全1陣,其實是空矩陣,等於沒補,如果是奇數,這個位置出現單獨的元素1,相當於最後一列不發生變化。以magic(8)為例,blkdiag的執行結果是:

>> blkdiag(kron(eye(floor(b/2)), str2num([0 1; 1 0])), ones(mod(b,2)))ans = 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0

剩下的就是兩個同維度矩陣相乘,不再贅述。

這個簡約而不簡單的代碼方案和後續的2個優化方案,使得yurenchu在我內心的歷史地位從高手正式跳升到大神,因為我對真正具有矩陣思維的人,讚譽從來不加掩飾。難得處在於:yurenchu脫離了學習線性代數的一般低級趣味,真正用矩陣思維思考和解決問題,「矩陣化」地思考數據和問題的處理。絕非拿來考試或者炫耀考試成績的一般吃瓜群眾。也許還覺得不過癮(大神通常都是這樣),又提出了採用遞歸演算法換列的操作:

function ans = flip_columns(ans) if size(ans,2)>1 [ ans(:,str2num([2 1])), flip_columns(ans(:,3:end)) ]; endend

yurenchu也是整個Cody幾位大神中,遞歸玩兒得最溜,也最興高采烈的。遞歸流程核心思想逆序回溯比較好理解,不再對上述代碼詳細介紹了。

這個問題最開始提到for循環中,利用min(k-(-1)^k,b)來控制換列索引編號,那麼,能不能去掉外層for循環呢?答案是肯定的,這個思路由劉鵬首先提出,yurenchu在其基礎上又縮小了一個size,兩段代碼在基本思路相同的情況下,細微處的控制方法還略有差異,出於學習提高的出發點,不妨都提一下:

先看劉鵬的方案:

function ans = flip_columns(x) 1:size(x,2); x(:,min(ans-(-1).^mod(ans,2),max(ans)));end

這個思路和for循環的一樣,但點乘方運算讓換列操作矢量化了,索引位控制做得非常簡潔流暢。min裡面的第2個參數max(...)是最大列數,同樣用於預防第1參數結果大於最高列的總數。再看yurenchu的優化方案:

function ans = flip_columns(x) b = 1:size(x,2); x(:,min(b-(-1).^b,length(b)))end

說是優化略勉強,兩個方案最大區別就在點乘方上,前者是mod操作出來的邏輯索引[1 0 1 0 ...];而yurenchu後面的方案中,發現其實不用做這種判斷,因為(-1)^0是1,(-1)^2還是1,所以直接用b做冪,結果是一樣的。

也許到這兒,你會以為已經是最優代碼方案,簡無可簡了。

呵呵...

還有一個重要的武器沒有拿出來,那就是正則表達式。

先是J.S.Kowontan的傳統做法:

function ans = flip_columns(x) x(:,str2num(regexprep(num2str(1:size(x,2)),d+s+d+,${flip($0)} )));end

代碼用正則替換regexprep命令,對矩陣列序號搜索和動態正則替換,後面兩個字元串, d+s+d+用於搜索符合:「一串數字帶空格再接段數字」的模式,加號的意思是數字+空格+數字的模式,每樣都必須搜索到。

>> num2str(1:size(magic(5),2))ans =1 2 3 4 5

用regexp,也就是正則搜索命令看看正則語法的搜索結果:

>> regexp(ans,d+s+d+,match)ans = 1×2 cell 數組 {1 2} {3 4}

對前面列號序列1:5,按前述:「數字-空格-數字」模式,共搜索到:1和2、3和4這2組結果,再由第2參數動態替換正則表達式:「${flip($0)} 」對當前搜索結果($0),以fliplr命令翻轉,索引號更換是在字元串中一次得到解決,優勢在於根本不判定是否存在最後遺留的奇數列。

下面是問題中彬彬提出的壓軸代碼:

function ans = flip_columns(x)x(:,regexprep(char(1:length(x)),.{2},${flip($0)}))end

思路和前例相同,沿用正則替換,替換表達式也都一樣,用fliplr或者flip翻轉。但這個代碼好處在於,使用char函數,把序列轉換成ascii碼值:

>> regexprep(char(1:length(magic(5))),.{2},${flip($0)})ans =

輸出結果可能有點抽象,因為結果似乎就5個空格,錯了!這不是空格,空格ascii碼值是32,而這裡的這堆看似是空格的東西,其實是搜索到任意兩個ascii碼值就互換,也就是說,正則搜索表達式:.{2}包含兩部分,第1部分——那個不起眼的句點兒,代表在表達式中搜索到任意1個數字、大小寫字母、特殊字元...等等一切可以搜索到的字元都算滿足條件,而前面一篇提到了char可以天然無空格轉換1:5為char字元,所以連續匹配到兩個ascii碼值都是列號。最妙的是:列索引可以是ascii碼值!所以介紹了邏輯索引和絕對索引後,我們又看到了點名式絕對索引的另一種特殊表達形式:ascii碼值。

有人可能會說:「可這個列索引表述形式看起來就是一堆空格,我不認識啊」

呵呵...你不認識沒關係,MATLAB認識就行,如果不放心,我們讓這堆貌似空格的字元串,重新變成索引序列:

>> regexprep(char(1:length(magic(5))),.{2},${flip($0)})ans = >> +ansans = 2 1 4 3 5

如果這樣能讓你好受點兒的話...

小結

以上是索引系列文章的第1篇,其實已經短兵相接、智慧火花四射了,大體通過幾個例子的多解,應該能覺察出索引使用的基本脈絡,而且也能有所收穫。點名式絕對索引、邏輯索引、基本函數操作綜合控制索引序列的編排,然後等維度賦值。邏輯條件相當於此處的索引開關,通過設定特殊條件決定打開(引用),還是不打開(不引用)該位置的元素,邏輯索引是0-1序列,類型是Bolean值,可以用logical命令和其他「與(|)」、「或(&)」、「非(~)」以及許多其他函數命令例如ismember等,把普通0-1陣換成邏輯索引;點名式的絕對索引則不講條件,直接構造獲得,且絕對索引不能有0,只能是正整數序列。


推薦閱讀:

CodyNote004:簡單解碼環加密問題
CodyNote012:再談動態長度子序列(Part.1)
助力國賽 | 第3彈 規劃問題(MATLAB版)
MATLAB mpctoolbox學習筆記
matlab之快速傅里葉變換(fft)

TAG:MATLAB |