標籤:

CodyNote002:富有彈性的數組長度(Part.1)

CodyNote002:富有彈性的數組長度(Part.1)

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

馬良,祁彬彬


序言

彈性維度在MATLAB的矩陣操作中是個挺重要的知識。體現了在控制矩陣構造時,何時應「放鬆」,何時又應「收緊」的理解程度,需要更進一步理解函數命令、邏輯數組、特殊矩陣構造、索引等諸多MATLAB特徵類型的操作。例如當構造維度n>=3的0-1矩陣,使其中的元素1看起來像是大寫字母「D」:

n=3ans = 1 1 0 1 0 1 1 1 0n=5ans = 1 1 1 1 0 1 0 0 0 1 1 0 0 0 1 1 0 0 0 1 1 1 1 1 0

這是很簡單的問題,通常寫法是先構造全0或全1矩陣再對特定位置重新賦值,例如構造全1陣:

function ans = your_fcn_name(x) ones(x); ans([1 x],x) = 0; ans(2:x-1,2:x-1) = 0;end

再如先構造全0陣:

function ans = your_fcn_name(x) zeros(x); ans([1,end],:)=1; ans(:,[1,end])=1; ans([1,end],end)=0;end

暫時不談合理性,畢竟這種不先聲明變數維度,隨便擴展的做法並非推薦,但先行構造全0或者全1陣的步驟在MATLAB中的確是可以省略的:

function ans = your_fcn_name(x) ans([1 x],1:x-1)=1; ans(2:x-1,[1 x])=1;end

也就是說,MATLAB中的矩陣,維度無需事先聲明,如果賦值行或者列數超過原有維度則自行補足,未賦值部分以0填充。如果再綜合前述邏輯數組、特殊矩陣構造等技巧,在MATLAB中構造一個特定長度、形狀、元素數值的矩陣,有時候其簡潔性會讓相當多的所謂「MATLAB高手」都感到難以置信。本篇打算通過幾個例子,介紹一下在MATLAB中,矩陣長度的「彈性」。

元素的分組和索引定位

Pro44491. Shuffle

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

【原題】:Shuffle a vector by breaking it up to segments of n elements, and rearranging them in a reversed order.

For example, the vector:

vector = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

should be shffuled by segments of n=3 like so:

cetvor = [8,9,10, 5,6,7, 2,3,4, 1]

The shuffled vector should have the same dimensions as the original one.

【解釋】:指定每組元素的數量n,把原數組vector按小分組正序,大分組逆序排列。例如:每組元素n=2時,排成[9, 10, 7, 8, 5, 6, 3, 4, 1, 2];當每組元素n=3時,排成題目中的樣子,即:[8, 9, 10, 5, 6, 7, 2, 3, 4, 1],其餘類推。方便下面的分析起見,把test suite放在前面:

Test

Code Input

1

%%filetext = fileread(shuffle.m);assert(isempty(strfind(filetext, regexp)),regexp hacks are forbidden)

2

%%

v = [1, 2, 3, 4, 5, 6, 7, 8];n = 1;w_correct = 8 : -1 : 1;assert(isequal(shuffle(v, n), w_correct))

3

%%v = [1; 2; 3; 4; 5; 6; 7; 8];n = 2;w_correct = [7;8; 5;6; 3;4; 1;2];assert(isequal(shuffle(v, n), w_correct))

4

%%v = [1, 2, 3, 4, 5, 6, 7, 8];n = 3;w_correct = [6,7,8, 3,4,5, 1,2];assert(isequal(shuffle(v, n), w_correct))

5

%%v = [1; 2; 3; 4; 5; 6; 7; 8];n = 4;w_correct = [5;6;7;8; 1;2;3;4];assert(isequal(shuffle(v, n), w_correct))

6

%%v = [1, 2, 3, 4, 5, 6, 7, 8];n = 5;w_correct = [4,5,6,7,8, 1,2,3];assert(isequal(shuffle(v, n), w_correct))

7

%%v = [1; 2; 3; 4; 5; 6; 7; 8];n = 6;w_correct = [3;4;5;6;7;8; 1;2];assert(isequal(shuffle(v, n), w_correct))

8

%%v = [1, 2, 3, 4, 5, 6, 7, 8];n = 7;w_correct = [2,3,4,5,6,7,8, 1];assert(isequal(shuffle(v, n), w_correct))

