C語言如何打印一個數組排列組合?

問題來自《Linux C一站式編程》,是個挺有意思的題目。算法

二、定義一個數組,編程打印它的全排列。好比定義:編程

#define N 3 
int a[N] = { 1, 2, 3 };

則運行結果是:數組

$ ./a.out
1 2 3 
1 3 2 
2 1 3 
2 3 1 
3 2 1 
3 1 2

程序的主要思路是:函數

  1. 把第1個數換到最前面來(原本就在最前面),準備打印1xx,再對後兩個數2和3作全排列。
  2. 把第2個數換到最前面來,準備打印2xx,再對後兩個數1和3作全排列。
  3. 把第3個數換到最前面來,準備打印3xx,再對後兩個數1和2作全排列。

可見這是一個遞歸的過程,把對整個序列作全排列的問題歸結爲對它的子序列作全排列的問題,注意我沒有描述Base Case怎麼處理,你須要本身想。
你的程序要具備通用性,若是改變了N和數組a的定義(好比改爲4個數的數組),其它代碼不須要修改就能夠作4個數的全排列(共24種排列)。code

完成了上述要求以後再考慮第二個問題:若是再定義一個常量M表示從N個數中取幾個數作排列(N==M時表示全排列),原來的程序應該怎麼改?遞歸

最後再考慮第三個問題:若是要求從N個數中取M個數作組合而不是作排列,就不能用原來的遞歸過程了,想一想組合的遞歸過程應該怎麼描述,編程實現它。io

不考慮數組元素相同的狀況,咱們能夠按照題目提供的思路寫出以下代碼:變量

#include <stdio.h>
#define N 3

int a[N];

void perm(int); /*求數組的全排列 */
void print();
void swap(int, int);

int main(){
    int i;
    for(i = 0; i < N; ++i){
        a[i] = i + 1;
    }
    perm(0);
}

void perm(int offset){
    int i, temp;
    if(offset == N-1){  // BaseCase
        print();
        return;
    }else{
        for(i = offset;i < N; ++i){
            swap(i, offset);//交換前綴
            perm(offset + 1);//遞歸
            swap(i, offset);//將前綴換回來,繼續作前一次排列
        }
    }
}

void print(){
    int i;
    for(i = 0; i < N; ++i)
        printf(" %d ",a[i]);
    printf("\n");
}   

void swap(int i, int offset){
    int temp;
    temp = a[offset];
    a[offset] = a[i];
    a[i] = temp;
}

若是日常遞歸寫的很少的話,這段代碼仍是很容易寫錯的(沒錯,我就是在說我本身)。
在perm函數遞歸調用本身以後記得把元素位置交換回去,保證回溯時條件一致。程序

而後看第二個問題,這是更加通常的排列。仔細觀察上面的代碼,把特殊推導到通常,主要修改以下(用註釋符標出):註釋

#include <stdio.h>
#define N 4
#define M 2   // 取出M個元素進行排列,默認M<=N

void print(){  
    int i;  
    for(i = 0; i < M; ++i)  // N->M,打印a裏前M個排列元素
        printf(" %d ",a[i]);  
    printf("\n");  
}

void perm(int offset){  
    int i;  
    if(offset == M){  // N->M,排列到M個數時遞歸到達BaseCase
        print();  
        return;  
    }else{  
        for(i = offset;i < N; ++i){  
            swap(i, offset);
            perm(offset + 1);
            swap(i, offset); 
        }  
    }  
}

再來看組合,一樣用要求用遞歸解決,若是相關概念沒有搞得很清楚,加上上面寫的排列的代碼,很容易寫不出來(沒錯,說的仍是我),然而代碼其實很簡單,不過遞歸確實更加複雜。

void comb(int n, int m)
{
    int i;
    if (m == 0) {
        print();
        return;
    } else {
        for (int i = n-1; i >= 0; --i)
        {
            b[m-1] = a[i];
            comb(i, m-1);
        }
    }
}

複雜之處在於,排列都是(n->n-1)這樣的遞歸,然而組合這裏是(n->i,m->m-1)這樣非規律的遞歸調用,由於i是個變量。
可是組合的算法描述很簡單,假設有一個兩兩元素互不相同的N長數組a,從數組尾端依次取M個數(b1,b2,···,bm)成爲一個組合,而且知足條件:若是i>j,那麼bi在a中的下標必定小於bj。

簡單來講,就是從後往前取數組裏的M個數,只有保持這樣的偏序關係才能保證不會重複組合。

相關文章
相關標籤/搜索