9

%%v = [1; 2; 3; 4; 5; 6; 7; 8];n = 8;w_correct = [1;2;3;4;5;6;7;8];assert(isequal(shuffle(v, n), w_correct))

10

%%v = [1, 2, 3, 4, 5, 6, 7, 8];n = 9;w_correct = [1,2,3,4,5,6,7,8];assert(isequal(shuffle(v, n), w_correct))

【分析】:數組按索引編號倒序抽取分組,組內正序排號,最後再按輸入數組的維度維度重新排列。從給出的10個測試代碼來看,如下問題在寫代碼時也許需要考慮:

(1). 不用字元串和正則表達式cheat,這事兒最好不做,沒意思;

(2). 字元串選擇1:8整數,不過Test有1×n,有n×1,代碼應兼顧可能出現行或列數組的情況;

(3). 最後一個例子是n=9,意味著超過原數組向量的長度值,可能需要加個判斷,或者...不加呢?

題目要求使用其指定的兩個子函數,按框架寫代碼,我覺得沒必要,去掉了,但後面有些示例代碼中可能多數存在這類函數調用子函數的形式,特別說明一下。

元素逐點循環

首先是個元素逐點循環的代碼,寫得非常啰嗦:

function cetvor = shuffle(vector, n) % the function Shuffle a vector by breaking it up to segments of n elements, %and rearranging them in a reversed order. cetvor=[]; %Setting a variable sum=0; %Setting a variable [row,col]=size(vector); % the size of the vector sizeVector=length(vector); % the length of vector for i=1: fix(sizeVector/n) % Performs the loop in the number of times n enters a vector [v, w] = pop(vector, n); % Call to function [cetvor, sizeNew] = push(cetvor, w); % Call to function sum=sum+sizeNew; vector=v; % the vector without the rem end if (sizeVector/n) ~= fix(sizeVector/n) % If the division does not give an integer a=sizeVector- sum; % the number of numbers that left from the begining of the original vector if row==1 % if the vector given in one row cetvor=[cetvor, vector]; % add vector to cetvor to the right else col==1 % if the vector given in one col cetvor=[cetvor;vector]; % add vector to cetvor down end end end% You must call the following functions from the shuffle() function% (copy-paste your solutions)function [v, n] = push(v, x) [row,col]=size(v); [rowX,colX]=size(x); a=length(v); vector=[]; if a==0 v=x; n= length(x); elseif row==1 if rowX==1 vector=[v,x]; else colX==1 vector=[v,x]; end v=vector; n= length(vector); else col==1 if rowX==1 vector=[v;x]; else colX==1 vector=[v;x]; end v=vector; n= length(v); end endfunction [v, w] = pop(v, n) w = []; a=length(v); [row,col]=size(v); if n~=0 if a<=n w=v; v=[]; else w=v((a-n+1):a); v=v(1:(a-n)); end endend

十來個if流程,多種for循環,MATLAB基本被當成C++、fortran等其他語言的翻譯器,用最簡單流程式控制制語句和幾個基礎函數,數組或矩陣元素選擇逐點處理計算,對分組和排序「直譯」,平鋪直敘又啰嗦冗長。此外非關鍵部分注釋過於詳細,子函數幾處關鍵語句注釋又莫名其妙消失。子程序push充斥大量if判斷,但真的全都必要嗎?

開宗明義,絕大多數問題里,不推薦這種MATLAB編程方式,原因是並沒有體現出MATLAB自身的語言特點,許多MATLAB中特有的函數命令潛力也都無從發揮。下面是有一定程度簡化的逐個元素處理代碼:

function [v] = shuffle(vector, n) w=[];t=fix(length(vector)/n); for i= 1:t [v,b]=pop(vector,n); w=[w,b]; vector=v; end v=push(w,vector);end function [x, w] = pop(v, n)a=length(v);x=v(1:a-n); %Delete the vectorv(1:a-n)=[];%Create a disabled vectorw=v;endfunction [v,n]=push(v,x) n=size(v,1); a=[v(:);x(:)]; if n==1 v=a; elseif n>1 v=a else v=x end n=length(v);end

子程序pop前3行對原輸入數組v按給定長度n截尾,獲取新數組w,主程序在w後部通過循環添加分組;子程序push通過if-elseif-end流程將不同維度形式輸入數組轉換成統一形式。

需要說明的是:三個function要放在同一M文件里,後兩個是子程序,在流程複雜,尤其是需要對某流程反覆調用的情況下,是值得推薦的。把大的問題分解成子問題,用子程序單獨解決,處理好和主程序間的輸入和輸出關係,後期維護更加方便。

邏輯索引

仔細分析前一節的逐個元素for循環+判斷的代碼,發現相當一部分代碼篇幅被用在維度統一上,也就是輸入為行數組或者列數組時,此外程序採用逐段循環添加小分組的傳統處理方式,能用邏輯索引優化。

function v = shuffle(vector, n)v=[]; for i = 1:n:length(vector) [vector,w] = pop(vector,n) [v,n1] = push(v,w) endendfunction [v, n] = push(v, x) n=size(v,1); %Consolidation of two vector vectors in one column a=[v(:);x(:)]; %Check whether the given vector is a row or column vector if n==1 v=a; elseif n>1 v=a; else v=x end n=length(v);endfunction [v, w] = pop(v, n) w = v(max(1,end-n+1):end); v = v(1:max(end-n,0));end

亮點在倒數第3行

w = v(max(1,end-n+1):end)

pop子程序中的w變數內部索引,採用max函數取得v向量彈性起點位置,這是MATLAB編程中常用,也值得學習的手法。在此處使用max命令的原因是保證運行Test 10時,n=9能正確通過。n=9時,分組元素數量起點為0,w將變成空向量出現錯誤。max命令此時即成為保險措施,保證分組索引數量至少保證以1為起點,因此隨著輸入指定的分組數量不同,子序列長度就具有了彈性。

這個思路仍具有很大優化空間,下面代碼藉助isrow判斷是否輸入為行向量、再用ismember獲取截尾部分的剩餘數組元素,看起來清爽得多。

function y = shuffle(x,n) y = []; while ~isempty(x) [x,z] = pop(x,n); [y,~] = push(y,z); endendfunction [v, n] = push(v, x) if isempty(v) v = x; elseif isrow(v) v = [v,x(:)]; else v = [v;x(:)]; end n = length(v);endfunction [v, w] = pop(v,n) L = length(v); ind = L-min(n,L)+1:L; w = v(ind); v = v(~ismember(1:L,ind));end

還有類似思路的代碼:

function cetvor = shuffle(vector, n)cetvor = [];while numel(vector) [vector,u] = pop(vector,n); cetvor = push(cetvor,u);end function [x, n] = push(v, x) if isrow(v) x = [v x(:)]; elseif iscolumn(v) x = [v;x(:)]; end n = numel(x); end function [v, w] = pop(v, n) numel(v); max(1,ans-n+1):ans; w = v(ans); v(ans) = []; endend

意思差不多,子程序push的if控制得有點太死:isrow和iscolumn有一個就行,不必elseif。

Arrayfun+setdiff獲得截尾彈性長度

根據前一節代碼分析,不同輸入數組的維度統一交給iscolumn,小分組逆序、大分組順序的步驟通過循環,在尾部逐次累積,可用arrayfun等效替換for循環。

function ans = shuffle(v, n)vn=v(:);numel(vn)-n+1:-n:1;cell2mat(arrayfun(@(i)vn(ans(i):(ans(i)+n-1)),1:numel(ans),un,0));ans((end+1):numel(vn))=setdiff(vn,ans);if iscolumn(v) ans;endend

整體思路與之前一致,語句「numel(vn)-n+1:-n:1」得到所有分組起點位置,依次向後推n-1個元素完成主體分組意圖,注意向量為1×8,分組為n=3時,上述語句得到結果是「8-3+1:-3:1=6:-3:1」,結果是[6, 3],再往後就不夠減了,所以不到1,截尾通過setdiff函數,在構造主體分組尾巴上擴維添加,但顯然,這種方法有其缺陷:在元素存在相同的時候,尤其截尾部分,setdiff就會出現錯誤,此時得先處理索引位置,再對索引批量賦值。選擇setdiff和max/min函數用靜態值比較得到索引,都是獲得彈性長度數組的常用辦法。

最後,輸入變數維度的不同仍然棘手,貌似難以避免用1次if。和之前不同的是:既然不能提前分辨,乾脆就先在第1行通過vn=v(:)變成列,完成主要操作後再按要求修改為符合要求的維度。

沒有維度判斷的判斷及神奇的數組翻轉

彬彬提交的是目前為止的leading solution:

function v = shuffle(v, n)c = repelem(n,ceil(length(v)/n));c(end) = length(v) - sum(c(1:end-1));v(:)=cell2mat(cellfun(@flip,mat2cell(flip(v(:)), c),uni,0));end

這屬於典型的矢量化代碼,以v=1:8; n=3為例逐行分析,第1行運行結果:

>> v=1:8; n=3;>> c = repelem(n,ceil(length(v)/n))c = 3 3 3

意思是對原數組按repelem命令做擴維分組,分成numel(c)=3份,但原數組大小是8,repelem命令執行之後元素總數卻變成sum(c)=9,多了1個,接下句代碼:

>> c(end) = length(v) - sum(c(1:end-1))c = 3 3 2

顯然:分垛時,其最後1個元素的數量按照輸入向量v的元素總數,減去c的前兩個滿員分組,看還剩幾個,也就是截尾組的元素個數就確定了,本例中是2個。

第3句是分組的關鍵,為便於理解,把這句代碼從裡到外逐步分解執行,內層:

>> t1=flip(v(:))t1 = 8 7 6 5 4 3 2 1

從test suite得知程序處理的不是1×8就是8×1,數組,因此統一用v(:)變成8×1,flip函數等效於flipud,把索引序號上下翻轉;因為前一步已經確定了分組形式(變數c),因此可把統一維度形式後的向量按前一步的c分組,用mat2cell命令分別裝入cell數組:

>> mat2cell(flip(v(:)), c)ans = 3×1 cell 數組 {3×1 double} {3×1 double} {2×1 double}

每個cell中的數組是按照c的分組數量,把原數組v重新打成了小包裝,而且順序和原數組相同,可用逗號表達式打開看看:

>> [k1,k2,k3]=ans{:}k1 = 8 7 6k2 = 5 4 3k3 = 2 1

順便,關於逗號表達式的其他內容介紹有興趣可以看看我和彬彬的書,以後的筆記中也會常常用到。

此時運行結果和y_correct答案已經很類似,但順序不對,分組內部編號要正向,所以還得再繼續做第2次翻轉,但這一次,每個分組元素都還在cell里存著,要想實現翻轉操作,必須打開每個cell進到內部,於是想到了cellfun函數,調用「@flip」句柄對每個cell的元素執行翻轉操作:

>> cellfun(@flip,mat2cell(flip(v(:)), c),uni,0)ans = 3×1 cell 數組 {3×1 double} {3×1 double} {2×1 double}>> [k1,k2,k3]=ans{:}k1 = 6 7 8k2 = 3 4 5k3 = 1 2

由於元素還都在各個cell數組內部,最後想辦法提取出來做重排列,因此順勢接cell2mat,這個函數經常和arrayfun、cellfun兩個命令組合,把cell內部的數組以1×n形式重排:

>> cell2mat(ans)ans = 6 7 8 3 4 5 1 2

下面是另一個關鍵部分:前面例子輸入為列向量,如給定輸入數組v是行向量怎麼辦?為什麼彬彬的代碼中沒看到if判斷?

注意左端賦值部分,還有一個冒號操作符,這就是整個3句代碼的精妙處!不妨舉個例子:

>> vs=zeros(1,10)vs = 0 0 0 0 0 0 0 0 0 0>> vr=randi(10,10,1)vr = 2 10 10 5 9 2 5 10 8 10>> vs(:)=vrvs = 2 10 10 5 9 2 5 10 8 10

有上述運行結果發現:vs是1×10行全零向量,vr是10×1隨機整數列向量,如果用「vs=vr」直接賦值,vs將繼承vr的數據和維度,成為10×1數據,源數據也就是0向量和新賦值後的vs就完全沒關係了,而用「vs(:)=vr」賦值,雖然值變了,但卻保留了它原來的維度條件,也就是1×10。

所以根本不需要有什麼維度判斷,加個冒號就行。

(未完待續)

推薦閱讀:

基於 SPM 對 FMRI 進行數據預處理
兩個實用的稀疏矩陣函數: nnz和nonzeros
MATLAB 車道線識別
ROS技術點滴 —— Matlab中的ROS
MATLAB筆記(1)基礎

TAG:MATLAB |